@spzxg/cli-ui 0.4.0 → 0.6.1

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.
Files changed (44) hide show
  1. package/README.md +84 -10
  2. package/dist/cli-ui-embed-Dk2QGHxn.js +41146 -0
  3. package/dist/cli-ui-index-BFpRZXn4.js +11209 -0
  4. package/dist/cli-ui-jszip.min-B9tqMA-e.js +2324 -0
  5. package/dist/cli-ui-jszip.min-DIavJzUG.js +26 -0
  6. package/dist/cli-ui-xlsx-BKHhqcoa.js +93 -0
  7. package/dist/cli-ui.css +1 -1
  8. package/dist/cli-ui.js +1 -1
  9. package/package.json +3 -1
  10. package/src/board/BrowserPane.tsx +396 -96
  11. package/src/board/ChangesPane.tsx +13 -4
  12. package/src/board/CliBoard.tsx +219 -37
  13. package/src/board/FileViewer.tsx +387 -73
  14. package/src/board/FilesPane.tsx +51 -4
  15. package/src/board/blocks.tsx +3 -2
  16. package/src/board/fileKind.ts +31 -0
  17. package/src/board/fileOps.ts +55 -0
  18. package/src/board/folderDownload.ts +161 -0
  19. package/src/board/frameQuote.ts +50 -0
  20. package/src/board/textLoad.ts +99 -0
  21. package/src/board/workspace.ts +129 -0
  22. package/src/board/zip.ts +85 -0
  23. package/src/chat/CliChat.tsx +122 -23
  24. package/src/chat/Composer.tsx +4 -4
  25. package/src/core/blocks.ts +69 -11
  26. package/src/core/client.ts +285 -41
  27. package/src/core/config.ts +18 -5
  28. package/src/core/conversation.ts +17 -7
  29. package/src/core/store.ts +340 -71
  30. package/src/core/textDecode.ts +24 -0
  31. package/src/core/types.ts +21 -6
  32. package/src/core/xlsx.ts +142 -0
  33. package/src/elements.tsx +106 -11
  34. package/src/index.ts +7 -2
  35. package/src/layout/CliSplit.tsx +53 -0
  36. package/src/layout/split.ts +214 -0
  37. package/src/msglist/CliMsgList.tsx +50 -22
  38. package/src/styles.css +101 -0
  39. package/src/ui/ErrorBoundary.tsx +38 -0
  40. package/src/ui/TextBlock.tsx +1 -1
  41. package/src/ui/Transcript.tsx +10 -5
  42. package/src/ui/transcriptModel.ts +66 -9
  43. package/dist/cli-ui-embed-D7xtcvGG.js +0 -37967
  44. package/dist/cli-ui-index-O0krpZBg.js +0 -13516
@@ -58,37 +58,94 @@ export function hasOpenAsk(messages: Msg[], scan = 400): boolean {
58
58
  }
59
59
 
60
60
  /**
61
- * 把(重连时的)全量转录合进已有的:没变的消息保持对象引用,memo 的行就不会重渲染(不会重新解析 markdown)。
61
+ * 把(重连时的)快照合进已有的:没变的消息保持对象引用,memo 的行就不会重渲染(不会重新解析 markdown)。
62
+ * 快照只是最新一段(older > 0)时,保留这个页面已经往上翻出来的更早的消息(与 Operator 的 mergeSnapshot 一致)。
62
63
  */
