@tt-a1i/openpi 0.4.0 → 0.6.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 (141) hide show
  1. package/README.md +116 -46
  2. package/SETUP.md +29 -7
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/assets/openpi-launch-card-v1.webp +0 -0
  5. package/bin/openpi.js +155 -0
  6. package/extensions/ai-providers/LICENSE.upstream +23 -0
  7. package/extensions/ai-providers/README.md +59 -0
  8. package/extensions/ai-providers/antigravity/credentials.ts +52 -0
  9. package/extensions/ai-providers/antigravity/discovery.ts +130 -0
  10. package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
  11. package/extensions/ai-providers/antigravity/models.ts +84 -0
  12. package/extensions/ai-providers/antigravity/oauth.ts +700 -0
  13. package/extensions/ai-providers/antigravity/provider.ts +1116 -0
  14. package/extensions/ai-providers/antigravity/routing.ts +340 -0
  15. package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
  16. package/extensions/ai-providers/cursor/constants.ts +5 -0
  17. package/extensions/ai-providers/cursor/credentials.ts +14 -0
  18. package/extensions/ai-providers/cursor/discovery.ts +291 -0
  19. package/extensions/ai-providers/cursor/input-images.ts +106 -0
  20. package/extensions/ai-providers/cursor/models.ts +45 -0
  21. package/extensions/ai-providers/cursor/oauth.ts +263 -0
  22. package/extensions/ai-providers/cursor/proto.ts +1064 -0
  23. package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
  24. package/extensions/ai-providers/cursor/provider.ts +1175 -0
  25. package/extensions/ai-providers/cursor/proxy.ts +213 -0
  26. package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
  27. package/extensions/ai-providers/index.ts +86 -0
  28. package/extensions/ai-providers/oauth-adapter.ts +81 -0
  29. package/extensions/ai-providers/usage.ts +10 -0
  30. package/extensions/background-terminals/index.ts +38 -3
  31. package/extensions/background-terminals/src/domain.ts +2 -0
  32. package/extensions/background-terminals/src/manager.ts +484 -106
  33. package/extensions/background-terminals/src/output.ts +33 -0
  34. package/extensions/background-terminals/src/prompt.ts +13 -5
  35. package/extensions/background-terminals/src/result-delivery.ts +47 -24
  36. package/extensions/clear-context/index.ts +83 -0
  37. package/extensions/context-pivot/index.ts +16 -6
  38. package/extensions/cron/index.ts +68 -27
  39. package/extensions/cron/schedule.ts +12 -2
  40. package/extensions/file-mutation-display/render.ts +17 -257
  41. package/extensions/file-search/src/binaries.ts +57 -41
  42. package/extensions/git-read/index.ts +1 -3
  43. package/extensions/model-info/cache-diagnostics.ts +220 -0
  44. package/extensions/model-info/index.ts +65 -33
  45. package/extensions/model-info/session-metrics.ts +96 -0
  46. package/extensions/plan-mode/bash-policy.ts +54 -9
  47. package/extensions/plan-mode/index.ts +82 -6
  48. package/extensions/post-edit/index.ts +16 -6
  49. package/extensions/sessions/git-stats.ts +258 -72
  50. package/extensions/sessions/index.ts +153 -86
  51. package/extensions/sessions/preview-cache.ts +104 -0
  52. package/extensions/sessions/preview-loader.ts +856 -0
  53. package/extensions/sessions/sessions.ts +43 -4
  54. package/extensions/setup/index.ts +138 -130
  55. package/extensions/shared/activity-status.ts +30 -0
  56. package/extensions/shared/agent-session-page.ts +319 -0
  57. package/extensions/shared/agent-tool-renderer.ts +218 -0
  58. package/extensions/shared/agent-transcript.ts +524 -0
  59. package/extensions/shared/capability-intent.ts +1 -1
  60. package/extensions/shared/child-session.ts +457 -21
  61. package/extensions/shared/completion-inbox.ts +193 -0
  62. package/extensions/shared/result-delivery.ts +34 -0
  63. package/extensions/shared/setup-config.ts +83 -34
  64. package/extensions/shared/setup-episode-state.ts +1 -1
  65. package/extensions/shared/structured-output.ts +154 -0
  66. package/extensions/shared/terminal-text.ts +110 -23
  67. package/extensions/shared/text-projection.ts +72 -15
  68. package/extensions/shared/tool-activity.ts +382 -0
  69. package/extensions/shared/tool-surface.ts +29 -2
  70. package/extensions/shared/transcript-viewport.ts +46 -0
  71. package/extensions/shared/web-observer-registry.ts +390 -0
  72. package/extensions/shared/worktree.ts +11 -0
  73. package/extensions/subagents/index.ts +313 -62
  74. package/extensions/subagents/navigation.ts +34 -5
  75. package/extensions/subagents/src/backend.ts +12 -1
  76. package/extensions/subagents/src/backends/pi.ts +450 -70
  77. package/extensions/subagents/src/domain.ts +21 -1
  78. package/extensions/subagents/src/manager.ts +39 -2
  79. package/extensions/subagents/src/prompt.ts +49 -7
  80. package/extensions/subagents/src/result-artifact.ts +36 -0
  81. package/extensions/subagents/src/result-delivery.ts +39 -14
  82. package/extensions/subagents/src/runtime.ts +15 -1
  83. package/extensions/subagents/src/ui/takeover.ts +73 -257
  84. package/extensions/subagents/src/ui/transcript.ts +38 -535
  85. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  86. package/extensions/suggestions/src/ui.ts +10 -4
  87. package/extensions/tasks/index.ts +0 -3
  88. package/extensions/ui-customization/footer.ts +16 -45
  89. package/extensions/ui-customization/index.ts +0 -4
  90. package/extensions/user-input-fold/index.ts +42 -6
  91. package/extensions/web/index.ts +257 -0
  92. package/extensions/workflows/acceptance.ts +43 -19
  93. package/extensions/workflows/artifacts.ts +137 -47
  94. package/extensions/workflows/completion-projection.ts +459 -0
  95. package/extensions/workflows/coordinator.ts +8 -10
  96. package/extensions/workflows/dashboard.ts +175 -228
  97. package/extensions/workflows/handoff.ts +70 -16
  98. package/extensions/workflows/index.ts +501 -198
  99. package/extensions/workflows/journal.ts +148 -13
  100. package/extensions/workflows/model.ts +79 -5
  101. package/extensions/workflows/navigation.ts +32 -8
  102. package/extensions/workflows/progress-projection.ts +306 -0
  103. package/extensions/workflows/prompt.ts +70 -16
  104. package/extensions/workflows/replay-safety.ts +42 -21
  105. package/extensions/workflows/result-delivery.ts +214 -76
  106. package/extensions/workflows/retention.ts +599 -0
  107. package/extensions/workflows/runner.ts +389 -345
  108. package/extensions/workflows/sandbox-child.cjs +25 -3
  109. package/extensions/workflows/sandbox.ts +62 -8
  110. package/extensions/workflows/serialization.ts +325 -17
  111. package/extensions/workflows/tool-renderer.ts +22 -0
  112. package/extensions/workflows/transcript.ts +149 -0
  113. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  114. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  115. package/package.json +34 -14
  116. package/skills/subagents/REFERENCE.md +190 -0
  117. package/skills/subagents/SKILL.md +2 -1
  118. package/skills/workflows/REFERENCE.md +6 -4
  119. package/skills/workflows/SKILL.md +1 -1
  120. package/web/adapter/pi-adapter.ts +664 -0
  121. package/web/host/browser-launcher.ts +20 -0
  122. package/web/host/pi-coding-agent-entry.ts +162 -0
  123. package/web/host/static-assets.ts +4 -0
  124. package/web/host/terminal-status.ts +38 -0
  125. package/web/host/web-host.ts +1069 -0
  126. package/web/http-dispatcher.ts +125 -0
  127. package/web/protocol/types.ts +467 -0
  128. package/web/runtime/pi-runtime.ts +1206 -0
  129. package/web/runtime/types.ts +102 -0
  130. package/web/runtime/web-host-lease.ts +497 -0
  131. package/web/trace.ts +18 -0
  132. package/web/ui/app.js +1700 -0
  133. package/web/ui/index.html +142 -0
  134. package/web/ui/styles.css +680 -0
  135. package/web/vite.config.mjs +34 -0
  136. package/extensions/execution-convergence/active-evidence.ts +0 -129
  137. package/extensions/execution-convergence/index.ts +0 -442
  138. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  139. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  140. package/extensions/setup/intercom.ts +0 -603
  141. 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;
