Skip to content
Merged
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
5 changes: 3 additions & 2 deletions AGENTS.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions apps/kimi-code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@
"test:native:smoke": "node scripts/native/smoke.mjs",
"dev": "node scripts/dev.mjs",
"dev:cli-only": "tsx --import ../../build/register-raw-text-loader.mjs ./src/main.ts",
"dev:server": "tsx --tsconfig ./tsconfig.dev.json --import ../../build/register-raw-text-loader.mjs ./src/main.ts web --no-open --debug-endpoints",
"dev:kap-server": "tsx --tsconfig ./tsconfig.dev.json --import ../../build/register-raw-text-loader.mjs ./src/main.ts web --no-open --debug-endpoints",
"dev:kap-server:multi": "tsx --tsconfig ./tsconfig.dev.json --import ../../build/register-raw-text-loader.mjs ./src/main.ts web --no-open --debug-endpoints",
"dev:server": "KIMI_CODE_DEV_SERVER=1 tsx --tsconfig ./tsconfig.dev.json --import ../../build/register-raw-text-loader.mjs ./src/main.ts web --no-open --debug-endpoints",
"dev:kap-server": "KIMI_CODE_DEV_SERVER=1 tsx --tsconfig ./tsconfig.dev.json --import ../../build/register-raw-text-loader.mjs ./src/main.ts web --no-open --debug-endpoints",
"dev:kap-server:multi": "KIMI_CODE_DEV_SERVER=1 tsx --tsconfig ./tsconfig.dev.json --import ../../build/register-raw-text-loader.mjs ./src/main.ts web --no-open --debug-endpoints",
"dev:server:restart": "node scripts/dev-server-restart.mjs",
"dev:plugin-marketplace": "node scripts/dev-plugin-marketplace-server.mjs",
"build:plugin-marketplace": "node scripts/build-plugin-marketplace-cdn.mjs",
Expand Down
2 changes: 1 addition & 1 deletion apps/kimi-code/scripts/dev-server-restart.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function start() {
console.error('[dev:server:restart] starting server…');
child = spawn(tsxBin, tsxArgs, {
cwd: APP_ROOT,
env: process.env,
env: { ...process.env, KIMI_CODE_DEV_SERVER: '1' },
// Server does not read stdin; keep ours free for the Enter trigger.
stdio: ['ignore', 'inherit', 'inherit'],
});
Expand Down
28 changes: 25 additions & 3 deletions apps/kimi-code/src/cli/sub/web/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* `startServer`).
*/

import { existsSync } from 'node:fs';
import { join } from 'node:path';

