Skip to content
This repository was archived by the owner on Jul 29, 2026. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/kimi-web/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import type { SwarmMember } from './composables/swarmGroups';
import ServerAuthDialog from './components/ServerAuthDialog.vue';
import { initServerAuth, onAuthRequired } from './api/daemon/serverAuth';
import type { AppConfig, ThinkingLevel } from './api/types';
import { commitLevel, effectiveThinkingLevel, segmentsFor } from './lib/modelThinking';
import { commitLevel, defaultThinkingLevelFor, effectiveThinkingLevel, segmentsFor } from './lib/modelThinking';
import { stripSkillPrefix } from './lib/slashCommands';
import Button from './components/ui/Button.vue';
import IconButton from './components/ui/IconButton.vue';
Expand Down Expand Up @@ -119,7 +119,7 @@ function nextThinkingLevel(current: ThinkingLevel | undefined): ThinkingLevel {
// No stored preference means the model default is in effect — cycle from
// there; a level the model doesn't declare (indexOf → -1) starts the cycle
// at the first segment.
const idx = segs.indexOf(effectiveThinkingLevel(model, current));
const idx = segs.indexOf(effectiveThinkingLevel(model, current ?? defaultThinkingLevelFor(model)));
const next = segs[(idx + 1) % segs.length] ?? segs[0] ?? 'off';
return commitLevel(model, next);
}
Expand All @@ -129,7 +129,7 @@ function nextThinkingLevel(current: ThinkingLevel | undefined): ThinkingLevel {
// will actually run, not a blank.
const statusPanelThinking = computed<ThinkingLevel>(() => {
const model = client.models.value.find((m) => m.id === client.status.value.modelId);
return effectiveThinkingLevel(model, client.thinking.value);
return effectiveThinkingLevel(model, client.thinking.value ?? defaultThinkingLevelFor(model));
});

// First-run onboarding (language + welcome greeting). Shown until the user
Expand Down
15 changes: 15 additions & 0 deletions apps/kimi-web/src/api/daemon/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,21 @@ export class DaemonKimiWebApi implements KimiWebApi {
};
}

/** POST /sessions/{id}/fs:mkdir — create a directory. FS_ALREADY_EXISTS (40919)
* is treated as success (the directory is ready to use). */
async makeDirectory(
sessionId: string,
input: { path: string },
): Promise<{ made: boolean; path: string }> {
const data = await this.http.post<{ made: boolean; path: string }>(
`/sessions/${encodeURIComponent(sessionId)}/fs:mkdir`,
{ path: input.path },
{ allowCodes: [40919] }, // FS_ALREADY_EXISTS
);
return { made: data?.made ?? false, path: input.path };
}


