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.
- package/README.md +39 -0
- package/dist/onboarding.js +2 -2
- package/dist/remote-questions-config.d.ts +10 -0
- package/dist/remote-questions-config.js +36 -0
- package/dist/resources/extensions/gsd/activity-log.ts +37 -7
- package/dist/resources/extensions/gsd/auto-dashboard.ts +14 -2
- package/dist/resources/extensions/gsd/auto-prompts.ts +65 -16
- package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/dist/resources/extensions/gsd/auto.ts +399 -29
- package/dist/resources/extensions/gsd/captures.ts +384 -0
- package/dist/resources/extensions/gsd/commands.ts +382 -23
- package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +10 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +201 -2
- package/dist/resources/extensions/gsd/files.ts +123 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +237 -4
- package/dist/resources/extensions/gsd/index.ts +47 -3
- package/dist/resources/extensions/gsd/metrics.ts +48 -0
- package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
- package/dist/resources/extensions/gsd/model-router.ts +256 -0
- package/dist/resources/extensions/gsd/paths.ts +9 -0
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/dist/resources/extensions/gsd/preferences.ts +132 -1
- package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
- package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
- package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
- package/dist/resources/extensions/gsd/prompts/system.md +2 -0
- package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
- package/dist/resources/extensions/gsd/queue-order.ts +231 -0
- package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/dist/resources/extensions/gsd/state.ts +15 -3
- package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
- package/dist/resources/extensions/gsd/tests/captures.test.ts +438 -0
- package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
- package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
- package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
- package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
- package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -0
- package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
- package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
- package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
- package/dist/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
- package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
- package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
- package/dist/resources/extensions/gsd/triage-resolution.ts +200 -0
- package/dist/resources/extensions/gsd/triage-ui.ts +175 -0
- package/dist/resources/extensions/gsd/visualizer-data.ts +154 -0
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +193 -0
- package/dist/resources/extensions/gsd/visualizer-views.ts +293 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/dist/resources/extensions/gsd/worktree.ts +22 -0
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +33 -0
- package/dist/resources/extensions/remote-questions/format.ts +12 -6
- package/dist/resources/extensions/remote-questions/manager.ts +8 -0
- package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
- package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +21 -0
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
- package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
- package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +5 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +4 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +17 -2
- package/packages/pi-coding-agent/dist/main.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +32 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/cli/args.ts +21 -0
- package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
- package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
- package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
- package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
- package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
- package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
- package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
- package/packages/pi-coding-agent/src/index.ts +5 -0
- package/packages/pi-coding-agent/src/main.ts +19 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
- package/src/resources/extensions/gsd/activity-log.ts +37 -7
- package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +65 -16
- package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/src/resources/extensions/gsd/auto.ts +399 -29
- package/src/resources/extensions/gsd/captures.ts +384 -0
- package/src/resources/extensions/gsd/commands.ts +382 -23
- package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/src/resources/extensions/gsd/docs/preferences-reference.md +201 -2
- package/src/resources/extensions/gsd/files.ts +123 -1
- package/src/resources/extensions/gsd/guided-flow.ts +237 -4
- package/src/resources/extensions/gsd/index.ts +47 -3
- package/src/resources/extensions/gsd/metrics.ts +48 -0
- package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
- package/src/resources/extensions/gsd/model-router.ts +256 -0
- package/src/resources/extensions/gsd/paths.ts +9 -0
- package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/src/resources/extensions/gsd/preferences.ts +132 -1
- package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
- package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
- package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
- package/src/resources/extensions/gsd/prompts/system.md +2 -0
- package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
- package/src/resources/extensions/gsd/queue-order.ts +231 -0
- package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/src/resources/extensions/gsd/state.ts +15 -3
- package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/src/resources/extensions/gsd/templates/preferences.md +14 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/captures.test.ts +438 -0
- package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
- package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
- package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
- package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -0
- package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
- package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
- package/src/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
- package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
- package/src/resources/extensions/gsd/triage-resolution.ts +200 -0
- package/src/resources/extensions/gsd/triage-ui.ts +175 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +154 -0
- package/src/resources/extensions/gsd/visualizer-overlay.ts +193 -0
- package/src/resources/extensions/gsd/visualizer-views.ts +293 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/src/resources/extensions/gsd/worktree.ts +22 -0
- package/src/resources/extensions/remote-questions/discord-adapter.ts +33 -0
- package/src/resources/extensions/remote-questions/format.ts +12 -6
- package/src/resources/extensions/remote-questions/manager.ts +8 -0
- 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
|
|
134
|
-
nativeBranchForceReset(basePath, branch,
|
|
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,
|
|
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
|
*
|