gsd-pi 2.17.0 → 2.19.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 (217) hide show
  1. package/README.md +39 -0
  2. package/dist/onboarding.js +2 -2
  3. package/dist/remote-questions-config.d.ts +10 -0
  4. package/dist/remote-questions-config.js +36 -0
  5. package/dist/resources/extensions/gsd/activity-log.ts +37 -7
  6. package/dist/resources/extensions/gsd/auto-dashboard.ts +14 -2
  7. package/dist/resources/extensions/gsd/auto-prompts.ts +65 -16
  8. package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
  9. package/dist/resources/extensions/gsd/auto.ts +399 -29
  10. package/dist/resources/extensions/gsd/captures.ts +384 -0
  11. package/dist/resources/extensions/gsd/commands.ts +382 -23
  12. package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
  13. package/dist/resources/extensions/gsd/dashboard-overlay.ts +10 -0
  14. package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
  15. package/dist/resources/extensions/gsd/docs/preferences-reference.md +201 -2
  16. package/dist/resources/extensions/gsd/files.ts +123 -1
  17. package/dist/resources/extensions/gsd/guided-flow.ts +237 -4
  18. package/dist/resources/extensions/gsd/index.ts +47 -3
  19. package/dist/resources/extensions/gsd/metrics.ts +48 -0
  20. package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
  21. package/dist/resources/extensions/gsd/model-router.ts +256 -0
  22. package/dist/resources/extensions/gsd/paths.ts +9 -0
  23. package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  24. package/dist/resources/extensions/gsd/preferences.ts +132 -1
  25. package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
  26. package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
  27. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  28. package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  29. package/dist/resources/extensions/gsd/prompts/system.md +2 -0
  30. package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  31. package/dist/resources/extensions/gsd/queue-order.ts +231 -0
  32. package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  33. package/dist/resources/extensions/gsd/state.ts +15 -3
  34. package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
  35. package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
  36. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  37. package/dist/resources/extensions/gsd/tests/captures.test.ts +438 -0
  38. package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  39. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  40. package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  41. package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  42. package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  43. package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  44. package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  45. package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  46. package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  47. package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  48. package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  49. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  50. package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
  51. package/dist/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  52. package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  53. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  54. package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  55. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
  56. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
  57. package/dist/resources/extensions/gsd/triage-resolution.ts +200 -0
  58. package/dist/resources/extensions/gsd/triage-ui.ts +175 -0
  59. package/dist/resources/extensions/gsd/visualizer-data.ts +154 -0
  60. package/dist/resources/extensions/gsd/visualizer-overlay.ts +193 -0
  61. package/dist/resources/extensions/gsd/visualizer-views.ts +293 -0
  62. package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
  63. package/dist/resources/extensions/gsd/worktree.ts +22 -0
  64. package/dist/resources/extensions/remote-questions/discord-adapter.ts +33 -0
  65. package/dist/resources/extensions/remote-questions/format.ts +12 -6
  66. package/dist/resources/extensions/remote-questions/manager.ts +8 -0
  67. package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
  68. package/package.json +1 -1
  69. package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
  70. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  71. package/packages/pi-coding-agent/dist/cli/args.js +21 -0
  72. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  73. package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
  74. package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
  75. package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
  76. package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
  77. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
  78. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
  79. package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
  80. package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
  81. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
  82. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
  83. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
  84. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
  85. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
  86. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
  87. package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
  88. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
  89. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
  90. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
  91. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
  92. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
  93. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
  94. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
  95. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
  96. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
  97. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
  98. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
  100. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
  102. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
  103. package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
  104. package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
  105. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
  106. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
  107. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
  108. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
  109. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
  110. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
  112. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  113. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  115. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  116. package/packages/pi-coding-agent/dist/index.d.ts +5 -1
  117. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  118. package/packages/pi-coding-agent/dist/index.js +4 -1
  119. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  121. package/packages/pi-coding-agent/dist/main.js +17 -2
  122. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
  128. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
  130. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
  131. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
  132. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
  133. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
  134. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  135. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +32 -0
  136. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  137. package/packages/pi-coding-agent/src/cli/args.ts +21 -0
  138. package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
  139. package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
  140. package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
  141. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
  142. package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
  143. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
  144. package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
  145. package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
  146. package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
  147. package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
  148. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  149. package/packages/pi-coding-agent/src/index.ts +5 -0
  150. package/packages/pi-coding-agent/src/main.ts +19 -2
  151. package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
  152. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
  153. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
  154. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
  155. package/src/resources/extensions/gsd/activity-log.ts +37 -7
  156. package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
  157. package/src/resources/extensions/gsd/auto-prompts.ts +65 -16
  158. package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
  159. package/src/resources/extensions/gsd/auto.ts +399 -29
  160. package/src/resources/extensions/gsd/captures.ts +384 -0
  161. package/src/resources/extensions/gsd/commands.ts +382 -23
  162. package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
  163. package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -0
  164. package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
  165. package/src/resources/extensions/gsd/docs/preferences-reference.md +201 -2
  166. package/src/resources/extensions/gsd/files.ts +123 -1
  167. package/src/resources/extensions/gsd/guided-flow.ts +237 -4
  168. package/src/resources/extensions/gsd/index.ts +47 -3
  169. package/src/resources/extensions/gsd/metrics.ts +48 -0
  170. package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
  171. package/src/resources/extensions/gsd/model-router.ts +256 -0
  172. package/src/resources/extensions/gsd/paths.ts +9 -0
  173. package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  174. package/src/resources/extensions/gsd/preferences.ts +132 -1
  175. package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
  176. package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
  177. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  178. package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  179. package/src/resources/extensions/gsd/prompts/system.md +2 -0
  180. package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  181. package/src/resources/extensions/gsd/queue-order.ts +231 -0
  182. package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  183. package/src/resources/extensions/gsd/state.ts +15 -3
  184. package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
  185. package/src/resources/extensions/gsd/templates/preferences.md +14 -0
  186. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  187. package/src/resources/extensions/gsd/tests/captures.test.ts +438 -0
  188. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  189. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  190. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  191. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  192. package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  193. package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  194. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  195. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  196. package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  197. package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  198. package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  199. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  200. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
  201. package/src/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  202. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  203. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  204. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  205. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
  206. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
  207. package/src/resources/extensions/gsd/triage-resolution.ts +200 -0
  208. package/src/resources/extensions/gsd/triage-ui.ts +175 -0
  209. package/src/resources/extensions/gsd/visualizer-data.ts +154 -0
  210. package/src/resources/extensions/gsd/visualizer-overlay.ts +193 -0
  211. package/src/resources/extensions/gsd/visualizer-views.ts +293 -0
  212. package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
  213. package/src/resources/extensions/gsd/worktree.ts +22 -0
  214. package/src/resources/extensions/remote-questions/discord-adapter.ts +33 -0
  215. package/src/resources/extensions/remote-questions/format.ts +12 -6
  216. package/src/resources/extensions/remote-questions/manager.ts +8 -0
  217. package/src/resources/extensions/shared/next-action-ui.ts +16 -1
