返回 DeepSeek-Reasonix
useTodoPanelCommands.ts
根目录 / desktop / frontend / src / app-runtime / useTodoPanelCommands.ts
1 import { useEffect, useMemo, useState } from "react";
2 import { useCommittedCommand } from "../lib/useCommittedCommand";
3 import type { Todo } from "../lib/tools";
4 import {
5 dismissedTodoKeyForScope,
6 resolveTodoPanelTodos,
7 scopedTodoBatchKey,
8 scopedTodoDismissalKey,
9 shouldShowTodoPanel,
10 todoBatchKey,
11 todoContinueTarget,
12 todoDismissalKey,
13 todoPanelScope,
14 } from "../lib/todoVisibility";
15 import type { Translator } from "../lib/i18n";
16 import type { Item } from "../lib/useController";
17 import type { TabMeta } from "../lib/types";
18 import type { useSessionOperations } from "./useSessionOperations";
19
20 export type TodoPanelCommandsInput = {
21 items: readonly Item[];
22 running: boolean;
23 pendingPrompt: boolean;
24 meta: {
25 canonicalTodos?: Todo[] | null;
26 sessionPath?: string;
27 eventChannel?: string;
28 } | undefined | null;
29 activeTab: TabMeta | undefined;
30 activeTabId: string | undefined;
31 remote: boolean;
32 remoteReady: boolean;
33 controllerReady: boolean;
34 sessionKey: string;
35 operations: ReturnType<typeof useSessionOperations>;
36 t: Translator;
37 ports: {
38 remoteSend(text: string): Promise<void>;
39 sendToTab(tabId: string, text: string): Promise<void>;
40 };
41 };
42
43 /**
44 * Owns the pinned task list above the composer: the canonical todo_write
45 * projection and the dismiss/continue commands. The shared runtime snapshot
46 * is the only current-state source; transcript tool cards remain history and
47 * are never replayed into the live panel. Dismissal is local presentation state
48 * for this mounted UI and cannot outlive a real turn boundary.
49 */
50 export function useTodoPanelCommands(input: TodoPanelCommandsInput) {
51 const { activeTab, activeTabId, remote, t, ports } = input;
52 const metaTodos = input.meta?.canonicalTodos;
53 const todos = useMemo(() => resolveTodoPanelTodos(metaTodos), [metaTodos]);
54 // Dismissal is a view preference for this mounted UI only. Persisting a todo
55 // fingerprint made stale progress survive real turn boundaries and become a
56 // second business-state store.
57 const [dismissedTodoKeys, setDismissedTodoKeys] = useState<Set<string>>(() => new Set());
58
59 useEffect(() => {
60 // turn_started publishes an authoritative empty list before any new
61 // todo_write. Dropping presentation keys here prevents an identical list
62 // in the next turn from inheriting the previous turn's dismissal.
63 if (todos.length === 0) setDismissedTodoKeys(new Set());
64 }, [todos]);
65 const todoKey = useMemo(() => todoDismissalKey(todos), [todos]);
66 const todoBatch = useMemo(() => todoBatchKey(todos), [todos]);
67 const todoScope = useMemo(
68 () => todoPanelScope({ activeTab, activeTabId, eventChannel: remote ? undefined : input.meta?.eventChannel }),
69 [activeTab, activeTabId, remote, input.meta?.eventChannel],
70 );
71 const dismissedTodo = useMemo(
72 () => dismissedTodoKeyForScope(todoScope, dismissedTodoKeys, todoKey),
73 [dismissedTodoKeys, todoKey, todoScope],
74 );
75 const scopedTodoKey = useMemo(() => scopedTodoDismissalKey(todoScope, todoKey), [todoKey, todoScope]);
76 const scopedTodoBatch = useMemo(() => scopedTodoBatchKey(todoScope, todoBatch), [todoBatch, todoScope]);
77 const showTodos = shouldShowTodoPanel(todoKey, dismissedTodo, todos);
78 const dismissTodos = useCommittedCommand(() => {
79 if (!scopedTodoKey) return;
80 setDismissedTodoKeys((current) => {
81 if (current.has(scopedTodoKey)) return current;
82 const next = new Set(current);
83 next.add(scopedTodoKey);
84 return next;
85 });
86 });
87 const handleTodoContinue = useCommittedCommand(() => {
88 const targetTabId = todoContinueTarget(activeTabId, activeTabId, {
89 ready: remote ? input.remoteReady : input.controllerReady,
90 readOnly: Boolean(activeTab?.readOnly),
91 running: input.running,
92 pendingPrompt: input.pendingPrompt,
93 });
94 if (!targetTabId) return;
95 const prompt = t("todo.continue");
96 if (remote) {
97 void ports.remoteSend(prompt);
98 return;
99 }
100 void ports.sendToTab(targetTabId, prompt);
101 });
102
103 return { showTodos, scopedTodoBatch, todos, dismissTodos, handleTodoContinue };
104 }
105
105 lines TYPESCRIPT