@spzxg/cli-ui 0.6.6 → 0.6.8
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 +40 -1
- package/README.md +90 -21
- package/dist/cli-ui-embed-Be9_d9jB.js +49691 -0
- package/dist/{cli-ui-index-b8wF7wrN.js → cli-ui-index-CzNfiynu.js} +2 -2
- package/dist/{cli-ui-jszip.min-B0uLrCHg.js → cli-ui-jszip.min-BL0mSekN.js} +2 -2
- package/dist/{cli-ui-jszip.min-ggP8OokP.js → cli-ui-jszip.min-DjCIAfse.js} +1 -1
- package/dist/{cli-ui-xlsx-DEDdHrJJ.js → cli-ui-xlsx-CscI4xk0.js} +1 -1
- package/dist/cli-ui.css +1 -1
- package/dist/cli-ui.js +1 -1
- package/package.json +3 -1
- package/src/board/CliBoard.tsx +128 -100
- package/src/board/FileViewer.tsx +259 -132
- package/src/board/FilesPane.tsx +129 -40
- package/src/board/HtmlReadingPreview.tsx +52 -0
- package/src/board/MarkdownPreview.tsx +134 -0
- package/src/board/PanelControls.tsx +19 -7
- package/src/board/fileKind.ts +138 -0
- package/src/board/fileOps.ts +9 -2
- package/src/board/fileScope.ts +29 -0
- package/src/board/fileSync.ts +343 -0
- package/src/board/fileSyncCache.ts +250 -0
- package/src/board/fileSyncProcessor.ts +71 -0
- package/src/board/fileSyncQueue.ts +56 -0
- package/src/board/fileSyncTransport.ts +85 -0
- package/src/board/fileWatch.ts +177 -76
- package/src/board/folderDownload.ts +39 -5
- package/src/board/frameQuote.ts +10 -8
- package/src/board/htmlReading.ts +148 -0
- package/src/board/textLoad.ts +14 -39
- package/src/board/useBoardUpload.tsx +51 -0
- package/src/board/useBoardVisibility.ts +35 -0
- package/src/board/workspace.ts +38 -6
- package/src/chat/CliChat.tsx +121 -51
- package/src/chat/Composer.tsx +54 -22
- package/src/chat/GroupChat.tsx +113 -0
- package/src/chat/RunControls.tsx +143 -46
- package/src/chat/draft.ts +12 -4
- package/src/chat/useExecutionConfig.ts +93 -0
- package/src/components.tsx +1 -1
- package/src/core/blocks.ts +2 -1
- package/src/core/client.ts +65 -61
- package/src/core/config.ts +6 -2
- package/src/core/documentUpload.ts +196 -0
- package/src/core/execution.ts +24 -0
- package/src/core/store.ts +81 -20
- package/src/core/types.ts +3 -1
- package/src/index.ts +4 -0
- package/src/msglist/CliMsgList.tsx +9 -2
- package/src/operator/node/Chat.tsx +11 -9
- package/src/operator/node/Composer.tsx +103 -75
- package/src/operator/node/Orchestrator.tsx +1 -1
- package/src/operator/node/SessionRail.tsx +71 -32
- package/src/operator/node/SessionView.tsx +31 -28
- package/src/operator/node/Transcript.tsx +21 -9
- package/src/operator/node/boardBackend.ts +0 -19
- package/src/operator/node/boardEvents.ts +21 -3
- package/src/operator/node/boardState.ts +18 -1
- package/src/operator/node/boardWorkspace.ts +4 -1
- package/src/operator/node/composerDraft.ts +6 -2
- package/src/operator/node/format.ts +4 -2
- package/src/operator/node/liveStream.ts +14 -2
- package/src/operator/node/useTaskStream.ts +3 -1
- package/src/operator.base.css +150 -43
- package/src/operator.css +150 -43
- package/src/styles.css +253 -39
- package/src/ui/Markdown.tsx +2 -2
- package/src/ui/Popover.tsx +25 -12
- package/src/ui/QueuedMessages.tsx +69 -0
- package/src/ui/ReferenceCard.tsx +28 -0
- package/src/ui/TextBlock.tsx +6 -4
- package/src/ui/Transcript.tsx +20 -10
- package/src/ui/UploadProgress.tsx +46 -0
- package/src/ui/WorkspaceDrawer.tsx +64 -0
- package/src/ui/WorkspaceTabs.tsx +60 -0
- package/src/ui/format.ts +3 -1
- package/src/ui/reference.ts +40 -0
- package/src/ui/useChatScrollbar.ts +29 -0
- package/src/ui/useComposerAutosize.ts +54 -0
- package/dist/cli-ui-embed-BAqonuoD.js +0 -45952
package/dist/cli-ui.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as a, b as i, d as r, e as t, f as o, h as C, i as l, j as n, k as m, m as d, l as u, n as w, o as B, p as f } from "./cli-ui-embed-
|
|
1
|
+
import { C as a, b as i, d as r, e as t, f as o, h as C, i as l, j as n, k as m, m as d, l as u, n as w, o as B, p as f } from "./cli-ui-embed-Be9_d9jB.js";
|
|
2
2
|
export {
|
|
3
3
|
a as CliBoard,
|
|
4
4
|
i as CliBrowserView,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spzxg/cli-ui",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "相互联动的 CliMsgList、CliChat、CliBoard,支持任务中枢和完整 Operator 工作区。",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"@types/react": "19.2.17",
|
|
42
42
|
"@types/react-dom": "19.2.3",
|
|
43
43
|
"@vitejs/plugin-react": "4.7.0",
|
|
44
|
+
"fake-indexeddb": "^6.2.7",
|
|
44
45
|
"jsdom": "^25.0.1",
|
|
45
46
|
"postcss": "^8.5.32",
|
|
46
47
|
"typescript": "^5.7.3",
|
|
@@ -51,6 +52,7 @@
|
|
|
51
52
|
"@xterm/addon-fit": "^0.11.0",
|
|
52
53
|
"@xterm/addon-web-links": "^0.12.0",
|
|
53
54
|
"@xterm/xterm": "^6.0.0",
|
|
55
|
+
"htmlparser2": "10.0.0",
|
|
54
56
|
"jszip": "^3.10.2",
|
|
55
57
|
"mammoth": "^1.13.0",
|
|
56
58
|
"react-markdown": "^10.1.0",
|
package/src/board/CliBoard.tsx
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { retryWorkspaceFiles } from "./fileWatch";
|
|
2
|
+
import { retainFileScope } from "./fileScope";
|
|
1
3
|
// cli-board:右侧看板。界面取自 Operator 的会话右栏(SessionRail):文件 / 改动 / 浏览器三个页签。
|
|
2
4
|
// 数据经 center 的 /api/jobs/:id/board、files、diff、browser 转发自执行节点(浏览器不直连节点)。
|
|
3
5
|
//
|
|
@@ -5,17 +7,16 @@
|
|
|
5
7
|
// · 改动:分支上的 diff(只读);
|
|
6
8
|
// · 浏览器:任务浏览器的实时画面、接管 / 交还、打开 / 关闭。
|
|
7
9
|
//
|
|
8
|
-
//
|
|
10
|
+
// 同步:每轮实时输出结束、恢复页面或手动刷新时检查打开的文件,只读取缺失变化块;没有周期轮询。
|
|
9
11
|
// 和 cli-chat 放在同一个 Provider 里时,对话里点本地文件 / 「查看浏览器」会在这里打开(store.openInBoard)。
|
|
10
12
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from "react";
|
|
13
|
+
import { createPortal } from "react-dom";
|
|
11
14
|
import { CliErrorBoundary } from "../ui/ErrorBoundary";
|
|
12
15
|
import type { BoardRef, CliEvent, CliStore, CliStoreOptions, OpenTarget } from "../core/store";
|
|
13
16
|
import { useCliState, useCliStore, useCliStoreContext, useComponentEvents, withOwnProvider } from "../core/provider";
|
|
14
17
|
import { convKey, parseConvKey, isRunning } from "../core/conversation";
|
|
15
|
-
import { toolDataOf } from "../ui/transcriptModel";
|
|
16
18
|
import type { BoardInfo, FileEntry } from "../core/types";
|
|
17
19
|
import { Icon } from "../ui/icons";
|
|
18
|
-
import { CenterError } from "../core/client";
|
|
19
20
|
import { FileViewer, type FileAction } from "./FileViewer";
|
|
20
21
|
import { FilesPane } from "./FilesPane";
|
|
21
22
|
import { ChangesPane } from "./ChangesPane";
|
|
@@ -24,7 +25,9 @@ import { rememberBoardFile } from "./context";
|
|
|
24
25
|
import type { BoardFile } from "./workspace";
|
|
25
26
|
import type { WorkspaceHandlers } from "../core/provider";
|
|
26
27
|
import { useBoardPanels } from "./panels";
|
|
27
|
-
import { PanelControls, PanelTab } from "./PanelControls";
|
|
28
|
+
import { PanelControls, PanelTab, type InlineBoardLayout } from "./PanelControls";
|
|
29
|
+
import { useBoardVisibility } from "./useBoardVisibility";
|
|
30
|
+
import { useBoardUpload } from "./useBoardUpload";
|
|
28
31
|
|
|
29
32
|
export type BoardTab = "files" | "changes" | "browser";
|
|
30
33
|
|
|
@@ -34,7 +37,7 @@ export interface BoardExtraTab {
|
|
|
34
37
|
label: string;
|
|
35
38
|
render: (ctx: { jobId: string; store: CliStore; quote: (ref: Omit<BoardRef, "id"> & { id?: string }) => void }) => ReactNode;
|
|
36
39
|
}
|
|
37
|
-
const TAB_LABEL: Record<BoardTab, string> = { files: "
|
|
40
|
+
const TAB_LABEL: Record<BoardTab, string> = { files: "工作区", changes: "改动", browser: "浏览器" };
|
|
38
41
|
/** 同时开着的文件页签最多几个:再开新的,关掉最早打开、当前没在看的那个。 */
|
|
39
42
|
export const MAX_FILE_TABS = 8;
|
|
40
43
|
const FILE_TAB = "file:";
|
|
@@ -53,12 +56,19 @@ const useIsoLayoutEffect = typeof window === "undefined" ? useEffect : useLayout
|
|
|
53
56
|
function saveTabs(jobId: string, files: string[], tab: string, history: BoardFile[]) {
|
|
54
57
|
try { sessionStorage.setItem(tabsKey(jobId), JSON.stringify({ files, tab, history })); } catch { /* 存不下就不记 */ }
|
|
55
58
|
}
|
|
56
|
-
/** 有输出就刷新文件,但两次之间至少隔这么久(智能体连写几十个文件时不至于每个都打一次中枢)。 */
|
|
57
|
-
const ACTIVITY_GAP_MS = 700;
|
|
58
59
|
|
|
59
60
|
export interface CliBoardProps extends CliStoreOptions, WorkspaceHandlers {
|
|
60
|
-
/**
|
|
61
|
+
/** @internal 同一个看板在 Chat 标题区放置页签。 */
|
|
62
|
+
inlineLayout?: InlineBoardLayout;
|
|
63
|
+
/** @internal 将当前 Chat 的打开请求交给自己的内嵌看板。 */
|
|
64
|
+
inlineOwner?: string;
|
|
65
|
+
/** @internal 手机抽屉沿用看板内容,独立控制它的可见状态。 */
|
|
66
|
+
visible?: boolean;
|
|
67
|
+
/** @internal 引用后返回当前 Chat。 */
|
|
68
|
+
onQuoteToChat?: () => void;
|
|
69
|
+
/** 默认在轮次结束、恢复页面和手动刷新时检查已打开文件与可见目录。收起后暂停文件读取。 */
|
|
61
70
|
watchFiles?: boolean;
|
|
71
|
+
/** @deprecated 保留兼容;文件同步由事件触发,不再使用定时间隔。 */
|
|
62
72
|
fileWatchIntervalMs?: number;
|
|
63
73
|
/** 看哪个对话(center 的 job_id)。不给就跟共享仓库的选中走(和 cli-msglist / cli-chat 联动)。 */
|
|
64
74
|
jobId?: string | null;
|
|
@@ -73,7 +83,7 @@ export interface CliBoardProps extends CliStoreOptions, WorkspaceHandlers {
|
|
|
73
83
|
readOnly?: boolean;
|
|
74
84
|
/** 文件上的自定义操作(预览头部的按钮)。 */
|
|
75
85
|
fileActions?: FileAction[];
|
|
76
|
-
/**
|
|
86
|
+
/** @deprecated 保留兼容;文件同步由事件触发,不再进行定时轮询。 */
|
|
77
87
|
pollMs?: number;
|
|
78
88
|
/** 接不接对话里的「打开文件 / 查看浏览器」,缺省接。 */
|
|
79
89
|
acceptOpen?: boolean;
|
|
@@ -113,15 +123,23 @@ export function CliBoard(props: CliBoardProps) {
|
|
|
113
123
|
return <CliErrorBoundary component="cli-board" theme={props.theme}>{withOwnProvider(props, <BoardInner {...props} inProvider={!!ctx} />, !!ctx)}</CliErrorBoundary>;
|
|
114
124
|
}
|
|
115
125
|
|
|
116
|
-
interface OpenFile { reloadKey?:number; path: string; size?: number; mtime?: number; version: number }
|
|
126
|
+
interface OpenFile { name?: string; reloadKey?:number; path: string; size?: number; mtime?: number; version: number }
|
|
117
127
|
|
|
118
|
-
function BoardInner({ jobId: jobIdProp, taskId: taskIdProp, tabs, availableTabs, defaultTab, readOnly, fileActions,
|
|
128
|
+
function BoardInner({ jobId: jobIdProp, taskId: taskIdProp, tabs, availableTabs, defaultTab, readOnly, fileActions, acceptOpen = true, quoteToChat, autoQuoteOpenFile, extraTabs, onCollapse, collapsed, onExpand, onNavigate, onEvent, onFilesChange, onFilesWatch, onBoardContextChange, watchFiles = true, fileWatchIntervalMs, theme = "light", className, style, inProvider, inlineLayout, inlineOwner, visible: visibilityOverride, onQuoteToChat }: CliBoardProps & { inProvider: boolean }) {
|
|
119
129
|
useComponentEvents(onEvent, {onFilesChange,onFilesWatch,onBoardContextChange}, inProvider);
|
|
120
130
|
const store = useCliStore();
|
|
121
131
|
const backend = store.client.board;
|
|
122
132
|
const selected = useCliState((s) => s.selected);
|
|
123
133
|
// 内部用对话键(见 core/conversation.ts):看板接口按它找对话任务里的那个对话。
|
|
124
134
|
const jobId = jobIdProp !== undefined ? (jobIdProp ? convKey(parseConvKey(jobIdProp).jobId, taskIdProp ?? parseConvKey(jobIdProp).taskId) : null) : selected;
|
|
135
|
+
const rootRef = useRef<HTMLElement | null>(null);
|
|
136
|
+
const isInline = !!inlineOwner || !!inlineLayout;
|
|
137
|
+
const cssVisible = useBoardVisibility(rootRef, !isInline);
|
|
138
|
+
const visible = !collapsed && (isInline ? visibilityOverride ?? inlineLayout?.active ?? true : cssVisible);
|
|
139
|
+
const layoutRef = useRef(inlineLayout);
|
|
140
|
+
layoutRef.current = inlineLayout;
|
|
141
|
+
const quoteCallbackRef = useRef(onQuoteToChat);
|
|
142
|
+
quoteCallbackRef.current = onQuoteToChat;
|
|
125
143
|
|
|
126
144
|
const storageId = jobId && store.draftScope ? `${store.draftScope}:${jobId}` : jobId;
|
|
127
145
|
const [info, setInfo] = useState<BoardInfo | null>(null);
|
|
@@ -131,12 +149,8 @@ function BoardInner({ jobId: jobIdProp, taskId: taskIdProp, tabs, availableTabs,
|
|
|
131
149
|
const [dir, setDir] = useState("");
|
|
132
150
|
const [files, setFiles] = useState<OpenFile[]>([]);
|
|
133
151
|
const [history,setHistory] = useState<BoardFile[]>([]);
|
|
134
|
-
//
|
|
135
|
-
// tick 进展(实时流有新输出 / 轮次变化,节流)、切回页面、手动刷新 —— 文件、改动、打开的文件都跟着刷新;
|
|
136
|
-
// pollTick 运行中的定时刷新 —— 只刷文件列表和打开的文件(改动要在节点上跑 git diff,不轮询);
|
|
137
|
-
// statusKey 轮次 / 对话状态变化 —— 才重新查看板概况(它会探测节点的浏览器接口,不随输出刷新)。
|
|
152
|
+
// 文件变化、恢复页面与手动刷新更新视图;流式轮末检查由 store 直接触发。
|
|
138
153
|
const [tick, setTick] = useState(0);
|
|
139
|
-
const [pollTick, setPollTick] = useState(0);
|
|
140
154
|
|
|
141
155
|
// 换了对话:回到初始状态。
|
|
142
156
|
const lastJob = useRef(jobId);
|
|
@@ -147,14 +161,8 @@ function BoardInner({ jobId: jobIdProp, taskId: taskIdProp, tabs, availableTabs,
|
|
|
147
161
|
}
|
|
148
162
|
// 恢复上次开着的文件页签(存在 sessionStorage):挂上之后、绘制之前做——服务端渲染和首次渲染一致,不会对不上。
|
|
149
163
|
const restored = useRef<string | null>(null);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
restored.current = storageId;
|
|
153
|
-
const saved = loadTabs(storageId);
|
|
154
|
-
setHistory(saved.history.map(file=>({...file,sourceId:file.sourceId??jobId??undefined})));
|
|
155
|
-
if (saved.files.length) setFiles((cur) => (cur.length ? cur : saved.files.map((path) => ({ path, version: 0 }))));
|
|
156
|
-
if (saved.tab) setTab((cur) => (cur === (defaultTab ?? tabs?.[0] ?? "files") ? saved.tab! : cur));
|
|
157
|
-
}, [storageId]);
|
|
164
|
+
const [hydrated, setHydrated] = useState<{ id: string | null; visible: boolean }>({ id: null, visible: false });
|
|
165
|
+
const restoreNeeded = hydrated.id !== storageId || (!isInline && visible && !hydrated.visible);
|
|
158
166
|
|
|
159
167
|
// 实时流:看板单独用时也要知道任务在不在跑(同一对话只连一次)。
|
|
160
168
|
useEffect(() => (jobId ? store.open(jobId) : undefined), [store, jobId]);
|
|
@@ -164,67 +172,58 @@ function BoardInner({ jobId: jobIdProp, taskId: taskIdProp, tabs, availableTabs,
|
|
|
164
172
|
const statusKey = conv
|
|
165
173
|
? `${conv.turns.map((t) => `${t.turn_id}:${t.status}`).join(",")}|${conv.job?.status ?? ""}|${conv.task?.id ?? ""}:${conv.task?.status ?? ""}:${conv.remoteTaskId ?? conv.task?.remote_task_id ?? ""}`
|
|
166
174
|
: "";
|
|
167
|
-
// 能改动文件的只有工具调用:签名 = 轮次状态 + 消息条数 + 最近一个工具拿没拿到结果(拿到结果就是文件刚写完)。
|
|
168
|
-
// 智能体逐字输出回复时条数和工具都不变,不触发刷新——几十个人同时看也不会每秒去节点上列一遍目录。
|
|
169
|
-
const lastTool = useMemo(() => {
|
|
170
|
-
const msgs = conv?.msgs ?? [];
|
|
171
|
-
for (let i = msgs.length - 1, n = 0; i >= 0 && n < 50; i--, n++) if (msgs[i].role === "tool") return `${msgs[i].id}:${toolDataOf(msgs[i]).result !== undefined}`;
|
|
172
|
-
return "";
|
|
173
|
-
}, [conv?.msgs]);
|
|
174
|
-
const activity = conv ? `${statusKey}|${conv.msgs.length}|${lastTool}` : "";
|
|
175
|
-
|
|
176
175
|
const bump = useCallback(() => setTick((n) => n + 1), []);
|
|
177
|
-
// 手动刷新(页签栏的 ↻):连看板概况一起重查。
|
|
178
176
|
const [manual, setManual] = useState(0);
|
|
179
|
-
const refreshAll = useCallback(() => {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (!activity) return;
|
|
184
|
-
const wait = Math.max(0, ACTIVITY_GAP_MS - (Date.now() - lastBump.current));
|
|
185
|
-
if (pending.current) clearTimeout(pending.current);
|
|
186
|
-
pending.current = setTimeout(() => { lastBump.current = Date.now(); bump(); }, wait);
|
|
187
|
-
return () => { if (pending.current) clearTimeout(pending.current); };
|
|
188
|
-
}, [activity, bump]);
|
|
189
|
-
useEffect(() => {
|
|
190
|
-
if (!running || !pollMs) return;
|
|
191
|
-
const t = setInterval(() => { if (document.visibilityState === "visible") setPollTick((n) => n + 1); }, pollMs);
|
|
192
|
-
return () => clearInterval(t);
|
|
193
|
-
}, [running, pollMs, bump]);
|
|
177
|
+
const refreshAll = useCallback(() => {
|
|
178
|
+
setManual((n) => n + 1); bump();
|
|
179
|
+
if (backend && jobId) retryWorkspaceFiles(backend, jobId, { manual: true });
|
|
180
|
+
}, [bump, backend, jobId]);
|
|
194
181
|
useEffect(() => {
|
|
195
182
|
const on = () => { if (document.visibilityState === "visible") bump(); };
|
|
196
183
|
document.addEventListener("visibilitychange", on);
|
|
197
184
|
return () => document.removeEventListener("visibilitychange", on);
|
|
198
185
|
}, [bump]);
|
|
199
186
|
|
|
200
|
-
//
|
|
187
|
+
// 派发和节点变化由已有聊天流驱动,不为文件看板再开启轮询。
|
|
201
188
|
useEffect(() => {
|
|
202
189
|
if (!backend || !jobId) return;
|
|
203
190
|
let alive = true;
|
|
204
|
-
|
|
205
|
-
const load = () => backend.info(jobId).then((i) => {
|
|
191
|
+
void backend.info(jobId).then((i) => {
|
|
206
192
|
if (!alive) return;
|
|
207
193
|
setInfo((old) => (old && old.dispatched === i.dispatched && old.remote_task_id === i.remote_task_id && old.files === i.files && old.browser === i.browser && old.node?.health === i.node?.health ? old : i));
|
|
208
194
|
setInfoError(null);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
setInfoError(e instanceof Error ? e.message : String(e));
|
|
213
|
-
timer = setTimeout(load, 5000);
|
|
214
|
-
});
|
|
215
|
-
void load();
|
|
216
|
-
return () => { alive = false; if (timer) clearTimeout(timer); };
|
|
217
|
-
}, [backend, jobId, statusKey, manual]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
195
|
+
}, (e: unknown) => { if (alive) setInfoError(e instanceof Error ? e.message : String(e)); });
|
|
196
|
+
return () => { alive = false; };
|
|
197
|
+
}, [backend, jobId, statusKey, manual]);
|
|
218
198
|
|
|
219
199
|
const builtinTabs = useMemo<string[]>(() => (availableTabs ?? ["files", "changes", "browser"]).filter(t => ["files","changes","browser"].includes(t)).filter(t => t !== "browser" || !info || info.browser !== false).filter(t => t !== "files" || !info || info.files !== false), [availableTabs, info?.browser, info?.files]);
|
|
220
200
|
const allowedPanels = [...builtinTabs, ...(extraTabs ?? []).map(t => t.id)];
|
|
221
201
|
const panels = useBoardPanels(jobId ? `center:${storageId}` : null, allowedPanels, [...(tabs ?? builtinTabs.filter(tab=>tab!=="changes")), ...(extraTabs ?? []).map(t => t.id)]);
|
|
202
|
+
useIsoLayoutEffect(() => {
|
|
203
|
+
if (!restoreNeeded) {
|
|
204
|
+
if (!visible && hydrated.visible) setHydrated({ id: storageId, visible: false });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (storageId) {
|
|
208
|
+
const current = jobId ? store.getState().boardContext[jobId] : undefined;
|
|
209
|
+
const saved = loadTabs(storageId);
|
|
210
|
+
const paths = current?.files ?? saved.files;
|
|
211
|
+
restored.current = storageId;
|
|
212
|
+
setFiles(previous => paths.map(path => previous.find(file => file.path === path) ?? { path, version: 0 }));
|
|
213
|
+
setHistory((current?.history ?? saved.history).map(file => ({ ...file, sourceId: file.sourceId ?? jobId ?? undefined })));
|
|
214
|
+
setDir(current?.dir ?? "");
|
|
215
|
+
const selectedTab = current?.tab ?? saved.tab;
|
|
216
|
+
if (selectedTab) { setTab(selectedTab); if (allowedPanels.includes(selectedTab)) panels.show(selectedTab); }
|
|
217
|
+
}
|
|
218
|
+
setHydrated({ id: storageId, visible });
|
|
219
|
+
}, [store, storageId, jobId, visible, restoreNeeded]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
222
220
|
const shownTabs = [...panels.open, ...(builtinTabs.includes("files") ? files.map(f => fileTabId(f.path)) : [])];
|
|
223
221
|
const activeTab = shownTabs.includes(tab) ? tab : shownTabs[0] ?? "";
|
|
224
222
|
const file = activeTab.startsWith(FILE_TAB) ? files.find((f) => fileTabId(f.path) === activeTab) ?? null : null;
|
|
225
223
|
useEffect(()=>{if(file&&jobId)setHistory(previous=>{const next=rememberBoardFile(previous,{path:file.path,sourceId:jobId});return JSON.stringify(next)===JSON.stringify(previous)?previous:next;});},[file?.path,jobId]);
|
|
226
|
-
const tabLabel = (t: string) => TAB_LABEL[t as BoardTab] ?? (t.startsWith(FILE_TAB) ? t.slice(FILE_TAB.length).split("/").pop() || t : undefined) ?? extraTabs?.find((x) => x.id === t)?.label ?? t;
|
|
224
|
+
const tabLabel = (t: string) => TAB_LABEL[t as BoardTab] ?? (t.startsWith(FILE_TAB) ? files.find(file => file.path === t.slice(FILE_TAB.length))?.name || t.slice(FILE_TAB.length).split("/").pop() || t : undefined) ?? extraTabs?.find((x) => x.id === t)?.label ?? t;
|
|
227
225
|
// 页签多了横向滚动:当前页签总是滚到看得见的地方(新开的文件页签在最后,窄看板里容易被挤出去)。
|
|
226
|
+
const [toolbarHost, setToolbarHost] = useState<HTMLSpanElement | null>(null);
|
|
228
227
|
const tabsRef = useRef<HTMLDivElement | null>(null);
|
|
229
228
|
const revealTab = useCallback(() => {
|
|
230
229
|
// 直接算滚动位置,不用 scrollIntoView:它会连带滚动宿主页面。
|
|
@@ -242,7 +241,7 @@ function BoardInner({ jobId: jobIdProp, taskId: taskIdProp, tabs, availableTabs,
|
|
|
242
241
|
let alive = true;
|
|
243
242
|
void document.fonts?.ready.then(() => { if (alive) revealTab(); });
|
|
244
243
|
return () => { alive = false; timers.forEach(clearTimeout); };
|
|
245
|
-
}, [activeTab, shownTabs.length, collapsed, revealTab]);
|
|
244
|
+
}, [activeTab, shownTabs.length, collapsed, visible, inlineLayout?.active, inlineLayout?.tabsHost, revealTab]);
|
|
246
245
|
// 看板宽度或页签宽度变了(分栏拖动、刚挂上时分栏才给出宽度、字体加载完页签变宽):再对一次。
|
|
247
246
|
useEffect(() => {
|
|
248
247
|
const box = tabsRef.current;
|
|
@@ -251,7 +250,7 @@ function BoardInner({ jobId: jobIdProp, taskId: taskIdProp, tabs, availableTabs,
|
|
|
251
250
|
ro.observe(box);
|
|
252
251
|
for (const child of box.children) ro.observe(child);
|
|
253
252
|
return () => ro.disconnect();
|
|
254
|
-
}, [collapsed, revealTab, shownTabs.length]);
|
|
253
|
+
}, [collapsed, visible, inlineLayout?.tabsHost, revealTab, shownTabs.length]);
|
|
255
254
|
// 记下开着的文件页签和当前页签(刷新页面后恢复)。恢复之前不写,免得用空的盖掉上次的。
|
|
256
255
|
useEffect(() => { if (storageId && restored.current === storageId) saveTabs(storageId, files.map((f) => f.path), activeTab, history); }, [storageId, files, activeTab, history]);
|
|
257
256
|
// 引用到对话:和对话在同一个 Provider 里时缺省开。
|
|
@@ -265,21 +264,25 @@ function BoardInner({ jobId: jobIdProp, taskId: taskIdProp, tabs, availableTabs,
|
|
|
265
264
|
},[store,backend,jobId,watchFiles,fileWatchIntervalMs,info?.dispatched,info?.files]);
|
|
266
265
|
const canQuote = (quoteToChat ?? inProvider) && !!jobId;
|
|
267
266
|
const quote = useCallback((ref: Omit<BoardRef, "id"> & { id?: string }) => {
|
|
268
|
-
if (jobId) store.addRef(jobId, { ...ref, id: ref.id ?? `${ref.kind}:${ref.label}` });
|
|
267
|
+
if (jobId) { store.addRef(jobId, { ...ref, id: ref.id ?? `${ref.kind}:${ref.label}` }); if (quoteCallbackRef.current) quoteCallbackRef.current(); else layoutRef.current?.onChat(); }
|
|
269
268
|
}, [store, jobId]);
|
|
270
|
-
const quoteText = useCallback((path: string, text: string) => {
|
|
269
|
+
const quoteText = useCallback((path: string, text: string, context?: { revision: string }) => {
|
|
271
270
|
const name = path.split("/").pop() || path;
|
|
272
271
|
const short = text.replace(/\s+/g, " ").slice(0, 24);
|
|
273
272
|
// 同一个文件里选不同的段落各算一条(按内容区分),同一段重复点只留一条。
|
|
274
|
-
quote({ id: `sel:${path}:${text.length}:${short}`, kind: "selection", label: `${name}:${short}${text.length > 24 ? "…" : ""}`, text:
|
|
273
|
+
quote({ id: `sel:${path}:${text.length}:${short}`, kind: "selection", label: `${name}:${short}${text.length > 24 ? "…" : ""}`, text: `${context ? `文件修订:${context.revision}\n` : ""}工作区文件 ${path} 里选中的这段:\n${text.split("\n").map((l) => ` > ${l}`).join("\n")}` });
|
|
275
274
|
}, [quote]);
|
|
276
275
|
// 正在看的东西(打开的文件、浏览器当前页面):告诉对话(缺省带进下一条消息一次)。收起、切走、关掉、离开时撤掉。
|
|
277
276
|
const autoView = (autoQuoteOpenFile ?? canQuote) && !!jobId;
|
|
278
277
|
const [browserPage, setBrowserPage] = useState<{ url: string; title: string } | null>(null);
|
|
279
|
-
const viewingPath = autoView &&
|
|
280
|
-
const viewingPage = autoView &&
|
|
278
|
+
const viewingPath = autoView && visible && file ? file.path : null;
|
|
279
|
+
const viewingPage = autoView && visible && activeTab === "browser" && browserPage ? browserPage : null;
|
|
281
280
|
// 这个看板自己的标识:同一个对话开着两个看板时,各撤各的「正在看」和看板状态。
|
|
282
281
|
const owner = useMemo(() => `board-${Math.random().toString(36).slice(2, 10)}`, []);
|
|
282
|
+
useIsoLayoutEffect(() => {
|
|
283
|
+
if (jobId && !isInline) store.reportBoardPresence(jobId, owner, visible, { availableTabs, readOnly });
|
|
284
|
+
}, [store, jobId, owner, visible, isInline, availableTabs?.join(","), readOnly]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
285
|
+
useIsoLayoutEffect(() => () => { if (jobId && !isInline) store.reportBoardPresence(jobId, owner, null); }, [store, jobId, owner, isInline]);
|
|
283
286
|
useEffect(() => {
|
|
284
287
|
if (!jobId || !autoView) return;
|
|
285
288
|
store.setViewing(jobId, viewingPath
|
|
@@ -303,14 +306,14 @@ function BoardInner({ jobId: jobIdProp, taskId: taskIdProp, tabs, availableTabs,
|
|
|
303
306
|
backend.stat(jobId, path).then((s) => {
|
|
304
307
|
if (!alive || s.type !== "file") return;
|
|
305
308
|
// 第一次拿到修改时间(从对话里打开的、刷新后恢复的)只记下来;之后变了才重读。
|
|
306
|
-
update((f) => (f.mtime === s.mtime ? f : { ...f, size: s.size, mtime: s.mtime, version: f.mtime === undefined ? f.version : f.version + 1 }));
|
|
309
|
+
update((f) => (f.mtime === s.mtime && f.name === s.name ? f : { ...f, name: s.name, size: s.size, mtime: s.mtime, version: f.mtime === undefined ? f.version : f.version + 1 }));
|
|
307
310
|
}, (e: unknown) => {
|
|
308
311
|
// 打开着的文件被删了:重读一次,预览里显示「文件不存在」,不继续显示旧内容。
|
|
309
|
-
if (alive && e
|
|
312
|
+
if (alive && (e as { status?: number }).status === 404) update((f) => (f.mtime !== -1 ? { ...f, mtime: -1, version: f.version + 1 } : f));
|
|
310
313
|
});
|
|
311
314
|
return () => { alive = false; };
|
|
312
315
|
// 只看正在看的那个文件:切到别的文件页签时查一次它(后台的页签不轮询)。
|
|
313
|
-
}, [tick,
|
|
316
|
+
}, [tick, file?.path, info?.dispatched]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
314
317
|
|
|
315
318
|
const activeRef = useRef(activeTab);
|
|
316
319
|
activeRef.current = activeTab;
|
|
@@ -320,8 +323,8 @@ function BoardInner({ jobId: jobIdProp, taskId: taskIdProp, tabs, availableTabs,
|
|
|
320
323
|
if (!clean) return;
|
|
321
324
|
setFiles((list) => {
|
|
322
325
|
const have = list.find((f) => f.path === clean);
|
|
323
|
-
if (have) return list.map((f) => (f.path === clean ? { ...f, size: entry?.size ?? f.size, mtime: entry?.mtime ?? f.mtime, version: f.version + 1 } : f));
|
|
324
|
-
const next = [...list, { path: clean, size: entry?.size, mtime: entry?.mtime, version: 0 }];
|
|
326
|
+
if (have) return list.map((f) => (f.path === clean ? { ...f, name: entry?.name ?? f.name, size: entry?.size ?? f.size, mtime: entry?.mtime ?? f.mtime, version: f.version + 1 } : f));
|
|
327
|
+
const next = [...list, { path: clean, name: entry?.name, size: entry?.size, mtime: entry?.mtime, version: 0 }];
|
|
325
328
|
while (next.length > MAX_FILE_TABS) {
|
|
326
329
|
const i = next.findIndex((f) => fileTabId(f.path) !== activeRef.current && f.path !== clean);
|
|
327
330
|
next.splice(i >= 0 ? i : 0, 1);
|
|
@@ -332,6 +335,13 @@ function BoardInner({ jobId: jobIdProp, taskId: taskIdProp, tabs, availableTabs,
|
|
|
332
335
|
const parent = clean.includes("/") && !clean.startsWith("/") ? clean.split("/").slice(0, -1).join("/") : "";
|
|
333
336
|
setDir(parent);
|
|
334
337
|
}, []);
|
|
338
|
+
const uploadToWorkspace = useMemo(() => !readOnly && jobId && backend?.uploadFile
|
|
339
|
+
? (file: File, options: import("../core/documentUpload").DocumentUploadOptions) => backend.uploadFile!(jobId, file, options)
|
|
340
|
+
: undefined, [backend, jobId, readOnly]);
|
|
341
|
+
const upload = useBoardUpload(jobId, uploadToWorkspace, receipt => {
|
|
342
|
+
openPath(receipt.path!, { name: receipt.name, size: receipt.size });
|
|
343
|
+
setTick(value => value + 1);
|
|
344
|
+
});
|
|
335
345
|
/** 关文件页签:关的是正在看的,就切到它旁边的文件页签,没有了回文件列表。 */
|
|
336
346
|
const closeFile = useCallback((path: string) => {
|
|
337
347
|
setFiles((list) => {
|
|
@@ -348,6 +358,8 @@ function BoardInner({ jobId: jobIdProp, taskId: taskIdProp, tabs, availableTabs,
|
|
|
348
358
|
jobRef.current = jobId;
|
|
349
359
|
const expandRef = useRef(onExpand);
|
|
350
360
|
expandRef.current = onExpand;
|
|
361
|
+
const visibleRef = useRef(visible);
|
|
362
|
+
visibleRef.current = visible;
|
|
351
363
|
useEffect(() => {
|
|
352
364
|
if (!acceptOpen || !backend) return;
|
|
353
365
|
return store.registerBoard((target: string, what: OpenTarget) => {
|
|
@@ -359,30 +371,38 @@ function BoardInner({ jobId: jobIdProp, taskId: taskIdProp, tabs, availableTabs,
|
|
|
359
371
|
setTab(targetPanel);
|
|
360
372
|
if (what.kind === "files") setDir(what.path ?? "");
|
|
361
373
|
}
|
|
362
|
-
expandRef.current?.();
|
|
374
|
+
if (layoutRef.current) layoutRef.current.onActivate(); else expandRef.current?.();
|
|
363
375
|
return true;
|
|
364
|
-
});
|
|
365
|
-
}, [store, backend, acceptOpen, openPath, builtinTabs.join(","), allowedPanels.join(","), panels.show]);
|
|
376
|
+
}, { owner: inlineOwner ?? owner, inline: isInline, priority: () => isInline ? 1 : visibleRef.current ? 2 : 0 });
|
|
377
|
+
}, [store, backend, acceptOpen, openPath, builtinTabs.join(","), allowedPanels.join(","), panels.show, inlineOwner, owner, isInline]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
366
378
|
|
|
367
379
|
const navRef = useRef(onNavigate);
|
|
368
380
|
navRef.current = onNavigate;
|
|
369
381
|
useEffect(() => { navRef.current?.({ tab: activeTab, dir, file: file?.path ?? null }); }, [activeTab, dir, file?.path]);
|
|
370
382
|
// 看板现在的样子报给仓库:业务方用 store.workspace().openFile() / openFiles() 拿到当前打开的文件。
|
|
371
|
-
const openPaths = files.map((f) => f.path)
|
|
383
|
+
const openPaths = JSON.stringify(files.map((f) => f.path));
|
|
384
|
+
const filesEnabled = builtinTabs.includes("files");
|
|
385
|
+
useEffect(() => {
|
|
386
|
+
if (!backend || !jobId || !watchFiles || !visible || !filesEnabled) return;
|
|
387
|
+
return retainFileScope(backend, jobId, { paths: JSON.parse(openPaths) as string[], dirs: [] });
|
|
388
|
+
}, [backend, jobId, watchFiles, visible, openPaths, filesEnabled]);
|
|
372
389
|
useEffect(() => {
|
|
373
|
-
if (!jobId) return;
|
|
374
|
-
|
|
375
|
-
|
|
390
|
+
if (!jobId || restoreNeeded) return;
|
|
391
|
+
const currentFile = isInline && !visible ? null : file;
|
|
392
|
+
store.reportBoard(jobId, { tab: activeTab, dir, file: currentFile?.path ?? null, files: files.map(file=>file.path), current:file?{path:file.path,sourceId:jobId}:null,opened:files.map(file=>({path:file.path,sourceId:jobId})),history,browser:activeTab==="browser"?browserPage:null,visible,autoContext:autoQuoteOpenFile??canQuote }, owner);
|
|
393
|
+
}, [store, jobId, activeTab, dir, file?.path, openPaths, history, browserPage, visible, autoQuoteOpenFile, canQuote, owner, restoreNeeded]);
|
|
376
394
|
useEffect(() => () => { if (jobId) store.reportBoard(jobId, null, owner); }, [store, jobId, owner]);
|
|
377
395
|
|
|
378
396
|
const body = () => {
|
|
379
397
|
if (!activeTab) return <div className="cli-board-empty">看板模块已全部关闭。通过右上角“管理看板模块”重新打开。</div>;
|
|
380
398
|
if (!backend) return <div className="tc-note">当前后端不支持看板(需要任务中心提供 /api/jobs/:id/board)。</div>;
|
|
381
399
|
if (!jobId) return <div className="tc-note">选择或开始一个对话后,这里显示它的文件、改动和浏览器。</div>;
|
|
382
|
-
if (!info) return <div className="tc-note">{infoError ?
|
|
400
|
+
if (!info) return <div className="tc-note">{infoError ? <>读取看板失败:{infoError} <button className="cli-fv-btn" onClick={refreshAll}>重试</button></> : "读取中…"}</div>;
|
|
383
401
|
if (!info.dispatched) return <div className="tc-note">任务还没开始执行({info.detail ?? "等待分配执行节点"})。开始后这里会显示工作区文件。</div>;
|
|
402
|
+
if (!visible) return null;
|
|
384
403
|
if (activeTab === "changes") return <ChangesPane key={jobId} backend={backend} jobId={jobId} refreshKey={tick} onOpen={(p) => openPath(p)} />;
|
|
385
404
|
if (activeTab === "browser") {
|
|
405
|
+
if (!visible) return null;
|
|
386
406
|
return <BrowserPane key={`${jobId}:${info.remote_task_id}`} backend={backend} jobId={jobId} readOnly={readOnly}
|
|
387
407
|
onPage={setBrowserPage}
|
|
388
408
|
onQuote={canQuote ? (p) => quote({ id: "browser:page", kind: "browser", label: p.title || p.url, text: `任务浏览器当前打开的页面:${p.title ? `${p.title} — ` : ""}${p.url}` }) : undefined}
|
|
@@ -396,27 +416,28 @@ function BoardInner({ jobId: jobIdProp, taskId: taskIdProp, tabs, availableTabs,
|
|
|
396
416
|
if (extra) return extra.render({ jobId, store, quote });
|
|
397
417
|
if (activeTab.startsWith(FILE_TAB) && file) {
|
|
398
418
|
return (
|
|
399
|
-
<FileViewer
|
|
419
|
+
<FileViewer toolbarHost={toolbarHost}
|
|
400
420
|
key={file.path}
|
|
401
421
|
backend={backend}
|
|
402
422
|
jobId={jobId}
|
|
403
423
|
path={file.path}
|
|
424
|
+
displayName={file.name}
|
|
404
425
|
size={file.size}
|
|
405
426
|
mtime={file.mtime}
|
|
406
|
-
version={file.version} reloadKey={file.reloadKey}
|
|
427
|
+
version={file.version + manual} reloadKey={file.reloadKey}
|
|
407
428
|
actions={allFileActions}
|
|
408
429
|
onOpenPath={(p) => openPath(p)}
|
|
409
|
-
onQuoteText={canQuote ? (text) => quoteText(file.path, text) : undefined}
|
|
410
|
-
|
|
430
|
+
onQuoteText={canQuote ? (text, context) => quoteText(file.path, text, context) : undefined}
|
|
431
|
+
onBack={() => { setDir(file.path.includes("/") ? file.path.slice(0, file.path.lastIndexOf("/")) : ""); panels.show("files"); setTab("files"); }}
|
|
411
432
|
/>
|
|
412
433
|
);
|
|
413
434
|
}
|
|
414
|
-
return <FilesPane key={jobId} backend={backend} jobId={jobId} dir={dir} onDir={setDir} onOpen={(e) => openPath(e.path, e)} refreshKey={tick
|
|
435
|
+
return <FilesPane toolbarHost={toolbarHost} key={jobId} backend={backend} jobId={jobId} dir={dir} onDir={setDir} onOpen={(e) => openPath(e.path, e)} refreshKey={tick} />;
|
|
415
436
|
};
|
|
416
437
|
|
|
417
438
|
if (collapsed) {
|
|
418
439
|
return (
|
|
419
|
-
<aside className={`cli-ui cli-board collapsed ${className ?? ""}`} data-theme={theme} style={style}>
|
|
440
|
+
<aside ref={rootRef} className={`cli-ui cli-board collapsed ${className ?? ""}`} data-theme={theme} style={style}>
|
|
420
441
|
<button className="cli-board-strip" onClick={onExpand} title="显示看板">
|
|
421
442
|
{Icon.chevRight()}
|
|
422
443
|
{shownTabs.map((t) => <span key={t} className="cli-board-strip-t">{tabLabel(t)}</span>)}
|
|
@@ -426,32 +447,39 @@ function BoardInner({ jobId: jobIdProp, taskId: taskIdProp, tabs, availableTabs,
|
|
|
426
447
|
);
|
|
427
448
|
}
|
|
428
449
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
<div className="rail-tabs-scroll" role="tablist" ref={tabsRef}>
|
|
450
|
+
const selectTab = (id: string) => { setTab(id); layoutRef.current?.onActivate(); };
|
|
451
|
+
const tabBar = <div className="rail-tabs">
|
|
452
|
+
<div className="rail-tabs-scroll" role={inlineLayout ? undefined : "tablist"} ref={tabsRef}>
|
|
433
453
|
{shownTabs.map((t) => t.startsWith(FILE_TAB) ? (
|
|
434
|
-
<span key={t} className={`rail-tab rail-ftab ${activeTab === t ? "on" : ""}`}
|
|
454
|
+
<span key={t} className={`rail-tab rail-ftab ${activeTab === t && (!inlineLayout || inlineLayout.active) ? "on" : ""}`} title={t.slice(FILE_TAB.length)}
|
|
435
455
|
onMouseDown={(e) => { if (e.button === 1) { e.preventDefault(); closeFile(t.slice(FILE_TAB.length)); } }}>
|
|
436
|
-
<button className="rail-ftab-l" onClick={() =>
|
|
456
|
+
<button className="rail-ftab-l" role="tab" aria-selected={activeTab === t && (!inlineLayout || inlineLayout.active)} aria-label={tabLabel(t)} onClick={() => selectTab(t)}>{Icon.doc()}<span className="rail-ftab-text">{tabLabel(t)}</span></button>
|
|
437
457
|
<button className="rail-ftab-x" onClick={() => closeFile(t.slice(FILE_TAB.length))} title="关闭(鼠标中键也可以)" aria-label={`关闭 ${tabLabel(t)}`}>{Icon.x()}</button>
|
|
438
458
|
</span>
|
|
439
459
|
) : (
|
|
440
|
-
<PanelTab key={t} id={t} label={tabLabel(t)} active={activeTab === t} onSelect={() =>
|
|
460
|
+
<PanelTab key={t} id={t} label={tabLabel(t)} active={activeTab === t && (!inlineLayout || inlineLayout.active)} onSelect={() => selectTab(t)} onClose={() => {
|
|
441
461
|
panels.close(t);
|
|
442
462
|
if (activeTab === t) setTab(shownTabs.find(id => id !== t) ?? "");
|
|
463
|
+
if (inlineLayout && shownTabs.length === 1) inlineLayout.onChat();
|
|
443
464
|
}} />
|
|
444
465
|
))}
|
|
445
466
|
</div>
|
|
446
|
-
<PanelControls available={allowedPanels} open={panels.open} label={tabLabel}
|
|
447
|
-
|
|
467
|
+
<PanelControls toolbarRef={setToolbarHost} available={allowedPanels} open={panels.open} label={tabLabel}
|
|
468
|
+
upload={!readOnly && builtinTabs.includes("files") && uploadToWorkspace ? { onChoose: upload.choose, disabled: upload.busy || !info?.dispatched, hint: !info?.dispatched ? "工作区尚未就绪" : "保存到当前会话,不发送消息" } : undefined}
|
|
469
|
+
onShow={id => { panels.show(id); selectTab(id); }}
|
|
448
470
|
onClose={id => { panels.close(id); if (activeTab === id) setTab(shownTabs.find(t => t !== id) ?? ""); }} />
|
|
449
|
-
{info?.dispatched && <button className="rail-collapse" onClick={refreshAll} title={running ? "运行中 ·
|
|
471
|
+
{info?.dispatched && !file && <button className="rail-collapse" onClick={refreshAll} title={running ? "运行中 · 本轮结束后同步,点击立即检查" : "刷新"}>
|
|
450
472
|
<span className={`cli-sync-dot ${running ? "on" : ""}`} />↻
|
|
451
473
|
</button>}
|
|
452
474
|
{onCollapse && <button className="rail-collapse" onClick={onCollapse} title="隐藏面板">{Icon.chevRight()}</button>}
|
|
453
|
-
</div
|
|
475
|
+
</div>;
|
|
476
|
+
return <>
|
|
477
|
+
{inlineLayout?.tabsHost && createPortal(tabBar, inlineLayout.tabsHost)}
|
|
478
|
+
<aside ref={rootRef} className={`cli-ui cli-board sess-rail${isInline ? " cli-inline-board" : ""} ${className ?? ""}`} data-theme={theme} hidden={isInline && !visible} style={{ ...style, ...(isInline && !visible ? { display: "none" } : {}) }} onPointerDown={()=>jobId&&store.activateBoard(jobId,owner)} onClickCapture={()=>jobId&&store.activateBoard(jobId,owner)} onFocusCapture={()=>jobId&&store.activateBoard(jobId,owner)}>
|
|
479
|
+
{!inlineLayout && tabBar}
|
|
480
|
+
{upload.input}
|
|
481
|
+
{upload.progress}
|
|
454
482
|
<div className="rail-scroll">{body()}</div>
|
|
455
483
|
</aside>
|
|
456
|
-
|
|
484
|
+
</>;
|
|
457
485
|
}
|