@tt-a1i/openpi 0.4.0 → 0.5.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.
Files changed (109) hide show
  1. package/README.md +102 -40
  2. package/SETUP.md +22 -6
  3. package/assets/openpi-launch-card-v1.webp +0 -0
  4. package/bin/openpi.js +145 -0
  5. package/extensions/background-terminals/index.ts +30 -2
  6. package/extensions/background-terminals/src/domain.ts +2 -0
  7. package/extensions/background-terminals/src/manager.ts +486 -106
  8. package/extensions/background-terminals/src/output.ts +33 -0
  9. package/extensions/background-terminals/src/prompt.ts +13 -5
  10. package/extensions/background-terminals/src/result-delivery.ts +4 -1
  11. package/extensions/clear-context/index.ts +83 -0
  12. package/extensions/context-pivot/index.ts +16 -6
  13. package/extensions/cron/schedule.ts +7 -1
  14. package/extensions/file-mutation-display/render.ts +17 -257
  15. package/extensions/file-search/src/binaries.ts +57 -41
  16. package/extensions/git-read/index.ts +1 -3
  17. package/extensions/model-info/index.ts +21 -33
  18. package/extensions/model-info/session-metrics.ts +96 -0
  19. package/extensions/plan-mode/bash-policy.ts +54 -9
  20. package/extensions/plan-mode/index.ts +7 -2
  21. package/extensions/post-edit/index.ts +16 -6
  22. package/extensions/sessions/git-stats.ts +258 -72
  23. package/extensions/sessions/index.ts +153 -86
  24. package/extensions/sessions/preview-cache.ts +104 -0
  25. package/extensions/sessions/preview-loader.ts +856 -0
  26. package/extensions/sessions/sessions.ts +43 -4
  27. package/extensions/setup/index.ts +123 -127
  28. package/extensions/shared/activity-status.ts +30 -0
  29. package/extensions/shared/agent-session-page.ts +319 -0
  30. package/extensions/shared/agent-tool-renderer.ts +218 -0
  31. package/extensions/shared/agent-transcript.ts +524 -0
  32. package/extensions/shared/capability-intent.ts +1 -1
  33. package/extensions/shared/child-session.ts +437 -21
  34. package/extensions/shared/result-delivery.ts +34 -0
  35. package/extensions/shared/setup-config.ts +73 -33
  36. package/extensions/shared/setup-episode-state.ts +1 -1
  37. package/extensions/shared/terminal-text.ts +110 -23
  38. package/extensions/shared/text-projection.ts +72 -15
  39. package/extensions/shared/tool-activity.ts +382 -0
  40. package/extensions/shared/tool-surface.ts +29 -2
  41. package/extensions/shared/transcript-viewport.ts +46 -0
  42. package/extensions/shared/web-observer-registry.ts +390 -0
  43. package/extensions/shared/worktree.ts +11 -0
  44. package/extensions/subagents/index.ts +270 -59
  45. package/extensions/subagents/navigation.ts +34 -5
  46. package/extensions/subagents/src/backend.ts +12 -1
  47. package/extensions/subagents/src/backends/pi.ts +375 -66
  48. package/extensions/subagents/src/domain.ts +5 -0
  49. package/extensions/subagents/src/manager.ts +34 -2
  50. package/extensions/subagents/src/prompt.ts +32 -4
  51. package/extensions/subagents/src/result-artifact.ts +4 -0
  52. package/extensions/subagents/src/result-delivery.ts +7 -1
  53. package/extensions/subagents/src/runtime.ts +15 -1
  54. package/extensions/subagents/src/ui/takeover.ts +73 -257
  55. package/extensions/subagents/src/ui/transcript.ts +38 -535
  56. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  57. package/extensions/suggestions/src/ui.ts +10 -4
  58. package/extensions/tasks/index.ts +0 -3
  59. package/extensions/ui-customization/footer.ts +0 -40
  60. package/extensions/ui-customization/index.ts +0 -4
  61. package/extensions/user-input-fold/index.ts +1 -1
  62. package/extensions/web/index.ts +234 -0
  63. package/extensions/workflows/artifacts.ts +137 -47
  64. package/extensions/workflows/completion-projection.ts +457 -0
  65. package/extensions/workflows/coordinator.ts +8 -10
  66. package/extensions/workflows/dashboard.ts +167 -228
  67. package/extensions/workflows/handoff.ts +70 -16
  68. package/extensions/workflows/index.ts +488 -198
  69. package/extensions/workflows/journal.ts +148 -13
  70. package/extensions/workflows/model.ts +74 -4
  71. package/extensions/workflows/navigation.ts +32 -8
  72. package/extensions/workflows/progress-projection.ts +306 -0
  73. package/extensions/workflows/prompt.ts +66 -6
  74. package/extensions/workflows/replay-safety.ts +42 -21
  75. package/extensions/workflows/result-delivery.ts +128 -64
  76. package/extensions/workflows/retention.ts +593 -0
  77. package/extensions/workflows/runner.ts +388 -279
  78. package/extensions/workflows/sandbox-child.cjs +25 -3
  79. package/extensions/workflows/sandbox.ts +62 -8
  80. package/extensions/workflows/serialization.ts +325 -17
  81. package/extensions/workflows/tool-renderer.ts +22 -0
  82. package/extensions/workflows/transcript.ts +149 -0
  83. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  84. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  85. package/package.json +28 -8
  86. package/skills/subagents/REFERENCE.md +189 -0
  87. package/skills/subagents/SKILL.md +1 -1
  88. package/skills/workflows/REFERENCE.md +4 -2
  89. package/web/adapter/pi-adapter.ts +661 -0
  90. package/web/host/browser-launcher.ts +20 -0
  91. package/web/host/static-assets.ts +4 -0
  92. package/web/host/terminal-status.ts +38 -0
  93. package/web/host/web-host.ts +789 -0
  94. package/web/http-dispatcher.ts +125 -0
  95. package/web/protocol/types.ts +462 -0
  96. package/web/runtime/pi-runtime.ts +991 -0
  97. package/web/runtime/types.ts +71 -0
  98. package/web/runtime/web-host-lease.ts +497 -0
  99. package/web/trace.ts +18 -0
  100. package/web/ui/app.js +1398 -0
  101. package/web/ui/index.html +139 -0
  102. package/web/ui/styles.css +598 -0
  103. package/web/vite.config.mjs +34 -0
  104. package/extensions/execution-convergence/active-evidence.ts +0 -129
  105. package/extensions/execution-convergence/index.ts +0 -442
  106. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  107. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  108. package/extensions/setup/intercom.ts +0 -603
  109. package/extensions/subagents/src/backends/stub.ts +0 -303