async readFile(
sessionId: string,
input: { path: string; offset?: number; length?: number },
Expand Down
2 changes: 2 additions & 0 deletions apps/kimi-web/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,8 @@ export interface KimiWebApi {
getTerminal(sessionId: string, terminalId: string): Promise<AppTerminal>;
closeTerminal(sessionId: string, terminalId: string): Promise<{ closed: true }>;
listDirectory(sessionId: string, input: { path?: string; depth?: number; includeGitStatus?: boolean }): Promise<{ items: FsEntry[]; childrenByPath?: Record<string, FsEntry[]>; truncated: boolean }>;
/** Create a directory. Returns FS_ALREADY_EXISTS (40919) when the path already exists — callers should treat that as success. */
makeDirectory(sessionId: string, input: { path: string }): Promise<{ made: boolean; path: string }>;
readFile(sessionId: string, input: { path: string; offset?: number; length?: number }): Promise<{ path: string; content: string; encoding: 'utf-8' | 'base64'; size: number; truncated: boolean; etag: string; mime: string; languageId?: string; lineCount?: number; isBinary: boolean }>;
searchFiles(sessionId: string, input: { query: string; limit?: number }): Promise<{ items: Array<{ path: string; name: string; kind: FsKind; score: number; matchPositions: number[] }>; truncated: boolean }>;
grepFiles(sessionId: string, input: { pattern: string; regex?: boolean; caseSensitive?: boolean }): Promise<{ files: Array<{ path: string; matches: Array<{ line: number; col: number; text: string; before: string[]; after: string[] }> }>; filesScanned: number; truncated: boolean; elapsedMs: number }>;
Expand Down
3 changes: 2 additions & 1 deletion apps/kimi-web/src/components/chat/Composer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { FileItem } from './MentionMenu.vue';
import type { ActivationBadges, ConversationStatus, PermissionMode, QueuedPromptView } from '../../types';
import type { AppGoal, AppModel, AppSkill, ThinkingLevel } from '../../api/types';
import {
defaultThinkingLevelFor,
commitLevel,
effectiveThinkingLevel,
effortLabel,
Expand Down Expand Up @@ -617,7 +618,7 @@ const thinkingSegments = computed(() => segmentsFor(currentModel.value));
// the model default, which is what the daemon will resolve for the prompt. A
// level the model doesn't declare highlights no segment but still shows in the
// suffix.
const thinkingLevel = computed(() => effectiveThinkingLevel(currentModel.value, props.thinking));
const thinkingLevel = computed(() => effectiveThinkingLevel(currentModel.value, props.thinking ?? defaultThinkingLevelFor(currentModel.value)));
const activeThinkingSegment = computed(() => {
const segs = thinkingSegments.value;
return segs.includes(thinkingLevel.value) ? thinkingLevel.value : '';
Expand Down
27 changes: 22 additions & 5 deletions apps/kimi-web/src/i18n/locales/en/study.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,21 +71,38 @@ export default {
outlineDesigning: 'Kimi is designing the outline from your material and goal…',
outlineCounts: '{chapters} chapters · {pages} pages · {quizzes} quizzes',
outlineReviseTitle: 'Ask for a change',
outlineRevisePlaceholder: 'e.g. “Add a chapter on boundary cases”…',
outlineRevisePlaceholder: 'Describe the outline change in one sentence…',
outlineReviseExample: 'For example: assume no prior knowledge; lead with examples; reduce to 6 lessons; move risk analysis first.',
outlineReviseAction: 'Update outline',
outlineReviseSent: 'Change requested — a new outline revision will appear here once it passes review.',
outlineReviseError: 'The outline could not be updated right now. Try again in a moment.',
outlineReviseWaiting: 'Kimi is revising and checking the outline. The current version stays in place until the new one is ready.',
outlineReviseSuccess: 'The new outline passed its checks and is ready for your confirmation.',
outlineReviseError: 'This change did not finish. The previous outline is intact, and you can retry now.',
planRevision: 'Outline revision {revision}',
generate: 'Generate course',
generating: 'Generating lessons…',
generationProgress: '{published} of {total} lessons ready',
upgradeTitle: 'Ready to go deeper?',
upgradeDesc: 'Upgrade to Deep mastery without uploading again — your quick survey stays as the starting point.',
upgradeAction: 'Upgrade to Deep',
upgradeTitle: 'Deepen this course',
upgradeDesc: 'No need to upload again. Kimi will read the source carefully and improve the whole course while keeping existing lessons until replacements pass review.',
upgradeAction: 'Deepen course',
learningTitle: 'Your course is ready',
lessonsReady: '{published} of {total} lessons published',
lessonsUnavailable: 'Lesson files are not available yet — they appear here as they are published.',
lessonLoadError: 'This lesson could not be loaded.',
lessonReviseAction: 'Revise this lesson',
lessonRegenerateAction: 'Regenerate this lesson',
lessonReviseTitle: 'Revise the current lesson in one sentence',
lessonRevisePlaceholder: 'Describe how you want this lesson adjusted…',
lessonReviseExample: 'For example: add a beginner-friendly example; slow down the second explanation; add a self-check tied to the learning objective.',
lessonReviseSubmit: 'Update lesson',
lessonActionCancel: 'Cancel',
lessonRegenerateConfirmTitle: 'Regenerate the current lesson?',
lessonRegenerateConfirmBody: 'Only this lesson will be replaced. The current version stays visible until the replacement passes its checks.',
lessonRegenerateConfirmAction: 'Regenerate lesson',
lessonReviseWaiting: 'Kimi is revising this lesson. The current version stays in place until the replacement passes its checks.',
lessonRegenerateWaiting: 'Kimi is regenerating this lesson. The current version stays in place until the replacement passes its checks.',
lessonChangeSuccess: 'The current lesson passed its checks and was updated.',
lessonChangeError: 'This update did not finish. The previous lesson is intact, and you can retry now.',
type_lesson: 'Lesson',
type_reference: 'Reference',
type_quiz: 'Quiz',
Expand Down
27 changes: 22 additions & 5 deletions apps/kimi-web/src/i18n/locales/zh/study.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,21 +71,38 @@ export default {
outlineDesigning: 'Kimi 正在根据材料和目标设计大纲…',
outlineCounts: '{chapters} 章 · {pages} 页 · {quizzes} 个测验',
outlineReviseTitle: '提出修改',
outlineRevisePlaceholder: '例如:“加一章关于边界情况的讲解”…',
outlineRevisePlaceholder: '用一句话说明你想怎样调整大纲…',
outlineReviseExample: '例如:面向零基础读者;先讲案例再讲原理;缩减为 6 节;把风险分析移到最前面。',
outlineReviseAction: '更新大纲',
outlineReviseSent: '已提交修改——通过检查后,新的大纲版本会出现在这里。',
outlineReviseError: '暂时无法更新大纲,请稍后再试。',
outlineReviseWaiting: 'Kimi 正在按你的要求修改并检查大纲,当前版本会保留到新版本就绪。',
outlineReviseSuccess: '新大纲已通过检查并更新。请确认后再开始生成课程。',
outlineReviseError: '这次修改没有完成,原大纲已保留。你可以直接重试。',
planRevision: '大纲版本 {revision}',
generate: '生成课程',
generating: '正在生成课程…',
generationProgress: '{total} 节课已就绪 {published} 节',
upgradeTitle: '想学得更深?',
upgradeDesc: '无需重新上传即可升级为深度精学,快速通读成果会保留为起点。',
upgradeAction: '升级为深度精学',
upgradeTitle: '深入完善课程',
upgradeDesc: '不用重新上传。Kimi 会仔细通读材料并完善整门课,已有课节会保留到新版本通过检查。',
upgradeAction: '深入完善',
learningTitle: '课程已就绪',
lessonsReady: '已发布 {published} / {total} 节课',
lessonsUnavailable: '课程文件尚未就绪——发布后会出现在这里。',
lessonLoadError: '这节课暂时无法加载。',
lessonReviseAction: '修改本节',
lessonRegenerateAction: '重新生成本节',
lessonReviseTitle: '用一句话修改当前课节',
lessonRevisePlaceholder: '说明你希望这节课怎样调整…',
lessonReviseExample: '例如:补一个零基础也能懂的例子;把第二部分讲得更慢;增加一道对应学习目标的自测题。',
lessonReviseSubmit: '更新本节',
lessonActionCancel: '取消',
lessonRegenerateConfirmTitle: '确定重新生成当前课节?',
lessonRegenerateConfirmBody: '只会替换当前课节。新版本通过检查前,你仍然会看到现在的内容。',
lessonRegenerateConfirmAction: '确定重新生成',
lessonReviseWaiting: 'Kimi 正在按要求修改本节,旧版本会保留到新版本通过检查。',
lessonRegenerateWaiting: 'Kimi 正在重新生成本节,旧版本会保留到新版本通过检查。',
lessonChangeSuccess: '当前课节已通过检查并更新。',
lessonChangeError: '这次更新没有完成,原课节已保留。你可以直接重试。',
type_lesson: '课程',
type_reference: '参考',
type_quiz: '测验',
Expand Down
46 changes: 13 additions & 33 deletions apps/kimi-web/src/study/StudyApp.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<!-- apps/kimi-web/src/study/StudyApp.vue -->
<!-- Kimi Study product shell. Every screen renders against the product facade
(useStudyProduct) and the pure screen mapper — never against raw sessions,
models, permissions, or chat state. -->
<!-- Kimi Study product shell. The core path is deliberately narrow:
upload material, generate with Quick, optionally deepen, then learn. -->
<script setup lang="ts">
import { computed, onMounted, provide, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
Expand All @@ -14,19 +13,15 @@ import {
deriveStudyScreen,
STUDY_PRODUCT_INJECTION_KEY,
useStudyProduct,
type CertifiedCatalogMaterial,
type QuestionResponse,
type StudyCourseBinding,
} from './foundation';
import StudyProductHome from './components/product/StudyProductHome.vue';
import StudyModeSelect from './components/product/StudyModeSelect.vue';
import StudyPreparing from './components/product/StudyPreparing.vue';
import StudyOutline from './components/product/StudyOutline.vue';
import StudyLearning from './components/product/StudyLearning.vue';
import StudyQuestionCard from './components/product/StudyQuestionCard.vue';

// Hydrate the server-transport credential (#token fragment or localStorage)
// before the facade's first REST/WS call, mirroring the chat client's boot.
initServerAuth();

const { t } = useI18n();
Expand All @@ -35,19 +30,13 @@ useAppearance();
const product = useStudyProduct();
provide(STUDY_PRODUCT_INJECTION_KEY, product);
const courses = ref<readonly StudyCourseBinding[]>([]);
const catalog = ref<readonly CertifiedCatalogMaterial[]>([]);

async function reloadCourses(): Promise<void> {
try {
courses.value = await product.listCourses();
} catch {
courses.value = [];
}
try {
catalog.value = await product.listCatalog();
} catch {
catalog.value = [];
}
}

onMounted(async () => {
Expand All @@ -63,8 +52,6 @@ const model = computed(() => deriveStudyScreen(product.view.value));
const snapshot = computed(() => product.view.value.snapshot);
const question = computed(() => product.view.value.question);

// A 401 from the server means the transport credential is missing/expired —
// offer a token entry instead of the generic "unreachable" screen.
const needsServerCredential = computed(() =>
model.value.screen === 'unavailable'
&& /401|unauthorized/i.test(model.value.readinessMessage ?? ''));
Expand Down Expand Up @@ -99,16 +86,16 @@ async function guard(action: () => Promise<unknown>): Promise<void> {
try {
await action();
} catch {
// Failures transition the facade to the error screen; nothing to do here.
// Failures transition the facade to the error screen.
}
}

function onUpload(file: File): Promise<void> {
return guard(() => product.upload(file));
}

function onSelectMode(mode: 'quick' | 'deep'): Promise<void> {
return guard(() => product.selectMode(mode));
return guard(async () => {
await product.upload(file);
// Quick is the product default. Deep remains an optional later enhancement.
await product.selectMode('quick');
});
}

function onOpen(courseId: string): Promise<void> {
Expand All @@ -118,10 +105,6 @@ function onOpen(courseId: string): Promise<void> {
});
}

function onStartCatalog(material: CertifiedCatalogMaterial): Promise<void> {
return guard(() => product.startCatalog(material));
}

function onAnswer(response: QuestionResponse): Promise<void> {
return guard(() => product.answerQuestion(response));
}
Expand Down Expand Up @@ -206,22 +189,19 @@ function onRecheck(): Promise<void> {
<StudyProductHome
v-else-if="model.screen === 'home'"
:courses="courses"
:catalog="catalog"
:busy="model.busy"
:auth-required="model.authRequired"
:readiness-message="model.readinessMessage"
@upload="onUpload"
@open="onOpen"
@catalog="onStartCatalog"
@recheck="onRecheck"
/>

<StudyModeSelect
v-else-if="model.screen === 'mode_select' && snapshot"
:source-title="snapshot.source.title"
:busy="model.busy"
@select="onSelectMode"
/>
<!-- mode_select is intentionally transient: uploads immediately select Quick. -->
<div v-else-if="model.screen === 'mode_select'" class="study-center">
<Spinner size="lg" />
<p class="study-center-text">{{ t('study.product.loading') }}</p>
</div>

<StudyPreparing
v-else-if="model.screen === 'preparing' && snapshot"
Expand Down
2 changes: 1 addition & 1 deletion apps/kimi-web/src/study/components/StudyCourseList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,6 @@ function statusText(course: CourseSummary): string {

.course-status-review {
color: var(--color-accent);
font-weight: 600;
font-weight: var(--weight-semibold);
}
</style>
18 changes: 9 additions & 9 deletions apps/kimi-web/src/study/components/StudyGenerator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ function openLesson(file: string): void {
align-items: center;
gap: var(--space-2);
font-size: 16px;
font-weight: 600;
font-weight: var(--weight-semibold);
margin: 0;
}

Expand All @@ -388,14 +388,14 @@ function openLesson(file: string): void {

.reading-bar {
height: 6px;
border-radius: 3px;
border-radius: var(--radius-xs);
background: var(--color-surface-sunken);
overflow: hidden;
}

.reading-bar-fill {
height: 100%;
border-radius: 3px;
border-radius: var(--radius-xs);
background: var(--color-accent);
transition: width 0.4s ease;
}
Expand Down Expand Up @@ -423,7 +423,7 @@ function openLesson(file: string): void {

.course-title {
font-size: 22px;
font-weight: 700;
font-weight: var(--weight-semibold);
margin: 0 0 var(--space-3);
line-height: 1.3;
}
Expand Down Expand Up @@ -489,7 +489,7 @@ function openLesson(file: string): void {
cursor: pointer;
list-style: none;
font-size: 15px;
font-weight: 600;
font-weight: var(--weight-semibold);
}

.chapter-summary::-webkit-details-marker {
Expand Down Expand Up @@ -549,7 +549,7 @@ function openLesson(file: string): void {

.published-title {
font-size: 13px;
font-weight: 600;
font-weight: var(--weight-semibold);
color: var(--color-success);
margin-bottom: var(--space-2);
}
Expand Down Expand Up @@ -601,7 +601,7 @@ function openLesson(file: string): void {
.chat-rail-title {
flex: 1;
min-width: 0;
font-weight: 600;
font-weight: var(--weight-semibold);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
Expand All @@ -617,8 +617,8 @@ function openLesson(file: string): void {
/* Coursebox generate button: centred pill, min(400px,100%) × 44, radius 50. */
.primary-wide {
width: min(400px, 100%);
border-radius: 50px !important;
font-weight: 600 !important;
border-radius: var(--radius-full);
font-weight: var(--weight-semibold) !important;
transition:
background 0.15s,
transform 0.1s !important;
Expand Down
Loading
Loading