63
- export function mergeSnapshot(prev: Msg[] | undefined, committed: Msg[]): Msg[] {
64
+ export function mergeSnapshot(prev: Msg[] | undefined, committed: Msg[], older = 0): { msgs: Msg[]; older: number } {
64
65
  const byId = new Map<string, Msg>();
65
66
  for (const m of prev ?? []) byId.set(m.id, m);
66
- return committed.map((m) => {
67
+ const fresh = committed.map((m) => {
67
68
  const p = byId.get(m.id);
68
69
  if (!p || p.role !== m.role || p.content !== m.content || p.generation !== m.generation) return m;
69
70
  return p.ts === m.ts ? p : { ...p, ts: m.ts };
70
71
  });
72
+ let kept: Msg[] = [];
73
+ if (prev && fresh.length && older > 0) {
74
+ const at = prev.findIndex((m) => m.id === fresh[0].id);
75
+ // 只留落过库的(带时间的):本地补的说明行随旧视图一起作废。
76
+ if (at > 0) kept = prev.slice(0, at).filter((m) => m.role !== "queued" && m.ts != null);
77
+ }
78
+ return { msgs: [...kept, ...fresh], older: Math.max(0, older - kept.length) };
79
+ }
80
+
81
+ /** 往上翻取到的一页更早的消息接在最前面(已经有的跳过)。 */
82
+ export function prependOlder(prev: Msg[], page: Msg[]): Msg[] {
83
+ const have = new Set(prev.map((m) => m.id));
84
+ const add = page.filter((m) => !have.has(m.id));
85
+ return add.length ? [...add, ...prev] : prev;
71
86
  }
72
87
 
73
88
  export type TranscriptItem =
74
89
  | { kind: "label"; key: string; n: number; current: boolean }
75
90
  | { kind: "break"; key: string; summary: string }
76
- | { kind: "msg"; key: string; m: Msg; initial: boolean; hideWho: boolean };
91
+ | { kind: "msg"; key: string; m: Msg; initial: boolean; hideWho: boolean }
92
+ /** 收起的「等你在浏览器里操作」:AI 每 ~50 秒续等一次,每次都是「思考 + 工具 + 一句话」,几分钟就刷满一屏。 */
93
+ | { kind: "fold"; key: string; msgs: Msg[]; waits: number };
94
+
95
+ const WAIT_TOOL = /browser_handoff|browser_ask_user/;
96
+ /** 续等的结果:用户还没做完 / 还没填。 */
97
+ const STILL_WAITING = /hasn'?t (finished|answered|responded)|not (finished|answered|done) yet|still waiting|keep waiting|wait_only|还没(有)?(完成|回答|填)|继续等/i;
77
98
 
78
- /** 转录展开成扁平的行(会话标签、/clear 分隔、消息),界面可以只渲染其中一个窗口。 */
79
- export function buildTranscriptItems(messages: Msg[]): TranscriptItem[] {
99
+ /** 续等浏览器那边的人(browser_handoff / browser_ask_user 带 wait_only,或结果是「还没好」)。第一次请求不算。 */
100
+ export function isWaitCall(m: Msg): boolean {
101
+ if (m.role !== "tool") return false;
102
+ const d = toolDataOf(m);
103
+ if (!WAIT_TOOL.test(d.title)) return false;
104
+ if (d.result === undefined) return /wait_only/.test(d.detail ?? "");
105
+ return STILL_WAITING.test(d.result);
106
+ }
107
+ const isThinking = (m: Msg) => m.role === "tool" && /^\W*thinking\b/i.test(toolDataOf(m).title);
108
+ /** 两次续等之间的消息:智能体的一句话、思考。 */
109
+ const between = (m: Msg) => m.role === "assistant" || isThinking(m);
110
+
111
+ /**
112
+ * 一段连续的续等(中间只夹着思考和一句话):最后一次照常显示(它的状态是最新的),前面的收成一行。
113
+ * 返回收起的那段的结束位置(不含)和收起了几次续等;不到两次续等不收,返回 null。
114
+ */
115
+ function waitRun(msgs: Msg[], start: number): { end: number; waits: number } | null {
116
+ let last = start;
117
+ let waits = 1;
118
+ for (let i = start + 1; i < msgs.length; i++) {
119
+ if (isWaitCall(msgs[i])) { last = i; waits++; continue; }
120
+ if (!between(msgs[i])) break;
121
+ }
122
+ return waits >= 2 ? { end: last, waits: waits - 1 } : null;
123
+ }
124
+
125
+ /**
126
+ * 转录展开成扁平的行(会话标签、/clear 分隔、消息),界面可以只渲染其中一个窗口。
127
+ * hasOlder:更早的消息还没取,第一条就不一定是它那个会话的开场。
128
+ */
129
+ export function buildTranscriptItems(messages: Msg[], hasOlder = false): TranscriptItem[] {
80
130
  const sessions = buildSessions(messages);
81
131
  const items: TranscriptItem[] = [];
82
132
  sessions.forEach((s, si) => {
83
133
  if (si > 0 && s.summaryBefore) items.push({ kind: "break", key: `b-${s.n}`, summary: s.summaryBefore });
84
134
  items.push({ kind: "label", key: `s-${s.n}`, n: s.n, current: si === sessions.length - 1 });
85
- s.messages.forEach((m, mi) => {
135
+ for (let mi = 0; mi < s.messages.length; mi++) {
136
+ const m = s.messages[mi];
137
+ const run = isWaitCall(m) ? waitRun(s.messages, mi) : null;
138
+ if (run) {
139
+ items.push({ kind: "fold", key: `f-${m.id}`, msgs: s.messages.slice(mi, run.end), waits: run.waits });
140
+ mi = run.end - 1;
141
+ continue;
142
+ }
86
143
  const prev = s.messages[mi - 1];
87
144
  // 一段连续的智能体输出(文字 → 工具 → 文字)只显示一次头像行
88
145
  const hideWho = m.role === "assistant" && !!prev && (prev.role === "assistant" || prev.role === "tool");
89
- const initial = mi === 0 && m.role === "user";
146
+ const initial = mi === 0 && m.role === "user" && !(si === 0 && hasOlder);
90
147
  items.push({ kind: "msg", key: m.id, m, initial, hideWho });
91
- });
148
+ }
92
149
  });
93
150
  return items;
94
151
  }