pi-one-ui 0.2.1 → 0.3.0

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.
@@ -19,20 +19,12 @@ export type ConfigRecord = Record<string, unknown>;
19
19
 
20
20
  export type ConfigStorePaths = {
21
21
  canonical: string;
22
- legacyUnified: string;
23
- legacyShell: string;
24
- legacyRenderer: string;
25
22
  };
26
23
 
27
- /**
28
- * Returns canonical and legacy configuration locations for an agent directory.
29
- */
24
+ /** Returns the canonical configuration location for an agent directory. */
30
25
  export function configPaths(agentDir = getAgentDir()): ConfigStorePaths {
31
26
  return {
32
27
  canonical: join(agentDir, "pi-one-ui.json"),
33
- legacyUnified: join(agentDir, "pi-mine-ui.json"),
34
- legacyShell: join(agentDir, "zentui.json"),
35
- legacyRenderer: join(agentDir, "claude-code-style.json"),
36
28
  };
37
29
  }
38
30
 
@@ -138,7 +130,7 @@ function configReadError(path: string, error: unknown): Error {
138
130
  }
139
131
 
140
132
  /**
141
- * Builds the compatibility error used when a config write is unsafe.
133
+ * Builds the safety error used when a config write is unsafe.
142
134
  */
143
135
  function configWriteError(path: string, label: string, error: unknown): Error {
144
136
  const detail = error instanceof Error ? ` (${error.message})` : "";
@@ -147,79 +139,15 @@ function configWriteError(path: string, label: string, error: unknown): Error {
147
139
  );
148
140
  }
149
141
 
150
- type SelectedConfig = {
151
- record: ConfigRecord;
152
- writePath: string;
153
- mode?: number;
154
- materialize: boolean;
155
- };
156
-
157
- /**
158
- * Reads an optional config source and turns corruption into an explicit error.
159
- */
160
- function optionalConfig(
161
- path: string,
162
- ): { record: ConfigRecord; writePath: string; mode: number } | undefined {
163
- const state = readConfigFileState(path);
164
- if (state.kind === "missing") return undefined;
165
- if (state.kind === "corrupt") throw configReadError(path, state.error);
166
- return state;
167
- }
168
-
169
- /**
170
- * Selects the active source and the destination used by a later update.
171
- */
172
- function selectUnifiedConfig(paths: ConfigStorePaths): SelectedConfig {
173
- const canonical = optionalConfig(paths.canonical);
174
- if (canonical) return { ...canonical, materialize: false };
175
-
176
- const legacyUnified = optionalConfig(paths.legacyUnified);
177
- if (legacyUnified)
178
- return {
179
- ...legacyUnified,
180
- writePath: paths.canonical,
181
- materialize: true,
182
- };
183
-
184
- const shell = optionalConfig(paths.legacyShell);
185
- const renderer = optionalConfig(paths.legacyRenderer);
186
- if (!shell && !renderer) {
187
- return {
188
- record: {},
189
- writePath: paths.canonical,
190
- materialize: false,
191
- };
192
- }
193
-
194
- return {
195
- record: {
196
- ...(shell?.record ?? {}),
197
- version: 1,
198
- renderer: renderer?.record ?? {},
199
- },
200
- writePath: paths.canonical,
201
- mode: shell?.mode ?? renderer?.mode,
202
- materialize: true,
203
- };
204
- }
205
-
206
- /**
207
- * Read the one raw unified record used by all configuration domains.
208
- * Legacy sources are best-effort materialized through the same atomic writer;
209
- * the in-memory record remains usable if the agent directory is read-only.
210
- */
142
+ /** Reads the raw canonical record used by every configuration domain. */
211
143
  export function readUnifiedConfigRecord(
212
144
  paths: ConfigStorePaths = defaultConfigPaths,
213
145
  ): ConfigRecord {
214
- const selected = selectUnifiedConfig(paths);
215
- if (selected.materialize) {
216
- try {
217
- writeConfigAtomically(selected.writePath, selected.record, selected.mode);
218
- } catch {
219
- // Keep using the selected legacy record in memory when migration cannot be written.
220
- }
221
- }
222
- return selected.record;
146
+ const state = readConfigFileState(paths.canonical);
147
+ if (state.kind === "missing") return {};
148
+ if (state.kind === "corrupt")
149
+ throw configReadError(paths.canonical, state.error);
150
+ return state.record;
223
151
  }
