gsd-pi 2.24.0 → 2.26.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 (206) hide show
  1. package/README.md +13 -3
  2. package/dist/headless.js +24 -4
  3. package/dist/models-resolver.d.ts +0 -11
  4. package/dist/models-resolver.js +0 -15
  5. package/dist/resource-loader.d.ts +0 -1
  6. package/dist/resource-loader.js +0 -9
  7. package/dist/resources/GSD-WORKFLOW.md +12 -9
  8. package/dist/resources/extensions/async-jobs/index.ts +9 -1
  9. package/dist/resources/extensions/bg-shell/index.ts +3 -2
  10. package/dist/resources/extensions/bg-shell/overlay.ts +18 -17
  11. package/dist/resources/extensions/get-secrets-from-user.ts +5 -23
  12. package/dist/resources/extensions/gsd/activity-log.ts +5 -3
  13. package/dist/resources/extensions/gsd/auto-prompts.ts +14 -0
  14. package/dist/resources/extensions/gsd/auto-recovery.ts +7 -4
  15. package/dist/resources/extensions/gsd/auto-worktree.ts +132 -3
  16. package/dist/resources/extensions/gsd/auto.ts +265 -48
  17. package/dist/resources/extensions/gsd/cache.ts +3 -1
  18. package/dist/resources/extensions/gsd/doctor-proactive.ts +7 -6
  19. package/dist/resources/extensions/gsd/doctor.ts +26 -1
  20. package/dist/resources/extensions/gsd/files.ts +13 -2
  21. package/dist/resources/extensions/gsd/git-service.ts +74 -14
  22. package/dist/resources/extensions/gsd/gsd-db.ts +78 -1
  23. package/dist/resources/extensions/gsd/guided-flow.ts +54 -22
  24. package/dist/resources/extensions/gsd/index.ts +62 -8
  25. package/dist/resources/extensions/gsd/memory-extractor.ts +352 -0
  26. package/dist/resources/extensions/gsd/memory-store.ts +441 -0
  27. package/dist/resources/extensions/gsd/migrate/command.ts +2 -2
  28. package/dist/resources/extensions/gsd/migrate/writer.ts +39 -0
  29. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +122 -4
  30. package/dist/resources/extensions/gsd/preferences.ts +2 -1
  31. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  32. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +4 -4
  33. package/dist/resources/extensions/gsd/prompts/discuss.md +5 -5
  34. package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
  35. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  36. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  37. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  38. package/dist/resources/extensions/gsd/prompts/queue.md +3 -3
  39. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  40. package/dist/resources/extensions/gsd/roadmap-slices.ts +45 -1
  41. package/dist/resources/extensions/gsd/state.ts +17 -6
  42. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
  43. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  44. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +70 -0
  45. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +23 -3
  46. package/dist/resources/extensions/gsd/tests/git-service.test.ts +70 -4
  47. package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
  48. package/dist/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
  49. package/dist/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
  50. package/dist/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
  51. package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +13 -7
  52. package/dist/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +171 -0
  53. package/dist/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
  54. package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +8 -4
  55. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
  56. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
  57. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
  58. package/dist/resources/extensions/gsd/triage-ui.ts +1 -1
  59. package/dist/resources/extensions/gsd/types.ts +2 -0
  60. package/dist/resources/extensions/gsd/visualizer-data.ts +291 -10
  61. package/dist/resources/extensions/gsd/visualizer-overlay.ts +237 -28
  62. package/dist/resources/extensions/gsd/visualizer-views.ts +462 -48
  63. package/dist/resources/extensions/gsd/worktree.ts +9 -2
  64. package/dist/resources/extensions/search-the-web/native-search.ts +19 -5
  65. package/dist/resources/extensions/shared/path-display.ts +19 -0
  66. package/package.json +1 -6
  67. package/packages/pi-agent-core/dist/agent-loop.js +2 -0
  68. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  69. package/packages/pi-agent-core/src/agent-loop.ts +2 -0
  70. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  71. package/packages/pi-ai/dist/providers/anthropic.js +64 -0
  72. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  73. package/packages/pi-ai/dist/providers/mistral.js +3 -0
  74. package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
  75. package/packages/pi-ai/dist/types.d.ts +23 -1
  76. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  77. package/packages/pi-ai/dist/types.js.map +1 -1
  78. package/packages/pi-ai/src/providers/anthropic.ts +65 -1
  79. package/packages/pi-ai/src/providers/mistral.ts +3 -0
  80. package/packages/pi-ai/src/types.ts +19 -1
  81. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +7 -0
  82. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  83. package/packages/pi-coding-agent/dist/core/agent-session.js +32 -0
  84. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  85. package/packages/pi-coding-agent/dist/core/keybindings.js +1 -1
  86. package/packages/pi-coding-agent/dist/core/keybindings.js.map +1 -1
  87. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  88. package/packages/pi-coding-agent/dist/core/lsp/client.js +12 -1
  89. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  90. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
  91. package/packages/pi-coding-agent/dist/core/lsp/index.js +7 -0
  92. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/sdk.d.ts +2 -2
  94. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  95. package/packages/pi-coding-agent/dist/core/sdk.js +8 -3
  96. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  98. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/settings-manager.js +8 -0
  100. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  102. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  103. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  104. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/core/system-prompt.js +2 -1
  106. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/index.d.ts +2 -1
  108. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  109. package/packages/pi-coding-agent/dist/index.js +5 -1
  110. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  111. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts +41 -3
  112. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  113. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +301 -62
  114. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  115. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -0
  117. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +5 -0
  119. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +135 -30
  121. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/tests/path-display.test.d.ts +8 -0
  123. package/packages/pi-coding-agent/dist/tests/path-display.test.d.ts.map +1 -0
  124. package/packages/pi-coding-agent/dist/tests/path-display.test.js +60 -0
  125. package/packages/pi-coding-agent/dist/tests/path-display.test.js.map +1 -0
  126. package/packages/pi-coding-agent/dist/utils/clipboard-image.d.ts.map +1 -1
  127. package/packages/pi-coding-agent/dist/utils/clipboard-image.js +32 -6
  128. package/packages/pi-coding-agent/dist/utils/clipboard-image.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/utils/path-display.d.ts +34 -0
  130. package/packages/pi-coding-agent/dist/utils/path-display.d.ts.map +1 -0
  131. package/packages/pi-coding-agent/dist/utils/path-display.js +36 -0
  132. package/packages/pi-coding-agent/dist/utils/path-display.js.map +1 -0
  133. package/packages/pi-coding-agent/src/core/agent-session.ts +36 -0
  134. package/packages/pi-coding-agent/src/core/keybindings.ts +1 -1
  135. package/packages/pi-coding-agent/src/core/lsp/client.ts +11 -1
  136. package/packages/pi-coding-agent/src/core/lsp/index.ts +7 -0
  137. package/packages/pi-coding-agent/src/core/sdk.ts +17 -1
  138. package/packages/pi-coding-agent/src/core/settings-manager.ts +11 -0
  139. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  140. package/packages/pi-coding-agent/src/core/system-prompt.ts +2 -1
  141. package/packages/pi-coding-agent/src/index.ts +15 -0
  142. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +347 -62
  143. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -0
  144. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +124 -4
  145. package/packages/pi-coding-agent/src/tests/path-display.test.ts +85 -0
  146. package/packages/pi-coding-agent/src/utils/clipboard-image.ts +33 -6
  147. package/packages/pi-coding-agent/src/utils/path-display.ts +36 -0
  148. package/src/resources/GSD-WORKFLOW.md +12 -9
  149. package/src/resources/extensions/async-jobs/index.ts +9 -1
  150. package/src/resources/extensions/bg-shell/index.ts +3 -2
  151. package/src/resources/extensions/bg-shell/overlay.ts +18 -17
  152. package/src/resources/extensions/get-secrets-from-user.ts +5 -23
  153. package/src/resources/extensions/gsd/activity-log.ts +5 -3
  154. package/src/resources/extensions/gsd/auto-prompts.ts +14 -0
  155. package/src/resources/extensions/gsd/auto-recovery.ts +7 -4
  156. package/src/resources/extensions/gsd/auto-worktree.ts +132 -3
  157. package/src/resources/extensions/gsd/auto.ts +265 -48
  158. package/src/resources/extensions/gsd/cache.ts +3 -1
  159. package/src/resources/extensions/gsd/doctor-proactive.ts +7 -6
  160. package/src/resources/extensions/gsd/doctor.ts +26 -1
  161. package/src/resources/extensions/gsd/files.ts +13 -2
  162. package/src/resources/extensions/gsd/git-service.ts +74 -14
  163. package/src/resources/extensions/gsd/gsd-db.ts +78 -1
  164. package/src/resources/extensions/gsd/guided-flow.ts +54 -22
  165. package/src/resources/extensions/gsd/index.ts +62 -8
  166. package/src/resources/extensions/gsd/memory-extractor.ts +352 -0
  167. package/src/resources/extensions/gsd/memory-store.ts +441 -0
  168. package/src/resources/extensions/gsd/migrate/command.ts +2 -2
  169. package/src/resources/extensions/gsd/migrate/writer.ts +39 -0
  170. package/src/resources/extensions/gsd/parallel-orchestrator.ts +122 -4
  171. package/src/resources/extensions/gsd/preferences.ts +2 -1
  172. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  173. package/src/resources/extensions/gsd/prompts/discuss-headless.md +4 -4
  174. package/src/resources/extensions/gsd/prompts/discuss.md +5 -5
  175. package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
  176. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  177. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  178. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  179. package/src/resources/extensions/gsd/prompts/queue.md +3 -3
  180. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  181. package/src/resources/extensions/gsd/roadmap-slices.ts +45 -1
  182. package/src/resources/extensions/gsd/state.ts +17 -6
  183. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
  184. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  185. package/src/resources/extensions/gsd/tests/derive-state.test.ts +70 -0
  186. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +23 -3
  187. package/src/resources/extensions/gsd/tests/git-service.test.ts +70 -4
  188. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
  189. package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
  190. package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
  191. package/src/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
  192. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +13 -7
  193. package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +171 -0
  194. package/src/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
  195. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +8 -4
  196. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
  197. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
  198. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
  199. package/src/resources/extensions/gsd/triage-ui.ts +1 -1
  200. package/src/resources/extensions/gsd/types.ts +2 -0
  201. package/src/resources/extensions/gsd/visualizer-data.ts +291 -10
  202. package/src/resources/extensions/gsd/visualizer-overlay.ts +237 -28
  203. package/src/resources/extensions/gsd/visualizer-views.ts +462 -48
  204. package/src/resources/extensions/gsd/worktree.ts +9 -2
  205. package/src/resources/extensions/search-the-web/native-search.ts +19 -5
  206. package/src/resources/extensions/shared/path-display.ts +19 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  import type { Theme } from "@gsd/pi-coding-agent";
