@xgspz/cli-ui 0.6.15 → 0.6.16
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 +24 -0
- package/README.md +21 -1
- package/dist/{cli-ui-embed-vIXVNs6k.js → cli-ui-embed-D9xgur2y.js} +18299 -18587
- package/dist/{cli-ui-index-R24x28OX.js → cli-ui-index-WBtxmw23.js} +2 -2
- package/dist/{cli-ui-jszip.min-DUghzByz.js → cli-ui-jszip.min-CU3ZzoXb.js} +1 -1
- package/dist/{cli-ui-jszip.min-DfH6rdQD.js → cli-ui-jszip.min-DdU0X3VJ.js} +2 -2
- package/dist/{cli-ui-xlsx-CP0lJhff.js → cli-ui-xlsx-Dx6T7qhC.js} +1 -1
- package/dist/cli-ui.css +1 -1
- package/dist/cli-ui.js +1 -1
- package/package.json +1 -1
- package/src/board/ChangesPane.tsx +10 -4
- package/src/board/CliBoard.tsx +48 -16
- package/src/board/FileViewer.tsx +34 -1
- package/src/board/diffRefresh.ts +63 -0
- package/src/board/linkage.ts +68 -0
- package/src/board/pathResolve.ts +108 -0
- package/src/chat/CliChat.tsx +76 -181
- package/src/chat/Composer.tsx +4 -3
- package/src/core/conversation.ts +9 -1
- package/src/core/store.ts +112 -38
- package/src/msglist/CliMsgList.tsx +3 -2
- package/src/operator/node/AgentConnect.tsx +19 -15
- package/src/operator/node/AppearancePanel.tsx +1 -1
- package/src/operator/node/BranchNavigator.tsx +1 -1
- package/src/operator/node/BrowserPane.tsx +1 -1
- package/src/operator/node/Chat.tsx +16 -261
- package/src/operator/node/CommandPalette.tsx +1 -1
- package/src/operator/node/Composer.tsx +1 -1
- package/src/operator/node/Layout.tsx +1 -1
- package/src/operator/node/Markdown.tsx +6 -36
- package/src/operator/node/Modal.tsx +1 -1
- package/src/operator/node/OnboardingWizard.tsx +9 -5
- package/src/operator/node/Orchestrator.tsx +1 -1
- package/src/operator/node/ProjectLanding.tsx +1 -1
- package/src/operator/node/ProjectsColumn.tsx +1 -1
- package/src/operator/node/ProviderAuth.tsx +12 -10
- package/src/operator/node/RunSwitch.tsx +1 -1
- package/src/operator/node/Services.tsx +1 -1
- package/src/operator/node/SessionRail.tsx +27 -8
- package/src/operator/node/SessionView.tsx +1 -1
- package/src/operator/node/SettingsView.tsx +1 -1
- package/src/operator/node/TaskBoard.tsx +1 -1
- package/src/operator/node/TaskChanges.tsx +34 -5
- package/src/operator/node/TaskHero.tsx +1 -1
- package/src/operator/node/TasksColumn.tsx +1 -1
- package/src/operator/node/Transcript.tsx +10 -237
- package/src/operator/node/Welcome.tsx +1 -1
- package/src/operator/node/WorktreePrune.tsx +1 -1
- package/src/operator/node/boardInteraction.ts +6 -3
- package/src/operator/node/boardState.ts +12 -13
- package/src/operator/node/fileLinks.ts +4 -38
- package/src/operator/node/format.ts +12 -104
- package/src/operator/node/github.tsx +10 -8
- package/src/operator/node/modals.tsx +15 -13
- package/src/operator/node/recheckOnReturn.ts +28 -0
- package/src/operator/node/shared.tsx +10 -50
- package/src/operator/node/useTaskStream.ts +2 -2
- package/src/styles.css +5 -0
- package/src/ui/Markdown.tsx +36 -21
- package/src/ui/Transcript.tsx +8 -5
- package/src/ui/fileLinks.ts +6 -3
- package/src/ui/format.ts +11 -5
- package/src/ui/shared.tsx +11 -7
- package/src/ui/transcriptModel.ts +40 -9
- package/src/ui/useTranscriptWindow.ts +253 -0
- package/src/operator/node/boundedPoll.ts +0 -47
- package/src/operator/node/icons.tsx +0 -88
- package/src/operator/node/transcriptModel.ts +0 -129
package/src/core/store.ts
CHANGED
|
@@ -16,8 +16,10 @@ import { notifyCallback } from "./callbacks";
|
|
|
16
16
|
import { CenterError, createCenterClient, firstLine, type CenterClient, type CenterClientOptions } from "./client";
|
|
17
17
|
import { addTurnsIfAbsent, applyItems, applyOlderPage, applySnapshot, closedCallback, convEnded, convKey, emptyConv, isChatTask, NEW_TASK, parseConvKey, TERMINAL_JOB, TERMINAL_TURN, turnCallback, upsertTurns, type ConvState } from "./conversation";
|
|
18
18
|
import { formatAnswersText } from "../ui/format";
|
|
19
|
+
import { KEEP_TRANSCRIPTS, trimToTail } from "../ui/transcriptModel";
|
|
19
20
|
import { backgroundBlock, charCount, MAX_MESSAGE_CHARS, serializeBlock } from "./blocks";
|
|
20
|
-
import { cloneBoardView,
|
|
21
|
+
import { cloneBoardView, mergeBoardHistory, rememberBoardFile } from "../board/context";
|
|
22
|
+
import { createBoardOwners, createBoardSurfaces } from "../board/linkage";
|
|
21
23
|
import { cloneWorkspaceChange, subscribeWorkspaceFiles, retryWorkspaceFiles, type WorkspaceWatchOptions } from "../board/fileWatch";
|
|
22
24
|
import { retainFileScope } from "../board/fileScope";
|
|
23
25
|
import type { WorkspaceChangeEvent, WorkspaceWatchStatus } from "./types";
|
|
@@ -193,6 +195,12 @@ export const jitter = (ms: number) => Math.round(ms * (0.7 + Math.random() * 0.6
|
|
|
193
195
|
* 很多人开着页面不看时,中枢和节点不用一直为他们推数据。
|
|
194
196
|
*/
|
|
195
197
|
export const PARK_HIDDEN_MS = 5 * 60_000;
|
|
198
|
+
/**
|
|
199
|
+
* 没人看的对话(对话、看板都不看了)最多留这么多个的记录在内存里,每个离开时截到最新 SNAPSHOT_TAIL 条(往上翻过的更早消息丢掉,
|
|
200
|
+
* 再往上翻时重新取):来回切换最近看过的仍然立刻显示,更早的整个释放,再打开时从实时流重新读。
|
|
201
|
+
* 与节点适配(useTaskStream)同一条规则、同一组常量(ui/transcriptModel 的 KEEP_TRANSCRIPTS / trimToTail)。
|
|
202
|
+
*/
|
|
203
|
+
export const IDLE_CONV_MAX = KEEP_TRANSCRIPTS;
|
|
196
204
|
/**
|
|
197
205
|
* 401 / 403:多半是宿主的登录会话过期了。隔 30 秒再试,最多 AUTH_RETRIES 次;之后停下等事件
|
|
198
206
|
* (宿主续上会话后切回页面、获得焦点或调用 store.reconnect() 即恢复),不无限重试。
|
|
@@ -359,11 +367,14 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
359
367
|
}
|
|
360
368
|
|
|
361
369
|
const refreshAgain = new Set<string>();
|
|
370
|
+
/** 每个对话任务拉过几次详情(成功或失败都算):列表排队期间别处拉过了就不再重复。 */
|
|
371
|
+
const fetches = new Map<string, number>();
|
|
362
372
|
async function refreshJob(jobId: string, force = false) {
|
|
363
373
|
const entry = state.jobs[jobId];
|
|
364
374
|
// 正在拉:拉完再补一次,免得错过这期间的变化(例如轮次刚从排队变成执行中)。
|
|
365
375
|
if (entry?.loading) { refreshAgain.add(jobId); return; }
|
|
366
376
|
if (!force && entry && Date.now() - entry.fetchedAt < JOB_REFRESH_GAP_MS) return;
|
|
377
|
+
fetches.set(jobId, (fetches.get(jobId) ?? 0) + 1);
|
|
367
378
|
patchJob(jobId, { loading: true });
|
|
368
379
|
try {
|
|
369
380
|
const job = await client.getJob(jobId);
|
|
@@ -374,8 +385,11 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
374
385
|
firstInput = first?.turns[0]?.input ?? "";
|
|
375
386
|
}
|
|
376
387
|
patchJob(jobId, { job, firstInput, loading: false, error: null, fetchedAt: Date.now() });
|
|
377
|
-
//
|
|
378
|
-
|
|
388
|
+
// 新数据里已包含实时用量,清零累计(这个对话任务里的每个对话)。已经是 0 的不动:不然每次刷新都换一个对话对象,
|
|
389
|
+
// 看着它的对话组件会把整份消息重算、重画一遍。
|
|
390
|
+
for (const conv of Object.values(state.convs)) {
|
|
391
|
+
if (conv.jobId === jobId && (conv.liveUsage.fresh || conv.liveUsage.cacheRead)) patchConv(conv.key, (c) => ({ ...c, liveUsage: { fresh: 0, cacheRead: 0 } }));
|
|
392
|
+
}
|
|
379
393
|
} catch (error) {
|
|
380
394
|
patchJob(jobId, { loading: false, error: error instanceof Error ? error.message : String(error), fetchedAt: Date.now() });
|
|
381
395
|
// 本地记住的对话在 center 已经不存在(被删、换了环境):从列表里拿掉。宿主给的 jobIds 不动,由宿主决定。
|
|
@@ -388,6 +402,38 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
388
402
|
if (refreshAgain.delete(jobId)) void refreshJob(jobId, true);
|
|
389
403
|
}
|
|
390
404
|
|
|
405
|
+
/** 只要拿到详情就行(打开对话、列表补拉):已经在拉的不再排一次「拉完再补」。 */
|
|
406
|
+
function loadJob(jobId: string, force = false) {
|
|
407
|
+
if (state.jobs[jobId]?.loading) return Promise.resolve();
|
|
408
|
+
return refreshJob(jobId, force);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// 列表的后台拉取(首次加载、刷新、补拉新加进来的)共用一个并发上限 LIST_CONCURRENCY;
|
|
412
|
+
// 同一个任务排着或正在拉时不再排第二次。打开对话、发消息这类用户在等的不走这里。
|
|
413
|
+
const listQueue = new Map<string, Promise<void>>();
|
|
414
|
+
let listActive = 0;
|
|
415
|
+
const listWaiting: Array<() => void> = [];
|
|
416
|
+
function queueListJob(jobId: string, force: boolean): Promise<void> {
|
|
417
|
+
const queued = listQueue.get(jobId);
|
|
418
|
+
if (queued) return queued;
|
|
419
|
+
const seen = fetches.get(jobId) ?? 0;
|
|
420
|
+
const run = (async () => {
|
|
421
|
+
if (listActive >= LIST_CONCURRENCY) await new Promise<void>((resolve) => listWaiting.push(resolve));
|
|
422
|
+
listActive += 1;
|
|
423
|
+
try {
|
|
424
|
+
// 排队期间别处已经开始拉了(打开了这个对话、轮次变化):不再重复。
|
|
425
|
+
if ((fetches.get(jobId) ?? 0) !== seen) return;
|
|
426
|
+
await loadJob(jobId, force);
|
|
427
|
+
} finally {
|
|
428
|
+
listActive -= 1;
|
|
429
|
+
listWaiting.shift()?.();
|
|
430
|
+
}
|
|
431
|
+
})();
|
|
432
|
+
listQueue.set(jobId, run);
|
|
433
|
+
void run.finally(() => { if (listQueue.get(jobId) === run) listQueue.delete(jobId); });
|
|
434
|
+
return run;
|
|
435
|
+
}
|
|
436
|
+
|
|
391
437
|
/**
|
|
392
438
|
* 刷新列表。缺省只刷还没结束的对话(结束了的不会再变,轮询不必每次都拉);all = true 全部重拉(首次加载、手动刷新)。
|
|
393
439
|
*/
|
|
@@ -398,13 +444,18 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
398
444
|
const status = state.jobs[id]?.job?.status;
|
|
399
445
|
return !status || !TERMINAL_JOB.has(status);
|
|
400
446
|
});
|
|
401
|
-
await
|
|
447
|
+
await Promise.all(ids.map((id) => queueListJob(id, true)));
|
|
402
448
|
set({ listLoading: false });
|
|
403
449
|
} catch (error) {
|
|
404
450
|
set({ listLoading: false, listError: error instanceof Error ? error.message : String(error) });
|
|
405
451
|
}
|
|
406
452
|
}
|
|
407
453
|
|
|
454
|
+
/** 列表里还没有数据的对话任务(宿主新给的、别的组件新建的):排进列表的后台拉取(列表组件用)。 */
|
|
455
|
+
function loadMissingJobs() {
|
|
456
|
+
for (const id of state.jobIds) if (!state.jobs[id]) void queueListJob(id, false);
|
|
457
|
+
}
|
|
458
|
+
|
|
408
459
|
// ── 实时流(引用计数 + 断线重连) ─────────────────────
|
|
409
460
|
|
|
410
461
|
// synced:这个流已经收到过快照;之后的快照是断线重连后的补齐。
|
|
@@ -430,6 +481,8 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
430
481
|
if (streams.get(key) !== entry || entry.refs <= 0) return;
|
|
431
482
|
const { jobId, taskId } = parseConvKey(key);
|
|
432
483
|
let ended = false, retired = false, snapshotReceived = false;
|
|
484
|
+
// 这条连接上节点回合结束时查过文件、当时的工具执行次数(ConvState.edits);中枢随后把这一轮标成结束时据此去重。
|
|
485
|
+
let roundEndEdits: number | null = null;
|
|
433
486
|
let endReason: string | null = null;
|
|
434
487
|
let cleanup: (() => void) | null = null;
|
|
435
488
|
const owns = () => streams.get(key) === entry && entry.refs > 0 && entry.stop === retire;
|
|
@@ -526,11 +579,18 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
526
579
|
// applyItems also rejects events from another batch task's node session.
|
|
527
580
|
if (next === previous) continue;
|
|
528
581
|
// Observe every event: end + next user in one chunk can leave running=true.
|
|
529
|
-
if (item.type === "
|
|
582
|
+
if (item.type === "transcript") { refresh = true; roundEndEdits = null; }
|
|
583
|
+
// 节点上一个回合结束就查文件(中枢的这一轮可能还没结束:节点接着跑下一回合)。
|
|
584
|
+
if (item.type === "node" && item.event.type === "turn_end") { refresh = true; roundEndEdits = next.edits; }
|
|
530
585
|
// 有轮次的对话以中枢把这一轮标成结束为准(节点已停、轮末保存已做);没有轮次的看节点 turn_end。
|
|
531
586
|
if (item.type === "node" && item.event.type === "turn_end" && !next.turns.length) settled = true;
|
|
532
587
|
if (item.type === "turn" && item.turn.status !== "merged" && TERMINAL_TURN.has(item.turn.status)
|
|
533
|
-
&& !TERMINAL_TURN.has(previous.turns.find((turn) => turn.turn_id === item.turn.turn_id)?.status ?? "")) {
|
|
588
|
+
&& !TERMINAL_TURN.has(previous.turns.find((turn) => turn.turn_id === item.turn.turn_id)?.status ?? "")) {
|
|
589
|
+
settled = true;
|
|
590
|
+
// 节点回合结束时已经查过、之后再没有工具执行完:文件不会变,不再查第二次。
|
|
591
|
+
if (roundEndEdits !== next.edits) refresh = true;
|
|
592
|
+
roundEndEdits = null;
|
|
593
|
+
}
|
|
534
594
|
}
|
|
535
595
|
return settled ? { ...next, settled: next.settled + 1 } : next;
|
|
536
596
|
});
|
|
@@ -608,7 +668,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
608
668
|
function open(key: string): () => void {
|
|
609
669
|
const { jobId, taskId } = parseConvKey(key);
|
|
610
670
|
if (taskId === NEW_TASK) {
|
|
611
|
-
void
|
|
671
|
+
void loadJob(jobId);
|
|
612
672
|
return () => undefined;
|
|
613
673
|
}
|
|
614
674
|
let entry = streams.get(key);
|
|
@@ -619,7 +679,9 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
619
679
|
}
|
|
620
680
|
entry.refs += 1;
|
|
621
681
|
if (entry.refs === 1) {
|
|
622
|
-
|
|
682
|
+
markActive(key);
|
|
683
|
+
// 列表正在拉这个任务(页面刚打开时常见):用它的结果,不再排一次「拉完再补」。
|
|
684
|
+
void loadJob(jobId);
|
|
623
685
|
connect(key);
|
|
624
686
|
}
|
|
625
687
|
const owned = entry;
|
|
@@ -638,10 +700,35 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
638
700
|
settleDrop(current);
|
|
639
701
|
syncLive();
|
|
640
702
|
current.stop?.();
|
|
641
|
-
if (!streams.has(key))
|
|
703
|
+
if (!streams.has(key)) {
|
|
704
|
+
patchConv(key, (conv) => ({ ...conv, stream: conv.stream === "ended" ? "ended" : "idle" }));
|
|
705
|
+
markIdle(key);
|
|
706
|
+
}
|
|
642
707
|
};
|
|
643
708
|
}
|
|
644
709
|
|
|
710
|
+
// 没人看的对话按最近不看的先后排着(最早不看的在前),超过 IDLE_CONV_MAX 个就释放最早的那些的记录。
|
|
711
|
+
const idleConvs: string[] = [];
|
|
712
|
+
function markIdle(key: string) {
|
|
713
|
+
markActive(key);
|
|
714
|
+
idleConvs.push(key);
|
|
715
|
+
// 离开的对话截回快照大小:往上翻过很远的对话在后台不留几千条消息。
|
|
716
|
+
patchConv(key, (conv) => {
|
|
717
|
+
const trimmed = trimToTail(conv.msgs, conv.older);
|
|
718
|
+
return trimmed.msgs === conv.msgs ? conv : { ...conv, msgs: trimmed.msgs, older: trimmed.older };
|
|
719
|
+
});
|
|
720
|
+
if (idleConvs.length <= IDLE_CONV_MAX) return;
|
|
721
|
+
const drop = idleConvs.splice(0, idleConvs.length - IDLE_CONV_MAX).filter((k) => !streams.has(k) && state.convs[k]);
|
|
722
|
+
if (!drop.length) return;
|
|
723
|
+
const convs = { ...state.convs };
|
|
724
|
+
for (const k of drop) delete convs[k];
|
|
725
|
+
set({ convs });
|
|
726
|
+
}
|
|
727
|
+
function markActive(key: string) {
|
|
728
|
+
const at = idleConvs.indexOf(key);
|
|
729
|
+
if (at >= 0) idleConvs.splice(at, 1);
|
|
730
|
+
}
|
|
731
|
+
|
|
645
732
|
// ── 发消息 / 新建 / 撤回 / 结束 ───────────────────────
|
|
646
733
|
|
|
647
734
|
/**
|
|
@@ -830,7 +917,8 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
830
917
|
* 同一个对话同一时刻只取一页。返回取到没有。
|
|
831
918
|
*/
|
|
832
919
|
const loadingOlder = new Set<string>();
|
|
833
|
-
|
|
920
|
+
/** beforeApply:这一页接上去之前调用(界面趁这时量下第一行的位置,接上后视图不跳)。 */
|
|
921
|
+
async function loadOlder(key: string, opts: { beforeApply?: () => void } = {}): Promise<boolean> {
|
|
834
922
|
const conv = state.convs[key];
|
|
835
923
|
// 节点会话:一次性任务是正在看的那个对话,对话任务是流里这个对话的远端任务。
|
|
836
924
|
const remote = conv ? conv.remoteTaskId ?? conv.task?.remote_task_id ?? null : null;
|
|
@@ -840,6 +928,9 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
840
928
|
try {
|
|
841
929
|
const page = await client.history(key, remote, first.id, 200);
|
|
842
930
|
// 取的过程中换了节点会话(重建、续派切过去):这一页作废。
|
|
931
|
+
const current = state.convs[key];
|
|
932
|
+
if (!current || (current.remoteTaskId ?? current.task?.remote_task_id ?? null) !== remote || current.msgs[0]?.id !== first.id) return false;
|
|
933
|
+
notifyCallback(opts.beforeApply, undefined);
|
|
843
934
|
patchConv(key, (c) => ((c.remoteTaskId ?? c.task?.remote_task_id ?? null) === remote && c.msgs[0]?.id === first.id ? applyOlderPage(c, page.messages ?? [], page.older) : c));
|
|
844
935
|
return true;
|
|
845
936
|
} catch (error) {
|
|
@@ -851,7 +942,9 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
851
942
|
}
|
|
852
943
|
|
|
853
944
|
// 同一个仓库里登记的看板(cli-board)。后登记的优先(例如弹出的看板盖在常驻的上面)。
|
|
854
|
-
|
|
945
|
+
// 「在看板里打开」的接收方和各看板报告的样子:规则与节点适配共用(board/linkage)。
|
|
946
|
+
const surfaces = createBoardSurfaces<{ jobId: string; target: OpenTarget }>();
|
|
947
|
+
const boardOwners = createBoardOwners();
|
|
855
948
|
const visibleLists = new Set<string>();
|
|
856
949
|
function reportListPresence(owner: string, visible: boolean) {
|
|
857
950
|
if (visible) visibleLists.add(owner); else visibleLists.delete(owner);
|
|
@@ -916,7 +1009,6 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
916
1009
|
*/
|
|
917
1010
|
// 每个看板各自报告(按 owner 记),生效的是最近报告、还开着的那个;一个看板撤掉只撤它自己的。
|
|
918
1011
|
const viewSources = new Map<string, Map<string, BoardRef>>();
|
|
919
|
-
const boardSources = new Map<string, Map<string, BoardView>>();
|
|
920
1012
|
/** 记下 owner 的报告,返回现在生效的那个(最近报告的)。 */
|
|
921
1013
|
function latestReport<T>(sources: Map<string, Map<string, T>>, key: string, owner: string, value: T | null): T | null {
|
|
922
1014
|
const m = sources.get(key) ?? new Map<string, T>();
|
|
@@ -949,16 +1041,12 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
949
1041
|
if(contextChanged&&state.boardContext[key]===retained)emit({type:"board_context",jobId:key,view:cloneBoardView(retained!)});
|
|
950
1042
|
}
|
|
951
1043
|
function reportBoard(key:string,view:BoardView|null,owner="default") {
|
|
952
|
-
const sources=boardSources.get(key)??new Map<string,BoardView>();
|
|
953
1044
|
let history=state.boardContext[key]?.history??[];
|
|
954
|
-
if(view)
|
|
955
|
-
|
|
956
|
-
if(sources.size)boardSources.set(key,sources);else boardSources.delete(key);
|
|
957
|
-
const top=selectedBoardOwner(sources)?.[1]??null;applyBoardSelection(key,top,history);
|
|
1045
|
+
if(view)history=mergeBoardHistory(history,boardOwners.previous(key,owner),view);
|
|
1046
|
+
const top=boardOwners.report(key,owner,view?cloneBoardView(view):null);applyBoardSelection(key,top,history);
|
|
958
1047
|
}
|
|
959
1048
|
function activateBoard(key:string,owner:string) {
|
|
960
|
-
const
|
|
961
|
-
const top=activateBoardOwner(sources,owner)?.[1];if(!top)return;
|
|
1049
|
+
const top=boardOwners.activate(key,owner);if(!top)return;
|
|
962
1050
|
const file=top.current??(top.file?{path:top.file,sourceId:key}:null);
|
|
963
1051
|
const history=file?rememberBoardFile(state.boardContext[key]?.history??[],file):state.boardContext[key]?.history??[];
|
|
964
1052
|
applyBoardSelection(key,top,history);
|
|
@@ -1037,14 +1125,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
1037
1125
|
}
|
|
1038
1126
|
|
|
1039
1127
|
function openInBoard(jobId: string, target: OpenTarget, preferredInlineOwner?: string): boolean {
|
|
1040
|
-
|
|
1041
|
-
const available = [...surfaces].reverse().filter(item => !preferredInlineOwner || !item.inline || item.owner === preferredInlineOwner)
|
|
1042
|
-
.sort((a, b) => (b.priority?.() ?? 2) - (a.priority?.() ?? 2));
|
|
1043
|
-
for (const { surface } of available) {
|
|
1044
|
-
try { handled = surface(jobId, { ...target }); }
|
|
1045
|
-
catch (error) { console.error("[@xgspz/cli-ui] board surface callback failed", error); }
|
|
1046
|
-
if (handled) break;
|
|
1047
|
-
}
|
|
1128
|
+
const handled = surfaces.open({ jobId, target }, (item) => !preferredInlineOwner || !item.inline || item.owner === preferredInlineOwner);
|
|
1048
1129
|
emit({ type: "open", jobId, target, handled });
|
|
1049
1130
|
return handled;
|
|
1050
1131
|
}
|
|
@@ -1058,9 +1139,8 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
1058
1139
|
openInBoard,
|
|
1059
1140
|
/** 看板登记自己;返回注销函数。 */
|
|
1060
1141
|
registerBoard(surface: BoardSurface, options: { owner?: string; inline?: boolean; priority?: () => number } = {}) {
|
|
1061
|
-
|
|
1062
|
-
surfaces.
|
|
1063
|
-
return () => { const i = surfaces.indexOf(entry); if (i >= 0) surfaces.splice(i, 1); };
|
|
1142
|
+
// 每个接收方拿到自己的一份 target,改了也不影响下一个。
|
|
1143
|
+
return surfaces.register(({ jobId, target }) => surface(jobId, { ...target }), options);
|
|
1064
1144
|
},
|
|
1065
1145
|
subscribe(fn: () => void) { listeners.add(fn); return () => { listeners.delete(fn); }; },
|
|
1066
1146
|
on(fn: (event: CliEvent) => void) { eventListeners.add(fn); return () => { eventListeners.delete(fn); }; },
|
|
@@ -1070,6 +1150,8 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
1070
1150
|
emit({ type: "select", jobId });
|
|
1071
1151
|
},
|
|
1072
1152
|
setJobIds, addJob, removeJob, replaceJob, refreshJob, refreshAll,
|
|
1153
|
+
/** 列表组件用:补拉列表里还没有数据的对话任务。 */
|
|
1154
|
+
loadMissingJobs,
|
|
1073
1155
|
addRef, removeRef, takeRefs, restoreRefs,
|
|
1074
1156
|
setViewing, dismissViewing, takeViewing, restoreViewing,
|
|
1075
1157
|
/** 看板报告自己的样子(看板组件用)。 */
|
|
@@ -1085,14 +1167,6 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
1085
1167
|
export type CliStore = ReturnType<typeof createCliStore>;
|
|
1086
1168
|
|
|
1087
1169
|
|
|
1088
|
-
async function runLimited<T>(items: T[], limit: number, fn: (item: T) => Promise<unknown>) {
|
|
1089
|
-
let index = 0;
|
|
1090
|
-
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
1091
|
-
while (index < items.length) await fn(items[index++]);
|
|
1092
|
-
});
|
|
1093
|
-
await Promise.all(workers);
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
1170
|
const REF_MAX = 20;
|
|
1097
1171
|
const REF_TEXT_MAX = 4_000;
|
|
1098
1172
|
|
|
@@ -343,8 +343,9 @@ function MsgListInner({ jobIds, selectedId, onSelect, onNew, title = "对话", c
|
|
|
343
343
|
};
|
|
344
344
|
}, [store]);
|
|
345
345
|
|
|
346
|
-
//
|
|
347
|
-
|
|
346
|
+
// 新加进来的对话(宿主后给的、别的组件新建的 / 接续出来的)还没数据:补拉一次。
|
|
347
|
+
// 和首次加载、刷新共用列表的并发上限;已经在排、在拉的不重复。
|
|
348
|
+
useEffect(() => { store.loadMissingJobs(); }, [ids, store]);
|
|
348
349
|
|
|
349
350
|
const cards = useMemo(() => ids.map((id) => cardView(id, jobs[id])), [ids, jobs]);
|
|
350
351
|
const q = query.trim().toLowerCase();
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
-
import { Icon } from "
|
|
4
|
+
import { Icon } from "../../ui/icons";
|
|
5
5
|
import { jget, jsend } from "./api";
|
|
6
6
|
import { Modal } from "./Modal";
|
|
7
7
|
import { AUTH_BANNER_HINT } from "./protocol/authFailure";
|
|
8
8
|
import { isOptInAgent } from "./protocol/agents/optIn";
|
|
9
9
|
import { ProviderAuthPanel } from "./ProviderAuth";
|
|
10
10
|
import type { AgentInfo, AgentInfoT, AgentsResponseT, AgentLoginT, ClaudeVerifyT } from "./types";
|
|
11
|
-
import {
|
|
11
|
+
import { useRecheckOnReturn } from "./recheckOnReturn";
|
|
12
12
|
|
|
13
13
|
const NUDGE_DISMISSED = "orch_agent_nudge_dismissed";
|
|
14
14
|
|
|
@@ -247,7 +247,8 @@ function BedrockConnect({ agent, onConnected, compatible = false }: { agent: Age
|
|
|
247
247
|
// setting, or a device-code `aws sso login` for the SSO profile) and surfaces
|
|
248
248
|
// the device URL + one-time code here. Clicking through lands on the IdP
|
|
249
249
|
// (silent if that session is still live) then the AWS approval page; nothing is
|
|
250
|
-
// pasted back — the command exits on approval
|
|
250
|
+
// pasted back — the command exits on approval; coming back to this tab (or 检查状态)
|
|
251
|
+
// re-reads the login and flips it to success.
|
|
251
252
|
function BedrockRefresh({ agent, onConnected }: { agent: AgentInfoT; onConnected?: () => void }) {
|
|
252
253
|
const [login, setLogin] = useState<AgentLoginT | null>(null);
|
|
253
254
|
const [busy, setBusy] = useState(false);
|
|
@@ -283,14 +284,15 @@ function BedrockRefresh({ agent, onConnected }: { agent: AgentInfoT; onConnected
|
|
|
283
284
|
}
|
|
284
285
|
};
|
|
285
286
|
|
|
286
|
-
// Re-check while the CLI waits for the browser-side approval —
|
|
287
|
-
//
|
|
288
|
-
|
|
287
|
+
// Re-check while the CLI waits for the browser-side approval — whenever the user
|
|
288
|
+
// comes back to this tab, or presses 检查状态 (useRecheckOnReturn; no polling).
|
|
289
|
+
const checkLogin = () => {
|
|
289
290
|
jget<AgentLoginT>(base).then((l) => {
|
|
290
291
|
setLogin(l);
|
|
291
292
|
if (l.status === "success") succeed();
|
|
292
293
|
}).catch(() => {});
|
|
293
|
-
}
|
|
294
|
+
};
|
|
295
|
+
useRecheckOnReturn(!!login && (login.status === "starting" || login.status === "awaiting"), checkLogin);
|
|
294
296
|
|
|
295
297
|
if (login?.status === "success") {
|
|
296
298
|
return (
|
|
@@ -323,14 +325,14 @@ function BedrockRefresh({ agent, onConnected }: { agent: AgentInfoT; onConnected
|
|
|
323
325
|
2. 如果页面要求输入验证码,请输入{login.code ? ":" : "页面上显示的验证码。"}
|
|
324
326
|
</div>
|
|
325
327
|
{login.code && <div className="ctx-mono" style={{ fontSize: 20, letterSpacing: 2, margin: "8px 0 2px", fontWeight: 600 }}>{login.code}</div>}
|
|
326
|
-
<div className="wiz-verify" style={{ marginTop: 10 }}><span className="wiz-spin" /> <span
|
|
328
|
+
<div className="wiz-verify" style={{ marginTop: 10 }}><span className="wiz-spin" /> <span>等待你在浏览器中确认…回到这个页面时会自动检查。</span> <button className="linkbtn" onClick={checkLogin}>检查状态</button></div>
|
|
327
329
|
<LogToggle log={login.log} show={showLog} setShow={setShowLog} />
|
|
328
330
|
</div>
|
|
329
331
|
);
|
|
330
332
|
}
|
|
331
333
|
|
|
332
334
|
if (login?.status === "starting") {
|
|
333
|
-
return <div className="wiz-verify" style={{ marginTop: 12 }}><span className="wiz-spin" /> <span>正在启动 AWS 刷新…获取登录链接。</span></div>;
|
|
335
|
+
return <div className="wiz-verify" style={{ marginTop: 12 }}><span className="wiz-spin" /> <span>正在启动 AWS 刷新…获取登录链接。</span> <button className="linkbtn" onClick={checkLogin}>检查状态</button></div>;
|
|
334
336
|
}
|
|
335
337
|
|
|
336
338
|
// idle — the refresh CTA under the verify button
|
|
@@ -426,14 +428,15 @@ function SubscriptionConnect({ agent, onConnected, compact }: { agent: AgentInfo
|
|
|
426
428
|
};
|
|
427
429
|
|
|
428
430
|
// Re-check while the CLI waits for the user to authorize / exchange the code —
|
|
429
|
-
//
|
|
430
|
-
|
|
431
|
-
&& (login.status === "starting" || login.status === "awaiting" || login.status === "submitting"), () => {
|
|
431
|
+
// whenever the user comes back to this tab, or presses 检查状态 (no polling).
|
|
432
|
+
const checkLogin = () => {
|
|
432
433
|
jget<AgentLoginT>(base).then((l) => {
|
|
433
434
|
setLogin(l);
|
|
434
435
|
if (l.status === "success") succeed();
|
|
435
436
|
}).catch(() => {});
|
|
436
|
-
}
|
|
437
|
+
};
|
|
438
|
+
useRecheckOnReturn(!checkingAuth && !(existingAuth?.authenticated && !chooseLogin) && !!login
|
|
439
|
+
&& (login.status === "starting" || login.status === "awaiting" || login.status === "submitting"), checkLogin);
|
|
437
440
|
|
|
438
441
|
const submitCode = async () => {
|
|
439
442
|
if (!code.trim()) return;
|
|
@@ -511,6 +514,7 @@ function SubscriptionConnect({ agent, onConnected, compact }: { agent: AgentInfo
|
|
|
511
514
|
{login.status === "submitting" || busy ? "验证中…" : "提交"}
|
|
512
515
|
</button>
|
|
513
516
|
</div>
|
|
517
|
+
{login.status === "submitting" && <div className="hlp" style={{ marginTop: 6 }}>正在验证授权码… <button className="linkbtn" onClick={checkLogin}>检查状态</button></div>}
|
|
514
518
|
</>
|
|
515
519
|
) : (
|
|
516
520
|
<>
|
|
@@ -518,7 +522,7 @@ function SubscriptionConnect({ agent, onConnected, compact }: { agent: AgentInfo
|
|
|
518
522
|
2. 出现提示时,输入这个一次性验证码{login.code ? "" : "(显示在登录页上)"}:
|
|
519
523
|
</div>
|
|
520
524
|
{login.code && <div className="ctx-mono" style={{ fontSize: 20, letterSpacing: 2, margin: "8px 0 2px", fontWeight: 600 }}>{login.code}</div>}
|
|
521
|
-
<div className="wiz-verify" style={{ marginTop: 10 }}><span className="wiz-spin" /> <span
|
|
525
|
+
<div className="wiz-verify" style={{ marginTop: 10 }}><span className="wiz-spin" /> <span>等待你在浏览器中完成授权…回到这个页面时会自动检查。</span> <button className="linkbtn" onClick={checkLogin}>检查状态</button></div>
|
|
522
526
|
</>
|
|
523
527
|
)}
|
|
524
528
|
<LogToggle log={login.log} show={showLog} setShow={setShowLog} />
|
|
@@ -527,7 +531,7 @@ function SubscriptionConnect({ agent, onConnected, compact }: { agent: AgentInfo
|
|
|
527
531
|
}
|
|
528
532
|
|
|
529
533
|
if (login?.status === "starting") {
|
|
530
|
-
return <div className="wiz-verify"><span className="wiz-spin" /> <span>正在启动登录…准备授权链接。</span></div>;
|
|
534
|
+
return <div className="wiz-verify"><span className="wiz-spin" /> <span>正在启动登录…准备授权链接。</span> <button className="linkbtn" onClick={checkLogin}>检查状态</button></div>;
|
|
531
535
|
}
|
|
532
536
|
|
|
533
537
|
// idle — initial CTA
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { Icon } from "
|
|
3
|
+
import { Icon } from "../../ui/icons";
|
|
4
4
|
import type { Appearance } from "./types";
|
|
5
5
|
|
|
6
6
|
export function AppearancePanel({ appearance, setAppearance, onClose }: { appearance: Appearance; setAppearance: (k: keyof Appearance, v: string) => void; onClose: () => void }) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useMemo, useState } from "react";
|
|
3
|
-
import { Icon } from "
|
|
3
|
+
import { Icon } from "../../ui/icons";
|
|
4
4
|
import { BrowserPane as BrowserSurface } from "../../board/BrowserPane";
|
|
5
5
|
import { createOperatorBoardBackend } from "./boardBackend";
|
|
6
6
|
import type { BrowserStatus } from "../../core/types";
|