gsd-pi 2.28.0-dev.853dfc5 → 2.28.0-dev.a8d3050

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.
Files changed (73) hide show
  1. package/dist/resource-loader.js +80 -8
  2. package/dist/resources/extensions/gsd/auto-verification.ts +41 -7
  3. package/dist/resources/extensions/gsd/auto.ts +2 -2
  4. package/dist/resources/extensions/gsd/commands-handlers.ts +1 -9
  5. package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +14 -22
  6. package/dist/resources/extensions/gsd/commands.ts +1 -7
  7. package/dist/resources/extensions/gsd/index.ts +2 -1
  8. package/dist/resources/extensions/gsd/json-persistence.ts +52 -0
  9. package/dist/resources/extensions/gsd/metrics.ts +17 -31
  10. package/dist/resources/extensions/gsd/paths.ts +0 -8
  11. package/dist/resources/extensions/gsd/routing-history.ts +13 -17
  12. package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +1 -1
  13. package/dist/resources/extensions/gsd/tests/verification-evidence.test.ts +26 -24
  14. package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +136 -7
  15. package/dist/resources/extensions/gsd/types.ts +1 -0
  16. package/dist/resources/extensions/gsd/unit-runtime.ts +16 -13
  17. package/dist/resources/extensions/gsd/verification-evidence.ts +2 -0
  18. package/dist/resources/extensions/gsd/verification-gate.ts +13 -2
  19. package/dist/resources/extensions/remote-questions/discord-adapter.ts +2 -2
  20. package/dist/resources/extensions/remote-questions/notify.ts +1 -2
  21. package/dist/resources/extensions/remote-questions/slack-adapter.ts +1 -2
  22. package/dist/resources/extensions/remote-questions/telegram-adapter.ts +1 -2
  23. package/dist/resources/extensions/remote-questions/types.ts +3 -0
  24. package/dist/resources/extensions/shared/mod.ts +3 -0
  25. package/package.json +4 -1
  26. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  27. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  28. package/packages/pi-coding-agent/dist/core/settings-manager.js +8 -0
  29. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  30. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  31. package/packages/pi-coding-agent/dist/core/system-prompt.js +10 -0
  32. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  33. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  34. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -1
  35. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  36. package/packages/pi-coding-agent/scripts/copy-assets.cjs +39 -8
  37. package/packages/pi-coding-agent/src/core/settings-manager.ts +11 -0
  38. package/packages/pi-coding-agent/src/core/system-prompt.ts +11 -0
  39. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +4 -1
  40. package/packages/pi-tui/dist/autocomplete.d.ts +3 -0
  41. package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
  42. package/packages/pi-tui/dist/autocomplete.js +14 -0
  43. package/packages/pi-tui/dist/autocomplete.js.map +1 -1
  44. package/packages/pi-tui/src/autocomplete.ts +19 -1
  45. package/src/resources/extensions/gsd/auto-verification.ts +41 -7
  46. package/src/resources/extensions/gsd/auto.ts +2 -2
  47. package/src/resources/extensions/gsd/commands-handlers.ts +1 -9
  48. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +14 -22
  49. package/src/resources/extensions/gsd/commands.ts +1 -7
  50. package/src/resources/extensions/gsd/index.ts +2 -1
  51. package/src/resources/extensions/gsd/json-persistence.ts +52 -0
  52. package/src/resources/extensions/gsd/metrics.ts +17 -31
  53. package/src/resources/extensions/gsd/paths.ts +0 -8
  54. package/src/resources/extensions/gsd/routing-history.ts +13 -17
  55. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +1 -1
  56. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +26 -24
  57. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +136 -7
  58. package/src/resources/extensions/gsd/types.ts +1 -0
  59. package/src/resources/extensions/gsd/unit-runtime.ts +16 -13
  60. package/src/resources/extensions/gsd/verification-evidence.ts +2 -0
  61. package/src/resources/extensions/gsd/verification-gate.ts +13 -2
  62. package/src/resources/extensions/remote-questions/discord-adapter.ts +2 -2
  63. package/src/resources/extensions/remote-questions/notify.ts +1 -2
  64. package/src/resources/extensions/remote-questions/slack-adapter.ts +1 -2
  65. package/src/resources/extensions/remote-questions/telegram-adapter.ts +1 -2
  66. package/src/resources/extensions/remote-questions/types.ts +3 -0
  67. package/src/resources/extensions/shared/mod.ts +3 -0
  68. package/dist/resources/extensions/gsd/preferences-hooks.ts +0 -10
  69. package/dist/resources/extensions/shared/progress-widget.ts +0 -282
  70. package/dist/resources/extensions/shared/thinking-widget.ts +0 -107
  71. package/src/resources/extensions/gsd/preferences-hooks.ts +0 -10
  72. package/src/resources/extensions/shared/progress-widget.ts +0 -282
  73. package/src/resources/extensions/shared/thinking-widget.ts +0 -107
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
1
+ import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import {
4
4
  gsdRoot,
@@ -8,6 +8,7 @@ import {
8
8
  resolveTaskFile,
9
9
  } from "./paths.js";
10
10
  import { loadFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
11
+ import { loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
11
12
 
12
13
  export type UnitRuntimePhase =
13
14
  | "dispatched"
@@ -46,13 +47,23 @@ export interface AutoUnitRuntimeRecord {
46
47
  lastRecoveryReason?: "idle" | "hard";
47
48
  }
48
49
 
50
+ function isAutoUnitRuntimeRecord(data: unknown): data is AutoUnitRuntimeRecord {
51
+ return (
52
+ typeof data === "object" &&
53
+ data !== null &&
54
+ (data as AutoUnitRuntimeRecord).version === 1 &&
55
+ typeof (data as AutoUnitRuntimeRecord).unitType === "string" &&
56
+ typeof (data as AutoUnitRuntimeRecord).unitId === "string"
57
+ );
58
+ }
59
+
49
60
  function runtimeDir(basePath: string): string {
50
61
  return join(gsdRoot(basePath), "runtime", "units");
51
62
  }
52
63
 
53
64
  function runtimePath(basePath: string, unitType: string, unitId: string): string {
54
- const sanitizedUnitType = unitType.replace(/[\/]/g, "-");
55
- const sanitizedUnitId = unitId.replace(/[\/]/g, "-");
65
+ const sanitizedUnitType = unitType.replace(/[^a-zA-Z0-9._-]+/g, "-");
66
+ const sanitizedUnitId = unitId.replace(/[^a-zA-Z0-9._-]+/g, "-");
56
67
  return join(runtimeDir(basePath), `${sanitizedUnitType}-${sanitizedUnitId}.json`);
57
68
  }
58
69
 
@@ -63,8 +74,6 @@ export function writeUnitRuntimeRecord(
63
74
  startedAt: number,
64
75
  updates: Partial<AutoUnitRuntimeRecord> = {},
65
76
  ): AutoUnitRuntimeRecord {
66
- const dir = runtimeDir(basePath);
67
- mkdirSync(dir, { recursive: true });
68
77
  const path = runtimePath(basePath, unitType, unitId);
69
78
  const prev = readUnitRuntimeRecord(basePath, unitType, unitId);
70
79
  const next: AutoUnitRuntimeRecord = {
@@ -84,18 +93,12 @@ export function writeUnitRuntimeRecord(
84
93
  recoveryAttempts: updates.recoveryAttempts ?? prev?.recoveryAttempts ?? 0,
85
94
  lastRecoveryReason: updates.lastRecoveryReason ?? prev?.lastRecoveryReason,
86
95
  };
87
- writeFileSync(path, JSON.stringify(next, null, 2) + "\n", "utf-8");
96
+ saveJsonFile(path, next);
88
97
  return next;
89
98
  }
90
99
 
91
100
  export function readUnitRuntimeRecord(basePath: string, unitType: string, unitId: string): AutoUnitRuntimeRecord | null {
92
- const path = runtimePath(basePath, unitType, unitId);
93
- if (!existsSync(path)) return null;
94
- try {
95
- return JSON.parse(readFileSync(path, "utf-8")) as AutoUnitRuntimeRecord;
96
- } catch {
97
- return null;
98
- }
101
+ return loadJsonFileOrNull(runtimePath(basePath, unitType, unitId), isAutoUnitRuntimeRecord);
99
102
  }
100
103
 
101
104
  export function clearUnitRuntimeRecord(basePath: string, unitType: string, unitId: string): void {
@@ -20,6 +20,7 @@ export interface EvidenceCheckJSON {
20
20
  exitCode: number;
21
21
  durationMs: number;
22
22
  verdict: "pass" | "fail";
23
+ blocking: boolean;
23
24
  }
24
25
 
25
26
  export interface RuntimeErrorJSON {
@@ -80,6 +81,7 @@ export function writeVerificationJSON(
80
81
  exitCode: check.exitCode,
81
82
  durationMs: check.durationMs,
82
83
  verdict: check.exitCode === 0 ? "pass" : "fail",
84
+ blocking: check.blocking,
83
85
  })),
84
86
  ...(retryAttempt !== undefined ? { retryAttempt } : {}),
85
87
  ...(maxRetries !== undefined ? { maxRetries } : {}),
@@ -112,7 +112,9 @@ const MAX_FAILURE_CONTEXT_CHARS = 10_000;
112
112
  * Returns an empty string when all checks pass or the checks array is empty.
113
113
  */
114
114
  export function formatFailureContext(result: VerificationResult): string {
115
- const failures = result.checks.filter((c) => c.exitCode !== 0);
115
+ // Only include blocking failures in retry context non-blocking (advisory) failures
116
+ // should not be injected into retry prompts to avoid noise pollution.
117
+ const failures = result.checks.filter((c) => c.exitCode !== 0 && c.blocking);
116
118
  if (failures.length === 0) return "";
117
119
 
118
120
  const blocks: string[] = [];
@@ -256,6 +258,10 @@ export function runVerificationGate(options: RunVerificationGateOptions): Verifi
256
258
  };
257
259
  }
258
260
 
261
+ // Commands from preference and task-plan sources are blocking;
262
+ // package-json discovered commands are advisory (non-blocking).
263
+ const blocking = source === "preference" || source === "task-plan";
264
+
259
265
  const checks: VerificationCheck[] = [];
260
266
 
261
267
  for (const command of commands) {
@@ -291,11 +297,16 @@ export function runVerificationGate(options: RunVerificationGateOptions): Verifi
291
297
  stdout: truncate(result.stdout, MAX_OUTPUT_BYTES),
292
298
  stderr,
293
299
  durationMs,
300
+ blocking,
294
301
  });
295
302
  }
296
303
 
304
+ // Gate passes if all blocking checks pass (non-blocking failures are advisory)
305
+ const blockingChecks = checks.filter(c => c.blocking);
306
+ const passed = blockingChecks.length === 0 || blockingChecks.every(c => c.exitCode === 0);
307
+
297
308
  return {
298
- passed: checks.every(c => c.exitCode === 0),
309
+ passed,
299
310
  checks,
300
311
  discoverySource: source,
301
312
  timestamp,
@@ -2,11 +2,11 @@
2
2
  * Remote Questions — Discord adapter
3
3
  */
4
4
 
5
- import type { ChannelAdapter, RemotePrompt, RemoteDispatchResult, RemoteAnswer, RemotePromptRef } from "./types.js";
5
+ import { PER_REQUEST_TIMEOUT_MS, type ChannelAdapter, type RemotePrompt, type RemoteDispatchResult, type RemoteAnswer, type RemotePromptRef } from "./types.js";
6
6
  import { formatForDiscord, parseDiscordResponse, DISCORD_NUMBER_EMOJIS } from "./format.js";
7
7
 
8
8
  const DISCORD_API = "https://discord.com/api/v10";
9
- const PER_REQUEST_TIMEOUT_MS = 15_000;
9
+
10
10
  export class DiscordAdapter implements ChannelAdapter {
11
11
  readonly name = "discord" as const;
12
12
  private botUserId: string | null = null;
@@ -9,8 +9,7 @@
9
9
 
10
10
  import { resolveRemoteConfig } from "./config.js";
11
11
  import type { ResolvedConfig } from "./config.js";
12
-
13
- const PER_REQUEST_TIMEOUT_MS = 15_000;
12
+ import { PER_REQUEST_TIMEOUT_MS } from "./types.js";
14
13
 
15
14
  /**
16
15
  * Send a one-way notification to the configured remote channel.
@@ -2,11 +2,10 @@
2
2
  * Remote Questions — Slack adapter
3
3
  */
4
4
 
5
- import type { ChannelAdapter, RemotePrompt, RemoteDispatchResult, RemoteAnswer, RemotePromptRef } from "./types.js";
5
+ import { PER_REQUEST_TIMEOUT_MS, type ChannelAdapter, type RemotePrompt, type RemoteDispatchResult, type RemoteAnswer, type RemotePromptRef } from "./types.js";
6
6
  import { formatForSlack, parseSlackReply, parseSlackReactionResponse, SLACK_NUMBER_REACTION_NAMES } from "./format.js";
7
7
 
8
8
  const SLACK_API = "https://slack.com/api";
9
- const PER_REQUEST_TIMEOUT_MS = 15_000;
10
9
  const SLACK_ACK_REACTION = "white_check_mark";
11
10
 
12
11
  export class SlackAdapter implements ChannelAdapter {
@@ -2,11 +2,10 @@
2
2
  * Remote Questions — Telegram adapter
3
3
  */
4
4
 
5
- import type { ChannelAdapter, RemotePrompt, RemoteDispatchResult, RemoteAnswer, RemotePromptRef } from "./types.js";
5
+ import { PER_REQUEST_TIMEOUT_MS, type ChannelAdapter, type RemotePrompt, type RemoteDispatchResult, type RemoteAnswer, type RemotePromptRef } from "./types.js";
6
6
  import { formatForTelegram, parseTelegramResponse } from "./format.js";
7
7
 
8
8
  const TELEGRAM_API = "https://api.telegram.org";
9
- const PER_REQUEST_TIMEOUT_MS = 15_000;
10
9
 
11
10
  export class TelegramAdapter implements ChannelAdapter {
12
11
  readonly name = "telegram" as const;
@@ -2,6 +2,9 @@
2
2
  * Remote Questions — shared types
3
3
  */
4
4
 
5
+ /** Timeout applied to every outbound HTTP request across all channel adapters. */
6
+ export const PER_REQUEST_TIMEOUT_MS = 15_000;
7
+
5
8
  export type RemoteChannel = "slack" | "discord" | "telegram";
6
9
 
7
10
  export interface RemoteQuestionOption {
@@ -28,3 +28,6 @@ export { showInterviewRound } from "./interview-ui.js";
28
28
  export type { Question, QuestionOption, RoundResult } from "./interview-ui.js";
29
29
  export { showNextAction } from "./next-action-ui.js";
30
30
  export { showConfirm } from "./confirm-ui.js";
31
+ export { sanitizeError } from "./sanitize.js";
32
+ export { formatDateShort, truncateWithEllipsis } from "./format-utils.js";
33
+ export { splitFrontmatter, parseFrontmatterMap } from "./frontmatter.js";
@@ -1,10 +0,0 @@
1
- /**
2
- * Focused re-export: hook-related preferences.
3
- *
4
- * Consumers that only need hook resolution can import from this module
5
- * instead of the monolithic preferences.ts, reducing coupling surface.
6
- */
7
- export {
8
- resolvePostUnitHooks,
9
- resolvePreDispatchHooks,
10
- } from "./preferences.js";
@@ -1,282 +0,0 @@
1
- /**
2
- * Shared persistent progress/status panel widget.
3
- *
4
- * Renders an ordered list of progress items with status glyphs, optional
5
- * badge, subtitle, metadata, and footer hints. Supports pulse animation
6
- * for active items during agent execution.
7
- *
8
- * Usage:
9
- *
10
- * import { createProgressPanel } from "./shared/progress-widget.js";
11
- *
12
- * const panel = createProgressPanel(ctx.ui, {
13
- * widgetKey: "workflow",
14
- * statusKey: "workflow",
15
- * statusPrefix: "wf",
16
- * });
17
- *
18
- * panel.update(model); // render/re-render with new model
19
- * panel.startPulse(); // animate active items
20
- * panel.stopPulse(); // stop animation
21
- * panel.dispose(); // remove widget and status
22
- */
23
-
24
- import type { ExtensionUIContext, Theme } from "@gsd/pi-coding-agent";
25
- import type { TUI } from "@gsd/pi-tui";
26
- import { makeUI, type ProgressStatus } from "./ui.js";
27
-
28
- // ─── Exported types ───────────────────────────────────────────────────────────
29
-
30
- export type ProgressItemStatus = ProgressStatus;
31
-
32
- export interface ProgressItem {
33
- /** Display label */
34
- label: string;
35
- /** Drives glyph and color */
36
- status: ProgressItemStatus;
37
- /** Optional text after label — e.g. artifact type, task ID */
38
- detail?: string;
39
- /** Optional secondary line below item — e.g. "waiting for /workflow-continue" */
40
- annotation?: string;
41
- }
42
-
43
- export interface ProgressPanelModel {
44
- /** Panel title */
45
- title: string;
46
- /** Optional badge next to title — e.g. "RUNNING", "PAUSED" */
47
- badge?: string;
48
- /** Badge color control — maps to ProgressItemStatus color */
49
- badgeStatus?: ProgressItemStatus;
50
- /** Optional subtitle lines below title */
51
- subtitle?: string[];
52
- /** Ordered progress items */
53
- items: ProgressItem[];
54
- /** Optional metadata lines below items */
55
- meta?: string[];
56
- /** Optional footer hint strings */
57
- hints?: string[];
58
- }
59
-
60
- export interface ProgressPanelOptions {
61
- /**
62
- * Widget key used with ctx.ui.setWidget(...).
63
- * Must be unique per extension.
64
- */
65
- widgetKey: string;
66
- /**
67
- * Status key used with ctx.ui.setStatus(...).
68
- * Must be unique per extension.
69
- */
70
- statusKey: string;
71
- /**
72
- * Short prefix for footer status text.
73
- * Example: "wf" produces "wf:2/3 RUNNING"
74
- */
75
- statusPrefix: string;
76
- }
77
-
78
- export interface ProgressPanel {
79
- /** Update the widget with a new model. Triggers re-render. */
80
- update(model: ProgressPanelModel): void;
81
- /** Start pulsing items with status "active". */
82
- startPulse(): void;
83
- /** Stop pulsing. Active items render at full brightness. */
84
- stopPulse(): void;
85
- /** Remove the widget and status from the UI. */
86
- dispose(): void;
87
- }
88
-
89
- // ─── Internal constants ───────────────────────────────────────────────────────
90
-
91
- const PULSE_INTERVAL_MS = 500;
92
-
93
- // ─── Implementation ───────────────────────────────────────────────────────────
94
-
95
- /**
96
- * Create and register a persistent progress widget.
97
- *
98
- * @param ui The `ctx.ui` object from ExtensionContext or ExtensionCommandContext
99
- * @param options Widget key, status key, and status prefix
100
- * @returns ProgressPanel controller
101
- */
102
- export function createProgressPanel(
103
- ui: ExtensionUIContext,
104
- options: ProgressPanelOptions,
105
- ): ProgressPanel {
106
- const { widgetKey, statusKey, statusPrefix } = options;
107
-
108
- // ── Internal state ────────────────────────────────────────────────────────
109
-
110
- let currentModel: ProgressPanelModel | null = null;
111
- let stateVersion = 0;
112
- let cachedLines: string[] | undefined;
113
- let cachedWidth: number | undefined;
114
- let cachedVersion = -1;
115
- let pulseBright = true;
116
- let pulseTimer: ReturnType<typeof setInterval> | null = null;
117
- let widgetRef: { invalidate: () => void; requestRender: () => void } | null = null;
118
-
119
- // ── Footer status ─────────────────────────────────────────────────────────
120
-
121
- function updateFooterStatus(): void {
122
- if (!currentModel) return;
123
- const { items, badge } = currentModel;
124
- const total = items.length;
125
- let current = 0;
126
-
127
- // Find first active item index (1-based)
128
- const activeIdx = items.findIndex((it) => it.status === "active");
129
- if (activeIdx >= 0) {
130
- current = activeIdx + 1;
131
- } else {
132
- // Count done items + 1
133
- current = items.filter((it) => it.status === "done").length + 1;
134
- }
135
- if (current > total) current = total;
136
-
137
- const badgePart = badge ? ` ${badge}` : "";
138
- const statusText = ui.theme.fg("accent", `${statusPrefix}:${current}/${total}${badgePart}`);
139
- ui.setStatus(statusKey, statusText);
140
- }
141
-
142
- // ── Render function ───────────────────────────────────────────────────────
143
-
144
- function renderPanel(width: number, theme: Theme): string[] {
145
- // Version-based cache check
146
- if (cachedLines && cachedWidth === width && cachedVersion === stateVersion) {
147
- return cachedLines;
148
- }
149
-
150
- if (!currentModel) return [];
151
-
152
- const uiHelper = makeUI(theme, width);
153
- const lines: string[] = [];
154
- const push = (...rows: string[][]) => { for (const r of rows) lines.push(...r); };
155
- const model = currentModel;
156
-
157
- // 1. Top bar
158
- push(uiHelper.bar());
159
-
160
- // 2. Title area — title with optional inline badge
161
- if (model.badge && model.badgeStatus) {
162
- const titleText = uiHelper.header(model.title)[0];
163
- const badgeGlyph = uiHelper.statusGlyph(model.badgeStatus);
164
- const badgeLabel = uiHelper.statusBadge(model.badge, model.badgeStatus)[0];
165
- lines.push(`${titleText} ${badgeGlyph} ${badgeLabel.trimStart()}`);
166
- } else {
167
- push(uiHelper.header(model.title));
168
- }
169
-
170
- // 3. Subtitle
171
- if (model.subtitle?.length) {
172
- for (const line of model.subtitle) {
173
- push(uiHelper.meta(line));
174
- }
175
- }
176
-
177
- // 4. Blank line
178
- push(uiHelper.blank());
179
-
180
- // 5. Items
181
- for (const item of model.items) {
182
- // Pulse: when pulseBright is false and item is active, render as pending (dimmed)
183
- const renderStatus: ProgressStatus = (!pulseBright && item.status === "active")
184
- ? "pending"
185
- : item.status;
186
-
187
- push(uiHelper.progressItem(item.label, renderStatus, {
188
- detail: item.detail,
189
- emphasized: item.status === "active",
190
- }));
191
-
192
- if (item.annotation) {
193
- push(uiHelper.progressAnnotation(item.annotation));
194
- }
195
- }
196
-
197
- // 6. Blank line (if meta or hints follow)
198
- if (model.meta?.length || model.hints?.length) {
199
- push(uiHelper.blank());
200
- }
201
-
202
- // 7. Meta
203
- if (model.meta?.length) {
204
- for (const line of model.meta) {
205
- push(uiHelper.meta(line));
206
- }
207
- }
208
-
209
- // 8. Hints
210
- if (model.hints?.length) {
211
- push(uiHelper.hints(model.hints));
212
- }
213
-
214
- // 9. Bottom bar
215
- push(uiHelper.bar());
216
-
217
- cachedLines = lines;
218
- cachedWidth = width;
219
- cachedVersion = stateVersion;
220
- return lines;
221
- }
222
-
223
- // ── Register widget ───────────────────────────────────────────────────────
224
-
225
- ui.setWidget(widgetKey, (tui: TUI, theme: Theme) => {
226
- widgetRef = {
227
- invalidate: () => { cachedLines = undefined; },
228
- requestRender: () => tui.requestRender(),
229
- };
230
-
231
- return {
232
- render(width: number): string[] {
233
- return renderPanel(width, theme);
234
- },
235
- invalidate() {
236
- cachedLines = undefined;
237
- },
238
- };
239
- });
240
-
241
- // ── Controller ────────────────────────────────────────────────────────────
242
-
243
- return {
244
- update(model: ProgressPanelModel): void {
245
- currentModel = model;
246
- stateVersion++;
247
- cachedLines = undefined;
248
- updateFooterStatus();
249
- if (widgetRef) widgetRef.requestRender();
250
- },
251
-
252
- startPulse(): void {
253
- if (pulseTimer) return; // already pulsing
254
- pulseTimer = setInterval(() => {
255
- pulseBright = !pulseBright;
256
- cachedLines = undefined;
257
- if (widgetRef) widgetRef.requestRender();
258
- }, PULSE_INTERVAL_MS);
259
- },
260
-
261
- stopPulse(): void {
262
- if (pulseTimer) {
263
- clearInterval(pulseTimer);
264
- pulseTimer = null;
265
- }
266
- pulseBright = true;
267
- cachedLines = undefined;
268
- if (widgetRef) widgetRef.requestRender();
269
- },
270
-
271
- dispose(): void {
272
- if (pulseTimer) {
273
- clearInterval(pulseTimer);
274
- pulseTimer = null;
275
- }
276
- ui.setWidget(widgetKey, undefined);
277
- ui.setStatus(statusKey, undefined);
278
- currentModel = null;
279
- widgetRef = null;
280
- },
281
- };
282
- }
@@ -1,107 +0,0 @@
1
- /**
2
- * Shared thinking/spinner widget.
3
- *
4
- * Shows an animated spinner with a label and an optional live-preview of
5
- * streamed text (e.g. LLM output) while a background operation is running.
6
- *
7
- * Usage:
8
- *
9
- * import { showThinkingWidget } from "./shared/thinking-widget.js";
10
- *
11
- * const widget = showThinkingWidget("Generating questions…", ctx);
12
- *
13
- * // Optionally stream partial text into the preview line:
14
- * widget.setText(partialLlmOutput);
15
- *
16
- * // Always dispose when done — removes the widget from the UI:
17
- * widget.dispose();
18
- *
19
- * Each call gets a unique widget key derived from a monotonic counter, so
20
- * multiple widgets can safely coexist without key collisions.
21
- */
22
-
23
- import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
24
- import { type Theme } from "@gsd/pi-coding-agent";
25
- import { truncateToWidth, type TUI } from "@gsd/pi-tui";
26
-
27
- // ─── Public API ───────────────────────────────────────────────────────────────
28
-
29
- export interface ThinkingWidget {
30
- /**
31
- * Update the streamed-text preview line.
32
- * Pass the full accumulated text — the widget trims and previews the tail.
33
- */
34
- setText(text: string): void;
35
- /** Remove the widget from the UI. Always call this when the operation completes. */
36
- dispose(): void;
37
- }
38
-
39
- // ─── Internal constants ───────────────────────────────────────────────────────
40
-
41
- const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
42
- const SPINNER_INTERVAL_MS = 80;
43
- const PREVIEW_MAX_CHARS = 120;
44
-
45
- let widgetCounter = 0;
46
-
47
- // ─── Implementation ───────────────────────────────────────────────────────────
48
-
49
- /**
50
- * Show an animated thinking spinner as a TUI widget.
51
- *
52
- * @param label Short description of what is happening, e.g. "Writing PROJECT.md…"
53
- * @param ctx Extension command context
54
- * @returns Handle with setText() and dispose()
55
- */
56
- export function showThinkingWidget(label: string, ctx: ExtensionCommandContext): ThinkingWidget {
57
- const widgetKey = `thinking-widget-${++widgetCounter}`;
58
-
59
- let streamedText = "";
60
- let widgetRef: { invalidate: () => void; requestRender: () => void } | null = null;
61
-
62
- ctx.ui.setWidget(widgetKey, (tui: TUI, theme: Theme) => {
63
- let frame = 0;
64
- let cachedLines: string[] | undefined;
65
-
66
- const interval = setInterval(() => {
67
- frame = (frame + 1) % SPINNER_FRAMES.length;
68
- cachedLines = undefined;
69
- tui.requestRender();
70
- }, SPINNER_INTERVAL_MS);
71
-
72
- widgetRef = {
73
- invalidate: () => { cachedLines = undefined; },
74
- requestRender: () => tui.requestRender(),
75
- };
76
-
77
- return {
78
- render(width: number): string[] {
79
- if (cachedLines) return cachedLines;
80
- const spinner = theme.fg("accent", SPINNER_FRAMES[frame]);
81
- const lines: string[] = [];
82
- lines.push(truncateToWidth(` ${spinner} ${theme.fg("muted", label)}`, width));
83
- if (streamedText) {
84
- const preview = streamedText.replace(/\s+/g, " ").trim().slice(-PREVIEW_MAX_CHARS);
85
- lines.push(truncateToWidth(` ${theme.fg("dim", preview)}`, width));
86
- }
87
- cachedLines = lines;
88
- return lines;
89
- },
90
- invalidate() { cachedLines = undefined; },
91
- dispose() { clearInterval(interval); },
92
- };
93
- });
94
-
95
- return {
96
- setText(text: string) {
97
- streamedText = text;
98
- if (widgetRef) {
99
- widgetRef.invalidate();
100
- widgetRef.requestRender();
101
- }
102
- },
103
- dispose() {
104
- ctx.ui.setWidget(widgetKey, undefined);
105
- },
106
- };
107
- }
@@ -1,10 +0,0 @@
1
- /**
2
- * Focused re-export: hook-related preferences.
3
- *
4
- * Consumers that only need hook resolution can import from this module
5
- * instead of the monolithic preferences.ts, reducing coupling surface.
6
- */
7
- export {
8
- resolvePostUnitHooks,
9
- resolvePreDispatchHooks,
10
- } from "./preferences.js";