Skip to content
Open
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo

## Project Map

- `apps/kimi-code`: the CLI / TUI application. It consumes core capabilities through `@moonshot-ai/kimi-code-sdk` and must not depend directly on `@moonshot-ai/agent-core`. When writing or modifying its terminal UI, use the `write-tui` skill (`.agents/skills/write-tui/SKILL.md`).
- `apps/kimi-code`: the CLI / TUI application. The interactive TUI runs on `@moonshot-ai/agent-core-v2` through the in-app facade `src/core/` (`CoreHarness`/`CoreSession`); the remaining paths (`kimi -p`, ACP, misc subcommands) still consume v1 through `@moonshot-ai/kimi-code-sdk`. It must not depend directly on `@moonshot-ai/agent-core` (v1). When writing or modifying its terminal UI, use the `write-tui` skill (`.agents/skills/write-tui/SKILL.md`).
- `apps/kimi-web`: the browser web UI, a peer to the TUI. Vue 3 + Vite + vue-i18n; talks to the server over REST + WebSocket under `/api/v1`. It must not depend on `@moonshot-ai/agent-core` (wire types are re-implemented locally). See `apps/kimi-web/AGENTS.md`.
- `apps/vis`, `apps/vis/server`, `apps/vis/web`: visual debugging tools for sessions and replays.
- `packages/agent-core`: the unified agent engine, including Agent, Session, profile, skills, tools, plan, permission, background, records, the in-process DI service layer (`src/services/`), and other core capabilities.
Expand Down
15 changes: 8 additions & 7 deletions apps/kimi-code/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,17 @@ This file only contains rules local to `apps/kimi-code`. For cross-repo rules, s

`apps/kimi-code` is the terminal UI / CLI app. The entry chain is:

`src/main.ts` -> `src/cli/commands.ts` -> `src/cli/run-shell.ts` -> SDK `KimiHarness` -> `src/tui/kimi-tui.ts`
`src/main.ts` -> `src/cli/commands.ts` -> `src/cli/run-shell.ts` -> `src/core` `CoreHarness` (agent-core-v2 facade) -> `src/tui/kimi-tui.ts`

Main directories:

- `src/constant/`: non-copy constants shared by CLI/TUI — product, protocol, paths, terminal control, updates, and so on.
- `src/cli/`: command-line arguments, subcommands, and CLI startup.
- `src/core/`: the TUI engine facade over `packages/agent-core-v2` — `CoreHarness` (app scope: bootstrap, session CRUD, config, plugins, telemetry) and `CoreSession` (session scope: prompts, modes, goals, tasks, event fan-in, approval/question pending). The only path through which the interactive TUI reaches the agent engine. Print (`src/cli/v2/`), ACP, and other subcommands do not use this module — they still consume `@moonshot-ai/kimi-code-sdk`.
- `src/tui/`: the interactive terminal UI.
- `src/tui/kimi-tui.ts`: the `KimiTUI` coordinator — wires state, layout, editor, session, SDK events, and dialogs together, and dispatches slash-command handlers. Heavy logic is delegated to `controllers/`, not accumulated here.
- `src/tui/tui-state.ts`: `TUIState`, `createTUIState`, `createInitialAppState` — the single global UI-state shape.
- `src/tui/controllers/`: independently-testable responsibilities — `session-event-handler` (SDK event routing), `streaming-ui` (streaming render), `session-replay` (resume/replay), `tasks-browser`, `editor-keyboard`, `auth-flow`.
- `src/tui/controllers/`: independently-testable responsibilities — `session-event-handler` (core event routing), `streaming-ui` (streaming render), `session-replay` (resume/replay), `tasks-browser`, `editor-keyboard`, `auth-flow`.
- `src/tui/commands/`: slash command definitions, parsing, ordering, and dynamic skill command generation.
- `src/tui/components/`: pi-tui components, organized by UI type.
- `src/tui/constant/`: non-copy constants reused across TUI modules — symbols, terminal sequences, render sizing, streaming-arg match rules, and so on.
Expand All @@ -27,22 +28,22 @@ Main directories:
- `src/tui/components/media/`: image, diff, code highlight, and other media displays.
- `src/tui/components/messages/`: message blocks in the transcript — assistant, user, tool call, thinking, usage, subagent, and so on.
- `src/tui/components/panes/`: right-side / activity-area panes such as the activity pane and queue pane.
- `src/tui/reverse-rpc/`: the adapter layer that bridges SDK approval/question callbacks to the UI.
- `src/tui/interactions/`: consumes the core approval/question pending model — watches `CoreSession.approvals`/`questions` pending lists, drives the approval/question panels, and writes `decide`/`answer`/`dismiss` back into the kernel.
- `src/tui/theme/`: themes, color tokens, style helpers, terminal-background detection, and the pi-tui markdown theme.
- `src/tui/utils/`: TUI-only utility functions.
- `src/utils/`: app-wide utilities — clipboard, git, history, image, process, usage, and so on.

