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
|
@@ -559,7 +559,9 @@ export class Editor implements Component, Focusable {
|
|
|
559
559
|
this.state.cursorLine = result.cursorLine;
|
|
560
560
|
this.setCursorCol(result.cursorCol);
|
|
561
561
|
|
|
562
|
-
if (this.autocompletePrefix.startsWith("/")
|
|
562
|
+
if (this.autocompletePrefix.startsWith("/") || this.isInSlashCommandContext(
|
|
563
|
+
(this.state.lines[this.state.cursorLine] || "").slice(0, this.state.cursorCol),
|
|
564
|
+
)) {
|
|
563
565
|
this.cancelAutocomplete();
|
|
564
566
|
// Fall through to submit
|
|
565
567
|
} else {
|
|
@@ -10,8 +10,12 @@
|
|
|
10
10
|
* to resolve. This script bridges the gap.
|
|
11
11
|
*
|
|
12
12
|
* Runs as part of postinstall (before any ESM code that imports @gsd/*).
|
|
13
|
+
*
|
|
14
|
+
* On Windows without Developer Mode or administrator rights, creating symlinks
|
|
15
|
+
* (even NTFS junctions) can fail with EPERM. In that case we fall back to
|
|
16
|
+
* cpSync (directory copy) which works universally.
|
|
13
17
|
*/
|
|
14
|
-
const { existsSync, mkdirSync, symlinkSync, lstatSync, readlinkSync, unlinkSync
|
|
18
|
+
const { existsSync, mkdirSync, symlinkSync, cpSync, lstatSync, readlinkSync, unlinkSync } = require('fs')
|
|
15
19
|
const { resolve, join } = require('path')
|
|
16
20
|
|
|
17
21
|
const root = resolve(__dirname, '..')
|
|
@@ -33,6 +37,7 @@ if (!existsSync(nodeModulesGsd)) {
|
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
let linked = 0
|
|
40
|
+
let copied = 0
|
|
36
41
|
for (const [dir, name] of Object.entries(packageMap)) {
|
|
37
42
|
const source = join(packagesDir, dir)
|
|
38
43
|
const target = join(nodeModulesGsd, name)
|
|
@@ -50,21 +55,32 @@ for (const [dir, name] of Object.entries(packageMap)) {
|
|
|
50
55
|
}
|
|
51
56
|
unlinkSync(target) // Wrong target, relink
|
|
52
57
|
} else {
|
|
53
|
-
continue // Real directory (e.g., from bundleDependencies), don't touch
|
|
58
|
+
continue // Real directory (e.g., copied or from bundleDependencies), don't touch
|
|
54
59
|
}
|
|
55
60
|
} catch {
|
|
56
61
|
continue
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
|
|
65
|
+
let symlinkOk = false
|
|
60
66
|
try {
|
|
61
67
|
symlinkSync(source, target, 'junction') // junction works on Windows too
|
|
68
|
+
symlinkOk = true
|
|
62
69
|
linked++
|
|
63
70
|
} catch {
|
|
64
|
-
//
|
|
71
|
+
// Symlink failed — common on Windows without Developer Mode or admin rights.
|
|
72
|
+
// Fall back to a directory copy so the package is still resolvable.
|
|
65
73
|
}
|
|
66
|
-
}
|
|
67
74
|
|
|
68
|
-
if (
|
|
69
|
-
|
|
75
|
+
if (!symlinkOk) {
|
|
76
|
+
try {
|
|
77
|
+
cpSync(source, target, { recursive: true })
|
|
78
|
+
copied++
|
|
79
|
+
} catch {
|
|
80
|
+
// Non-fatal — loader.ts will emit a clearer error if resolution still fails
|
|
81
|
+
}
|
|
82
|
+
}
|
|
70
83
|
}
|
|
84
|
+
|
|
85
|
+
if (linked > 0) process.stderr.write(` Linked ${linked} workspace package${linked !== 1 ? 's' : ''}\n`)
|
|
86
|
+
if (copied > 0) process.stderr.write(` Copied ${copied} workspace package${copied !== 1 ? 's' : ''} (symlinks unavailable)\n`)
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
getGroupStatus,
|
|
53
53
|
pruneDeadProcesses,
|
|
54
54
|
cleanupAll,
|
|
55
|
+
cleanupSessionProcesses,
|
|
55
56
|
persistManifest,
|
|
56
57
|
loadManifest,
|
|
57
58
|
pushAlert,
|
|
@@ -71,7 +72,7 @@ import { toPosixPath } from "../shared/path-display.js";
|
|
|
71
72
|
// ── Re-exports for consumers ───────────────────────────────────────────────
|
|
72
73
|
|
|
73
74
|
export type { ProcessStatus, ProcessType, BgProcess, BgProcessInfo, OutputDigest, OutputLine, ProcessEvent } from "./types.js";
|
|
74
|
-
export { processes, startProcess, killProcess, restartProcess, cleanupAll } from "./process-manager.js";
|
|
75
|
+
export { processes, startProcess, killProcess, restartProcess, cleanupAll, cleanupSessionProcesses } from "./process-manager.js";
|
|
75
76
|
export { generateDigest, getHighlights, getOutput, formatDigestText } from "./output-formatter.js";
|
|
76
77
|
export { waitForReady, probePort } from "./readiness-detector.js";
|
|
77
78
|
export { sendAndWait, runOnSession, queryShellEnv } from "./interaction.js";
|
|
@@ -136,7 +137,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
136
137
|
});
|
|
137
138
|
|
|
138
139
|
// Session switch resets the agent's context.
|
|
139
|
-
pi.on("session_switch", async () => {
|
|
140
|
+
pi.on("session_switch", async (event, ctx) => {
|
|
141
|
+
latestCtx = ctx;
|
|
142
|
+
if (event.reason === "new" && event.previousSessionFile) {
|
|
143
|
+
await cleanupSessionProcesses(event.previousSessionFile);
|
|
144
|
+
syncLatestCtxCwd();
|
|
145
|
+
if (latestCtx) persistManifest(latestCtx.cwd);
|
|
146
|
+
}
|
|
140
147
|
buildProcessStateAlert("Session was switched.");
|
|
141
148
|
});
|
|
142
149
|
|
|
@@ -232,6 +239,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
232
239
|
"Use 'run' to execute a command on a persistent shell session and block until it completes — returns structured output + exit code. Shell state (env vars, cwd, virtualenvs) persists across runs.",
|
|
233
240
|
"Use 'send_and_wait' for interactive CLIs: send input and wait for expected output pattern.",
|
|
234
241
|
"Use 'env' to check the current working directory and active environment variables of a shell session — useful after cd, source, or export commands.",
|
|
242
|
+
"Background processes are session-scoped by default: a new session reaps them unless you set persist_across_sessions:true.",
|
|
235
243
|
"Use 'restart' to kill and relaunch with the same config — preserves restart count.",
|
|
236
244
|
"Background processes are auto-classified (server/build/test/watcher) based on the command.",
|
|
237
245
|
"Process crashes and errors are automatically surfaced as alerts at the start of your next turn — you don't need to poll.",
|
|
@@ -300,6 +308,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
300
308
|
group: Type.Optional(
|
|
301
309
|
Type.String({ description: "Group name for related processes (for start, group_status)" }),
|
|
302
310
|
),
|
|
311
|
+
persist_across_sessions: Type.Optional(
|
|
312
|
+
Type.Boolean({
|
|
313
|
+
description: "Keep this process running after a new session starts. Default: false.",
|
|
314
|
+
default: false,
|
|
315
|
+
}),
|
|
316
|
+
),
|
|
303
317
|
}),
|
|
304
318
|
|
|
305
319
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
@@ -318,6 +332,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
318
332
|
const bg = startProcess({
|
|
319
333
|
command: params.command,
|
|
320
334
|
cwd: ctx.cwd,
|
|
335
|
+
ownerSessionFile: ctx.sessionManager.getSessionFile() ?? null,
|
|
336
|
+
persistAcrossSessions: params.persist_across_sessions ?? false,
|
|
321
337
|
label: params.label,
|
|
322
338
|
type: params.type as ProcessType | undefined,
|
|
323
339
|
readyPattern: params.ready_pattern,
|
|
@@ -341,6 +357,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
341
357
|
text += ` cwd: ${toPosixPath(bg.cwd)}`;
|
|
342
358
|
|
|
343
359
|
if (bg.group) text += `\n group: ${bg.group}`;
|
|
360
|
+
if (bg.persistAcrossSessions) text += `\n persist_across_sessions: true`;
|
|
344
361
|
if (bg.readyPort) text += `\n ready_port: ${bg.readyPort}`;
|
|
345
362
|
if (bg.readyPattern) text += `\n ready_pattern: ${bg.readyPattern}`;
|
|
346
363
|
if (bg.ports.length > 0) text += `\n detected ports: ${bg.ports.join(", ")}`;
|
|
@@ -67,6 +67,8 @@ export function getInfo(p: BgProcess): BgProcessInfo {
|
|
|
67
67
|
label: p.label,
|
|
68
68
|
command: p.command,
|
|
69
69
|
cwd: p.cwd,
|
|
70
|
+
ownerSessionFile: p.ownerSessionFile,
|
|
71
|
+
persistAcrossSessions: p.persistAcrossSessions,
|
|
70
72
|
startedAt: p.startedAt,
|
|
71
73
|
alive: p.alive,
|
|
72
74
|
exitCode: p.exitCode,
|
|
@@ -138,6 +140,8 @@ export function startProcess(opts: StartOptions): BgProcess {
|
|
|
138
140
|
label: opts.label || command.slice(0, 60),
|
|
139
141
|
command,
|
|
140
142
|
cwd: opts.cwd,
|
|
143
|
+
ownerSessionFile: opts.ownerSessionFile ?? null,
|
|
144
|
+
persistAcrossSessions: opts.persistAcrossSessions ?? false,
|
|
141
145
|
startedAt: Date.now(),
|
|
142
146
|
proc,
|
|
143
147
|
output: [],
|
|
@@ -170,6 +174,8 @@ export function startProcess(opts: StartOptions): BgProcess {
|
|
|
170
174
|
cwd: opts.cwd,
|
|
171
175
|
label: opts.label || command.slice(0, 60),
|
|
172
176
|
processType,
|
|
177
|
+
ownerSessionFile: opts.ownerSessionFile ?? null,
|
|
178
|
+
persistAcrossSessions: opts.persistAcrossSessions ?? false,
|
|
173
179
|
readyPattern: opts.readyPattern || null,
|
|
174
180
|
readyPort: opts.readyPort || null,
|
|
175
181
|
group: opts.group || null,
|
|
@@ -312,6 +318,8 @@ export async function restartProcess(id: string): Promise<BgProcess | null> {
|
|
|
312
318
|
cwd: config.cwd,
|
|
313
319
|
label: config.label,
|
|
314
320
|
type: config.processType,
|
|
321
|
+
ownerSessionFile: config.ownerSessionFile,
|
|
322
|
+
persistAcrossSessions: config.persistAcrossSessions,
|
|
315
323
|
readyPattern: config.readyPattern || undefined,
|
|
316
324
|
readyPort: config.readyPort || undefined,
|
|
317
325
|
group: config.group || undefined,
|
|
@@ -367,6 +375,41 @@ export function cleanupAll(): void {
|
|
|
367
375
|
processes.clear();
|
|
368
376
|
}
|
|
369
377
|
|
|
378
|
+
async function waitForProcessExit(bg: BgProcess, timeoutMs: number): Promise<boolean> {
|
|
379
|
+
if (!bg.alive) return true;
|
|
380
|
+
await new Promise<void>((resolve) => {
|
|
381
|
+
const done = () => resolve();
|
|
382
|
+
const timer = setTimeout(done, timeoutMs);
|
|
383
|
+
bg.proc.once("exit", () => {
|
|
384
|
+
clearTimeout(timer);
|
|
385
|
+
resolve();
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
return !bg.alive;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export async function cleanupSessionProcesses(
|
|
392
|
+
sessionFile: string,
|
|
393
|
+
options?: { graceMs?: number },
|
|
394
|
+
): Promise<string[]> {
|
|
395
|
+
const graceMs = Math.max(0, options?.graceMs ?? 300);
|
|
396
|
+
const matches = Array.from(processes.values()).filter(
|
|
397
|
+
(bg) => bg.alive && !bg.persistAcrossSessions && bg.ownerSessionFile === sessionFile,
|
|
398
|
+
);
|
|
399
|
+
if (matches.length === 0) return [];
|
|
400
|
+
|
|
401
|
+
for (const bg of matches) {
|
|
402
|
+
killProcess(bg.id, "SIGTERM");
|
|
403
|
+
}
|
|
404
|
+
if (graceMs > 0) {
|
|
405
|
+
await Promise.all(matches.map((bg) => waitForProcessExit(bg, graceMs)));
|
|
406
|
+
}
|
|
407
|
+
for (const bg of matches) {
|
|
408
|
+
if (bg.alive) killProcess(bg.id, "SIGKILL");
|
|
409
|
+
}
|
|
410
|
+
return matches.map((bg) => bg.id);
|
|
411
|
+
}
|
|
412
|
+
|
|
370
413
|
// ── Persistence ────────────────────────────────────────────────────────────
|
|
371
414
|
|
|
372
415
|
export function getManifestPath(cwd: string): string {
|
|
@@ -384,6 +427,8 @@ export function persistManifest(cwd: string): void {
|
|
|
384
427
|
label: p.label,
|
|
385
428
|
command: p.command,
|
|
386
429
|
cwd: p.cwd,
|
|
430
|
+
ownerSessionFile: p.ownerSessionFile,
|
|
431
|
+
persistAcrossSessions: p.persistAcrossSessions,
|
|
387
432
|
startedAt: p.startedAt,
|
|
388
433
|
processType: p.processType,
|
|
389
434
|
group: p.group,
|
|
@@ -53,6 +53,10 @@ export interface BgProcess {
|
|
|
53
53
|
label: string;
|
|
54
54
|
command: string;
|
|
55
55
|
cwd: string;
|
|
56
|
+
/** Session file that created this process (used for per-session cleanup) */
|
|
57
|
+
ownerSessionFile: string | null;
|
|
58
|
+
/** Whether this process should survive a new-session boundary */
|
|
59
|
+
persistAcrossSessions: boolean;
|
|
56
60
|
startedAt: number;
|
|
57
61
|
proc: import("node:child_process").ChildProcess;
|
|
58
62
|
/** Unified chronologically-interleaved output buffer */
|
|
@@ -103,7 +107,17 @@ export interface BgProcess {
|
|
|
103
107
|
/** Restart count */
|
|
104
108
|
restartCount: number;
|
|
105
109
|
/** Original start config for restart */
|
|
106
|
-
startConfig: {
|
|
110
|
+
startConfig: {
|
|
111
|
+
command: string;
|
|
112
|
+
cwd: string;
|
|
113
|
+
label: string;
|
|
114
|
+
processType: ProcessType;
|
|
115
|
+
ownerSessionFile: string | null;
|
|
116
|
+
persistAcrossSessions: boolean;
|
|
117
|
+
readyPattern: string | null;
|
|
118
|
+
readyPort: number | null;
|
|
119
|
+
group: string | null;
|
|
120
|
+
};
|
|
107
121
|
}
|
|
108
122
|
|
|
109
123
|
export interface BgProcessInfo {
|
|
@@ -111,6 +125,8 @@ export interface BgProcessInfo {
|
|
|
111
125
|
label: string;
|
|
112
126
|
command: string;
|
|
113
127
|
cwd: string;
|
|
128
|
+
ownerSessionFile: string | null;
|
|
129
|
+
persistAcrossSessions: boolean;
|
|
114
130
|
startedAt: number;
|
|
115
131
|
alive: boolean;
|
|
116
132
|
exitCode: number | null;
|
|
@@ -133,6 +149,8 @@ export interface BgProcessInfo {
|
|
|
133
149
|
export interface StartOptions {
|
|
134
150
|
command: string;
|
|
135
151
|
cwd: string;
|
|
152
|
+
ownerSessionFile?: string | null;
|
|
153
|
+
persistAcrossSessions?: boolean;
|
|
136
154
|
label?: string;
|
|
137
155
|
type?: ProcessType;
|
|
138
156
|
readyPattern?: string;
|
|
@@ -154,6 +172,8 @@ export interface ProcessManifest {
|
|
|
154
172
|
label: string;
|
|
155
173
|
command: string;
|
|
156
174
|
cwd: string;
|
|
175
|
+
ownerSessionFile: string | null;
|
|
176
|
+
persistAcrossSessions: boolean;
|
|
157
177
|
startedAt: number;
|
|
158
178
|
processType: ProcessType;
|
|
159
179
|
group: string | null;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AutoSession — encapsulates all mutable auto-mode state into a single instance.
|
|
3
|
+
*
|
|
4
|
+
* Replaces ~40 module-level variables scattered across auto.ts with typed
|
|
5
|
+
* properties on a class instance. Benefits:
|
|
6
|
+
*
|
|
7
|
+
* - reset() clears everything in one call (was 25+ manual resets in stopAuto)
|
|
8
|
+
* - toJSON() provides diagnostic snapshots
|
|
9
|
+
* - grep `s.` shows every state access
|
|
10
|
+
* - Constructable for testing
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
14
|
+
import type { GitServiceImpl } from "../git-service.js";
|
|
15
|
+
import type { CaptureEntry } from "../captures.js";
|
|
16
|
+
import type { BudgetAlertLevel } from "../auto-budget.js";
|
|
17
|
+
|
|
18
|
+
// ─── Exported Types ──────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export interface CompletedUnit {
|
|
21
|
+
type: string;
|
|
22
|
+
id: string;
|
|
23
|
+
startedAt: number;
|
|
24
|
+
finishedAt: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CurrentUnit {
|
|
28
|
+
type: string;
|
|
29
|
+
id: string;
|
|
30
|
+
startedAt: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface UnitRouting {
|
|
34
|
+
tier: string;
|
|
35
|
+
modelDowngraded: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface StartModel {
|
|
39
|
+
provider: string;
|
|
40
|
+
id: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface PendingVerificationRetry {
|
|
44
|
+
unitId: string;
|
|
45
|
+
failureContext: string;
|
|
46
|
+
attempt: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
export const MAX_UNIT_DISPATCHES = 3;
|
|
52
|
+
export const STUB_RECOVERY_THRESHOLD = 2;
|
|
53
|
+
export const MAX_LIFETIME_DISPATCHES = 6;
|
|
54
|
+
export const MAX_CONSECUTIVE_SKIPS = 3;
|
|
55
|
+
export const DISPATCH_GAP_TIMEOUT_MS = 5_000;
|
|
56
|
+
export const MAX_SKIP_DEPTH = 20;
|
|
57
|
+
|
|
58
|
+
// ─── AutoSession ─────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
export class AutoSession {
|
|
61
|
+
// ── Lifecycle ────────────────────────────────────────────────────────────
|
|
62
|
+
active = false;
|
|
63
|
+
paused = false;
|
|
64
|
+
stepMode = false;
|
|
65
|
+
verbose = false;
|
|
66
|
+
cmdCtx: ExtensionCommandContext | null = null;
|
|
67
|
+
|
|
68
|
+
// ── Paths ────────────────────────────────────────────────────────────────
|
|
69
|
+
basePath = "";
|
|
70
|
+
originalBasePath = "";
|
|
71
|
+
gitService: GitServiceImpl | null = null;
|
|
72
|
+
|
|
73
|
+
// ── Dispatch counters ────────────────────────────────────────────────────
|
|
74
|
+
readonly unitDispatchCount = new Map<string, number>();
|
|
75
|
+
readonly unitLifetimeDispatches = new Map<string, number>();
|
|
76
|
+
readonly unitRecoveryCount = new Map<string, number>();
|
|
77
|
+
readonly unitConsecutiveSkips = new Map<string, number>();
|
|
78
|
+
readonly completedKeySet = new Set<string>();
|
|
79
|
+
|
|
80
|
+
// ── Timers ───────────────────────────────────────────────────────────────
|
|
81
|
+
unitTimeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
82
|
+
wrapupWarningHandle: ReturnType<typeof setTimeout> | null = null;
|
|
83
|
+
idleWatchdogHandle: ReturnType<typeof setInterval> | null = null;
|
|
84
|
+
continueHereHandle: ReturnType<typeof setInterval> | null = null;
|
|
85
|
+
dispatchGapHandle: ReturnType<typeof setTimeout> | null = null;
|
|
86
|
+
|
|
87
|
+
// ── Current unit ─────────────────────────────────────────────────────────
|
|
88
|
+
currentUnit: CurrentUnit | null = null;
|
|
89
|
+
currentUnitRouting: UnitRouting | null = null;
|
|
90
|
+
completedUnits: CompletedUnit[] = [];
|
|
91
|
+
currentMilestoneId: string | null = null;
|
|
92
|
+
|
|
93
|
+
// ── Model state ──────────────────────────────────────────────────────────
|
|
94
|
+
autoModeStartModel: StartModel | null = null;
|
|
95
|
+
originalModelId: string | null = null;
|
|
96
|
+
originalModelProvider: string | null = null;
|
|
97
|
+
lastBudgetAlertLevel: BudgetAlertLevel = 0;
|
|
98
|
+
|
|
99
|
+
// ── Recovery ─────────────────────────────────────────────────────────────
|
|
100
|
+
pendingCrashRecovery: string | null = null;
|
|
101
|
+
pendingVerificationRetry: PendingVerificationRetry | null = null;
|
|
102
|
+
readonly verificationRetryCount = new Map<string, number>();
|
|
103
|
+
pausedSessionFile: string | null = null;
|
|
104
|
+
resourceVersionOnStart: string | null = null;
|
|
105
|
+
lastStateRebuildAt = 0;
|
|
106
|
+
|
|
107
|
+
// ── Guards ───────────────────────────────────────────────────────────────
|
|
108
|
+
handlingAgentEnd = false;
|
|
109
|
+
dispatching = false;
|
|
110
|
+
skipDepth = 0;
|
|
111
|
+
readonly recentlyEvictedKeys = new Set<string>();
|
|
112
|
+
|
|
113
|
+
// ── Metrics ──────────────────────────────────────────────────────────────
|
|
114
|
+
autoStartTime = 0;
|
|
115
|
+
lastPromptCharCount: number | undefined;
|
|
116
|
+
lastBaselineCharCount: number | undefined;
|
|
117
|
+
pendingQuickTasks: CaptureEntry[] = [];
|
|
118
|
+
|
|
119
|
+
// ── Signal handler ───────────────────────────────────────────────────────
|
|
120
|
+
sigtermHandler: (() => void) | null = null;
|
|
121
|
+
|
|
122
|
+
// ── Methods ──────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
clearTimers(): void {
|
|
125
|
+
if (this.unitTimeoutHandle) { clearTimeout(this.unitTimeoutHandle); this.unitTimeoutHandle = null; }
|
|
126
|
+
if (this.wrapupWarningHandle) { clearTimeout(this.wrapupWarningHandle); this.wrapupWarningHandle = null; }
|
|
127
|
+
if (this.idleWatchdogHandle) { clearInterval(this.idleWatchdogHandle); this.idleWatchdogHandle = null; }
|
|
128
|
+
if (this.continueHereHandle) { clearInterval(this.continueHereHandle); this.continueHereHandle = null; }
|
|
129
|
+
if (this.dispatchGapHandle) { clearTimeout(this.dispatchGapHandle); this.dispatchGapHandle = null; }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
resetDispatchCounters(): void {
|
|
133
|
+
this.unitDispatchCount.clear();
|
|
134
|
+
this.unitLifetimeDispatches.clear();
|
|
135
|
+
this.unitConsecutiveSkips.clear();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
get lockBasePath(): string {
|
|
139
|
+
return this.originalBasePath || this.basePath;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
completeCurrentUnit(): CompletedUnit | null {
|
|
143
|
+
if (!this.currentUnit) return null;
|
|
144
|
+
const done: CompletedUnit = { ...this.currentUnit, finishedAt: Date.now() };
|
|
145
|
+
this.completedUnits.push(done);
|
|
146
|
+
this.currentUnit = null;
|
|
147
|
+
return done;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
reset(): void {
|
|
151
|
+
this.clearTimers();
|
|
152
|
+
|
|
153
|
+
// Lifecycle
|
|
154
|
+
this.active = false;
|
|
155
|
+
this.paused = false;
|
|
156
|
+
this.stepMode = false;
|
|
157
|
+
this.verbose = false;
|
|
158
|
+
this.cmdCtx = null;
|
|
159
|
+
|
|
160
|
+
// Paths
|
|
161
|
+
this.basePath = "";
|
|
162
|
+
this.originalBasePath = "";
|
|
163
|
+
this.gitService = null;
|
|
164
|
+
|
|
165
|
+
// Dispatch
|
|
166
|
+
this.unitDispatchCount.clear();
|
|
167
|
+
this.unitLifetimeDispatches.clear();
|
|
168
|
+
this.unitRecoveryCount.clear();
|
|
169
|
+
this.unitConsecutiveSkips.clear();
|
|
170
|
+
// Note: completedKeySet is intentionally NOT cleared — it persists
|
|
171
|
+
// across restarts to prevent re-dispatching completed units.
|
|
172
|
+
|
|
173
|
+
// Unit
|
|
174
|
+
this.currentUnit = null;
|
|
175
|
+
this.currentUnitRouting = null;
|
|
176
|
+
this.completedUnits = [];
|
|
177
|
+
this.currentMilestoneId = null;
|
|
178
|
+
|
|
179
|
+
// Model
|
|
180
|
+
this.autoModeStartModel = null;
|
|
181
|
+
this.originalModelId = null;
|
|
182
|
+
this.originalModelProvider = null;
|
|
183
|
+
this.lastBudgetAlertLevel = 0;
|
|
184
|
+
|
|
185
|
+
// Recovery
|
|
186
|
+
this.pendingCrashRecovery = null;
|
|
187
|
+
this.pendingVerificationRetry = null;
|
|
188
|
+
this.verificationRetryCount.clear();
|
|
189
|
+
this.pausedSessionFile = null;
|
|
190
|
+
this.resourceVersionOnStart = null;
|
|
191
|
+
this.lastStateRebuildAt = 0;
|
|
192
|
+
|
|
193
|
+
// Guards
|
|
194
|
+
this.handlingAgentEnd = false;
|
|
195
|
+
this.dispatching = false;
|
|
196
|
+
this.skipDepth = 0;
|
|
197
|
+
this.recentlyEvictedKeys.clear();
|
|
198
|
+
|
|
199
|
+
// Metrics
|
|
200
|
+
this.autoStartTime = 0;
|
|
201
|
+
this.lastPromptCharCount = undefined;
|
|
202
|
+
this.lastBaselineCharCount = undefined;
|
|
203
|
+
this.pendingQuickTasks = [];
|
|
204
|
+
|
|
205
|
+
// Signal handler
|
|
206
|
+
this.sigtermHandler = null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
toJSON(): Record<string, unknown> {
|
|
210
|
+
return {
|
|
211
|
+
active: this.active,
|
|
212
|
+
paused: this.paused,
|
|
213
|
+
stepMode: this.stepMode,
|
|
214
|
+
basePath: this.basePath,
|
|
215
|
+
currentMilestoneId: this.currentMilestoneId,
|
|
216
|
+
currentUnit: this.currentUnit,
|
|
217
|
+
completedUnits: this.completedUnits.length,
|
|
218
|
+
completedKeySet: this.completedKeySet.size,
|
|
219
|
+
unitDispatchCount: Object.fromEntries(this.unitDispatchCount),
|
|
220
|
+
dispatching: this.dispatching,
|
|
221
|
+
skipDepth: this.skipDepth,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Budget alert level tracking and enforcement for auto-mode.
|
|
3
|
+
* Pure functions — no module state or side effects.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { BudgetEnforcementMode } from "./types.js";
|
|
7
|
+
|
|
8
|
+
export type BudgetAlertLevel = 0 | 75 | 80 | 90 | 100;
|
|
9
|
+
|
|
10
|
+
export function getBudgetAlertLevel(budgetPct: number): BudgetAlertLevel {
|
|
11
|
+
if (budgetPct >= 1.0) return 100;
|
|
12
|
+
if (budgetPct >= 0.90) return 90;
|
|
13
|
+
if (budgetPct >= 0.80) return 80;
|
|
14
|
+
if (budgetPct >= 0.75) return 75;
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getNewBudgetAlertLevel(previousLevel: BudgetAlertLevel, budgetPct: number): BudgetAlertLevel | null {
|
|
19
|
+
const currentLevel = getBudgetAlertLevel(budgetPct);
|
|
20
|
+
if (currentLevel === 0 || currentLevel <= previousLevel) return null;
|
|
21
|
+
return currentLevel;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getBudgetEnforcementAction(
|
|
25
|
+
enforcement: BudgetEnforcementMode,
|
|
26
|
+
budgetPct: number,
|
|
27
|
+
): "none" | "warn" | "pause" | "halt" {
|
|
28
|
+
if (budgetPct < 1.0) return "none";
|
|
29
|
+
if (enforcement === "halt") return "halt";
|
|
30
|
+
if (enforcement === "pause") return "pause";
|
|
31
|
+
return "warn";
|
|
32
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* or AutoContext dependency. State accessors are passed as callbacks.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { ExtensionContext, ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
9
|
+
import type { ExtensionContext, ExtensionCommandContext, SessionMessageEntry } from "@gsd/pi-coding-agent";
|
|
10
10
|
import type { GSDState } from "./types.js";
|
|
11
11
|
import { getCurrentBranch } from "./worktree.js";
|
|
12
12
|
import { getActiveHook } from "./post-unit-hooks.js";
|
|
@@ -159,6 +159,49 @@ export function formatWidgetTokens(count: number): string {
|
|
|
159
159
|
return `${Math.round(count / 1000000)}M`;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
// ─── ETA Estimation ──────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Estimate remaining time based on average unit duration from the metrics ledger.
|
|
166
|
+
* Returns a formatted string like "~12m remaining" or null if insufficient data.
|
|
167
|
+
*/
|
|
168
|
+
export function estimateTimeRemaining(): string | null {
|
|
169
|
+
const ledger = getLedger();
|
|
170
|
+
if (!ledger || ledger.units.length < 2) return null;
|
|
171
|
+
|
|
172
|
+
const sliceProgress = getRoadmapSlicesSync();
|
|
173
|
+
if (!sliceProgress || sliceProgress.total === 0) return null;
|
|
174
|
+
|
|
175
|
+
const remainingSlices = sliceProgress.total - sliceProgress.done;
|
|
176
|
+
if (remainingSlices <= 0) return null;
|
|
177
|
+
|
|
178
|
+
// Compute average duration per completed slice from the ledger
|
|
179
|
+
const completedSliceUnits = ledger.units.filter(
|
|
180
|
+
u => u.finishedAt > 0 && u.startedAt > 0,
|
|
181
|
+
);
|
|
182
|
+
if (completedSliceUnits.length < 2) return null;
|
|
183
|
+
|
|
184
|
+
const totalDuration = completedSliceUnits.reduce(
|
|
185
|
+
(sum, u) => sum + (u.finishedAt - u.startedAt), 0,
|
|
186
|
+
);
|
|
187
|
+
const avgDuration = totalDuration / completedSliceUnits.length;
|
|
188
|
+
|
|
189
|
+
// Rough estimate: remaining slices × average units per slice × avg duration
|
|
190
|
+
const completedSlices = sliceProgress.done || 1;
|
|
191
|
+
const unitsPerSlice = completedSliceUnits.length / completedSlices;
|
|
192
|
+
const estimatedMs = remainingSlices * unitsPerSlice * avgDuration;
|
|
193
|
+
|
|
194
|
+
if (estimatedMs < 5_000) return null; // Too small to display
|
|
195
|
+
|
|
196
|
+
const s = Math.floor(estimatedMs / 1000);
|
|
197
|
+
if (s < 60) return `~${s}s remaining`;
|
|
198
|
+
const m = Math.floor(s / 60);
|
|
199
|
+
if (m < 60) return `~${m}m remaining`;
|
|
200
|
+
const h = Math.floor(m / 60);
|
|
201
|
+
const rm = m % 60;
|
|
202
|
+
return rm > 0 ? `~${h}h ${rm}m remaining` : `~${h}h remaining`;
|
|
203
|
+
}
|
|
204
|
+
|
|
162
205
|
// ─── Slice Progress Cache ─────────────────────────────────────────────────────
|
|
163
206
|
|
|
164
207
|
/** Cached slice progress for the widget — avoid async in render */
|
|
@@ -277,15 +320,16 @@ export function updateProgressWidget(
|
|
|
277
320
|
tui.requestRender();
|
|
278
321
|
}, 800);
|
|
279
322
|
|
|
280
|
-
// Refresh progress cache from disk every
|
|
323
|
+
// Refresh progress cache from disk every 15s so the widget reflects
|
|
281
324
|
// task/slice completion mid-unit. Without this, the progress bar only
|
|
282
325
|
// updates at dispatch time, appearing frozen during long-running units.
|
|
326
|
+
// 15s (vs 5s) reduces synchronous file I/O on the hot path.
|
|
283
327
|
const progressRefreshTimer = mid ? setInterval(() => {
|
|
284
328
|
try {
|
|
285
329
|
updateSliceProgressCache(accessors.getBasePath(), mid.id, slice?.id);
|
|
286
330
|
cachedLines = undefined;
|
|
287
331
|
} catch { /* non-fatal */ }
|
|
288
|
-
},
|
|
332
|
+
}, 15_000) : null;
|
|
289
333
|
|
|
290
334
|
return {
|
|
291
335
|
render(width: number): string[] {
|
|
@@ -346,6 +390,12 @@ export function updateProgressWidget(
|
|
|
346
390
|
meta += theme.fg("dim", ` · task ${taskNum}/${activeSliceTasks.total}`);
|
|
347
391
|
}
|
|
348
392
|
|
|
393
|
+
// ETA estimate
|
|
394
|
+
const eta = estimateTimeRemaining();
|
|
395
|
+
if (eta) {
|
|
396
|
+
meta += theme.fg("dim", ` · ${eta}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
349
399
|
lines.push(truncateToWidth(`${pad}${bar} ${meta}`, width));
|
|
350
400
|
}
|
|
351
401
|
}
|
|
@@ -370,13 +420,16 @@ export function updateProgressWidget(
|
|
|
370
420
|
let totalCacheRead = 0, totalCacheWrite = 0;
|
|
371
421
|
if (cmdCtx) {
|
|
372
422
|
for (const entry of cmdCtx.sessionManager.getEntries()) {
|
|
373
|
-
if (entry.type === "message"
|
|
374
|
-
const
|
|
375
|
-
if (
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
423
|
+
if (entry.type === "message") {
|
|
424
|
+
const msgEntry = entry as SessionMessageEntry;
|
|
425
|
+
if (msgEntry.message?.role === "assistant") {
|
|
426
|
+
const u = (msgEntry.message as any).usage;
|
|
427
|
+
if (u) {
|
|
428
|
+
totalInput += u.input || 0;
|
|
429
|
+
totalOutput += u.output || 0;
|
|
430
|
+
totalCacheRead += u.cacheRead || 0;
|
|
431
|
+
totalCacheWrite += u.cacheWrite || 0;
|
|
432
|
+
}
|
|
380
433
|
}
|
|
381
434
|
}
|
|
382
435
|
}
|