返回 DeepSeek-Reasonix
independent-sessions.tsx
根目录 / desktop / frontend / bench / independent-sessions.tsx
1 // Actual product components and command owners; only the Desktop RPC boundary
2 // is replaced. No production mock data or identity implementation is changed.
3 import React, { useState } from "react";
4 import { createRoot } from "react-dom/client";
5 import { ProjectTree } from "../src/components/ProjectTree";
6 import { HistoryPanel } from "../src/components/HistoryPanel";
7 import { TopicbarRegion } from "../src/app-shell/TopicbarRegion";
8 import { useProjectTopicCommands } from "../src/app-runtime/useProjectTopicCommands";
9 import { useHistoryCommands } from "../src/app-runtime/useHistoryCommands";
10 import { desktopProjectAdapter } from "../src/app-runtime/desktopProjectAdapter";
11 import type { HistoryViewState } from "../src/app-runtime/historyViewProjection";
12 import { app } from "../src/lib/bridge";
13 import { DESKTOP_COMMANDS, type SessionSelector, type SessionOrganizationMutation, type SessionOrganizationSnapshot } from "../src/generated/desktopContract.generated";
14 import { ApprovalModal } from "../src/components/ApprovalModal";
15 import { useSessionOperations } from "../src/app-runtime/useSessionOperations";
16 import { useSessionPromptCommands } from "../src/app-runtime/useSessionPromptCommands";
17 import { useSessionControlCommands } from "../src/app-runtime/useSessionControlCommands";
18 import { resolvePromptForTab } from "../src/lib/exactPromptSubmit";
19 import { requestSessionCancel } from "../src/lib/inboxCancel";
20 import { installDesktopHostStub } from "../src/__tests__/desktopHostStub";
21 import { LocaleProvider } from "../src/lib/i18n";
22 import { ToastProvider } from "../src/lib/toast";
23 import type { ProjectNode, SessionMeta } from "../src/lib/types";
24 import "../src/styles.css";
25
26 localStorage.setItem("reasonix-lang", "en");
27 const root = "/fixture/independent";
28 const topicId = "same-topic";
29 const paginationScenario = new URLSearchParams(location.search).has("pagination");
30 let paginationInvalidated = false;
31 const pageRequests: { cursor: string; revision: number; rejected?: boolean }[] = [];
32 let revision = 1;
33 let stale = false;
34 let archived = false;
35 const rows: ProjectNode[] = (paginationScenario ? ["a", "b", "c", "d", "e", "f", "g"] : ["a", "b"]).map((id, index) => ({
36 key: `session-${id}`, kind: "topic", label: `Session ${id.toUpperCase()}`, root, topicId,
37 session: { hostId: "local", sessionId: id }, sessionPath: `session-id:${id}`,
38 lifecycleGeneration: 1, resultSequence: index === 0 ? 100 : 10,
39 turns: 1, turnsState: "ready", createdAt: 100, lastActivityAt: 100, children: [],
40 }));
41 const folder: ProjectNode = { key: "project-fixture", kind: "project", label: "Independent sessions", root, children: [] };
42 const catalogStatus = () => ({ state: "ready", mode: "memory", revision, indexed: 2, total: 2, repairPending: 0, sourceCount: 2, unindexedTargetCount: 0, canRebuild: false });
43 const listRows = () => structuredClone(rows.filter(row => !archived || stale || row.session?.sessionId !== "b"));
44 const sessions = (): SessionMeta[] => rows.filter(row => !archived || row.session?.sessionId !== "b").map(row => ({
45 path: row.sessionPath!, sessionId: row.session!.sessionId, hostId: "local", topicId,
46 title: row.label, topicTitle: row.label, preview: row.label, turns: 1, createdAt: 100, lastActivityAt: 100, modTime: 100,
47 current: false, open: false, scope: "project", workspaceRoot: root,
48 }));
49 const calls: { method: string; id: string; title?: string }[] = [];
50 const organizationCalls: SessionOrganizationMutation[] = [];
51 const runtimeCalls: { method: string; tabId: string; promptId?: string; turnId?: string; epoch?: string }[] = [];
52 const organization: SessionOrganizationSnapshot = { revision: 1, applied: true, groups: [], order: ["ref\x00local\x00a", "ref\x00local\x00b"], manualOrderEnabled: false };
53 const resolve = (selector: SessionSelector) => {
54 const row = rows.find(row => selector.ref ? row.session?.sessionId === selector.ref.sessionId && selector.ref.hostId === "local" : row.sessionPath === selector.sessionPath);
55 if (!row) throw new Error("fixture target_not_found");
56 return row;
57 };
58 const fallback = Object.fromEntries(DESKTOP_COMMANDS.map(name => [name, app[name]]));
59 const host = installDesktopHostStub({ ...fallback,
60 Platform: async () => "linux",
61 GetProjectTreeSnapshot: async () => ({ revision, projects: [folder], catalog: catalogStatus(), indexed: 2, total: 2, indexingDone: true }),
62 ListProjectTopics: async (request: { groupId?: string; cursor?: string; limit?: number }) => {
63 if (paginationScenario) {
64 const entry = { cursor: request.cursor || "", revision, rejected: false };
65 pageRequests.push(entry);
66 if (request.cursor === "old:5") {
67 paginationInvalidated = true; revision++; entry.rejected = true;
68 throw new Error("session_operation:stale_cursor:The session list changed. Reload it.");
69 }
70 const ids = paginationInvalidated ? ["c", "a", "b", "d", "e", "f", "g"] : ["a", "b", "d", "e", "f", "g"];
71 const start = request.cursor ? Number(request.cursor.split(":")[1]) : 0;
72 const stop = Math.min(ids.length, start + (request.limit ?? 5));
73 const items = ids.slice(start, stop).map((id, index) => ({ ...structuredClone(rows.find(row => row.session!.sessionId === id)!), sortOrder: start + index }));
74 return { revision, items, nextCursor: stop < ids.length ? `${paginationInvalidated ? "new" : "old"}:${stop}` : "", complete: true, readyDirectories: 1, pendingDirectories: 0, failedDirectories: 0 };
75 }
76 const members = organization.groups.find(group => group.id === request.groupId)?.sessionKeys ?? [];
77 const grouped = new Set(organization.groups.flatMap(group => group.sessionKeys ?? []));
78 const items = listRows().filter(row => request.groupId ? members.includes(`ref\x00local\x00${row.session!.sessionId}`) : !grouped.has(`ref\x00local\x00${row.session!.sessionId}`));
79 if (organization.manualOrderEnabled) {
80 for (const row of items) row.sortOrder = organization.order.indexOf(`ref\x00local\x00${row.session!.sessionId}`);
81 items.sort((a,b) => a.sortOrder! - b.sortOrder!);
82 }
83 return { revision, items, nextCursor: "", complete: true, readyDirectories: 1, pendingDirectories: 0, failedDirectories: 0 };
84 },
85 GetSessionCatalogStatus: async () => catalogStatus(),
86 GetProjectTreeRuntimeSnapshot: async () => ({ revision, topics: [] }),
87 GetTopicSummary: async () => ({ key: "", kind: "topic", label: "", children: [] }),
88 GetSessionOrganization: async () => structuredClone(organization),
89 UpdateSessionOrganization: async (_workspace: unknown, expectedRevision: number, mutation: SessionOrganizationMutation) => {
90 if (expectedRevision !== organization.revision) return { ...structuredClone(organization), applied: false };
91 organizationCalls.push(structuredClone(mutation));
92 const key = mutation.target ? `ref\x00local\x00${resolve(mutation.target).session!.sessionId}` : "";
93 if (mutation.kind === "create-group") organization.groups.push({ id: mutation.groupId!, title: mutation.title!, sessionKeys: [] });
94 else if (mutation.kind === "set-group") {
95 for (const group of organization.groups) group.sessionKeys = (group.sessionKeys ?? []).filter(item => item !== key);
96 organization.groups.find(group => group.id === mutation.groupId)?.sessionKeys?.push(key);
97 } else if (mutation.kind === "move") {
98 const anchor = `ref\x00local\x00${resolve(mutation.anchor!).session!.sessionId}`;
99 organization.order = organization.order.filter(item => item !== key);
100 organization.order.splice(organization.order.indexOf(anchor) + (mutation.position === "after" ? 1 : 0), 0, key);
101 organization.manualOrderEnabled = true;
102 } else throw new Error(`unhandled organization mutation ${mutation.kind}`);
103 organization.revision++; revision++;
104 return structuredClone(organization);
105 },
106 ResolvePromptForTab: async (tabId: string, promptId: string, turnId: string, epoch: string) => { runtimeCalls.push({ method: "approve", tabId, promptId, turnId, epoch }); },
107 CancelSessionForTab: async (tabId: string) => { runtimeCalls.push({ method: "stop", tabId }); return { accepted: true, alreadyIdle: false, recoveryRequired: false }; },
108 GetProjectGroups: async () => ({ revision: 1, groups: [], applied: true }),
109 ListProjectGroups: async () => [],
110 ListTabs: async () => [],
111 RemoteConnectionStatuses: async () => [],
112 ListSessions: async () => sessions(),
113 ListHistorySessions: async () => ({ items: sessions(), nextCursor: "", revision, partial: false }),
114 GetSessionRecoveryVersions: async () => ({ items: [] }),
115 GetSessionActivityBaseline: async (selector: SessionSelector) => ({ ref: resolve(selector).session, complete: true, resultSequence: resolve(selector).resultSequence, eventVersion: "100", lifecycleGeneration: 1 }),
116 RenameTopic: async () => { throw new Error("single-session UI called forbidden topic bulk rename"); },
117 RenameSessionTarget: async (selector: SessionSelector, title: string) => {
118 const row = resolve(selector); row.label = title; revision++;
119 calls.push({ method: "rename", id: row.session!.sessionId, title });
120 host.emit("project-tree:changed-v2", { revision, roots: [root], reason: "metadata" });
121 host.emit("history-index:changed-v1", { revision, indexed: 2, total: 2, pending: 0 });
122 return { committed: true, operationId: `rename-${revision}`, lifecycleGeneration: 1, targetKey: `ref\x00local\x00${row.session!.sessionId}` };
123 },
124 ArchiveSessionTarget: async (selector: SessionSelector) => {
125 const row = resolve(selector);
126 if (row.session!.sessionId !== "b") throw new Error("archive targeted sibling A");
127 archived = true; revision++;
128 calls.push({ method: "archive", id: "b" });
129 return { committed: true, operationId: "archive-b", lifecycleGeneration: 2, targetKey: "ref\x00local\x00b" };
130 },
131 });
132 (window as any).__independentEvidence = { calls, rows, organizationCalls, organization, runtimeCalls, pageRequests };
133
134 function Fixture() {
135 const [selected, setSelected] = useState("a");
136 const [refresh, setRefresh] = useState(0);
137 const [mount, setMount] = useState(0);
138 const [variant, setVariant] = useState<"workbench" | "creation">("workbench");
139 const [history, setHistory] = useState<HistoryViewState | null>(null);
140 const [showApproval, setShowApproval] = useState(false);
141 const [activityStep, setActivityStep] = useState(0);
142 const target = { tabId: `tab-${selected}`, sessionKey: selected };
143 const resources = ["a", "b"].map(id => ({ tabId: `tab-${id}`, sessionKey: id }));
144 const operations = useSessionOperations({ visible: target, resources });
145 const promptCommands = useSessionPromptCommands({ target, approval: { id: `approval-${selected}`, tool: "bash" }, remote: false, goal: "", toolApprovalMode: "read-only", operations,
146 reportError: error => { throw error; }, ports: {
147 isPromptCurrentForTab: (tab, _kind, id) => id === `approval-${tab.replace("tab-", "")}`,
148 approveForTab: (tab, id, allow, session, persist) => { void resolvePromptForTab(app, tab, id, "approval", { allow, session, persist }, `turn-${tab}`, `epoch-${tab}`); setShowApproval(false); },
149 resolvePlanForTab: () => {}, resolveRecoveryForTab: () => {}, answerQuestionForTab: async () => {}, answerMCPForTab: () => {},
150 setCollaborationModeForTab: async () => {}, clearGoalForTab: async () => {}, setRemoteComposerProfile: async () => [],
151 patchComposerProfile: () => {}, notePlanMode: () => {}, drainRemoteApprovals: () => {}, rememberRevision: () => {},
152 } });
153 const controlCommands = useSessionControlCommands({ activeTabId: target.tabId, resources, operations, showToast: message => { throw new Error(message); }, clearWorkspaceConflict: () => {}, ports: {
154 cancel: async () => { throw new Error("stop must retain its source tab"); }, cancelForTab: (tab, items) => requestSessionCancel(app, tab, items),
155 acceptDelivery: async () => {}, disconnectRemote: async () => {}, cancelJobForTab: async () => false, refreshBackgroundRuntimes: async () => {},
156 } });
157 const row = rows.find(row => row.session?.sessionId === selected)!;
158 const commands = useProjectTopicCommands({
159 visible: { tabId: `tab-${selected}`, sessionKey: selected },
160 topic: { id: topicId, title: row.label, target: { kind: "local", topicId, selector: { ref: row.session } } },
161 ports: { ...desktopProjectAdapter, markChanged: () => setRefresh(value => value + 1), refreshTabs: async () => [{ id: `tab-${selected}` }], syncActive: async () => {} },
162 navigation: { openBlank: async () => {}, enqueue: async () => {}, switchFolder: async () => {} }, reportError: error => { throw error; },
163 });
164 const historyCommands = useHistoryCommands({ running: false, setHistView: setHistory,
165 ports: { listSessions: async () => sessions(), deleteSession: async () => {}, renameSession: async () => { throw new Error("ambiguous rename"); }, openPage: () => {} } });
166 return <div className={`app app--${variant}`} style={{ display: "block", height: "100vh" }}>
167 <div style={{ padding: 12, display: "flex", gap: 12 }}>
168 <button onClick={() => setHistory({ kind: "history", source: "all", sessions: sessions() })}>Open fixture history</button>
169 <button onClick={() => { document.documentElement.dataset.theme = "light"; }}>Light theme</button>
170 <button onClick={() => { document.documentElement.dataset.theme = "dark"; }}>Dark theme</button>
171 <button onClick={() => setVariant(value => value === "workbench" ? "creation" : "workbench")}>Toggle creation</button>
172 <button onClick={() => setMount(value => value + 1)}>Remount sidebar</button>
173 <button onClick={() => setShowApproval(true)}>Show selected runtime approval</button>
174 <button onClick={() => {
175 rows[1]!.resultSequence = activityStep === 1 ? 10 : 20;
176 setActivityStep(value => value + 1); revision++; setRefresh(value => value + 1);
177 host.emit("project-tree:changed-v2", { revision, roots: [root], reason: "metadata" });
178 setMount(value => value + 1);
179 }}>Advance B activity observation</button>
180 <button onClick={() => {
181 stale = true; revision++;
182 host.emit("project-tree:runtime-changed", { revision, topics: [{ scope: "project", workspaceRoot: root, node: { ...rows[1], runtimeOnly: true, running: true, status: "thinking" } }] });
183 setRefresh(value => value + 1);
184 }}>Inject late snapshots</button>
185 </div>
186 <TopicbarRegion view={{ automationReturn: false, automationReturnLabel: "", chromeHidden: false, brand: false,
187 sidebar: { title: "Sidebar", blocked: false, pressed: false, collapsed: false },
188 title: { text: row.label, hover: row.label, renameLabel: "Rename selected session", editing: commands.topicbarEditing, draft: commands.topicTitleDraft, canRename: true },
189 subtitle: { visible: false, title: "", mergeLabel: "", mergeTooltip: "" },
190 }} commands={{ openAutomation: () => {}, toggleSidebar: () => {}, setTitleDraft: commands.setTopicTitleDraft,
191 commitRename: commands.commitActiveTopicRename, cancelRename: commands.cancelActiveTopicRename,
192 startRename: commands.startActiveTopicRename, openWorktree: () => {} }}>{null}</TopicbarRegion>
193 <aside className={`sidebar sidebar--${variant}`} style={{ width: 360, height: "calc(100vh - 120px)", position: "relative" }}>
194 <ProjectTree key={mount} variant={variant} activeScope="project" activeWorkspaceRoot={root} activeTopicId={topicId} activeSessionPath={`session-id:${selected}`}
195 refreshSignal={refresh} onOpenTopic={async (_scope, _root, _topic, path) => { setSelected(path?.replace("session-id:", "") ?? "a"); }}
196 onAddProject={async () => {}} onTopicsChanged={() => setRefresh(value => value + 1)} />
197 </aside>
198 {showApproval && <div style={{ position: "fixed", bottom: 20, left: 400, right: 20 }}><ApprovalModal key={selected}
199 approval={{ id: `approval-${selected}`, tool: "bash", subject: `echo Session ${selected.toUpperCase()}`, fresh: true }}
200 tabId={target.tabId} onAnswer={promptCommands.handleApprovalAnswer} onStop={() => { void controlCommands.handleCancelActive(); setShowApproval(false); }} /></div>}
201 {history && <HistoryPanel sessions={history.sessions} running={false} onResume={s => { setSelected(s.sessionId!); setHistory(null); }}
202 onPreview={async () => []} onDelete={() => {}} onRename={historyCommands.onRenameHistorySession} onClose={() => setHistory(null)} />}
203 </div>;
204 }
205 createRoot(document.getElementById("root")!).render(<LocaleProvider><ToastProvider><Fixture /></ToastProvider></LocaleProvider>);
206
206 lines Plain Text