4
4
  import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
5
- import type { VisualizerData, VisualizerMilestone } from "./visualizer-data.js";
5
+ import type { VisualizerData, VisualizerMilestone, SliceVerification, VisualizerSliceActivity, VisualizerStats, VisualizerSliceRef } from "./visualizer-data.js";
6
6
  import { formatCost, formatTokenCount, classifyUnitPhase } from "./metrics.js";
7
7
 
8
8
  // ─── Local Helpers ───────────────────────────────────────────────────────────
@@ -34,12 +34,103 @@ function joinColumns(left: string, right: string, width: number): string {
34
34
 
35
35
  function sparkline(values: number[]): string {
36
36
  if (values.length === 0) return "";
37
- const chars = "▁▂▃▄▅▆▇█";
37
+ const chars = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
38
38
  const max = Math.max(...values);
39
39
  if (max === 0) return chars[0].repeat(values.length);
40
40
  return values.map(v => chars[Math.min(7, Math.floor((v / max) * 7))]).join("");
41
41
  }
42
42
 
43
+ function formatCompletionDate(input: string): string {
44
+ if (!input) return "unknown";
45
+ const parsed = new Date(input);
46
+ if (Number.isNaN(parsed.getTime())) return input;
47
+ return parsed.toLocaleDateString("en-US", { month: "short", day: "numeric" });
48
+ }
49
+
50
+ function sliceLabel(slice: VisualizerSliceRef): string {
51
+ return `${slice.milestoneId}/${slice.sliceId}`;
52
+ }
53
+
54
+ function renderFeatureStats(data: VisualizerData, th: Theme, width: number): string[] {
55
+ const stats = data.stats;
56
+ const lines: string[] = [];
57
+ lines.push(th.fg("accent", th.bold("Feature Snapshot")));
58
+ lines.push("");
59
+
60
+ const missingLabel = `Missing slices: ${th.fg("warning", String(stats.missingCount))}`;
61
+ lines.push(truncateToWidth(` ${missingLabel}`, width));
62
+ if (stats.missingSlices.length > 0) {
63
+ for (const slice of stats.missingSlices) {
64
+ const row = ` ${th.fg("dim", sliceLabel(slice))} ${slice.title}`;
65
+ lines.push(truncateToWidth(row, width));
66
+ }
67
+ const remaining = stats.missingCount - stats.missingSlices.length;
68
+ if (remaining > 0) {
69
+ lines.push(truncateToWidth(` ... and ${remaining} more`, width));
70
+ }
71
+ }
72
+
73
+ lines.push("");
74
+ const updatedLabel = `Updated (last 7 days): ${th.fg("accent", String(stats.updatedCount))}`;
75
+ lines.push(truncateToWidth(` ${updatedLabel}`, width));
76
+ if (stats.updatedSlices.length > 0) {
77
+ for (const slice of stats.updatedSlices) {
78
+ const when = formatCompletionDate(slice.completedAt);
79
+ const row = ` ${th.fg("text", sliceLabel(slice))} ${th.fg("dim", when)} ${slice.title}`;
80
+ lines.push(truncateToWidth(row, width));
81
+ }
82
+ }
83
+
84
+ lines.push("");
85
+ lines.push(truncateToWidth(` Recent completions: ${th.fg("success", String(stats.recentEntries.length))}`, width));
86
+ for (const entry of stats.recentEntries) {
87
+ const when = formatCompletionDate(entry.completedAt);
88
+ const row = ` ${th.fg("text", entry.sliceId)} — ${entry.oneLiner || entry.title} ${th.fg("dim", when)}`;
89
+ lines.push(truncateToWidth(row, width));
90
+ }
91
+
92
+ lines.push("");
93
+ return lines;
94
+ }
95
+
96
+ function renderDiscussionStatus(data: VisualizerData, th: Theme, width: number): string[] {
97
+ const states = data.discussion;
98
+ if (states.length === 0) return [];
99
+
100
+ const counts = {
101
+ discussed: 0,
102
+ draft: 0,
103
+ undiscussed: 0,
104
+ };
105
+ for (const state of states) counts[state.state]++;
106
+
107
+ const lines: string[] = [];
108
+ lines.push(th.fg("accent", th.bold("Discussion Status")));
109
+ lines.push("");
110
+ const summary = ` Discussed: ${th.fg("success", String(counts.discussed))} Draft: ${th.fg("warning", String(counts.draft))} Pending: ${th.fg("dim", String(counts.undiscussed))}`;
111
+ lines.push(truncateToWidth(summary, width));
112
+ lines.push("");
113
+
114
+ for (const state of states) {
115
+ const badge =
116
+ state.state === "discussed"
117
+ ? th.fg("success", "Discussed")
118
+ : state.state === "draft"
119
+ ? th.fg("warning", "Draft")
120
+ : th.fg("dim", "Pending");
121
+ const when = state.lastUpdated ? ` ${th.fg("dim", formatCompletionDate(state.lastUpdated))}` : "";
122
+ const row = ` ${th.fg("text", state.milestoneId)} ${badge} ${state.title}${when}`;
123
+ lines.push(truncateToWidth(row, width));
124
+ }
125
+
126
+ lines.push("");
127
+ return lines;
128
+ }
129
+
130
+ function findVerification(data: VisualizerData, milestoneId: string, sliceId: string): SliceVerification | undefined {
131
+ return data.sliceVerifications.find(v => v.milestoneId === milestoneId && v.sliceId === sliceId);
132
+ }
133
+
43
134
  // ─── Progress View ───────────────────────────────────────────────────────────
44
135
 
45
136
  export interface ProgressFilter {
@@ -52,6 +143,7 @@ export function renderProgressView(
52
143
  th: Theme,
53
144
  width: number,
54
145
  filter?: ProgressFilter,
146
+ collapsed?: Set<string>,
55
147
  ): string[] {
56
148
  const lines: string[] = [];
57
149
 
@@ -65,6 +157,9 @@ export function renderProgressView(
65
157
  lines.push("");
66
158
  }
67
159
 
160
+ lines.push(...renderFeatureStats(data, th, width));
161
+ lines.push(...renderDiscussionStatus(data, th, width));
162
+
68
163
  for (const ms of data.milestones) {
69
164
  // Apply filter to milestones
70
165
  if (filter && filter.text) {
@@ -75,20 +170,25 @@ export function renderProgressView(
75
170
  // Milestone header line
76
171
  const statusGlyph =
77
172
  ms.status === "complete"
78
- ? th.fg("success", "")
173
+ ? th.fg("success", "\u2713")
79
174
  : ms.status === "active"
80
- ? th.fg("accent", "")
81
- : th.fg("dim", "");
175
+ ? th.fg("accent", "\u25b8")
176
+ : th.fg("dim", "\u25cb");
82
177
  const statusLabel =
83
178
  ms.status === "complete"
84
179
  ? th.fg("success", "complete")
85
180
  : ms.status === "active"
86
181
  ? th.fg("accent", "active")
87
182
  : th.fg("dim", "pending");
88
- const msLeft = `${ms.id}: ${ms.title}`;
183
+
184
+ const collapseIndicator = collapsed?.has(ms.id) ? "[+] " : "";
185
+ const msLeft = `${collapseIndicator}${ms.id}: ${ms.title}`;
89
186
  const msRight = `${statusGlyph} ${statusLabel}`;
90
187
  lines.push(joinColumns(msLeft, msRight, width));
91
188
 
189
+ // If collapsed, skip rendering slices/tasks
190
+ if (collapsed?.has(ms.id)) continue;
191
+
92
192
  if (ms.slices.length === 0 && ms.dependsOn.length > 0) {
93
193
  lines.push(th.fg("dim", ` (depends on ${ms.dependsOn.join(", ")})`));
94
194
  continue;
@@ -107,10 +207,10 @@ export function renderProgressView(
107
207
 
108
208
  // Slice line
109
209
  const slGlyph = sl.done
110
- ? th.fg("success", "")
210
+ ? th.fg("success", "\u2713")
111
211
  : sl.active
112
- ? th.fg("accent", "")
113
- : th.fg("dim", "");
212
+ ? th.fg("accent", "\u25b8")
213
+ : th.fg("dim", "\u25cb");
114
214
  const riskColor =
115
215
  sl.risk === "high"
116
216
  ? "warning"
@@ -118,18 +218,36 @@ export function renderProgressView(
118
218
  ? "text"
119
219
  : "dim";
120
220
  const riskBadge = th.fg(riskColor, sl.risk);
121
- const slLeft = ` ${slGlyph} ${sl.id}: ${sl.title}`;
221
+
222
+ // Verification badge
223
+ const ver = findVerification(data, ms.id, sl.id);
224
+ let verBadge = "";
225
+ if (ver) {
226
+ if (ver.verificationResult === "passed") {
227
+ verBadge = " " + th.fg("success", "\u2713");
228
+ } else if (ver.verificationResult === "failed") {
229
+ verBadge = " " + th.fg("error", "\u2717");
230
+ } else if (ver.verificationResult === "untested" || ver.verificationResult === "") {
231
+ verBadge = " " + th.fg("dim", "?");
232
+ }
233
+ if (ver.blockerDiscovered) {
234
+ verBadge += " " + th.fg("warning", "\u26a0");
235
+ }
236
+ }
237
+
238
+ const slLeft = ` ${slGlyph} ${sl.id}: ${sl.title}${verBadge}`;
122
239
  lines.push(joinColumns(slLeft, riskBadge, width));
123
240
 
124
241
  // Show tasks for active slice
125
242
  if (sl.active && sl.tasks.length > 0) {
126
243
  for (const task of sl.tasks) {
127
244
  const tGlyph = task.done
128
- ? th.fg("success", "")
245
+ ? th.fg("success", "\u2713")
129
246
  : task.active
130
- ? th.fg("accent", "")
131
- : th.fg("dim", "");
132
- lines.push(` ${tGlyph} ${task.id}: ${task.title}`);
247
+ ? th.fg("accent", "\u25b8")
248
+ : th.fg("dim", "\u25cb");
249
+ const estimateStr = task.estimate ? th.fg("dim", ` (${task.estimate})`) : "";
250
+ lines.push(` ${tGlyph} ${task.id}: ${task.title}${estimateStr}`);
133
251
  }
134
252
  }
135
253
  }
@@ -176,7 +294,7 @@ function renderRiskHeatmap(data: VisualizerData, th: Theme, width: number): stri
176
294
  if (ms.slices.length === 0) continue;
177
295
  const blocks = ms.slices.map(s => {
178
296
  const color = s.risk === "high" ? "error" : s.risk === "medium" ? "warning" : "success";
179
- return th.fg(color, "██");
297
+ return th.fg(color, "\u2588\u2588");
180
298
  });
181
299
  const row = ` ${padRight(ms.id, 6)} ${blocks.join(" ")}`;
182
300
  lines.push(truncateToWidth(row, width));
@@ -184,7 +302,7 @@ function renderRiskHeatmap(data: VisualizerData, th: Theme, width: number): stri
184
302
 
185
303
  lines.push("");
186
304
  lines.push(
187
- ` ${th.fg("success", "██")} low ${th.fg("warning", "██")} med ${th.fg("error", "██")} high`,
305
+ ` ${th.fg("success", "\u2588\u2588")} low ${th.fg("warning", "\u2588\u2588")} med ${th.fg("error", "\u2588\u2588")} high`,
188
306
  );
189
307
 
190
308
  // Summary counts
@@ -230,7 +348,7 @@ export function renderDepsView(
230
348
  for (const ms of msDeps) {
231
349
  for (const dep of ms.dependsOn) {
232
350
  lines.push(
233
- ` ${th.fg("text", dep)} ${th.fg("accent", "──►")} ${th.fg("text", ms.id)}`,
351
+ ` ${th.fg("text", dep)} ${th.fg("accent", "\u2500\u2500\u25ba")} ${th.fg("text", ms.id)}`,
234
352
  );
235
353
  }
236
354
  }
@@ -253,7 +371,7 @@ export function renderDepsView(
253
371
  for (const sl of slDeps) {
254
372
  for (const dep of sl.depends) {
255
373
  lines.push(
256
- ` ${th.fg("text", dep)} ${th.fg("accent", "──►")} ${th.fg("text", sl.id)}`,
374
+ ` ${th.fg("text", dep)} ${th.fg("accent", "\u2500\u2500\u25ba")} ${th.fg("text", sl.id)}`,
257
375
  );
258
376
  }
259
377
  }
@@ -265,6 +383,37 @@ export function renderDepsView(
265
383
  // Critical Path section
266
384
  lines.push(...renderCriticalPath(data, th, width));
267
385
 
386
+ // Data Flow section from slice verifications
387
+ lines.push("");
388
+ lines.push(...renderDataFlow(data, th));
389
+
390
+ return lines;
391
+ }
392
+
393
+ // ─── Data Flow ───────────────────────────────────────────────────────────────
394
+
395
+ function renderDataFlow(data: VisualizerData, th: Theme): string[] {
396
+ const lines: string[] = [];
397
+ const versWithProvides = data.sliceVerifications.filter(v => v.provides.length > 0);
398
+ const versWithRequires = data.sliceVerifications.filter(v => v.requires.length > 0);
399
+
400
+ if (versWithProvides.length === 0 && versWithRequires.length === 0) return lines;
401
+
402
+ lines.push(th.fg("accent", th.bold("Data Flow")));
403
+ lines.push("");
404
+
405
+ for (const v of versWithProvides) {
406
+ for (const artifact of v.provides) {
407
+ lines.push(` ${th.fg("text", v.sliceId)} ${th.fg("accent", "\u2500\u2500\u25ba")} ${th.fg("dim", `[${artifact}]`)}`);
408
+ }
409
+ }
410
+
411
+ for (const v of versWithRequires) {
412
+ for (const req of v.requires) {
413
+ lines.push(` ${th.fg("dim", `[${req.provides}]`)} ${th.fg("accent", "\u25c4\u2500\u2500")} ${th.fg("text", req.slice)}`);
414
+ }
415
+ }
416
+
268
417
  return lines;
269
418
  }
270
419
 
@@ -284,10 +433,9 @@ function renderCriticalPath(data: VisualizerData, th: Theme, _width: number): st
284
433
 
285
434
  // Milestone chain
286
435
  const chain = cp.milestonePath.map(id => {
287
- const ms = data.milestones.find(m => m.id === id);
288
436
  const badge = th.fg("error", "[CRITICAL]");
289
437
  return `${id} ${badge}`;
290
- }).join(` ${th.fg("accent", "──►")} `);
438
+ }).join(` ${th.fg("accent", "\u2500\u2500\u25ba")} `);
291
439
  lines.push(` ${chain}`);
292
440
  lines.push("");
293
441
 
@@ -304,7 +452,7 @@ function renderCriticalPath(data: VisualizerData, th: Theme, _width: number): st
304
452
  lines.push(th.fg("accent", th.bold("Slice Critical Path")));
305
453
  lines.push("");
306
454
 
307
- const sliceChain = cp.slicePath.join(` ${th.fg("accent", "──►")} `);
455
+ const sliceChain = cp.slicePath.join(` ${th.fg("accent", "\u2500\u2500\u25ba")} `);
308
456
  lines.push(` ${sliceChain}`);
309
457
 
310
458
  // Bottleneck warnings
@@ -313,7 +461,7 @@ function renderCriticalPath(data: VisualizerData, th: Theme, _width: number): st
313
461
  for (const sid of cp.slicePath) {
314
462
  const sl = activeMs.slices.find(s => s.id === sid);
315
463
  if (sl && !sl.done && !sl.active) {
316
- lines.push(th.fg("warning", ` ${sid}: critical but not yet started`));
464
+ lines.push(th.fg("warning", ` \u26a0 ${sid}: critical but not yet started`));
317
465
  }
318
466
  }
319
467
  }
@@ -347,6 +495,10 @@ export function renderMetricsView(
347
495
  `Tokens: ${th.fg("text", formatTokenCount(totals.tokens.total))} ` +
348
496
  `Units: ${th.fg("text", String(totals.units))}`,
349
497
  );
498
+ lines.push(
499
+ ` Tools: ${th.fg("text", String(totals.toolCalls))} ` +
500
+ `Messages: ${th.fg("text", String(totals.assistantMessages))} sent / ${th.fg("text", String(totals.userMessages))} received`,
501
+ );
350
502
  lines.push("");
351
503
 
352
504
  const barWidth = Math.max(10, width - 40);
@@ -365,8 +517,8 @@ export function renderMetricsView(
365
517
  ? Math.round((phase.cost / maxPhaseCost) * barWidth)
366
518
  : 0;
367
519
  const bar =
368
- th.fg("accent", "".repeat(fillLen)) +
369
- th.fg("dim", "".repeat(barWidth - fillLen));
520
+ th.fg("accent", "\u2588".repeat(fillLen)) +
521
+ th.fg("dim", "\u2591".repeat(barWidth - fillLen));
370
522
  const label = padRight(phase.phase, 14);
371
523
  const costStr = formatCost(phase.cost);
372
524
  const pctStr = `${pct.toFixed(1)}%`;
@@ -391,8 +543,8 @@ export function renderMetricsView(
391
543
  ? Math.round((model.cost / maxModelCost) * barWidth)
392
544
  : 0;
393
545
  const bar =
394
- th.fg("accent", "".repeat(fillLen)) +
395
- th.fg("dim", "".repeat(barWidth - fillLen));
546
+ th.fg("accent", "\u2588".repeat(fillLen)) +
547
+ th.fg("dim", "\u2591".repeat(barWidth - fillLen));
396
548
  const label = padRight(model.model, 20);
397
549
  const costStr = formatCost(model.cost);
398
550
  const pctStr = `${pct.toFixed(1)}%`;
@@ -402,6 +554,36 @@ export function renderMetricsView(
402
554
  lines.push("");
403
555
  }
404
556
 
557
+ // By Tier
558
+ if (data.byTier.length > 0) {
559
+ lines.push(th.fg("accent", th.bold("By Tier")));
560
+ lines.push("");
561
+
562
+ const maxTierCost = Math.max(...data.byTier.map((t) => t.cost));
563
+
564
+ for (const tier of data.byTier) {
565
+ const pct = totals.cost > 0 ? (tier.cost / totals.cost) * 100 : 0;
566
+ const fillLen =
567
+ maxTierCost > 0
568
+ ? Math.round((tier.cost / maxTierCost) * barWidth)
569
+ : 0;
570
+ const bar =
571
+ th.fg("accent", "\u2588".repeat(fillLen)) +
572
+ th.fg("dim", "\u2591".repeat(barWidth - fillLen));
573
+ const label = padRight(tier.tier, 12);
574
+ const costStr = formatCost(tier.cost);
575
+ const pctStr = `${pct.toFixed(1)}%`;
576
+ const unitsStr = `${tier.units} units`;
577
+ lines.push(` ${label} ${bar} ${costStr} ${pctStr} ${unitsStr}`);
578
+ }
579
+
580
+ if (data.tierSavingsLine) {
581
+ lines.push(` ${th.fg("success", data.tierSavingsLine)}`);
582
+ }
583
+
584
+ lines.push("");
585
+ }
586
+
405
587
  // Cost Projections
406
588
  lines.push(...renderCostProjections(data, th, width));
407
589
 
@@ -432,7 +614,7 @@ function renderCostProjections(data: VisualizerData, th: Theme, _width: number):
432
614
  lines.push(` Avg cost/slice: ${th.fg("text", formatCost(avgCostPerSlice))}`);
433
615
  lines.push(
434
616
  ` Projected remaining: ${th.fg("text", formatCost(projectedRemaining))} ` +
435
- `(${formatCost(avgCostPerSlice)}/slice × ${data.remainingSliceCount} remaining)`,
617
+ `(${formatCost(avgCostPerSlice)}/slice \u00d7 ${data.remainingSliceCount} remaining)`,
436
618
  );
437
619
 
438
620
  // Burn rate
@@ -448,10 +630,10 @@ function renderCostProjections(data: VisualizerData, th: Theme, _width: number):
448
630
  lines.push(` Cost trend: ${spark}`);
449
631
  }
450
632
 
451
- // Budget warning: projected total > current spend
633
+ // Budget warning: projected total > 2x current spend
452
634
  const projectedTotal = data.totals.cost + projectedRemaining;
453
635
  if (projectedTotal > 2 * data.totals.cost && data.remainingSliceCount > 0) {
454
- lines.push(th.fg("warning", ` Projected total ${formatCost(projectedTotal)} exceeds 2× current spend`));
636
+ lines.push(th.fg("warning", ` \u26a0 Projected total ${formatCost(projectedTotal)} exceeds 2\u00d7 current spend`));
455
637
  }
456
638
 
457
639
  return lines;
@@ -479,6 +661,10 @@ export function renderTimelineView(
479
661
  return renderTimelineList(data, th, width);
480
662
  }
481
663
 
664
+ function shortenModel(model: string): string {
665
+ return model.replace(/^claude-/, "").slice(0, 12);
666
+ }
667
+
482
668
  function renderTimelineList(data: VisualizerData, th: Theme, width: number): string[] {
483
669
  const lines: string[] = [];
484
670
 
@@ -499,8 +685,8 @@ function renderTimelineList(data: VisualizerData, th: Theme, width: number): str
499
685
  const duration = unit.finishedAt - unit.startedAt;
500
686
  const glyph =
501
687
  unit.finishedAt > 0
502
- ? th.fg("success", "")
503
- : th.fg("accent", "");
688
+ ? th.fg("success", "\u2713")
689
+ : th.fg("accent", "\u25b8");
504
690
 
505
691
  const typeLabel = padRight(unit.type, 16);
506
692
  const idLabel = padRight(unit.id, 14);
@@ -510,13 +696,18 @@ function renderTimelineList(data: VisualizerData, th: Theme, width: number): str
510
696
  ? Math.round((duration / maxDuration) * timeBarWidth)
511
697
  : 0;
512
698
  const bar =
513
- th.fg("accent", "".repeat(fillLen)) +
514
- th.fg("dim", "".repeat(timeBarWidth - fillLen));
699
+ th.fg("accent", "\u2588".repeat(fillLen)) +
700
+ th.fg("dim", "\u2591".repeat(timeBarWidth - fillLen));
515
701
 
516
702
  const durStr = formatDuration(duration);
517
703
  const costStr = formatCost(unit.cost);
518
704
 
519
- const line = ` ${time} ${glyph} ${typeLabel} ${idLabel} ${bar} ${durStr} ${costStr}`;
705
+ // Tier and model info
706
+ const tierLabel = unit.tier ? th.fg("dim", `[${unit.tier}]`) : "";
707
+ const modelLabel = th.fg("dim", shortenModel(unit.model));
708
+ const tierModelPart = [tierLabel, modelLabel].filter(Boolean).join(" ");
709
+
710
+ const line = ` ${time} ${glyph} ${typeLabel} ${tierModelPart} ${idLabel} ${bar} ${durStr} ${costStr}`;
520
711
  lines.push(truncateToWidth(line, width));
521
712
  }
522
713
 
@@ -554,7 +745,7 @@ function renderGanttView(data: VisualizerData, th: Theme, width: number): string
554
745
  for (const unit of recent) {
555
746
  const phase = classifyUnitPhase(unit.type);
556
747
  if (phase !== lastPhase && lastPhase !== "") {
557
- lines.push(th.fg("dim", " " + "".repeat(width - 4)));
748
+ lines.push(th.fg("dim", " " + "\u2500".repeat(width - 4)));
558
749
  }
559
750
  lastPhase = phase;
560
751
 
@@ -571,11 +762,12 @@ function renderGanttView(data: VisualizerData, th: Theme, width: number): string
571
762
 
572
763
  const barStr =
573
764
  " ".repeat(startPos) +
574
- th.fg(phaseColor, "".repeat(barLen)) +
765
+ th.fg(phaseColor, "\u2588".repeat(barLen)) +
575
766
  " ".repeat(Math.max(0, barArea - startPos - barLen));
576
767
 
768
+ const tierTag = unit.tier ? `[${unit.tier[0]}]` : "";
577
769
  const gutter = padRight(
578
- truncateToWidth(`${unit.type.slice(0, 8)} ${unit.id}`, gutterWidth - 1),
770
+ truncateToWidth(`${unit.type.slice(0, 8)} ${unit.id}${tierTag}`, gutterWidth - 1),
579
771
  gutterWidth,
580
772
  );
581
773
 
@@ -611,10 +803,10 @@ export function renderAgentView(
611
803
 
612
804
  // Status line
613
805
  const statusDot = activity.active
614
- ? th.fg("success", "")
615
- : th.fg("dim", "");
806
+ ? th.fg("success", "\u25cf")
807
+ : th.fg("dim", "\u25cb");
616
808
  const statusText = activity.active ? "ACTIVE" : "IDLE";
617
- const elapsedStr = activity.active ? formatDuration(activity.elapsed) : "";
809
+ const elapsedStr = activity.active ? formatDuration(activity.elapsed) : "\u2014";
618
810
 
619
811
  lines.push(
620
812
  joinColumns(
@@ -640,15 +832,15 @@ export function renderAgentView(
640
832
  const barW = Math.max(10, Math.min(30, width - 30));
641
833
  const fillLen = Math.round(pct * barW);
642
834
  const bar =
643
- th.fg("accent", "".repeat(fillLen)) +
644
- th.fg("dim", "".repeat(barW - fillLen));
835
+ th.fg("accent", "\u2588".repeat(fillLen)) +
836
+ th.fg("dim", "\u2591".repeat(barW - fillLen));
645
837
  lines.push(`Progress ${bar} ${completed}/${total} slices`);
646
838
  }
647
839
 
648
840
  // Rate and session stats
649
841
  const rateStr = activity.completionRate > 0
650
842
  ? `${activity.completionRate.toFixed(1)} units/hr`
651
- : "";
843
+ : "\u2014";
652
844
  lines.push(
653
845
  `Rate: ${th.fg("text", rateStr)} ` +
654
846
  `Session: ${th.fg("text", formatCost(activity.sessionCost))} ` +
@@ -657,6 +849,21 @@ export function renderAgentView(
657
849
 
658
850
  lines.push("");
659
851
 
852
+ // Budget pressure
853
+ const health = data.health;
854
+ const truncColor = health.truncationRate < 10 ? "success" : health.truncationRate < 30 ? "warning" : "error";
855
+ const contColor = health.continueHereRate < 10 ? "success" : health.continueHereRate < 30 ? "warning" : "error";
856
+ lines.push(th.fg("accent", th.bold("Pressure")));
857
+ lines.push(` Truncation rate: ${th.fg(truncColor, `${health.truncationRate.toFixed(1)}%`)}`);
858
+ lines.push(` Continue-here rate: ${th.fg(contColor, `${health.continueHereRate.toFixed(1)}%`)}`);
859
+
860
+ // Pending captures
861
+ if (data.captures.pendingCount > 0) {
862
+ lines.push(` Pending captures: ${th.fg("warning", String(data.captures.pendingCount))}`);
863
+ }
864
+
865
+ lines.push("");
866
+
660
867
  // Recent completed units (last 5)
661
868
  const recentUnits = data.units.filter(u => u.finishedAt > 0).slice(-5).reverse();
662
869
  if (recentUnits.length > 0) {
@@ -670,7 +877,7 @@ export function renderAgentView(
670
877
  const typeLabel = padRight(u.type, 16);
671
878
  lines.push(
672
879
  truncateToWidth(
673
- ` ${hh}:${mm} ${th.fg("success", "")} ${typeLabel} ${padRight(u.id, 16)} ${dur} ${cost}`,
880
+ ` ${hh}:${mm} ${th.fg("success", "\u2713")} ${typeLabel} ${padRight(u.id, 16)} ${dur} ${cost}`,
674
881
  width,
675
882
  ),
676
883
  );
@@ -713,13 +920,30 @@ export function renderChangelogView(
713
920
  for (const f of entry.filesModified) {
714
921
  lines.push(
715
922
  truncateToWidth(
716
- ` ${th.fg("success", "")} ${f.path} ${f.description}`,
923
+ ` ${th.fg("success", "\u2713")} ${f.path} \u2014 ${f.description}`,
717
924
  width,
718
925
  ),
719
926
  );
720
927
  }
721
928
  }
722
929
 
930
+ // Decisions and patterns from slice verification
931
+ const ver = findVerification(data, entry.milestoneId, entry.sliceId);
932
+ if (ver) {
933
+ if (ver.keyDecisions.length > 0) {
934
+ lines.push(" Decisions:");
935
+ for (const d of ver.keyDecisions) {
936
+ lines.push(` - ${d}`);
937
+ }
938
+ }
939
+ if (ver.patternsEstablished.length > 0) {
940
+ lines.push(" Patterns:");
941
+ for (const p of ver.patternsEstablished) {
942
+ lines.push(` - ${p}`);
943
+ }
944
+ }
945
+ }
946
+
723
947
  if (entry.completedAt) {
724
948
  lines.push(th.fg("dim", ` Completed: ${entry.completedAt}`));
725
949
  }
@@ -742,9 +966,9 @@ export function renderExportView(
742
966
 
743
967
  lines.push(th.fg("accent", th.bold("Export Options")));
744
968
  lines.push("");
745
- lines.push(` ${th.fg("accent", "[m]")} Markdown report full project summary with tables`);
746
- lines.push(` ${th.fg("accent", "[j]")} JSON report machine-readable project data`);
747
- lines.push(` ${th.fg("accent", "[s]")} Snapshot current view as plain text`);
969
+ lines.push(` ${th.fg("accent", "[m]")} Markdown report \u2014 full project summary with tables`);
970
+ lines.push(` ${th.fg("accent", "[j]")} JSON report \u2014 machine-readable project data`);
971
+ lines.push(` ${th.fg("accent", "[s]")} Snapshot \u2014 current view as plain text`);
748
972
 
749
973
  if (lastExportPath) {
750
974
  lines.push("");
@@ -753,3 +977,193 @@ export function renderExportView(
753
977
 
754
978
  return lines;
755
979
  }
980
+
981
+ // ─── Knowledge View ──────────────────────────────────────────────────────────
982
+
983
+ export function renderKnowledgeView(
984
+ data: VisualizerData,
985
+ th: Theme,
986
+ width: number,
987
+ ): string[] {
988
+ const lines: string[] = [];
989
+ const knowledge = data.knowledge;
990
+
991
+ if (!knowledge.exists) {
992
+ lines.push(th.fg("dim", "No KNOWLEDGE.md found"));
993
+ return lines;
994
+ }
995
+
996
+ if (knowledge.rules.length === 0 && knowledge.patterns.length === 0 && knowledge.lessons.length === 0) {
997
+ lines.push(th.fg("dim", "KNOWLEDGE.md exists but is empty"));
998
+ return lines;
999
+ }
1000
+
1001
+ // Rules section
1002
+ if (knowledge.rules.length > 0) {
1003
+ lines.push(th.fg("accent", th.bold("Rules")));
1004
+ lines.push("");
1005
+ for (const rule of knowledge.rules) {
1006
+ lines.push(truncateToWidth(
1007
+ ` ${th.fg("accent", rule.id)} ${th.fg("dim", `[${rule.scope}]`)} ${rule.content}`,
1008
+ width,
1009
+ ));
1010
+ }
1011
+ lines.push("");
1012
+ }
1013
+
1014
+ // Patterns section
1015
+ if (knowledge.patterns.length > 0) {
1016
+ lines.push(th.fg("accent", th.bold("Patterns")));
1017
+ lines.push("");
1018
+ for (const pattern of knowledge.patterns) {
1019
+ lines.push(truncateToWidth(
1020
+ ` ${th.fg("accent", pattern.id)} ${pattern.content}`,
1021
+ width,
1022
+ ));
1023
+ }
1024
+ lines.push("");
1025
+ }
1026
+
1027
+ // Lessons section
1028
+ if (knowledge.lessons.length > 0) {
1029
+ lines.push(th.fg("accent", th.bold("Lessons Learned")));
1030
+ lines.push("");
1031
+ for (const lesson of knowledge.lessons) {
1032
+ lines.push(truncateToWidth(
1033
+ ` ${th.fg("accent", lesson.id)} ${lesson.content}`,
1034
+ width,
1035
+ ));
1036
+ }
1037
+ lines.push("");
1038
+ }
1039
+
1040
+ return lines;
1041
+ }
1042
+
1043
+ // ─── Captures View ───────────────────────────────────────────────────────────
1044
+
1045
+ export function renderCapturesView(
1046
+ data: VisualizerData,
1047
+ th: Theme,
1048
+ width: number,
1049
+ ): string[] {
1050
+ const lines: string[] = [];
1051
+ const captures = data.captures;
1052
+
1053
+ // Summary line
1054
+ const resolved = captures.entries.filter(e => e.status === "resolved").length;
1055
+ lines.push(
1056
+ `${th.fg("text", String(captures.totalCount))} total \u00b7 ` +
1057
+ `${th.fg("warning", String(captures.pendingCount))} pending \u00b7 ` +
1058
+ `${th.fg("dim", String(resolved))} resolved`,
1059
+ );
1060
+ lines.push("");
1061
+
1062
+ if (captures.entries.length === 0) {
1063
+ lines.push(th.fg("dim", "No captures recorded."));
1064
+ return lines;
1065
+ }
1066
+
1067
+ // Group by status: pending first, then triaged, then resolved
1068
+ const statusOrder: Record<string, number> = { pending: 0, triaged: 1, resolved: 2 };
1069
+ const sorted = [...captures.entries].sort((a, b) =>
1070
+ (statusOrder[a.status] ?? 3) - (statusOrder[b.status] ?? 3),
1071
+ );
1072
+
1073
+ for (const entry of sorted) {
1074
+ const statusColor =
1075
+ entry.status === "pending" ? "warning" :
1076
+ entry.status === "triaged" ? "accent" :
1077
+ "dim";
1078
+
1079
+ const classColor =
1080
+ entry.classification === "inject" ? "warning" :
1081
+ entry.classification === "quick-task" ? "accent" :
1082
+ entry.classification === "replan" ? "error" :
1083
+ entry.classification === "defer" ? "text" :
1084
+ "dim";
1085
+
1086
+ const classBadge = entry.classification
1087
+ ? th.fg(classColor, `(${entry.classification})`)
1088
+ : "";
1089
+
1090
+ const statusBadge = th.fg(statusColor, `[${entry.status}]`);
1091
+ const textPreview = truncateToWidth(entry.text, Math.max(20, width - 50));
1092
+
1093
+ lines.push(` ${th.fg("accent", entry.id)} ${statusBadge} ${textPreview} ${classBadge}`);
1094
+ if (entry.timestamp) {
1095
+ lines.push(` ${th.fg("dim", entry.timestamp)}`);
1096
+ }
1097
+ }
1098
+
1099
+ return lines;
1100
+ }
1101
+
1102
+ // ─── Health View ─────────────────────────────────────────────────────────────
1103
+
1104
+ export function renderHealthView(
1105
+ data: VisualizerData,
1106
+ th: Theme,
1107
+ width: number,
1108
+ ): string[] {
1109
+ const lines: string[] = [];
1110
+ const health = data.health;
1111
+
1112
+ // Budget section
1113
+ lines.push(th.fg("accent", th.bold("Budget")));
1114
+ lines.push("");
1115
+ if (health.budgetCeiling !== undefined) {
1116
+ const currentSpend = data.totals?.cost ?? 0;
1117
+ const pct = health.budgetCeiling > 0 ? Math.min(1, currentSpend / health.budgetCeiling) : 0;
1118
+ const barW = Math.max(10, Math.min(30, width - 40));
1119
+ const fillLen = Math.round(pct * barW);
1120
+ const budgetColor = pct < 0.7 ? "success" : pct < 0.9 ? "warning" : "error";
1121
+ const bar =
1122
+ th.fg(budgetColor, "\u2588".repeat(fillLen)) +
1123
+ th.fg("dim", "\u2591".repeat(barW - fillLen));
1124
+ lines.push(` Ceiling: ${th.fg("text", formatCost(health.budgetCeiling))}`);
1125
+ lines.push(` Spend: ${bar} ${formatCost(currentSpend)} (${(pct * 100).toFixed(1)}%)`);
1126
+ } else {
1127
+ lines.push(th.fg("dim", " No budget ceiling set"));
1128
+ }
1129
+ lines.push(` Token profile: ${th.fg("text", health.tokenProfile)}`);
1130
+ lines.push("");
1131
+
1132
+ // Pressure section
1133
+ lines.push(th.fg("accent", th.bold("Pressure")));
1134
+ lines.push("");
1135
+ const truncColor = health.truncationRate < 10 ? "success" : health.truncationRate < 30 ? "warning" : "error";
1136
+ const contColor = health.continueHereRate < 10 ? "success" : health.continueHereRate < 30 ? "warning" : "error";
1137
+ const pressBarW = Math.max(10, Math.min(20, width - 50));
1138
+
1139
+ const truncFill = Math.round((Math.min(health.truncationRate, 100) / 100) * pressBarW);
1140
+ const truncBar = th.fg(truncColor, "\u2588".repeat(truncFill)) + th.fg("dim", "\u2591".repeat(pressBarW - truncFill));
1141
+ lines.push(` Truncation: ${truncBar} ${health.truncationRate.toFixed(1)}%`);
1142
+
1143
+ const contFill = Math.round((Math.min(health.continueHereRate, 100) / 100) * pressBarW);
1144
+ const contBar = th.fg(contColor, "\u2588".repeat(contFill)) + th.fg("dim", "\u2591".repeat(pressBarW - contFill));
1145
+ lines.push(` Continue-here: ${contBar} ${health.continueHereRate.toFixed(1)}%`);
1146
+ lines.push("");
1147
+
1148
+ // Routing section
1149
+ if (health.tierBreakdown.length > 0) {
1150
+ lines.push(th.fg("accent", th.bold("Routing")));
1151
+ lines.push("");
1152
+ for (const tier of health.tierBreakdown) {
1153
+ const downTag = tier.downgraded > 0 ? th.fg("warning", ` (${tier.downgraded} downgraded)`) : "";
1154
+ lines.push(` ${padRight(tier.tier, 12)} ${tier.units} units ${formatCost(tier.cost)}${downTag}`);
1155
+ }
1156
+ if (health.tierSavingsLine) {
1157
+ lines.push(` ${th.fg("success", health.tierSavingsLine)}`);
1158
+ }
1159
+ lines.push("");
1160
+ }
1161
+
1162
+ // Session section
1163
+ lines.push(th.fg("accent", th.bold("Session")));
1164
+ lines.push("");
1165
+ lines.push(` Tool calls: ${th.fg("text", String(health.toolCalls))}`);
1166
+ lines.push(` Messages: ${th.fg("text", String(health.assistantMessages))} sent / ${th.fg("text", String(health.userMessages))} received`);
1167
+
1168
+ return lines;
1169
+ }