gsd-pi 2.71.0-dev.06b86c6 → 2.71.0-dev.7a61d89
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/headless-events.d.ts +2 -0
- package/dist/headless-events.js +7 -0
- package/dist/headless.js +16 -3
- package/dist/resource-loader.js +6 -3
- 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/session.js +8 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +22 -16
- package/dist/resources/extensions/gsd/auto.js +52 -0
- package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +66 -51
- package/dist/resources/extensions/gsd/commands/handlers/auto.js +10 -33
- package/dist/resources/extensions/gsd/commands/handlers/core.js +45 -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/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 +30 -0
- 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/shortcut-defs.js +34 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
- 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 +11 -11
- 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/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.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/src/providers/anthropic-auth.test.ts +32 -0
- package/packages/pi-ai/src/providers/anthropic.ts +8 -4
- 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/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/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/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/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/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.ts +45 -1
- package/src/resources/extensions/gsd/auto/session.ts +8 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +29 -18
- package/src/resources/extensions/gsd/auto.ts +68 -0
- package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +82 -60
- package/src/resources/extensions/gsd/commands/handlers/auto.ts +10 -36
- package/src/resources/extensions/gsd/commands/handlers/core.ts +46 -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/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 +30 -0
- 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/shortcut-defs.ts +49 -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/infra-errors-cooldown.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/notification-store.test.ts +18 -0
- package/src/resources/extensions/gsd/tests/notification-widget.test.ts +3 -2
- 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/register-shortcuts.test.ts +62 -5
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +90 -0
- /package/dist/web/standalone/.next/static/{dYVdRaunb2ZSEA8fjkT-V → ug91LJa0m7OdzrTVaz_48}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{dYVdRaunb2ZSEA8fjkT-V → ug91LJa0m7OdzrTVaz_48}/_ssgManifest.js +0 -0
|
@@ -650,19 +650,33 @@ function getDbCompletionCounts(): DbCompletionCounts | null {
|
|
|
650
650
|
* Exported for testability.
|
|
651
651
|
*/
|
|
652
652
|
export function detectStuckLoops(units: UnitMetrics[], anomalies: ForensicAnomaly[]): void {
|
|
653
|
-
// First, collect unique startedAt values per type/id key
|
|
654
|
-
|
|
653
|
+
// First, collect unique startedAt values per type/id key, bucketed by
|
|
654
|
+
// autoSessionKey when available so cross-session recovery does not look
|
|
655
|
+
// like a within-session stuck loop.
|
|
656
|
+
const dispatchMap = new Map<string, Map<string, Set<number>>>();
|
|
655
657
|
for (const u of units) {
|
|
656
658
|
const key = `${u.type}/${u.id}`;
|
|
657
|
-
let
|
|
659
|
+
let sessionBuckets = dispatchMap.get(key);
|
|
660
|
+
if (!sessionBuckets) {
|
|
661
|
+
sessionBuckets = new Map();
|
|
662
|
+
dispatchMap.set(key, sessionBuckets);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const sessionKey = u.autoSessionKey ?? "__legacy__";
|
|
666
|
+
let starts = sessionBuckets.get(sessionKey);
|
|
658
667
|
if (!starts) {
|
|
659
668
|
starts = new Set();
|
|
660
|
-
|
|
669
|
+
sessionBuckets.set(sessionKey, starts);
|
|
661
670
|
}
|
|
662
671
|
starts.add(u.startedAt);
|
|
663
672
|
}
|
|
664
|
-
|
|
665
|
-
|
|
673
|
+
|
|
674
|
+
for (const [key, sessionBuckets] of dispatchMap) {
|
|
675
|
+
const hasSessionAwareData = Array.from(sessionBuckets.keys()).some((sessionKey) => sessionKey !== "__legacy__");
|
|
676
|
+
const count = hasSessionAwareData
|
|
677
|
+
? Math.max(...Array.from(sessionBuckets.values(), (starts) => starts.size))
|
|
678
|
+
: (sessionBuckets.get("__legacy__")?.size ?? 0);
|
|
679
|
+
|
|
666
680
|
if (count > 1) {
|
|
667
681
|
const [unitType, ...idParts] = key.split("/");
|
|
668
682
|
anomalies.push({
|
|
@@ -671,7 +685,9 @@ export function detectStuckLoops(units: UnitMetrics[], anomalies: ForensicAnomal
|
|
|
671
685
|
unitType,
|
|
672
686
|
unitId: idParts.join("/"),
|
|
673
687
|
summary: `Unit ${key} was dispatched ${count} times`,
|
|
674
|
-
details:
|
|
688
|
+
details: hasSessionAwareData
|
|
689
|
+
? `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.`
|
|
690
|
+
: `Repeated dispatch suggests the unit completed but its artifacts weren't verified, or the state machine kept returning it.`,
|
|
675
691
|
});
|
|
676
692
|
}
|
|
677
693
|
}
|
|
@@ -15,7 +15,7 @@ import { loadPrompt, inlineTemplate } from "./prompt-loader.js";
|
|
|
15
15
|
import { buildSkillActivationBlock } from "./auto-prompts.js";
|
|
16
16
|
import { deriveState } from "./state.js";
|
|
17
17
|
import { invalidateAllCaches } from "./cache.js";
|
|
18
|
-
import {
|
|
18
|
+
import { startAutoDetached } from "./auto.js";
|
|
19
19
|
import { clearLock } from "./crash-recovery.js";
|
|
20
20
|
import {
|
|
21
21
|
assessInterruptedSession,
|
|
@@ -67,7 +67,6 @@ export {
|
|
|
67
67
|
showQueue, handleQueueReorder, showQueueAdd,
|
|
68
68
|
buildExistingMilestonesContext,
|
|
69
69
|
} from "./guided-flow-queue.js";
|
|
70
|
-
import { getErrorMessage } from "./error-utils.js";
|
|
71
70
|
import { logWarning } from "./workflow-logger.js";
|
|
72
71
|
|
|
73
72
|
// ─── ID Generation with Reservation ─────────────────────────────────────────
|
|
@@ -244,11 +243,7 @@ export function checkAutoStartAfterDiscuss(): boolean {
|
|
|
244
243
|
|
|
245
244
|
pendingAutoStartMap.delete(basePath);
|
|
246
245
|
ctx.ui.notify(`Milestone ${milestoneId} ready.`, "info");
|
|
247
|
-
|
|
248
|
-
ctx.ui.notify(`Auto-start failed: ${getErrorMessage(err)}`, "error");
|
|
249
|
-
logWarning("guided", `auto start error: ${getErrorMessage(err)}`);
|
|
250
|
-
debugLog("auto-start-failed", { error: getErrorMessage(err) });
|
|
251
|
-
});
|
|
246
|
+
startAutoDetached(ctx, pi, basePath, false, { step });
|
|
252
247
|
return true;
|
|
253
248
|
}
|
|
254
249
|
|
|
@@ -1305,7 +1300,7 @@ export async function showSmartEntry(
|
|
|
1305
1300
|
],
|
|
1306
1301
|
});
|
|
1307
1302
|
if (resume === "resume") {
|
|
1308
|
-
|
|
1303
|
+
startAutoDetached(ctx, pi, basePath, false, {
|
|
1309
1304
|
interrupted,
|
|
1310
1305
|
step: interrupted.pausedSession?.stepMode ?? false,
|
|
1311
1306
|
});
|
|
@@ -1647,7 +1642,7 @@ export async function showSmartEntry(
|
|
|
1647
1642
|
});
|
|
1648
1643
|
|
|
1649
1644
|
if (choice === "auto") {
|
|
1650
|
-
|
|
1645
|
+
startAutoDetached(ctx, pi, basePath, false);
|
|
1651
1646
|
} else if (choice === "status") {
|
|
1652
1647
|
const { fireStatusViaCommand } = await import("./commands.js");
|
|
1653
1648
|
await fireStatusViaCommand(ctx);
|
|
@@ -1859,7 +1854,7 @@ export async function showSmartEntry(
|
|
|
1859
1854
|
});
|
|
1860
1855
|
|
|
1861
1856
|
if (choice === "auto") {
|
|
1862
|
-
|
|
1857
|
+
startAutoDetached(ctx, pi, basePath, false);
|
|
1863
1858
|
return;
|
|
1864
1859
|
}
|
|
1865
1860
|
|
|
@@ -41,6 +41,7 @@ export interface UnitMetrics {
|
|
|
41
41
|
model: string; // model ID used
|
|
42
42
|
startedAt: number; // ms timestamp
|
|
43
43
|
finishedAt: number; // ms timestamp
|
|
44
|
+
autoSessionKey?: string; // identifies one auto-mode run across pause/resume
|
|
44
45
|
tokens: TokenCounts;
|
|
45
46
|
cost: number; // total USD cost
|
|
46
47
|
toolCalls: number;
|
|
@@ -133,7 +134,16 @@ export function snapshotUnitMetrics(
|
|
|
133
134
|
unitId: string,
|
|
134
135
|
startedAt: number,
|
|
135
136
|
model: string,
|
|
136
|
-
opts?: {
|
|
137
|
+
opts?: {
|
|
138
|
+
tier?: string;
|
|
139
|
+
modelDowngraded?: boolean;
|
|
140
|
+
contextWindowTokens?: number;
|
|
141
|
+
truncationSections?: number;
|
|
142
|
+
continueHereFired?: boolean;
|
|
143
|
+
promptCharCount?: number;
|
|
144
|
+
baselineCharCount?: number;
|
|
145
|
+
autoSessionKey?: string;
|
|
146
|
+
},
|
|
137
147
|
): UnitMetrics | null {
|
|
138
148
|
if (!ledger) return null;
|
|
139
149
|
|
|
@@ -181,6 +191,7 @@ export function snapshotUnitMetrics(
|
|
|
181
191
|
model,
|
|
182
192
|
startedAt,
|
|
183
193
|
finishedAt: Date.now(),
|
|
194
|
+
...(opts?.autoSessionKey ? { autoSessionKey: opts.autoSessionKey } : {}),
|
|
184
195
|
tokens,
|
|
185
196
|
cost,
|
|
186
197
|
toolCalls,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from "./paths.js";
|
|
21
21
|
import { invalidateAllCaches } from "./cache.js";
|
|
22
22
|
import { loadQueueOrder, saveQueueOrder } from "./queue-order.js";
|
|
23
|
-
import { isDbAvailable, updateMilestoneStatus } from "./gsd-db.js";
|
|
23
|
+
import { getMilestone, isDbAvailable, updateMilestoneStatus } from "./gsd-db.js";
|
|
24
24
|
import { logWarning } from "./workflow-logger.js";
|
|
25
25
|
|
|
26
26
|
// ─── Park ──────────────────────────────────────────────────────────────────
|
|
@@ -77,9 +77,16 @@ export function unparkMilestone(basePath: string, milestoneId: string): boolean
|
|
|
77
77
|
if (!mDir || !existsSync(mDir)) return false;
|
|
78
78
|
|
|
79
79
|
const parkedPath = join(mDir, buildMilestoneFileName(milestoneId, "PARKED"));
|
|
80
|
-
|
|
80
|
+
const hadParkedFile = existsSync(parkedPath);
|
|
81
|
+
const dbThinksParked = isDbAvailable() && getMilestone(milestoneId)?.status === "parked";
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
// Recover the reverse desync too: DB can still say "parked" even when the
|
|
84
|
+
// PARKED marker was lost on disk, and /gsd unpark should repair that state.
|
|
85
|
+
if (!hadParkedFile && !dbThinksParked) return false;
|
|
86
|
+
|
|
87
|
+
if (hadParkedFile) {
|
|
88
|
+
unlinkSync(parkedPath);
|
|
89
|
+
}
|
|
83
90
|
// Sync DB status so deriveStateFromDb picks up the unparked milestone (#2694)
|
|
84
91
|
if (isDbAvailable()) {
|
|
85
92
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
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
|
|
|
5
5
|
import type { Theme } from "@gsd/pi-coding-agent";
|
|
6
6
|
import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui";
|
|
@@ -9,11 +9,11 @@ import {
|
|
|
9
9
|
readNotifications,
|
|
10
10
|
markAllRead,
|
|
11
11
|
clearNotifications,
|
|
12
|
-
getUnreadCount,
|
|
13
12
|
type NotificationEntry,
|
|
14
13
|
type NotifySeverity,
|
|
15
14
|
} from "./notification-store.js";
|
|
16
|
-
import {
|
|
15
|
+
import { formattedShortcutPair } from "./shortcut-defs.js";
|
|
16
|
+
import { padRight, joinColumns } from "../shared/mod.js";
|
|
17
17
|
|
|
18
18
|
type FilterMode = "all" | "error" | "warning" | "info";
|
|
19
19
|
const FILTER_CYCLE: FilterMode[] = ["all", "error", "warning", "info"];
|
|
@@ -63,6 +63,12 @@ function formatTimestamp(ts: string): string {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
function notificationSignature(entries: readonly NotificationEntry[]): string {
|
|
67
|
+
return entries
|
|
68
|
+
.map((entry) => `${entry.ts}|${entry.severity}|${entry.read ? 1 : 0}|${entry.message}`)
|
|
69
|
+
.join("\n");
|
|
70
|
+
}
|
|
71
|
+
|
|
66
72
|
export class GSDNotificationOverlay {
|
|
67
73
|
private tui: { requestRender: () => void };
|
|
68
74
|
private theme: Theme;
|
|
@@ -72,6 +78,7 @@ export class GSDNotificationOverlay {
|
|
|
72
78
|
private scrollOffset = 0;
|
|
73
79
|
private filterIndex = 0;
|
|
74
80
|
private entries: NotificationEntry[] = [];
|
|
81
|
+
private entriesSignature = "";
|
|
75
82
|
private refreshTimer: ReturnType<typeof setInterval>;
|
|
76
83
|
private disposed = false;
|
|
77
84
|
private resizeHandler: (() => void) | null = null;
|
|
@@ -88,6 +95,7 @@ export class GSDNotificationOverlay {
|
|
|
88
95
|
// Mark all as read on open
|
|
89
96
|
markAllRead();
|
|
90
97
|
this.entries = readNotifications();
|
|
98
|
+
this.entriesSignature = notificationSignature(this.entries);
|
|
91
99
|
|
|
92
100
|
// Resize handler
|
|
93
101
|
this.resizeHandler = () => {
|
|
@@ -101,9 +109,11 @@ export class GSDNotificationOverlay {
|
|
|
101
109
|
this.refreshTimer = setInterval(() => {
|
|
102
110
|
if (this.disposed) return;
|
|
103
111
|
const fresh = readNotifications();
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
const signature = notificationSignature(fresh);
|
|
113
|
+
if (signature !== this.entriesSignature) {
|
|
106
114
|
markAllRead();
|
|
115
|
+
this.entries = readNotifications();
|
|
116
|
+
this.entriesSignature = notificationSignature(this.entries);
|
|
107
117
|
this.invalidate();
|
|
108
118
|
this.tui.requestRender();
|
|
109
119
|
}
|
|
@@ -120,7 +130,12 @@ export class GSDNotificationOverlay {
|
|
|
120
130
|
}
|
|
121
131
|
|
|
122
132
|
handleInput(data: string): void {
|
|
123
|
-
if (
|
|
133
|
+
if (
|
|
134
|
+
matchesKey(data, Key.escape) ||
|
|
135
|
+
matchesKey(data, Key.ctrl("c")) ||
|
|
136
|
+
matchesKey(data, Key.ctrlAlt("n")) ||
|
|
137
|
+
matchesKey(data, Key.ctrlShift("n"))
|
|
138
|
+
) {
|
|
124
139
|
this.dispose();
|
|
125
140
|
this.onClose();
|
|
126
141
|
return;
|
|
@@ -165,6 +180,7 @@ export class GSDNotificationOverlay {
|
|
|
165
180
|
if (data === "c") {
|
|
166
181
|
clearNotifications();
|
|
167
182
|
this.entries = [];
|
|
183
|
+
this.entriesSignature = notificationSignature(this.entries);
|
|
168
184
|
this.scrollOffset = 0;
|
|
169
185
|
this.invalidate();
|
|
170
186
|
this.tui.requestRender();
|
|
@@ -250,7 +266,8 @@ export class GSDNotificationOverlay {
|
|
|
250
266
|
lines.push(hr());
|
|
251
267
|
|
|
252
268
|
// Controls
|
|
253
|
-
|
|
269
|
+
const closeShortcut = formattedShortcutPair("notifications");
|
|
270
|
+
lines.push(row(th.fg("dim", `↑/↓ scroll f filter c clear Esc close (${closeShortcut})`)));
|
|
254
271
|
lines.push(blank());
|
|
255
272
|
|
|
256
273
|
// Entries
|
|
@@ -35,6 +35,7 @@ let _basePath: string | null = null;
|
|
|
35
35
|
let _lineCount = 0; // Hint for rotation — not authoritative for public API
|
|
36
36
|
let _suppressCount = 0;
|
|
37
37
|
let _recentMessageTimestamps = new Map<string, number>();
|
|
38
|
+
const _changeListeners = new Set<() => void>();
|
|
38
39
|
|
|
39
40
|
// ─── Public API ─────────────────────────────────────────────────────────
|
|
40
41
|
|
|
@@ -93,6 +94,7 @@ export function appendNotification(
|
|
|
93
94
|
if (_lineCount > MAX_ENTRIES) {
|
|
94
95
|
_rotate();
|
|
95
96
|
}
|
|
97
|
+
_emitChange();
|
|
96
98
|
} catch {
|
|
97
99
|
// Non-fatal — never let persistence break the caller
|
|
98
100
|
}
|
|
@@ -121,6 +123,7 @@ export function markAllRead(basePath?: string): void {
|
|
|
121
123
|
const hasUnread = entries.some((e) => !e.read);
|
|
122
124
|
if (!hasUnread) return;
|
|
123
125
|
|
|
126
|
+
let changed = false;
|
|
124
127
|
try {
|
|
125
128
|
_withLock(bp, () => {
|
|
126
129
|
// Re-read inside lock to get freshest state
|
|
@@ -128,10 +131,12 @@ export function markAllRead(basePath?: string): void {
|
|
|
128
131
|
if (fresh.length === 0 || !fresh.some((e) => !e.read)) return;
|
|
129
132
|
const lines = fresh.map((e) => JSON.stringify({ ...e, read: true }));
|
|
130
133
|
_atomicWrite(bp, lines.join("\n") + "\n");
|
|
134
|
+
changed = true;
|
|
131
135
|
});
|
|
132
136
|
} catch {
|
|
133
137
|
// Non-fatal
|
|
134
138
|
}
|
|
139
|
+
if (changed) _emitChange();
|
|
135
140
|
}
|
|
136
141
|
|
|
137
142
|
/**
|
|
@@ -145,6 +150,8 @@ export function clearNotifications(basePath?: string): void {
|
|
|
145
150
|
_withLock(bp, () => {
|
|
146
151
|
_atomicWrite(bp, "");
|
|
147
152
|
});
|
|
153
|
+
_lineCount = 0;
|
|
154
|
+
_emitChange();
|
|
148
155
|
} catch {
|
|
149
156
|
// Non-fatal
|
|
150
157
|
}
|
|
@@ -189,6 +196,17 @@ export function unsuppressPersistence(): void {
|
|
|
189
196
|
_suppressCount = Math.max(0, _suppressCount - 1);
|
|
190
197
|
}
|
|
191
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Subscribe to notification-store mutations (append, mark-read, clear).
|
|
201
|
+
* Returns an unsubscribe function.
|
|
202
|
+
*/
|
|
203
|
+
export function onNotificationStoreChange(listener: () => void): () => void {
|
|
204
|
+
_changeListeners.add(listener);
|
|
205
|
+
return () => {
|
|
206
|
+
_changeListeners.delete(listener);
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
192
210
|
// ─── Test Helpers ───────────────────────────────────────────────────────
|
|
193
211
|
|
|
194
212
|
/**
|
|
@@ -199,6 +217,7 @@ export function _resetNotificationStore(): void {
|
|
|
199
217
|
_lineCount = 0;
|
|
200
218
|
_suppressCount = 0;
|
|
201
219
|
_recentMessageTimestamps = new Map();
|
|
220
|
+
_changeListeners.clear();
|
|
202
221
|
}
|
|
203
222
|
|
|
204
223
|
// ─── Internal ───────────────────────────────────────────────────────────
|
|
@@ -234,12 +253,23 @@ function _rotate(): void {
|
|
|
234
253
|
const trimmed = entries.slice(entries.length - MAX_ENTRIES);
|
|
235
254
|
const lines = trimmed.map((e) => JSON.stringify(e));
|
|
236
255
|
_atomicWrite(_basePath!, lines.join("\n") + "\n");
|
|
256
|
+
_lineCount = trimmed.length;
|
|
237
257
|
});
|
|
238
258
|
} catch {
|
|
239
259
|
// Non-fatal
|
|
240
260
|
}
|
|
241
261
|
}
|
|
242
262
|
|
|
263
|
+
function _emitChange(): void {
|
|
264
|
+
for (const listener of _changeListeners) {
|
|
265
|
+
try {
|
|
266
|
+
listener();
|
|
267
|
+
} catch {
|
|
268
|
+
// Non-fatal
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
243
273
|
/**
|
|
244
274
|
* Atomic file rewrite via temp-file + rename. Prevents partial reads
|
|
245
275
|
* by other processes (web API subprocess, parallel workers).
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ExtensionContext } from "@gsd/pi-coding-agent";
|
|
7
7
|
|
|
8
|
-
import { getUnreadCount,
|
|
9
|
-
import {
|
|
8
|
+
import { getUnreadCount, onNotificationStoreChange } from "./notification-store.js";
|
|
9
|
+
import { formattedShortcutPair } from "./shortcut-defs.js";
|
|
10
10
|
|
|
11
11
|
// ─── Pure rendering ──���────────────────────────���─────────────────────────
|
|
12
12
|
|
|
@@ -14,18 +14,7 @@ export function buildNotificationWidgetLines(): string[] {
|
|
|
14
14
|
const unread = getUnreadCount();
|
|
15
15
|
if (unread === 0) return [];
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
const latest = entries[0]; // newest-first
|
|
19
|
-
if (!latest) return [];
|
|
20
|
-
|
|
21
|
-
const icon = latest.severity === "error" ? "✗" : latest.severity === "warning" ? "⚠" : "●";
|
|
22
|
-
const badge = `${unread} unread`;
|
|
23
|
-
const msgMax = 80;
|
|
24
|
-
const truncated = latest.message.length > msgMax
|
|
25
|
-
? latest.message.slice(0, msgMax - 1) + "…"
|
|
26
|
-
: latest.message;
|
|
27
|
-
|
|
28
|
-
return [` ${icon} [${badge}] ${truncated} (${formatShortcut("Ctrl+Alt+N")} or /gsd notifications)`];
|
|
17
|
+
return [` 🔔 Notifications: ${unread} unread (${formattedShortcutPair("notifications")})`];
|
|
29
18
|
}
|
|
30
19
|
|
|
31
20
|
// ─── Widget init ────────────────────────────────────────────────────────
|
|
@@ -51,6 +40,7 @@ export function initNotificationWidget(ctx: ExtensionContext): void {
|
|
|
51
40
|
_tui.requestRender();
|
|
52
41
|
};
|
|
53
42
|
|
|
43
|
+
const unsubscribe = onNotificationStoreChange(refresh);
|
|
54
44
|
const refreshTimer = setInterval(refresh, REFRESH_INTERVAL_MS);
|
|
55
45
|
|
|
56
46
|
return {
|
|
@@ -62,6 +52,7 @@ export function initNotificationWidget(ctx: ExtensionContext): void {
|
|
|
62
52
|
cachedLines = undefined;
|
|
63
53
|
},
|
|
64
54
|
dispose(): void {
|
|
55
|
+
unsubscribe();
|
|
65
56
|
clearInterval(refreshTimer);
|
|
66
57
|
},
|
|
67
58
|
};
|
|
@@ -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
|
*/
|
|
@@ -15,6 +16,7 @@ import type { Theme } from "@gsd/pi-coding-agent";
|
|
|
15
16
|
import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui";
|
|
16
17
|
|
|
17
18
|
import { formatDuration, STATUS_GLYPH, STATUS_COLOR } from "../shared/mod.js";
|
|
19
|
+
import { formattedShortcutPair } from "./shortcut-defs.js";
|
|
18
20
|
|
|
19
21
|
// ─── Types ────────────────────────────────────────────────────────────────
|
|
20
22
|
|
|
@@ -347,7 +349,12 @@ export class ParallelMonitorOverlay {
|
|
|
347
349
|
}
|
|
348
350
|
|
|
349
351
|
handleInput(data: string): void {
|
|
350
|
-
if (
|
|
352
|
+
if (
|
|
353
|
+
matchesKey(data, Key.escape) ||
|
|
354
|
+
matchesKey(data, Key.ctrlAlt("p")) ||
|
|
355
|
+
matchesKey(data, Key.ctrlShift("p")) ||
|
|
356
|
+
data === "q"
|
|
357
|
+
) {
|
|
351
358
|
this.dispose();
|
|
352
359
|
this.onClose();
|
|
353
360
|
return;
|
|
@@ -486,7 +493,7 @@ export class ParallelMonitorOverlay {
|
|
|
486
493
|
}
|
|
487
494
|
lines.push(` ${t.bold("Total: $" + this.workers.reduce((s, wk) => s + wk.cost, 0).toFixed(2))}`);
|
|
488
495
|
}
|
|
489
|
-
lines.push(t.fg("muted",
|
|
496
|
+
lines.push(t.fg("muted", ` ESC/q/${formattedShortcutPair("parallel")} close │ ↑↓ scroll`));
|
|
490
497
|
|
|
491
498
|
// Apply scroll — use terminal rows as height estimate
|
|
492
499
|
const termHeight = process.stdout.rows || 40;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Canonical GSD shortcut definitions used by registration, help text, and overlays.
|
|
2
|
+
|
|
3
|
+
import { formatShortcut } from "./files.js";
|
|
4
|
+
|
|
5
|
+
export type GSDShortcutId = "dashboard" | "notifications" | "parallel";
|
|
6
|
+
|
|
7
|
+
type GSDShortcutDef = {
|
|
8
|
+
key: "g" | "n" | "p";
|
|
9
|
+
action: string;
|
|
10
|
+
command: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const GSD_SHORTCUTS: Record<GSDShortcutId, GSDShortcutDef> = {
|
|
14
|
+
dashboard: {
|
|
15
|
+
key: "g",
|
|
16
|
+
action: "Open GSD dashboard",
|
|
17
|
+
command: "/gsd status",
|
|
18
|
+
},
|
|
19
|
+
notifications: {
|
|
20
|
+
key: "n",
|
|
21
|
+
action: "Open notification history",
|
|
22
|
+
command: "/gsd notifications",
|
|
23
|
+
},
|
|
24
|
+
parallel: {
|
|
25
|
+
key: "p",
|
|
26
|
+
action: "Open parallel worker monitor",
|
|
27
|
+
command: "/gsd parallel watch",
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function combo(prefix: "Ctrl+Alt+" | "Ctrl+Shift+", key: string): string {
|
|
32
|
+
return `${prefix}${key.toUpperCase()}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function primaryShortcutCombo(id: GSDShortcutId): string {
|
|
36
|
+
return combo("Ctrl+Alt+", GSD_SHORTCUTS[id].key);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function fallbackShortcutCombo(id: GSDShortcutId): string {
|
|
40
|
+
return combo("Ctrl+Shift+", GSD_SHORTCUTS[id].key);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function shortcutPair(id: GSDShortcutId, formatter: (combo: string) => string = (combo) => combo): string {
|
|
44
|
+
return `${formatter(primaryShortcutCombo(id))} / ${formatter(fallbackShortcutCombo(id))}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formattedShortcutPair(id: GSDShortcutId): string {
|
|
48
|
+
return shortcutPair(id, formatShortcut);
|
|
49
|
+
}
|
|
@@ -101,3 +101,65 @@ test("#1943 detectStuckLoops ignores watchdog duplicates but flags real re-dispa
|
|
|
101
101
|
assert.equal(anomalies.length, 1, `expected 1 anomaly (for the 3x dispatched task), got ${anomalies.length}`);
|
|
102
102
|
assert.ok(anomalies[0].summary.includes("3 times"));
|
|
103
103
|
});
|
|
104
|
+
|
|
105
|
+
test("#3760 detectStuckLoops ignores cross-session recovery re-dispatches", () => {
|
|
106
|
+
const anomalies: ForensicAnomaly[] = [];
|
|
107
|
+
|
|
108
|
+
const units: UnitMetrics[] = [
|
|
109
|
+
makeUnit({
|
|
110
|
+
type: "plan-slice",
|
|
111
|
+
id: "M001/S02",
|
|
112
|
+
startedAt: 1000,
|
|
113
|
+
finishedAt: 2000,
|
|
114
|
+
autoSessionKey: "session-a",
|
|
115
|
+
}),
|
|
116
|
+
makeUnit({
|
|
117
|
+
type: "plan-slice",
|
|
118
|
+
id: "M001/S02",
|
|
119
|
+
startedAt: 5000,
|
|
120
|
+
finishedAt: 6000,
|
|
121
|
+
autoSessionKey: "session-b",
|
|
122
|
+
}),
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
detectStuckLoops(units, anomalies);
|
|
126
|
+
|
|
127
|
+
assert.equal(anomalies.length, 0, "cross-session recovery should not be flagged as a stuck loop");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("#3760 detectStuckLoops still flags repeated dispatches within one auto session", () => {
|
|
131
|
+
const anomalies: ForensicAnomaly[] = [];
|
|
132
|
+
|
|
133
|
+
const units: UnitMetrics[] = [
|
|
134
|
+
makeUnit({
|
|
135
|
+
type: "complete-slice",
|
|
136
|
+
id: "M011/S02",
|
|
137
|
+
startedAt: 1000,
|
|
138
|
+
finishedAt: 2000,
|
|
139
|
+
autoSessionKey: "session-a",
|
|
140
|
+
}),
|
|
141
|
+
makeUnit({
|
|
142
|
+
type: "complete-slice",
|
|
143
|
+
id: "M011/S02",
|
|
144
|
+
startedAt: 5000,
|
|
145
|
+
finishedAt: 6000,
|
|
146
|
+
autoSessionKey: "session-a",
|
|
147
|
+
}),
|
|
148
|
+
makeUnit({
|
|
149
|
+
type: "complete-slice",
|
|
150
|
+
id: "M011/S02",
|
|
151
|
+
startedAt: 9000,
|
|
152
|
+
finishedAt: 10000,
|
|
153
|
+
autoSessionKey: "session-b",
|
|
154
|
+
}),
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
detectStuckLoops(units, anomalies);
|
|
158
|
+
|
|
159
|
+
assert.equal(anomalies.length, 1, "within-session retries should still be flagged");
|
|
160
|
+
assert.ok(anomalies[0].summary.includes("2 times"), `summary should reflect the worst same-session loop: ${anomalies[0].summary}`);
|
|
161
|
+
assert.ok(
|
|
162
|
+
anomalies[0].details.includes("Cross-session recovery runs are ignored"),
|
|
163
|
+
`details should explain the session-aware rule: ${anomalies[0].details}`,
|
|
164
|
+
);
|
|
165
|
+
});
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import test from 'node:test';
|
|
5
5
|
import assert from 'node:assert/strict';
|
|
6
6
|
import { formatShortcut } from '../files.ts';
|
|
7
|
+
import { formattedShortcutPair, primaryShortcutCombo, fallbackShortcutCombo } from '../shortcut-defs.ts';
|
|
7
8
|
|
|
8
9
|
// ─── formatShortcut renders per-platform shortcuts ──────────────────────
|
|
9
10
|
|
|
@@ -67,3 +68,17 @@ test('formatShortcut: passes through plain key names', () => {
|
|
|
67
68
|
assert.strictEqual(formatShortcut('Escape'), 'Escape');
|
|
68
69
|
assert.strictEqual(formatShortcut('Enter'), 'Enter');
|
|
69
70
|
});
|
|
71
|
+
|
|
72
|
+
test("shortcut-defs: exposes canonical dashboard combos", () => {
|
|
73
|
+
assert.equal(primaryShortcutCombo("dashboard"), "Ctrl+Alt+G");
|
|
74
|
+
assert.equal(fallbackShortcutCombo("dashboard"), "Ctrl+Shift+G");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("shortcut-defs: formats shortcut pair using platform symbols", () => {
|
|
78
|
+
const pair = formattedShortcutPair("notifications");
|
|
79
|
+
if (process.platform === "darwin") {
|
|
80
|
+
assert.equal(pair, "⌃⌥N / ⌃⇧N");
|
|
81
|
+
} else {
|
|
82
|
+
assert.equal(pair, "Ctrl+Alt+N / Ctrl+Shift+N");
|
|
83
|
+
}
|
|
84
|
+
});
|