@@ -0,0 +1,154 @@
1
+ // Data loader for workflow visualizer overlay — aggregates state + metrics.
2
+
3
+ import { deriveState } from './state.js';
4
+ import { parseRoadmap, parsePlan, loadFile } from './files.js';
5
+ import { findMilestoneIds } from './guided-flow.js';
6
+ import { resolveMilestoneFile, resolveSliceFile } from './paths.js';
7
+ import {
8
+ getLedger,
9
+ getProjectTotals,
10
+ aggregateByPhase,
11
+ aggregateBySlice,
12
+ aggregateByModel,
13
+ loadLedgerFromDisk,
14
+ } from './metrics.js';
15
+
16
+ import type { Phase } from './types.js';
17
+ import type {
18
+ ProjectTotals,
19
+ PhaseAggregate,
20
+ SliceAggregate,
21
+ ModelAggregate,
22
+ UnitMetrics,
23
+ } from './metrics.js';
24
+
25
+ // ─── Visualizer Types ─────────────────────────────────────────────────────────
26
+
27
+ export interface VisualizerMilestone {
28
+ id: string;
29
+ title: string;
30
+ status: 'complete' | 'active' | 'pending';
31
+ dependsOn: string[];
32
+ slices: VisualizerSlice[];
33
+ }
34
+
35
+ export interface VisualizerSlice {
36
+ id: string;
37
+ title: string;
38
+ done: boolean;
39
+ active: boolean;
40
+ risk: string;
41
+ depends: string[];
42
+ tasks: VisualizerTask[];
43
+ }
44
+
45
+ export interface VisualizerTask {
46
+ id: string;
47
+ title: string;
48
+ done: boolean;
49
+ active: boolean;
50
+ }
51
+
52
+ export interface VisualizerData {
53
+ milestones: VisualizerMilestone[];
54
+ phase: Phase;
55
+ totals: ProjectTotals | null;
56
+ byPhase: PhaseAggregate[];
57
+ bySlice: SliceAggregate[];
58
+ byModel: ModelAggregate[];
59
+ units: UnitMetrics[];
60
+ }
61
+
62
+ // ─── Loader ───────────────────────────────────────────────────────────────────
63
+
64
+ export async function loadVisualizerData(basePath: string): Promise<VisualizerData> {
65
+ const state = await deriveState(basePath);
66
+ const milestoneIds = findMilestoneIds(basePath);
67
+
68
+ const milestones: VisualizerMilestone[] = [];
69
+
70
+ for (const mid of milestoneIds) {
71
+ const entry = state.registry.find(r => r.id === mid);
72
+ const status = entry?.status ?? 'pending';
73
+ const dependsOn = entry?.dependsOn ?? [];
74
+
75
+ const slices: VisualizerSlice[] = [];
76
+
77
+ const roadmapFile = resolveMilestoneFile(basePath, mid, 'ROADMAP');
78
+ const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
79
+
80
+ if (roadmapContent) {
81
+ const roadmap = parseRoadmap(roadmapContent);
82
+
83
+ for (const s of roadmap.slices) {
84
+ const isActiveSlice =
85
+ state.activeMilestone?.id === mid &&
86
+ state.activeSlice?.id === s.id;
87
+
88
+ const tasks: VisualizerTask[] = [];
89
+
90
+ if (isActiveSlice) {
91
+ const planFile = resolveSliceFile(basePath, mid, s.id, 'PLAN');
92
+ const planContent = planFile ? await loadFile(planFile) : null;
93
+
94
+ if (planContent) {
95
+ const plan = parsePlan(planContent);
96
+ for (const t of plan.tasks) {
97
+ tasks.push({
98
+ id: t.id,
99
+ title: t.title,
100
+ done: t.done,
101
+ active: state.activeTask?.id === t.id,
102
+ });
103
+ }
104
+ }
105
+ }
106
+
107
+ slices.push({
108
+ id: s.id,
109
+ title: s.title,
110
+ done: s.done,
111
+ active: isActiveSlice,
112
+ risk: s.risk,
113
+ depends: s.depends,
114
+ tasks,
115
+ });
116
+ }
117
+ }
118
+
119
+ milestones.push({
120
+ id: mid,
121
+ title: entry?.title ?? mid,
122
+ status,
123
+ dependsOn,
124
+ slices,
125
+ });
126
+ }
127
+
128
+ // Metrics
129
+ let totals: ProjectTotals | null = null;
130
+ let byPhase: PhaseAggregate[] = [];
131
+ let bySlice: SliceAggregate[] = [];
132
+ let byModel: ModelAggregate[] = [];
133
+ let units: UnitMetrics[] = [];
134
+
135
+ const ledger = getLedger() ?? loadLedgerFromDisk(basePath);
136
+
137
+ if (ledger && ledger.units.length > 0) {
138
+ units = [...ledger.units].sort((a, b) => a.startedAt - b.startedAt);
139
+ totals = getProjectTotals(units);
140
+ byPhase = aggregateByPhase(units);
141
+ bySlice = aggregateBySlice(units);
142
+ byModel = aggregateByModel(units);
143
+ }
144
+
145
+ return {
146
+ milestones,
147
+ phase: state.phase,
148
+ totals,
149
+ byPhase,
150
+ bySlice,
151
+ byModel,
152
+ units,
153
+ };
154
+ }
@@ -0,0 +1,193 @@
1
+ import type { Theme } from "@gsd/pi-coding-agent";
2
+ import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui";
3
+ import { loadVisualizerData, type VisualizerData } from "./visualizer-data.js";
4
+ import {
5
+ renderProgressView,
6
+ renderDepsView,
7
+ renderMetricsView,
8
+ renderTimelineView,
9
+ } from "./visualizer-views.js";
10
+
11
+ const TAB_LABELS = ["1 Progress", "2 Deps", "3 Metrics", "4 Timeline"];
12
+
13
+ export class GSDVisualizerOverlay {
14
+ private tui: { requestRender: () => void };
15
+ private theme: Theme;
16
+ private onClose: () => void;
17
+
18
+ activeTab = 0;
19
+ scrollOffsets: number[] = [0, 0, 0, 0];
20
+ loading = true;
21
+ disposed = false;
22
+ cachedWidth?: number;
23
+ cachedLines?: string[];
24
+ refreshTimer: ReturnType<typeof setInterval>;
25
+ data: VisualizerData | null = null;
26
+ basePath: string;
27
+
28
+ constructor(
29
+ tui: { requestRender: () => void },
30
+ theme: Theme,
31
+ onClose: () => void,
32
+ ) {
33
+ this.tui = tui;
34
+ this.theme = theme;
35
+ this.onClose = onClose;
36
+ this.basePath = process.cwd();
37
+
38
+ loadVisualizerData(this.basePath).then((d) => {
39
+ this.data = d;
40
+ this.loading = false;
41
+ this.tui.requestRender();
42
+ });
43
+
44
+ this.refreshTimer = setInterval(() => {
45
+ loadVisualizerData(this.basePath).then((d) => {
46
+ if (this.disposed) return;
47
+ this.data = d;
48
+ this.invalidate();
49
+ this.tui.requestRender();
50
+ });
51
+ }, 2000);
52
+ }
53
+
54
+ handleInput(data: string): void {
55
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
56
+ this.dispose();
57
+ this.onClose();
58
+ return;
59
+ }
60
+
61
+ if (matchesKey(data, Key.tab)) {
62
+ this.activeTab = (this.activeTab + 1) % 4;
63
+ this.invalidate();
64
+ this.tui.requestRender();
65
+ return;
66
+ }
67
+
68
+ if (data === "1" || data === "2" || data === "3" || data === "4") {
69
+ this.activeTab = parseInt(data, 10) - 1;
70
+ this.invalidate();
71
+ this.tui.requestRender();
72
+ return;
73
+ }
74
+
75
+ if (matchesKey(data, Key.down) || matchesKey(data, "j")) {
76
+ this.scrollOffsets[this.activeTab]++;
77
+ this.invalidate();
78
+ this.tui.requestRender();
79
+ return;
80
+ }
81
+
82
+ if (matchesKey(data, Key.up) || matchesKey(data, "k")) {
83
+ this.scrollOffsets[this.activeTab] = Math.max(0, this.scrollOffsets[this.activeTab] - 1);
84
+ this.invalidate();
85
+ this.tui.requestRender();
86
+ return;
87
+ }
88
+
89
+ if (data === "g") {
90
+ this.scrollOffsets[this.activeTab] = 0;
91
+ this.invalidate();
92
+ this.tui.requestRender();
93
+ return;
94
+ }
95
+
96
+ if (data === "G") {
97
+ this.scrollOffsets[this.activeTab] = 999;
98
+ this.invalidate();
99
+ this.tui.requestRender();
100
+ return;
101
+ }
102
+ }
103
+
104
+ render(width: number): string[] {
105
+ if (this.cachedLines && this.cachedWidth === width) {
106
+ return this.cachedLines;
107
+ }
108
+
109
+ const th = this.theme;
110
+ const innerWidth = width - 4;
111
+ const content: string[] = [];
112
+
113
+ // Tab bar
114
+ const tabs = TAB_LABELS.map((label, i) => {
115
+ if (i === this.activeTab) {
116
+ return th.fg("accent", `[${label}]`);
117
+ }
118
+ return th.fg("dim", `[${label}]`);
119
+ });
120
+ content.push(" " + tabs.join(" "));
121
+ content.push("");
122
+
123
+ if (this.loading) {
124
+ const loadingText = "Loading…";
125
+ const vis = visibleWidth(loadingText);
126
+ const leftPad = Math.max(0, Math.floor((innerWidth - vis) / 2));
127
+ content.push(" ".repeat(leftPad) + loadingText);
128
+ } else if (this.data) {
129
+ let viewLines: string[] = [];
130
+ switch (this.activeTab) {
131
+ case 0:
132
+ viewLines = renderProgressView(this.data, th, innerWidth);
133
+ break;
134
+ case 1:
135
+ viewLines = renderDepsView(this.data, th, innerWidth);
136
+ break;
137
+ case 2:
138
+ viewLines = renderMetricsView(this.data, th, innerWidth);
139
+ break;
140
+ case 3:
141
+ viewLines = renderTimelineView(this.data, th, innerWidth);
142
+ break;
143
+ }
144
+ content.push(...viewLines);
145
+ }
146
+
147
+ // Apply scroll
148
+ const viewportHeight = Math.max(5, process.stdout.rows ? process.stdout.rows - 8 : 24);
149
+ const chromeHeight = 2;
150
+ const visibleContentRows = Math.max(1, viewportHeight - chromeHeight);
151
+ const maxScroll = Math.max(0, content.length - visibleContentRows);
152
+ this.scrollOffsets[this.activeTab] = Math.min(this.scrollOffsets[this.activeTab], maxScroll);
153
+ const offset = this.scrollOffsets[this.activeTab];
154
+ const visibleContent = content.slice(offset, offset + visibleContentRows);
155
+
156
+ const lines = this.wrapInBox(visibleContent, width);
157
+
158
+ // Footer hint
159
+ const hint = th.fg("dim", "Tab/1-4 switch · ↑↓ scroll · g/G top/end · esc close");
160
+ const hintVis = visibleWidth(hint);
161
+ const hintPad = Math.max(0, Math.floor((width - hintVis) / 2));
162
+ lines.push(" ".repeat(hintPad) + hint);
163
+
164
+ this.cachedWidth = width;
165
+ this.cachedLines = lines;
166
+ return lines;
167
+ }
168
+
169
+ private wrapInBox(inner: string[], width: number): string[] {
170
+ const th = this.theme;
171
+ const border = (s: string) => th.fg("borderAccent", s);
172
+ const innerWidth = width - 4;
173
+ const lines: string[] = [];
174
+ lines.push(border("╭" + "─".repeat(width - 2) + "╮"));
175
+ for (const line of inner) {
176
+ const truncated = truncateToWidth(line, innerWidth);
177
+ const padWidth = Math.max(0, innerWidth - visibleWidth(truncated));
178
+ lines.push(border("│") + " " + truncated + " ".repeat(padWidth) + " " + border("│"));
179
+ }
180
+ lines.push(border("╰" + "─".repeat(width - 2) + "╯"));
181
+ return lines;
182
+ }
183
+
184
+ invalidate(): void {
185
+ this.cachedWidth = undefined;
186
+ this.cachedLines = undefined;
187
+ }
188
+
189
+ dispose(): void {
190
+ this.disposed = true;
191
+ clearInterval(this.refreshTimer);
192
+ }
193
+ }
@@ -0,0 +1,293 @@
1
+ // View renderers for the GSD workflow visualizer overlay.
2
+
3
+ import type { Theme } from "@gsd/pi-coding-agent";
4
+ import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
5
+ import type { VisualizerData, VisualizerMilestone } from "./visualizer-data.js";
6
+ import { formatCost, formatTokenCount } from "./metrics.js";
7
+
8
+ // ─── Local Helpers ───────────────────────────────────────────────────────────
9
+
10
+ function formatDuration(ms: number): string {
11
+ const s = Math.floor(ms / 1000);
12
+ if (s < 60) return `${s}s`;
13
+ const m = Math.floor(s / 60);
14
+ const rs = s % 60;
15
+ if (m < 60) return `${m}m ${rs}s`;
16
+ const h = Math.floor(m / 60);
17
+ const rm = m % 60;
18
+ return `${h}h ${rm}m`;
19
+ }
20
+
21
+ function padRight(content: string, width: number): string {
22
+ const vis = visibleWidth(content);
23
+ return content + " ".repeat(Math.max(0, width - vis));
24
+ }
25
+
26
+ function joinColumns(left: string, right: string, width: number): string {
27
+ const leftW = visibleWidth(left);
28
+ const rightW = visibleWidth(right);
29
+ if (leftW + rightW + 2 > width) {
30
+ return truncateToWidth(`${left} ${right}`, width);
31
+ }
32
+ return left + " ".repeat(width - leftW - rightW) + right;
33
+ }
34
+
35
+ // ─── Progress View ───────────────────────────────────────────────────────────
36
+
37
+ export function renderProgressView(
38
+ data: VisualizerData,
39
+ th: Theme,
40
+ width: number,
41
+ ): string[] {
42
+ const lines: string[] = [];
43
+
44
+ for (const ms of data.milestones) {
45
+ // Milestone header line
46
+ const statusGlyph =
47
+ ms.status === "complete"
48
+ ? th.fg("success", "✓")
49
+ : ms.status === "active"
50
+ ? th.fg("accent", "▸")
51
+ : th.fg("dim", "○");
52
+ const statusLabel =
53
+ ms.status === "complete"
54
+ ? th.fg("success", "complete")
55
+ : ms.status === "active"
56
+ ? th.fg("accent", "active")
57
+ : th.fg("dim", "pending");
58
+ const msLeft = `${ms.id}: ${ms.title}`;
59
+ const msRight = `${statusGlyph} ${statusLabel}`;
60
+ lines.push(joinColumns(msLeft, msRight, width));
61
+
62
+ if (ms.slices.length === 0 && ms.dependsOn.length > 0) {
63
+ lines.push(th.fg("dim", ` (depends on ${ms.dependsOn.join(", ")})`));
64
+ continue;
65
+ }
66
+
67
+ if (ms.status === "pending" && ms.dependsOn.length > 0) {
68
+ lines.push(th.fg("dim", ` (depends on ${ms.dependsOn.join(", ")})`));
69
+ continue;
70
+ }
71
+
72
+ for (const sl of ms.slices) {
73
+ // Slice line
74
+ const slGlyph = sl.done
75
+ ? th.fg("success", "✓")
76
+ : sl.active
77
+ ? th.fg("accent", "▸")
78
+ : th.fg("dim", "○");
79
+ const riskColor =
80
+ sl.risk === "high"
81
+ ? "warning"
82
+ : sl.risk === "medium"
83
+ ? "text"
84
+ : "dim";
85
+ const riskBadge = th.fg(riskColor, sl.risk);
86
+ const slLeft = ` ${slGlyph} ${sl.id}: ${sl.title}`;
87
+ lines.push(joinColumns(slLeft, riskBadge, width));
88
+
89
+ // Show tasks for active slice
90
+ if (sl.active && sl.tasks.length > 0) {
91
+ for (const task of sl.tasks) {
92
+ const tGlyph = task.done
93
+ ? th.fg("success", "✓")
94
+ : task.active
95
+ ? th.fg("accent", "▸")
96
+ : th.fg("dim", "○");
97
+ lines.push(` ${tGlyph} ${task.id}: ${task.title}`);
98
+ }
99
+ }
100
+ }
101
+ }
102
+
103
+ return lines;
104
+ }
105
+
106
+ // ─── Dependencies View ───────────────────────────────────────────────────────
107
+
108
+ export function renderDepsView(
109
+ data: VisualizerData,
110
+ th: Theme,
111
+ width: number,
112
+ ): string[] {
113
+ const lines: string[] = [];
114
+
115
+ // Milestone Dependencies
116
+ lines.push(th.fg("accent", th.bold("Milestone Dependencies")));
117
+ lines.push("");
118
+
119
+ const msDeps = data.milestones.filter((ms) => ms.dependsOn.length > 0);
120
+ if (msDeps.length === 0) {
121
+ lines.push(th.fg("dim", " No milestone dependencies."));
122
+ } else {
123
+ for (const ms of msDeps) {
124
+ for (const dep of ms.dependsOn) {
125
+ lines.push(
126
+ ` ${th.fg("text", dep)} ${th.fg("accent", "──►")} ${th.fg("text", ms.id)}`,
127
+ );
128
+ }
129
+ }
130
+ }
131
+
132
+ lines.push("");
133
+
134
+ // Slice Dependencies (active milestone)
135
+ lines.push(th.fg("accent", th.bold("Slice Dependencies (active milestone)")));
136
+ lines.push("");
137
+
138
+ const activeMs = data.milestones.find((ms) => ms.status === "active");
139
+ if (!activeMs) {
140
+ lines.push(th.fg("dim", " No active milestone."));
141
+ } else {
142
+ const slDeps = activeMs.slices.filter((sl) => sl.depends.length > 0);
143
+ if (slDeps.length === 0) {
144
+ lines.push(th.fg("dim", " No slice dependencies."));
145
+ } else {
146
+ for (const sl of slDeps) {
147
+ for (const dep of sl.depends) {
148
+ lines.push(
149
+ ` ${th.fg("text", dep)} ${th.fg("accent", "──►")} ${th.fg("text", sl.id)}`,
150
+ );
151
+ }
152
+ }
153
+ }
154
+ }
155
+
156
+ return lines;
157
+ }
158
+
159
+ // ─── Metrics View ────────────────────────────────────────────────────────────
160
+
161
+ export function renderMetricsView(
162
+ data: VisualizerData,
163
+ th: Theme,
164
+ width: number,
165
+ ): string[] {
166
+ const lines: string[] = [];
167
+
168
+ if (data.totals === null) {
169
+ lines.push(th.fg("dim", "No metrics data available."));
170
+ return lines;
171
+ }
172
+
173
+ const totals = data.totals;
174
+
175
+ // Summary line
176
+ lines.push(
177
+ th.fg("accent", th.bold("Summary")),
178
+ );
179
+ lines.push(
180
+ ` Cost: ${th.fg("text", formatCost(totals.cost))} ` +
181
+ `Tokens: ${th.fg("text", formatTokenCount(totals.tokens.total))} ` +
182
+ `Units: ${th.fg("text", String(totals.units))}`,
183
+ );
184
+ lines.push("");
185
+
186
+ const barWidth = Math.max(10, width - 40);
187
+
188
+ // By Phase
189
+ if (data.byPhase.length > 0) {
190
+ lines.push(th.fg("accent", th.bold("By Phase")));
191
+ lines.push("");
192
+
193
+ const maxPhaseCost = Math.max(...data.byPhase.map((p) => p.cost));
194
+
195
+ for (const phase of data.byPhase) {
196
+ const pct = totals.cost > 0 ? (phase.cost / totals.cost) * 100 : 0;
197
+ const fillLen =
198
+ maxPhaseCost > 0
199
+ ? Math.round((phase.cost / maxPhaseCost) * barWidth)
200
+ : 0;
201
+ const bar =
202
+ th.fg("accent", "█".repeat(fillLen)) +
203
+ th.fg("dim", "░".repeat(barWidth - fillLen));
204
+ const label = padRight(phase.phase, 14);
205
+ const costStr = formatCost(phase.cost);
206
+ const pctStr = `${pct.toFixed(1)}%`;
207
+ const tokenStr = formatTokenCount(phase.tokens.total);
208
+ lines.push(` ${label} ${bar} ${costStr} ${pctStr} ${tokenStr}`);
209
+ }
210
+
211
+ lines.push("");
212
+ }
213
+
214
+ // By Model
215
+ if (data.byModel.length > 0) {
216
+ lines.push(th.fg("accent", th.bold("By Model")));
217
+ lines.push("");
218
+
219
+ const maxModelCost = Math.max(...data.byModel.map((m) => m.cost));
220
+
221
+ for (const model of data.byModel) {
222
+ const pct = totals.cost > 0 ? (model.cost / totals.cost) * 100 : 0;
223
+ const fillLen =
224
+ maxModelCost > 0
225
+ ? Math.round((model.cost / maxModelCost) * barWidth)
226
+ : 0;
227
+ const bar =
228
+ th.fg("accent", "█".repeat(fillLen)) +
229
+ th.fg("dim", "░".repeat(barWidth - fillLen));
230
+ const label = padRight(model.model, 20);
231
+ const costStr = formatCost(model.cost);
232
+ const pctStr = `${pct.toFixed(1)}%`;
233
+ lines.push(` ${label} ${bar} ${costStr} ${pctStr}`);
234
+ }
235
+ }
236
+
237
+ return lines;
238
+ }
239
+
240
+ // ─── Timeline View ──────────────────────────────────────────────────────────
241
+
242
+ export function renderTimelineView(
243
+ data: VisualizerData,
244
+ th: Theme,
245
+ width: number,
246
+ ): string[] {
247
+ const lines: string[] = [];
248
+
249
+ if (data.units.length === 0) {
250
+ lines.push(th.fg("dim", "No execution history."));
251
+ return lines;
252
+ }
253
+
254
+ // Show up to 20 most recent (units are sorted by startedAt asc, show most recent)
255
+ const recent = data.units.slice(-20).reverse();
256
+
257
+ const maxDuration = Math.max(
258
+ ...recent.map((u) => u.finishedAt - u.startedAt),
259
+ );
260
+ const timeBarWidth = Math.max(4, Math.min(12, width - 60));
261
+
262
+ for (const unit of recent) {
263
+ const dt = new Date(unit.startedAt);
264
+ const hh = String(dt.getHours()).padStart(2, "0");
265
+ const mm = String(dt.getMinutes()).padStart(2, "0");
266
+ const time = `${hh}:${mm}`;
267
+
268
+ const duration = unit.finishedAt - unit.startedAt;
269
+ const glyph =
270
+ unit.finishedAt > 0
271
+ ? th.fg("success", "✓")
272
+ : th.fg("accent", "▸");
273
+
274
+ const typeLabel = padRight(unit.type, 16);
275
+ const idLabel = padRight(unit.id, 14);
276
+
277
+ const fillLen =
278
+ maxDuration > 0
279
+ ? Math.round((duration / maxDuration) * timeBarWidth)
280
+ : 0;
281
+ const bar =
282
+ th.fg("accent", "█".repeat(fillLen)) +
283
+ th.fg("dim", "░".repeat(timeBarWidth - fillLen));
284
+
285
+ const durStr = formatDuration(duration);
286
+ const costStr = formatCost(unit.cost);
287
+
288
+ const line = ` ${time} ${glyph} ${typeLabel} ${idLabel} ${bar} ${durStr} ${costStr}`;
289
+ lines.push(truncateToWidth(line, width));
290
+ }
291
+
292
+ return lines;
293
+ }
@@ -94,7 +94,7 @@ export function worktreeBranchName(name: string): string {
94
94
  *
95
95
  * @param opts.branch — override the default `worktree/<name>` branch name
96
96
  */
97
- export function createWorktree(basePath: string, name: string, opts: { branch?: string } = {}): WorktreeInfo {
97
+ export function createWorktree(basePath: string, name: string, opts: { branch?: string; startPoint?: string } = {}): WorktreeInfo {
98
98
  // Validate name: alphanumeric, hyphens, underscores only
99
99
  if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
100
100
  throw new Error(`Invalid worktree name "${name}". Use only letters, numbers, hyphens, and underscores.`);
@@ -114,9 +114,12 @@ export function createWorktree(basePath: string, name: string, opts: { branch?:
114
114
  // Prune any stale worktree entries from a previous removal
115
115
  nativeWorktreePrune(basePath);
116
116
 
117
+ // Use the explicit start point (e.g. integration branch) if provided,
118
+ // otherwise fall back to the repo's detected main branch.
119
+ const startPoint = opts.startPoint ?? nativeDetectMainBranch(basePath);
120
+
117
121
  // Check if the branch already exists (leftover from a previous worktree)
118
122
  const branchAlreadyExists = nativeBranchExists(basePath, branch);
119
- const mainBranch = nativeDetectMainBranch(basePath);
120
123
 
121
124
  if (branchAlreadyExists) {
122
125
  // Check if the branch is actively used by an existing worktree.
@@ -130,11 +133,11 @@ export function createWorktree(basePath: string, name: string, opts: { branch?:
130
133
  );
131
134
  }
132
135
 
133
- // Reset the stale branch to current main, then attach worktree to it
134
- nativeBranchForceReset(basePath, branch, mainBranch);
136
+ // Reset the stale branch to the start point, then attach worktree to it
137
+ nativeBranchForceReset(basePath, branch, startPoint);
135
138
  nativeWorktreeAdd(basePath, wtPath, branch);
136
139
  } else {
137
- nativeWorktreeAdd(basePath, wtPath, branch, true, mainBranch);
140
+ nativeWorktreeAdd(basePath, wtPath, branch, true, startPoint);
138
141
  }
139
142
 
140
143
  return {
@@ -76,6 +76,28 @@ export function detectWorktreeName(basePath: string): string | null {
76
76
  return name || null;
77
77
  }
78
78
 
79
+ /**
80
+ * Resolve the project root from a path that may be inside a worktree.
81
+ * If the path contains `/.gsd/worktrees/<name>/`, returns the portion
82
+ * before `/.gsd/`. Otherwise returns the input unchanged.
83
+ *
84
+ * Use this in commands that call `process.cwd()` to ensure they always
85
+ * operate against the real project root, not a worktree subdirectory.
86
+ */
87
+ export function resolveProjectRoot(basePath: string): string {
88
+ const normalizedPath = basePath.replaceAll("\\", "/");
89
+ const marker = "/.gsd/worktrees/";
90
+ const idx = normalizedPath.indexOf(marker);
91
+ if (idx === -1) return basePath;
92
+ // Return the original path up to the .gsd/ marker (un-normalized)
93
+ // Account for potential OS-specific separators
94
+ const sep = basePath.includes("\\") ? "\\" : "/";
95
+ const markerOs = `${sep}.gsd${sep}worktrees${sep}`;
96
+ const idxOs = basePath.indexOf(markerOs);
97
+ if (idxOs !== -1) return basePath.slice(0, idxOs);
98
+ return basePath.slice(0, idx);
99
+ }
100
+
79
101
  /**
80
102
  * Get the slice branch name, namespaced by worktree when inside one.
81
103
  *