gsd-pi 2.54.0-dev.e1efc1a → 2.55.0-dev.9ec7cdf
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/dist/cli.js +19 -19
- package/dist/headless-ui.d.ts +27 -1
- package/dist/headless-ui.js +203 -13
- package/dist/headless.js +60 -3
- package/dist/resources/extensions/bg-shell/bg-shell-lifecycle.js +2 -2
- package/dist/resources/extensions/bg-shell/utilities.js +34 -5
- package/dist/resources/extensions/gsd/auto/phases.js +19 -3
- package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
- package/dist/resources/extensions/gsd/auto-model-selection.js +17 -1
- package/dist/resources/extensions/gsd/auto-prompts.js +9 -0
- package/dist/resources/extensions/gsd/auto-start.js +12 -5
- package/dist/resources/extensions/gsd/auto-worktree.js +39 -14
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +5 -1
- package/dist/resources/extensions/gsd/bootstrap/provider-error-resume.js +18 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +18 -5
- package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +20 -0
- package/dist/resources/extensions/gsd/commands/catalog.js +2 -1
- package/dist/resources/extensions/gsd/commands/handlers/parallel.js +15 -1
- package/dist/resources/extensions/gsd/crash-recovery.js +2 -2
- package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +413 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.js +5 -1
- package/dist/resources/extensions/gsd/session-lock.js +46 -12
- package/dist/resources/extensions/gsd/skill-health.js +2 -2
- package/dist/resources/extensions/gsd/visualizer-overlay.js +3 -3
- package/dist/resources/extensions/shared/format-utils.js +1 -1
- package/dist/resources/extensions/subagent/worker-registry.js +2 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +18 -18
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +2 -2
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +18 -18
- package/dist/web/standalone/.next/server/chunks/2229.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/6502.2305d0afd2385711.js +9 -0
- package/dist/web/standalone/.next/static/chunks/app/{page-b950e4e384cc62b3.js → page-0c485498795110d6.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/{webpack-bca0e732db0dcec3.js → webpack-4332cbd5dd1be584.js} +1 -1
- package/package.json +6 -4
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +14 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/model-registry.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +16 -2
- package/pkg/package.json +1 -1
- package/scripts/ensure-workspace-builds.cjs +45 -41
- package/src/resources/extensions/bg-shell/bg-shell-lifecycle.ts +2 -2
- package/src/resources/extensions/bg-shell/utilities.ts +39 -4
- package/src/resources/extensions/gsd/auto/phases.ts +25 -4
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
- package/src/resources/extensions/gsd/auto-model-selection.ts +21 -1
- package/src/resources/extensions/gsd/auto-prompts.ts +15 -0
- package/src/resources/extensions/gsd/auto-start.ts +13 -5
- package/src/resources/extensions/gsd/auto-worktree.ts +46 -13
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +5 -4
- package/src/resources/extensions/gsd/bootstrap/provider-error-resume.ts +53 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +19 -6
- package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +24 -0
- package/src/resources/extensions/gsd/commands/catalog.ts +2 -1
- package/src/resources/extensions/gsd/commands/handlers/parallel.ts +19 -1
- package/src/resources/extensions/gsd/crash-recovery.ts +2 -3
- package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +497 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +6 -1
- package/src/resources/extensions/gsd/session-lock.ts +46 -12
- package/src/resources/extensions/gsd/skill-health.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +139 -0
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +28 -0
- package/src/resources/extensions/gsd/tests/{all-milestones-complete-merge.test.ts → integration/all-milestones-complete-merge.test.ts} +3 -3
- package/src/resources/extensions/gsd/tests/{atomic-task-closeout.test.ts → integration/atomic-task-closeout.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{auto-preflight.test.ts → integration/auto-preflight.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{auto-recovery.test.ts → integration/auto-recovery.test.ts} +7 -7
- package/src/resources/extensions/gsd/tests/{auto-secrets-gate.test.ts → integration/auto-secrets-gate.test.ts} +2 -2
- package/src/resources/extensions/gsd/tests/{auto-stash-merge.test.ts → integration/auto-stash-merge.test.ts} +3 -3
- package/src/resources/extensions/gsd/tests/{auto-worktree-milestone-merge.test.ts → integration/auto-worktree-milestone-merge.test.ts} +4 -4
- package/src/resources/extensions/gsd/tests/{auto-worktree.test.ts → integration/auto-worktree.test.ts} +5 -5
- package/src/resources/extensions/gsd/tests/{continue-here.test.ts → integration/continue-here.test.ts} +3 -3
- package/src/resources/extensions/gsd/tests/{doctor-completion-deferral.test.ts → integration/doctor-completion-deferral.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{doctor-delimiter-fix.test.ts → integration/doctor-delimiter-fix.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{doctor-enhancements.test.ts → integration/doctor-enhancements.test.ts} +3 -3
- package/src/resources/extensions/gsd/tests/{doctor-environment-worktree.test.ts → integration/doctor-environment-worktree.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{doctor-environment.test.ts → integration/doctor-environment.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{doctor-fixlevel.test.ts → integration/doctor-fixlevel.test.ts} +2 -2
- package/src/resources/extensions/gsd/tests/{doctor-git.test.ts → integration/doctor-git.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{doctor-proactive.test.ts → integration/doctor-proactive.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{doctor-roadmap-summary-atomicity.test.ts → integration/doctor-roadmap-summary-atomicity.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{doctor-runtime.test.ts → integration/doctor-runtime.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{doctor.test.ts → integration/doctor.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{e2e-workflow-pipeline-integration.test.ts → integration/e2e-workflow-pipeline-integration.test.ts} +5 -5
- package/src/resources/extensions/gsd/tests/{feature-branch-lifecycle-integration.test.ts → integration/feature-branch-lifecycle-integration.test.ts} +4 -4
- package/src/resources/extensions/gsd/tests/{git-locale.test.ts → integration/git-locale.test.ts} +4 -4
- package/src/resources/extensions/gsd/tests/{git-self-heal.test.ts → integration/git-self-heal.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{git-service.test.ts → integration/git-service.test.ts} +4 -4
- package/src/resources/extensions/gsd/tests/{gitignore-tracked-gsd.test.ts → integration/gitignore-tracked-gsd.test.ts} +2 -2
- package/src/resources/extensions/gsd/tests/{idle-recovery.test.ts → integration/idle-recovery.test.ts} +3 -3
- package/src/resources/extensions/gsd/tests/{inherited-repo-home-dir.test.ts → integration/inherited-repo-home-dir.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{integration-lifecycle.test.ts → integration/integration-lifecycle.test.ts} +4 -4
- package/src/resources/extensions/gsd/tests/{integration-mixed-milestones.test.ts → integration/integration-mixed-milestones.test.ts} +6 -6
- package/src/resources/extensions/gsd/tests/{integration-proof.test.ts → integration/integration-proof.test.ts} +12 -12
- package/src/resources/extensions/gsd/tests/{migrate-command.test.ts → integration/migrate-command.test.ts} +2 -2
- package/src/resources/extensions/gsd/tests/{milestone-transition-worktree.test.ts → integration/milestone-transition-worktree.test.ts} +3 -3
- package/src/resources/extensions/gsd/tests/{parallel-merge.test.ts → integration/parallel-merge.test.ts} +3 -3
- package/src/resources/extensions/gsd/tests/{parallel-workers-multi-milestone-e2e.test.ts → integration/parallel-workers-multi-milestone-e2e.test.ts} +3 -3
- package/src/resources/extensions/gsd/tests/{paths.test.ts → integration/paths.test.ts} +1 -1
- package/src/resources/extensions/gsd/tests/{plugin-importer-live.test.ts → integration/plugin-importer-live.test.ts} +2 -2
- package/src/resources/extensions/gsd/tests/{queue-completed-milestone-perf.test.ts → integration/queue-completed-milestone-perf.test.ts} +3 -3
- package/src/resources/extensions/gsd/tests/{queue-reorder-e2e.test.ts → integration/queue-reorder-e2e.test.ts} +5 -5
- package/src/resources/extensions/gsd/tests/{quick-branch-lifecycle.test.ts → integration/quick-branch-lifecycle.test.ts} +5 -5
- package/src/resources/extensions/gsd/tests/{run-uat.test.ts → integration/run-uat.test.ts} +4 -4
- package/src/resources/extensions/gsd/tests/{token-savings.test.ts → integration/token-savings.test.ts} +3 -3
- package/src/resources/extensions/gsd/tests/{worktree-e2e.test.ts → integration/worktree-e2e.test.ts} +4 -4
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +60 -0
- package/src/resources/extensions/gsd/tests/parallel-worker-lock-contention.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/plan-milestone-queue-context.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +61 -19
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/register-extension-guard.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/worktree-preferences-sync.test.ts +49 -24
- package/src/resources/extensions/gsd/visualizer-overlay.ts +3 -3
- package/src/resources/extensions/shared/format-utils.ts +1 -1
- package/src/resources/extensions/subagent/worker-registry.ts +2 -1
- package/dist/web/standalone/.next/static/chunks/4024.87fd909ae0110f50.js +0 -9
- /package/dist/web/standalone/.next/static/{nISuDzAIpGYC-DVTvs4Po → k92jvAf8IfV4dZE3nnrAr}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{nISuDzAIpGYC-DVTvs4Po → k92jvAf8IfV4dZE3nnrAr}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionCommandContext,
|
|
4
|
+
ExtensionContext,
|
|
5
|
+
} from "@gsd/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
import { getAutoDashboardData, startAuto, type AutoDashboardData } from "../auto.js";
|
|
8
|
+
|
|
9
|
+
type AutoResumeSnapshot = Pick<AutoDashboardData, "active" | "paused" | "stepMode" | "basePath">;
|
|
10
|
+
|
|
11
|
+
export interface ProviderErrorResumeDeps {
|
|
12
|
+
getSnapshot(): AutoResumeSnapshot;
|
|
13
|
+
startAuto(
|
|
14
|
+
ctx: ExtensionCommandContext,
|
|
15
|
+
pi: ExtensionAPI,
|
|
16
|
+
base: string,
|
|
17
|
+
verboseMode: boolean,
|
|
18
|
+
options?: { step?: boolean },
|
|
19
|
+
): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const defaultDeps: ProviderErrorResumeDeps = {
|
|
23
|
+
getSnapshot: () => getAutoDashboardData(),
|
|
24
|
+
startAuto,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export async function resumeAutoAfterProviderDelay(
|
|
28
|
+
pi: ExtensionAPI,
|
|
29
|
+
ctx: ExtensionContext,
|
|
30
|
+
deps: ProviderErrorResumeDeps = defaultDeps,
|
|
31
|
+
): Promise<"resumed" | "already-active" | "not-paused" | "missing-base"> {
|
|
32
|
+
const snapshot = deps.getSnapshot();
|
|
33
|
+
|
|
34
|
+
if (snapshot.active) return "already-active";
|
|
35
|
+
if (!snapshot.paused) return "not-paused";
|
|
36
|
+
|
|
37
|
+
if (!snapshot.basePath) {
|
|
38
|
+
ctx.ui.notify(
|
|
39
|
+
"Provider error recovery delay elapsed, but no paused auto-mode base path was available. Leaving auto-mode paused.",
|
|
40
|
+
"warning",
|
|
41
|
+
);
|
|
42
|
+
return "missing-base";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await deps.startAuto(
|
|
46
|
+
ctx as ExtensionCommandContext,
|
|
47
|
+
pi,
|
|
48
|
+
snapshot.basePath,
|
|
49
|
+
false,
|
|
50
|
+
{ step: snapshot.stepMode },
|
|
51
|
+
);
|
|
52
|
+
return "resumed";
|
|
53
|
+
}
|
|
@@ -9,14 +9,28 @@ import { registerJournalTools } from "./journal-tools.js";
|
|
|
9
9
|
import { registerHooks } from "./register-hooks.js";
|
|
10
10
|
import { registerShortcuts } from "./register-shortcuts.js";
|
|
11
11
|
|
|
12
|
+
export function handleRecoverableExtensionProcessError(err: Error): boolean {
|
|
13
|
+
if ((err as NodeJS.ErrnoException).code === "EPIPE") {
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
17
|
+
const syscall = (err as NodeJS.ErrnoException).syscall;
|
|
18
|
+
if (syscall?.startsWith("spawn")) {
|
|
19
|
+
process.stderr.write(`[gsd] spawn ENOENT: ${(err as any).path ?? "unknown"} — command not found\n`);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
if (syscall === "uv_cwd") {
|
|
23
|
+
process.stderr.write(`[gsd] ENOENT (${syscall}): ${err.message}\n`);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
12
30
|
function installEpipeGuard(): void {
|
|
13
31
|
if (!process.listeners("uncaughtException").some((listener) => listener.name === "_gsdEpipeGuard")) {
|
|
14
32
|
const _gsdEpipeGuard = (err: Error): void => {
|
|
15
|
-
if ((err
|
|
16
|
-
process.exit(0);
|
|
17
|
-
}
|
|
18
|
-
if ((err as NodeJS.ErrnoException).code === "ENOENT" && (err as any).syscall?.startsWith("spawn")) {
|
|
19
|
-
process.stderr.write(`[gsd] spawn ENOENT: ${(err as any).path ?? "unknown"} — command not found\n`);
|
|
33
|
+
if (handleRecoverableExtensionProcessError(err)) {
|
|
20
34
|
return;
|
|
21
35
|
}
|
|
22
36
|
throw err;
|
|
@@ -45,4 +59,3 @@ export function registerGsdExtension(pi: ExtensionAPI): void {
|
|
|
45
59
|
registerShortcuts(pi);
|
|
46
60
|
registerHooks(pi);
|
|
47
61
|
}
|
|
48
|
-
|
|
@@ -5,6 +5,7 @@ import type { ExtensionAPI } from "@gsd/pi-coding-agent";
|
|
|
5
5
|
import { Key } from "@gsd/pi-tui";
|
|
6
6
|
|
|
7
7
|
import { GSDDashboardOverlay } from "../dashboard-overlay.js";
|
|
8
|
+
import { ParallelMonitorOverlay } from "../parallel-monitor-overlay.js";
|
|
8
9
|
import { shortcutDesc } from "../../shared/mod.js";
|
|
9
10
|
|
|
10
11
|
export function registerShortcuts(pi: ExtensionAPI): void {
|
|
@@ -29,4 +30,27 @@ export function registerShortcuts(pi: ExtensionAPI): void {
|
|
|
29
30
|
);
|
|
30
31
|
},
|
|
31
32
|
});
|
|
33
|
+
|
|
34
|
+
pi.registerShortcut(Key.ctrlAlt("p"), {
|
|
35
|
+
description: shortcutDesc("Open parallel worker monitor", "/gsd parallel watch"),
|
|
36
|
+
handler: async (ctx) => {
|
|
37
|
+
const parallelDir = join(process.cwd(), ".gsd", "parallel");
|
|
38
|
+
if (!existsSync(parallelDir)) {
|
|
39
|
+
ctx.ui.notify("No parallel workers found. Run /gsd parallel start first.", "info");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
await ctx.ui.custom<void>(
|
|
43
|
+
(tui, theme, _kb, done) => new ParallelMonitorOverlay(tui, theme, () => done()),
|
|
44
|
+
{
|
|
45
|
+
overlay: true,
|
|
46
|
+
overlayOptions: {
|
|
47
|
+
width: "90%",
|
|
48
|
+
minWidth: 80,
|
|
49
|
+
maxHeight: "92%",
|
|
50
|
+
anchor: "center",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
32
56
|
}
|
|
@@ -59,7 +59,7 @@ export const TOP_LEVEL_SUBCOMMANDS: readonly GsdCommandDefinition[] = [
|
|
|
59
59
|
{ cmd: "inspect", desc: "Show SQLite DB diagnostics" },
|
|
60
60
|
{ cmd: "knowledge", desc: "Add persistent project knowledge (rule, pattern, or lesson)" },
|
|
61
61
|
{ cmd: "new-milestone", desc: "Create a milestone from a specification document (headless)" },
|
|
62
|
-
{ cmd: "parallel", desc: "Parallel milestone orchestration (start, status, stop, merge)" },
|
|
62
|
+
{ cmd: "parallel", desc: "Parallel milestone orchestration (start, status, stop, merge, watch)" },
|
|
63
63
|
{ cmd: "cmux", desc: "Manage cmux integration (status, sidebar, notifications, splits)" },
|
|
64
64
|
{ cmd: "park", desc: "Park a milestone — skip without deleting" },
|
|
65
65
|
{ cmd: "unpark", desc: "Reactivate a parked milestone" },
|
|
@@ -100,6 +100,7 @@ const NESTED_COMPLETIONS: CompletionMap = {
|
|
|
100
100
|
{ cmd: "pause", desc: "Pause a specific worker" },
|
|
101
101
|
{ cmd: "resume", desc: "Resume a paused worker" },
|
|
102
102
|
{ cmd: "merge", desc: "Merge completed milestone branches" },
|
|
103
|
+
{ cmd: "watch", desc: "Live TUI dashboard monitoring all workers" },
|
|
103
104
|
],
|
|
104
105
|
setup: [
|
|
105
106
|
{ cmd: "llm", desc: "Configure LLM provider settings" },
|
|
@@ -111,7 +111,25 @@ export async function handleParallelCommand(trimmed: string, _ctx: ExtensionComm
|
|
|
111
111
|
return true;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
if (subcommand === "watch") {
|
|
115
|
+
const root = projectRoot();
|
|
116
|
+
const { ParallelMonitorOverlay } = await import("../../parallel-monitor-overlay.js");
|
|
117
|
+
await _ctx.ui.custom<void>(
|
|
118
|
+
(tui, theme, _kb, done) => new ParallelMonitorOverlay(tui, theme, () => done(), root),
|
|
119
|
+
{
|
|
120
|
+
overlay: true,
|
|
121
|
+
overlayOptions: {
|
|
122
|
+
width: "90%",
|
|
123
|
+
minWidth: 80,
|
|
124
|
+
maxHeight: "92%",
|
|
125
|
+
anchor: "center",
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
emitParallelMessage(pi, `Unknown parallel subcommand "${subcommand}". Usage: /gsd parallel [start|status|stop|pause|resume|merge|watch]`);
|
|
115
133
|
return true;
|
|
116
134
|
}
|
|
117
135
|
|
|
@@ -14,8 +14,7 @@ import { readFileSync, unlinkSync, existsSync } from "node:fs";
|
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { gsdRoot } from "./paths.js";
|
|
16
16
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
17
|
-
|
|
18
|
-
const LOCK_FILE = "auto.lock";
|
|
17
|
+
import { effectiveLockFile } from "./session-lock.js";
|
|
19
18
|
|
|
20
19
|
export interface LockData {
|
|
21
20
|
pid: number;
|
|
@@ -28,7 +27,7 @@ export interface LockData {
|
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
function lockPath(basePath: string): string {
|
|
31
|
-
return join(gsdRoot(basePath),
|
|
30
|
+
return join(gsdRoot(basePath), effectiveLockFile());
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
/** Write or update the lock file with current auto-mode state. */
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Parallel Monitor Overlay
|
|
3
|
+
*
|
|
4
|
+
* Full-screen TUI overlay showing real-time parallel worker progress.
|
|
5
|
+
* Opened via `/gsd parallel watch` or Ctrl+Alt+P.
|
|
6
|
+
* Reads the same data sources as `scripts/parallel-monitor.mjs` but
|
|
7
|
+
* renders as a native pi-tui overlay with theme integration.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, statSync, readFileSync, openSync, readSync, closeSync, readdirSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
|
|
14
|
+
import type { Theme } from "@gsd/pi-coding-agent";
|
|
15
|
+
import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui";
|
|
16
|
+
|
|
17
|
+
import { formatDuration, STATUS_GLYPH, STATUS_COLOR } from "../shared/mod.js";
|
|
18
|
+
|
|
19
|
+
// ─── Types ────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
interface StatusJson {
|
|
22
|
+
milestoneId: string;
|
|
23
|
+
pid: number;
|
|
24
|
+
state: string;
|
|
25
|
+
cost: number;
|
|
26
|
+
lastHeartbeat: number;
|
|
27
|
+
startedAt: number;
|
|
28
|
+
worktreePath: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface AutoLock {
|
|
32
|
+
pid: number;
|
|
33
|
+
startedAt: string;
|
|
34
|
+
unitType: string;
|
|
35
|
+
unitId: string;
|
|
36
|
+
unitStartedAt: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface SliceProgress {
|
|
40
|
+
id: string;
|
|
41
|
+
status: string;
|
|
42
|
+
total: number;
|
|
43
|
+
done: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface WorkerView {
|
|
47
|
+
mid: string;
|
|
48
|
+
pid: number;
|
|
49
|
+
alive: boolean;
|
|
50
|
+
state: string;
|
|
51
|
+
cost: number;
|
|
52
|
+
heartbeatAge: number;
|
|
53
|
+
currentUnit: string | null;
|
|
54
|
+
unitType: string | null;
|
|
55
|
+
unitElapsed: number;
|
|
56
|
+
elapsed: number;
|
|
57
|
+
totalTasks: number;
|
|
58
|
+
doneTasks: number;
|
|
59
|
+
totalSlices: number;
|
|
60
|
+
doneSlices: number;
|
|
61
|
+
slices: SliceProgress[];
|
|
62
|
+
errors: string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Data Helpers ─────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
function readJsonSafe<T>(filePath: string): T | null {
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(readFileSync(filePath, "utf-8")) as T;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isPidAlive(pid: number): boolean {
|
|
76
|
+
try {
|
|
77
|
+
process.kill(pid, 0);
|
|
78
|
+
return true;
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function tailRead(filePath: string, maxBytes: number): string {
|
|
85
|
+
try {
|
|
86
|
+
const stat = statSync(filePath);
|
|
87
|
+
const readSize = Math.min(stat.size, maxBytes);
|
|
88
|
+
const fd = openSync(filePath, "r");
|
|
89
|
+
const buf = Buffer.alloc(readSize);
|
|
90
|
+
readSync(fd, buf, 0, readSize, Math.max(0, stat.size - readSize));
|
|
91
|
+
closeSync(fd);
|
|
92
|
+
return buf.toString("utf-8");
|
|
93
|
+
} catch {
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function discoverWorkers(basePath: string): string[] {
|
|
99
|
+
const parallelDir = join(basePath, ".gsd", "parallel");
|
|
100
|
+
const worktreeDir = join(basePath, ".gsd", "worktrees");
|
|
101
|
+
const mids = new Set<string>();
|
|
102
|
+
|
|
103
|
+
if (existsSync(parallelDir)) {
|
|
104
|
+
try {
|
|
105
|
+
for (const f of readdirSync(parallelDir)) {
|
|
106
|
+
if (f.endsWith(".status.json")) mids.add(f.replace(".status.json", ""));
|
|
107
|
+
const m = f.match(/^(M\d+)\.(stderr|stdout)\.log$/);
|
|
108
|
+
if (m) mids.add(m[1]);
|
|
109
|
+
}
|
|
110
|
+
} catch { /* skip */ }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (existsSync(worktreeDir)) {
|
|
114
|
+
try {
|
|
115
|
+
for (const d of readdirSync(worktreeDir)) {
|
|
116
|
+
if (d.startsWith("M") && existsSync(join(worktreeDir, d, ".gsd", "auto.lock"))) {
|
|
117
|
+
mids.add(d);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch { /* skip */ }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return [...mids].sort();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function querySliceProgress(basePath: string, mid: string): SliceProgress[] {
|
|
127
|
+
const dbPath = join(basePath, ".gsd", "worktrees", mid, ".gsd", "gsd.db");
|
|
128
|
+
if (!existsSync(dbPath)) return [];
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const sql = `SELECT s.id, s.status, COUNT(t.id), SUM(CASE WHEN t.status='complete' THEN 1 ELSE 0 END) FROM slices s LEFT JOIN tasks t ON s.milestone_id=t.milestone_id AND s.id=t.slice_id WHERE s.milestone_id='${mid}' GROUP BY s.id ORDER BY s.id`;
|
|
132
|
+
const result = spawnSync("sqlite3", [dbPath, sql], { timeout: 3000, encoding: "utf-8" });
|
|
133
|
+
const out = (result.stdout || "").trim();
|
|
134
|
+
if (!out || result.status !== 0) return [];
|
|
135
|
+
return out.split("\n").map((line) => {
|
|
136
|
+
const [id, status, total, done] = line.split("|");
|
|
137
|
+
return { id, status, total: parseInt(total, 10), done: parseInt(done || "0", 10) };
|
|
138
|
+
});
|
|
139
|
+
} catch {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function extractCostFromNdjson(basePath: string, mid: string): number {
|
|
145
|
+
const stdoutPath = join(basePath, ".gsd", "parallel", `${mid}.stdout.log`);
|
|
146
|
+
if (!existsSync(stdoutPath)) return 0;
|
|
147
|
+
try {
|
|
148
|
+
const content = readFileSync(stdoutPath, "utf-8");
|
|
149
|
+
let total = 0;
|
|
150
|
+
for (const line of content.split("\n")) {
|
|
151
|
+
if (!line.includes("message_end")) continue;
|
|
152
|
+
try {
|
|
153
|
+
const obj = JSON.parse(line);
|
|
154
|
+
if (obj.type === "message_end") {
|
|
155
|
+
const cost = obj.message?.usage?.cost?.total;
|
|
156
|
+
if (typeof cost === "number") total += cost;
|
|
157
|
+
}
|
|
158
|
+
} catch { /* skip */ }
|
|
159
|
+
}
|
|
160
|
+
return total;
|
|
161
|
+
} catch {
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function queryRecentCompletions(basePath: string, mid: string): string[] {
|
|
167
|
+
const dbPath = join(basePath, ".gsd", "worktrees", mid, ".gsd", "gsd.db");
|
|
168
|
+
if (!existsSync(dbPath)) return [];
|
|
169
|
+
try {
|
|
170
|
+
const sql = `SELECT id, slice_id, one_liner FROM tasks WHERE milestone_id='${mid}' AND status='complete' AND completed_at IS NOT NULL ORDER BY completed_at DESC LIMIT 5`;
|
|
171
|
+
const result = spawnSync("sqlite3", [dbPath, sql], { timeout: 3000, encoding: "utf-8" });
|
|
172
|
+
const out = (result.stdout || "").trim();
|
|
173
|
+
if (!out || result.status !== 0) return [];
|
|
174
|
+
return out.split("\n").map((line) => {
|
|
175
|
+
const [taskId, sliceId, oneLiner] = line.split("|");
|
|
176
|
+
return `✓ ${mid}/${sliceId}/${taskId}${oneLiner ? ": " + oneLiner : ""}`;
|
|
177
|
+
});
|
|
178
|
+
} catch {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function collectWorkerData(basePath: string): WorkerView[] {
|
|
184
|
+
const mids = discoverWorkers(basePath);
|
|
185
|
+
const parallelDir = join(basePath, ".gsd", "parallel");
|
|
186
|
+
const workers: WorkerView[] = [];
|
|
187
|
+
|
|
188
|
+
for (const mid of mids) {
|
|
189
|
+
const status = readJsonSafe<StatusJson>(join(parallelDir, `${mid}.status.json`));
|
|
190
|
+
const lock = readJsonSafe<AutoLock>(join(basePath, ".gsd", "worktrees", mid, ".gsd", "auto.lock"));
|
|
191
|
+
const slices = querySliceProgress(basePath, mid);
|
|
192
|
+
|
|
193
|
+
const pid = lock?.pid || status?.pid || 0;
|
|
194
|
+
const alive = pid ? isPidAlive(pid) : false;
|
|
195
|
+
|
|
196
|
+
// Heartbeat: prefer status.json if PID matches, else use file mtime
|
|
197
|
+
let heartbeatAge = Infinity;
|
|
198
|
+
const statusPidMatches = status?.pid === pid && status?.lastHeartbeat;
|
|
199
|
+
if (statusPidMatches) {
|
|
200
|
+
heartbeatAge = Date.now() - status!.lastHeartbeat;
|
|
201
|
+
} else {
|
|
202
|
+
const mtimes: number[] = [];
|
|
203
|
+
const stdoutLog = join(parallelDir, `${mid}.stdout.log`);
|
|
204
|
+
const stderrLog = join(parallelDir, `${mid}.stderr.log`);
|
|
205
|
+
if (existsSync(stdoutLog)) mtimes.push(statSync(stdoutLog).mtimeMs);
|
|
206
|
+
if (existsSync(stderrLog)) mtimes.push(statSync(stderrLog).mtimeMs);
|
|
207
|
+
if (lock?.unitStartedAt) mtimes.push(new Date(lock.unitStartedAt).getTime());
|
|
208
|
+
if (mtimes.length > 0) heartbeatAge = Date.now() - Math.max(...mtimes);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let cost = status?.cost || 0;
|
|
212
|
+
if (cost === 0) cost = extractCostFromNdjson(basePath, mid);
|
|
213
|
+
|
|
214
|
+
const totalTasks = slices.reduce((sum, s) => sum + s.total, 0);
|
|
215
|
+
const doneTasks = slices.reduce((sum, s) => sum + s.done, 0);
|
|
216
|
+
const doneSlices = slices.filter((s) => s.status === "complete").length;
|
|
217
|
+
|
|
218
|
+
const elapsed = status?.startedAt
|
|
219
|
+
? Date.now() - status.startedAt
|
|
220
|
+
: lock?.startedAt
|
|
221
|
+
? Date.now() - new Date(lock.startedAt).getTime()
|
|
222
|
+
: 0;
|
|
223
|
+
|
|
224
|
+
// Errors from stderr (last 4KB, only new content)
|
|
225
|
+
const errors: string[] = [];
|
|
226
|
+
const stderrLog = join(parallelDir, `${mid}.stderr.log`);
|
|
227
|
+
if (existsSync(stderrLog)) {
|
|
228
|
+
const content = tailRead(stderrLog, 4096);
|
|
229
|
+
for (const line of content.trim().split("\n").slice(-5)) {
|
|
230
|
+
if (line.includes("error") || line.includes("Error") || line.includes("exited")) {
|
|
231
|
+
errors.push(line.trim());
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
workers.push({
|
|
237
|
+
mid,
|
|
238
|
+
pid,
|
|
239
|
+
alive,
|
|
240
|
+
state: alive ? "running" : (status?.state || "dead"),
|
|
241
|
+
cost,
|
|
242
|
+
heartbeatAge,
|
|
243
|
+
currentUnit: lock?.unitId || null,
|
|
244
|
+
unitType: lock?.unitType || null,
|
|
245
|
+
unitElapsed: lock?.unitStartedAt ? Date.now() - new Date(lock.unitStartedAt).getTime() : 0,
|
|
246
|
+
elapsed,
|
|
247
|
+
totalTasks,
|
|
248
|
+
doneTasks,
|
|
249
|
+
totalSlices: slices.length,
|
|
250
|
+
doneSlices,
|
|
251
|
+
slices,
|
|
252
|
+
errors,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return workers;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ─── Rendering Helpers ────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
function unitTypeLabel(unitType: string | null): string {
|
|
262
|
+
const labels: Record<string, string> = {
|
|
263
|
+
"execute-task": "EXEC",
|
|
264
|
+
"research-slice": "RSRCH",
|
|
265
|
+
"plan-slice": "PLAN",
|
|
266
|
+
"complete-slice": "DONE",
|
|
267
|
+
"complete-task": "DONE",
|
|
268
|
+
"reassess": "ASSESS",
|
|
269
|
+
"validate": "VALID",
|
|
270
|
+
"reassess-roadmap": "ASSESS",
|
|
271
|
+
};
|
|
272
|
+
return labels[unitType || ""] || (unitType || "---").toUpperCase().slice(0, 5);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function progressBar(done: number, total: number, width: number): string {
|
|
276
|
+
if (total === 0) return "░".repeat(width);
|
|
277
|
+
const filled = Math.round((done / total) * width);
|
|
278
|
+
return "█".repeat(filled) + "░".repeat(width - filled);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function healthGlyph(alive: boolean, heartbeatAge: number): string {
|
|
282
|
+
if (!alive) return "○";
|
|
283
|
+
return "●";
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ─── Overlay Class ────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
export class ParallelMonitorOverlay {
|
|
289
|
+
private tui: { requestRender: () => void };
|
|
290
|
+
private theme: Theme;
|
|
291
|
+
private onClose: () => void;
|
|
292
|
+
private basePath: string;
|
|
293
|
+
private refreshTimer: ReturnType<typeof setInterval>;
|
|
294
|
+
private workers: WorkerView[] = [];
|
|
295
|
+
private events: string[] = [];
|
|
296
|
+
private cachedLines?: string[];
|
|
297
|
+
private scrollOffset = 0;
|
|
298
|
+
private disposed = false;
|
|
299
|
+
private resizeHandler: (() => void) | null = null;
|
|
300
|
+
|
|
301
|
+
constructor(
|
|
302
|
+
tui: { requestRender: () => void },
|
|
303
|
+
theme: Theme,
|
|
304
|
+
onClose: () => void,
|
|
305
|
+
basePath?: string,
|
|
306
|
+
) {
|
|
307
|
+
this.tui = tui;
|
|
308
|
+
this.theme = theme;
|
|
309
|
+
this.onClose = onClose;
|
|
310
|
+
this.basePath = basePath || process.cwd();
|
|
311
|
+
|
|
312
|
+
this.resizeHandler = () => {
|
|
313
|
+
if (this.disposed) return;
|
|
314
|
+
this.invalidate();
|
|
315
|
+
this.tui.requestRender();
|
|
316
|
+
};
|
|
317
|
+
process.stdout.on("resize", this.resizeHandler);
|
|
318
|
+
|
|
319
|
+
this.refresh();
|
|
320
|
+
this.refreshTimer = setInterval(() => this.refresh(), 5000);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private refresh(): void {
|
|
324
|
+
if (this.disposed) return;
|
|
325
|
+
this.workers = collectWorkerData(this.basePath);
|
|
326
|
+
|
|
327
|
+
// Collect completion events
|
|
328
|
+
for (const wk of this.workers) {
|
|
329
|
+
const completions = queryRecentCompletions(this.basePath, wk.mid);
|
|
330
|
+
for (const evt of completions) {
|
|
331
|
+
if (!this.events.includes(evt)) this.events.push(evt);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
this.events = this.events.slice(-10);
|
|
335
|
+
|
|
336
|
+
this.cachedLines = undefined;
|
|
337
|
+
this.tui.requestRender();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
dispose(): void {
|
|
341
|
+
this.disposed = true;
|
|
342
|
+
clearInterval(this.refreshTimer);
|
|
343
|
+
if (this.resizeHandler) {
|
|
344
|
+
process.stdout.removeListener("resize", this.resizeHandler);
|
|
345
|
+
this.resizeHandler = null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
handleInput(data: string): void {
|
|
350
|
+
if (matchesKey(data, Key.escape) || data === "q") {
|
|
351
|
+
this.dispose();
|
|
352
|
+
this.onClose();
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (matchesKey(data, Key.down) || data === "j") {
|
|
356
|
+
this.scrollOffset++;
|
|
357
|
+
this.invalidate();
|
|
358
|
+
this.tui.requestRender();
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (matchesKey(data, Key.up) || data === "k") {
|
|
362
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - 1);
|
|
363
|
+
this.invalidate();
|
|
364
|
+
this.tui.requestRender();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
invalidate(): void {
|
|
370
|
+
this.cachedLines = undefined;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
render(width: number): string[] {
|
|
374
|
+
if (this.cachedLines) return this.cachedLines;
|
|
375
|
+
|
|
376
|
+
const t = this.theme;
|
|
377
|
+
const lines: string[] = [];
|
|
378
|
+
const w = Math.max(width, 60);
|
|
379
|
+
|
|
380
|
+
// Header
|
|
381
|
+
const totalCost = this.workers.reduce((s, wk) => s + wk.cost, 0);
|
|
382
|
+
const aliveCount = this.workers.filter((wk) => wk.alive).length;
|
|
383
|
+
const now = new Date().toLocaleTimeString();
|
|
384
|
+
|
|
385
|
+
lines.push(t.bold(t.fg("accent", " GSD Parallel Monitor ")));
|
|
386
|
+
lines.push(
|
|
387
|
+
t.fg("muted", ` ${now} │ ${aliveCount}/${this.workers.length} alive │ Total: `) +
|
|
388
|
+
t.bold(`$${totalCost.toFixed(2)}`) +
|
|
389
|
+
t.fg("muted", " │ 5s refresh"),
|
|
390
|
+
);
|
|
391
|
+
lines.push(t.fg("muted", "─".repeat(w)));
|
|
392
|
+
|
|
393
|
+
if (this.workers.length === 0) {
|
|
394
|
+
lines.push("");
|
|
395
|
+
lines.push(t.fg("warning", " No parallel workers found."));
|
|
396
|
+
lines.push(t.fg("muted", " Run /gsd parallel start to begin."));
|
|
397
|
+
} else {
|
|
398
|
+
for (const wk of this.workers) {
|
|
399
|
+
lines.push("");
|
|
400
|
+
|
|
401
|
+
// Health + ID + state
|
|
402
|
+
const healthColor = wk.alive ? "success" : "error";
|
|
403
|
+
const glyph = healthGlyph(wk.alive, wk.heartbeatAge);
|
|
404
|
+
const stateText = wk.alive
|
|
405
|
+
? t.fg("success", "RUNNING")
|
|
406
|
+
: t.fg("error", t.bold("DEAD"));
|
|
407
|
+
const heartbeatText = wk.heartbeatAge === Infinity
|
|
408
|
+
? "never"
|
|
409
|
+
: formatDuration(wk.heartbeatAge) + " ago";
|
|
410
|
+
|
|
411
|
+
lines.push(
|
|
412
|
+
` ${t.fg(healthColor, glyph)} ${t.bold(wk.mid)} ${stateText} ` +
|
|
413
|
+
t.fg("muted", `PID ${wk.pid} │ elapsed ${formatDuration(wk.elapsed)} │ `) +
|
|
414
|
+
`cost ${t.bold("$" + wk.cost.toFixed(2))} ` +
|
|
415
|
+
t.fg("muted", "│ heartbeat ") + t.fg(healthColor, heartbeatText),
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
// Current unit
|
|
419
|
+
if (wk.currentUnit) {
|
|
420
|
+
const phaseColor =
|
|
421
|
+
wk.unitType === "execute-task" ? "accent"
|
|
422
|
+
: wk.unitType === "research-slice" ? "warning"
|
|
423
|
+
: wk.unitType?.includes("complete") ? "success"
|
|
424
|
+
: "text";
|
|
425
|
+
lines.push(
|
|
426
|
+
` ${t.fg("muted", "▸")} ${t.fg(phaseColor, unitTypeLabel(wk.unitType))} ${wk.currentUnit} ` +
|
|
427
|
+
t.fg("muted", `(${formatDuration(wk.unitElapsed)})`),
|
|
428
|
+
);
|
|
429
|
+
} else if (!wk.alive) {
|
|
430
|
+
lines.push(` ${t.fg("muted", "▸")} ${t.fg("error", "stopped")}`);
|
|
431
|
+
} else {
|
|
432
|
+
lines.push(` ${t.fg("muted", "▸ idle / between units")}`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Slice progress chips
|
|
436
|
+
if (wk.slices.length > 0) {
|
|
437
|
+
const chips = wk.slices.map((s) => {
|
|
438
|
+
const pct = s.total > 0 ? s.done / s.total : 0;
|
|
439
|
+
const color = s.status === "complete" ? "success" : pct > 0 ? "warning" : "muted";
|
|
440
|
+
return t.fg(color, `${s.id}:${s.done}/${s.total}`);
|
|
441
|
+
});
|
|
442
|
+
lines.push(` ${t.fg("muted", "slices")} ${chips.join(" ")}`);
|
|
443
|
+
|
|
444
|
+
// Task progress bar
|
|
445
|
+
const bar = progressBar(wk.doneTasks, wk.totalTasks, 25);
|
|
446
|
+
const pct = wk.totalTasks > 0 ? Math.round((wk.doneTasks / wk.totalTasks) * 100) : 0;
|
|
447
|
+
lines.push(
|
|
448
|
+
` ${t.fg("muted", "tasks")} ${t.fg("success", bar)} ${wk.doneTasks}/${wk.totalTasks} ` +
|
|
449
|
+
t.fg("muted", `(${pct}%) │ slices done ${wk.doneSlices}/${wk.totalSlices}`),
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Errors
|
|
454
|
+
for (const err of wk.errors.slice(-2)) {
|
|
455
|
+
const truncated = err.length > w - 10 ? err.slice(0, w - 11) + "…" : err;
|
|
456
|
+
lines.push(` ${t.fg("error", "⚠ " + truncated)}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Event feed
|
|
462
|
+
lines.push("");
|
|
463
|
+
lines.push(t.fg("muted", "─".repeat(w)));
|
|
464
|
+
lines.push(` ${t.bold("Recent Events")}`);
|
|
465
|
+
|
|
466
|
+
if (this.events.length === 0) {
|
|
467
|
+
lines.push(t.fg("muted", " No events yet..."));
|
|
468
|
+
} else {
|
|
469
|
+
for (const evt of this.events.slice(-8)) {
|
|
470
|
+
const mid = evt.match(/^✓ (M\d+)\//)?.[1] || "";
|
|
471
|
+
const truncated = evt.length > w - 10 ? evt.slice(0, w - 11) + "…" : evt;
|
|
472
|
+
lines.push(` ${t.fg("muted", "│")} ${t.fg("accent", mid)} ${truncated.replace(/^✓ M\d+\//, "")}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Footer
|
|
477
|
+
lines.push("");
|
|
478
|
+
const allDone = this.workers.length > 0 && this.workers.every((wk) => !wk.alive);
|
|
479
|
+
if (allDone) {
|
|
480
|
+
lines.push(t.bold(t.fg("success", " ALL WORKERS COMPLETE")));
|
|
481
|
+
for (const wk of this.workers) {
|
|
482
|
+
lines.push(
|
|
483
|
+
` ${wk.mid} $${wk.cost.toFixed(2)} │ ${wk.doneSlices}/${wk.totalSlices} slices ` +
|
|
484
|
+
`${wk.doneTasks}/${wk.totalTasks} tasks │ ${formatDuration(wk.elapsed)}`,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
lines.push(` ${t.bold("Total: $" + this.workers.reduce((s, wk) => s + wk.cost, 0).toFixed(2))}`);
|
|
488
|
+
}
|
|
489
|
+
lines.push(t.fg("muted", " ESC/q to close │ ↑↓ scroll"));
|
|
490
|
+
|
|
491
|
+
// Apply scroll — use terminal rows as height estimate
|
|
492
|
+
const termHeight = process.stdout.rows || 40;
|
|
493
|
+
const visible = lines.slice(this.scrollOffset, this.scrollOffset + termHeight);
|
|
494
|
+
this.cachedLines = visible;
|
|
495
|
+
return visible;
|
|
496
|
+
}
|
|
497
|
+
}
|