@spzxg/cli-ui 0.6.2 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/README.md +165 -19
- package/dist/cli-ui-embed-BAqonuoD.js +45952 -0
- package/dist/{cli-ui-index-BY5i3wId.js → cli-ui-index-b8wF7wrN.js} +2 -2
- package/dist/{cli-ui-jszip.min-m01fexHW.js → cli-ui-jszip.min-B0uLrCHg.js} +2 -2
- package/dist/{cli-ui-jszip.min-BZsUwI-s.js → cli-ui-jszip.min-ggP8OokP.js} +1 -1
- package/dist/{cli-ui-xlsx-CpWicJLT.js → cli-ui-xlsx-DEDdHrJJ.js} +1 -1
- package/dist/cli-ui.css +32 -1
- package/dist/cli-ui.js +1 -1
- package/package.json +17 -10
- package/src/board/BrowserPane.tsx +346 -115
- package/src/board/ChangesPane.tsx +2 -2
- package/src/board/CliBoard.tsx +79 -50
- package/src/board/FileViewer.tsx +54 -31
- package/src/board/FilesPane.tsx +32 -15
- package/src/board/PanelControls.tsx +28 -0
- package/src/board/blocks.tsx +5 -5
- package/src/board/context.ts +65 -0
- package/src/board/fileOps.ts +26 -3
- package/src/board/fileWatch.ts +94 -0
- package/src/board/folderDownload.ts +30 -14
- package/src/board/frameQuote.ts +2 -2
- package/src/board/panels.ts +33 -0
- package/src/board/textLoad.ts +9 -7
- package/src/board/workspace.ts +53 -14
- package/src/chat/CliChat.tsx +93 -40
- package/src/chat/Composer.tsx +44 -42
- package/src/chat/RunControls.tsx +99 -0
- package/src/chat/configuration.ts +15 -0
- package/src/chat/draft.ts +41 -0
- package/src/chat/export.ts +2 -2
- package/src/components.tsx +13 -0
- package/src/core/blocks.ts +17 -6
- package/src/core/callbacks.ts +8 -0
- package/src/core/client.ts +103 -19
- package/src/core/conversation.ts +4 -3
- package/src/core/id.ts +15 -0
- package/src/core/identity.ts +33 -0
- package/src/core/provider.tsx +31 -14
- package/src/core/store.ts +151 -53
- package/src/core/types.ts +64 -9
- package/src/elements.tsx +135 -29
- package/src/embed.ts +6 -8
- package/src/index.ts +24 -17
- package/src/layout/CliSplit.tsx +1 -1
- package/src/layout/split.ts +67 -30
- package/src/msglist/CliMsgList.tsx +19 -13
- package/src/operator/branchFlow.ts +35 -0
- package/src/operator/index.ts +9 -0
- package/src/operator/liveStream.ts +1 -0
- package/src/operator/node/AgentConnect.tsx +624 -0
- package/src/operator/node/AppearancePanel.tsx +35 -0
- package/src/operator/node/BranchHistory.tsx +67 -0
- package/src/operator/node/BranchNavigator.tsx +36 -0
- package/src/operator/node/BrowserPane.tsx +128 -0
- package/src/operator/node/BrowserPopup.tsx +22 -0
- package/src/operator/node/Chat.tsx +517 -0
- package/src/operator/node/CommandPalette.tsx +213 -0
- package/src/operator/node/Composer.tsx +287 -0
- package/src/operator/node/InsightsView.tsx +831 -0
- package/src/operator/node/Layout.tsx +190 -0
- package/src/operator/node/Markdown.tsx +61 -0
- package/src/operator/node/Modal.tsx +179 -0
- package/src/operator/node/NeedsYouMenu.tsx +54 -0
- package/src/operator/node/OnboardingWizard.tsx +250 -0
- package/src/operator/node/Orchestrator.tsx +634 -0
- package/src/operator/node/ProjectLanding.tsx +86 -0
- package/src/operator/node/ProjectsColumn.tsx +137 -0
- package/src/operator/node/ProviderAuth.tsx +561 -0
- package/src/operator/node/RunControls.tsx +2 -0
- package/src/operator/node/RunSwitch.tsx +113 -0
- package/src/operator/node/Services.tsx +245 -0
- package/src/operator/node/SessionRail.tsx +260 -0
- package/src/operator/node/SessionView.tsx +288 -0
- package/src/operator/node/SettingsView.tsx +690 -0
- package/src/operator/node/TaskBoard.tsx +352 -0
- package/src/operator/node/TaskChanges.tsx +530 -0
- package/src/operator/node/TaskHero.tsx +50 -0
- package/src/operator/node/TasksColumn.tsx +200 -0
- package/src/operator/node/Terminal.tsx +143 -0
- package/src/operator/node/Transcript.tsx +468 -0
- package/src/operator/node/ViewFile.tsx +79 -0
- package/src/operator/node/Welcome.tsx +101 -0
- package/src/operator/node/WorktreePrune.tsx +333 -0
- package/src/operator/node/agents.ts +58 -0
- package/src/operator/node/api.ts +40 -0
- package/src/operator/node/boardBackend.ts +82 -0
- package/src/operator/node/boardEvents.ts +22 -0
- package/src/operator/node/boardInteraction.ts +15 -0
- package/src/operator/node/boardState.ts +57 -0
- package/src/operator/node/boardWorkspace.ts +12 -0
- package/src/operator/node/branchFlow.ts +1 -0
- package/src/operator/node/composerDraft.ts +52 -0
- package/src/operator/node/fileLinks.ts +81 -0
- package/src/operator/node/format.ts +290 -0
- package/src/operator/node/github.tsx +234 -0
- package/src/operator/node/icons.tsx +88 -0
- package/src/operator/node/liveStream.ts +270 -0
- package/src/operator/node/modals.tsx +720 -0
- package/src/operator/node/navHistory.ts +70 -0
- package/src/operator/node/persist.ts +26 -0
- package/src/operator/node/protocol/agents/optIn.ts +9 -0
- package/src/operator/node/protocol/agents/types.ts +394 -0
- package/src/operator/node/protocol/approvalFailure.ts +77 -0
- package/src/operator/node/protocol/authFailure.ts +66 -0
- package/src/operator/node/protocol/customModels.ts +23 -0
- package/src/operator/node/protocol/events.ts +13 -0
- package/src/operator/node/protocol/features.ts +70 -0
- package/src/operator/node/protocol/internalUsage.ts +1 -0
- package/src/operator/node/protocol/promptLimits.ts +52 -0
- package/src/operator/node/protocol/storageRetention.ts +13 -0
- package/src/operator/node/protocol/types.ts +332 -0
- package/src/operator/node/protocol/usageLimit.ts +72 -0
- package/src/operator/node/shared.tsx +89 -0
- package/src/operator/node/transcriptModel.ts +129 -0
- package/src/operator/node/types.ts +296 -0
- package/src/operator/node/useGlobalEvents.ts +97 -0
- package/src/operator/node/useOrchestrator.ts +696 -0
- package/src/operator/node/usePrefs.ts +114 -0
- package/src/operator/node/useRecaps.ts +91 -0
- package/src/operator/node/useTaskStream.ts +280 -0
- package/src/operator.base.css +1806 -0
- package/src/operator.css +194 -16
- package/src/styles.css +49 -1
- package/src/ui/Markdown.tsx +7 -2
- package/src/ui/Popover.tsx +82 -0
- package/src/ui/TextBlock.tsx +3 -3
- package/src/ui/Transcript.tsx +17 -10
- package/src/ui/fileLinks.ts +6 -0
- package/src/ui/format.ts +1 -1
- package/src/ui/shared.tsx +1 -1
- package/src/ui/transcriptModel.ts +2 -2
- package/dist/cli-ui-embed-6IJ_jRVh.js +0 -41223
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef } from "react";
|
|
4
|
+
import { Icon } from "./icons";
|
|
5
|
+
import { TerminalView, type TermApi } from "./Terminal";
|
|
6
|
+
import { PROJ_W, TASK_W, DEFAULT_LAYOUT } from "./types";
|
|
7
|
+
import { useOrchestrator } from "./useOrchestrator";
|
|
8
|
+
import { ProjectsColumn } from "./ProjectsColumn";
|
|
9
|
+
import { CliMsgList, CliChat } from "../../components";
|
|
10
|
+
import type { ComponentProps } from "react";
|
|
11
|
+
import type { TasksColumn as NodeTasksColumn } from "./TasksColumn";
|
|
12
|
+
import type { SessionView as NodeSessionView } from "./SessionView";
|
|
13
|
+
const TasksColumn=(props:ComponentProps<typeof NodeTasksColumn>)=><CliMsgList operator={props}/>;
|
|
14
|
+
const SessionView=(props:ComponentProps<typeof NodeSessionView>)=><CliChat operator={props}/>;
|
|
15
|
+
import { BoardWorkspace } from "./TaskBoard";
|
|
16
|
+
|
|
17
|
+
import { ProjectLanding } from "./ProjectLanding";
|
|
18
|
+
import { SettingsView } from "./SettingsView";
|
|
19
|
+
import { InsightsView } from "./InsightsView";
|
|
20
|
+
import { AppearancePanel } from "./AppearancePanel";
|
|
21
|
+
import { ColResize, ColRail, TerminalDrawer, BootSkeleton } from "./Layout";
|
|
22
|
+
import { ServicesDrawer } from "./Services";
|
|
23
|
+
import { clientFeatures } from "./protocol/features";
|
|
24
|
+
import { NewTaskModal, EditTaskModal, ContextModal, NewProjectModal, SessionsModal } from "./modals";
|
|
25
|
+
import { OnboardingWizard } from "./OnboardingWizard";
|
|
26
|
+
import { AgentNudge, AgentAuthBanner } from "./AgentConnect";
|
|
27
|
+
import { WelcomeCoach, WelcomeNudge } from "./Welcome";
|
|
28
|
+
import { Popover } from "./shared";
|
|
29
|
+
import { NeedsYouMenu } from "./NeedsYouMenu";
|
|
30
|
+
import { CommandPalette, type PaletteCommand } from "./CommandPalette";
|
|
31
|
+
|
|
32
|
+
// Below this width the three columns can't coexist, so the workspace collapses to
|
|
33
|
+
// one pane at a time (projects → tasks → session) with back affordances. matchMedia
|
|
34
|
+
// keeps it in sync with rotation/resize; SSR renders the desktop layout (false) and
|
|
35
|
+
// the effect corrects on mount — selection state alone drives which pane shows.
|
|
36
|
+
const MOBILE_QUERY = "(max-width: 760px)";
|
|
37
|
+
function useIsMobile() {
|
|
38
|
+
const [mobile, setMobile] = useState(false);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const mq = window.matchMedia(MOBILE_QUERY);
|
|
41
|
+
const sync = () => setMobile(mq.matches);
|
|
42
|
+
sync();
|
|
43
|
+
mq.addEventListener("change", sync);
|
|
44
|
+
return () => mq.removeEventListener("change", sync);
|
|
45
|
+
}, []);
|
|
46
|
+
return mobile;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Phone terminal: a full-screen sheet (vs. the cramped desktop bottom-drawer) so
|
|
50
|
+
// output is actually legible. It's a read-mostly surface — glancing at a dev
|
|
51
|
+
// server, reading an error, pasting the Claude login code, tapping the OAuth URL
|
|
52
|
+
// — not a place to hand-type code, so input is just the few buttons people need.
|
|
53
|
+
function MobileTerminalSheet({ cwd, port, visible, onClose }: { cwd: string; port?: number; visible: boolean; onClose: () => void }) {
|
|
54
|
+
const [epoch, setEpoch] = useState(0); // bump → fresh shell
|
|
55
|
+
const [fontSize, setFontSize] = useState(13);
|
|
56
|
+
const apiRef = useRef<TermApi | null>(null);
|
|
57
|
+
const sheetRef = useRef<HTMLDivElement>(null);
|
|
58
|
+
|
|
59
|
+
// Pin the sheet to the *visual* viewport so the on-screen keyboard pushes the
|
|
60
|
+
// button-bar and output up rather than covering them. visualViewport shrinks
|
|
61
|
+
// when the keyboard opens; falling back to 100% when it's unavailable.
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const vv = window.visualViewport;
|
|
64
|
+
if (!vv || !visible) return;
|
|
65
|
+
const apply = () => { if (sheetRef.current) sheetRef.current.style.height = `${vv.height}px`; };
|
|
66
|
+
apply();
|
|
67
|
+
vv.addEventListener("resize", apply);
|
|
68
|
+
vv.addEventListener("scroll", apply);
|
|
69
|
+
return () => { vv.removeEventListener("resize", apply); vv.removeEventListener("scroll", apply); };
|
|
70
|
+
}, [visible]);
|
|
71
|
+
|
|
72
|
+
const send = (d: string) => apiRef.current?.send(d);
|
|
73
|
+
const paste = async () => {
|
|
74
|
+
try { const t = await navigator.clipboard.readText(); if (t) send(t); } catch { /* clipboard blocked — long-press paste still works */ }
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div ref={sheetRef} className={`mterm${visible ? "" : " hidden"}`}>
|
|
79
|
+
<div className="mterm-bar">
|
|
80
|
+
{Icon.terminal()}
|
|
81
|
+
<span className="mterm-cwd">{cwd || "~ (no working dir)"}</span>
|
|
82
|
+
<span style={{ flex: 1 }} />
|
|
83
|
+
<button className="icon-btn" onClick={() => setFontSize((f) => Math.max(9, f - 1))} title="缩小文字" aria-label="缩小文字">A−</button>
|
|
84
|
+
<button className="icon-btn" onClick={() => setFontSize((f) => Math.min(22, f + 1))} title="放大文字" aria-label="放大文字">A+</button>
|
|
85
|
+
<button className="icon-btn" onClick={() => setEpoch((e) => e + 1)} title="重启 shell">{Icon.clear()}</button>
|
|
86
|
+
<button className="icon-btn" onClick={onClose} title="关闭终端(shell 会继续运行)">{Icon.x()}</button>
|
|
87
|
+
</div>
|
|
88
|
+
<TerminalView key={epoch} cwd={cwd} port={port} fontSize={fontSize} onReady={(api) => { apiRef.current = api; }} />
|
|
89
|
+
<div className="mterm-keys">
|
|
90
|
+
<button className="mtk" onClick={paste}>粘贴</button>
|
|
91
|
+
<span style={{ flex: 1 }} />
|
|
92
|
+
<button className="mtk" onClick={() => send("\x03")} title="发送 Ctrl-C">Ctrl-C</button>
|
|
93
|
+
<button className="mtk mtk-enter" onClick={() => send("\r")}>⏎ Enter</button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default function Orchestrator({ board }: { board?: import("./boardInteraction").OperatorBoardOptions } = {}) {
|
|
100
|
+
const o = useOrchestrator();
|
|
101
|
+
const { project, task, selProj, selTask, layout } = o;
|
|
102
|
+
const isMobile = useIsMobile();
|
|
103
|
+
const features = clientFeatures();
|
|
104
|
+
const isDark = o.appearance.theme !== "light";
|
|
105
|
+
const [workspaceMenu,setWorkspaceMenu]=useState(false);
|
|
106
|
+
const [needsYouOpen, setNeedsYouOpen] = useState(false);
|
|
107
|
+
// Which Settings section to land on when opened programmatically (e.g. the
|
|
108
|
+
// "connect another agent" nudge deep-links to Agents). undefined = default.
|
|
109
|
+
const [settingsSection, setSettingsSection] = useState<string | undefined>();
|
|
110
|
+
const openSettings = (sect?: string) => { setSettingsSection(sect); o.setView("settings"); };
|
|
111
|
+
// Drop the open flag if the pill itself disappears (count → 0), so it doesn't
|
|
112
|
+
// silently re-open when a task next starts waiting.
|
|
113
|
+
useEffect(() => { if (o.needsYouTotal === 0) setNeedsYouOpen(false); }, [o.needsYouTotal]);
|
|
114
|
+
|
|
115
|
+
// ⌘K / Ctrl-K command palette. Same flag as the top-bar omni button, so
|
|
116
|
+
// re-enabling the feature turns on both the visual affordance and the shortcut.
|
|
117
|
+
const [paletteOpen, setPaletteOpen] = useState(false);
|
|
118
|
+
const [clearRequest, setClearRequest] = useState<string | null>(null);
|
|
119
|
+
useEffect(() => setClearRequest(null), [selTask]);
|
|
120
|
+
const requestClear = (taskId: string) => setClearRequest(taskId);
|
|
121
|
+
const confirmClear = () => {
|
|
122
|
+
if (!clearRequest) return;
|
|
123
|
+
const taskId = clearRequest;
|
|
124
|
+
setClearRequest(null);
|
|
125
|
+
void o.clearSession(taskId);
|
|
126
|
+
};
|
|
127
|
+
const omniEnabled = features.omniSearch && !isMobile;
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (!omniEnabled) return;
|
|
130
|
+
const onKey = (e: KeyboardEvent) => {
|
|
131
|
+
if ((e.metaKey || e.ctrlKey) && !e.altKey && e.key.toLowerCase() === "k") {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
setPaletteOpen((v) => !v);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
window.addEventListener("keydown", onKey);
|
|
137
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
138
|
+
}, [omniEnabled]);
|
|
139
|
+
|
|
140
|
+
// Board mode (desktop): the board replaces BOTH the tasks column and the
|
|
141
|
+
// session pane — it owns everything right of the projects sidebar. On mobile
|
|
142
|
+
// the board still renders inside the tasks pane (single-pane navigation).
|
|
143
|
+
const boardMode = o.taskView === "board" && !isMobile && o.view === "workspace" && !!project;
|
|
144
|
+
// The slide-over session panel opens only from an explicit card click — the
|
|
145
|
+
// app's auto-selection paths (landing on a project picks its first task)
|
|
146
|
+
// just highlight the card, they don't pop a panel over the board.
|
|
147
|
+
const [boardPanel, setBoardPanel] = useState(false);
|
|
148
|
+
const openBoardTask = (id: string) => { o.setSelTask(id); setBoardPanel(true); };
|
|
149
|
+
const closeBoardPanel = () => { setBoardPanel(false); o.setSelTask(null); };
|
|
150
|
+
const setTaskView = (v: "list" | "board") => {
|
|
151
|
+
if (v === "board") setBoardPanel(false);
|
|
152
|
+
o.setTaskView(v);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// ⌘⇧B — flip list/board (sticky, same pref the header toggles write).
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
const onKey = (e: KeyboardEvent) => {
|
|
158
|
+
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "b") {
|
|
159
|
+
e.preventDefault();
|
|
160
|
+
setTaskView(o.taskView === "board" ? "list" : "board");
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
window.addEventListener("keydown", onKey);
|
|
164
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Esc closes the board's slide-over session panel (back to the full board).
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (!boardMode || !task || !boardPanel) return;
|
|
170
|
+
const onKey = (e: KeyboardEvent) => {
|
|
171
|
+
if (e.key === "Escape" && !paletteOpen) { e.preventDefault(); closeBoardPanel(); }
|
|
172
|
+
};
|
|
173
|
+
window.addEventListener("keydown", onKey);
|
|
174
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Server-side time-in-app + retention pulse: ping on load, then every 2 min
|
|
178
|
+
// while the tab is open. The route turns the first ping (or one after a long
|
|
179
|
+
// gap) into app_opened and the rest into heartbeat (see app/api/heartbeat).
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
const beat = () => { void fetch("/api/heartbeat", { method: "POST", keepalive: true }).catch(() => {}); };
|
|
182
|
+
beat();
|
|
183
|
+
const iv = setInterval(() => { if (!document.hidden) beat(); }, 120_000);
|
|
184
|
+
return () => clearInterval(iv);
|
|
185
|
+
}, []);
|
|
186
|
+
|
|
187
|
+
// On a phone only one of these is mounted at a time; on desktop the same
|
|
188
|
+
// elements sit side by side. Which pane shows is derived purely from the
|
|
189
|
+
// selection state, so the titlebar "needs you" pill (which drives selection)
|
|
190
|
+
// navigates correctly from any level.
|
|
191
|
+
const mobilePane: "projects" | "tasks" | "session" | "settings" | "insights" =
|
|
192
|
+
o.view === "settings" ? "settings" : o.view === "insights" ? "insights" : !project ? "projects" : !task ? "tasks" : "session";
|
|
193
|
+
|
|
194
|
+
const projectsColumn = (
|
|
195
|
+
<ProjectsColumn
|
|
196
|
+
mobile={isMobile}
|
|
197
|
+
width={layout.projW} onCollapse={() => o.setLayout((l) => ({ ...l, projCollapsed: true }))}
|
|
198
|
+
projects={o.activeProjects} deprecated={o.deprecatedProjects} agents={o.agents.agents} selId={selProj} running={o.running}
|
|
199
|
+
onSelect={o.selectProject} onNew={() => o.setModal("project")} onOpenAppearance={() => o.setAppearanceOpen((t) => !t)}
|
|
200
|
+
onReorder={o.reorderProjects} onRestore={(id) => o.setDeprecated(id, false)}
|
|
201
|
+
settingsActive={o.view === "settings"} onOpenSettings={() => openSettings()}
|
|
202
|
+
/>
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const tasksColumn = project && (
|
|
206
|
+
<TasksColumn
|
|
207
|
+
mobile={isMobile}
|
|
208
|
+
onBack={isMobile ? () => window.history.back() : undefined}
|
|
209
|
+
width={layout.taskW}
|
|
210
|
+
onCollapse={() => o.setLayout((l) => ({ ...l, taskCollapsed: true }))}
|
|
211
|
+
project={project} agents={o.agents} tasks={o.realTasks} suggested={o.suggested} selTaskId={selTask} running={o.running} blockedBy={o.blockedBy}
|
|
212
|
+
loading={o.tasksLoading}
|
|
213
|
+
view={o.taskView} onSetView={setTaskView} onMoveTask={o.moveTask}
|
|
214
|
+
onSelectTask={o.setSelTask} onNewTask={() => o.setModal("task")} onEditContext={() => o.setModal("context")}
|
|
215
|
+
onShowSessions={() => o.setModal("sessions")} onShowRecap={() => o.setSelTask(null)} onEditTask={o.setEditId}
|
|
216
|
+
onStartSuggestion={o.startSuggestion} onAcceptSuggestion={o.acceptSuggestion} onDismissSuggestion={o.dismissSuggestion}
|
|
217
|
+
/>
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const sessionColumn = (
|
|
221
|
+
<div className="col col-session">
|
|
222
|
+
{project?.seeded === 1 && !isMobile && <WelcomeCoach />}
|
|
223
|
+
<div className="session-body">
|
|
224
|
+
{task && project ? (
|
|
225
|
+
<SessionView board={board}
|
|
226
|
+
key={task.id}
|
|
227
|
+
mobile={isMobile}
|
|
228
|
+
onBack={isMobile ? () => window.history.back() : undefined}
|
|
229
|
+
project={project} task={task} tasks={o.realTasks} onSelectTask={o.setSelTask} onFork={o.forkFromTasks} agents={o.agents} messages={o.messages} running={o.running.has(task.id)} blockedBy={o.blockedBy.get(task.id)}
|
|
230
|
+
transcriptLoading={o.transcriptLoading} older={o.olderCount} onLoadOlder={o.loadOlderMessages}
|
|
231
|
+
onSend={(text) => o.runTurn(task.id, text, false)}
|
|
232
|
+
onStart={() => o.runTurn(task.id, "", true)}
|
|
233
|
+
onStop={() => o.stopTurn(task.id)}
|
|
234
|
+
onClear={() => requestClear(task.id)} clearConfirming={clearRequest === task.id} onConfirmClear={confirmClear} onCancelClear={() => setClearRequest(null)} onEdit={() => o.setEditId(task.id)}
|
|
235
|
+
onReconnect={() => openSettings("agents")}
|
|
236
|
+
onSetStatus={o.setStatus} onSetPriority={o.setPriority} onSetModel={o.setModel} onSetAgent={o.setAgent}
|
|
237
|
+
onSetReasoning={o.setReasoning} onSetPermission={o.setPermission} onSetSendContext={o.setSendContext}
|
|
238
|
+
onResolveWithAI={o.resolveConflictsWithAI}
|
|
239
|
+
onMerged={o.onMerged}
|
|
240
|
+
onPrCreated={o.onPrCreated}
|
|
241
|
+
onAnswer={(askId, questions, answers) => o.answerQuestion(task.id, askId, questions, answers)}
|
|
242
|
+
onCancelQueued={(pendingId) => o.cancelQueued(task.id, pendingId)}
|
|
243
|
+
railW={layout.railW}
|
|
244
|
+
onRailWidth={(w) => o.setLayout((l) => ({ ...l, railW: w }))}
|
|
245
|
+
onRailReset={() => o.setLayout((l) => ({ ...l, railW: DEFAULT_LAYOUT.railW }))}
|
|
246
|
+
railCollapsed={layout.railCollapsed}
|
|
247
|
+
onRailCollapse={() => o.setLayout((l) => ({ ...l, railCollapsed: true }))}
|
|
248
|
+
onRailExpand={() => o.setLayout((l) => ({ ...l, railCollapsed: false }))}
|
|
249
|
+
/>
|
|
250
|
+
) : project ? (
|
|
251
|
+
<ProjectLanding
|
|
252
|
+
project={project}
|
|
253
|
+
recap={o.recaps[project.id]}
|
|
254
|
+
onNewTask={() => o.setModal("task")}
|
|
255
|
+
onRefreshRecap={() => o.fetchRecap(project.id, true)}
|
|
256
|
+
/>
|
|
257
|
+
) : (
|
|
258
|
+
<div className="empty" style={{ margin: "auto" }}>
|
|
259
|
+
<div className="e-ic">{Icon.bolt()}</div>
|
|
260
|
+
<div className="e-t">未选择任务</div>
|
|
261
|
+
<div className="e-s">创建一个任务来启动智能体会话。</div>
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
{project && features.services && o.servicesMounted && !isMobile && (
|
|
266
|
+
<ServicesDrawer
|
|
267
|
+
key={`svc-${project.id}`}
|
|
268
|
+
projectId={project.id}
|
|
269
|
+
hasConfig={!!(project.dev_command || project.setup_command || project.test_command)}
|
|
270
|
+
visible={o.servicesOpen}
|
|
271
|
+
height={o.servicesHeight}
|
|
272
|
+
onClose={() => o.setServicesOpen(false)}
|
|
273
|
+
onResize={o.setServicesHeight}
|
|
274
|
+
/>
|
|
275
|
+
)}
|
|
276
|
+
{project && o.termMounted && !isMobile && (
|
|
277
|
+
<TerminalDrawer
|
|
278
|
+
key={project.id}
|
|
279
|
+
cwd={project.repo_path}
|
|
280
|
+
taskDir={task?.worktree_path || undefined}
|
|
281
|
+
taskTitle={task?.title}
|
|
282
|
+
port={project.port}
|
|
283
|
+
visible={o.termOpen}
|
|
284
|
+
height={o.termHeight}
|
|
285
|
+
onClose={() => o.setTermOpen(false)}
|
|
286
|
+
onResize={o.setTermHeight}
|
|
287
|
+
/>
|
|
288
|
+
)}
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// Board mode's full-workspace surface: header + board, with the slide-over
|
|
293
|
+
// session panel and the project drawers mounted on top (so the titlebar's
|
|
294
|
+
// Services/Terminal toggles keep working while the board is up).
|
|
295
|
+
const boardWorkspace = project && (
|
|
296
|
+
<BoardWorkspace
|
|
297
|
+
project={project} agents={o.agents} tasks={o.realTasks} suggested={o.suggested}
|
|
298
|
+
selTaskId={selTask} running={o.running} blockedBy={o.blockedBy} loading={o.tasksLoading}
|
|
299
|
+
onSetView={setTaskView} onMoveTask={o.moveTask}
|
|
300
|
+
onSelectTask={openBoardTask} onNewTask={() => o.setModal("task")} onEditContext={() => o.setModal("context")}
|
|
301
|
+
onShowSessions={() => o.setModal("sessions")} onEditTask={o.setEditId}
|
|
302
|
+
onStartSuggestion={o.startSuggestion} onAcceptSuggestion={o.acceptSuggestion} onDismissSuggestion={o.dismissSuggestion}
|
|
303
|
+
>
|
|
304
|
+
{task && boardPanel && (
|
|
305
|
+
<>
|
|
306
|
+
<div className="bpanel-scrim" onClick={closeBoardPanel} />
|
|
307
|
+
<div className="bpanel">
|
|
308
|
+
<div className="bpanel-bar">
|
|
309
|
+
<button className="icon-btn" title="展开为列表 + 对话布局" onClick={() => o.setTaskView("list")}>{Icon.external()}</button>
|
|
310
|
+
<span className="bp-hint">按 esc 返回看板</span>
|
|
311
|
+
<span className="spacer" />
|
|
312
|
+
<button className="icon-btn" title="返回看板" onClick={closeBoardPanel}>{Icon.x()}</button>
|
|
313
|
+
</div>
|
|
314
|
+
<div className="session-body">
|
|
315
|
+
<SessionView board={board}
|
|
316
|
+
key={task.id}
|
|
317
|
+
project={project} task={task} tasks={o.realTasks} onSelectTask={o.setSelTask} onFork={o.forkFromTasks} agents={o.agents} messages={o.messages} running={o.running.has(task.id)} blockedBy={o.blockedBy.get(task.id)}
|
|
318
|
+
transcriptLoading={o.transcriptLoading} older={o.olderCount} onLoadOlder={o.loadOlderMessages}
|
|
319
|
+
onSend={(text) => o.runTurn(task.id, text, false)}
|
|
320
|
+
onStart={() => o.runTurn(task.id, "", true)}
|
|
321
|
+
onStop={() => o.stopTurn(task.id)}
|
|
322
|
+
onClear={() => requestClear(task.id)} clearConfirming={clearRequest === task.id} onConfirmClear={confirmClear} onCancelClear={() => setClearRequest(null)} onEdit={() => o.setEditId(task.id)}
|
|
323
|
+
onReconnect={() => openSettings("agents")}
|
|
324
|
+
onSetStatus={o.setStatus} onSetPriority={o.setPriority} onSetModel={o.setModel} onSetAgent={o.setAgent}
|
|
325
|
+
onSetReasoning={o.setReasoning} onSetPermission={o.setPermission} onSetSendContext={o.setSendContext}
|
|
326
|
+
onResolveWithAI={o.resolveConflictsWithAI}
|
|
327
|
+
onMerged={o.onMerged}
|
|
328
|
+
onPrCreated={o.onPrCreated}
|
|
329
|
+
onAnswer={(askId, questions, answers) => o.answerQuestion(task.id, askId, questions, answers)}
|
|
330
|
+
onCancelQueued={(pendingId) => o.cancelQueued(task.id, pendingId)}
|
|
331
|
+
railW={layout.railW}
|
|
332
|
+
onRailWidth={(w) => o.setLayout((l) => ({ ...l, railW: w }))}
|
|
333
|
+
onRailReset={() => o.setLayout((l) => ({ ...l, railW: DEFAULT_LAYOUT.railW }))}
|
|
334
|
+
railCollapsed={layout.railCollapsed}
|
|
335
|
+
onRailCollapse={() => o.setLayout((l) => ({ ...l, railCollapsed: true }))}
|
|
336
|
+
onRailExpand={() => o.setLayout((l) => ({ ...l, railCollapsed: false }))}
|
|
337
|
+
/>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</>
|
|
341
|
+
)}
|
|
342
|
+
{features.services && o.servicesMounted && (
|
|
343
|
+
<ServicesDrawer
|
|
344
|
+
key={`svc-${project.id}`}
|
|
345
|
+
projectId={project.id}
|
|
346
|
+
hasConfig={!!(project.dev_command || project.setup_command || project.test_command)}
|
|
347
|
+
visible={o.servicesOpen}
|
|
348
|
+
height={o.servicesHeight}
|
|
349
|
+
onClose={() => o.setServicesOpen(false)}
|
|
350
|
+
onResize={o.setServicesHeight}
|
|
351
|
+
/>
|
|
352
|
+
)}
|
|
353
|
+
{o.termMounted && (
|
|
354
|
+
<TerminalDrawer
|
|
355
|
+
key={project.id}
|
|
356
|
+
cwd={project.repo_path}
|
|
357
|
+
taskDir={task?.worktree_path || undefined}
|
|
358
|
+
taskTitle={task?.title}
|
|
359
|
+
port={project.port}
|
|
360
|
+
visible={o.termOpen}
|
|
361
|
+
height={o.termHeight}
|
|
362
|
+
onClose={() => o.setTermOpen(false)}
|
|
363
|
+
onResize={o.setTermHeight}
|
|
364
|
+
/>
|
|
365
|
+
)}
|
|
366
|
+
</BoardWorkspace>
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
const insightsColumn = (
|
|
370
|
+
<InsightsView agents={o.agents} onClose={() => o.setView("workspace")} onOpenSettings={openSettings} />
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
const settingsColumn = (
|
|
374
|
+
<SettingsView
|
|
375
|
+
key={settingsSection ?? "default"}
|
|
376
|
+
settings={o.settings}
|
|
377
|
+
setSetting={o.setSetting}
|
|
378
|
+
appDefaults={o.appDefaults}
|
|
379
|
+
setAppDefault={o.setAppDefault}
|
|
380
|
+
agents={o.agents}
|
|
381
|
+
onAgentsRefresh={o.refreshAgents}
|
|
382
|
+
onReset={o.resetSettings}
|
|
383
|
+
onRerunSetup={o.rerunOnboarding}
|
|
384
|
+
onClose={() => o.setView("workspace")}
|
|
385
|
+
initialSection={settingsSection}
|
|
386
|
+
/>
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
<div className={`cli-ui app${isMobile ? " mobile" : ""}`} data-theme={o.appearance.theme}>
|
|
391
|
+
<div className="titlebar">
|
|
392
|
+
<div className="tb-left">
|
|
393
|
+
<div className="tb-logo" title="Operator">
|
|
394
|
+
<span className="tb-ring"><span className="tb-core" /><span className="tb-arc" /></span>
|
|
395
|
+
<span className="tb-word">OPERATOR</span>
|
|
396
|
+
</div>
|
|
397
|
+
{!isMobile && (
|
|
398
|
+
<>
|
|
399
|
+
<span className="tb-div" />
|
|
400
|
+
<div className="tb-crumb">
|
|
401
|
+
<span className="cz">fleet</span><span className="cs">/</span>
|
|
402
|
+
<span className="cn">{o.view === "insights" ? "insights" : project ? project.name : "—"}</span>
|
|
403
|
+
</div>
|
|
404
|
+
</>
|
|
405
|
+
)}
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
{omniEnabled && (
|
|
409
|
+
<button className="tb-omni" onClick={() => setPaletteOpen(true)} title="命令面板 —— 跳转到项目、会话或命令">
|
|
410
|
+
<span className="omni-ic">{Icon.search()}</span>
|
|
411
|
+
<span className="omni-txt">跳转到项目、会话或命令…</span>
|
|
412
|
+
<span className="omni-k">⌘K</span>
|
|
413
|
+
</button>
|
|
414
|
+
)}
|
|
415
|
+
|
|
416
|
+
<div className="tb-right">
|
|
417
|
+
{o.needsYouTotal > 0 && (
|
|
418
|
+
<div style={{ position: "relative" }}>
|
|
419
|
+
<button
|
|
420
|
+
className="needs-you-pill"
|
|
421
|
+
onClick={(e) => { e.stopPropagation(); setNeedsYouOpen((v) => !v); }}
|
|
422
|
+
title="选择一个等待你处理的任务"
|
|
423
|
+
>
|
|
424
|
+
<span className="ny-dot" />
|
|
425
|
+
{o.needsYouTotal} 项待处理
|
|
426
|
+
</button>
|
|
427
|
+
{needsYouOpen && (
|
|
428
|
+
<NeedsYouMenu
|
|
429
|
+
onJump={(projectId, taskId) => o.goToTask(projectId, taskId)}
|
|
430
|
+
onClose={() => setNeedsYouOpen(false)}
|
|
431
|
+
/>
|
|
432
|
+
)}
|
|
433
|
+
</div>
|
|
434
|
+
)}
|
|
435
|
+
{isMobile && <div style={{ position: "relative" }}>
|
|
436
|
+
<button className="tb-icon workspace-menu" aria-label="工作区菜单" aria-expanded={workspaceMenu} onClick={(e) => { e.stopPropagation(); setWorkspaceMenu(!workspaceMenu); }}>{Icon.dots()}</button>
|
|
437
|
+
{workspaceMenu && <Popover onClose={() => setWorkspaceMenu(false)}>
|
|
438
|
+
<button className="pop-item" onClick={() => { setWorkspaceMenu(false); openSettings(); }}>{Icon.gear()} 设置</button>
|
|
439
|
+
<button className="pop-item" onClick={() => { setWorkspaceMenu(false); o.setAppearance("theme", isDark ? "light" : "dark"); }}>{isDark ? Icon.sun() : Icon.moon()} 切换主题</button>
|
|
440
|
+
<button className="pop-item" disabled={!project} onClick={() => { setWorkspaceMenu(false); o.setTermMounted(true); o.setTermOpen(true); }}>{Icon.terminal()} 项目终端</button>
|
|
441
|
+
{features.services && <button className="pop-item" disabled={!project} onClick={()=>{setWorkspaceMenu(false);o.setServicesMounted(true);o.setServicesOpen(true);}}>{Icon.sliders()} 项目服务</button>}
|
|
442
|
+
<button className="pop-item" onClick={()=>{setWorkspaceMenu(false);o.setAppearanceOpen(true);}}>{Icon.sliders()} 外观设置</button>
|
|
443
|
+
<button className="pop-item" onClick={() => { setWorkspaceMenu(false); o.setView("insights"); }}>{Icon.chart()} 用量洞察</button>
|
|
444
|
+
</Popover>}
|
|
445
|
+
</div>}
|
|
446
|
+
|
|
447
|
+
<div className="tb-actions">
|
|
448
|
+
{features.services && (
|
|
449
|
+
<button
|
|
450
|
+
className={`tb-btn${o.servicesOpen ? " on" : ""}`}
|
|
451
|
+
disabled={!project}
|
|
452
|
+
title={project ? "开关该项目的托管服务(开发服务器、初始化、测试)" : "请先选择一个项目"}
|
|
453
|
+
onClick={() => { if (!project) return; o.setServicesMounted(true); o.setServicesOpen((s) => !s); }}
|
|
454
|
+
>
|
|
455
|
+
{Icon.sliders()} 服务
|
|
456
|
+
</button>
|
|
457
|
+
)}
|
|
458
|
+
<button
|
|
459
|
+
className={`tb-btn${o.termOpen ? " on" : ""}`}
|
|
460
|
+
disabled={!project}
|
|
461
|
+
title={project ? "开关终端(项目工作目录 —— 可在抽屉中切换到所选任务的 worktree)" : "请先选择一个项目"}
|
|
462
|
+
onClick={() => { if (!project) return; o.setTermMounted(true); o.setTermOpen((t) => !t); }}
|
|
463
|
+
>
|
|
464
|
+
{Icon.terminal()} 终端
|
|
465
|
+
</button>
|
|
466
|
+
<button className="tb-btn" onClick={() => o.setAppearanceOpen((t) => !t)} title="外观">{Icon.sliders()} 外观</button>
|
|
467
|
+
</div>
|
|
468
|
+
|
|
469
|
+
<button
|
|
470
|
+
className={`tb-icon${o.view === "insights" ? " on" : ""}`}
|
|
471
|
+
title="洞察 —— 花费、token、已交付任务、已合并代码" aria-label="洞察"
|
|
472
|
+
onClick={() => o.setView(o.view === "insights" ? "workspace" : "insights")}
|
|
473
|
+
>
|
|
474
|
+
{Icon.chart()}
|
|
475
|
+
</button>
|
|
476
|
+
<button className="tb-icon" title={isDark ? "切换到浅色主题" : "切换到深色主题"} aria-label="切换主题" onClick={() => o.setAppearance("theme", isDark ? "light" : "dark")}>
|
|
477
|
+
{isDark ? Icon.sun() : Icon.moon()}
|
|
478
|
+
</button>
|
|
479
|
+
<div className="tb-avatar" title={o.accessEmail ? `已登录:${o.accessEmail}` : "你的工作区"}>
|
|
480
|
+
{(o.accessEmail?.[0] ?? "A").toUpperCase()}
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
|
|
485
|
+
{/* An agent's login died — nothing can run until it's reconnected, and that
|
|
486
|
+
is true for every project, so it lives above the whole workspace rather
|
|
487
|
+
than inside the task that happened to hit it first. */}
|
|
488
|
+
<AgentAuthBanner broken={o.brokenAgents} onReconnect={() => openSettings("agents")} />
|
|
489
|
+
|
|
490
|
+
<div className={`body${isMobile ? " mobile" : ""}`}>
|
|
491
|
+
{o.bootError ? (
|
|
492
|
+
// The very first fetch failed — nothing to render behind this, so a
|
|
493
|
+
// centered retry beats an empty workspace that looks "hung".
|
|
494
|
+
<div className="empty" style={{ margin: "auto" }}>
|
|
495
|
+
<div className="e-ic">{Icon.bolt()}</div>
|
|
496
|
+
<div className="e-t">无法连接工作区</div>
|
|
497
|
+
<div className="e-s">{o.bootError}</div>
|
|
498
|
+
<button className="btn btn-line" style={{ marginTop: 16 }} onClick={o.retryBoot}>{Icon.restore()} 重试</button>
|
|
499
|
+
</div>
|
|
500
|
+
) : !o.booted ? (
|
|
501
|
+
<BootSkeleton mobile={isMobile} />
|
|
502
|
+
) : isMobile ? (
|
|
503
|
+
mobilePane === "projects" ? projectsColumn
|
|
504
|
+
: mobilePane === "settings" ? settingsColumn
|
|
505
|
+
: mobilePane === "insights" ? insightsColumn
|
|
506
|
+
: mobilePane === "tasks" ? tasksColumn
|
|
507
|
+
: sessionColumn
|
|
508
|
+
) : (
|
|
509
|
+
<>
|
|
510
|
+
{layout.projCollapsed ? (
|
|
511
|
+
<ColRail label="项目" onExpand={() => o.setLayout((l) => ({ ...l, projCollapsed: false }))} />
|
|
512
|
+
) : (
|
|
513
|
+
<>
|
|
514
|
+
{projectsColumn}
|
|
515
|
+
<ColResize
|
|
516
|
+
min={PROJ_W.min} max={PROJ_W.max}
|
|
517
|
+
onWidth={(w) => o.setLayout((l) => ({ ...l, projW: w }))}
|
|
518
|
+
onReset={() => o.setLayout((l) => ({ ...l, projW: DEFAULT_LAYOUT.projW }))}
|
|
519
|
+
/>
|
|
520
|
+
</>
|
|
521
|
+
)}
|
|
522
|
+
|
|
523
|
+
{o.view === "settings" ? settingsColumn : o.view === "insights" ? insightsColumn : boardMode ? boardWorkspace : (
|
|
524
|
+
<>
|
|
525
|
+
{project ? (
|
|
526
|
+
layout.taskCollapsed ? (
|
|
527
|
+
<ColRail label="任务" task onExpand={() => o.setLayout((l) => ({ ...l, taskCollapsed: false }))} />
|
|
528
|
+
) : (
|
|
529
|
+
<>
|
|
530
|
+
{tasksColumn}
|
|
531
|
+
<ColResize
|
|
532
|
+
min={TASK_W.min} max={TASK_W.max}
|
|
533
|
+
onWidth={(w) => o.setLayout((l) => ({ ...l, taskW: w }))}
|
|
534
|
+
onReset={() => o.setLayout((l) => ({ ...l, taskW: DEFAULT_LAYOUT.taskW }))}
|
|
535
|
+
/>
|
|
536
|
+
</>
|
|
537
|
+
)
|
|
538
|
+
) : (
|
|
539
|
+
// First-run (or everything deprecated): make the empty shell a
|
|
540
|
+
// doorway, not a dead end — explain what a project is and offer
|
|
541
|
+
// the create action right here.
|
|
542
|
+
<div className="col col-tasks">
|
|
543
|
+
<div className="empty" style={{ margin: "auto", maxWidth: 300 }}>
|
|
544
|
+
<div className="e-ic">{Icon.folder()}</div>
|
|
545
|
+
<div className="e-t">{o.projects.length > 0 ? "没有活跃项目" : "还没有项目"}</div>
|
|
546
|
+
<div className="e-s">
|
|
547
|
+
{o.projects.length > 0
|
|
548
|
+
? "所有项目都已归档 —— 可从侧栏恢复一个,或新建一个。"
|
|
549
|
+
: "每个项目都是你在构建的一个应用:拥有各自的工作目录、上下文和智能体会话。"}
|
|
550
|
+
</div>
|
|
551
|
+
<button className="btn btn-accent" style={{ marginTop: 16 }} onClick={() => o.setModal("project")}>
|
|
552
|
+
{Icon.plus()} {o.projects.length > 0 ? "新建项目" : "创建第一个项目"}
|
|
553
|
+
</button>
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
)}
|
|
557
|
+
|
|
558
|
+
{sessionColumn}
|
|
559
|
+
</>
|
|
560
|
+
)}
|
|
561
|
+
</>
|
|
562
|
+
)}
|
|
563
|
+
</div>
|
|
564
|
+
|
|
565
|
+
{o.modal === "task" && project && <NewTaskModal project={project} agents={o.agents} tasks={o.realTasks} onClose={() => o.setModal(null)} onCreate={o.createTask} onOpenSetup={o.rerunOnboarding} />}
|
|
566
|
+
{o.editId && o.tasks.find((t) => t.id === o.editId) && (
|
|
567
|
+
<EditTaskModal task={o.tasks.find((t) => t.id === o.editId)!} tasks={o.realTasks} agents={o.agents} onClose={() => o.setEditId(null)} onSave={o.saveTask} onDelete={o.removeTask} onOpenSetup={o.rerunOnboarding} />
|
|
568
|
+
)}
|
|
569
|
+
{o.modal === "context" && project && <ContextModal project={project} agents={o.agents} onSetDefaultAgent={o.setProjectDefaultAgent} onClose={() => o.setModal(null)} onSave={o.saveContext} onDelete={() => o.removeProject(project.id)} onDeprecate={() => o.setDeprecated(project.id, true)} />}
|
|
570
|
+
{o.modal === "project" && <NewProjectModal onClose={() => o.setModal(null)} onCreate={o.createProject} />}
|
|
571
|
+
{o.modal === "sessions" && project && (
|
|
572
|
+
<SessionsModal
|
|
573
|
+
project={project}
|
|
574
|
+
onClose={() => o.setModal(null)}
|
|
575
|
+
onJump={(taskId) => { o.setSelTask(taskId); o.setModal(null); }}
|
|
576
|
+
/>
|
|
577
|
+
)}
|
|
578
|
+
|
|
579
|
+
{/* ⌘K palette. Commands are assembled here (not inside the palette) so each
|
|
580
|
+
row can close over the same handlers the top bar and rails use; rows that
|
|
581
|
+
need a project/task are simply omitted when there isn't one. */}
|
|
582
|
+
{paletteOpen && (
|
|
583
|
+
<CommandPalette
|
|
584
|
+
projects={o.activeProjects}
|
|
585
|
+
commands={([
|
|
586
|
+
{ id: "new-project", label: "新建项目", keywords: "new project create add repo xinjian xiangmu", icon: Icon.plus(), run: () => o.setModal("project") },
|
|
587
|
+
project && { id: "new-task", label: "新建任务", hint: `在 ${project.name} 中`, keywords: "new task session create start renwu", icon: Icon.plus(), run: () => o.setModal("task") },
|
|
588
|
+
project && { id: "toggle-task-view", label: o.taskView === "board" ? "以列表显示任务" : "以看板显示任务", hint: "⌘⇧B", keywords: "kanban board list columns view kanban liebiao", icon: o.taskView === "board" ? Icon.list() : Icon.board(), run: () => setTaskView(o.taskView === "board" ? "list" : "board") },
|
|
589
|
+
{ id: "toggle-theme", label: "切换主题", hint: isDark ? "切换到浅色" : "切换到深色", keywords: "theme dark light mode appearance zhuti", icon: isDark ? Icon.sun() : Icon.moon(), run: () => o.setAppearance("theme", isDark ? "light" : "dark") },
|
|
590
|
+
{ id: "open-settings", label: "打开设置", keywords: "settings preferences defaults setup shezhi", icon: Icon.gear(), run: () => openSettings() },
|
|
591
|
+
{ id: "open-insights", label: "打开洞察", keywords: "insights usage spend cost tokens analytics dashboard metrics stats dongcha", icon: Icon.chart(), run: () => o.setView("insights") },
|
|
592
|
+
{ id: "connect-agent", label: "连接智能体", keywords: "codex claude agent connect login subscription zhinengti", icon: Icon.bolt(), run: () => openSettings("agents") },
|
|
593
|
+
{ id: "open-appearance", label: "打开外观设置", keywords: "appearance density theme dark light mode waiguan", icon: Icon.sliders(), run: () => o.setAppearanceOpen(true) },
|
|
594
|
+
project && features.services && { id: "toggle-services", label: "开关服务面板", hint: o.servicesOpen ? "隐藏" : "显示", keywords: "services dev server setup test drawer fuwu", icon: Icon.sliders(), run: () => { o.setServicesMounted(true); o.setServicesOpen((s) => !s); } },
|
|
595
|
+
project && { id: "toggle-terminal", label: "开关终端", hint: o.termOpen ? "隐藏" : "显示", keywords: "terminal shell console pty zhongduan", icon: Icon.terminal(), run: () => { o.setTermMounted(true); o.setTermOpen((t) => !t); } },
|
|
596
|
+
task && task.started === 1 && !o.running.has(task.id) && { id: "clear-session", label: "/clear 清空当前会话", hint: task.title, keywords: "clear new session restart fresh context compact qingkong", icon: Icon.clear(), run: () => requestClear(task.id) },
|
|
597
|
+
] as (PaletteCommand | false | null)[]).filter((c): c is PaletteCommand => !!c)}
|
|
598
|
+
onPickProject={o.selectProject}
|
|
599
|
+
onPickTask={o.goToTask}
|
|
600
|
+
onClose={() => setPaletteOpen(false)}
|
|
601
|
+
/>
|
|
602
|
+
)}
|
|
603
|
+
|
|
604
|
+
{o.appearanceOpen && <AppearancePanel appearance={o.appearance} setAppearance={o.setAppearance} onClose={() => o.setAppearanceOpen(false)} />}
|
|
605
|
+
|
|
606
|
+
{/* Phone terminal lives as a full-screen sheet over everything. Kept mounted
|
|
607
|
+
(hidden) while a project is selected so a dev server survives pane hops. */}
|
|
608
|
+
{isMobile && project && o.termMounted && (
|
|
609
|
+
<MobileTerminalSheet key={project.id} cwd={project.repo_path} port={project.port} visible={o.termOpen} onClose={() => o.setTermOpen(false)} />
|
|
610
|
+
)}
|
|
611
|
+
|
|
612
|
+
{/* First-run onboarding — a full-screen wizard over the (empty) workspace
|
|
613
|
+
on a fresh instance, or when re-run from Settings. */}
|
|
614
|
+
{o.wizardOpen && o.onboarding && (
|
|
615
|
+
<OnboardingWizard initial={o.onboarding} onFinish={o.finishWizard} />
|
|
616
|
+
)}
|
|
617
|
+
|
|
618
|
+
{/* Optional post-setup nudge to connect a second agent (Codex). Only once
|
|
619
|
+
the required first-run wizard is done, and never stacked on the wizard
|
|
620
|
+
or the tutorial-payoff modal. Dismissible once (localStorage). */}
|
|
621
|
+
{o.onboarding?.complete && !o.wizardOpen && !o.nudge && (
|
|
622
|
+
<AgentNudge ready onConnect={() => openSettings("agents")} />
|
|
623
|
+
)}
|
|
624
|
+
|
|
625
|
+
{/* Post-tutorial payoff: fires once the seeded "Welcome" task is merged. */}
|
|
626
|
+
{o.nudge && (
|
|
627
|
+
<WelcomeNudge
|
|
628
|
+
onClose={() => o.setNudge(false)}
|
|
629
|
+
onCreateProject={() => { o.setNudge(false); o.setModal("project"); }}
|
|
630
|
+
/>
|
|
631
|
+
)}
|
|
632
|
+
</div>
|
|
633
|
+
);
|
|
634
|
+
}
|