import { hostRequestHeadersSeed } from '@moonshot-ai/agent-core-v2';
Expand Down Expand Up @@ -266,6 +267,12 @@ async function runServerInProcess(
// logger, close }`, so adapt it to the `RoutedServer` surface the rest of
// this runner consumes.
const logger = createServerLogger({ level: options.logLevel });
const webAssetsDir = serverWebAssetsDir();
if (webAssetsDir === undefined) {
logger.info(
'dev mode: web assets not built; starting the API server without the web UI',
);
}
const v2 = await startServer({
host: options.host,
port: options.port,
Expand All @@ -288,7 +295,7 @@ async function runServerInProcess(
// requests (model, WebSearch, FetchURL) carry the same User-Agent +
// X-Msh-* identity as direct CLI runs.
seeds: hostRequestHeadersSeed(buildKimiDefaultHeaders(version)),
webAssetsDir: serverWebAssetsDir(),
webAssetsDir,
});
logger.info('serving the REST/WS API and the bundled web UI');
running = {
Expand All @@ -315,8 +322,23 @@ async function runServerInProcess(
});
}

function serverWebAssetsDir(): string {
return resolveServerWebAssetsDir();
/**
* Resolve the web assets directory passed to kap-server. In dev mode
* (`KIMI_CODE_DEV_SERVER=1`, set by the repo's `dev:server` / `dev:kap-server*`
* scripts) a missing `dist-web` build is tolerated: the server starts API-only
* and the web UI is expected to come from the kimi-web Vite dev server.
* Outside dev mode the directory is always returned and kap-server keeps
* failing fast when the assets are missing.
*/
export function serverWebAssetsDir(
env: NodeJS.ProcessEnv = process.env,
nativeWebAssetsDir: string | null = getNativeWebAssetsDir(),
): string | undefined {
const dir = resolveServerWebAssetsDir(nativeWebAssetsDir);
if (env['KIMI_CODE_DEV_SERVER'] === '1' && !existsSync(join(dir, 'index.html'))) {
return undefined;
}
return dir;
}

export function resolveServerWebAssetsDir(
Expand Down
32 changes: 32 additions & 0 deletions apps/kimi-code/test/cli/web/web.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,38 @@ describe('server web asset directory resolution', () => {
const { resolveServerWebAssetsDir } = await import('#/cli/sub/web/run');
expect(resolveServerWebAssetsDir(null)).toMatch(/[/\\]dist-web$/);
});

it('returns the assets dir when it is built, dev mode or not', async () => {
const { serverWebAssetsDir } = await import('#/cli/sub/web/run');
const dir = mkdtempSync(join(tmpdir(), 'kimi-web-assets-'));
try {
writeFileSync(join(dir, 'index.html'), '<html></html>');
expect(serverWebAssetsDir({}, dir)).toBe(dir);
expect(serverWebAssetsDir({ KIMI_CODE_DEV_SERVER: '1' }, dir)).toBe(dir);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

it('requires built assets outside dev mode', async () => {
const { serverWebAssetsDir } = await import('#/cli/sub/web/run');
const dir = mkdtempSync(join(tmpdir(), 'kimi-web-assets-'));
try {
expect(serverWebAssetsDir({}, dir)).toBe(dir);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

it('tolerates missing assets in dev mode (API-only server)', async () => {
const { serverWebAssetsDir } = await import('#/cli/sub/web/run');
const dir = mkdtempSync(join(tmpdir(), 'kimi-web-assets-'));
try {
expect(serverWebAssetsDir({ KIMI_CODE_DEV_SERVER: '1' }, dir)).toBeUndefined();
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});

function makeLegacyKillDeps(overrides: Partial<LegacyKillDeps> = {}): {
Expand Down
32 changes: 28 additions & 4 deletions apps/kimi-inspect/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,26 @@
* the `models` view is the full-width model catalog; the `services` view is
* the full-width app-scope Service reflection (`AppServicesView`); the
* `bash` view is the full-width `IBashParserService` playground
* (`BashParserView`).
* (`BashParserView`); the `search` view is the full-width global message
* search (`SearchView`) whose hits navigate back into the chat timeline.
*/

import { useEffect, useState } from 'react';

import { ISessionLifecycleService } from '@moonshot-ai/agent-core-v2/app/sessionLifecycle/sessionLifecycle';
import { useEffect, useState } from 'react';

import type { AuditTrail } from './audit/trail';
import { AppServicesView } from './components/AppServicesView';
import { BashParserView } from './components/BashParserView';
import { ChatView } from './components/ChatView';
import { ChatView, type ChatJump } from './components/ChatView';
import { ModelCatalogView } from './components/ModelCatalogView';
import { NavRail, type AppView } from './components/NavRail';
import { RightPanel } from './components/RightPanel';
import { SearchView } from './components/SearchView';
import { ServerSwitcher } from './components/ServerSwitcher';
import { SessionPane } from './components/SessionPane';
import { Sidebar } from './components/Sidebar';
import { useConnection } from './connection';
import type { SearchHit } from './search/api';
import { errorMessage } from './ui';

export function App() {
Expand All @@ -39,6 +41,8 @@ export function App() {
const [resumeError, setResumeError] = useState<unknown>(null);
/** Audit trail of the chat view's transcript channel, rendered in the right dock. */
const [trail, setTrail] = useState<AuditTrail | null>(null);
/** Pending chat navigation requested from another view (search result click). */
const [jump, setJump] = useState<ChatJump | null>(null);

// Resume (materialize) the session on the server when it is selected, so
// session / agent scoped Services become reachable.
Expand Down Expand Up @@ -66,8 +70,23 @@ export function App() {
useEffect(() => {
setSessionId(null);
setAgentId('main');
setJump(null);
}, [baseUrl]);

// A search hit opens the chat view at its session / agent / turn / step.
// Title hits belong to the session (agent '') and carry no turn: switch
// over without a scroll target.
const openSearchHit = (hit: SearchHit): void => {
setSessionId(hit.sessionId);
setAgentId(hit.agentId === '' ? 'main' : hit.agentId);
setView('chat');
setJump({
turnId: hit.turn === undefined ? undefined : `t${hit.turn}`,
stepId: hit.stepId,
nonce: Date.now(),
});
};

return (
<div className="flex h-screen flex-col">
<header className="flex items-center gap-3 border-b border-neutral-800 px-4 py-1.5">
Expand All @@ -94,6 +113,8 @@ export function App() {
setView('chat');
}}
/>
) : view === 'search' ? (
<SearchView onOpenResult={openSearchHit} />
) : (
<>
<Sidebar activeSessionId={sessionId} onSelectSession={setSessionId} />
Expand All @@ -108,6 +129,9 @@ export function App() {
agentId={agentId}
ready={ready}
onTrailChange={setTrail}
jump={jump}
onJumpHandled={() => setJump(null)}
onOpenSearchHit={openSearchHit}
/>
)}
<RightPanel
Expand Down
5 changes: 4 additions & 1 deletion apps/kimi-inspect/src/activity/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,10 @@ describe('SessionActivityHub', () => {
expect(hub.store.get('s1')).toEqual(facts({ busy: true, mainTurnActive: true }));
expect(hub.store.get('s2')?.pendingInteraction).toBe('approval');
// The hello goes out with no subscriptions — global facts flow regardless.
const hello = JSON.parse(instances[0]!.sent[0]!) as { type: string; payload: { subscriptions: string[] } };
const hello = JSON.parse(instances[0]!.sent[0]!) as {
type: string;
payload: { subscriptions: string[] };
};
expect(hello.type).toBe('client_hello');
expect(hello.payload.subscriptions).toEqual([]);
hub.close();
Expand Down
8 changes: 2 additions & 6 deletions apps/kimi-inspect/src/activity/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,8 @@
* `useSyncExternalStore`.
*/

import {
GlobalEventsWs,
type SessionWorkFacts,
} from './ws';
import type { WsLikeCtor } from '../channel/wsLike';
import { GlobalEventsWs, type SessionWorkFacts } from './ws';

export type { SessionWorkFacts };

Expand Down Expand Up @@ -131,8 +128,7 @@ export class SessionActivityHub {
{
busy: item['busy'],
mainTurnActive: item['main_turn_active'] === true,
pendingInteraction:
pending === 'approval' || pending === 'question' ? pending : 'none',
pendingInteraction: pending === 'approval' || pending === 'question' ? pending : 'none',
lastTurnReason:
reason === 'completed' || reason === 'cancelled' || reason === 'failed'
? reason
Expand Down
2 changes: 1 addition & 1 deletion apps/kimi-inspect/src/activity/useSessionActivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
* and a memo-created hub would stay closed for the rest of the page's life.
*/

import { useEffect, useState, useSyncExternalStore } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { useEffect, useState, useSyncExternalStore } from 'react';

import { useConnection } from '../connection';
import { SessionActivityHub, SessionActivityStore, type SessionWorkFacts } from './store';
Expand Down
3 changes: 1 addition & 2 deletions apps/kimi-inspect/src/activity/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,7 @@ function parseWorkFacts(payload: unknown): SessionWorkFacts | undefined {
return {
busy: p['busy'],
mainTurnActive: p['main_turn_active'] === true,
pendingInteraction:
pending === 'approval' || pending === 'question' ? pending : 'none',
pendingInteraction: pending === 'approval' || pending === 'question' ? pending : 'none',
lastTurnReason:
reason === 'completed' || reason === 'cancelled' || reason === 'failed' ? reason : undefined,
};
Expand Down
43 changes: 30 additions & 13 deletions apps/kimi-inspect/src/audit/audit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@
* and tail-preserving truncation used by the chat view's audit panel.
*/

import { EMPTY_AGENT_STATE, type AgentState, type TranscriptTurn } from '@moonshot-ai/transcript';
import { describe, expect, it } from 'vitest';

import {
EMPTY_AGENT_STATE,
type AgentState,
type TranscriptTurn,
} from '@moonshot-ai/transcript';

import { diffValue, type DiffNode } from './diff';
import { serializeState } from './serialize';
import { AuditTrail, AUDIT_TRAIL_MAX_ENTRIES } from './trail';
import { tailTrunc } from './truncate';

function turnItem(n: number): TranscriptTurn {
return { kind: 'turn', turnId: `t${n}`, ordinal: n, state: 'completed', origin: { kind: 'user' }, steps: [] };
return {
kind: 'turn',
turnId: `t${n}`,
ordinal: n,
state: 'completed',
origin: { kind: 'user' },
steps: [],
};
}

function stateWith(items: readonly TranscriptTurn[]): AgentState {
Expand All @@ -35,12 +37,19 @@ describe('diffValue', () => {
});

it('marks added, removed, and modified object keys', () => {
const node = diffValue({ keep: 1, gone: 'x', changed: 'a' }, { keep: 1, fresh: true, changed: 'b' });
const node = diffValue(
{ keep: 1, gone: 'x', changed: 'a' },
{ keep: 1, fresh: true, changed: 'b' },
);
expect(node.status).toBe('modified');
expect(node.children?.get('keep')?.status).toBe('unchanged');
expect(node.children?.get('fresh')?.status).toBe('added');
expect(node.children?.get('gone')).toMatchObject({ status: 'removed', prev: 'x' });
expect(node.children?.get('changed')).toMatchObject({ status: 'modified', prev: 'a', value: 'b' });
expect(node.children?.get('changed')).toMatchObject({
status: 'modified',
prev: 'a',
value: 'b',
});
});

it('matches entity arrays by id instead of index', () => {
Expand Down Expand Up @@ -70,7 +79,7 @@ describe('diffValue', () => {
[step('t1.1', 'completed'), step('t1.2', 'completed')],
[step('t1.1', 'completed'), step('t1.2', 'running')],
);
expect([...node.children?.keys() ?? []]).toEqual(['t1.1', 't1.2']);
expect([...(node.children?.keys() ?? [])]).toEqual(['t1.1', 't1.2']);
expect(node.children?.get('t1.1')?.status).toBe('unchanged');
expect(node.children?.get('t1.2')?.status).toBe('modified');
});
Expand Down Expand Up @@ -124,8 +133,14 @@ describe('serializeState', () => {
const state: AgentState = {
...EMPTY_AGENT_STATE,
tasks: new Map([
['b-task', { taskId: 'b-task', kind: 'shell', state: 'running', detached: false, outputTail: '' }],
['a-task', { taskId: 'a-task', kind: 'tool', state: 'completed', detached: false, outputTail: '' }],
[
'b-task',
{ taskId: 'b-task', kind: 'shell', state: 'running', detached: false, outputTail: '' },
],
[
'a-task',
{ taskId: 'a-task', kind: 'tool', state: 'completed', detached: false, outputTail: '' },
],
]),
pendingInteractions: new Set(['z', 'a']),
};
Expand Down Expand Up @@ -188,7 +203,9 @@ describe('AuditTrail', () => {
expect(entries[1]!.state).toBe(s2);
expect(entries[1]).toMatchObject({ delivery: 'live', envelopeAt: '2026-01-01T00:00:00Z' });
expect(entries[2]).toMatchObject({ event: 'prompt', detail: 'hello' });
expect(entries.every((entry) => typeof entry.at === 'string' && entry.at.length > 0)).toBe(true);
expect(entries.every((entry) => typeof entry.at === 'string' && entry.at.length > 0)).toBe(
true,
);
expect(entries.every((entry) => entry.summary.length > 0)).toBe(true);
});

Expand Down
6 changes: 5 additions & 1 deletion apps/kimi-inspect/src/audit/trail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,11 @@ export class AuditTrail {
});
}

recordEvent(event: EventAuditEntry['event'], detail: string | undefined, state: AgentState): void {
recordEvent(
event: EventAuditEntry['event'],
detail: string | undefined,
state: AgentState,
): void {
const label =
event === 'ack-refresh'
? 'subscribe ack → REST refresh'
Expand Down
8 changes: 3 additions & 5 deletions apps/kimi-inspect/src/channel/channel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,11 @@ describe('makeProxy', () => {
it('routes methods to call and onXxx members to listen', async () => {
const seen = { calls: [] as [string, unknown[]][], listens: [] as string[] };
const channel: IChannel = {
call: async <T,>(command: string, args?: unknown[]): Promise<T> => {
call: async <T>(command: string, args?: unknown[]): Promise<T> => {
seen.calls.push([command, args ?? []]);
return 'ret' as T;
},
listen: <T,>(event: string): Event<T> => {
listen: <T>(event: string): Event<T> => {
seen.listens.push(event);
return () => ({ dispose: () => {} });
},
Expand Down Expand Up @@ -137,9 +137,7 @@ describe('probeDebugSurface', () => {

it('throws a --debug-endpoints hint when the surface is not mounted (HTTP 404)', async () => {
stubProbeFetch(() => ({ ok: false, status: 404 }));
await expect(probeDebugSurface({ baseUrl: 'http://h:6' })).rejects.toThrow(
/--debug-endpoints/,
);
await expect(probeDebugSurface({ baseUrl: 'http://h:6' })).rejects.toThrow(/--debug-endpoints/);
});

it('throws an unreachable-server error when fetch itself fails', async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/kimi-inspect/src/channel/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@

import { createDecorator } from '@moonshot-ai/agent-core-v2/_base/di/instantiation';

import type { ServiceProxy } from './channel';
import { DEBUG_RPC_BASE, type InspectClient } from './client';
import { RPCError } from './errors';
import type { ServiceProxy } from './channel';

/** Wire scope kinds reported by the channels endpoint (`app` ≡ the core route). */
export type ChannelScope = 'app' | 'session' | 'agent';
Expand Down
Loading
Loading