@@ -225,6 +220,14 @@ function normalizeDelivery(value: unknown): WorkflowDetails["delivery"] {
225
220
  : 0;
226
221
  return {
227
222
  id: sanitizeLine(record.id, 256),
223
+ ...(typeof record.ownerSessionId === "string" && record.ownerSessionId
224
+ ? { ownerSessionId: sanitizeLine(record.ownerSessionId, 256) }
225
+ : {}),
226
+ ...(typeof record.ownerEpoch === "number" &&
227
+ Number.isSafeInteger(record.ownerEpoch) &&
228
+ record.ownerEpoch >= 0
229
+ ? { ownerEpoch: record.ownerEpoch }
230
+ : {}),
228
231
  state,
229
232
  attempts,
230
233
  updatedAt,
@@ -261,6 +264,10 @@ function normalizeTranscript(value: unknown): TranscriptEntry[] {
261
264
  typeof entry.name === "string"
262
265
  ? sanitizeLine(entry.name, 160) || undefined
263
266
  : undefined,
267
+ toolCallId:
268
+ typeof entry.toolCallId === "string"
269
+ ? sanitizeLine(entry.toolCallId, 1_024) || undefined
270
+ : undefined,
264
271
  isError: entry.isError === true,
265
272
  timestamp:
266
273
  typeof entry.timestamp === "number" ? entry.timestamp : undefined,
@@ -269,6 +276,35 @@ function normalizeTranscript(value: unknown): TranscriptEntry[] {
269
276
  return transcript;
270
277
  }
271
278
 
279
+ function normalizeAgentState(value: unknown): AgentRecord["state"] {
280
+ switch (value) {
281
+ case "done":
282
+ case "completed":
283
+ return "done";
284
+ case "error":
285
+ case "failed":
286
+ return "error";
287
+ case "running":
288
+ case "uncertain":
289
+ return value;
290
+ default:
291
+ return "uncertain";
292
+ }
293
+ }
294
+
295
+ function normalizeWorkflowStatus(value: unknown): WorkflowDetails["status"] {
296
+ switch (value) {
297
+ case "running":
298
+ case "completed":
299
+ case "failed":
300
+ case "aborted":
301
+ case "uncertain":
302
+ return value;
303
+ default:
304
+ return "uncertain";
305
+ }
306
+ }
307
+
272
308
  /** Leniently normalize a workflow.json (including runs from older tooling). */
273
309
  export function normalizePersistedWorkflowDetails(
274
310
  runId: string,
@@ -284,14 +320,7 @@ export function normalizePersistedWorkflowDetails(
284
320
  for (const item of rawAgents) {
285
321
  if (!item || typeof item !== "object") continue;
286
322
  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";
323
+ const state = normalizeAgentState(a.state);
295
324
  const index = typeof a.index === "number" ? a.index : agents.length + 1;
296
325
  const decodedInvocation = decodeInvocationRecord(a.invocation);
297
326
  const invocation =
@@ -407,16 +436,23 @@ export function normalizePersistedWorkflowDetails(
407
436
  logs.push({
408
437
  at: typeof entry.at === "number" ? entry.at : startedAt,
409
438
  text: sanitizeLine(entry.text, MAX_LOG_TEXT),
439
+ ...(entry.kind === "pipeline-drop"
440
+ ? { kind: "pipeline-drop" as const }
441
+ : {}),
410
442
  });
411
443
  }
412
444
 
413
- const status =
414
- record.status === "running" ||
415
- record.status === "failed" ||
416
- record.status === "aborted" ||
417
- record.status === "uncertain"
418
- ? record.status
419
- : "completed";
445
+ let status = normalizeWorkflowStatus(record.status);
446
+ if (status !== "running") {
447
+ for (const agent of agents) {
448
+ if (agent.state !== "running" && agent.state !== "uncertain") continue;
449
+ status = "uncertain";
450
+ agent.state = "uncertain";
451
+ agent.error =
452
+ agent.error ??
453
+ "Persisted terminal workflow contained an agent without terminal evidence";
454
+ }
455
+ }
420
456
 
421
457
  return {
422
458
  runId,
@@ -541,22 +577,31 @@ export function loadRunEntries(
541
577
  referencedRunIds: ReadonlySet<string>,
542
578
  /** Hide runs untouched by the current request; live runs always show. */
543
579
  startedSince = 0,
580
+ /** Bounded settled projections used only if canonical disk state is unreadable. */
581
+ retained: ReadonlyMap<string, WorkflowDetails> = new Map(),
544
582
  ): RunEntry[] {
545
583
  const entries: RunEntry[] = [];
546
- for (const runId of listPersistedRunIds()) {
584
+ const runIds = new Set([...listPersistedRunIds(), ...retained.keys()]);
585
+ for (const runId of runIds) {
547
586
  const live = active.get(runId);
548
587
  if (live) {
549
588
  entries.push({ runId, details: live, live: true });
550
589
  continue;
551
590
  }
552
- const details = readPersistedWorkflowDetails(runId, {
591
+ const persisted = readPersistedWorkflowDetails(runId, {
553
592
  hydrateArtifacts: true,
554
593
  });
594
+ const retainedDetails = retained.get(runId);
595
+ const details = persisted ?? retainedDetails;
555
596
  if (!details) continue;
597
+ const fromRetention =
598
+ persisted === undefined && retainedDetails !== undefined;
556
599
  const touchedAt = Math.max(details.startedAt, details.finishedAt ?? 0);
557
600
  if (
558
601
  touchedAt < startedSince ||
559
- (details.sessionId !== sessionId && !referencedRunIds.has(runId))
602
+ (!fromRetention &&
603
+ details.sessionId !== sessionId &&
604
+ !referencedRunIds.has(runId))
560
605
  ) {
561
606
  continue;
562
607
  }
@@ -677,24 +722,24 @@ export class WorkflowDashboard {
677
722
  private phaseIndex = 0;
678
723
  private agentIndex = 0;
679
724
  private detailFocus: DetailFocus = "phases";
680
- private transcriptScroll = 0;
681
- private transcriptRowCount = 0;
682
- private transcriptViewportSize = 1;
725
+ private transcriptPage?: AgentSessionPage;
683
726
  private current?: RunEntry;
684
727
  private openedDirectly = false;
685
728
  private notice?: string;
686
729
  private noticeAt = 0;
687
730
  private disposed = false;
688
- private timer: ReturnType<typeof setInterval>;
731
+ private timer?: ReturnType<typeof setInterval>;
689
732
  private tui: TUI;
690
733
  private theme: Theme;
691
734
  private keybindings: KeybindingsManager;
692
735
  private getActive: () => Map<string, WorkflowDetails>;
736
+ private getRetained: () => ReadonlyMap<string, WorkflowDetails>;
693
737
  private sessionId: string;
694
738
  private referencedRunIds: ReadonlySet<string>;
695
739
  private startedSince: number;
696
740
  private close: () => void;
697
741
  private onAbort?: (runId: string) => boolean;
742
+ private initialToolsExpanded: boolean;
698
743
 
699
744
  constructor(
700
745
  tui: TUI,
@@ -707,16 +752,20 @@ export class WorkflowDashboard {
707
752
  close: () => void,
708
753
  initialRunId?: string,
709
754
  onAbort?: (runId: string) => boolean,
755
+ getRetained: () => ReadonlyMap<string, WorkflowDetails> = () => new Map(),
756
+ initialToolsExpanded = false,
710
757
  ) {
711
758
  this.tui = tui;
712
759
  this.theme = theme;
713
760
  this.keybindings = keybindings;
714
761
  this.getActive = getActive;
762
+ this.getRetained = getRetained;
715
763
  this.sessionId = sessionId;
716
764
  this.referencedRunIds = referencedRunIds;
717
765
  this.startedSince = startedSince;
718
766
  this.close = close;
719
767
  this.onAbort = onAbort;
768
+ this.initialToolsExpanded = initialToolsExpanded;
720
769
  this.refresh();
721
770
  if (initialRunId) {
722
771
  const resolution = resolveWorkflowRunTarget(
@@ -736,25 +785,44 @@ export class WorkflowDashboard {
736
785
  this.noticeAt = Date.now();
737
786
  }
738
787
  }
788
+ this.refreshTimer();
789
+ }
790
+
791
+ private refreshTimer() {
792
+ const active = Boolean(
793
+ this.notice ||
794
+ (this.view === "list"
795
+ ? this.entries.some(
796
+ (entry) => entry.live && entry.details.status === "running",
797
+ )
798
+ : this.view === "detail"
799
+ ? this.current?.live && this.current.details.status === "running"
800
+ : this.current?.live && this.selectedAgent()?.state === "running"),
801
+ );
802
+ if (!active) {
803
+ if (this.timer) clearInterval(this.timer);
804
+ this.timer = undefined;
805
+ return;
806
+ }
807
+ if (this.timer) return;
739
808
  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
- }
809
+ this.refresh();
810
+ this.tui.requestRender();
811
+ this.refreshTimer();
748
812
  }, SPINNER_INTERVAL_MS);
749
813
  }
750
814
 
751
815
  dispose() {
752
816
  if (this.disposed) return;
753
817
  this.disposed = true;
754
- clearInterval(this.timer);
818
+ if (this.timer) clearInterval(this.timer);
819
+ this.timer = undefined;
820
+ this.transcriptPage = undefined;
755
821
  }
756
822
 
757
- invalidate() {}
823
+ invalidate() {
824
+ this.transcriptPage?.invalidate();
825
+ }
758
826
 
759
827
  private refresh() {
760
828
  const selected = this.entries[this.listIndex]?.runId;
@@ -763,6 +831,7 @@ export class WorkflowDashboard {
763
831
  this.sessionId,
764
832
  this.referencedRunIds,
765
833
  this.startedSince,
834
+ this.getRetained(),
766
835
  );
767
836
  if (selected) {
768
837
  const index = this.entries.findIndex((e) => e.runId === selected);
@@ -819,11 +888,12 @@ export class WorkflowDashboard {
819
888
  const target = path.join(runsDir(), entry.runId, "report.md");
820
889
  try {
821
890
  writeFileAtomic(target, buildWorkflowReport(entry.details));
822
- this.notice = `saved ${shortenHome(target)}`;
891
+ this.setNotice(`saved ${shortenHome(target)}`);
823
892
  } catch (error) {
824
- this.notice = `save failed: ${error instanceof Error ? error.message : String(error)}`;
893
+ this.setNotice(
894
+ `save failed: ${error instanceof Error ? error.message : String(error)}`,
895
+ );
825
896
  }
826
- this.noticeAt = Date.now();
827
897
  }
828
898
 
829
899
  /** Request cancellation of a run by id, surfacing the outcome as a notice. */
@@ -842,6 +912,7 @@ export class WorkflowDashboard {
842
912
  private setNotice(text: string) {
843
913
  this.notice = text;
844
914
  this.noticeAt = Date.now();
915
+ this.refreshTimer();
845
916
  }
846
917
 
847
918
  handleInput(data: string) {
@@ -923,57 +994,27 @@ export class WorkflowDashboard {
923
994
  } else if (left || cancel) {
924
995
  this.detailFocus = "phases";
925
996
  } else if ((right || confirm) && this.selectedAgent()) {
926
- this.transcriptScroll = 0;
927
- this.view = "transcript";
997
+ this.openTranscriptPage();
928
998
  }
929
999
  }
930
1000
  if (data === "s") this.saveReport();
931
1001
  if (data === "x") this.abortRun(this.current);
932
1002
  } 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
- }
1003
+ this.transcriptPage?.handleInput(data);
1004
+ this.refreshTimer();
1005
+ return;
962
1006
  }
1007
+ this.refreshTimer();
963
1008
  this.tui.requestRender();
964
1009
  }
965
1010
 
966
1011
  render(width: number): string[] {
1012
+ if (this.view === "transcript" && this.transcriptPage) {
1013
+ return this.transcriptPage.render(width);
1014
+ }
967
1015
  const height = Math.max(MIN_HEIGHT, this.tui.terminal.rows - 1);
968
1016
  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) {
1017
+ if (this.view === "detail" && this.current) {
977
1018
  lines = this.renderDetail(this.current.details, width, height);
978
1019
  } else {
979
1020
  lines = this.renderList(width, height);
@@ -981,6 +1022,53 @@ export class WorkflowDashboard {
981
1022
  return lines.map((line) => truncateToWidth(line, width, ""));
982
1023
  }
983
1024
 
1025
+ private openTranscriptPage() {
1026
+ const transcriptAdapter = new WorkflowTranscriptAdapter();
1027
+ this.view = "transcript";
1028
+ this.transcriptPage = new AgentSessionPage(
1029
+ this.tui,
1030
+ this.theme,
1031
+ this.keybindings,
1032
+ {
1033
+ getState: () => {
1034
+ const details = this.current?.details;
1035
+ const agent = this.selectedAgent();
1036
+ if (!details || !agent) return undefined;
1037
+ return {
1038
+ id: agent.callId ?? `agent-${agent.index}`,
1039
+ title: agent.label,
1040
+ status: agent.state,
1041
+ document: transcriptAdapter.document(
1042
+ agent.transcript,
1043
+ agent.worktreePath,
1044
+ ),
1045
+ metadata: [
1046
+ `${details.name ?? details.runId} · ${agent.phase ?? "unphased"}`,
1047
+ agent.model,
1048
+ agentContext(agent),
1049
+ agent.acceptance
1050
+ ? `acceptance:${agent.acceptance.status}`
1051
+ : undefined,
1052
+ formatElapsed(agent.startedAt, agent.finishedAt),
1053
+ ],
1054
+ errorText: agent.error,
1055
+ emptyText:
1056
+ "transcript unavailable (this run predates transcript capture)",
1057
+ };
1058
+ },
1059
+ close: () => {
1060
+ this.transcriptPage = undefined;
1061
+ this.view = "detail";
1062
+ this.detailFocus = "agents";
1063
+ this.refreshTimer();
1064
+ this.tui.requestRender();
1065
+ },
1066
+ },
1067
+ { toolsExpanded: this.initialToolsExpanded },
1068
+ );
1069
+ this.tui.requestRender();
1070
+ }
1071
+
984
1072
  /** Bordered panel with a title in the top border, padded to exact height. */
985
1073
  private panel(
986
1074
  title: string,
@@ -1313,117 +1401,6 @@ export class WorkflowDashboard {
1313
1401
  lines.push(this.hintLine(hints, width));
1314
1402
  return lines;
1315
1403
  }
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
1404
  }
1428
1405
 
1429
1406
  /**
@@ -1438,39 +1415,6 @@ function displayError(error: string) {
1438
1415
  return clean;
1439
1416
  }
1440
1417
 
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
1418
  function groupGlyph(group: PhaseGroup, theme: Theme) {
1475
1419
  if (group.agents.length === 0) return theme.fg("dim", "○");
1476
1420
  if (group.agents.some((a) => a.state === "running"))
@@ -1489,6 +1433,7 @@ export async function showWorkflowDashboard(
1489
1433
  initialRunId?: string,
1490
1434
  startedSince = 0,
1491
1435
  onAbort?: (runId: string) => boolean,
1436
+ getRetained?: () => ReadonlyMap<string, WorkflowDetails>,
1492
1437
  ) {
1493
1438
  await ctx.ui.custom<void>(
1494
1439
  (tui, theme, keybindings, done) => {
@@ -1506,12 +1451,14 @@ export async function showWorkflowDashboard(
1506
1451
  },
1507
1452
  initialRunId,
1508
1453
  onAbort,
1454
+ getRetained,
1455
+ ctx.ui.getToolsExpanded(),
1509
1456
  );
1510
1457
  return dashboard;
1511
1458
  },
1512
1459
  {
1513
1460
  overlay: true,
1514
- overlayOptions: { anchor: "center", width: "100%", maxHeight: "100%" },
1461
+ overlayOptions: { anchor: "top-left", width: "100%", maxHeight: "100%" },
1515
1462
  },
1516
1463
  );
1517
1464
  }