@@ -18,15 +18,10 @@ import {
18
18
  getAgentDir,
19
19
  type KeybindingsManager,
20
20
  } from "@earendil-works/pi-coding-agent";
21
- import {
22
- Key,
23
- matchesKey,
24
- type TUI,
25
- truncateToWidth,
26
- wrapTextWithAnsi,
27
- } from "@earendil-works/pi-tui";
28
- import { contextPercent } from "../shared/context-utilization.ts";
21
+ import { type TUI, truncateToWidth } from "@earendil-works/pi-tui";
22
+ import { AgentSessionPage } from "../shared/agent-session-page.ts";
29
23
  import { fitNavigationSides } from "../shared/below-editor-navigation.ts";
24
+ import { contextPercent } from "../shared/context-utilization.ts";
30
25
  import {
31
26
  panelFrame,
32
27
  type ScreenHint,
@@ -68,10 +63,10 @@ import {
68
63
  workflowGraphRecords,
69
64
  } from "./model.ts";
70
65
  import { writeFileAtomic } from "./serialization.ts";
66
+ import { WorkflowTranscriptAdapter } from "./transcript.ts";
71
67
 
72
68
  const NOTICE_TTL_MS = 4000;
73
69
  const MIN_HEIGHT = 10;
74
- const TRANSCRIPT_SCROLL_STEP = 20;
75
70
 
76
71
  function wrapSelection(index: number, delta: number, length: number): number {
77
72
  if (length === 0) return 0;
@@ -261,6 +256,10 @@ function normalizeTranscript(value: unknown): TranscriptEntry[] {
261
256
  typeof entry.name === "string"
262
257
  ? sanitizeLine(entry.name, 160) || undefined
263
258
  : undefined,
259
+ toolCallId:
260
+ typeof entry.toolCallId === "string"
261
+ ? sanitizeLine(entry.toolCallId, 1_024) || undefined
262
+ : undefined,
264
263
  isError: entry.isError === true,
265
264
  timestamp:
266
265
  typeof entry.timestamp === "number" ? entry.timestamp : undefined,
@@ -269,6 +268,35 @@ function normalizeTranscript(value: unknown): TranscriptEntry[] {
269
268
  return transcript;
270
269
  }
271
270
 
271
+ function normalizeAgentState(value: unknown): AgentRecord["state"] {
272
+ switch (value) {
273
+ case "done":
274
+ case "completed":
275
+ return "done";
276
+ case "error":
277
+ case "failed":
278
+ return "error";
279
+ case "running":
280
+ case "uncertain":
281
+ return value;
282
+ default:
283
+ return "uncertain";
284
+ }
285
+ }
286
+
287
+ function normalizeWorkflowStatus(value: unknown): WorkflowDetails["status"] {
288
+ switch (value) {
289
+ case "running":
290
+ case "completed":
291
+ case "failed":
292
+ case "aborted":
293
+ case "uncertain":
294
+ return value;
295
+ default:
296
+ return "uncertain";
297
+ }
298
+ }
299
+
272
300
  /** Leniently normalize a workflow.json (including runs from older tooling). */
273
301
  export function normalizePersistedWorkflowDetails(
274
302
  runId: string,
@@ -284,14 +312,7 @@ export function normalizePersistedWorkflowDetails(
284
312
  for (const item of rawAgents) {
285
313
  if (!item || typeof item !== "object") continue;
286
314
  const a = item as Record<string, unknown>;
287
- const state =
288
- a.state === "error" || a.state === "failed"
289
- ? "error"
290
- : a.state === "uncertain"
291
- ? "uncertain"
292
- : a.state === "running"
293
- ? "running"
294
- : "done";
315
+ const state = normalizeAgentState(a.state);
295
316
  const index = typeof a.index === "number" ? a.index : agents.length + 1;
296
317
  const decodedInvocation = decodeInvocationRecord(a.invocation);
297
318
  const invocation =
@@ -407,16 +428,23 @@ export function normalizePersistedWorkflowDetails(
407
428
  logs.push({
408
429
  at: typeof entry.at === "number" ? entry.at : startedAt,
409
430
  text: sanitizeLine(entry.text, MAX_LOG_TEXT),
431
+ ...(entry.kind === "pipeline-drop"
432
+ ? { kind: "pipeline-drop" as const }
433
+ : {}),
410
434
  });
411
435
  }
412
436
 
413
- const status =
414
- record.status === "running" ||
415
- record.status === "failed" ||
416
- record.status === "aborted" ||
417
- record.status === "uncertain"
418
- ? record.status
419
- : "completed";
437
+ let status = normalizeWorkflowStatus(record.status);
438
+ if (status !== "running") {
439
+ for (const agent of agents) {
440
+ if (agent.state !== "running" && agent.state !== "uncertain") continue;
441
+ status = "uncertain";
442
+ agent.state = "uncertain";
443
+ agent.error =
444
+ agent.error ??
445
+ "Persisted terminal workflow contained an agent without terminal evidence";
446
+ }
447
+ }
420
448
 
421
449
  return {
422
450
  runId,
@@ -541,22 +569,31 @@ export function loadRunEntries(
541
569
  referencedRunIds: ReadonlySet<string>,
542
570
  /** Hide runs untouched by the current request; live runs always show. */
543
571
  startedSince = 0,
572
+ /** Bounded settled projections used only if canonical disk state is unreadable. */
573
+ retained: ReadonlyMap<string, WorkflowDetails> = new Map(),
544
574
  ): RunEntry[] {
545
575
  const entries: RunEntry[] = [];
546
- for (const runId of listPersistedRunIds()) {
576
+ const runIds = new Set([...listPersistedRunIds(), ...retained.keys()]);
577
+ for (const runId of runIds) {
547
578
  const live = active.get(runId);
548
579
  if (live) {
549
580
  entries.push({ runId, details: live, live: true });
550
581
  continue;
551
582
  }
552
- const details = readPersistedWorkflowDetails(runId, {
583
+ const persisted = readPersistedWorkflowDetails(runId, {
553
584
  hydrateArtifacts: true,
554
585
  });
586
+ const retainedDetails = retained.get(runId);
587
+ const details = persisted ?? retainedDetails;
555
588
  if (!details) continue;
589
+ const fromRetention =
590
+ persisted === undefined && retainedDetails !== undefined;
556
591
  const touchedAt = Math.max(details.startedAt, details.finishedAt ?? 0);
557
592
  if (
558
593
  touchedAt < startedSince ||
559
- (details.sessionId !== sessionId && !referencedRunIds.has(runId))
594
+ (!fromRetention &&
595
+ details.sessionId !== sessionId &&
596
+ !referencedRunIds.has(runId))
560
597
  ) {
561
598
  continue;
562
599
  }
@@ -677,24 +714,24 @@ export class WorkflowDashboard {
677
714
  private phaseIndex = 0;
678
715
  private agentIndex = 0;
679
716
  private detailFocus: DetailFocus = "phases";
680
- private transcriptScroll = 0;
681
- private transcriptRowCount = 0;
682
- private transcriptViewportSize = 1;
717
+ private transcriptPage?: AgentSessionPage;
683
718
  private current?: RunEntry;
684
719
  private openedDirectly = false;
685
720
  private notice?: string;
686
721
  private noticeAt = 0;
687
722
  private disposed = false;
688
- private timer: ReturnType<typeof setInterval>;
723
+ private timer?: ReturnType<typeof setInterval>;
689
724
  private tui: TUI;
690
725
  private theme: Theme;
691
726
  private keybindings: KeybindingsManager;
692
727
  private getActive: () => Map<string, WorkflowDetails>;
728
+ private getRetained: () => ReadonlyMap<string, WorkflowDetails>;
693
729
  private sessionId: string;
694
730
  private referencedRunIds: ReadonlySet<string>;
695
731
  private startedSince: number;
696
732
  private close: () => void;
697
733
  private onAbort?: (runId: string) => boolean;
734
+ private initialToolsExpanded: boolean;
698
735
 
699
736
  constructor(
700
737
  tui: TUI,
@@ -707,16 +744,20 @@ export class WorkflowDashboard {
707
744
  close: () => void,
708
745
  initialRunId?: string,
709
746
  onAbort?: (runId: string) => boolean,
747
+ getRetained: () => ReadonlyMap<string, WorkflowDetails> = () => new Map(),
748
+ initialToolsExpanded = false,
710
749
  ) {
711
750
  this.tui = tui;
712
751
  this.theme = theme;
713
752
  this.keybindings = keybindings;
714
753
  this.getActive = getActive;
754
+ this.getRetained = getRetained;
715
755
  this.sessionId = sessionId;
716
756
  this.referencedRunIds = referencedRunIds;
717
757
  this.startedSince = startedSince;
718
758
  this.close = close;
719
759
  this.onAbort = onAbort;
760
+ this.initialToolsExpanded = initialToolsExpanded;
720
761
  this.refresh();
721
762
  if (initialRunId) {
722
763
  const resolution = resolveWorkflowRunTarget(
@@ -736,25 +777,44 @@ export class WorkflowDashboard {
736
777
  this.noticeAt = Date.now();
737
778
  }
738
779
  }
780
+ this.refreshTimer();
781
+ }
782
+
783
+ private refreshTimer() {
784
+ const active = Boolean(
785
+ this.notice ||
786
+ (this.view === "list"
787
+ ? this.entries.some(
788
+ (entry) => entry.live && entry.details.status === "running",
789
+ )
790
+ : this.view === "detail"
791
+ ? this.current?.live && this.current.details.status === "running"
792
+ : this.current?.live && this.selectedAgent()?.state === "running"),
793
+ );
794
+ if (!active) {
795
+ if (this.timer) clearInterval(this.timer);
796
+ this.timer = undefined;
797
+ return;
798
+ }
799
+ if (this.timer) return;
739
800
  this.timer = setInterval(() => {
740
- if (
741
- this.entries.some((e) => e.live) ||
742
- this.current?.live ||
743
- this.notice
744
- ) {
745
- this.refresh();
746
- this.tui.requestRender();
747
- }
801
+ this.refresh();
802
+ this.tui.requestRender();
803
+ this.refreshTimer();
748
804
  }, SPINNER_INTERVAL_MS);
749
805
  }
750
806
 
751
807
  dispose() {
752
808
  if (this.disposed) return;
753
809
  this.disposed = true;
754
- clearInterval(this.timer);
810
+ if (this.timer) clearInterval(this.timer);
811
+ this.timer = undefined;
812
+ this.transcriptPage = undefined;
755
813
  }
756
814
 
757
- invalidate() {}
815
+ invalidate() {
816
+ this.transcriptPage?.invalidate();
817
+ }
758
818
 
759
819
  private refresh() {
760
820
  const selected = this.entries[this.listIndex]?.runId;
@@ -763,6 +823,7 @@ export class WorkflowDashboard {
763
823
  this.sessionId,
764
824
  this.referencedRunIds,
765
825
  this.startedSince,
826
+ this.getRetained(),
766
827
  );
767
828
  if (selected) {
768
829
  const index = this.entries.findIndex((e) => e.runId === selected);
@@ -819,11 +880,12 @@ export class WorkflowDashboard {
819
880
  const target = path.join(runsDir(), entry.runId, "report.md");
820
881
  try {
821
882
  writeFileAtomic(target, buildWorkflowReport(entry.details));
822
- this.notice = `saved ${shortenHome(target)}`;
883
+ this.setNotice(`saved ${shortenHome(target)}`);
823
884
  } catch (error) {
824
- this.notice = `save failed: ${error instanceof Error ? error.message : String(error)}`;
885
+ this.setNotice(
886
+ `save failed: ${error instanceof Error ? error.message : String(error)}`,
887
+ );
825
888
  }
826
- this.noticeAt = Date.now();
827
889
  }
828
890
 
829
891
  /** Request cancellation of a run by id, surfacing the outcome as a notice. */
@@ -842,6 +904,7 @@ export class WorkflowDashboard {
842
904
  private setNotice(text: string) {
843
905
  this.notice = text;
844
906
  this.noticeAt = Date.now();
907
+ this.refreshTimer();
845
908
  }
846
909
 
847
910
  handleInput(data: string) {
@@ -923,57 +986,27 @@ export class WorkflowDashboard {
923
986
  } else if (left || cancel) {
924
987
  this.detailFocus = "phases";
925
988
  } else if ((right || confirm) && this.selectedAgent()) {
926
- this.transcriptScroll = 0;
927
- this.view = "transcript";
989
+ this.openTranscriptPage();
928
990
  }
929
991
  }
930
992
  if (data === "s") this.saveReport();
931
993
  if (data === "x") this.abortRun(this.current);
932
994
  } else {
933
- const maxScroll = Math.max(
934
- 0,
935
- this.transcriptRowCount - this.transcriptViewportSize,
936
- );
937
- const scrollStep =
938
- data === "j" || data === "k" ? TRANSCRIPT_SCROLL_STEP : 1;
939
- const pageStep = Math.max(1, this.transcriptViewportSize - 2);
940
- if (up) {
941
- this.transcriptScroll = Math.max(0, this.transcriptScroll - scrollStep);
942
- } else if (down) {
943
- this.transcriptScroll = Math.min(
944
- maxScroll,
945
- this.transcriptScroll + scrollStep,
946
- );
947
- } else if (matchesKey(data, Key.ctrl("u"))) {
948
- this.transcriptScroll = Math.max(0, this.transcriptScroll - pageStep);
949
- } else if (matchesKey(data, Key.ctrl("d"))) {
950
- this.transcriptScroll = Math.min(
951
- maxScroll,
952
- this.transcriptScroll + pageStep,
953
- );
954
- } else if (data === "g") {
955
- this.transcriptScroll = 0;
956
- } else if (data === "G") {
957
- this.transcriptScroll = maxScroll;
958
- } else if (cancel || left) {
959
- this.view = "detail";
960
- this.detailFocus = "agents";
961
- }
995
+ this.transcriptPage?.handleInput(data);
996
+ this.refreshTimer();
997
+ return;
962
998
  }
999
+ this.refreshTimer();
963
1000
  this.tui.requestRender();
964
1001
  }
965
1002
 
966
1003
  render(width: number): string[] {
1004
+ if (this.view === "transcript" && this.transcriptPage) {
1005
+ return this.transcriptPage.render(width);
1006
+ }
967
1007
  const height = Math.max(MIN_HEIGHT, this.tui.terminal.rows - 1);
968
1008
  let lines: string[];
969
- if (this.view === "transcript" && this.current && this.selectedAgent()) {
970
- lines = this.renderTranscript(
971
- this.current.details,
972
- this.selectedAgent()!,
973
- width,
974
- height,
975
- );
976
- } else if (this.view === "detail" && this.current) {
1009
+ if (this.view === "detail" && this.current) {
977
1010
  lines = this.renderDetail(this.current.details, width, height);
978
1011
  } else {
979
1012
  lines = this.renderList(width, height);
@@ -981,6 +1014,53 @@ export class WorkflowDashboard {
981
1014
  return lines.map((line) => truncateToWidth(line, width, ""));
982
1015
  }
983
1016
 
1017
+ private openTranscriptPage() {
1018
+ const transcriptAdapter = new WorkflowTranscriptAdapter();
1019
+ this.view = "transcript";
1020
+ this.transcriptPage = new AgentSessionPage(
1021
+ this.tui,
1022
+ this.theme,
1023
+ this.keybindings,
1024
+ {
1025
+ getState: () => {
1026
+ const details = this.current?.details;
1027
+ const agent = this.selectedAgent();
1028
+ if (!details || !agent) return undefined;
1029
+ return {
1030
+ id: agent.callId ?? `agent-${agent.index}`,
1031
+ title: agent.label,
1032
+ status: agent.state,
1033
+ document: transcriptAdapter.document(
1034
+ agent.transcript,
1035
+ agent.worktreePath,
1036
+ ),
1037
+ metadata: [
1038
+ `${details.name ?? details.runId} · ${agent.phase ?? "unphased"}`,
1039
+ agent.model,
1040
+ agentContext(agent),
1041
+ agent.acceptance
1042
+ ? `acceptance:${agent.acceptance.status}`
1043
+ : undefined,
1044
+ formatElapsed(agent.startedAt, agent.finishedAt),
1045
+ ],
1046
+ errorText: agent.error,
1047
+ emptyText:
1048
+ "transcript unavailable (this run predates transcript capture)",
1049
+ };
1050
+ },
1051
+ close: () => {
1052
+ this.transcriptPage = undefined;
1053
+ this.view = "detail";
1054
+ this.detailFocus = "agents";
1055
+ this.refreshTimer();
1056
+ this.tui.requestRender();
1057
+ },
1058
+ },
1059
+ { toolsExpanded: this.initialToolsExpanded },
1060
+ );
1061
+ this.tui.requestRender();
1062
+ }
1063
+
984
1064
  /** Bordered panel with a title in the top border, padded to exact height. */
985
1065
  private panel(
986
1066
  title: string,
@@ -1313,117 +1393,6 @@ export class WorkflowDashboard {
1313
1393
  lines.push(this.hintLine(hints, width));
1314
1394
  return lines;
1315
1395
  }
1316
-
1317
- private transcriptRows(agent: AgentRecord, width: number): string[] {
1318
- const theme = this.theme;
1319
- const rows: string[] = [];
1320
- if (agent.transcript.length === 0) {
1321
- return [
1322
- theme.fg(
1323
- "dim",
1324
- " transcript unavailable (this run predates transcript capture)",
1325
- ),
1326
- ];
1327
- }
1328
-
1329
- for (const entry of agent.transcript) {
1330
- // Tool calls are one-liners: the arrow shows direction, the accent name
1331
- // says what ran, and the arguments collapse to a single compact line
1332
- // instead of a vertical JSON block.
1333
- if (entry.role === "tool") {
1334
- const name = entry.name ? sanitizeLine(entry.name, 160) : "unknown";
1335
- const args = compactInlineJson(entry.text);
1336
- rows.push(
1337
- ` ${theme.fg("muted", "→")} ${theme.fg("accent", name)}${args ? theme.fg("dim", ` ${args}`) : ""}`,
1338
- );
1339
- continue;
1340
- }
1341
- const label = transcriptLabel(entry);
1342
- const color = transcriptColor(entry);
1343
- const marker = entry.role === "toolResult" ? "←" : "●";
1344
- rows.push(
1345
- ` ${theme.fg(color, marker)} ${theme.bold(theme.fg(color, label))}`,
1346
- );
1347
- const contentWidth = Math.max(8, width - 4);
1348
- const styled = theme.fg(
1349
- entry.role === "thinking" || entry.role === "toolResult"
1350
- ? "dim"
1351
- : entry.isError
1352
- ? "error"
1353
- : "text",
1354
- sanitizeTerminalText(entry.text),
1355
- );
1356
- for (const line of wrapTextWithAnsi(styled, contentWidth)) {
1357
- rows.push(` ${line}`);
1358
- }
1359
- rows.push("");
1360
- }
1361
- return rows;
1362
- }
1363
-
1364
- private renderTranscript(
1365
- details: WorkflowDetails,
1366
- agent: AgentRecord,
1367
- width: number,
1368
- height: number,
1369
- ): string[] {
1370
- const theme = this.theme;
1371
- const lines: string[] = [];
1372
- const right = theme.fg(
1373
- "dim",
1374
- [
1375
- agent.model,
1376
- agentContext(agent),
1377
- agent.acceptance ? `acceptance:${agent.acceptance.status}` : undefined,
1378
- formatElapsed(agent.startedAt, agent.finishedAt),
1379
- ]
1380
- .filter(Boolean)
1381
- .join(" · ") + " ",
1382
- );
1383
- lines.push(
1384
- fitNavigationSides(
1385
- ` ${stateGlyph(agent.state, theme, Date.now())} ${theme.bold(theme.fg("accent", agent.label))}`,
1386
- right,
1387
- width,
1388
- ),
1389
- );
1390
- lines.push(
1391
- fitNavigationSides(
1392
- ` ${theme.fg("muted", `${details.name ?? details.runId} · ${agent.phase ?? "unphased"}`)}`,
1393
- theme.fg("dim", `${agent.transcript.length} entries `),
1394
- width,
1395
- ),
1396
- );
1397
-
1398
- const panelHeight = height - 3;
1399
- const bodyHeight = Math.max(1, panelHeight - 2);
1400
- const rows = this.transcriptRows(agent, width - 2);
1401
- this.transcriptRowCount = rows.length;
1402
- this.transcriptViewportSize = bodyHeight;
1403
- const maxScroll = Math.max(0, rows.length - bodyHeight);
1404
- this.transcriptScroll = Math.min(this.transcriptScroll, maxScroll);
1405
- const visible = rows.slice(
1406
- this.transcriptScroll,
1407
- this.transcriptScroll + bodyHeight,
1408
- );
1409
- const position =
1410
- rows.length > bodyHeight
1411
- ? `Transcript · ${this.transcriptScroll + 1}-${Math.min(rows.length, this.transcriptScroll + bodyHeight)}/${rows.length}`
1412
- : "Transcript";
1413
- lines.push(...this.panel(position, visible, width, panelHeight));
1414
- lines.push(
1415
- this.hintLine(
1416
- [
1417
- ["j/k", "scroll"],
1418
- ["ctrl-u/d", "page"],
1419
- ["g/G", "top/bottom"],
1420
- ["h/left/esc", "back"],
1421
- ],
1422
- width,
1423
- ),
1424
- );
1425
- return lines;
1426
- }
1427
1396
  }
1428
1397
 
1429
1398
  /**
@@ -1438,39 +1407,6 @@ function displayError(error: string) {
1438
1407
  return clean;
1439
1408
  }
1440
1409
 
1441
- function transcriptLabel(entry: TranscriptEntry): string {
1442
- if (entry.role === "user") return "user";
1443
- if (entry.role === "assistant") return "assistant";
1444
- if (entry.role === "thinking") return "thinking";
1445
- const name = entry.name ? sanitizeLine(entry.name, 160) : "unknown";
1446
- return name;
1447
- }
1448
-
1449
- /**
1450
- * Tool arguments arrive pretty-printed over many lines; the transcript shows
1451
- * them inline. Non-JSON text passes through flattened.
1452
- */
1453
- function compactInlineJson(text: string) {
1454
- const flat = sanitizeTerminalText(text).trim();
1455
- if (!flat) return "";
1456
- try {
1457
- return JSON.stringify(JSON.parse(flat));
1458
- } catch {
1459
- return flat.replace(/\s+/g, " ");
1460
- }
1461
- }
1462
-
1463
- function transcriptColor(
1464
- entry: TranscriptEntry,
1465
- ): "accent" | "success" | "dim" | "warning" | "error" | "muted" {
1466
- if (entry.isError) return "error";
1467
- if (entry.role === "user") return "accent";
1468
- if (entry.role === "assistant") return "success";
1469
- if (entry.role === "thinking") return "dim";
1470
- if (entry.role === "tool") return "warning";
1471
- return "muted";
1472
- }
1473
-
1474
1410
  function groupGlyph(group: PhaseGroup, theme: Theme) {
1475
1411
  if (group.agents.length === 0) return theme.fg("dim", "○");
1476
1412
  if (group.agents.some((a) => a.state === "running"))
@@ -1489,6 +1425,7 @@ export async function showWorkflowDashboard(
1489
1425
  initialRunId?: string,
1490
1426
  startedSince = 0,
1491
1427
  onAbort?: (runId: string) => boolean,
1428
+ getRetained?: () => ReadonlyMap<string, WorkflowDetails>,
1492
1429
  ) {
1493
1430
  await ctx.ui.custom<void>(
1494
1431
  (tui, theme, keybindings, done) => {
@@ -1506,12 +1443,14 @@ export async function showWorkflowDashboard(
1506
1443
  },
1507
1444
  initialRunId,
1508
1445
  onAbort,
1446
+ getRetained,
1447
+ ctx.ui.getToolsExpanded(),
1509
1448
  );
1510
1449
  return dashboard;
1511
1450
  },
1512
1451
  {
1513
1452
  overlay: true,
1514
- overlayOptions: { anchor: "center", width: "100%", maxHeight: "100%" },
1453
+ overlayOptions: { anchor: "top-left", width: "100%", maxHeight: "100%" },
1515
1454
  },
1516
1455
  );
1517
1456
  }