224
152
 
225
153
  /**
@@ -247,22 +175,18 @@ export type ConfigStoreListener = (record: ConfigRecord) => void;
247
175
 
248
176
  /**
249
177
  * Process-wide raw configuration store. Domain config modules own parsing and
250
- * selectors; this module owns source selection, migration and persistence.
178
+ * selectors; this module owns canonical persistence.
251
179
  */
252
180
  export class ConfigStore {
253
181
  readonly paths: ConfigStorePaths;
254
182
  private readonly listeners = new Set<ConfigStoreListener>();
255
183
 
256
- /**
257
- * Creates a store for canonical and legacy paths.
258
- */
184
+ /** Creates a store for the canonical path. */
259
185
  constructor(paths: ConfigStorePaths = defaultConfigPaths) {
260
186
  this.paths = paths;
261
187
  }
262
188
 
263
- /**
264
- * Reads the current canonical or legacy-backed raw configuration record.
265
- */
189
+ /** Reads the current canonical raw configuration record. */
266
190
  read(): ConfigRecord {
267
191
  return readUnifiedConfigRecord(this.paths);
268
192
  }
@@ -271,11 +195,18 @@ export class ConfigStore {
271
195
  * Mutates and atomically persists the active raw configuration record.
272
196
  */
273
197
  update(mutate: (record: ConfigRecord) => void): ConfigRecord {
274
- const selected = selectUnifiedConfig(this.paths);
275
- mutate(selected.record);
276
- writeConfigAtomically(selected.writePath, selected.record, selected.mode);
277
- for (const listener of this.listeners) listener(selected.record);
278
- return selected.record;
198
+ const state = readConfigFileState(this.paths.canonical);
199
+ if (state.kind === "corrupt")
200
+ throw configReadError(this.paths.canonical, state.error);
201
+
202
+ mutate(state.record);
203
+ writeConfigAtomically(
204
+ state.writePath,
205
+ state.record,
206
+ state.kind === "valid" ? state.mode : undefined,
207
+ );
208
+ for (const listener of this.listeners) listener(state.record);
209
+ return state.record;
279
210
  }
280
211
 
281
212
  /**
@@ -21,7 +21,7 @@ export function applyPreset(preset: Preset): void {
21
21
  saveUserMessagesComponentPatch({ enabled: false });
22
22
  saveWorkingLineComponentPatch({ enabled: false });
23
23
  saveFooterComponentPatch({ style: "native" });
24
- updateRendererConfig({ mode: "off", enableWorkingMessage: false });
24
+ updateRendererConfig({ mode: "off" });
25
25
  return;
26
26
  }
27
27
 
@@ -31,6 +31,5 @@ export function applyPreset(preset: Preset): void {
31
31
  saveFooterComponentPatch({ style: "starship" });
32
32
  updateRendererConfig({
33
33
  mode: preset === "compact" ? "compact" : "on",
34
- enableWorkingMessage: false,
35
34
  });
36
35
  }
@@ -9,10 +9,7 @@ import {
9
9
  visibleWidth,
10
10
  type Component,
11
11
  } from "@earendil-works/pi-tui";
12
- import {
13
- TOOL_LOADING_INTERVAL_MS,
14
- toolLoadingIcon,
15
- } from "../../../../tools/tool-loading-icon.ts";
12
+ import { toolLoadingIcon } from "../../../../tools/tool-loading-icon.ts";
16
13
  import { isToolTuiFullscreen, showMoreHintText } from "./show-more-hint.ts";
17
14
  import {
18
15
  stripAnsi,
@@ -21,6 +18,15 @@ import {
21
18
  } from "../../../../tools/ansi-text.ts";
22
19
  import { walkComponentTree } from "../../../../tools/component-tree.ts";
23
20
  import { humanizeToolLabel, toolCallSummary } from "./names.ts";
21
+ import {
22
+ captureIoViewMarkers,
23
+ getActiveIoViewFrame,
24
+ isExpandedToolIoView,
25
+ replayIoViewMarkers,
26
+ type CapturedIoViewMarker,
27
+ type ExpandedToolIoView,
28
+ type ToolIoSection,
29
+ } from "./result.ts";
24
30
  import {
25
31
  patchRegistry,
26
32
  TOOL_GROUPING_GENERATION_KEY as GENERATION_KEY,
@@ -41,7 +47,6 @@ type Patch = {
41
47
  generation: number;
42
48
  lastEnabled: boolean;
43
49
  theme?: any;
44
- animationTimer: ReturnType<typeof setTimeout> | null;
45
50
  };
46
51
 
47
52
  function toolName(tool: any): string {
@@ -93,23 +98,6 @@ function statusIcon(value: ToolStatus): string {
93
98
  return toolLoadingIcon();
94
99
  }
95
100
 
96
- function scheduleGroupAnimation(patch: Patch): void {
97
- if (patch.animationTimer || !patch.active) return;
98
- patch.animationTimer = setTimeout(() => {
99
- patch.animationTimer = null;
100
- if (!patch.active) return;
101
- for (const group of patch.groups) {
102
- if (
103
- (group.children as any[]).some(
104
- (tool) => tool?.executionStarted && status(tool) === "pending",
105
- )
106
- )
107
- group.invalidate();
108
- }
109
- }, TOOL_LOADING_INTERVAL_MS);
110
- patch.animationTimer.unref?.();
111
- }
112
-
113
101
  function visibleLines(lines: string[]): string[] {
114
102
  return lines.filter((line) => stripAnsi(line).trim());
115
103
  }
@@ -180,15 +168,74 @@ let nextGroupId = 1;
180
168
 
181
169
  type SettledGroupCache = {
182
170
  width: number;
171
+ expanded: boolean;
183
172
  hover: boolean;
184
173
  theme: unknown;
185
174
  fullscreen: boolean;
186
175
  children: readonly unknown[];
187
176
  args: unknown[];
188
177
  results: unknown[];
178
+ statuses: ToolStatus[];
179
+ callComponents: unknown[];
180
+ resultComponents: unknown[];
181
+ ioViews: Array<ExpandedToolIoView | undefined>;
182
+ ioHoveredSections: Array<ToolIoSection | null | undefined>;
183
+ ioRevisions: Array<number | undefined>;
184
+ capturedIoFrame: boolean;
185
+ lines: string[];
186
+ ioMarkers: CapturedIoViewMarker[];
187
+ };
188
+
189
+ type SettledGroupCacheSlots = {
190
+ collapsed?: SettledGroupCache;
191
+ expanded?: SettledGroupCache;
192
+ };
193
+
194
+ type SettledExpandedChildCache = {
195
+ width: number;
196
+ theme: unknown;
197
+ fullscreen: boolean;
198
+ index: number;
199
+ total: number;
200
+ args: unknown;
201
+ result: unknown;
202
+ status: ToolStatus;
203
+ expanded: boolean;
204
+ callComponent: unknown;
205
+ resultComponent: unknown;
206
+ ioView: ExpandedToolIoView | undefined;
207
+ ioHoveredSection: ToolIoSection | null | undefined;
208
+ ioRevision: number | undefined;
209
+ capturedIoFrame: boolean;
189
210
  lines: string[];
211
+ ioMarkers: CapturedIoViewMarker[];
190
212
  };
191
213
 
214
+ function toolIoView(tool: any): ExpandedToolIoView | undefined {
215
+ for (const candidate of [
216
+ tool?.rendererState?.ccstyleIoView,
217
+ tool?.state?.ccstyleIoView,
218
+ tool?.resultRendererComponent,
219
+ ]) {
220
+ if (isExpandedToolIoView(candidate)) {
221
+ return candidate;
222
+ }
223
+ }
224
+ return undefined;
225
+ }
226
+
227
+ function ioHoveredSection(
228
+ view: ExpandedToolIoView | undefined,
229
+ ): ToolIoSection | null | undefined {
230
+ return view?.getHoveredSection();
231
+ }
232
+
233
+ function ioRenderRevision(
234
+ view: ExpandedToolIoView | undefined,
235
+ ): number | undefined {
236
+ return view?.getRenderRevision();
237
+ }
238
+
192
239
  export class ToolGroupComponent extends Container {
193
240
  readonly toolCallId = `ccstyle-tool-group-${nextGroupId++}`;
194
241
  readonly toolName = "Tool group";
@@ -199,8 +246,12 @@ export class ToolGroupComponent extends Container {
199
246
  }
200
247
  private hintHovered = false;
201
248
  private readonly patch: Patch;
202
- /** 仅缓存已完成且折叠的分组;pending / expanded 每帧现算。 */
203
- private settledCache: SettledGroupCache | undefined;
249
+ /** 已完成分组按折叠/展开槽位跨帧缓存;pending 走子工具级缓存。 */
250
+ private settledCaches: SettledGroupCacheSlots = {};
251
+ private settledExpandedChildCaches = new WeakMap<
252
+ object,
253
+ SettledExpandedChildCache
254
+ >();
204
255
 
205
256
  constructor(patch: Patch) {
206
257
  super();
@@ -209,13 +260,13 @@ export class ToolGroupComponent extends Container {
209
260
  }
210
261
 
211
262
  addTool(tool: any): void {
212
- this.settledCache = undefined;
263
+ this.clearAllCaches();
213
264
  this.children.push(tool);
214
265
  tool[PARENT_KEY] = this;
215
266
  }
216
267
 
217
268
  releaseTools(): any[] {
218
- this.settledCache = undefined;
269
+ this.clearAllCaches();
219
270
  const tools = [...this.children];
220
271
  this.children.length = 0;
221
272
  this.patch.groups.delete(this);
@@ -223,14 +274,13 @@ export class ToolGroupComponent extends Container {
223
274
  }
224
275
 
225
276
  removeTool(tool: any): void {
226
- this.settledCache = undefined;
277
+ this.clearAllCaches();
227
278
  const index = this.children.indexOf(tool);
228
279
  if (index >= 0) this.children.splice(index, 1);
229
280
  if (tool?.[PARENT_KEY] === this) delete tool[PARENT_KEY];
230
281
  }
231
282
 
232
283
  setExpanded(expanded: boolean): void {
233
- if (this._expanded !== expanded) this.settledCache = undefined;
234
284
  this._expanded = expanded;
235
285
  for (const tool of this.children)
236
286
  (
@@ -239,7 +289,9 @@ export class ToolGroupComponent extends Container {
239
289
  }
240
290
 
241
291
  setHintHovered(hovered: boolean): void {
242
- if (this.hintHovered !== hovered) this.settledCache = undefined;
292
+ if (this.hintHovered !== hovered) {
293
+ this.clearSettledCaches();
294
+ }
243
295
  this.hintHovered = hovered;
244
296
  }
245
297
 
@@ -272,53 +324,205 @@ export class ToolGroupComponent extends Container {
272
324
  }
273
325
 
274
326
  invalidate(): void {
275
- this.settledCache = undefined;
327
+ this.clearAllCaches();
276
328
  for (const tool of this.children) tool.invalidate?.();
277
329
  }
278
330
 
331
+ private clearSettledCaches(): void {
332
+ this.settledCaches = {};
333
+ }
334
+
335
+ private clearAllCaches(): void {
336
+ this.clearSettledCaches();
337
+ this.settledExpandedChildCaches = new WeakMap();
338
+ }
339
+
279
340
  private settledCacheHit(width: number): string[] | undefined {
280
- const cache = this.settledCache;
281
- if (!cache || this._expanded) return;
341
+ const slot = this._expanded ? "expanded" : "collapsed";
342
+ const cache = this.settledCaches[slot];
343
+ if (!cache) {
344
+ return;
345
+ }
282
346
  if (
283
347
  cache.width !== width ||
348
+ cache.expanded !== this._expanded ||
284
349
  cache.hover !== this.hintHovered ||
285
350
  cache.theme !== this.patch.theme ||
286
- cache.fullscreen !== isToolTuiFullscreen()
351
+ cache.fullscreen !== isToolTuiFullscreen() ||
352
+ (getActiveIoViewFrame() !== null &&
353
+ !cache.capturedIoFrame &&
354
+ cache.ioViews.some(Boolean))
287
355
  ) {
288
356
  return;
289
357
  }
290
358
  const tools = this.children as any[];
291
- if (cache.children.length !== tools.length) return;
359
+ if (cache.children.length !== tools.length) {
360
+ return;
361
+ }
292
362
  for (let i = 0; i < tools.length; i++) {
293
363
  const tool = tools[i];
364
+ const ioView = toolIoView(tool);
365
+ const toolStatus = status(tool);
294
366
  if (
295
367
  cache.children[i] !== tool ||
296
368
  cache.args[i] !== tool?.args ||
297
369
  cache.results[i] !== tool?.result ||
298
- status(tool) === "pending"
370
+ cache.statuses[i] !== toolStatus ||
371
+ cache.callComponents[i] !== tool?.callRendererComponent ||
372
+ cache.resultComponents[i] !== tool?.resultRendererComponent ||
373
+ cache.ioViews[i] !== ioView ||
374
+ cache.ioHoveredSections[i] !== ioHoveredSection(ioView) ||
375
+ cache.ioRevisions[i] !== ioRenderRevision(ioView) ||
376
+ toolStatus === "pending"
299
377
  ) {
300
378
  return;
301
379
  }
302
380
  }
303
- return cache.lines;
381
+ return replayIoViewMarkers(cache.lines, cache.ioMarkers);
304
382
  }
305
383
 
306
384
  private storeSettledCache(width: number, lines: string[]): void {
307
- this.settledCache = {
385
+ const tools = this.children as any[];
386
+ const ioViews = tools.map(toolIoView);
387
+ const captured = captureIoViewMarkers(lines);
388
+ const cache: SettledGroupCache = {
308
389
  width,
390
+ expanded: this._expanded,
309
391
  hover: this.hintHovered,
310
392
  theme: this.patch.theme,
311
393
  fullscreen: isToolTuiFullscreen(),
312
- children: [...this.children],
313
- args: (this.children as any[]).map((tool) => tool?.args),
314
- results: (this.children as any[]).map((tool) => tool?.result),
315
- lines,
394
+ children: [...tools],
395
+ args: tools.map((tool) => tool?.args),
396
+ results: tools.map((tool) => tool?.result),
397
+ statuses: tools.map(status),
398
+ callComponents: tools.map((tool) => tool?.callRendererComponent),
399
+ resultComponents: tools.map((tool) => tool?.resultRendererComponent),
400
+ ioViews,
401
+ ioHoveredSections: ioViews.map(ioHoveredSection),
402
+ ioRevisions: ioViews.map(ioRenderRevision),
403
+ capturedIoFrame: getActiveIoViewFrame() !== null,
404
+ lines: captured.lines,
405
+ ioMarkers: captured.markers,
316
406
  };
407
+ this.settledCaches[this._expanded ? "expanded" : "collapsed"] = cache;
408
+ }
409
+
410
+ private settledExpandedChildCacheHit(
411
+ tool: any,
412
+ index: number,
413
+ total: number,
414
+ width: number,
415
+ ): string[] | undefined {
416
+ const cache = this.settledExpandedChildCaches.get(tool);
417
+ if (!cache) {
418
+ return;
419
+ }
420
+ const toolStatus = status(tool);
421
+ const ioView = toolIoView(tool);
422
+ if (
423
+ toolStatus === "pending" ||
424
+ cache.width !== width ||
425
+ cache.theme !== this.patch.theme ||
426
+ cache.fullscreen !== isToolTuiFullscreen() ||
427
+ cache.index !== index ||
428
+ cache.total !== total ||
429
+ cache.args !== tool?.args ||
430
+ cache.result !== tool?.result ||
431
+ cache.status !== toolStatus ||
432
+ cache.expanded !== (tool?.expanded === true) ||
433
+ cache.callComponent !== tool?.callRendererComponent ||
434
+ cache.resultComponent !== tool?.resultRendererComponent ||
435
+ cache.ioView !== ioView ||
436
+ cache.ioHoveredSection !== ioHoveredSection(ioView) ||
437
+ cache.ioRevision !== ioRenderRevision(ioView) ||
438
+ (getActiveIoViewFrame() !== null &&
439
+ !cache.capturedIoFrame &&
440
+ ioView !== undefined)
441
+ ) {
442
+ return;
443
+ }
444
+ return replayIoViewMarkers(cache.lines, cache.ioMarkers);
445
+ }
446
+
447
+ private storeSettledExpandedChildCache(
448
+ tool: any,
449
+ index: number,
450
+ total: number,
451
+ width: number,
452
+ lines: string[],
453
+ ): void {
454
+ const ioView = toolIoView(tool);
455
+ const captured = captureIoViewMarkers(lines);
456
+ this.settledExpandedChildCaches.set(tool, {
457
+ width,
458
+ theme: this.patch.theme,
459
+ fullscreen: isToolTuiFullscreen(),
460
+ index,
461
+ total,
462
+ args: tool?.args,
463
+ result: tool?.result,
464
+ status: status(tool),
465
+ expanded: tool?.expanded === true,
466
+ callComponent: tool?.callRendererComponent,
467
+ resultComponent: tool?.resultRendererComponent,
468
+ ioView,
469
+ ioHoveredSection: ioHoveredSection(ioView),
470
+ ioRevision: ioRenderRevision(ioView),
471
+ capturedIoFrame: getActiveIoViewFrame() !== null,
472
+ lines: captured.lines,
473
+ ioMarkers: captured.markers,
474
+ });
475
+ }
476
+
477
+ private renderExpandedChildBlock(
478
+ tool: any,
479
+ index: number,
480
+ total: number,
481
+ width: number,
482
+ theme: any,
483
+ fg: (color: string, text: string) => string,
484
+ ): string[] {
485
+ const cached = this.settledExpandedChildCacheHit(tool, index, total, width);
486
+ if (cached) {
487
+ return cached;
488
+ }
489
+
490
+ const toolStatus = status(tool);
491
+ const color = toolStatus === "pending" ? "accent" : toolStatus;
492
+ const branch = index === total - 1 ? "└" : "├";
493
+ const continuation = index === total - 1 ? " " : "│ ";
494
+ const rendered = visibleLines(tool.render(Math.max(1, width - 2)));
495
+ if (rendered.length) {
496
+ rendered[0] = stripLeadingStatusIcon(rendered[0])
497
+ .replace(/^ +/, "")
498
+ .replace(/^((?:\x1b\[[0-?]*[ -/]*[@-~])*) +/, "$1");
499
+ }
500
+ const childLines = rendered.length ? rendered : [toolSummary(tool).main];
501
+ const backgroundSlot = "userMessageBg";
502
+ const lines = childLines.map((line, lineIndex) => {
503
+ const content = lineIndex === 0 ? line : stripLeadingSpaces(line, 1);
504
+ const prefix =
505
+ lineIndex === 0
506
+ ? `${fg("dim", branch)} ${fg(color, statusIcon(toolStatus))} `
507
+ : fg("dim", continuation);
508
+ return paddedBackgroundRow(
509
+ theme,
510
+ backgroundSlot,
511
+ prefix + content,
512
+ width,
513
+ );
514
+ });
515
+ if (toolStatus !== "pending") {
516
+ this.storeSettledExpandedChildCache(tool, index, total, width, lines);
517
+ }
518
+ return lines;
317
519
  }
318
520
 
319
521
  render(width: number): string[] {
320
522
  const cached = this.settledCacheHit(width);
321
- if (cached) return cached;
523
+ if (cached) {
524
+ return cached;
525
+ }
322
526
  const theme = this.patch.theme;
323
527
  const fg = (color: string, text: string) =>
324
528
  theme?.fg?.(color, text) ?? text;
@@ -343,12 +547,6 @@ export class ToolGroupComponent extends Container {
343
547
  : counts.pending
344
548
  ? "pending"
345
549
  : "success";
346
- if (
347
- (this.children as any[]).some(
348
- (tool) => tool?.executionStarted && status(tool) === "pending",
349
- )
350
- )
351
- scheduleGroupAnimation(this.patch);
352
550
  const overallColor = overall === "pending" ? "accent" : overall;
353
551
  const nameList =
354
552
  names.size > 1 ? ` ${fg("dim", `• ${toolNameList(this.children)}`)}` : "";
@@ -363,52 +561,37 @@ export class ToolGroupComponent extends Container {
363
561
  ),
364
562
  ];
365
563
  const total = this.children.length;
366
- const expandedLines: string[] = [];
367
564
  for (let index = 0; index < total; index++) {
368
565
  const tool = this.children[index];
369
- const toolStatus = status(tool);
370
- const color = toolStatus === "pending" ? "accent" : toolStatus;
371
- const branch = index === total - 1 ? "└" : "├";
372
- const continuation = index === total - 1 ? " " : "│ ";
373
- if (!this._expanded) {
374
- const summary = toolSummary(tool);
566
+ if (this._expanded) {
375
567
  lines.push(
376
- truncateToWidth(
377
- ` ${fg("dim", branch)} ${fg(color, statusIcon(toolStatus))} ${fg("toolTitle", summary.main)}${fg("dim", summary.detail)}`,
568
+ ...this.renderExpandedChildBlock(
569
+ tool,
570
+ index,
571
+ total,
378
572
  width,
379
- "…",
573
+ theme,
574
+ fg,
380
575
  ),
381
576
  );
382
577
  continue;
383
578
  }
384
- const rendered = visibleLines(tool.render(Math.max(1, width - 2)));
385
- if (rendered.length) {
386
- rendered[0] = stripLeadingStatusIcon(rendered[0])
387
- .replace(/^ +/, "")
388
- .replace(/^((?:\x1b\[[0-?]*[ -/]*[@-~])*) +/, "$1");
389
- }
390
- const childLines = rendered.length ? rendered : [toolSummary(tool).main];
391
- for (let lineIndex = 0; lineIndex < childLines.length; lineIndex++) {
392
- const content =
393
- // 续行只剥外层 Box 的 1 格 left pad,保留 Input/Output 相对缩进
394
- lineIndex === 0
395
- ? childLines[lineIndex]
396
- : stripLeadingSpaces(childLines[lineIndex], 1);
397
- const prefix =
398
- lineIndex === 0
399
- ? `${fg("dim", branch)} ${fg(color, statusIcon(toolStatus))} `
400
- : fg("dim", continuation);
401
- expandedLines.push(prefix + content);
402
- }
579
+ const toolStatus = status(tool);
580
+ const color = toolStatus === "pending" ? "accent" : toolStatus;
581
+ const branch = index === total - 1 ? "└" : "├";
582
+ const summary = toolSummary(tool);
583
+ lines.push(
584
+ truncateToWidth(
585
+ ` ${fg("dim", branch)} ${fg(color, statusIcon(toolStatus))} ${fg("toolTitle", summary.main)}${fg("dim", summary.detail)}`,
586
+ width,
587
+ "…",
588
+ ),
589
+ );
403
590
  }
404
591
  if (this._expanded) {
405
- // 展开面板统一用 user message 背景色(ccstyle 约定),不按状态区分。
406
- const backgroundSlot = "userMessageBg";
407
- for (const line of expandedLines) {
408
- lines.push(paddedBackgroundRow(theme, backgroundSlot, line, width));
409
- }
410
- lines.push(paddedBackgroundRow(theme, backgroundSlot, "", width));
411
- } else if (counts.pending === 0) {
592
+ lines.push(paddedBackgroundRow(theme, "userMessageBg", "", width));
593
+ }
594
+ if (counts.pending === 0) {
412
595
  this.storeSettledCache(width, lines);
413
596
  }
414
597
  return lines;
@@ -517,8 +700,6 @@ export function installToolGrouping(
517
700
  if (previous) {
518
701
  previous.active = false;
519
702
  previous.enabled = () => false;
520
- if (previous.animationTimer) clearTimeout(previous.animationTimer);
521
- previous.animationTimer = null;
522
703
  ungroup(previous);
523
704
  }
524
705
  const original = {
@@ -545,7 +726,6 @@ export function installToolGrouping(
545
726
  enabled: getEnabled,
546
727
  generation: 0,
547
728
  lastEnabled: getEnabled(),
548
- animationTimer: null,
549
729
  };
550
730
  patch.installed = {
551
731
  addChild: function (this: any, component: any) {
@@ -604,8 +784,6 @@ export function installToolGrouping(
604
784
  shutdown() {
605
785
  if (!patch.active) return;
606
786
  patch.active = false;
607
- if (patch.animationTimer) clearTimeout(patch.animationTimer);
608
- patch.animationTimer = null;
609
787
  patch.enabled = () => false;
610
788
  ungroup(patch);
611
789
  if (prototype.addChild === patch.installed.addChild)