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
This commit is contained in:
tommy0103
2026-07-21 19:29:14 +08:00
parent f79f1b3e3b
commit 28fa99ac94
11 changed files with 92555 additions and 21 deletions
+25 -12
View File
@@ -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 {