gsd-pi 2.71.0-dev.977c553 → 2.71.0-dev.e17e0ce
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 +12 -3
- package/dist/headless-events.d.ts +2 -0
- package/dist/headless-events.js +7 -0
- package/dist/headless.js +16 -3
- package/dist/mcp-server.js +6 -6
- package/dist/provider-migrations.d.ts +10 -0
- package/dist/provider-migrations.js +12 -0
- package/dist/resource-loader.js +139 -13
- package/dist/resources/GSD-WORKFLOW.md +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +10 -4
- package/dist/resources/extensions/gsd/auto/infra-errors.js +34 -0
- package/dist/resources/extensions/gsd/auto/loop.js +32 -1
- package/dist/resources/extensions/gsd/auto/phases.js +1 -1
- package/dist/resources/extensions/gsd/auto/session.js +11 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +22 -16
- package/dist/resources/extensions/gsd/auto-model-selection.js +10 -2
- package/dist/resources/extensions/gsd/auto-start.js +13 -6
- package/dist/resources/extensions/gsd/auto-tool-tracking.js +1 -1
- package/dist/resources/extensions/gsd/auto-worktree.js +1 -1
- package/dist/resources/extensions/gsd/auto.js +52 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +66 -51
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +6 -0
- package/dist/resources/extensions/gsd/commands/context.js +15 -6
- package/dist/resources/extensions/gsd/commands/dispatcher.js +12 -2
- package/dist/resources/extensions/gsd/commands/handlers/auto.js +10 -33
- package/dist/resources/extensions/gsd/commands/handlers/core.js +56 -11
- package/dist/resources/extensions/gsd/commands/handlers/notifications-handler.js +15 -6
- package/dist/resources/extensions/gsd/commands/handlers/workflow.js +4 -10
- package/dist/resources/extensions/gsd/dashboard-overlay.js +8 -3
- package/dist/resources/extensions/gsd/dispatch-guard.js +18 -1
- package/dist/resources/extensions/gsd/error-classifier.js +1 -1
- package/dist/resources/extensions/gsd/forensics.js +19 -6
- package/dist/resources/extensions/gsd/guided-flow.js +5 -10
- package/dist/resources/extensions/gsd/metrics.js +1 -0
- package/dist/resources/extensions/gsd/milestone-actions.js +10 -4
- package/dist/resources/extensions/gsd/notification-overlay.js +20 -5
- package/dist/resources/extensions/gsd/notification-store.js +51 -1
- package/dist/resources/extensions/gsd/notification-widget.js +5 -13
- package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +8 -3
- package/dist/resources/extensions/gsd/pre-execution-checks.js +35 -2
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +2 -2
- package/dist/resources/extensions/gsd/prompts/discuss.md +2 -0
- package/dist/resources/extensions/gsd/prompts/execute-task.md +20 -19
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +2 -0
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +3 -2
- package/dist/resources/extensions/gsd/prompts/system.md +1 -0
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -1
- package/dist/resources/extensions/gsd/session-model-override.js +25 -0
- package/dist/resources/extensions/gsd/shortcut-defs.js +34 -0
- package/dist/resources/skills/create-skill/SKILL.md +2 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- 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.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/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- 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-paths-manifest.json +13 -13
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +21 -11
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +110 -0
- package/packages/mcp-server/src/workflow-tools.ts +31 -11
- package/packages/pi-ai/dist/providers/amazon-bedrock.js +11 -2
- package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-auth.test.d.ts +2 -0
- package/packages/pi-ai/dist/providers/anthropic-auth.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-auth.test.js +20 -0
- package/packages/pi-ai/dist/providers/anthropic-auth.test.js.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +4 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.js +8 -3
- package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.test.js +44 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.test.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.d.ts +2 -1
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +7 -4
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +11 -0
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/src/providers/amazon-bedrock.ts +13 -1
- package/packages/pi-ai/src/providers/anthropic-auth.test.ts +32 -0
- package/packages/pi-ai/src/providers/anthropic-shared.test.ts +55 -1
- package/packages/pi-ai/src/providers/anthropic-shared.ts +14 -3
- package/packages/pi-ai/src/providers/anthropic.ts +8 -4
- package/packages/pi-ai/src/providers/openai-completions.ts +14 -0
- package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.js +61 -0
- package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +2 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +10 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js +27 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js +85 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver-initial-model-auth.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-resolver-initial-model-auth.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver-initial-model-auth.test.js +64 -0
- package/packages/pi-coding-agent/dist/core/model-resolver-initial-model-auth.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +22 -18
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.d.ts +11 -0
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +38 -5
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/sdk.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/sdk.test.js +71 -0
- package/packages/pi-coding-agent/dist/core/sdk.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/login-dialog.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/login-dialog.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/login-dialog.test.js +13 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/login-dialog.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js +24 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +43 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +7 -2
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -3
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js +4 -2
- package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-renderable-tools.test.ts +70 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +2 -1
- package/packages/pi-coding-agent/src/core/auth-storage.test.ts +108 -0
- package/packages/pi-coding-agent/src/core/auth-storage.ts +30 -0
- package/packages/pi-coding-agent/src/core/model-resolver-initial-model-auth.test.ts +78 -0
- package/packages/pi-coding-agent/src/core/model-resolver.ts +22 -18
- package/packages/pi-coding-agent/src/core/sdk.test.ts +89 -0
- package/packages/pi-coding-agent/src/core/sdk.ts +45 -9
- package/packages/pi-coding-agent/src/index.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/login-dialog.test.ts +24 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts +30 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +47 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +7 -2
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +4 -3
- package/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts +4 -2
- package/src/resources/GSD-WORKFLOW.md +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +13 -5
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +56 -4
- package/src/resources/extensions/gsd/auto/infra-errors.ts +38 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -0
- package/src/resources/extensions/gsd/auto/loop.ts +45 -1
- package/src/resources/extensions/gsd/auto/phases.ts +2 -0
- package/src/resources/extensions/gsd/auto/session.ts +11 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +29 -18
- package/src/resources/extensions/gsd/auto-model-selection.ts +9 -1
- package/src/resources/extensions/gsd/auto-start.ts +13 -6
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +1 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +1 -1
- package/src/resources/extensions/gsd/auto.ts +68 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +82 -60
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +7 -0
- package/src/resources/extensions/gsd/commands/context.ts +16 -5
- package/src/resources/extensions/gsd/commands/dispatcher.ts +14 -2
- package/src/resources/extensions/gsd/commands/handlers/auto.ts +10 -36
- package/src/resources/extensions/gsd/commands/handlers/core.ts +58 -11
- package/src/resources/extensions/gsd/commands/handlers/notifications-handler.ts +17 -7
- package/src/resources/extensions/gsd/commands/handlers/workflow.ts +4 -10
- package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -3
- package/src/resources/extensions/gsd/dispatch-guard.ts +18 -1
- package/src/resources/extensions/gsd/error-classifier.ts +1 -1
- package/src/resources/extensions/gsd/forensics.ts +23 -7
- package/src/resources/extensions/gsd/guided-flow.ts +5 -10
- package/src/resources/extensions/gsd/interrupted-session.ts +1 -0
- package/src/resources/extensions/gsd/metrics.ts +12 -1
- package/src/resources/extensions/gsd/milestone-actions.ts +10 -3
- package/src/resources/extensions/gsd/notification-overlay.ts +24 -7
- package/src/resources/extensions/gsd/notification-store.ts +49 -1
- package/src/resources/extensions/gsd/notification-widget.ts +5 -14
- package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +10 -3
- package/src/resources/extensions/gsd/pre-execution-checks.ts +39 -2
- package/src/resources/extensions/gsd/prompts/complete-slice.md +2 -2
- package/src/resources/extensions/gsd/prompts/discuss.md +2 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +20 -19
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +2 -0
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +3 -2
- package/src/resources/extensions/gsd/prompts/system.md +1 -0
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -1
- package/src/resources/extensions/gsd/session-model-override.ts +36 -0
- package/src/resources/extensions/gsd/shortcut-defs.ts +49 -0
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +11 -9
- package/src/resources/extensions/gsd/tests/auto-start-worktree-db-path.test.ts +28 -0
- package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/complete-slice-prompt-task-summary-layout.test.ts +18 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/execute-task-prompt-existing-artifact-guard.test.ts +33 -0
- package/src/resources/extensions/gsd/tests/forensics-stuck-loops.test.ts +62 -0
- package/src/resources/extensions/gsd/tests/format-shortcut.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/gsd-no-project-error.test.ts +73 -0
- package/src/resources/extensions/gsd/tests/infra-errors-cooldown.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +66 -1
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +36 -51
- package/src/resources/extensions/gsd/tests/notification-store.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/notification-widget.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/notifications-handler.test.ts +90 -0
- package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/park-db-sync.test.ts +18 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/register-shortcuts.test.ts +62 -5
- package/src/resources/extensions/gsd/tests/session-model-override.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +90 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +18 -0
- package/src/resources/skills/create-skill/SKILL.md +2 -0
- /package/dist/web/standalone/.next/static/{4xyaXTn7-shVHaGMcl75o → cYPZv_bAhZk2ms-Pz6vsY}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{4xyaXTn7-shVHaGMcl75o → cYPZv_bAhZk2ms-Pz6vsY}/_ssgManifest.js +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// View, filter, and clear the persistent notification history.
|
|
3
3
|
import { readNotifications, clearNotifications, getUnreadCount, suppressPersistence, unsuppressPersistence, } from "../../notification-store.js";
|
|
4
4
|
import { GSDNotificationOverlay } from "../../notification-overlay.js";
|
|
5
|
+
const MAX_INLINE_ENTRIES = 40;
|
|
5
6
|
function severityIcon(severity) {
|
|
6
7
|
switch (severity) {
|
|
7
8
|
case "error": return "✗";
|
|
@@ -38,14 +39,18 @@ export async function handleNotificationsCommand(args, ctx, pi) {
|
|
|
38
39
|
if (args === "tail" || args.startsWith("tail ")) {
|
|
39
40
|
const countStr = args.replace(/^tail\s*/, "").trim();
|
|
40
41
|
const count = countStr ? parseInt(countStr, 10) : 20;
|
|
41
|
-
const
|
|
42
|
-
const
|
|
42
|
+
const all = readNotifications();
|
|
43
|
+
const n = isNaN(count) || count < 1 ? 20 : Math.min(count, MAX_INLINE_ENTRIES);
|
|
44
|
+
const entries = all.slice(0, n);
|
|
43
45
|
if (entries.length === 0) {
|
|
44
46
|
ctx.ui.notify("No notifications.", "info");
|
|
45
47
|
return true;
|
|
46
48
|
}
|
|
47
49
|
const lines = entries.map((e) => `${severityIcon(e.severity)} [${formatTimestamp(e.ts)}] ${e.message}`);
|
|
48
|
-
|
|
50
|
+
const suffix = all.length > entries.length
|
|
51
|
+
? `\n... and ${all.length - entries.length} more (open /gsd notifications to browse all)`
|
|
52
|
+
: "";
|
|
53
|
+
ctx.ui.notify(`Last ${entries.length} notification(s):\n${lines.join("\n")}${suffix}`, "info");
|
|
49
54
|
return true;
|
|
50
55
|
}
|
|
51
56
|
// /gsd notifications filter <severity>
|
|
@@ -61,7 +66,9 @@ export async function handleNotificationsCommand(args, ctx, pi) {
|
|
|
61
66
|
return true;
|
|
62
67
|
}
|
|
63
68
|
const lines = entries.slice(0, 20).map((e) => `${severityIcon(e.severity)} [${formatTimestamp(e.ts)}] ${e.message}`);
|
|
64
|
-
const suffix = entries.length > 20
|
|
69
|
+
const suffix = entries.length > 20
|
|
70
|
+
? `\n... and ${entries.length - 20} more (open /gsd notifications to browse all)`
|
|
71
|
+
: "";
|
|
65
72
|
ctx.ui.notify(`${severity} notifications (${entries.length}):\n${lines.join("\n")}${suffix}`, "info");
|
|
66
73
|
return true;
|
|
67
74
|
}
|
|
@@ -70,7 +77,7 @@ export async function handleNotificationsCommand(args, ctx, pi) {
|
|
|
70
77
|
// Try overlay first (TUI mode)
|
|
71
78
|
if (ctx.hasUI) {
|
|
72
79
|
try {
|
|
73
|
-
await ctx.ui.custom((tui, theme, _kb, done) => new GSDNotificationOverlay(tui, theme, () => done()), {
|
|
80
|
+
const result = await ctx.ui.custom((tui, theme, _kb, done) => new GSDNotificationOverlay(tui, theme, () => done(true)), {
|
|
74
81
|
overlay: true,
|
|
75
82
|
overlayOptions: {
|
|
76
83
|
width: "80%",
|
|
@@ -80,7 +87,9 @@ export async function handleNotificationsCommand(args, ctx, pi) {
|
|
|
80
87
|
backdrop: true,
|
|
81
88
|
},
|
|
82
89
|
});
|
|
83
|
-
|
|
90
|
+
if (result !== undefined) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
84
93
|
}
|
|
85
94
|
catch {
|
|
86
95
|
// Fall through to text output if overlay fails
|
|
@@ -12,7 +12,7 @@ import { nextMilestoneId } from "../../milestone-ids.js";
|
|
|
12
12
|
import { findMilestoneIds } from "../../guided-flow.js";
|
|
13
13
|
import { projectRoot } from "../context.js";
|
|
14
14
|
import { createRun, listRuns } from "../../run-manager.js";
|
|
15
|
-
import { setActiveEngineId, setActiveRunDir,
|
|
15
|
+
import { setActiveEngineId, setActiveRunDir, startAutoDetached, pauseAuto, isAutoActive, getActiveEngineId, } from "../../auto.js";
|
|
16
16
|
import { validateDefinition } from "../../definition-loader.js";
|
|
17
17
|
// ─── Custom Workflow Subcommands ─────────────────────────────────────────
|
|
18
18
|
const WORKFLOW_USAGE = [
|
|
@@ -58,7 +58,7 @@ async function handleCustomWorkflow(sub, ctx, pi) {
|
|
|
58
58
|
setActiveEngineId("custom");
|
|
59
59
|
setActiveRunDir(runDir);
|
|
60
60
|
ctx.ui.notify(`Created workflow run: ${defName}\nRun dir: ${runDir}`, "info");
|
|
61
|
-
|
|
61
|
+
startAutoDetached(ctx, pi, base, false);
|
|
62
62
|
}
|
|
63
63
|
catch (err) {
|
|
64
64
|
// Clean up engine state so a failed workflow run doesn't pollute the next /gsd auto
|
|
@@ -137,14 +137,8 @@ async function handleCustomWorkflow(sub, ctx, pi) {
|
|
|
137
137
|
ctx.ui.notify("No custom workflow to resume. Use /gsd auto for dev workflow.", "warning");
|
|
138
138
|
return true;
|
|
139
139
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
ctx.ui.notify("Custom workflow resumed.", "info");
|
|
143
|
-
}
|
|
144
|
-
catch (err) {
|
|
145
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
146
|
-
ctx.ui.notify(`Failed to resume workflow: ${msg}`, "error");
|
|
147
|
-
}
|
|
140
|
+
startAutoDetached(ctx, pi, projectRoot(), false);
|
|
141
|
+
ctx.ui.notify("Custom workflow resumed.", "info");
|
|
148
142
|
return true;
|
|
149
143
|
}
|
|
150
144
|
// Unknown subcommand — show usage
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Full-screen overlay showing auto-mode progress: milestone/slice/task
|
|
5
5
|
* breakdown, current unit, completed units, timing, and activity log.
|
|
6
|
-
* Toggled with Ctrl+Alt+G (⌃⌥G on macOS)
|
|
6
|
+
* Toggled with Ctrl+Alt+G (⌃⌥G on macOS), Ctrl+Shift+G fallback,
|
|
7
|
+
* or opened from /gsd status.
|
|
7
8
|
*/
|
|
8
9
|
import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui";
|
|
9
10
|
import { deriveState } from "./state.js";
|
|
@@ -19,6 +20,7 @@ import { formatDuration, padRight, joinColumns, centerLine, fitColumns, STATUS_G
|
|
|
19
20
|
import { estimateTimeRemaining } from "./auto-dashboard.js";
|
|
20
21
|
import { computeProgressScore } from "./progress-score.js";
|
|
21
22
|
import { runEnvironmentChecks } from "./doctor-environment.js";
|
|
23
|
+
import { formattedShortcutPair } from "./shortcut-defs.js";
|
|
22
24
|
function unitLabel(type) {
|
|
23
25
|
switch (type) {
|
|
24
26
|
case "discuss-milestone":
|
|
@@ -174,7 +176,10 @@ export class GSDDashboardOverlay {
|
|
|
174
176
|
}
|
|
175
177
|
}
|
|
176
178
|
handleInput(data) {
|
|
177
|
-
if (matchesKey(data, Key.escape) ||
|
|
179
|
+
if (matchesKey(data, Key.escape) ||
|
|
180
|
+
matchesKey(data, Key.ctrl("c")) ||
|
|
181
|
+
matchesKey(data, Key.ctrlAlt("g")) ||
|
|
182
|
+
matchesKey(data, Key.ctrlShift("g"))) {
|
|
178
183
|
this.dispose();
|
|
179
184
|
this.onClose();
|
|
180
185
|
return;
|
|
@@ -507,7 +512,7 @@ export class GSDDashboardOverlay {
|
|
|
507
512
|
}
|
|
508
513
|
lines.push(blank());
|
|
509
514
|
lines.push(hr());
|
|
510
|
-
lines.push(centered(th.fg("dim",
|
|
515
|
+
lines.push(centered(th.fg("dim", `↑↓ scroll · g/G top/end · Esc/${formattedShortcutPair("dashboard")} close`)));
|
|
511
516
|
return lines;
|
|
512
517
|
}
|
|
513
518
|
renderProgressRow(label, done, total, color, width) {
|
|
@@ -102,10 +102,27 @@ export function getPriorSliceCompletionBlocker(base, _mainBranch, unitType, unit
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
else {
|
|
105
|
+
// Positional fallback is only a heuristic for legacy slices with no
|
|
106
|
+
// declared dependencies. Skip any earlier slice that depends on the
|
|
107
|
+
// target, directly or transitively, or we can deadlock a valid zero-dep
|
|
108
|
+
// slice behind its own downstream dependents (#3720).
|
|
109
|
+
const reverseDependents = new Set();
|
|
110
|
+
let changed = true;
|
|
111
|
+
while (changed) {
|
|
112
|
+
changed = false;
|
|
113
|
+
for (const slice of slices) {
|
|
114
|
+
if (reverseDependents.has(slice.id))
|
|
115
|
+
continue;
|
|
116
|
+
if (slice.depends.some((depId) => depId === targetSid || reverseDependents.has(depId))) {
|
|
117
|
+
reverseDependents.add(slice.id);
|
|
118
|
+
changed = true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
105
122
|
const targetIndex = slices.findIndex((slice) => slice.id === targetSid);
|
|
106
123
|
const incomplete = slices
|
|
107
124
|
.slice(0, targetIndex)
|
|
108
|
-
.find((slice) => !slice.done);
|
|
125
|
+
.find((slice) => !slice.done && !reverseDependents.has(slice.id));
|
|
109
126
|
if (incomplete) {
|
|
110
127
|
return `Cannot dispatch ${unitType} ${unitId}: earlier slice ${targetMid}/${incomplete.id} is not complete.`;
|
|
111
128
|
}
|
|
@@ -23,7 +23,7 @@ const RATE_LIMIT_RE = /rate.?limit|too many requests|429/i;
|
|
|
23
23
|
const NETWORK_RE = /network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|fetch failed|connection.*reset|dns/i;
|
|
24
24
|
const SERVER_RE = /internal server error|500|502|503|overloaded|server_error|api_error|service.?unavailable/i;
|
|
25
25
|
// ECONNRESET/ECONNREFUSED are in NETWORK_RE (same-model retry first).
|
|
26
|
-
const CONNECTION_RE = /terminated|connection.?refused|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
|
|
26
|
+
const CONNECTION_RE = /terminated|connection.?(?:refused|error)|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
|
|
27
27
|
// Catch-all for V8 JSON.parse errors: all modern variants end with "in JSON at position \d+".
|
|
28
28
|
// This eliminates the need to enumerate every error message variant individually.
|
|
29
29
|
const STREAM_RE = /in JSON at position \d+|Unexpected end of JSON|SyntaxError.*JSON/i;
|
|
@@ -508,19 +508,30 @@ function getDbCompletionCounts() {
|
|
|
508
508
|
* Exported for testability.
|
|
509
509
|
*/
|
|
510
510
|
export function detectStuckLoops(units, anomalies) {
|
|
511
|
-
// First, collect unique startedAt values per type/id key
|
|
511
|
+
// First, collect unique startedAt values per type/id key, bucketed by
|
|
512
|
+
// autoSessionKey when available so cross-session recovery does not look
|
|
513
|
+
// like a within-session stuck loop.
|
|
512
514
|
const dispatchMap = new Map();
|
|
513
515
|
for (const u of units) {
|
|
514
516
|
const key = `${u.type}/${u.id}`;
|
|
515
|
-
let
|
|
517
|
+
let sessionBuckets = dispatchMap.get(key);
|
|
518
|
+
if (!sessionBuckets) {
|
|
519
|
+
sessionBuckets = new Map();
|
|
520
|
+
dispatchMap.set(key, sessionBuckets);
|
|
521
|
+
}
|
|
522
|
+
const sessionKey = u.autoSessionKey ?? "__legacy__";
|
|
523
|
+
let starts = sessionBuckets.get(sessionKey);
|
|
516
524
|
if (!starts) {
|
|
517
525
|
starts = new Set();
|
|
518
|
-
|
|
526
|
+
sessionBuckets.set(sessionKey, starts);
|
|
519
527
|
}
|
|
520
528
|
starts.add(u.startedAt);
|
|
521
529
|
}
|
|
522
|
-
for (const [key,
|
|
523
|
-
const
|
|
530
|
+
for (const [key, sessionBuckets] of dispatchMap) {
|
|
531
|
+
const hasSessionAwareData = Array.from(sessionBuckets.keys()).some((sessionKey) => sessionKey !== "__legacy__");
|
|
532
|
+
const count = hasSessionAwareData
|
|
533
|
+
? Math.max(...Array.from(sessionBuckets.values(), (starts) => starts.size))
|
|
534
|
+
: (sessionBuckets.get("__legacy__")?.size ?? 0);
|
|
524
535
|
if (count > 1) {
|
|
525
536
|
const [unitType, ...idParts] = key.split("/");
|
|
526
537
|
anomalies.push({
|
|
@@ -529,7 +540,9 @@ export function detectStuckLoops(units, anomalies) {
|
|
|
529
540
|
unitType,
|
|
530
541
|
unitId: idParts.join("/"),
|
|
531
542
|
summary: `Unit ${key} was dispatched ${count} times`,
|
|
532
|
-
details:
|
|
543
|
+
details: hasSessionAwareData
|
|
544
|
+
? `Repeated dispatch within the same auto session suggests the unit completed but its artifacts were not verified, or the state machine kept returning it. Cross-session recovery runs are ignored.`
|
|
545
|
+
: `Repeated dispatch suggests the unit completed but its artifacts weren't verified, or the state machine kept returning it.`,
|
|
533
546
|
});
|
|
534
547
|
}
|
|
535
548
|
}
|
|
@@ -13,7 +13,7 @@ import { loadPrompt, inlineTemplate } from "./prompt-loader.js";
|
|
|
13
13
|
import { buildSkillActivationBlock } from "./auto-prompts.js";
|
|
14
14
|
import { deriveState } from "./state.js";
|
|
15
15
|
import { invalidateAllCaches } from "./cache.js";
|
|
16
|
-
import {
|
|
16
|
+
import { startAutoDetached } from "./auto.js";
|
|
17
17
|
import { clearLock } from "./crash-recovery.js";
|
|
18
18
|
import { assessInterruptedSession, formatInterruptedSessionRunningMessage, formatInterruptedSessionSummary, } from "./interrupted-session.js";
|
|
19
19
|
import { listUnitRuntimeRecords, clearUnitRuntimeRecord } from "./unit-runtime.js";
|
|
@@ -40,7 +40,6 @@ import { runPreparation, formatCodebaseBrief, formatPriorContextBrief, } from ".
|
|
|
40
40
|
// ─── Re-exports (preserve public API for existing importers) ────────────────
|
|
41
41
|
export { MILESTONE_ID_RE, generateMilestoneSuffix, nextMilestoneId, extractMilestoneSeq, parseMilestoneId, milestoneIdSort, maxMilestoneNum, findMilestoneIds, reserveMilestoneId, claimReservedId, getReservedMilestoneIds, clearReservedMilestoneIds, } from "./milestone-ids.js";
|
|
42
42
|
export { showQueue, handleQueueReorder, showQueueAdd, buildExistingMilestonesContext, } from "./guided-flow-queue.js";
|
|
43
|
-
import { getErrorMessage } from "./error-utils.js";
|
|
44
43
|
import { logWarning } from "./workflow-logger.js";
|
|
45
44
|
// ─── ID Generation with Reservation ─────────────────────────────────────────
|
|
46
45
|
/**
|
|
@@ -200,11 +199,7 @@ export function checkAutoStartAfterDiscuss() {
|
|
|
200
199
|
}
|
|
201
200
|
pendingAutoStartMap.delete(basePath);
|
|
202
201
|
ctx.ui.notify(`Milestone ${milestoneId} ready.`, "info");
|
|
203
|
-
|
|
204
|
-
ctx.ui.notify(`Auto-start failed: ${getErrorMessage(err)}`, "error");
|
|
205
|
-
logWarning("guided", `auto start error: ${getErrorMessage(err)}`);
|
|
206
|
-
debugLog("auto-start-failed", { error: getErrorMessage(err) });
|
|
207
|
-
});
|
|
202
|
+
startAutoDetached(ctx, pi, basePath, false, { step });
|
|
208
203
|
return true;
|
|
209
204
|
}
|
|
210
205
|
/**
|
|
@@ -1097,7 +1092,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
1097
1092
|
],
|
|
1098
1093
|
});
|
|
1099
1094
|
if (resume === "resume") {
|
|
1100
|
-
|
|
1095
|
+
startAutoDetached(ctx, pi, basePath, false, {
|
|
1101
1096
|
interrupted,
|
|
1102
1097
|
step: interrupted.pausedSession?.stepMode ?? false,
|
|
1103
1098
|
});
|
|
@@ -1409,7 +1404,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
1409
1404
|
notYetMessage: "Run /gsd status for details.",
|
|
1410
1405
|
});
|
|
1411
1406
|
if (choice === "auto") {
|
|
1412
|
-
|
|
1407
|
+
startAutoDetached(ctx, pi, basePath, false);
|
|
1413
1408
|
}
|
|
1414
1409
|
else if (choice === "status") {
|
|
1415
1410
|
const { fireStatusViaCommand } = await import("./commands.js");
|
|
@@ -1622,7 +1617,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
1622
1617
|
notYetMessage: "Run /gsd when ready.",
|
|
1623
1618
|
});
|
|
1624
1619
|
if (choice === "auto") {
|
|
1625
|
-
|
|
1620
|
+
startAutoDetached(ctx, pi, basePath, false);
|
|
1626
1621
|
return;
|
|
1627
1622
|
}
|
|
1628
1623
|
if (choice === "execute") {
|
|
@@ -15,7 +15,7 @@ import { join } from "node:path";
|
|
|
15
15
|
import { resolveMilestonePath, resolveMilestoneFile, buildMilestoneFileName, } from "./paths.js";
|
|
16
16
|
import { invalidateAllCaches } from "./cache.js";
|
|
17
17
|
import { loadQueueOrder, saveQueueOrder } from "./queue-order.js";
|
|
18
|
-
import { isDbAvailable, updateMilestoneStatus } from "./gsd-db.js";
|
|
18
|
+
import { getMilestone, isDbAvailable, updateMilestoneStatus } from "./gsd-db.js";
|
|
19
19
|
import { logWarning } from "./workflow-logger.js";
|
|
20
20
|
// ─── Park ──────────────────────────────────────────────────────────────────
|
|
21
21
|
/**
|
|
@@ -68,9 +68,15 @@ export function unparkMilestone(basePath, milestoneId) {
|
|
|
68
68
|
if (!mDir || !existsSync(mDir))
|
|
69
69
|
return false;
|
|
70
70
|
const parkedPath = join(mDir, buildMilestoneFileName(milestoneId, "PARKED"));
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
const hadParkedFile = existsSync(parkedPath);
|
|
72
|
+
const dbThinksParked = isDbAvailable() && getMilestone(milestoneId)?.status === "parked";
|
|
73
|
+
// Recover the reverse desync too: DB can still say "parked" even when the
|
|
74
|
+
// PARKED marker was lost on disk, and /gsd unpark should repair that state.
|
|
75
|
+
if (!hadParkedFile && !dbThinksParked)
|
|
76
|
+
return false;
|
|
77
|
+
if (hadParkedFile) {
|
|
78
|
+
unlinkSync(parkedPath);
|
|
79
|
+
}
|
|
74
80
|
// Sync DB status so deriveStateFromDb picks up the unparked milestone (#2694)
|
|
75
81
|
if (isDbAvailable()) {
|
|
76
82
|
try {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// GSD Extension — Notification History Overlay
|
|
2
2
|
// Scrollable panel showing all persisted notifications with severity filtering.
|
|
3
|
-
// Toggled with Ctrl+Alt+N (⌃⌥N on macOS)
|
|
3
|
+
// Toggled with Ctrl+Alt+N (⌃⌥N on macOS), Ctrl+Shift+N fallback, or /gsd notifications.
|
|
4
4
|
import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui";
|
|
5
5
|
import { readNotifications, markAllRead, clearNotifications, } from "./notification-store.js";
|
|
6
|
+
import { formattedShortcutPair } from "./shortcut-defs.js";
|
|
6
7
|
import { padRight, joinColumns } from "../shared/mod.js";
|
|
7
8
|
const FILTER_CYCLE = ["all", "error", "warning", "info"];
|
|
8
9
|
function severityIcon(severity) {
|
|
@@ -55,6 +56,11 @@ function formatTimestamp(ts) {
|
|
|
55
56
|
return ts.slice(11, 19); // fallback: HH:MM:SS
|
|
56
57
|
}
|
|
57
58
|
}
|
|
59
|
+
function notificationSignature(entries) {
|
|
60
|
+
return entries
|
|
61
|
+
.map((entry) => `${entry.ts}|${entry.severity}|${entry.read ? 1 : 0}|${entry.message}`)
|
|
62
|
+
.join("\n");
|
|
63
|
+
}
|
|
58
64
|
export class GSDNotificationOverlay {
|
|
59
65
|
tui;
|
|
60
66
|
theme;
|
|
@@ -64,6 +70,7 @@ export class GSDNotificationOverlay {
|
|
|
64
70
|
scrollOffset = 0;
|
|
65
71
|
filterIndex = 0;
|
|
66
72
|
entries = [];
|
|
73
|
+
entriesSignature = "";
|
|
67
74
|
refreshTimer;
|
|
68
75
|
disposed = false;
|
|
69
76
|
resizeHandler = null;
|
|
@@ -74,6 +81,7 @@ export class GSDNotificationOverlay {
|
|
|
74
81
|
// Mark all as read on open
|
|
75
82
|
markAllRead();
|
|
76
83
|
this.entries = readNotifications();
|
|
84
|
+
this.entriesSignature = notificationSignature(this.entries);
|
|
77
85
|
// Resize handler
|
|
78
86
|
this.resizeHandler = () => {
|
|
79
87
|
if (this.disposed)
|
|
@@ -87,9 +95,11 @@ export class GSDNotificationOverlay {
|
|
|
87
95
|
if (this.disposed)
|
|
88
96
|
return;
|
|
89
97
|
const fresh = readNotifications();
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
const signature = notificationSignature(fresh);
|
|
99
|
+
if (signature !== this.entriesSignature) {
|
|
92
100
|
markAllRead();
|
|
101
|
+
this.entries = readNotifications();
|
|
102
|
+
this.entriesSignature = notificationSignature(this.entries);
|
|
93
103
|
this.invalidate();
|
|
94
104
|
this.tui.requestRender();
|
|
95
105
|
}
|
|
@@ -104,7 +114,10 @@ export class GSDNotificationOverlay {
|
|
|
104
114
|
return this.entries.filter((e) => e.severity === this.filter);
|
|
105
115
|
}
|
|
106
116
|
handleInput(data) {
|
|
107
|
-
if (matchesKey(data, Key.escape) ||
|
|
117
|
+
if (matchesKey(data, Key.escape) ||
|
|
118
|
+
matchesKey(data, Key.ctrl("c")) ||
|
|
119
|
+
matchesKey(data, Key.ctrlAlt("n")) ||
|
|
120
|
+
matchesKey(data, Key.ctrlShift("n"))) {
|
|
108
121
|
this.dispose();
|
|
109
122
|
this.onClose();
|
|
110
123
|
return;
|
|
@@ -146,6 +159,7 @@ export class GSDNotificationOverlay {
|
|
|
146
159
|
if (data === "c") {
|
|
147
160
|
clearNotifications();
|
|
148
161
|
this.entries = [];
|
|
162
|
+
this.entriesSignature = notificationSignature(this.entries);
|
|
149
163
|
this.scrollOffset = 0;
|
|
150
164
|
this.invalidate();
|
|
151
165
|
this.tui.requestRender();
|
|
@@ -215,7 +229,8 @@ export class GSDNotificationOverlay {
|
|
|
215
229
|
lines.push(row(joinColumns(`${title} ${th.fg("dim", "filter:")} ${filterLabel}`, th.fg("dim", count), contentWidth)));
|
|
216
230
|
lines.push(hr());
|
|
217
231
|
// Controls
|
|
218
|
-
|
|
232
|
+
const closeShortcut = formattedShortcutPair("notifications");
|
|
233
|
+
lines.push(row(th.fg("dim", `↑/↓ scroll f filter c clear Esc close (${closeShortcut})`)));
|
|
219
234
|
lines.push(blank());
|
|
220
235
|
// Entries
|
|
221
236
|
const filtered = this.filteredEntries;
|
|
@@ -9,16 +9,23 @@ import { randomUUID } from "node:crypto";
|
|
|
9
9
|
const MAX_ENTRIES = 500;
|
|
10
10
|
const FILENAME = "notifications.jsonl";
|
|
11
11
|
const LOCKFILE = "notifications.lock";
|
|
12
|
+
const DEDUP_WINDOW_MS = 30_000;
|
|
13
|
+
const DEDUP_PRUNE_THRESHOLD = 200;
|
|
12
14
|
// ─── Module State ───────────────────────────────────────────────────────
|
|
13
15
|
let _basePath = null;
|
|
14
16
|
let _lineCount = 0; // Hint for rotation — not authoritative for public API
|
|
15
17
|
let _suppressCount = 0;
|
|
18
|
+
let _recentMessageTimestamps = new Map();
|
|
19
|
+
const _changeListeners = new Set();
|
|
16
20
|
// ─── Public API ─────────────────────────────────────────────────────────
|
|
17
21
|
/**
|
|
18
22
|
* Initialize the notification store. Call once at session start with the
|
|
19
23
|
* project root. Seeds in-memory counters from the existing file on disk.
|
|
20
24
|
*/
|
|
21
25
|
export function initNotificationStore(basePath) {
|
|
26
|
+
if (_basePath !== basePath) {
|
|
27
|
+
_recentMessageTimestamps.clear();
|
|
28
|
+
}
|
|
22
29
|
_basePath = basePath;
|
|
23
30
|
// Seed line count hint for rotation — public counters read from disk
|
|
24
31
|
_lineCount = _readEntriesFromDisk(basePath).length;
|
|
@@ -32,11 +39,24 @@ export function appendNotification(message, severity, source = "notify") {
|
|
|
32
39
|
return;
|
|
33
40
|
if (_suppressCount > 0)
|
|
34
41
|
return;
|
|
42
|
+
const persistedMessage = message.length > 500 ? message.slice(0, 500) + "…" : message;
|
|
43
|
+
const dedupKey = `${_basePath}:${severity}:${source}:${persistedMessage}`;
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const lastSeen = _recentMessageTimestamps.get(dedupKey);
|
|
46
|
+
if (lastSeen !== undefined && now - lastSeen < DEDUP_WINDOW_MS)
|
|
47
|
+
return;
|
|
48
|
+
_recentMessageTimestamps.set(dedupKey, now);
|
|
49
|
+
if (_recentMessageTimestamps.size > DEDUP_PRUNE_THRESHOLD) {
|
|
50
|
+
for (const [key, ts] of _recentMessageTimestamps) {
|
|
51
|
+
if (now - ts > DEDUP_WINDOW_MS)
|
|
52
|
+
_recentMessageTimestamps.delete(key);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
35
55
|
const entry = {
|
|
36
56
|
id: randomUUID(),
|
|
37
57
|
ts: new Date().toISOString(),
|
|
38
58
|
severity,
|
|
39
|
-
message:
|
|
59
|
+
message: persistedMessage,
|
|
40
60
|
source,
|
|
41
61
|
read: false,
|
|
42
62
|
};
|
|
@@ -49,6 +69,7 @@ export function appendNotification(message, severity, source = "notify") {
|
|
|
49
69
|
if (_lineCount > MAX_ENTRIES) {
|
|
50
70
|
_rotate();
|
|
51
71
|
}
|
|
72
|
+
_emitChange();
|
|
52
73
|
}
|
|
53
74
|
catch {
|
|
54
75
|
// Non-fatal — never let persistence break the caller
|
|
@@ -77,6 +98,7 @@ export function markAllRead(basePath) {
|
|
|
77
98
|
const hasUnread = entries.some((e) => !e.read);
|
|
78
99
|
if (!hasUnread)
|
|
79
100
|
return;
|
|
101
|
+
let changed = false;
|
|
80
102
|
try {
|
|
81
103
|
_withLock(bp, () => {
|
|
82
104
|
// Re-read inside lock to get freshest state
|
|
@@ -85,11 +107,14 @@ export function markAllRead(basePath) {
|
|
|
85
107
|
return;
|
|
86
108
|
const lines = fresh.map((e) => JSON.stringify({ ...e, read: true }));
|
|
87
109
|
_atomicWrite(bp, lines.join("\n") + "\n");
|
|
110
|
+
changed = true;
|
|
88
111
|
});
|
|
89
112
|
}
|
|
90
113
|
catch {
|
|
91
114
|
// Non-fatal
|
|
92
115
|
}
|
|
116
|
+
if (changed)
|
|
117
|
+
_emitChange();
|
|
93
118
|
}
|
|
94
119
|
/**
|
|
95
120
|
* Clear all notifications. Atomic write of empty content under lock.
|
|
@@ -102,6 +127,8 @@ export function clearNotifications(basePath) {
|
|
|
102
127
|
_withLock(bp, () => {
|
|
103
128
|
_atomicWrite(bp, "");
|
|
104
129
|
});
|
|
130
|
+
_lineCount = 0;
|
|
131
|
+
_emitChange();
|
|
105
132
|
}
|
|
106
133
|
catch {
|
|
107
134
|
// Non-fatal
|
|
@@ -146,6 +173,16 @@ export function suppressPersistence() {
|
|
|
146
173
|
export function unsuppressPersistence() {
|
|
147
174
|
_suppressCount = Math.max(0, _suppressCount - 1);
|
|
148
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Subscribe to notification-store mutations (append, mark-read, clear).
|
|
178
|
+
* Returns an unsubscribe function.
|
|
179
|
+
*/
|
|
180
|
+
export function onNotificationStoreChange(listener) {
|
|
181
|
+
_changeListeners.add(listener);
|
|
182
|
+
return () => {
|
|
183
|
+
_changeListeners.delete(listener);
|
|
184
|
+
};
|
|
185
|
+
}
|
|
149
186
|
// ─── Test Helpers ───────────────────────────────────────────────────────
|
|
150
187
|
/**
|
|
151
188
|
* Reset module state. Only for use in tests.
|
|
@@ -154,6 +191,8 @@ export function _resetNotificationStore() {
|
|
|
154
191
|
_basePath = null;
|
|
155
192
|
_lineCount = 0;
|
|
156
193
|
_suppressCount = 0;
|
|
194
|
+
_recentMessageTimestamps = new Map();
|
|
195
|
+
_changeListeners.clear();
|
|
157
196
|
}
|
|
158
197
|
// ─── Internal ───────────────────────────────────────────────────────────
|
|
159
198
|
function _readEntriesFromDisk(basePath) {
|
|
@@ -191,12 +230,23 @@ function _rotate() {
|
|
|
191
230
|
const trimmed = entries.slice(entries.length - MAX_ENTRIES);
|
|
192
231
|
const lines = trimmed.map((e) => JSON.stringify(e));
|
|
193
232
|
_atomicWrite(_basePath, lines.join("\n") + "\n");
|
|
233
|
+
_lineCount = trimmed.length;
|
|
194
234
|
});
|
|
195
235
|
}
|
|
196
236
|
catch {
|
|
197
237
|
// Non-fatal
|
|
198
238
|
}
|
|
199
239
|
}
|
|
240
|
+
function _emitChange() {
|
|
241
|
+
for (const listener of _changeListeners) {
|
|
242
|
+
try {
|
|
243
|
+
listener();
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
// Non-fatal
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
200
250
|
/**
|
|
201
251
|
* Atomic file rewrite via temp-file + rename. Prevents partial reads
|
|
202
252
|
* by other processes (web API subprocess, parallel workers).
|
|
@@ -2,24 +2,14 @@
|
|
|
2
2
|
// Always-on ambient widget rendered belowEditor showing unread count and
|
|
3
3
|
// the most recent notification message. Refreshes every 5 seconds.
|
|
4
4
|
// Widget key: "gsd-notifications", placement: "belowEditor"
|
|
5
|
-
import { getUnreadCount,
|
|
6
|
-
import {
|
|
5
|
+
import { getUnreadCount, onNotificationStoreChange } from "./notification-store.js";
|
|
6
|
+
import { formattedShortcutPair } from "./shortcut-defs.js";
|
|
7
7
|
// ─── Pure rendering ──���────────────────────────���─────────────────────────
|
|
8
8
|
export function buildNotificationWidgetLines() {
|
|
9
9
|
const unread = getUnreadCount();
|
|
10
10
|
if (unread === 0)
|
|
11
11
|
return [];
|
|
12
|
-
|
|
13
|
-
const latest = entries[0]; // newest-first
|
|
14
|
-
if (!latest)
|
|
15
|
-
return [];
|
|
16
|
-
const icon = latest.severity === "error" ? "✗" : latest.severity === "warning" ? "⚠" : "●";
|
|
17
|
-
const badge = `${unread} unread`;
|
|
18
|
-
const msgMax = 80;
|
|
19
|
-
const truncated = latest.message.length > msgMax
|
|
20
|
-
? latest.message.slice(0, msgMax - 1) + "…"
|
|
21
|
-
: latest.message;
|
|
22
|
-
return [` ${icon} [${badge}] ${truncated} (${formatShortcut("Ctrl+Alt+N")} to view)`];
|
|
12
|
+
return [` 🔔 Notifications: ${unread} unread (${formattedShortcutPair("notifications")})`];
|
|
23
13
|
}
|
|
24
14
|
// ─── Widget init ────────────────────────────────────────────────────────
|
|
25
15
|
const REFRESH_INTERVAL_MS = 5_000;
|
|
@@ -39,6 +29,7 @@ export function initNotificationWidget(ctx) {
|
|
|
39
29
|
cachedLines = undefined;
|
|
40
30
|
_tui.requestRender();
|
|
41
31
|
};
|
|
32
|
+
const unsubscribe = onNotificationStoreChange(refresh);
|
|
42
33
|
const refreshTimer = setInterval(refresh, REFRESH_INTERVAL_MS);
|
|
43
34
|
return {
|
|
44
35
|
render(_width) {
|
|
@@ -50,6 +41,7 @@ export function initNotificationWidget(ctx) {
|
|
|
50
41
|
cachedLines = undefined;
|
|
51
42
|
},
|
|
52
43
|
dispose() {
|
|
44
|
+
unsubscribe();
|
|
53
45
|
clearInterval(refreshTimer);
|
|
54
46
|
},
|
|
55
47
|
};
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* GSD Parallel Monitor Overlay
|
|
3
3
|
*
|
|
4
4
|
* Full-screen TUI overlay showing real-time parallel worker progress.
|
|
5
|
-
* Opened via `/gsd parallel watch
|
|
5
|
+
* Opened via `/gsd parallel watch`, Ctrl+Alt+P (⌃⌥P on macOS),
|
|
6
|
+
* or Ctrl+Shift+P fallback.
|
|
6
7
|
* Reads the same data sources as `scripts/parallel-monitor.mjs` but
|
|
7
8
|
* renders as a native pi-tui overlay with theme integration.
|
|
8
9
|
*/
|
|
@@ -11,6 +12,7 @@ import { join } from "node:path";
|
|
|
11
12
|
import { spawnSync } from "node:child_process";
|
|
12
13
|
import { matchesKey, Key } from "@gsd/pi-tui";
|
|
13
14
|
import { formatDuration } from "../shared/mod.js";
|
|
15
|
+
import { formattedShortcutPair } from "./shortcut-defs.js";
|
|
14
16
|
// ─── Data Helpers ─────────────────────────────────────────────────────────
|
|
15
17
|
function readJsonSafe(filePath) {
|
|
16
18
|
try {
|
|
@@ -285,7 +287,10 @@ export class ParallelMonitorOverlay {
|
|
|
285
287
|
}
|
|
286
288
|
}
|
|
287
289
|
handleInput(data) {
|
|
288
|
-
if (matchesKey(data, Key.escape) ||
|
|
290
|
+
if (matchesKey(data, Key.escape) ||
|
|
291
|
+
matchesKey(data, Key.ctrlAlt("p")) ||
|
|
292
|
+
matchesKey(data, Key.ctrlShift("p")) ||
|
|
293
|
+
data === "q") {
|
|
289
294
|
this.dispose();
|
|
290
295
|
this.onClose();
|
|
291
296
|
return;
|
|
@@ -403,7 +408,7 @@ export class ParallelMonitorOverlay {
|
|
|
403
408
|
}
|
|
404
409
|
lines.push(` ${t.bold("Total: $" + this.workers.reduce((s, wk) => s + wk.cost, 0).toFixed(2))}`);
|
|
405
410
|
}
|
|
406
|
-
lines.push(t.fg("muted",
|
|
411
|
+
lines.push(t.fg("muted", ` ESC/q/${formattedShortcutPair("parallel")} close │ ↑↓ scroll`));
|
|
407
412
|
// Apply scroll — use terminal rows as height estimate
|
|
408
413
|
const termHeight = process.stdout.rows || 40;
|
|
409
414
|
const maxScroll = Math.max(0, lines.length - termHeight);
|