gsd-pi 2.26.0 → 2.27.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 +43 -6
- package/dist/cli.js +4 -2
- package/dist/headless.d.ts +3 -0
- package/dist/headless.js +136 -8
- package/dist/help-text.js +3 -0
- package/dist/loader.js +33 -4
- package/dist/resources/extensions/bg-shell/index.ts +19 -2
- package/dist/resources/extensions/bg-shell/process-manager.ts +45 -0
- package/dist/resources/extensions/bg-shell/types.ts +21 -1
- package/dist/resources/extensions/gsd/auto/session.ts +224 -0
- package/dist/resources/extensions/gsd/auto-budget.ts +32 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +63 -10
- package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
- package/dist/resources/extensions/gsd/auto-dispatch.ts +23 -10
- package/dist/resources/extensions/gsd/auto-model-selection.ts +179 -0
- package/dist/resources/extensions/gsd/auto-observability.ts +74 -0
- package/dist/resources/extensions/gsd/auto-prompts.ts +0 -1
- package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
- package/dist/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
- package/dist/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
- package/dist/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
- package/dist/resources/extensions/gsd/auto.ts +977 -1551
- package/dist/resources/extensions/gsd/commands.ts +3 -3
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +47 -72
- package/dist/resources/extensions/gsd/doctor-proactive.ts +9 -4
- package/dist/resources/extensions/gsd/export-html.ts +1001 -0
- package/dist/resources/extensions/gsd/export.ts +49 -1
- package/dist/resources/extensions/gsd/git-service.ts +6 -0
- package/dist/resources/extensions/gsd/gitignore.ts +4 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +24 -5
- package/dist/resources/extensions/gsd/index.ts +54 -1
- package/dist/resources/extensions/gsd/native-git-bridge.ts +30 -2
- package/dist/resources/extensions/gsd/observability-validator.ts +21 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
- package/dist/resources/extensions/gsd/preferences.ts +62 -1
- package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -3
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/reports.ts +510 -0
- package/dist/resources/extensions/gsd/roadmap-slices.ts +1 -1
- package/dist/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
- package/dist/resources/extensions/gsd/state.ts +30 -0
- package/dist/resources/extensions/gsd/templates/task-summary.md +9 -0
- package/dist/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
- package/dist/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
- package/dist/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
- package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
- package/dist/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
- package/dist/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
- package/dist/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
- package/dist/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
- package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
- package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
- package/dist/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
- package/dist/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
- package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
- package/dist/resources/extensions/gsd/tests/worktree.test.ts +3 -1
- package/dist/resources/extensions/gsd/types.ts +38 -0
- package/dist/resources/extensions/gsd/verification-evidence.ts +183 -0
- package/dist/resources/extensions/gsd/verification-gate.ts +567 -0
- package/dist/resources/extensions/gsd/visualizer-data.ts +25 -3
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +31 -21
- package/dist/resources/extensions/gsd/visualizer-views.ts +15 -66
- package/dist/resources/extensions/search-the-web/tool-search.ts +26 -0
- package/dist/resources/extensions/shared/format-utils.ts +85 -0
- package/dist/resources/extensions/shared/tests/format-utils.test.ts +153 -0
- package/dist/resources/extensions/subagent/index.ts +46 -1
- package/dist/resources/extensions/subagent/isolation.ts +9 -6
- package/package.json +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +7 -4
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/src/providers/openai-completions.ts +7 -4
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +7 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.js +9 -2
- package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
- package/packages/pi-coding-agent/src/core/lsp/client.ts +8 -0
- package/packages/pi-coding-agent/src/core/lsp/config.ts +9 -2
- package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/editor.js +1 -1
- package/packages/pi-tui/dist/components/editor.js.map +1 -1
- package/packages/pi-tui/src/components/editor.ts +3 -1
- package/scripts/link-workspace-packages.cjs +22 -6
- package/src/resources/extensions/bg-shell/index.ts +19 -2
- package/src/resources/extensions/bg-shell/process-manager.ts +45 -0
- package/src/resources/extensions/bg-shell/types.ts +21 -1
- package/src/resources/extensions/gsd/auto/session.ts +224 -0
- package/src/resources/extensions/gsd/auto-budget.ts +32 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +63 -10
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +23 -10
- package/src/resources/extensions/gsd/auto-model-selection.ts +179 -0
- package/src/resources/extensions/gsd/auto-observability.ts +74 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +0 -1
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
- package/src/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
- package/src/resources/extensions/gsd/auto.ts +977 -1551
- package/src/resources/extensions/gsd/commands.ts +3 -3
- package/src/resources/extensions/gsd/dashboard-overlay.ts +47 -72
- package/src/resources/extensions/gsd/doctor-proactive.ts +9 -4
- package/src/resources/extensions/gsd/export-html.ts +1001 -0
- package/src/resources/extensions/gsd/export.ts +49 -1
- package/src/resources/extensions/gsd/git-service.ts +6 -0
- package/src/resources/extensions/gsd/gitignore.ts +4 -1
- package/src/resources/extensions/gsd/guided-flow.ts +24 -5
- package/src/resources/extensions/gsd/index.ts +54 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +30 -2
- package/src/resources/extensions/gsd/observability-validator.ts +21 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
- package/src/resources/extensions/gsd/preferences.ts +62 -1
- package/src/resources/extensions/gsd/prompts/execute-task.md +4 -3
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/reports.ts +510 -0
- package/src/resources/extensions/gsd/roadmap-slices.ts +1 -1
- package/src/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
- package/src/resources/extensions/gsd/state.ts +30 -0
- package/src/resources/extensions/gsd/templates/task-summary.md +9 -0
- package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
- package/src/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
- package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
- package/src/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
- package/src/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
- package/src/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
- package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
- package/src/resources/extensions/gsd/tests/worktree.test.ts +3 -1
- package/src/resources/extensions/gsd/types.ts +38 -0
- package/src/resources/extensions/gsd/verification-evidence.ts +183 -0
- package/src/resources/extensions/gsd/verification-gate.ts +567 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +25 -3
- package/src/resources/extensions/gsd/visualizer-overlay.ts +31 -21
- package/src/resources/extensions/gsd/visualizer-views.ts +15 -66
- package/src/resources/extensions/search-the-web/tool-search.ts +26 -0
- package/src/resources/extensions/shared/format-utils.ts +85 -0
- package/src/resources/extensions/shared/tests/format-utils.test.ts +153 -0
- package/src/resources/extensions/subagent/index.ts +46 -1
- package/src/resources/extensions/subagent/isolation.ts +9 -6
|
@@ -15,25 +15,22 @@ import {
|
|
|
15
15
|
type ProgressFilter,
|
|
16
16
|
} from "./visualizer-views.js";
|
|
17
17
|
import { writeExportFile } from "./export.js";
|
|
18
|
+
import { stripAnsi } from "../shared/format-utils.js";
|
|
18
19
|
|
|
19
20
|
const TAB_COUNT = 10;
|
|
20
21
|
const TAB_LABELS = [
|
|
21
22
|
"1 Progress",
|
|
22
|
-
"2
|
|
23
|
-
"3
|
|
24
|
-
"4
|
|
25
|
-
"5
|
|
26
|
-
"6
|
|
27
|
-
"7
|
|
23
|
+
"2 Timeline",
|
|
24
|
+
"3 Deps",
|
|
25
|
+
"4 Metrics",
|
|
26
|
+
"5 Health",
|
|
27
|
+
"6 Agent",
|
|
28
|
+
"7 Changes",
|
|
28
29
|
"8 Knowledge",
|
|
29
30
|
"9 Captures",
|
|
30
|
-
"0
|
|
31
|
+
"0 Export",
|
|
31
32
|
];
|
|
32
33
|
|
|
33
|
-
function stripAnsi(s: string): string {
|
|
34
|
-
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
35
|
-
}
|
|
36
|
-
|
|
37
34
|
export class GSDVisualizerOverlay {
|
|
38
35
|
private tui: { requestRender: () => void };
|
|
39
36
|
private theme: Theme;
|
|
@@ -62,6 +59,7 @@ export class GSDVisualizerOverlay {
|
|
|
62
59
|
private lastVisibleRows = 20;
|
|
63
60
|
collapsedMilestones = new Set<string>();
|
|
64
61
|
showHelp = false;
|
|
62
|
+
private resizeHandler: (() => void) | null = null;
|
|
65
63
|
|
|
66
64
|
constructor(
|
|
67
65
|
tui: { requestRender: () => void },
|
|
@@ -76,6 +74,14 @@ export class GSDVisualizerOverlay {
|
|
|
76
74
|
// Enable SGR mouse tracking
|
|
77
75
|
process.stdout.write("\x1b[?1003h\x1b[?1006h");
|
|
78
76
|
|
|
77
|
+
// Invalidate cache on terminal resize
|
|
78
|
+
this.resizeHandler = () => {
|
|
79
|
+
if (this.disposed) return;
|
|
80
|
+
this.invalidate();
|
|
81
|
+
this.tui.requestRender();
|
|
82
|
+
};
|
|
83
|
+
process.stdout.on("resize", this.resizeHandler);
|
|
84
|
+
|
|
79
85
|
loadVisualizerData(this.basePath).then((d) => {
|
|
80
86
|
this.data = d;
|
|
81
87
|
this.loading = false;
|
|
@@ -89,7 +95,7 @@ export class GSDVisualizerOverlay {
|
|
|
89
95
|
this.invalidate();
|
|
90
96
|
this.tui.requestRender();
|
|
91
97
|
});
|
|
92
|
-
},
|
|
98
|
+
}, 5000);
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
private parseSGRMouse(data: string): { button: number; x: number; y: number; press: boolean } | null {
|
|
@@ -262,7 +268,7 @@ export class GSDVisualizerOverlay {
|
|
|
262
268
|
}
|
|
263
269
|
|
|
264
270
|
// Export tab key handling
|
|
265
|
-
if (this.activeTab ===
|
|
271
|
+
if (this.activeTab === 9 && this.data) {
|
|
266
272
|
if (data === "m" || data === "j" || data === "s") {
|
|
267
273
|
this.handleExportKey(data);
|
|
268
274
|
return;
|
|
@@ -372,23 +378,23 @@ export class GSDVisualizerOverlay {
|
|
|
372
378
|
return renderProgressView(this.data, th, width, filter, this.collapsedMilestones);
|
|
373
379
|
}
|
|
374
380
|
case 1:
|
|
375
|
-
return
|
|
381
|
+
return renderTimelineView(this.data, th, width);
|
|
376
382
|
case 2:
|
|
377
|
-
return
|
|
383
|
+
return renderDepsView(this.data, th, width);
|
|
378
384
|
case 3:
|
|
379
|
-
return
|
|
385
|
+
return renderMetricsView(this.data, th, width);
|
|
380
386
|
case 4:
|
|
381
|
-
return
|
|
387
|
+
return renderHealthView(this.data, th, width);
|
|
382
388
|
case 5:
|
|
383
|
-
return
|
|
389
|
+
return renderAgentView(this.data, th, width);
|
|
384
390
|
case 6:
|
|
385
|
-
return
|
|
391
|
+
return renderChangelogView(this.data, th, width);
|
|
386
392
|
case 7:
|
|
387
393
|
return renderKnowledgeView(this.data, th, width);
|
|
388
394
|
case 8:
|
|
389
395
|
return renderCapturesView(this.data, th, width);
|
|
390
396
|
case 9:
|
|
391
|
-
return
|
|
397
|
+
return renderExportView(this.data, th, width, this.lastExportPath);
|
|
392
398
|
default:
|
|
393
399
|
return [];
|
|
394
400
|
}
|
|
@@ -470,7 +476,7 @@ export class GSDVisualizerOverlay {
|
|
|
470
476
|
let viewLines = this.renderTabContent(this.activeTab, innerWidth);
|
|
471
477
|
|
|
472
478
|
// Show export status message if present
|
|
473
|
-
if (this.exportStatus && this.activeTab ===
|
|
479
|
+
if (this.exportStatus && this.activeTab === 9) {
|
|
474
480
|
content.push(th.fg("success", this.exportStatus));
|
|
475
481
|
content.push("");
|
|
476
482
|
this.exportStatus = undefined;
|
|
@@ -547,6 +553,10 @@ export class GSDVisualizerOverlay {
|
|
|
547
553
|
dispose(): void {
|
|
548
554
|
this.disposed = true;
|
|
549
555
|
clearInterval(this.refreshTimer);
|
|
556
|
+
if (this.resizeHandler) {
|
|
557
|
+
process.stdout.removeListener("resize", this.resizeHandler);
|
|
558
|
+
this.resizeHandler = null;
|
|
559
|
+
}
|
|
550
560
|
// Disable SGR mouse tracking
|
|
551
561
|
process.stdout.write("\x1b[?1003l\x1b[?1006l");
|
|
552
562
|
}
|
|
@@ -4,41 +4,8 @@ import type { Theme } from "@gsd/pi-coding-agent";
|
|
|
4
4
|
import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
|
|
5
5
|
import type { VisualizerData, VisualizerMilestone, SliceVerification, VisualizerSliceActivity, VisualizerStats, VisualizerSliceRef } from "./visualizer-data.js";
|
|
6
6
|
import { formatCost, formatTokenCount, classifyUnitPhase } from "./metrics.js";
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
function sparkline(values: number[]): string {
|
|
36
|
-
if (values.length === 0) return "";
|
|
37
|
-
const chars = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
38
|
-
const max = Math.max(...values);
|
|
39
|
-
if (max === 0) return chars[0].repeat(values.length);
|
|
40
|
-
return values.map(v => chars[Math.min(7, Math.floor((v / max) * 7))]).join("");
|
|
41
|
-
}
|
|
7
|
+
import { formatDuration, padRight, joinColumns, sparkline } from "../shared/format-utils.js";
|
|
8
|
+
import { STATUS_GLYPH, STATUS_COLOR } from "../shared/ui.js";
|
|
42
9
|
|
|
43
10
|
function formatCompletionDate(input: string): string {
|
|
44
11
|
if (!input) return "unknown";
|
|
@@ -168,18 +135,9 @@ export function renderProgressView(
|
|
|
168
135
|
}
|
|
169
136
|
|
|
170
137
|
// Milestone header line
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
: ms.status === "active"
|
|
175
|
-
? th.fg("accent", "\u25b8")
|
|
176
|
-
: th.fg("dim", "\u25cb");
|
|
177
|
-
const statusLabel =
|
|
178
|
-
ms.status === "complete"
|
|
179
|
-
? th.fg("success", "complete")
|
|
180
|
-
: ms.status === "active"
|
|
181
|
-
? th.fg("accent", "active")
|
|
182
|
-
: th.fg("dim", "pending");
|
|
138
|
+
const msStatus = ms.status === "complete" ? "done" : ms.status === "active" ? "active" : "pending";
|
|
139
|
+
const statusGlyph = th.fg(STATUS_COLOR[msStatus], STATUS_GLYPH[msStatus]);
|
|
140
|
+
const statusLabel = th.fg(STATUS_COLOR[msStatus], ms.status);
|
|
183
141
|
|
|
184
142
|
const collapseIndicator = collapsed?.has(ms.id) ? "[+] " : "";
|
|
185
143
|
const msLeft = `${collapseIndicator}${ms.id}: ${ms.title}`;
|
|
@@ -206,11 +164,8 @@ export function renderProgressView(
|
|
|
206
164
|
}
|
|
207
165
|
|
|
208
166
|
// Slice line
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
: sl.active
|
|
212
|
-
? th.fg("accent", "\u25b8")
|
|
213
|
-
: th.fg("dim", "\u25cb");
|
|
167
|
+
const slStatus = sl.done ? "done" : sl.active ? "active" : "pending";
|
|
168
|
+
const slGlyph = th.fg(STATUS_COLOR[slStatus], STATUS_GLYPH[slStatus]);
|
|
214
169
|
const riskColor =
|
|
215
170
|
sl.risk === "high"
|
|
216
171
|
? "warning"
|
|
@@ -241,11 +196,8 @@ export function renderProgressView(
|
|
|
241
196
|
// Show tasks for active slice
|
|
242
197
|
if (sl.active && sl.tasks.length > 0) {
|
|
243
198
|
for (const task of sl.tasks) {
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
: task.active
|
|
247
|
-
? th.fg("accent", "\u25b8")
|
|
248
|
-
: th.fg("dim", "\u25cb");
|
|
199
|
+
const tStatus = task.done ? "done" : task.active ? "active" : "pending";
|
|
200
|
+
const tGlyph = th.fg(STATUS_COLOR[tStatus], STATUS_GLYPH[tStatus]);
|
|
249
201
|
const estimateStr = task.estimate ? th.fg("dim", ` (${task.estimate})`) : "";
|
|
250
202
|
lines.push(` ${tGlyph} ${task.id}: ${task.title}${estimateStr}`);
|
|
251
203
|
}
|
|
@@ -683,10 +635,8 @@ function renderTimelineList(data: VisualizerData, th: Theme, width: number): str
|
|
|
683
635
|
const time = `${hh}:${mm}`;
|
|
684
636
|
|
|
685
637
|
const duration = unit.finishedAt - unit.startedAt;
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
? th.fg("success", "\u2713")
|
|
689
|
-
: th.fg("accent", "\u25b8");
|
|
638
|
+
const unitStatus = unit.finishedAt > 0 ? "done" : "active";
|
|
639
|
+
const glyph = th.fg(STATUS_COLOR[unitStatus], STATUS_GLYPH[unitStatus]);
|
|
690
640
|
|
|
691
641
|
const typeLabel = padRight(unit.type, 16);
|
|
692
642
|
const idLabel = padRight(unit.id, 14);
|
|
@@ -802,9 +752,8 @@ export function renderAgentView(
|
|
|
802
752
|
}
|
|
803
753
|
|
|
804
754
|
// Status line
|
|
805
|
-
const
|
|
806
|
-
|
|
807
|
-
: th.fg("dim", "\u25cb");
|
|
755
|
+
const agentStatus = activity.active ? "active" : "pending";
|
|
756
|
+
const statusDot = th.fg(STATUS_COLOR[agentStatus], STATUS_GLYPH[agentStatus]);
|
|
808
757
|
const statusText = activity.active ? "ACTIVE" : "IDLE";
|
|
809
758
|
const elapsedStr = activity.active ? formatDuration(activity.elapsed) : "\u2014";
|
|
810
759
|
|
|
@@ -877,7 +826,7 @@ export function renderAgentView(
|
|
|
877
826
|
const typeLabel = padRight(u.type, 16);
|
|
878
827
|
lines.push(
|
|
879
828
|
truncateToWidth(
|
|
880
|
-
` ${hh}:${mm} ${th.fg(
|
|
829
|
+
` ${hh}:${mm} ${th.fg(STATUS_COLOR.done, STATUS_GLYPH.done)} ${typeLabel} ${padRight(u.id, 16)} ${dur} ${cost}`,
|
|
881
830
|
width,
|
|
882
831
|
),
|
|
883
832
|
);
|
|
@@ -920,7 +869,7 @@ export function renderChangelogView(
|
|
|
920
869
|
for (const f of entry.filesModified) {
|
|
921
870
|
lines.push(
|
|
922
871
|
truncateToWidth(
|
|
923
|
-
` ${th.fg(
|
|
872
|
+
` ${th.fg(STATUS_COLOR.done, STATUS_GLYPH.done)} ${f.path} \u2014 ${f.description}`,
|
|
924
873
|
width,
|
|
925
874
|
),
|
|
926
875
|
);
|
|
@@ -104,6 +104,12 @@ interface SearchDetails {
|
|
|
104
104
|
const searchCache = new LRUTTLCache<CachedSearchResult>({ max: 100, ttlMs: 600_000 });
|
|
105
105
|
searchCache.startPurgeInterval(60_000);
|
|
106
106
|
|
|
107
|
+
// Consecutive duplicate search guard (#949)
|
|
108
|
+
// Tracks recent query keys to detect and break search loops.
|
|
109
|
+
const MAX_CONSECUTIVE_DUPES = 3;
|
|
110
|
+
let lastSearchKey = "";
|
|
111
|
+
let consecutiveDupeCount = 0;
|
|
112
|
+
|
|
107
113
|
// Summarizer responses: max 50 entries, 15-minute TTL
|
|
108
114
|
const summarizerCache = new LRUTTLCache<string>({ max: 50, ttlMs: 900_000 });
|
|
109
115
|
|
|
@@ -388,6 +394,26 @@ export function registerSearchTool(pi: ExtensionAPI) {
|
|
|
388
394
|
// Cache lookup (provider-prefixed key)
|
|
389
395
|
// ------------------------------------------------------------------
|
|
390
396
|
const cacheKey = normalizeQuery(effectiveQuery) + `|f:${freshness || ""}|s:${wantSummary}|p:${provider}`;
|
|
397
|
+
|
|
398
|
+
// ── Consecutive duplicate search guard (#949) ──────────────────────
|
|
399
|
+
// If the LLM keeps calling the same search query, break the loop
|
|
400
|
+
// with an explicit warning instead of returning the same results.
|
|
401
|
+
if (cacheKey === lastSearchKey) {
|
|
402
|
+
consecutiveDupeCount++;
|
|
403
|
+
if (consecutiveDupeCount >= MAX_CONSECUTIVE_DUPES) {
|
|
404
|
+
consecutiveDupeCount = 0;
|
|
405
|
+
lastSearchKey = "";
|
|
406
|
+
return {
|
|
407
|
+
content: [{ type: "text" as const, text: `⚠️ Search loop detected: the query "${params.query}" has been searched ${MAX_CONSECUTIVE_DUPES + 1} times consecutively with identical results. The information you need is already in the previous search results above. Stop searching and use those results to proceed with your task.` }],
|
|
408
|
+
isError: true,
|
|
409
|
+
details: { errorKind: "search_loop", error: "Consecutive duplicate search detected" } satisfies Partial<SearchDetails>,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
lastSearchKey = cacheKey;
|
|
414
|
+
consecutiveDupeCount = 0;
|
|
415
|
+
}
|
|
416
|
+
|
|
391
417
|
const cached = searchCache.get(cacheKey);
|
|
392
418
|
|
|
393
419
|
if (cached) {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared formatting and layout utilities for TUI dashboard components.
|
|
3
|
+
*
|
|
4
|
+
* Consolidates helpers that were previously duplicated across
|
|
5
|
+
* auto-dashboard.ts, dashboard-overlay.ts, and visualizer-views.ts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
|
|
9
|
+
|
|
10
|
+
// ─── Duration Formatting ──────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
/** Format a millisecond duration as a compact human-readable string. */
|
|
13
|
+
export function formatDuration(ms: number): string {
|
|
14
|
+
const s = Math.floor(ms / 1000);
|
|
15
|
+
if (s < 60) return `${s}s`;
|
|
16
|
+
const m = Math.floor(s / 60);
|
|
17
|
+
const rs = s % 60;
|
|
18
|
+
if (m < 60) return `${m}m ${rs}s`;
|
|
19
|
+
const h = Math.floor(m / 60);
|
|
20
|
+
const rm = m % 60;
|
|
21
|
+
return `${h}h ${rm}m`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── Layout Helpers ───────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/** Pad a string with trailing spaces to fill `width` (ANSI-aware). */
|
|
27
|
+
export function padRight(content: string, width: number): string {
|
|
28
|
+
const vis = visibleWidth(content);
|
|
29
|
+
return content + " ".repeat(Math.max(0, width - vis));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Build a line with left-aligned and right-aligned content. */
|
|
33
|
+
export function joinColumns(left: string, right: string, width: number): string {
|
|
34
|
+
const leftW = visibleWidth(left);
|
|
35
|
+
const rightW = visibleWidth(right);
|
|
36
|
+
if (leftW + rightW + 2 > width) {
|
|
37
|
+
return truncateToWidth(`${left} ${right}`, width);
|
|
38
|
+
}
|
|
39
|
+
return left + " ".repeat(width - leftW - rightW) + right;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Center content within `width` (ANSI-aware). */
|
|
43
|
+
export function centerLine(content: string, width: number): string {
|
|
44
|
+
const vis = visibleWidth(content);
|
|
45
|
+
if (vis >= width) return truncateToWidth(content, width);
|
|
46
|
+
const leftPad = Math.floor((width - vis) / 2);
|
|
47
|
+
return " ".repeat(leftPad) + content;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Join as many parts as fit within `width`, separated by `separator`. */
|
|
51
|
+
export function fitColumns(parts: string[], width: number, separator = " "): string {
|
|
52
|
+
const filtered = parts.filter(Boolean);
|
|
53
|
+
if (filtered.length === 0) return "";
|
|
54
|
+
let result = filtered[0];
|
|
55
|
+
for (let i = 1; i < filtered.length; i++) {
|
|
56
|
+
const candidate = `${result}${separator}${filtered[i]}`;
|
|
57
|
+
if (visibleWidth(candidate) > width) break;
|
|
58
|
+
result = candidate;
|
|
59
|
+
}
|
|
60
|
+
return truncateToWidth(result, width);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Data Visualization ───────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Render a sparkline from numeric values using Unicode block characters.
|
|
67
|
+
* Uses loop-based max to avoid stack overflow on large arrays.
|
|
68
|
+
*/
|
|
69
|
+
export function sparkline(values: number[]): string {
|
|
70
|
+
if (values.length === 0) return "";
|
|
71
|
+
const chars = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
72
|
+
let max = 0;
|
|
73
|
+
for (const v of values) {
|
|
74
|
+
if (v > max) max = v;
|
|
75
|
+
}
|
|
76
|
+
if (max === 0) return chars[0].repeat(values.length);
|
|
77
|
+
return values.map(v => chars[Math.min(7, Math.floor((v / max) * 7))]).join("");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── ANSI Stripping ───────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/** Strip ANSI escape sequences from a string. */
|
|
83
|
+
export function stripAnsi(s: string): string {
|
|
84
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
85
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
formatDuration,
|
|
5
|
+
padRight,
|
|
6
|
+
joinColumns,
|
|
7
|
+
centerLine,
|
|
8
|
+
fitColumns,
|
|
9
|
+
sparkline,
|
|
10
|
+
stripAnsi,
|
|
11
|
+
} from "../format-utils.js";
|
|
12
|
+
|
|
13
|
+
describe("formatDuration", () => {
|
|
14
|
+
it("formats seconds", () => {
|
|
15
|
+
assert.equal(formatDuration(0), "0s");
|
|
16
|
+
assert.equal(formatDuration(5_000), "5s");
|
|
17
|
+
assert.equal(formatDuration(59_000), "59s");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("formats minutes and seconds", () => {
|
|
21
|
+
assert.equal(formatDuration(60_000), "1m 0s");
|
|
22
|
+
assert.equal(formatDuration(90_000), "1m 30s");
|
|
23
|
+
assert.equal(formatDuration(3_540_000), "59m 0s");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("formats hours and minutes", () => {
|
|
27
|
+
assert.equal(formatDuration(3_600_000), "1h 0m");
|
|
28
|
+
assert.equal(formatDuration(5_400_000), "1h 30m");
|
|
29
|
+
assert.equal(formatDuration(7_200_000), "2h 0m");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("padRight", () => {
|
|
34
|
+
it("pads plain text to width", () => {
|
|
35
|
+
const result = padRight("abc", 6);
|
|
36
|
+
assert.equal(result, "abc ");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("does not pad when text fills width", () => {
|
|
40
|
+
const result = padRight("abcdef", 6);
|
|
41
|
+
assert.equal(result, "abcdef");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("does not pad when text exceeds width", () => {
|
|
45
|
+
const result = padRight("abcdefgh", 6);
|
|
46
|
+
assert.equal(result, "abcdefgh");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("joinColumns", () => {
|
|
51
|
+
it("joins left and right with spacing", () => {
|
|
52
|
+
const result = joinColumns("left", "right", 20);
|
|
53
|
+
assert.equal(result.length, 20);
|
|
54
|
+
assert.ok(result.startsWith("left"));
|
|
55
|
+
assert.ok(result.endsWith("right"));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("truncates when content overflows", () => {
|
|
59
|
+
const result = joinColumns("a".repeat(20), "b".repeat(20), 30);
|
|
60
|
+
// Should be truncated to 30 chars
|
|
61
|
+
assert.ok(result.length <= 30);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("centerLine", () => {
|
|
66
|
+
it("centers text within width", () => {
|
|
67
|
+
const result = centerLine("hi", 10);
|
|
68
|
+
assert.equal(result, " hi");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("truncates when content exceeds width", () => {
|
|
72
|
+
const result = centerLine("abcdefgh", 4);
|
|
73
|
+
assert.ok(result.length <= 4);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("fitColumns", () => {
|
|
78
|
+
it("joins parts that fit", () => {
|
|
79
|
+
const result = fitColumns(["aaa", "bbb", "ccc"], 20);
|
|
80
|
+
assert.ok(result.includes("aaa"));
|
|
81
|
+
assert.ok(result.includes("bbb"));
|
|
82
|
+
assert.ok(result.includes("ccc"));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("drops parts that overflow", () => {
|
|
86
|
+
const result = fitColumns(["aaa", "bbb", "ccc"], 10);
|
|
87
|
+
assert.ok(result.includes("aaa"));
|
|
88
|
+
// May or may not include bbb depending on separator width
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("returns empty string for empty array", () => {
|
|
92
|
+
assert.equal(fitColumns([], 80), "");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("filters out empty strings", () => {
|
|
96
|
+
const result = fitColumns(["aaa", "", "bbb"], 80);
|
|
97
|
+
assert.ok(result.includes("aaa"));
|
|
98
|
+
assert.ok(result.includes("bbb"));
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("sparkline", () => {
|
|
103
|
+
it("returns empty string for empty array", () => {
|
|
104
|
+
assert.equal(sparkline([]), "");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("renders all lowest blocks for all-zero values", () => {
|
|
108
|
+
const result = sparkline([0, 0, 0]);
|
|
109
|
+
assert.equal(result.length, 3);
|
|
110
|
+
// All chars should be the same (lowest block)
|
|
111
|
+
assert.equal(result[0], result[1]);
|
|
112
|
+
assert.equal(result[1], result[2]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("renders highest block for max value", () => {
|
|
116
|
+
const result = sparkline([0, 10, 5]);
|
|
117
|
+
assert.equal(result.length, 3);
|
|
118
|
+
// Middle should be highest block (█)
|
|
119
|
+
assert.equal(result[1], "\u2588");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("handles single value", () => {
|
|
123
|
+
const result = sparkline([42]);
|
|
124
|
+
assert.equal(result.length, 1);
|
|
125
|
+
assert.equal(result, "\u2588");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("handles large arrays without stack overflow", () => {
|
|
129
|
+
const largeArray = new Array(100_000).fill(0).map((_, i) => i);
|
|
130
|
+
const result = sparkline(largeArray);
|
|
131
|
+
assert.equal(result.length, 100_000);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("stripAnsi", () => {
|
|
136
|
+
it("strips ANSI escape sequences", () => {
|
|
137
|
+
const result = stripAnsi("\x1b[31mred\x1b[0m text");
|
|
138
|
+
assert.equal(result, "red text");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("returns plain text unchanged", () => {
|
|
142
|
+
assert.equal(stripAnsi("plain text"), "plain text");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("strips multiple escape sequences", () => {
|
|
146
|
+
const result = stripAnsi("\x1b[1m\x1b[32mbold green\x1b[0m");
|
|
147
|
+
assert.equal(result, "bold green");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("handles empty string", () => {
|
|
151
|
+
assert.equal(stripAnsi(""), "");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Uses JSON mode to capture structured output from subagents.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { spawn } from "node:child_process";
|
|
15
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
16
16
|
import * as crypto from "node:crypto";
|
|
17
17
|
import * as fs from "node:fs";
|
|
18
18
|
import * as os from "node:os";
|
|
@@ -38,6 +38,44 @@ import { registerWorker, updateWorker } from "./worker-registry.js";
|
|
|
38
38
|
const MAX_PARALLEL_TASKS = 8;
|
|
39
39
|
const MAX_CONCURRENCY = 4;
|
|
40
40
|
const COLLAPSED_ITEM_COUNT = 10;
|
|
41
|
+
const liveSubagentProcesses = new Set<ChildProcess>();
|
|
42
|
+
|
|
43
|
+
async function stopLiveSubagents(): Promise<void> {
|
|
44
|
+
const active = Array.from(liveSubagentProcesses);
|
|
45
|
+
if (active.length === 0) return;
|
|
46
|
+
|
|
47
|
+
for (const proc of active) {
|
|
48
|
+
try {
|
|
49
|
+
proc.kill("SIGTERM");
|
|
50
|
+
} catch {
|
|
51
|
+
/* ignore */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await Promise.all(
|
|
56
|
+
active.map(
|
|
57
|
+
(proc) =>
|
|
58
|
+
new Promise<void>((resolve) => {
|
|
59
|
+
const done = () => resolve();
|
|
60
|
+
const timer = setTimeout(done, 500);
|
|
61
|
+
proc.once("exit", () => {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
resolve();
|
|
64
|
+
});
|
|
65
|
+
}),
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
for (const proc of active) {
|
|
70
|
+
if (proc.exitCode === null) {
|
|
71
|
+
try {
|
|
72
|
+
proc.kill("SIGKILL");
|
|
73
|
+
} catch {
|
|
74
|
+
/* ignore */
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
41
79
|
|
|
42
80
|
function formatTokens(count: number): string {
|
|
43
81
|
if (count < 1000) return count.toString();
|
|
@@ -302,6 +340,7 @@ async function runSingleAgent(
|
|
|
302
340
|
[process.env.GSD_BIN_PATH!, ...extensionArgs, ...args],
|
|
303
341
|
{ cwd: cwd ?? defaultCwd, shell: false, stdio: ["ignore", "pipe", "pipe"] },
|
|
304
342
|
);
|
|
343
|
+
liveSubagentProcesses.add(proc);
|
|
305
344
|
let buffer = "";
|
|
306
345
|
|
|
307
346
|
const processLine = (line: string) => {
|
|
@@ -353,11 +392,13 @@ async function runSingleAgent(
|
|
|
353
392
|
});
|
|
354
393
|
|
|
355
394
|
proc.on("close", (code) => {
|
|
395
|
+
liveSubagentProcesses.delete(proc);
|
|
356
396
|
if (buffer.trim()) processLine(buffer);
|
|
357
397
|
resolve(code ?? 0);
|
|
358
398
|
});
|
|
359
399
|
|
|
360
400
|
proc.on("error", () => {
|
|
401
|
+
liveSubagentProcesses.delete(proc);
|
|
361
402
|
resolve(1);
|
|
362
403
|
});
|
|
363
404
|
|
|
@@ -432,6 +473,10 @@ const SubagentParams = Type.Object({
|
|
|
432
473
|
});
|
|
433
474
|
|
|
434
475
|
export default function (pi: ExtensionAPI) {
|
|
476
|
+
pi.on("session_shutdown", async () => {
|
|
477
|
+
await stopLiveSubagents();
|
|
478
|
+
});
|
|
479
|
+
|
|
435
480
|
// /subagent command - list available agents
|
|
436
481
|
pi.registerCommand("subagent", {
|
|
437
482
|
description: "List available subagents",
|
|
@@ -273,13 +273,16 @@ export async function createWorktreeIsolation(
|
|
|
273
273
|
async cleanup(): Promise<void> {
|
|
274
274
|
activeIsolations.delete(worktreeDir);
|
|
275
275
|
try {
|
|
276
|
-
await
|
|
277
|
-
["worktree", "remove", "--force", worktreeDir],
|
|
278
|
-
|
|
279
|
-
|
|
276
|
+
await Promise.race([
|
|
277
|
+
git(["worktree", "remove", "--force", worktreeDir], repoRoot),
|
|
278
|
+
new Promise<never>((_, reject) =>
|
|
279
|
+
setTimeout(() => reject(new Error("Worktree cleanup timed out")), 10_000),
|
|
280
|
+
),
|
|
281
|
+
]);
|
|
280
282
|
} catch {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
+
try {
|
|
284
|
+
fs.rmSync(worktreeDir, { recursive: true, force: true });
|
|
285
|
+
} catch { /* best effort */ }
|
|
283
286
|
}
|
|
284
287
|
},
|
|
285
288
|
};
|