@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,288 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import type { Status, Priority, AskQuestion, AskAnswers } from "./protocol/types";
|
|
5
|
+
import { OperatorBoardContext, OperatorBoardQuoteContext, type OperatorBoardOptions, type OperatorBoardRequest, type OperatorBoardTarget } from "./boardInteraction";
|
|
6
|
+
import { readDraft, writeDraft, type Quote } from "./composerDraft";
|
|
7
|
+
import { clientFeatures } from "./protocol/features";
|
|
8
|
+
import { toolDataOf } from "./transcriptModel";
|
|
9
|
+
import { Icon } from "./icons";
|
|
10
|
+
import { type ResolveResult } from "./TaskChanges";
|
|
11
|
+
import { fmtTokens, fmtCost, fmtJobCost, isAwaiting, buildSessions, usageSplit, costDisplay } from "./format";
|
|
12
|
+
import {
|
|
13
|
+
SLABEL, SSUB, AWAIT_LABEL, STATUSES, PLABEL, PRIORITIES,
|
|
14
|
+
RAIL_W,
|
|
15
|
+
type ProjectRow, type TaskRow, type Msg, type SyncStatusResp, type AgentsBundle, type InternalUsageEstimate,
|
|
16
|
+
} from "./types";
|
|
17
|
+
import { capsFor, agentLabel, findAgent } from "./agents";
|
|
18
|
+
import { StatusDot, Popover } from "./shared";
|
|
19
|
+
import { Chat } from "./Chat";
|
|
20
|
+
import { TaskHero } from "./TaskHero";
|
|
21
|
+
import { RunControls } from "./RunControls";
|
|
22
|
+
import { CliBoard } from "../../components";
|
|
23
|
+
import type { ComponentProps } from "react";
|
|
24
|
+
import { useOperatorContextEvents, useOperatorFileWatch } from "./boardEvents";
|
|
25
|
+
import { readBoardState, publishBoardState } from "./boardState";
|
|
26
|
+
import { registerOperatorBoard } from "./boardInteraction";
|
|
27
|
+
import { useConfigurationBusy } from "../../chat/configuration";
|
|
28
|
+
import type { SessionRail as NodeSessionRail } from "./SessionRail";
|
|
29
|
+
const SessionRail=(props:ComponentProps<typeof NodeSessionRail>)=><CliBoard operator={props}/>;
|
|
30
|
+
import { ColResize, ColRail } from "./Layout";
|
|
31
|
+
import { jget, downloadExport } from "./api";
|
|
32
|
+
|
|
33
|
+
// Non-blocking banner shown when a reopened task's worktree is behind its base
|
|
34
|
+
// branch. Computed (read-only) on open; the actual git op fires only when the user
|
|
35
|
+
// clicks. Fast-forward-able tasks show nothing here — they catch up silently on the
|
|
36
|
+
// next message — so the banner only appears for tier 2 (clean merge → Sync) and
|
|
37
|
+
// tier 3 (conflicts → Fix with AI).
|
|
38
|
+
function SyncBanner({ taskId, running, pinned, onResolveWithAI, onSwitchToChat }: {
|
|
39
|
+
taskId: string; running: boolean; pinned: boolean;
|
|
40
|
+
onResolveWithAI: (taskId: string) => Promise<ResolveResult>;
|
|
41
|
+
onSwitchToChat: () => void;
|
|
42
|
+
}) {
|
|
43
|
+
const [st, setSt] = useState<SyncStatusResp | null>(null);
|
|
44
|
+
const [busy, setBusy] = useState(false);
|
|
45
|
+
|
|
46
|
+
const load = useCallback(async () => {
|
|
47
|
+
try { const r = await fetch(`/api/tasks/${taskId}/sync`, { cache: "no-store" }); setSt(await r.json()); }
|
|
48
|
+
catch { setSt(null); }
|
|
49
|
+
}, [taskId]);
|
|
50
|
+
|
|
51
|
+
// Recompute on open and whenever a turn finishes (a turn may have fast-forwarded
|
|
52
|
+
// or otherwise moved the branch). Skip while running to avoid mid-merge reads.
|
|
53
|
+
useEffect(() => { if (!running) load(); }, [running, load]);
|
|
54
|
+
|
|
55
|
+
if (!st || !st.isolated || !st.behind) return null;
|
|
56
|
+
if (st.canFastForward && !pinned) return null; // only ordinary tasks auto-follow main
|
|
57
|
+
|
|
58
|
+
const conflicts = st.conflicts?.length ?? 0;
|
|
59
|
+
|
|
60
|
+
const doSync = async () => {
|
|
61
|
+
setBusy(true);
|
|
62
|
+
try {
|
|
63
|
+
const r = await fetch(`/api/tasks/${taskId}/sync`, { method: "POST" });
|
|
64
|
+
const res = await r.json();
|
|
65
|
+
// Prediction said clean but the real merge conflicted — escalate to Fix with AI.
|
|
66
|
+
if (res?.conflicts?.length) { const fix = await onResolveWithAI(taskId); if (fix.ok && !fix.merged) onSwitchToChat(); }
|
|
67
|
+
} finally { setBusy(false); load(); }
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const doFix = async () => {
|
|
71
|
+
setBusy(true);
|
|
72
|
+
try { const res = await onResolveWithAI(taskId); if (res.ok && !res.merged) onSwitchToChat(); }
|
|
73
|
+
finally { setBusy(false); load(); }
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div className={`sync-banner${conflicts ? " conflict" : ""}`}>
|
|
78
|
+
<span className="sync-msg">
|
|
79
|
+
{conflicts > 0
|
|
80
|
+
? `落后 ${st.baseBranch} ${st.behind} 个提交 · ${conflicts} 个文件存在冲突`
|
|
81
|
+
: `落后 ${st.baseBranch} ${st.behind} 个提交`}
|
|
82
|
+
</span>
|
|
83
|
+
<span className="sync-spacer" />
|
|
84
|
+
{conflicts > 0 ? (
|
|
85
|
+
<button className="tc-btn primary" onClick={doFix} disabled={busy || running}>{busy ? "…" : "用 AI 解决"}</button>
|
|
86
|
+
) : (
|
|
87
|
+
<button className="tc-btn primary" onClick={doSync} disabled={busy || running}>{busy ? "同步中…" : "同步"}</button>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function SessionView(props: Parameters<typeof SessionViewInner>[0]) { return <SessionViewInner key={props.task.id} {...props}/>; }
|
|
94
|
+
function SessionViewInner({ project, task, tasks, onSelectTask, onFork, agents, messages, running, blockedBy, transcriptLoading, older=0, onLoadOlder, onSend, onStart, onStop, onClear, clearConfirming, onConfirmClear, onCancelClear, onEdit, onReconnect, onSetStatus, onSetPriority, onSetModel, onSetAgent, onSetReasoning, onSetPermission, onSetSendContext, onResolveWithAI, onMerged, onPrCreated, onAnswer, onCancelQueued, onBack, mobile, railW, onRailWidth, onRailReset, railCollapsed, onRailCollapse, onRailExpand, board }: {
|
|
95
|
+
board?: OperatorBoardOptions;
|
|
96
|
+
tasks: TaskRow[]; onSelectTask: (id: string) => void; onFork: (text: string, sourceIds: string[], requestId: string) => Promise<boolean>;
|
|
97
|
+
project: ProjectRow; task: TaskRow; agents: AgentsBundle; messages: Msg[]; running: boolean; blockedBy?: string[]; transcriptLoading?: boolean;
|
|
98
|
+
older?: number; onLoadOlder?: (beforeApply?: () => void) => Promise<boolean>;
|
|
99
|
+
onSend: (t: string) => Promise<boolean>; onStart: () => void; onStop: () => Promise<boolean>; onClear: () => void; onEdit: () => void;
|
|
100
|
+
clearConfirming?: boolean; onConfirmClear?: () => void; onCancelClear?: () => void;
|
|
101
|
+
// Deep-link to Settings → Agents, for the transcript's "your login died" recovery button.
|
|
102
|
+
onReconnect?: () => void;
|
|
103
|
+
onSetStatus: (s: Status) => void; onSetPriority: (p: Priority) => void; onSetModel: (m: string | null) => Promise<void>;
|
|
104
|
+
onSetAgent: (id:string)=>Promise<void>;
|
|
105
|
+
onSetReasoning: (r: string | null) => Promise<void>; onSetPermission: (p: string | null) => Promise<void>;
|
|
106
|
+
onSetSendContext: (v: boolean) => void;
|
|
107
|
+
onResolveWithAI: (taskId: string) => Promise<ResolveResult>;
|
|
108
|
+
onMerged?: () => void;
|
|
109
|
+
onPrCreated?: (url: string) => void;
|
|
110
|
+
onAnswer: (askId: string, questions: AskQuestion[], answers: AskAnswers) => void;
|
|
111
|
+
onCancelQueued: (pendingId: string) => void;
|
|
112
|
+
onBack?: () => void; mobile?: boolean;
|
|
113
|
+
railW: number; onRailWidth: (w: number) => void; onRailReset: () => void;
|
|
114
|
+
railCollapsed: boolean; onRailCollapse: () => void; onRailExpand: () => void;
|
|
115
|
+
}) {
|
|
116
|
+
const sessionRef = useRef<HTMLDivElement>(null);
|
|
117
|
+
useOperatorFileWatch(task.id,board?.watchFiles!==false,board,undefined,board?.fileWatchIntervalMs);
|
|
118
|
+
useOperatorContextEvents(task.id,true,board);
|
|
119
|
+
const previousAuto=useRef(board?.autoContext);
|
|
120
|
+
useLayoutEffect(()=>{
|
|
121
|
+
if(board?.autoContext===undefined&&previousAuto.current===undefined)return;
|
|
122
|
+
previousAuto.current=board?.autoContext;const view=readBoardState(task.id),enabled=board?.autoContext!==false;
|
|
123
|
+
if(view&&view.autoContext!==enabled)publishBoardState(task.id,{...view,autoContext:enabled});
|
|
124
|
+
},[task.id,board?.autoContext]);
|
|
125
|
+
|
|
126
|
+
const [compact, setCompact] = useState(false);
|
|
127
|
+
const configBusy = useConfigurationBusy(task.id);
|
|
128
|
+
useLayoutEffect(() => {
|
|
129
|
+
const el = sessionRef.current;
|
|
130
|
+
if (!el) return;
|
|
131
|
+
setCompact(el.getBoundingClientRect().width < Math.max(760, railW + 400));
|
|
132
|
+
const observer = new ResizeObserver(([entry]) => setCompact(entry.contentRect.width < Math.max(760, railW + 400)));
|
|
133
|
+
observer.observe(el);
|
|
134
|
+
return () => observer.disconnect();
|
|
135
|
+
}, [railW]);
|
|
136
|
+
const singlePane = mobile || compact;
|
|
137
|
+
const [statusOpen, setStatusOpen] = useState(false);
|
|
138
|
+
const [moreOpen, setMoreOpen] = useState(false);
|
|
139
|
+
const [view, setView] = useState<"chat" | "changes">("chat");
|
|
140
|
+
const [boardRequest,setBoardRequest]=useState<OperatorBoardRequest|null>(null);
|
|
141
|
+
const boardSequence=useRef(0);
|
|
142
|
+
const acknowledgeBoardRequest=useCallback((sequence:number)=>setBoardRequest(previous=>previous?.sequence===sequence?null:previous),[]);
|
|
143
|
+
const routing=useRef({singlePane,railCollapsed,onRailExpand,task,tasks,board});
|
|
144
|
+
routing.current={singlePane,railCollapsed,onRailExpand,task,tasks,board};
|
|
145
|
+
const openBoard=useCallback((target:OperatorBoardTarget)=>{
|
|
146
|
+
const context=routing.current;
|
|
147
|
+
if(target.kind==='browser'&&(target.taskId!==context.task.id||!clientFeatures().browser))return false;
|
|
148
|
+
if(target.kind==='file'&&(!target.path||![context.task,...context.tasks].some(t=>t.id===target.taskId&&t.project_id===context.task.project_id)))return false;
|
|
149
|
+
if(context.board?.availableTabs&&!context.board.availableTabs.includes(target.kind==='file'?'files':'browser'))return false;
|
|
150
|
+
setBoardRequest({sequence:++boardSequence.current,target});
|
|
151
|
+
if(context.singlePane)setView('changes');else if(context.railCollapsed)context.onRailExpand();
|
|
152
|
+
return true;
|
|
153
|
+
},[]);
|
|
154
|
+
useEffect(()=>registerOperatorBoard(target=>target.taskId===task.id?openBoard(target):false),[task.id,openBoard]);
|
|
155
|
+
useEffect(()=>{
|
|
156
|
+
const show=(event:Event)=>{if((event as CustomEvent<{taskId?:string}>).detail?.taskId===task.id)openBoard({kind:'browser',taskId:task.id});};
|
|
157
|
+
window.addEventListener('orch:show-browser',show);return()=>window.removeEventListener('orch:show-browser',show);
|
|
158
|
+
},[task.id,openBoard]);
|
|
159
|
+
const quoteFromBoard=useCallback((quote:Quote)=>{
|
|
160
|
+
const context=routing.current;
|
|
161
|
+
writeDraft(context.task.id,{...readDraft(context.task.id),quote});
|
|
162
|
+
if(context.singlePane)setView('chat');
|
|
163
|
+
requestAnimationFrame(()=>sessionRef.current?.querySelector<HTMLTextAreaElement>('.composer textarea')?.focus({preventScroll:true}));
|
|
164
|
+
},[]);
|
|
165
|
+
const activity=useMemo(()=>{const tool=[...messages].reverse().find(m=>m.role==='tool');return `${messages.length}:${tool?.id??''}:${tool?toolDataOf(tool).result!==undefined:false}`;},[messages]);
|
|
166
|
+
const [clearEstimate, setClearEstimate] = useState<InternalUsageEstimate | null>(null);
|
|
167
|
+
const sessions = useMemo(() => buildSessions(messages), [messages]);
|
|
168
|
+
const hasSession = task.started === 1 || messages.length > 0 || !!task.parent_task_id;
|
|
169
|
+
const awaiting = isAwaiting(task);
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (!clearConfirming) { setClearEstimate(null); return; }
|
|
172
|
+
let alive = true;
|
|
173
|
+
jget<{ estimate: InternalUsageEstimate | null }>(`/api/tasks/${task.id}/clear`)
|
|
174
|
+
.then((r) => { if (alive) setClearEstimate(r.estimate); })
|
|
175
|
+
.catch(() => { if (alive) setClearEstimate(null); });
|
|
176
|
+
return () => { alive = false; };
|
|
177
|
+
}, [clearConfirming, task.id]);
|
|
178
|
+
// Run-control pickers + feature gates come from this task's agent capabilities,
|
|
179
|
+
// never a hardcoded list — so the options always match the agent it runs under.
|
|
180
|
+
const caps = capsFor(agents, task.agent);
|
|
181
|
+
const controls = { task, caps, agents:agents.agents.map(a=>({id:a.id,label:a.label,disabled:!a.authenticated||a.installed===false})), onSetAgent, defaults: findAgent(agents, task.agent)?.defaults, disabled: running, onSetModel, onSetReasoning, onSetPermission };
|
|
182
|
+
// Usage chip: tokens split into fresh work vs re-read cache (the raw total is
|
|
183
|
+
// mostly cache reads and wildly overstates what ran), and a dollar figure whose
|
|
184
|
+
// presentation follows how this agent is signed in — a subscription login's
|
|
185
|
+
// figure is an API-price equivalent covered by plan quota, not a bill. Both
|
|
186
|
+
// derivations live in ./format so the wording has one home.
|
|
187
|
+
const usage = usageSplit(task);
|
|
188
|
+
const cost = costDisplay(findAgent(agents, task.agent));
|
|
189
|
+
// PR number for the header chip, parsed from the stored URL (…/pull/42).
|
|
190
|
+
const prNum = task.pr_url?.match(/\/pull\/(\d+)/)?.[1];
|
|
191
|
+
return (
|
|
192
|
+
<OperatorBoardContext.Provider value={openBoard}><OperatorBoardQuoteContext.Provider value={quoteFromBoard}>
|
|
193
|
+
<div className={`session${singlePane ? " compact-session" : ""}`} ref={sessionRef}>
|
|
194
|
+
<div className="sess-head">
|
|
195
|
+
{onBack && <button className="mobile-back" onClick={onBack} title="返回任务列表" aria-label="返回任务列表">{Icon.chevRight({ style: { transform: "rotate(180deg)" } })}</button>}
|
|
196
|
+
<div className="sh-main">
|
|
197
|
+
<div className="crumb">
|
|
198
|
+
<span className="pic" style={{ width: 16, height: 16, borderRadius: 5, background: project.color, display: "grid", placeItems: "center", color: "#fff", fontSize: 9, fontWeight: 700 }}>{project.name[0]}</span>
|
|
199
|
+
<span className="session-project-name" title={project.name}>{project.name}</span>
|
|
200
|
+
|
|
201
|
+
</div>
|
|
202
|
+
<div className="sh-title">
|
|
203
|
+
<span className="sht-text" title={task.title}>{task.title}</span>
|
|
204
|
+
<button className="icon-btn sh-edit" title="查看并编辑任务详情" aria-label="查看并编辑任务详情" onClick={onEdit}>{Icon.edit()}</button>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
<div className="sh-tools">
|
|
208
|
+
<div style={{ position: "relative" }}>
|
|
209
|
+
<button className={`status-ctl ${awaiting ? "awaiting" : ""}`} onClick={(e) => { e.stopPropagation(); setStatusOpen((o) => !o); setMoreOpen(false); }}>
|
|
210
|
+
<StatusDot status={task.status} running={running} awaiting={awaiting} />
|
|
211
|
+
<span className="cv">{awaiting ? AWAIT_LABEL : SLABEL[task.status]}</span>
|
|
212
|
+
{Icon.chevDown()}
|
|
213
|
+
</button>
|
|
214
|
+
{statusOpen && (
|
|
215
|
+
<Popover onClose={() => setStatusOpen(false)}>
|
|
216
|
+
{STATUSES.map((s) => (
|
|
217
|
+
<button key={s} className="pop-item" onClick={() => { onSetStatus(s); setStatusOpen(false); }}>
|
|
218
|
+
<StatusDot status={s} />
|
|
219
|
+
<div><div>{SLABEL[s]}</div><div className="pi-sub">{SSUB[s]}</div></div>
|
|
220
|
+
{task.status === s && <span className="pi-check">{Icon.check()}</span>}
|
|
221
|
+
</button>
|
|
222
|
+
))}
|
|
223
|
+
</Popover>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
<div style={{ position: "relative" }}>
|
|
227
|
+
<button className="icon-btn session-more" aria-label="更多任务操作" aria-expanded={moreOpen} onClick={(e) => { e.stopPropagation(); setMoreOpen(!moreOpen); setStatusOpen(false); }}>{Icon.dots()}</button>
|
|
228
|
+
{moreOpen && <Popover onClose={() => setMoreOpen(false)}>
|
|
229
|
+
<div className="pop-sec">{agentLabel(agents, task.agent)} · {fmtTokens(usage.fresh)} tokens{cost.show && task.cost_usd > 0 ? ` · ${cost.approx ? "~" : ""}${fmtCost(task.cost_usd)}` : ""}</div>
|
|
230
|
+
<button className="pop-item" onClick={() => { setMoreOpen(false); onEdit(); }}>{Icon.edit()} 编辑任务详情</button>
|
|
231
|
+
<button className="pop-item" onClick={() => { setMoreOpen(false); downloadExport(`/api/tasks/${task.id}/export`); }}>{Icon.download()} 导出执行过程</button>
|
|
232
|
+
{task.pr_url && <a className="pop-item" href={task.pr_url} target="_blank" rel="noreferrer">{Icon.github()} 打开 PR{prNum ? ` #${prNum}` : ""}</a>}
|
|
233
|
+
<div className="divider" /><div className="pop-sec">优先级</div>
|
|
234
|
+
{PRIORITIES.map((p) => <button className="pop-item" key={p} aria-pressed={task.priority === p} onClick={() => { onSetPriority(p); setMoreOpen(false); }}><span className={`pri ${p}`}>{PLABEL[p]}</span>{task.priority === p && Icon.check()}</button>)}
|
|
235
|
+
<div className="divider" />
|
|
236
|
+
<button className="pop-item" onClick={() => { setMoreOpen(false); onClear(); }} disabled={running || task.started !== 1}>{Icon.clear()} 保存摘要,开启新上下文</button>
|
|
237
|
+
</Popover>}
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
{hasSession && <div className="session-viewbar">
|
|
243
|
+
{singlePane ? <div className="viewseg">
|
|
244
|
+
<button className={`viewseg-btn ${view === "chat" ? "on" : ""}`} onClick={() => setView("chat")}>对话</button>
|
|
245
|
+
<button className={`viewseg-btn ${view === "changes" ? "on" : ""}`} onClick={() => setView("changes")}>看板</button>
|
|
246
|
+
</div> : <span className="session-agent-label">{agentLabel(agents, task.agent)}</span>}
|
|
247
|
+
<span className="session-branch" title={task.work_branch || ""}>{Icon.git()} {task.parent_task_id ? `来自 ${task.fork_source_title}` : "独立任务分支"}</span>
|
|
248
|
+
</div>}
|
|
249
|
+
|
|
250
|
+
{hasSession && (
|
|
251
|
+
<SyncBanner taskId={task.id} running={running} pinned={!!task.fork_commit} onResolveWithAI={onResolveWithAI} onSwitchToChat={() => setView("chat")} />
|
|
252
|
+
)}
|
|
253
|
+
|
|
254
|
+
{clearConfirming && (
|
|
255
|
+
<div className="clear-confirm">
|
|
256
|
+
<span>保存摘要并开启新的上下文?</span>
|
|
257
|
+
{clearEstimate && <span className="job-cost-hint">预计消耗 {fmtJobCost(clearEstimate)}</span>}
|
|
258
|
+
<span className="spacer" />
|
|
259
|
+
<button className="btn btn-ghost btn-sm" onClick={onCancelClear}>取消</button>
|
|
260
|
+
<button className="btn btn-accent btn-sm" onClick={onConfirmClear}>{Icon.clear()} 确认 /clear</button>
|
|
261
|
+
</div>
|
|
262
|
+
)}
|
|
263
|
+
|
|
264
|
+
{!hasSession ? (
|
|
265
|
+
<><div className="hero-controls"><RunControls {...controls} /></div><TaskHero task={task} project={project} onStart={onStart} onEdit={onEdit} onSetSendContext={onSetSendContext} running={running || configBusy} blockedBy={blockedBy} /></>
|
|
266
|
+
) : (
|
|
267
|
+
// Keep the chat at the same React position across all widths. Moving
|
|
268
|
+
// it between different parents remounted the composer during resize
|
|
269
|
+
// and could drop a keystroke, an upload, or an in-flight setting save.
|
|
270
|
+
<div className={singlePane ? "sess-responsive" : "sess-split"} style={singlePane ? undefined : { gridTemplateColumns: railCollapsed ? "minmax(0,1fr) 30px" : `minmax(0,1fr) 0px ${railW}px` }}>
|
|
271
|
+
<div className={`sess-main${singlePane ? " compact-chat" : ""}`} hidden={singlePane && view !== "chat"}><Chat project={project} task={task} tasks={tasks} agents={agents} messages={messages}
|
|
272
|
+
running={running} blockedBy={blockedBy} transcriptLoading={transcriptLoading} older={older} onLoadOlder={onLoadOlder}
|
|
273
|
+
controls={controls} configBusy={configBusy} active={!singlePane || view === "chat"}
|
|
274
|
+
onSelectTask={onSelectTask} onFork={onFork} onSend={onSend} onStop={onStop}
|
|
275
|
+
onStart={onStart} onEdit={onEdit} onClear={onClear} onSetSendContext={onSetSendContext}
|
|
276
|
+
onReconnect={onReconnect} onAnswer={onAnswer} onCancelQueued={onCancelQueued} /></div>
|
|
277
|
+
{!singlePane && !railCollapsed && <ColResize side="right" min={RAIL_W.min} max={RAIL_W.max} onWidth={onRailWidth} onReset={onRailReset} />}
|
|
278
|
+
{!singlePane && railCollapsed ? <ColRail label="看板" right onExpand={onRailExpand} /> : (!singlePane || view === "changes") && (
|
|
279
|
+
<SessionRail {...board} openRequest={boardRequest} onRequestHandled={acknowledgeBoardRequest} activity={activity} project={project} task={task} sessions={sessions} running={running}
|
|
280
|
+
onResolveWithAI={onResolveWithAI} onMerged={onMerged} onPrCreated={onPrCreated} onClear={onClear}
|
|
281
|
+
onCollapse={singlePane ? () => setView("chat") : onRailCollapse} onSwitchToChat={() => setView("chat")} />
|
|
282
|
+
)}
|
|
283
|
+
</div>
|
|
284
|
+
)}
|
|
285
|
+
</div>
|
|
286
|
+
</OperatorBoardQuoteContext.Provider></OperatorBoardContext.Provider>
|
|
287
|
+
);
|
|
288
|
+
}
|