返回 DeepSeek-Reasonix
todoVisibility.ts
根目录 / desktop / frontend / src / lib / todoVisibility.ts
1 import type { Todo } from "./tools";
2
3 export interface TodoPanelScopeInput {
4 activeTabId?: string | null;
5 activeTab?: {
6 id?: string | null;
7 scope?: string | null;
8 workspaceRoot?: string | null;
9 topicId?: string | null;
10 sessionPath?: string | null;
11 } | null;
12 eventChannel?: string | null;
13 }
14
15 export function resolveTodoPanelTodos(
16 canonical: Todo[] | null | undefined,
17 live?: Todo[] | null,
18 ): Todo[] {
19 // `live` is set only when the transcript has a completed top-level todo_write.
20 // Prefer it over MetaForTab — meta only refreshes on turn_done / focus change,
21 // so mid-turn status flips otherwise freeze until the user switches tabs (#7642).
22 if (live !== undefined && live !== null) return live;
23 return Array.isArray(canonical) ? canonical : [];
24 }
25
26 export function sameTodoList(a: Todo[] | null | undefined, b: Todo[] | null | undefined): boolean {
27 if (a === b) return true;
28 if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
29 return a.every((todo, index) => {
30 const other = b[index];
31 return (
32 todo.content === other.content &&
33 todo.status === other.status &&
34 todo.activeForm === other.activeForm &&
35 todo.level === other.level
36 );
37 });
38 }
39
40 export function todoDismissalKey(todos: Todo[]): string {
41 if (todos.length === 0) return "";
42 return JSON.stringify(todos.map((todo) => ({
43 content: String(todo.content ?? ""),
44 status: todoStatus(todo.status),
45 activeForm: String(todo.activeForm ?? ""),
46 level: typeof todo.level === "number" ? todo.level : 0,
47 })));
48 }
49
50 export function todoPanelScope({ activeTab, activeTabId, eventChannel }: TodoPanelScopeInput): string {
51 const tabId = String(activeTabId ?? "").trim();
52 const tab = !tabId || activeTab?.id === tabId ? activeTab : null;
53 const sessionPath = tab?.sessionPath?.trim();
54 if (sessionPath) return `session:${sessionPath}`;
55 if (tabId) return `tab:${tabId}`;
56 const topicId = tab?.topicId?.trim();
57 if (tab && topicId) return `topic:${tab.scope ?? ""}:${tab.workspaceRoot ?? ""}:${topicId}`;
58 const channel = String(eventChannel ?? "").trim();
59 return channel ? `event:${channel}` : "";
60 }
61
62 export function scopedTodoDismissalKey(scope: string | null | undefined, todoKey: string | null | undefined): string {
63 const key = String(todoKey ?? "").trim();
64 if (!key) return "";
65 const prefix = String(scope ?? "").trim();
66 return prefix ? `${prefix}\0${key}` : key;
67 }
68
69 export function dismissedTodoKeyForScope(
70 scope: string | null | undefined,
71 dismissedKeys: ReadonlySet<string> | null | undefined,
72 todoKey: string | null | undefined,
73 ): string | null {
74 const scopedKey = scopedTodoDismissalKey(scope, todoKey);
75 if (!scopedKey || !dismissedKeys?.has(scopedKey)) return null;
76 return todoKey ?? null;
77 }
78
79 export function todoBatchKey(todos: Todo[]): string {
80 if (todos.length === 0) return "";
81 return JSON.stringify(todos.map((todo) => ({
82 content: String(todo.content ?? ""),
83 level: typeof todo.level === "number" ? todo.level : 0,
84 })));
85 }
86
87 export function scopedTodoBatchKey(scope: string | null | undefined, batchKey: string | null | undefined): string {
88 const key = String(batchKey ?? "").trim();
89 if (!key) return "";
90 const prefix = String(scope ?? "").trim();
91 return prefix ? `${prefix}\0${key}` : key;
92 }
93
94 export function shouldShowTodoPanel(
95 todoKey: string | null | undefined,
96 dismissedTodoKey: string | null,
97 todos: Todo[],
98 ): boolean {
99 if (!todoKey || todos.length === 0) return false;
100 if (hasIncompleteTodos(todos)) return true;
101 return todoKey !== dismissedTodoKey;
102 }
103
104 export function shouldOpenTodoPanelByDefault(): boolean {
105 return false;
106 }
107
108 function todoStatus(status: unknown): string {
109 const normalized = String(status ?? "").trim();
110 return normalized || "pending";
111 }
112
113 function hasIncompleteTodos(todos: Todo[]): boolean {
114 return todos.some((todo) => todoStatus(todo.status) !== "completed");
115 }
116
116 lines TYPESCRIPT