返回 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 type TodoPresentationStatus = "pending" | "in_progress" | "waiting" | "paused" | "completed";
16
17 export interface TodoRuntimePresentation {
18 running: boolean;
19 pendingPrompt: boolean;
20 }
21
22 export function todoPresentationStatus(
23 status: Todo["status"],
24 runtime: TodoRuntimePresentation,
25 ): TodoPresentationStatus {
26 switch (todoStatus(status)) {
27 case "completed":
28 return "completed";
29 case "in_progress":
30 if (runtime.pendingPrompt) return "waiting";
31 return runtime.running ? "in_progress" : "paused";
32 default:
33 return "pending";
34 }
35 }
36
37 export function todoContinueTarget(
38 targetTabId: string | null | undefined,
39 activeTabId: string | null | undefined,
40 runtime: TodoRuntimePresentation & { ready: boolean; readOnly?: boolean },
41 ): string | null {
42 const target = String(targetTabId ?? "").trim();
43 if (!target || target !== String(activeTabId ?? "").trim()) return null;
44 if (!runtime.ready || runtime.readOnly || runtime.running || runtime.pendingPrompt) return null;
45 return target;
46 }
47
48 export function resolveTodoPanelTodos(
49 canonical: Todo[] | null | undefined,
50 ): Todo[] {
51 if (Array.isArray(canonical)) return canonical;
52 return [];
53 }
54
55 export function sameTodoList(a: Todo[] | null | undefined, b: Todo[] | null | undefined): boolean {
56 if (a === b) return true;
57 if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
58 return a.every((todo, index) => {
59 const other = b[index];
60 return (
61 todo.content === other.content &&
62 todo.status === other.status
63 );
64 });
65 }
66
67 export function todoDismissalKey(todos: Todo[]): string {
68 if (todos.length === 0) return "";
69 return JSON.stringify(todos.map((todo) => ({
70 content: String(todo.content ?? ""),
71 status: todoStatus(todo.status),
72 })));
73 }
74
75 export function todoPanelScope({ activeTab, activeTabId, eventChannel }: TodoPanelScopeInput): string {
76 const tabId = String(activeTabId ?? "").trim();
77 const tab = !tabId || activeTab?.id === tabId ? activeTab : null;
78 const sessionPath = tab?.sessionPath?.trim();
79 if (sessionPath) return `session:${sessionPath}`;
80 if (tabId) return `tab:${tabId}`;
81 const topicId = tab?.topicId?.trim();
82 if (tab && topicId) return `topic:${tab.scope ?? ""}:${tab.workspaceRoot ?? ""}:${topicId}`;
83 const channel = String(eventChannel ?? "").trim();
84 return channel ? `event:${channel}` : "";
85 }
86
87 export function scopedTodoDismissalKey(scope: string | null | undefined, todoKey: string | null | undefined): string {
88 const key = String(todoKey ?? "").trim();
89 if (!key) return "";
90 const prefix = String(scope ?? "").trim();
91 return prefix ? `${prefix}\0${key}` : key;
92 }
93
94 export function dismissedTodoKeyForScope(
95 scope: string | null | undefined,
96 dismissedKeys: ReadonlySet<string> | null | undefined,
97 todoKey: string | null | undefined,
98 ): string | null {
99 const scopedKey = scopedTodoDismissalKey(scope, todoKey);
100 if (!scopedKey || !dismissedKeys?.has(scopedKey)) return null;
101 return todoKey ?? null;
102 }
103
104 export function todoBatchKey(todos: Todo[]): string {
105 if (todos.length === 0) return "";
106 return JSON.stringify(todos.map((todo) => ({
107 content: String(todo.content ?? ""),
108 })));
109 }
110
111 export function scopedTodoBatchKey(scope: string | null | undefined, batchKey: string | null | undefined): string {
112 const key = String(batchKey ?? "").trim();
113 if (!key) return "";
114 const prefix = String(scope ?? "").trim();
115 return prefix ? `${prefix}\0${key}` : key;
116 }
117
118 export function shouldShowTodoPanel(
119 todoKey: string | null | undefined,
120 dismissedTodoKey: string | null,
121 todos: Todo[],
122 ): boolean {
123 if (!todoKey || todos.length === 0) return false;
124 if (hasIncompleteTodos(todos)) return true;
125 if (todoKey === dismissedTodoKey) return false;
126 return true;
127 }
128
129 export function shouldOpenTodoPanelByDefault(): boolean {
130 return false;
131 }
132
133 function todoStatus(status: unknown): string {
134 const normalized = String(status ?? "").trim();
135 return normalized || "pending";
136 }
137
138 function hasIncompleteTodos(todos: Todo[]): boolean {
139 return todos.some((todo) => todoStatus(todo.status) !== "completed");
140 }
141
141 lines TYPESCRIPT