## Module Responsibilities

- `cli` only interprets command-line input, assembles startup arguments, and invokes the TUI. Do not put TUI interaction logic into the CLI.
- `KimiTUI` coordinates; it does not accumulate complex business rules. New logic that can be tested independently should be split into `controllers`, `commands`, `components`, `reverse-rpc`, or `utils` first.
- `KimiTUI` coordinates; it does not accumulate complex business rules. New logic that can be tested independently should be split into `controllers`, `commands`, `components`, `interactions`, or `utils` first.
- `controllers` own the heavy, independently-testable slices (event routing, streaming render, session replay, tasks browser, editor keyboard, auth). Event-routing and rendering logic belong here, not on the `KimiTUI` class.
- `commands` only owns slash-command declaration, parsing, and the parsed-result types. The actual execution can be dispatched from `KimiTUI`, but complex logic should continue to sink downward.
- `components` only handle presentation and local interaction; they must not call the SDK directly, and must not read or write session state directly.
- `reverse-rpc` converts SDK approval/question requests into the data shape a UI panel/dialog needs, and converts the user's choice back into an SDK response.
- `components` only handle presentation and local interaction; they must not call the core facade directly, and must not read or write session state directly.
- `interactions` turns the core pending approval/question entries into the data shape a UI panel/dialog needs, and writes the user's choice back through `decide`/`answer`/`dismiss`.
- `theme` is the single source of truth for colors and styles. Components must not bypass the theme system and use chalk named colors directly.
- `utils` holds utility functions with no UI-state dependency. Logic that needs `TUIState` or a component instance must not live under app-level `src/utils`.
- `apps/kimi-code` may only use core capabilities through `@moonshot-ai/kimi-code-sdk`. Do not import `@moonshot-ai/agent-core` directly in app code.
- The interactive TUI reaches the agent engine only through `src/core/` (`CoreHarness`/`CoreSession`). TUI files must not import `@moonshot-ai/agent-core-v2` or `@moonshot-ai/kimi-code-sdk` directly — add types/capabilities to the `src/core` facade instead. Print (`src/cli/v2/`), ACP, and other subcommands are exempt and still consume `@moonshot-ai/kimi-code-sdk`. No app code may import `@moonshot-ai/agent-core` (v1) directly.

## TUI Coding Conventions

Expand Down
41 changes: 29 additions & 12 deletions apps/kimi-code/src/cli/run-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,18 @@ import { execSync, spawnSync } from 'node:child_process';
import { homedir } from 'node:os';
import { join } from 'node:path';

import {
createKimiHarness,
log,
type KimiHarness,
type TelemetryClient,
} from '@moonshot-ai/kimi-code-sdk';
import { log } from '@moonshot-ai/kimi-code-sdk';
import {
setCrashPhase,
setTelemetryContext,
shutdownTelemetry,
track,
withTelemetryContext,
type TelemetryProperties,
} from '@moonshot-ai/kimi-telemetry';

