@xgspz/cli-ui 0.6.14 → 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 +37 -4
- package/README.md +29 -4
- package/dist/{cli-ui-embed-D-3DvyTN.js → cli-ui-embed-D9xgur2y.js} +18376 -18648
- package/dist/{cli-ui-index-C34qFb4u.js → cli-ui-index-WBtxmw23.js} +2 -2
- package/dist/{cli-ui-jszip.min-DojbZPiE.js → cli-ui-jszip.min-CU3ZzoXb.js} +1 -1
- package/dist/{cli-ui-jszip.min-BObcE2nn.js → cli-ui-jszip.min-DdU0X3VJ.js} +2 -2
- package/dist/{cli-ui-xlsx-JVLtx0qR.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 +105 -182
- package/src/chat/Composer.tsx +4 -3
- package/src/core/conversation.ts +16 -4
- package/src/core/store.ts +120 -40
- package/src/core/types.ts +2 -0
- 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/operator.base.css +3 -0
- package/src/operator.css +3 -0
- 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
|
});
|
|
@@ -573,12 +633,18 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
573
633
|
}
|
|
574
634
|
}
|
|
575
635
|
/** 手动重连:给了对话键只连这一个,否则所有停下的 / 正在退避等待的都立刻再连(重试次数重新计)。 */
|
|
576
|
-
|
|
636
|
+
/**
|
|
637
|
+
* 立刻重连。缺省只接上停下的(offline、页面停放)或正在等重连的;force:连着的也断开重连一次,
|
|
638
|
+
* 让中枢重新补发快照(例如会话记录一直没读到时的「重新读取」)。
|
|
639
|
+
*/
|
|
640
|
+
function reconnect(key?: string, options: { force?: boolean } = {}) {
|
|
577
641
|
if (!key) { reconnectNow(); return; }
|
|
578
642
|
const entry = streams.get(key);
|
|
579
|
-
if (!entry || entry.refs <= 0 || (!entry.parked && !entry.timer)) return;
|
|
643
|
+
if (!entry || entry.refs <= 0 || (!options.force && !entry.parked && !entry.timer)) return;
|
|
580
644
|
if (entry.timer) { clearTimeout(entry.timer); entry.timer = null; }
|
|
581
645
|
entry.parked = false; entry.attempt = 0;
|
|
646
|
+
if (options.force) { settleDrop(entry); patchConv(key, (conv) => ({ ...conv, transcriptError: null })); }
|
|
647
|
+
// connect 会先关掉还连着的那条(包括已经收到 end 的)。
|
|
582
648
|
connect(key);
|
|
583
649
|
}
|
|
584
650
|
// 页面在后台太久:停掉实时流(主动停止不算断线,不会触发重连),切回来由 reconnectNow 接上。
|
|
@@ -602,7 +668,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
602
668
|
function open(key: string): () => void {
|
|
603
669
|
const { jobId, taskId } = parseConvKey(key);
|
|
604
670
|
if (taskId === NEW_TASK) {
|
|
605
|
-
void
|
|
671
|
+
void loadJob(jobId);
|
|
606
672
|
return () => undefined;
|
|
607
673
|
}
|
|
608
674
|
let entry = streams.get(key);
|
|
@@ -613,7 +679,9 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
613
679
|
}
|
|
614
680
|
entry.refs += 1;
|
|
615
681
|
if (entry.refs === 1) {
|
|
616
|
-
|
|
682
|
+
markActive(key);
|
|
683
|
+
// 列表正在拉这个任务(页面刚打开时常见):用它的结果,不再排一次「拉完再补」。
|
|
684
|
+
void loadJob(jobId);
|
|
617
685
|
connect(key);
|
|
618
686
|
}
|
|
619
687
|
const owned = entry;
|
|
@@ -632,10 +700,35 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
632
700
|
settleDrop(current);
|
|
633
701
|
syncLive();
|
|
634
702
|
current.stop?.();
|
|
635
|
-
if (!streams.has(key))
|
|
703
|
+
if (!streams.has(key)) {
|
|
704
|
+
patchConv(key, (conv) => ({ ...conv, stream: conv.stream === "ended" ? "ended" : "idle" }));
|
|
705
|
+
markIdle(key);
|
|
706
|
+
}
|
|
636
707
|
};
|
|
637
708
|
}
|
|
638
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
|
+
|
|
639
732
|
// ── 发消息 / 新建 / 撤回 / 结束 ───────────────────────
|
|
640
733
|
|
|
641
734
|
/**
|
|
@@ -824,7 +917,8 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
824
917
|
* 同一个对话同一时刻只取一页。返回取到没有。
|
|
825
918
|
*/
|
|
826
919
|
const loadingOlder = new Set<string>();
|
|
827
|
-
|
|
920
|
+
/** beforeApply:这一页接上去之前调用(界面趁这时量下第一行的位置,接上后视图不跳)。 */
|
|
921
|
+
async function loadOlder(key: string, opts: { beforeApply?: () => void } = {}): Promise<boolean> {
|
|
828
922
|
const conv = state.convs[key];
|
|
829
923
|
// 节点会话:一次性任务是正在看的那个对话,对话任务是流里这个对话的远端任务。
|
|
830
924
|
const remote = conv ? conv.remoteTaskId ?? conv.task?.remote_task_id ?? null : null;
|
|
@@ -834,6 +928,9 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
834
928
|
try {
|
|
835
929
|
const page = await client.history(key, remote, first.id, 200);
|
|
836
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);
|
|
837
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));
|
|
838
935
|
return true;
|
|
839
936
|
} catch (error) {
|
|
@@ -845,7 +942,9 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
845
942
|
}
|
|
846
943
|
|
|
847
944
|
// 同一个仓库里登记的看板(cli-board)。后登记的优先(例如弹出的看板盖在常驻的上面)。
|
|
848
|
-
|
|
945
|
+
// 「在看板里打开」的接收方和各看板报告的样子:规则与节点适配共用(board/linkage)。
|
|
946
|
+
const surfaces = createBoardSurfaces<{ jobId: string; target: OpenTarget }>();
|
|
947
|
+
const boardOwners = createBoardOwners();
|
|
849
948
|
const visibleLists = new Set<string>();
|
|
850
949
|
function reportListPresence(owner: string, visible: boolean) {
|
|
851
950
|
if (visible) visibleLists.add(owner); else visibleLists.delete(owner);
|
|
@@ -910,7 +1009,6 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
910
1009
|
*/
|
|
911
1010
|
// 每个看板各自报告(按 owner 记),生效的是最近报告、还开着的那个;一个看板撤掉只撤它自己的。
|
|
912
1011
|
const viewSources = new Map<string, Map<string, BoardRef>>();
|
|
913
|
-
const boardSources = new Map<string, Map<string, BoardView>>();
|
|
914
1012
|
/** 记下 owner 的报告,返回现在生效的那个(最近报告的)。 */
|
|
915
1013
|
function latestReport<T>(sources: Map<string, Map<string, T>>, key: string, owner: string, value: T | null): T | null {
|
|
916
1014
|
const m = sources.get(key) ?? new Map<string, T>();
|
|
@@ -943,16 +1041,12 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
943
1041
|
if(contextChanged&&state.boardContext[key]===retained)emit({type:"board_context",jobId:key,view:cloneBoardView(retained!)});
|
|
944
1042
|
}
|
|
945
1043
|
function reportBoard(key:string,view:BoardView|null,owner="default") {
|
|
946
|
-
const sources=boardSources.get(key)??new Map<string,BoardView>();
|
|
947
1044
|
let history=state.boardContext[key]?.history??[];
|
|
948
|
-
if(view)
|
|
949
|
-
|
|
950
|
-
if(sources.size)boardSources.set(key,sources);else boardSources.delete(key);
|
|
951
|
-
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);
|
|
952
1047
|
}
|
|
953
1048
|
function activateBoard(key:string,owner:string) {
|
|
954
|
-
const
|
|
955
|
-
const top=activateBoardOwner(sources,owner)?.[1];if(!top)return;
|
|
1049
|
+
const top=boardOwners.activate(key,owner);if(!top)return;
|
|
956
1050
|
const file=top.current??(top.file?{path:top.file,sourceId:key}:null);
|
|
957
1051
|
const history=file?rememberBoardFile(state.boardContext[key]?.history??[],file):state.boardContext[key]?.history??[];
|
|
958
1052
|
applyBoardSelection(key,top,history);
|
|
@@ -1031,14 +1125,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
1031
1125
|
}
|
|
1032
1126
|
|
|
1033
1127
|
function openInBoard(jobId: string, target: OpenTarget, preferredInlineOwner?: string): boolean {
|
|
1034
|
-
|
|
1035
|
-
const available = [...surfaces].reverse().filter(item => !preferredInlineOwner || !item.inline || item.owner === preferredInlineOwner)
|
|
1036
|
-
.sort((a, b) => (b.priority?.() ?? 2) - (a.priority?.() ?? 2));
|
|
1037
|
-
for (const { surface } of available) {
|
|
1038
|
-
try { handled = surface(jobId, { ...target }); }
|
|
1039
|
-
catch (error) { console.error("[@xgspz/cli-ui] board surface callback failed", error); }
|
|
1040
|
-
if (handled) break;
|
|
1041
|
-
}
|
|
1128
|
+
const handled = surfaces.open({ jobId, target }, (item) => !preferredInlineOwner || !item.inline || item.owner === preferredInlineOwner);
|
|
1042
1129
|
emit({ type: "open", jobId, target, handled });
|
|
1043
1130
|
return handled;
|
|
1044
1131
|
}
|
|
@@ -1052,9 +1139,8 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
1052
1139
|
openInBoard,
|
|
1053
1140
|
/** 看板登记自己;返回注销函数。 */
|
|
1054
1141
|
registerBoard(surface: BoardSurface, options: { owner?: string; inline?: boolean; priority?: () => number } = {}) {
|
|
1055
|
-
|
|
1056
|
-
surfaces.
|
|
1057
|
-
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);
|
|
1058
1144
|
},
|
|
1059
1145
|
subscribe(fn: () => void) { listeners.add(fn); return () => { listeners.delete(fn); }; },
|
|
1060
1146
|
on(fn: (event: CliEvent) => void) { eventListeners.add(fn); return () => { eventListeners.delete(fn); }; },
|
|
@@ -1064,6 +1150,8 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
1064
1150
|
emit({ type: "select", jobId });
|
|
1065
1151
|
},
|
|
1066
1152
|
setJobIds, addJob, removeJob, replaceJob, refreshJob, refreshAll,
|
|
1153
|
+
/** 列表组件用:补拉列表里还没有数据的对话任务。 */
|
|
1154
|
+
loadMissingJobs,
|
|
1067
1155
|
addRef, removeRef, takeRefs, restoreRefs,
|
|
1068
1156
|
setViewing, dismissViewing, takeViewing, restoreViewing,
|
|
1069
1157
|
/** 看板报告自己的样子(看板组件用)。 */
|
|
@@ -1079,14 +1167,6 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
1079
1167
|
export type CliStore = ReturnType<typeof createCliStore>;
|
|
1080
1168
|
|
|
1081
1169
|
|
|
1082
|
-
async function runLimited<T>(items: T[], limit: number, fn: (item: T) => Promise<unknown>) {
|
|
1083
|
-
let index = 0;
|
|
1084
|
-
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
1085
|
-
while (index < items.length) await fn(items[index++]);
|
|
1086
|
-
});
|
|
1087
|
-
await Promise.all(workers);
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
1170
|
const REF_MAX = 20;
|
|
1091
1171
|
const REF_TEXT_MAX = 4_000;
|
|
1092
1172
|
|
package/src/core/types.ts
CHANGED
|
@@ -251,6 +251,8 @@ export type StreamItem =
|
|
|
251
251
|
/** remote_task_id:一次性任务的流里才有(大任务里可能同时有几个节点对话)。 */
|
|
252
252
|
/** older:快照只有最新一段时,更早还有几条(往上翻时经 history 分页取)。旧版中枢没有 = 全量。 */
|
|
253
253
|
| { type: "transcript"; remote_task_id?: string; messages: Message[] | null; pending: PendingMessage[] | null; running: boolean | null; older?: number }
|
|
254
|
+
/** 中枢读不到这个对话的记录(执行节点暂时连不上、读超时),会继续重试;旧版中枢没有。 */
|
|
255
|
+
| { type: "transcript_unavailable"; remote_task_id?: string; error: string }
|
|
254
256
|
| { type: "node"; remote_task_id?: string; event: NodeEvent }
|
|
255
257
|
| { type: "turn"; turn: CenterTurn }
|
|
256
258
|
| { type: "job"; job: { id: string; status: JobStatus; closed_reason?: string | null; node_id: string | null } }
|
|
@@ -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";
|