From 28fa99ac94ea04be0b149384e35827f097c464cc Mon Sep 17 00:00:00 2001 From: tommy0103 Date: Tue, 21 Jul 2026 19:29:14 +0800 Subject: [PATCH] feat: improve session sharing and Kimi transcript support - normalize Kimi thinking parts and omit empty placeholders - add standalone session-sharing and UI demo artifacts - add regression coverage and refresh the Kimi index marker - bump the Electron app and CLI packages to v0.2.1 - ignore generated app build assets --- .gitignore | 4 +- app/obelisk-session-share.html | 92214 ++++++++++++++++ app/obelisk-ui-mini.html | 258 + app/package-lock.json | 4 +- app/package.json | 3 +- .../src/session-timeline-presentation.mjs | 2 +- package-lock.json | 2 +- packages/cli/package.json | 2 +- packages/core/src/providers/kimi.ts | 37 +- tests/app-kimi-index.test.mjs | 4 +- tests/kimi-parse.test.mjs | 46 + 11 files changed, 92555 insertions(+), 21 deletions(-) create mode 100644 app/obelisk-session-share.html create mode 100644 app/obelisk-ui-mini.html diff --git a/.gitignore b/.gitignore index fe7675e..4d6824f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,6 @@ release/ dist/ app/out/ HANDOFF.md -.obelisk/ \ No newline at end of file +.obelisk/ +app/build/ +app/scripts/ diff --git a/app/obelisk-session-share.html b/app/obelisk-session-share.html new file mode 100644 index 0000000..67f084b --- /dev/null +++ b/app/obelisk-session-share.html @@ -0,0 +1,92214 @@ + + +添加 Obelisk UI 交互展示 Mini App · Obelisk
\ No newline at end of file diff --git a/app/obelisk-ui-mini.html b/app/obelisk-ui-mini.html new file mode 100644 index 0000000..14d618d --- /dev/null +++ b/app/obelisk-ui-mini.html @@ -0,0 +1,258 @@ + + + + + + +Obelisk UI Mini + + + +
+
+
+ + + diff --git a/app/package-lock.json b/app/package-lock.json index 3f90c4f..f51a6b4 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "obelisk", - "version": "0.2.0", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obelisk", - "version": "0.2.0", + "version": "0.2.1", "dependencies": { "better-sqlite3": "^11.0.0", "chokidar": "^4.0.3" diff --git a/app/package.json b/app/package.json index 04625cc..66aef48 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "obelisk", - "version": "0.2.0", + "version": "0.2.1", "description": "Memory management for Obelisk — let Claude Code search its own memory", "homepage": "https://github.com/tommy0103/obelisk", "author": { @@ -17,6 +17,7 @@ "dist:mac": "electron-vite build && electron-builder --mac", "dist:win": "electron-vite build && electron-builder --win", "dist:linux": "electron-vite build && electron-builder --linux", + "generate:session-share": "node scripts/generate-session-share.mjs", "verify:icons": "node scripts/verify-icons.mjs", "test:electron": "electron-vite build && electron --no-sandbox tests/electron-concurrency.mjs", "test:electron:timeline": "electron-vite build && electron --no-sandbox tests/electron-session-virtualization.mjs", diff --git a/app/src/renderer/src/session-timeline-presentation.mjs b/app/src/renderer/src/session-timeline-presentation.mjs index cc28087..fee7471 100644 --- a/app/src/renderer/src/session-timeline-presentation.mjs +++ b/app/src/renderer/src/session-timeline-presentation.mjs @@ -190,7 +190,7 @@ function renderOutput(output, isError) { return `
${escapeHtml(output)}
`; } -function renderPrettyTool(toolCall) { +export function renderPrettyTool(toolCall) { const args = parseToolInput(toolCall); const result = toolCall.result || {}; const isError = Boolean(result.is_error); diff --git a/package-lock.json b/package-lock.json index 6d29140..01bdd98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1289,7 +1289,7 @@ }, "packages/cli": { "name": "@obelisk-apps/cli", - "version": "0.2.0", + "version": "0.2.1", "license": "AGPL-3.0", "bin": { "obelisk": "dist/cli/src/obelisk.js" diff --git a/packages/cli/package.json b/packages/cli/package.json index 23ee17e..58cf4c6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@obelisk-apps/cli", - "version": "0.2.0", + "version": "0.2.1", "description": "Local Obelisk runtime for coding agents.", "license": "AGPL-3.0", "type": "module", diff --git a/packages/core/src/providers/kimi.ts b/packages/core/src/providers/kimi.ts index 839bf4e..4e49997 100644 --- a/packages/core/src/providers/kimi.ts +++ b/packages/core/src/providers/kimi.ts @@ -56,7 +56,7 @@ interface ProjectedSession { } const SOURCE = 'kimi'; -export const KIMI_CANONICAL_TRANSCRIPT_MARKER = '__kimi_canonical_transcript_v2__'; +export const KIMI_CANONICAL_TRANSCRIPT_MARKER = '__kimi_canonical_transcript_v3__'; function defaultKimiRoot(): string { return process.env['KIMI_CODE_HOME'] ?? join(homedir(), '.kimi-code'); @@ -141,12 +141,26 @@ function contentParts(content: unknown): JsonRecord[] { : []; } -function partText(part: JsonRecord): string | null { - if (part.type === 'text' && typeof part.text === 'string') return trunc(part.text); - if (part.type === 'thinking' && typeof part.thinking === 'string') return trunc(part.thinking); +function rawPartText(part: JsonRecord): string | null { + if (part.type === 'text' && typeof part.text === 'string') return part.text; + if (part.type === 'think' && typeof part.think === 'string') return part.think; + if (part.type === 'thinking' && typeof part.thinking === 'string') return part.thinking; return null; } +function partText(part: JsonRecord): string | null { + const text = rawPartText(part); + return text === null ? null : trunc(text); +} + +function partContentType(part: JsonRecord): string { + return part.type === 'think' || part.type === 'thinking' + ? 'thinking' + : typeof part.type === 'string' + ? part.type + : 'unknown'; +} + function messageText(content: unknown): string | null { const parts = contentParts(content); const text = parts.map(partText).filter((value): value is string => value !== null); @@ -154,7 +168,7 @@ function messageText(content: unknown): string | null { } function messageContentType(content: unknown): string { - const types = new Set(contentParts(content).map((part) => String(part.type ?? 'unknown'))); + const types = new Set(contentParts(content).map(partContentType)); return types.size === 1 ? [...types][0]! : 'unknown'; } @@ -437,6 +451,8 @@ function projectSession(meta: KimiSessionUnitMeta, sessionId: string, state: Jso if (event.type === 'content.part' && typeof event.stepUuid === 'string') { const part = event.part as JsonRecord | undefined; if (part === undefined) continue; + const text = partText(part); + if (text === null || text.trim().length === 0) continue; pushMessage({ kind: 'message', uuid: namespacedEventId(sessionId, wire.agentId, event.uuid, line), @@ -445,8 +461,8 @@ function projectSession(meta: KimiSessionUnitMeta, sessionId: string, state: Jso parent_uuid: previousUuid, timestamp, role: 'assistant', - text: partText(part), - content_type: typeof part.type === 'string' ? part.type : 'unknown', + text, + content_type: partContentType(part), is_meta: 0, visibility: 'visible', model, @@ -604,17 +620,14 @@ function rawFromWire(path: string, messageUuid: string): RawRecord | null { if (slashCommand !== null) projectedText = slashCommand; else { const parts = contentParts(message.content).map((part) => { - if (part.type === 'text' && typeof part.text === 'string') return part.text; - if (part.type === 'thinking' && typeof part.thinking === 'string') return part.thinking; - return null; + return rawPartText(part); }).filter((part): part is string => part !== null); projectedText = parts.length > 0 ? parts.join('\n') : null; } } } else if (record.type === 'context.append_loop_event') { const part = (record.event as JsonRecord | undefined)?.part as JsonRecord | undefined; - if (part?.type === 'text' && typeof part.text === 'string') projectedText = part.text; - if (part?.type === 'thinking' && typeof part.thinking === 'string') projectedText = part.thinking; + if (part !== undefined) projectedText = rawPartText(part); } } catch { /* malformed torn source line */ } return { diff --git a/tests/app-kimi-index.test.mjs b/tests/app-kimi-index.test.mjs index e331dbe..4a783d4 100644 --- a/tests/app-kimi-index.test.mjs +++ b/tests/app-kimi-index.test.mjs @@ -133,7 +133,7 @@ test('Kimi undo and clear replace the indexed session instead of leaving stale r db.close(); }); -test('Kimi prompt semantics marker replays unchanged sessions once', () => { +test('Kimi canonical transcript marker replays unchanged sessions once', () => { const home = mkdtempSync(join(tmpdir(), 'obelisk-kimi-prompt-marker-')); const claudeDir = join(home, '.claude'); const codexDir = join(home, '.codex'); @@ -151,7 +151,7 @@ test('Kimi prompt semantics marker replays unchanged sessions once', () => { buildIndex(options); let db = new TestDatabase(dbPath); const marker = createKimiProvider({ rootDir: kimiDir }).indexVersionMarker; - assert.equal(typeof marker, 'string'); + assert.equal(marker, '__kimi_canonical_transcript_v3__'); db.prepare("UPDATE messages SET text='stale expanded instructions', is_meta=1 WHERE source='kimi'").run(); db.prepare('DELETE FROM index_state WHERE jsonl_path=?').run(marker); db.close(); diff --git a/tests/kimi-parse.test.mjs b/tests/kimi-parse.test.mjs index 3082f7c..cd71c69 100644 --- a/tests/kimi-parse.test.mjs +++ b/tests/kimi-parse.test.mjs @@ -145,6 +145,52 @@ test('kimi provider ignores a torn final wire line until it is completed', () => assert.equal(ret, unit.meta.currentCursor); }); +test('kimi provider normalizes think parts and drops empty thinking placeholders', () => { + const root = mkdtempSync(join(tmpdir(), 'obelisk-kimi-think-')); + const sessionDir = join(root, 'sessions', 'workspace-1', 'session-think-1'); + const mainDir = join(sessionDir, 'agents', 'main'); + const wirePath = join(mainDir, 'wire.jsonl'); + mkdirSync(mainDir, { recursive: true }); + writeFileSync(join(sessionDir, 'state.json'), JSON.stringify({ workDir: '/tmp/think' })); + const records = [ + { type: 'metadata', protocol_version: '1.5', created_at: 1 }, + { type: 'context.append_loop_event', time: 2, event: { type: 'step.begin', uuid: 'step-1' } }, + { type: 'context.append_loop_event', time: 3, event: { + type: 'content.part', uuid: 'think-1', stepUuid: 'step-1', + part: { type: 'think', think: 'private reasoning' }, + } }, + { type: 'context.append_loop_event', time: 4, event: { + type: 'content.part', uuid: 'think-empty', stepUuid: 'step-1', + part: { type: 'think', think: '' }, + } }, + { type: 'context.append_loop_event', time: 5, event: { + type: 'content.part', uuid: 'answer-1', stepUuid: 'step-1', + part: { type: 'text', text: 'visible answer' }, + } }, + ]; + writeFileSync(wirePath, records.map(record => JSON.stringify(record)).join('\n') + '\n'); + const provider = createKimiProvider({ rootDir: root }); + const unit = provider.discover({ lastCursor: () => null })[0]; + + const { values } = drain(provider.parse(unit, null)); + const messages = values.filter(record => record.kind === 'message'); + + assert.deepEqual(messages.map(record => ({ + text: record.text, + content_type: record.content_type, + })), [ + { text: 'private reasoning', content_type: 'thinking' }, + { text: 'visible answer', content_type: 'text' }, + ]); + assert.equal(values.find(record => record.kind === 'session').message_count, 2); + assert.equal(provider.raw({ + source: 'kimi', + messageUuid: messages[0].uuid, + session: { jsonl_path: wirePath }, + agentId: null, + }).messageText, 'private reasoning'); +}); + test('kimi provider replays clear and undo markers with Kimi transcript semantics', () => { const root = mkdtempSync(join(tmpdir(), 'obelisk-kimi-undo-')); const sessionDir = join(root, 'sessions', 'workspace-1', 'session-undo-1');