import { CLI_SHUTDOWN_TIMEOUT_MS, CLI_UI_MODE } from '#/constant/app';
import { createCoreHarness, type TelemetryClient } from '#/core/index';
import { detectPendingMigration } from '#/migration/index';
import type { TuiConfig } from '#/tui/config';
import { loadTuiConfig, TuiConfigParseError } from '#/tui/config';
Expand Down Expand Up @@ -59,7 +56,7 @@ export async function runShell(
withContext: withTelemetryContext,
setContext: setTelemetryContext,
};
const harness = createKimiHarness({
const harness = createCoreHarness({
homeDir: telemetryBootstrap.homeDir,
identity: createKimiCodeHostIdentity(version),
skillDirs: opts.skillsDirs,
Expand Down Expand Up @@ -96,8 +93,23 @@ export async function runShell(
return;
}
const config = await harness.getConfig();
for (const warning of (await harness.getConfigDiagnostics()).warnings) {
configWarning = combineStartupNotice(configWarning, warning);
const configDiagnostics = await harness.getConfigDiagnostics();
// v1 fail-fast: an unparseable config.toml must not silently start with an
// empty config. v2 downgrades TOML parse failures to `severity: 'error'`
// diagnostics and keeps running, so surface every error and exit before the
// TUI grabs the terminal.
const configErrors = configDiagnostics.filter((diagnostic) => diagnostic.severity === 'error');
if (configErrors.length > 0) {
for (const diagnostic of configErrors) {
const domain = diagnostic.domain ?? 'config';
process.stderr.write(`Config error [${domain}]: ${diagnostic.message}\n`);
}
await harness.close();
process.exit(1);
}
for (const diagnostic of configDiagnostics) {
if (diagnostic.severity !== 'warning') continue;
configWarning = combineStartupNotice(configWarning, diagnostic.message);
}
const configMs = Date.now() - configStartedAt;
const tui = new KimiTUI(harness, {
Expand All @@ -114,7 +126,12 @@ export async function runShell(
initializeCliTelemetry({
harness,
bootstrap: telemetryBootstrap,
config,
// The v2 config document is domain-keyed and untyped on this surface;
// project the two fields the telemetry bootstrap reads.
config: {
defaultModel: typeof config['defaultModel'] === 'string' ? config['defaultModel'] : undefined,
telemetry: typeof config['telemetry'] === 'boolean' ? config['telemetry'] : undefined,
},
version,
uiMode: CLI_UI_MODE,
});
Expand All @@ -123,15 +140,15 @@ export async function runShell(
const trackLifecycleForSession = (
sessionId: string,
event: string,
properties?: Parameters<KimiHarness['track']>[1],
properties?: TelemetryProperties,
) => {
if (sessionId.length === 0) {
harness.track(event, properties);
return;
}
withTelemetryContext({ sessionId }).track(event, properties);
};
const trackLifecycle = (event: string, properties?: Parameters<KimiHarness['track']>[1]) => {
const trackLifecycle = (event: string, properties?: TelemetryProperties) => {
trackLifecycleForSession(tui.getCurrentSessionId(), event, properties);
};

Expand Down
16 changes: 14 additions & 2 deletions apps/kimi-code/src/cli/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
type TelemetryClient,
} from '@moonshot-ai/kimi-code-sdk';

import type { PromptHarness } from './prompt-session';
import {
initializeTelemetry,
setTelemetryContext,
Expand All @@ -17,6 +16,7 @@ import {
} from '@moonshot-ai/kimi-telemetry';

import { CLI_USER_AGENT_PRODUCT, WEB_UI_MODE } from '#/constant/app';
import type { TelemetryProperties } from '#/core/index';

import { createKimiCodeHostIdentity } from './version';

Expand All @@ -26,8 +26,20 @@ export interface CliTelemetryBootstrap {
readonly firstLaunch: boolean;
}

/**
* Minimal harness surface consumed by {@link initializeCliTelemetry}. Both the
* v1 SDK `KimiHarness` (print mode, `kimi export`) and the v2-backed
* `CoreHarness` (shell mode) satisfy it structurally — `KimiAuthFacade` is the
* same class on both paths.
*/
export interface CliTelemetryHarness {
readonly homeDir: string;
readonly auth: KimiAuthFacade;
track(event: string, properties?: TelemetryProperties): void;
}

export interface InitializeCliTelemetryOptions {
readonly harness: PromptHarness;
readonly harness: CliTelemetryHarness;
readonly bootstrap: CliTelemetryBootstrap;
readonly config: Pick<KimiConfig, 'defaultModel' | 'telemetry'>;
readonly version: string;
Expand Down
31 changes: 14 additions & 17 deletions apps/kimi-code/src/cli/v2/run-v2-print.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
type DomainEvent,
type IAgentScopeHandle,
type ISessionScopeHandle,
type LoopRunResult,
type Scope,
} from '@moonshot-ai/agent-core-v2';
import { createKimiDefaultHeaders, createKimiDeviceId } from '@moonshot-ai/kimi-code-oauth';
Expand Down Expand Up @@ -354,18 +355,17 @@ async function runNativeTurn(
// spawned has drained (config-bounded). Flush the buffered assistant
// message first so a long drain does not withhold the final message.
writer.flushAssistant();
if (result.reason === 'completed') {
try {
await drainBackgroundTasks(app, session);
} catch {
// Draining is best-effort; a wedged background task must not fail the
// (already completed) turn. Swallow and proceed to finish.
}
if (result.type !== 'completed') {
writer.finish();
return;
throw new Error(formatNativeTurnFailure(result));
}
try {
await drainBackgroundTasks(app, session);
} catch {
// Draining is best-effort; a wedged background task must not fail the
// (already completed) turn. Swallow and proceed to finish.
}
writer.finish();
throw new Error(formatNativeTurnFailure(result));
} catch (error) {
writer.finish();
throw error instanceof Error ? error : new Error(String(error));
Expand Down Expand Up @@ -493,10 +493,10 @@ async function drainBackgroundTasks(app: Scope, session: ISessionScopeHandle): P
if (allWaiters.length > 0) await Promise.all(allWaiters);
}

function formatNativeTurnFailure(result: {
readonly reason: string;
readonly error?: unknown;
}): string {
function formatNativeTurnFailure(result: Exclude<LoopRunResult, { readonly type: 'completed' }>): string {
if (result.type === 'cancelled') {
return `Prompt turn ended with reason: ${String(result.reason)}`;
}
const error = result.error as { readonly code?: string; readonly message?: string } | undefined;
if (error?.code === 'provider.filtered') {
return 'Provider safety policy blocked the response.';
Expand All @@ -507,8 +507,5 @@ function formatNativeTurnFailure(result: {
if (result.error instanceof Error) {
return result.error.message;
}
if (result.reason === 'blocked') {
return 'Prompt hook blocked the request.';
}
return `Prompt turn ended with reason: ${result.reason}`;
return 'Prompt turn failed.';
}
4 changes: 2 additions & 2 deletions apps/kimi-code/src/constant/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ErrorCodes } from '@moonshot-ai/kimi-code-sdk';
import { CoreErrorCodes } from '#/core/index';

export const PRODUCT_NAME = 'Kimi Code';
export const CLI_COMMAND_NAME = 'kimi';
Expand Down Expand Up @@ -58,7 +58,7 @@ export const DEFAULT_OAUTH_PROVIDER_NAME = 'managed:kimi-code';
// SDK/core error code that tells the TUI to show a login-required startup
// notice. Derived from sdk's ErrorCodes so a future rename in core
// auto-propagates instead of silently breaking the startup recovery path.
export const OAUTH_LOGIN_REQUIRED_CODE = ErrorCodes.AUTH_LOGIN_REQUIRED;
export const OAUTH_LOGIN_REQUIRED_CODE = CoreErrorCodes.AUTH_LOGIN_REQUIRED;

export const FEEDBACK_ISSUE_URL = 'https://github.com/MoonshotAI/kimi-code/issues';

Expand Down
Loading
Loading