gsd-pi 2.28.0-dev.a3afd44 → 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.
- package/dist/resources/extensions/gsd/commands-handlers.ts +1 -9
- package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +14 -22
- package/dist/resources/extensions/gsd/commands.ts +1 -7
- package/dist/resources/extensions/gsd/index.ts +2 -1
- package/dist/resources/extensions/gsd/json-persistence.ts +52 -0
- package/dist/resources/extensions/gsd/metrics.ts +17 -31
- package/dist/resources/extensions/gsd/paths.ts +0 -8
- package/dist/resources/extensions/gsd/routing-history.ts +13 -17
- package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +1 -1
- package/dist/resources/extensions/gsd/unit-runtime.ts +16 -13
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +2 -2
- package/dist/resources/extensions/remote-questions/notify.ts +1 -2
- package/dist/resources/extensions/remote-questions/slack-adapter.ts +1 -2
- package/dist/resources/extensions/remote-questions/telegram-adapter.ts +1 -2
- package/dist/resources/extensions/remote-questions/types.ts +3 -0
- package/dist/resources/extensions/shared/mod.ts +3 -0
- package/package.json +4 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +8 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.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 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/settings-manager.ts +11 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +4 -1
- package/packages/pi-tui/dist/autocomplete.d.ts +3 -0
- package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
- package/packages/pi-tui/dist/autocomplete.js +14 -0
- package/packages/pi-tui/dist/autocomplete.js.map +1 -1
- package/packages/pi-tui/src/autocomplete.ts +19 -1
- package/src/resources/extensions/gsd/commands-handlers.ts +1 -9
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +14 -22
- package/src/resources/extensions/gsd/commands.ts +1 -7
- package/src/resources/extensions/gsd/index.ts +2 -1
- package/src/resources/extensions/gsd/json-persistence.ts +52 -0
- package/src/resources/extensions/gsd/metrics.ts +17 -31
- package/src/resources/extensions/gsd/paths.ts +0 -8
- package/src/resources/extensions/gsd/routing-history.ts +13 -17
- package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +1 -1
- package/src/resources/extensions/gsd/unit-runtime.ts +16 -13
- package/src/resources/extensions/remote-questions/discord-adapter.ts +2 -2
- package/src/resources/extensions/remote-questions/notify.ts +1 -2
- package/src/resources/extensions/remote-questions/slack-adapter.ts +1 -2
- package/src/resources/extensions/remote-questions/telegram-adapter.ts +1 -2
- package/src/resources/extensions/remote-questions/types.ts +3 -0
- package/src/resources/extensions/shared/mod.ts +3 -0
- package/dist/resources/extensions/gsd/preferences-hooks.ts +0 -10
- package/dist/resources/extensions/shared/progress-widget.ts +0 -282
- package/dist/resources/extensions/shared/thinking-widget.ts +0 -107
- package/src/resources/extensions/gsd/preferences-hooks.ts +0 -10
- package/src/resources/extensions/shared/progress-widget.ts +0 -282
- package/src/resources/extensions/shared/thinking-widget.ts +0 -107
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// Tracks success/failure per tier per unit-type pattern to improve
|
|
3
3
|
// classification accuracy over time.
|
|
4
4
|
|
|
5
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
6
5
|
import { join } from "node:path";
|
|
7
6
|
import { gsdRoot } from "./paths.js";
|
|
8
7
|
import type { ComplexityTier } from "./types.js";
|
|
8
|
+
import { loadJsonFile, saveJsonFile } from "./json-persistence.js";
|
|
9
9
|
|
|
10
10
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
11
11
|
|
|
@@ -267,24 +267,20 @@ function historyPath(base: string): string {
|
|
|
267
267
|
return join(gsdRoot(base), HISTORY_FILE);
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
function isRoutingHistoryData(data: unknown): data is RoutingHistoryData {
|
|
271
|
+
return (
|
|
272
|
+
typeof data === "object" &&
|
|
273
|
+
data !== null &&
|
|
274
|
+
(data as RoutingHistoryData).version === 1 &&
|
|
275
|
+
typeof (data as RoutingHistoryData).patterns === "object" &&
|
|
276
|
+
(data as RoutingHistoryData).patterns !== null
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
270
280
|
function loadHistory(base: string): RoutingHistoryData {
|
|
271
|
-
|
|
272
|
-
const raw = readFileSync(historyPath(base), "utf-8");
|
|
273
|
-
const parsed = JSON.parse(raw);
|
|
274
|
-
if (parsed.version === 1 && parsed.patterns) {
|
|
275
|
-
return parsed as RoutingHistoryData;
|
|
276
|
-
}
|
|
277
|
-
} catch {
|
|
278
|
-
// File doesn't exist or is corrupt — start fresh
|
|
279
|
-
}
|
|
280
|
-
return createEmptyHistory();
|
|
281
|
+
return loadJsonFile(historyPath(base), isRoutingHistoryData, createEmptyHistory);
|
|
281
282
|
}
|
|
282
283
|
|
|
283
284
|
function saveHistory(base: string, data: RoutingHistoryData): void {
|
|
284
|
-
|
|
285
|
-
mkdirSync(gsdRoot(base), { recursive: true });
|
|
286
|
-
writeFileSync(historyPath(base), JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
287
|
-
} catch {
|
|
288
|
-
// Non-fatal — don't let history failures break auto-mode
|
|
289
|
-
}
|
|
285
|
+
saveJsonFile(historyPath(base), data);
|
|
290
286
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Tests the pure formatInspectOutput function with known data.
|
|
4
4
|
|
|
5
5
|
import { createTestContext } from './test-helpers.ts';
|
|
6
|
-
import { formatInspectOutput, type InspectData } from '../commands.ts';
|
|
6
|
+
import { formatInspectOutput, type InspectData } from '../commands-inspect.ts';
|
|
7
7
|
|
|
8
8
|
const { assertEq, assertTrue, assertMatch, report } = createTestContext();
|
|
9
9
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
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(/[
|
|
55
|
-
const sanitizedUnitId = unitId.replace(/[
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* Remote Questions — Discord adapter
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type
|
|
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
|
-
|
|
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
|
|
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
|
|
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";
|