返回 DeepSeek-Reasonix
canonicalUserDisplay.ts
根目录 / desktop / frontend / src / lib / canonicalUserDisplay.ts
1 // Presentation only: never rewrite persisted or provider-visible messages.
2 // Keep this tag vocabulary aligned with agent.TransientUserBlockTags.
3 export const transientUserTags = [
4 "response-language", "reasoning-language", "memory-update", "background-jobs",
5 "active-goal", "autoresearch-runtime", "hook-context", "capability-route",
6 "interrupted-turn-recovery", "execution-policy",
7 ] as const;
8 const leadingBlock = new RegExp(`^\\s*<(${transientUserTags.join("|")})(?:\\s+[^>]*)?>[\\s\\S]*?</\\1>\\s*`);
9 const steerPrefix = "[Mid-turn steer queued by the user. Do not treat this as a new task; use it only as additional guidance for the current task after completing the current step.]";
10 const deliveryMarker = `<delivery-runtime>
11 This session is in delivery-first mode. Use todo_write when a task benefits from
12 an explicit task list, update it from your own assessment, and finish when the
13 user's request is handled. Structured file tools require a current host-observed
14 file version; reading any useful window establishes that observation.
15 </delivery-runtime>`;
16 const hostRecoveryPrefixes = [
17 "A tool failed. Use read-only diagnosis as needed",
18 "The tool timed out or hit a transient execution limit.",
19 ];
20
21 function legacyText(content: string): string {
22 let text = content;
23 for (let depth = 0; depth < 24; depth++) {
24 const next = text.replace(/<memory-compiler-execution>\s*([\s\S]*?)\s*<\/memory-compiler-execution>/g, (_block, json: string) => {
25 try {
26 const contract = JSON.parse(json);
27 const source = contract?.planner_ir?.source_event || contract?.source_event;
28 return typeof source === "string" ? source : "";
29 } catch { return ""; }
30 }).replace(leadingBlock, "");
31 if (next === text) break;
32 text = next;
33 }
34 if (text.trimStart().startsWith("# Reasonix executor handoff")) {
35 const start = text.indexOf("Original task:\n");
36 if (start >= 0) text = text.slice(start + "Original task:\n".length).split("\n\nPlanner output:")[0];
37 while (leadingBlock.test(text)) text = text.replace(leadingBlock, "");
38 }
39 text = text.trimEnd();
40 if (text.endsWith(deliveryMarker)) text = text.slice(0, -deliveryMarker.length);
41 text = text.replace(/\n*<execution-policy(?:\s+[^>]*)?>[\s\S]*?<\/execution-policy>\s*$/, "");
42 if (text.trimEnd().endsWith("</memory-recall>")) {
43 const index = text.lastIndexOf("<memory-recall>");
44 if (index >= 0) text = text.slice(0, index);
45 }
46 return text.trim();
47 }
48
49 export function canonicalUserDisplay(raw: Record<string, unknown>, fallback: string): { role: string; content: string } {
50 // An explicit origin wins over text heuristics. A user's quoted XML stays
51 // visible, while host snapshots stay hidden even when they have RawContent.
52 if (raw.origin === "host") return { role: "hidden", content: "" };
53 const stored = typeof raw.content === "string" ? raw.content : fallback;
54 const authored = typeof raw.raw_content === "string" && raw.raw_content.trim() ? raw.raw_content : undefined;
55 const steer = legacyText(stored);
56 const text = authored ?? steer;
57 if (raw.origin !== "user" && !authored && /^<session-context version="1">\s*This host-generated snapshot supersedes every earlier session-context snapshot\./.test(text)) {
58 return { role: "hidden", content: "" };
59 }
60 if (steer.startsWith(steerPrefix)) {
61 const content = authored ?? steer.slice(steerPrefix.length).trim();
62 if (raw.origin !== "user" && !authored && hostRecoveryPrefixes.some(prefix => content.startsWith(prefix))) return { role: "hidden", content: "" };
63 return { role: "notice", content: `↪ ${content}` };
64 }
65 if (raw.origin !== "user" && !authored && hostRecoveryPrefixes.some(prefix => text.startsWith(prefix))) return { role: "hidden", content: "" };
66 return { role: "user", content: text };
67 }
68
68 lines TYPESCRIPT