gsd-pi 2.35.0-dev.30eec3f → 2.35.0-dev.640d5c7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2 -7
- package/dist/resource-loader.d.ts +1 -1
- package/dist/resource-loader.js +1 -13
- package/dist/resources/extensions/async-jobs/await-tool.js +2 -0
- package/dist/resources/extensions/async-jobs/job-manager.js +6 -0
- package/dist/resources/extensions/bg-shell/output-formatter.js +19 -1
- package/dist/resources/extensions/bg-shell/process-manager.js +4 -0
- package/dist/resources/extensions/bg-shell/types.js +2 -0
- package/dist/resources/extensions/context7/index.js +0 -5
- package/dist/resources/extensions/get-secrets-from-user.js +30 -2
- package/dist/resources/extensions/google-search/index.js +0 -5
- package/dist/resources/extensions/gsd/auto-dispatch.js +1 -43
- package/dist/resources/extensions/gsd/auto-loop.js +1 -10
- package/dist/resources/extensions/gsd/auto-recovery.js +0 -35
- package/dist/resources/extensions/gsd/auto-start.js +2 -35
- package/dist/resources/extensions/gsd/auto.js +4 -59
- package/dist/resources/extensions/gsd/commands-handlers.js +2 -2
- package/dist/resources/extensions/gsd/doctor-environment.js +17 -26
- package/dist/resources/extensions/gsd/files.js +1 -9
- package/dist/resources/extensions/gsd/gitignore.js +7 -54
- package/dist/resources/extensions/gsd/guided-flow.js +1 -1
- package/dist/resources/extensions/gsd/health-widget.js +46 -97
- package/dist/resources/extensions/gsd/index.js +1 -10
- package/dist/resources/extensions/gsd/migrate-external.js +2 -55
- package/dist/resources/extensions/gsd/paths.js +7 -74
- package/dist/resources/extensions/gsd/post-unit-hooks.js +1 -4
- package/dist/resources/extensions/gsd/preferences-validation.js +1 -16
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +0 -2
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +0 -2
- package/dist/resources/extensions/gsd/session-lock.js +2 -26
- package/dist/resources/extensions/gsd/templates/plan.md +0 -8
- package/dist/resources/extensions/gsd/worktree-resolver.js +0 -12
- package/dist/resources/extensions/remote-questions/remote-command.js +22 -2
- package/dist/resources/extensions/shared/mod.js +1 -1
- package/dist/resources/extensions/shared/sanitize.js +0 -30
- package/dist/resources/extensions/shared/wizard-ui.js +478 -0
- package/dist/resources/extensions/subagent/index.js +14 -6
- package/package.json +1 -2
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.js +2 -13
- package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
- package/packages/pi-coding-agent/src/core/resource-loader.ts +2 -13
- package/src/resources/extensions/async-jobs/await-tool.ts +2 -0
- package/src/resources/extensions/async-jobs/job-manager.ts +7 -0
- package/src/resources/extensions/bg-shell/output-formatter.ts +17 -0
- package/src/resources/extensions/bg-shell/process-manager.ts +4 -0
- package/src/resources/extensions/bg-shell/types.ts +12 -0
- package/src/resources/extensions/context7/index.ts +0 -7
- package/src/resources/extensions/get-secrets-from-user.ts +35 -2
- package/src/resources/extensions/google-search/index.ts +0 -7
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -49
- package/src/resources/extensions/gsd/auto-loop.ts +1 -11
- package/src/resources/extensions/gsd/auto-recovery.ts +0 -39
- package/src/resources/extensions/gsd/auto-start.ts +2 -42
- package/src/resources/extensions/gsd/auto.ts +3 -61
- package/src/resources/extensions/gsd/commands-handlers.ts +2 -2
- package/src/resources/extensions/gsd/doctor-environment.ts +16 -26
- package/src/resources/extensions/gsd/files.ts +1 -10
- package/src/resources/extensions/gsd/gitignore.ts +7 -54
- package/src/resources/extensions/gsd/guided-flow.ts +1 -1
- package/src/resources/extensions/gsd/health-widget.ts +59 -103
- package/src/resources/extensions/gsd/index.ts +1 -10
- package/src/resources/extensions/gsd/migrate-external.ts +2 -47
- package/src/resources/extensions/gsd/paths.ts +7 -73
- package/src/resources/extensions/gsd/post-unit-hooks.ts +1 -5
- package/src/resources/extensions/gsd/preferences-validation.ts +1 -16
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +0 -2
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +0 -2
- package/src/resources/extensions/gsd/session-lock.ts +2 -29
- package/src/resources/extensions/gsd/templates/plan.md +0 -8
- package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -12
- package/src/resources/extensions/gsd/tests/validate-directory.test.ts +0 -15
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +0 -2
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +0 -32
- package/src/resources/extensions/gsd/worktree-resolver.ts +0 -11
- package/src/resources/extensions/remote-questions/remote-command.ts +23 -2
- package/src/resources/extensions/shared/mod.ts +1 -1
- package/src/resources/extensions/shared/sanitize.ts +0 -36
- package/src/resources/extensions/shared/wizard-ui.ts +551 -0
- package/src/resources/extensions/subagent/index.ts +12 -6
- package/dist/resources/extensions/gsd/health-widget-core.js +0 -96
- package/dist/resources/extensions/gsd/roadmap-mutations.js +0 -55
- package/src/resources/extensions/gsd/health-widget-core.ts +0 -129
- package/src/resources/extensions/gsd/roadmap-mutations.ts +0 -66
- package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +0 -214
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +0 -158
- package/src/resources/extensions/gsd/tests/paths.test.ts +0 -113
- package/src/resources/extensions/gsd/tests/test-utils.ts +0 -165
|
@@ -113,14 +113,6 @@
|
|
|
113
113
|
- Tasks execute sequentially in order (T01, T02, T03, ...)
|
|
114
114
|
- est: is informational (e.g. 30m, 1h, 2h) and optional
|
|
115
115
|
|
|
116
|
-
Verify field rules:
|
|
117
|
-
- MUST be a mechanically executable command: `npm test`, `grep -q "pattern" file`, `test -f path`
|
|
118
|
-
- For content/document tasks: verify file existence, section count, YAML validity, or word count
|
|
119
|
-
NOT exact phrasing, specific formulas, or "zero TBD" aspirational criteria
|
|
120
|
-
- If no command can verify the output, write: "Manual review — file exists and is non-empty"
|
|
121
|
-
- BAD: "Sections 3.1 and 3.2 exist with exact formulas. Zero TBD/TODO."
|
|
122
|
-
- GOOD: `grep -c "^## " doc.md` returns >= 4 (4+ sections), `! grep -q "TBD\|TODO" doc.md`
|
|
123
|
-
|
|
124
116
|
Integration closure rule:
|
|
125
117
|
- At least one slice in any multi-boundary milestone should perform real composition/wiring, not just contract hardening
|
|
126
118
|
- For the final assembly slice, verification must exercise the real entrypoint or runtime path
|
|
@@ -12,8 +12,6 @@
|
|
|
12
12
|
* Key invariant: `createAutoWorktree()` and `enterAutoWorktree()` call
|
|
13
13
|
* `process.chdir()` internally — this class MUST NOT double-chdir.
|
|
14
14
|
*/
|
|
15
|
-
import { existsSync, unlinkSync } from "node:fs";
|
|
16
|
-
import { join } from "node:path";
|
|
17
15
|
import { debugLog } from "./debug-logger.js";
|
|
18
16
|
// ─── WorktreeResolver ──────────────────────────────────────────────────────
|
|
19
17
|
export class WorktreeResolver {
|
|
@@ -255,16 +253,6 @@ export class WorktreeResolver {
|
|
|
255
253
|
fallback: "chdir-to-project-root",
|
|
256
254
|
});
|
|
257
255
|
ctx.notify(`Milestone merge failed: ${msg}`, "warning");
|
|
258
|
-
// Clean up stale merge state left by failed squash-merge (#1389)
|
|
259
|
-
try {
|
|
260
|
-
const gitDir = join(originalBase || this.s.basePath, ".git");
|
|
261
|
-
for (const f of ["SQUASH_MSG", "MERGE_HEAD", "MERGE_MSG"]) {
|
|
262
|
-
const p = join(gitDir, f);
|
|
263
|
-
if (existsSync(p))
|
|
264
|
-
unlinkSync(p);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
catch { /* best-effort */ }
|
|
268
256
|
// Error recovery: always restore to project root
|
|
269
257
|
if (originalBase) {
|
|
270
258
|
try {
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Remote Questions — /gsd remote command
|
|
3
3
|
*/
|
|
4
4
|
import { AuthStorage } from "@gsd/pi-coding-agent";
|
|
5
|
-
import { Editor, Key, matchesKey, truncateToWidth } from "@gsd/pi-tui";
|
|
5
|
+
import { CURSOR_MARKER, Editor, Key, matchesKey, truncateToWidth } from "@gsd/pi-tui";
|
|
6
6
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
8
|
import { getGlobalGSDPreferencesPath, loadEffectiveGSDPreferences } from "../gsd/preferences.js";
|
|
9
9
|
import { getRemoteConfigStatus, isValidChannelId, resolveRemoteConfig } from "./config.js";
|
|
10
|
-
import {
|
|
10
|
+
import { sanitizeError } from "../shared/sanitize.js";
|
|
11
11
|
import { getLatestPromptSummary } from "./status.js";
|
|
12
12
|
export async function handleRemote(subcommand, ctx, _pi) {
|
|
13
13
|
const trimmed = subcommand.trim();
|
|
@@ -339,6 +339,26 @@ function removeRemoteQuestionsConfig() {
|
|
|
339
339
|
const next = frontmatter ? `---\n${frontmatter}\n---${content.slice(fmMatch[0].length)}` : content.slice(fmMatch[0].length).replace(/^\n+/, "");
|
|
340
340
|
writeFileSync(prefsPath, next, "utf-8");
|
|
341
341
|
}
|
|
342
|
+
function maskEditorLine(line) {
|
|
343
|
+
let output = "";
|
|
344
|
+
let i = 0;
|
|
345
|
+
while (i < line.length) {
|
|
346
|
+
if (line.startsWith(CURSOR_MARKER, i)) {
|
|
347
|
+
output += CURSOR_MARKER;
|
|
348
|
+
i += CURSOR_MARKER.length;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
const ansiMatch = /^\x1b\[[0-9;]*m/.exec(line.slice(i));
|
|
352
|
+
if (ansiMatch) {
|
|
353
|
+
output += ansiMatch[0];
|
|
354
|
+
i += ansiMatch[0].length;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
output += line[i] === " " ? " " : "*";
|
|
358
|
+
i += 1;
|
|
359
|
+
}
|
|
360
|
+
return output;
|
|
361
|
+
}
|
|
342
362
|
async function promptMaskedInput(ctx, label, hint) {
|
|
343
363
|
if (!ctx.hasUI)
|
|
344
364
|
return null;
|
|
@@ -6,6 +6,6 @@ export { toPosixPath } from "./path-display.js";
|
|
|
6
6
|
export { showInterviewRound } from "./interview-ui.js";
|
|
7
7
|
export { showNextAction } from "./next-action-ui.js";
|
|
8
8
|
export { showConfirm } from "./confirm-ui.js";
|
|
9
|
-
export { sanitizeError
|
|
9
|
+
export { sanitizeError } from "./sanitize.js";
|
|
10
10
|
export { formatDateShort, truncateWithEllipsis } from "./format-utils.js";
|
|
11
11
|
export { splitFrontmatter, parseFrontmatterMap } from "./frontmatter.js";
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sanitize error messages by redacting token-like strings before surfacing.
|
|
3
|
-
* Also provides maskEditorLine for masking sensitive TUI editor input.
|
|
4
3
|
*/
|
|
5
|
-
import { CURSOR_MARKER } from "@gsd/pi-tui";
|
|
6
4
|
const TOKEN_PATTERNS = [
|
|
7
5
|
/xoxb-[A-Za-z0-9\-]+/g, // Slack bot tokens
|
|
8
6
|
/xoxp-[A-Za-z0-9\-]+/g, // Slack user tokens
|
|
@@ -17,31 +15,3 @@ export function sanitizeError(msg) {
|
|
|
17
15
|
}
|
|
18
16
|
return sanitized;
|
|
19
17
|
}
|
|
20
|
-
/**
|
|
21
|
-
* Replace editor visible text with masked characters while preserving
|
|
22
|
-
* ANSI cursor/sequencer codes. Keeps border/metadata lines readable.
|
|
23
|
-
*/
|
|
24
|
-
export function maskEditorLine(line) {
|
|
25
|
-
if (line.startsWith("─")) {
|
|
26
|
-
return line;
|
|
27
|
-
}
|
|
28
|
-
let output = "";
|
|
29
|
-
let i = 0;
|
|
30
|
-
while (i < line.length) {
|
|
31
|
-
if (line.startsWith(CURSOR_MARKER, i)) {
|
|
32
|
-
output += CURSOR_MARKER;
|
|
33
|
-
i += CURSOR_MARKER.length;
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
const ansiMatch = /^\x1b\[[0-9;]*m/.exec(line.slice(i));
|
|
37
|
-
if (ansiMatch) {
|
|
38
|
-
output += ansiMatch[0];
|
|
39
|
-
i += ansiMatch[0].length;
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
const ch = line[i];
|
|
43
|
-
output += ch === " " ? " " : "*";
|
|
44
|
-
i += 1;
|
|
45
|
-
}
|
|
46
|
-
return output;
|
|
47
|
-
}
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* General-purpose multi-page wizard UI.
|
|
3
|
+
*
|
|
4
|
+
* Supports declarative page definitions with select and text fields.
|
|
5
|
+
* Pages can conditionally route to different next pages based on answers.
|
|
6
|
+
*
|
|
7
|
+
* Navigation:
|
|
8
|
+
* ← go back one page (on page 1: triggers exit confirmation)
|
|
9
|
+
* → / Enter advance to next page (or submit on last page)
|
|
10
|
+
* Escape triggers exit confirmation overlay
|
|
11
|
+
*
|
|
12
|
+
* Exit confirmation (shown on Escape or ← from page 1):
|
|
13
|
+
* 1. Go back — dismiss and return to current page
|
|
14
|
+
* 2. Exit — cancel the wizard, returns null to caller
|
|
15
|
+
*
|
|
16
|
+
* Returns:
|
|
17
|
+
* Record<pageId, Record<fieldId, string | string[]>> on completion
|
|
18
|
+
* null on exit/cancel
|
|
19
|
+
*
|
|
20
|
+
* Example:
|
|
21
|
+
*
|
|
22
|
+
* const result = await showWizard(ctx, {
|
|
23
|
+
* title: "New Project",
|
|
24
|
+
* pages: [
|
|
25
|
+
* {
|
|
26
|
+
* id: "mode",
|
|
27
|
+
* fields: [
|
|
28
|
+
* {
|
|
29
|
+
* type: "select",
|
|
30
|
+
* id: "start_type",
|
|
31
|
+
* question: "How do you want to start?",
|
|
32
|
+
* options: [
|
|
33
|
+
* { label: "Describe it", description: "Type what you want to build." },
|
|
34
|
+
* { label: "Provide a file", description: "Point to an existing doc." },
|
|
35
|
+
* ],
|
|
36
|
+
* },
|
|
37
|
+
* ],
|
|
38
|
+
* next: (answers) =>
|
|
39
|
+
* answers["mode"]?.["start_type"] === "Provide a file" ? "file_path" : null,
|
|
40
|
+
* },
|
|
41
|
+
* {
|
|
42
|
+
* id: "file_path",
|
|
43
|
+
* fields: [
|
|
44
|
+
* { type: "text", id: "path", label: "File path", placeholder: "/path/to/doc.md" },
|
|
45
|
+
* ],
|
|
46
|
+
* next: () => null,
|
|
47
|
+
* },
|
|
48
|
+
* ],
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* if (!result) return; // user exited
|
|
52
|
+
* const startType = result["mode"]["start_type"]; // "Describe it" | "Provide a file"
|
|
53
|
+
* const filePath = result["file_path"]?.["path"];
|
|
54
|
+
*/
|
|
55
|
+
import { Editor, Key, matchesKey, truncateToWidth, } from "@gsd/pi-tui";
|
|
56
|
+
import { makeUI } from "./ui.js";
|
|
57
|
+
// ─── Main export ──────────────────────────────────────────────────────────────
|
|
58
|
+
/**
|
|
59
|
+
* Show a multi-page wizard and return collected answers, or null if the user exits.
|
|
60
|
+
*/
|
|
61
|
+
export async function showWizard(ctx, opts) {
|
|
62
|
+
const pageMap = new Map(opts.pages.map((p) => [p.id, p]));
|
|
63
|
+
return ctx.ui.custom((tui, theme, _kb, done) => {
|
|
64
|
+
// ── State ──────────────────────────────────────────────────────────────
|
|
65
|
+
/** Stack of page ids visited — drives back navigation */
|
|
66
|
+
const pageStack = [opts.pages[0].id];
|
|
67
|
+
const pageStates = new Map();
|
|
68
|
+
/** Collected answers across all pages */
|
|
69
|
+
const answers = {};
|
|
70
|
+
/** Whether the exit-confirmation overlay is showing */
|
|
71
|
+
let showingExitConfirm = false;
|
|
72
|
+
/** Cursor in the exit-confirm overlay: 0 = go back, 1 = exit */
|
|
73
|
+
let exitCursor = 0;
|
|
74
|
+
let cachedLines;
|
|
75
|
+
// Editors keyed by fieldId — one per text field
|
|
76
|
+
// editorTheme is derived from the design system at first render
|
|
77
|
+
const editors = new Map();
|
|
78
|
+
let resolvedEditorTheme = null;
|
|
79
|
+
function getEditor(fieldId) {
|
|
80
|
+
if (!resolvedEditorTheme)
|
|
81
|
+
resolvedEditorTheme = makeUI(theme, 80).editorTheme;
|
|
82
|
+
if (!editors.has(fieldId))
|
|
83
|
+
editors.set(fieldId, new Editor(tui, resolvedEditorTheme));
|
|
84
|
+
return editors.get(fieldId);
|
|
85
|
+
}
|
|
86
|
+
// ── Page state helpers ─────────────────────────────────────────────────
|
|
87
|
+
function getPageState(pageId) {
|
|
88
|
+
if (!pageStates.has(pageId)) {
|
|
89
|
+
pageStates.set(pageId, {
|
|
90
|
+
selectStates: new Map(),
|
|
91
|
+
textValues: new Map(),
|
|
92
|
+
focusedFieldId: null,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return pageStates.get(pageId);
|
|
96
|
+
}
|
|
97
|
+
function getSelectState(pageId, fieldId, _optCount) {
|
|
98
|
+
const ps = getPageState(pageId);
|
|
99
|
+
if (!ps.selectStates.has(fieldId)) {
|
|
100
|
+
ps.selectStates.set(fieldId, {
|
|
101
|
+
cursorIndex: 0,
|
|
102
|
+
committedIndex: null, // nothing pre-committed — user must explicitly confirm
|
|
103
|
+
checkedIndices: new Set(),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return ps.selectStates.get(fieldId);
|
|
107
|
+
}
|
|
108
|
+
// ── Current page ───────────────────────────────────────────────────────
|
|
109
|
+
function currentPageId() {
|
|
110
|
+
return pageStack[pageStack.length - 1];
|
|
111
|
+
}
|
|
112
|
+
function currentPage() {
|
|
113
|
+
return pageMap.get(currentPageId());
|
|
114
|
+
}
|
|
115
|
+
function currentPageState() {
|
|
116
|
+
return getPageState(currentPageId());
|
|
117
|
+
}
|
|
118
|
+
// ── Validation ─────────────────────────────────────────────────────────
|
|
119
|
+
function isPageComplete(page, ps) {
|
|
120
|
+
for (const field of page.fields) {
|
|
121
|
+
if (field.type === "select") {
|
|
122
|
+
const ss = ps.selectStates.get(field.id);
|
|
123
|
+
if (!ss)
|
|
124
|
+
return false;
|
|
125
|
+
if (field.allowMultiple) {
|
|
126
|
+
if (ss.checkedIndices.size === 0)
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
if (ss.committedIndex === null)
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const val = ps.textValues.get(field.id) ?? "";
|
|
136
|
+
if (!val.trim())
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
// ── Collect answers for a page ─────────────────────────────────────────
|
|
143
|
+
function collectPageAnswers(page, ps) {
|
|
144
|
+
const result = {};
|
|
145
|
+
for (const field of page.fields) {
|
|
146
|
+
if (field.type === "select") {
|
|
147
|
+
const ss = ps.selectStates.get(field.id);
|
|
148
|
+
if (!ss)
|
|
149
|
+
continue;
|
|
150
|
+
if (field.allowMultiple) {
|
|
151
|
+
result[field.id] = Array.from(ss.checkedIndices)
|
|
152
|
+
.sort((a, b) => a - b)
|
|
153
|
+
.map((i) => field.options[i].label);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
if (ss.committedIndex !== null && ss.committedIndex < field.options.length) {
|
|
157
|
+
result[field.id] = field.options[ss.committedIndex].label;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
result[field.id] = ps.textValues.get(field.id) ?? "";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
// ── Auto-focus helper ──────────────────────────────────────────────────
|
|
168
|
+
/** If a page's first field is a text field, focus it immediately on arrival. */
|
|
169
|
+
function autoFocusPageIfText(pageId) {
|
|
170
|
+
const page = pageMap.get(pageId);
|
|
171
|
+
if (!page)
|
|
172
|
+
return;
|
|
173
|
+
const firstField = page.fields[0];
|
|
174
|
+
if (firstField?.type === "text") {
|
|
175
|
+
const ps = getPageState(pageId);
|
|
176
|
+
ps.focusedFieldId = firstField.id;
|
|
177
|
+
const editor = getEditor(firstField.id);
|
|
178
|
+
editor.setText(ps.textValues.get(firstField.id) ?? "");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Auto-focus the first page if it starts with a text field
|
|
182
|
+
autoFocusPageIfText(opts.pages[0].id);
|
|
183
|
+
// ── Navigation ─────────────────────────────────────────────────────────
|
|
184
|
+
function advance() {
|
|
185
|
+
const page = currentPage();
|
|
186
|
+
const ps = currentPageState();
|
|
187
|
+
if (!isPageComplete(page, ps)) {
|
|
188
|
+
refresh();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// Save text field values from editors
|
|
192
|
+
for (const field of page.fields) {
|
|
193
|
+
if (field.type === "text") {
|
|
194
|
+
ps.textValues.set(field.id, getEditor(field.id).getText().trim());
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Collect answers for this page
|
|
198
|
+
answers[page.id] = collectPageAnswers(page, ps);
|
|
199
|
+
// Route to next page
|
|
200
|
+
const nextId = page.next ? page.next(answers) : null;
|
|
201
|
+
if (!nextId) {
|
|
202
|
+
// End of wizard
|
|
203
|
+
done(answers);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const nextPage = pageMap.get(nextId);
|
|
207
|
+
if (!nextPage) {
|
|
208
|
+
done(answers);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
pageStack.push(nextId);
|
|
212
|
+
autoFocusPageIfText(nextId);
|
|
213
|
+
refresh();
|
|
214
|
+
}
|
|
215
|
+
function goBack() {
|
|
216
|
+
if (pageStack.length <= 1) {
|
|
217
|
+
// Already at first page — Esc here means exit
|
|
218
|
+
showingExitConfirm = true;
|
|
219
|
+
exitCursor = 0;
|
|
220
|
+
refresh();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
pageStack.pop();
|
|
224
|
+
autoFocusPageIfText(currentPageId());
|
|
225
|
+
refresh();
|
|
226
|
+
}
|
|
227
|
+
function refresh() {
|
|
228
|
+
cachedLines = undefined;
|
|
229
|
+
tui.requestRender();
|
|
230
|
+
}
|
|
231
|
+
// ── Input handler ──────────────────────────────────────────────────────
|
|
232
|
+
function handleInput(data) {
|
|
233
|
+
// ── Exit confirm overlay ─────────────────────────────────────────
|
|
234
|
+
if (showingExitConfirm) {
|
|
235
|
+
if (matchesKey(data, Key.up)) {
|
|
236
|
+
exitCursor = 0;
|
|
237
|
+
refresh();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (matchesKey(data, Key.down)) {
|
|
241
|
+
exitCursor = 1;
|
|
242
|
+
refresh();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (data === "1") {
|
|
246
|
+
showingExitConfirm = false;
|
|
247
|
+
refresh();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (data === "2") {
|
|
251
|
+
done(null);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (matchesKey(data, Key.enter) || matchesKey(data, Key.space)) {
|
|
255
|
+
if (exitCursor === 0) {
|
|
256
|
+
showingExitConfirm = false;
|
|
257
|
+
refresh();
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
done(null);
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
// Esc on the confirm screen = go back (dismiss confirm)
|
|
265
|
+
if (matchesKey(data, Key.escape)) {
|
|
266
|
+
showingExitConfirm = false;
|
|
267
|
+
refresh();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
// ── Text field focus ─────────────────────────────────────────────
|
|
273
|
+
const ps = currentPageState();
|
|
274
|
+
if (ps.focusedFieldId) {
|
|
275
|
+
const editor = getEditor(ps.focusedFieldId);
|
|
276
|
+
if (matchesKey(data, Key.escape)) {
|
|
277
|
+
// First Esc: unfocus the text field
|
|
278
|
+
ps.textValues.set(ps.focusedFieldId, editor.getText().trim());
|
|
279
|
+
ps.focusedFieldId = null;
|
|
280
|
+
refresh();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (matchesKey(data, Key.enter)) {
|
|
284
|
+
ps.textValues.set(ps.focusedFieldId, editor.getText().trim());
|
|
285
|
+
ps.focusedFieldId = null;
|
|
286
|
+
advance();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
editor.handleInput(data);
|
|
290
|
+
refresh();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
// ── Esc with no text field focused: go back (or exit if on page 1) ──
|
|
294
|
+
if (matchesKey(data, Key.escape)) {
|
|
295
|
+
goBack();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
// ── Enter / → to advance ─────────────────────────────────────────
|
|
299
|
+
if (matchesKey(data, Key.enter) || matchesKey(data, Key.right)) {
|
|
300
|
+
// For single-select fields, commit cursor before advancing
|
|
301
|
+
const page = currentPage();
|
|
302
|
+
for (const field of page.fields) {
|
|
303
|
+
if (field.type === "select" && !field.allowMultiple) {
|
|
304
|
+
const ss = getSelectState(currentPageId(), field.id, field.options.length);
|
|
305
|
+
if (ss.committedIndex === null)
|
|
306
|
+
ss.committedIndex = ss.cursorIndex;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
advance();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// ── Select field interactions ────────────────────────────────────
|
|
313
|
+
const page = currentPage();
|
|
314
|
+
for (const field of page.fields) {
|
|
315
|
+
if (field.type !== "select")
|
|
316
|
+
continue;
|
|
317
|
+
const ss = getSelectState(currentPageId(), field.id, field.options.length);
|
|
318
|
+
const totalOpts = field.options.length;
|
|
319
|
+
if (matchesKey(data, Key.up)) {
|
|
320
|
+
ss.cursorIndex = (ss.cursorIndex - 1 + totalOpts) % totalOpts;
|
|
321
|
+
refresh();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (matchesKey(data, Key.down)) {
|
|
325
|
+
ss.cursorIndex = (ss.cursorIndex + 1) % totalOpts;
|
|
326
|
+
refresh();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (field.allowMultiple) {
|
|
330
|
+
if (matchesKey(data, Key.space)) {
|
|
331
|
+
if (ss.checkedIndices.has(ss.cursorIndex))
|
|
332
|
+
ss.checkedIndices.delete(ss.cursorIndex);
|
|
333
|
+
else
|
|
334
|
+
ss.checkedIndices.add(ss.cursorIndex);
|
|
335
|
+
refresh();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
// Numeric shortcut: press the number to select and immediately advance
|
|
341
|
+
if (data.length === 1 && data >= "1" && data <= "9") {
|
|
342
|
+
const idx = parseInt(data, 10) - 1;
|
|
343
|
+
if (idx < totalOpts) {
|
|
344
|
+
ss.cursorIndex = idx;
|
|
345
|
+
ss.committedIndex = idx;
|
|
346
|
+
advance();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Enter/Space commit cursor and advance (Enter handled above, Space here)
|
|
351
|
+
if (matchesKey(data, Key.space)) {
|
|
352
|
+
ss.committedIndex = ss.cursorIndex;
|
|
353
|
+
advance();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// Only handle the first select field for nav
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// ── Render ─────────────────────────────────────────────────────────────
|
|
362
|
+
function renderExitConfirm(width) {
|
|
363
|
+
const ui = makeUI(theme, width);
|
|
364
|
+
const lines = [];
|
|
365
|
+
const push = (...rows) => { for (const r of rows)
|
|
366
|
+
lines.push(...r); };
|
|
367
|
+
push(ui.bar(), ui.blank(), ui.header(" Exit wizard?"), ui.blank(), ui.subtitle(" Your progress will be lost."), ui.blank());
|
|
368
|
+
if (exitCursor === 0)
|
|
369
|
+
push(ui.actionSelected(1, "Go back", "Return to where you were."));
|
|
370
|
+
else
|
|
371
|
+
push(ui.actionUnselected(1, "Go back", "Return to where you were."));
|
|
372
|
+
push(ui.blank());
|
|
373
|
+
if (exitCursor === 1)
|
|
374
|
+
push(ui.actionSelected(2, "Exit", "Cancel and discard all answers."));
|
|
375
|
+
else
|
|
376
|
+
push(ui.actionUnselected(2, "Exit", "Cancel and discard all answers."));
|
|
377
|
+
push(ui.blank(), ui.hints(["↑/↓ to choose", "1/2 to quick-select", "enter to confirm"]), ui.bar());
|
|
378
|
+
return lines;
|
|
379
|
+
}
|
|
380
|
+
function renderSelectField(ui, field, lines) {
|
|
381
|
+
const push = (...rows) => { for (const r of rows)
|
|
382
|
+
lines.push(...r); };
|
|
383
|
+
const ss = getSelectState(currentPageId(), field.id, field.options.length);
|
|
384
|
+
const multi = !!field.allowMultiple;
|
|
385
|
+
push(ui.question(` ${field.question}`));
|
|
386
|
+
if (multi)
|
|
387
|
+
push(ui.meta(" (select all that apply — space to toggle, enter to confirm)"));
|
|
388
|
+
push(ui.blank());
|
|
389
|
+
for (let i = 0; i < field.options.length; i++) {
|
|
390
|
+
const opt = field.options[i];
|
|
391
|
+
const isCursor = i === ss.cursorIndex;
|
|
392
|
+
const isCommitted = i === ss.committedIndex;
|
|
393
|
+
if (multi) {
|
|
394
|
+
const isChecked = ss.checkedIndices.has(i);
|
|
395
|
+
if (isCursor)
|
|
396
|
+
push(ui.checkboxSelected(opt.label, opt.description, isChecked));
|
|
397
|
+
else
|
|
398
|
+
push(ui.checkboxUnselected(opt.label, opt.description, isChecked));
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
if (isCursor)
|
|
402
|
+
push(ui.optionSelected(i + 1, opt.label, opt.description, isCommitted));
|
|
403
|
+
else
|
|
404
|
+
push(ui.optionUnselected(i + 1, opt.label, opt.description, { isCommitted }));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function renderTextField(ui, field, ps, lines, width) {
|
|
409
|
+
const push = (...rows) => { for (const r of rows)
|
|
410
|
+
lines.push(...r); };
|
|
411
|
+
const isFocused = ps.focusedFieldId === field.id;
|
|
412
|
+
const value = isFocused ? getEditor(field.id).getText() : (ps.textValues.get(field.id) ?? "");
|
|
413
|
+
push(ui.question(` ${field.label}`), ui.blank());
|
|
414
|
+
if (isFocused) {
|
|
415
|
+
for (const line of getEditor(field.id).render(width - 2))
|
|
416
|
+
lines.push(truncateToWidth(` ${line}`, width));
|
|
417
|
+
}
|
|
418
|
+
else if (value) {
|
|
419
|
+
push(ui.answer(` ${value}`));
|
|
420
|
+
}
|
|
421
|
+
else if (field.placeholder) {
|
|
422
|
+
push(ui.meta(` ${field.placeholder}`));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function render(width) {
|
|
426
|
+
if (cachedLines)
|
|
427
|
+
return cachedLines;
|
|
428
|
+
if (showingExitConfirm) {
|
|
429
|
+
cachedLines = renderExitConfirm(width);
|
|
430
|
+
return cachedLines;
|
|
431
|
+
}
|
|
432
|
+
const ui = makeUI(theme, width);
|
|
433
|
+
const lines = [];
|
|
434
|
+
const push = (...rows) => { for (const r of rows)
|
|
435
|
+
lines.push(...r); };
|
|
436
|
+
push(ui.bar(), ui.header(` ${opts.title}`));
|
|
437
|
+
// ── Page indicator ────────────────────────────────────────────────
|
|
438
|
+
if (opts.pages.length > 1) {
|
|
439
|
+
push(ui.pageDots(opts.pages.length, pageStack.length - 1));
|
|
440
|
+
}
|
|
441
|
+
// ── Page content ──────────────────────────────────────────────────
|
|
442
|
+
const page = currentPage();
|
|
443
|
+
const ps = currentPageState();
|
|
444
|
+
if (page.subtitle) {
|
|
445
|
+
push(ui.blank(), ui.subtitle(` ${page.subtitle}`));
|
|
446
|
+
}
|
|
447
|
+
push(ui.blank());
|
|
448
|
+
for (const field of page.fields) {
|
|
449
|
+
if (field.type === "select")
|
|
450
|
+
renderSelectField(ui, field, lines);
|
|
451
|
+
else
|
|
452
|
+
renderTextField(ui, field, ps, lines, width);
|
|
453
|
+
push(ui.blank());
|
|
454
|
+
}
|
|
455
|
+
// ── Footer hints ──────────────────────────────────────────────────
|
|
456
|
+
const isFirst = pageStack.length === 1;
|
|
457
|
+
const ps2 = currentPageState();
|
|
458
|
+
const hints = [];
|
|
459
|
+
if (ps2.focusedFieldId) {
|
|
460
|
+
hints.push("enter to continue");
|
|
461
|
+
hints.push("esc to unfocus");
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
hints.push("↑/↓ to move");
|
|
465
|
+
hints.push("enter to select");
|
|
466
|
+
hints.push(!isFirst ? "esc to go back" : "esc to exit");
|
|
467
|
+
}
|
|
468
|
+
push(ui.hints(hints), ui.bar());
|
|
469
|
+
cachedLines = lines;
|
|
470
|
+
return lines;
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
render,
|
|
474
|
+
invalidate: () => { cachedLines = undefined; },
|
|
475
|
+
handleInput,
|
|
476
|
+
};
|
|
477
|
+
});
|
|
478
|
+
}
|
|
@@ -20,7 +20,6 @@ import { StringEnum } from "@gsd/pi-ai";
|
|
|
20
20
|
import { getMarkdownTheme } from "@gsd/pi-coding-agent";
|
|
21
21
|
import { Container, Markdown, Spacer, Text } from "@gsd/pi-tui";
|
|
22
22
|
import { Type } from "@sinclair/typebox";
|
|
23
|
-
import { formatTokenCount } from "../shared/mod.js";
|
|
24
23
|
import { discoverAgents } from "./agents.js";
|
|
25
24
|
import { createIsolation, mergeDeltaPatches, readIsolationMode, } from "./isolation.js";
|
|
26
25
|
import { registerWorker, updateWorker } from "./worker-registry.js";
|
|
@@ -59,22 +58,31 @@ async function stopLiveSubagents() {
|
|
|
59
58
|
}
|
|
60
59
|
}
|
|
61
60
|
}
|
|
61
|
+
function formatTokens(count) {
|
|
62
|
+
if (count < 1000)
|
|
63
|
+
return count.toString();
|
|
64
|
+
if (count < 10000)
|
|
65
|
+
return `${(count / 1000).toFixed(1)}k`;
|
|
66
|
+
if (count < 1000000)
|
|
67
|
+
return `${Math.round(count / 1000)}k`;
|
|
68
|
+
return `${(count / 1000000).toFixed(1)}M`;
|
|
69
|
+
}
|
|
62
70
|
function formatUsageStats(usage, model) {
|
|
63
71
|
const parts = [];
|
|
64
72
|
if (usage.turns)
|
|
65
73
|
parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
66
74
|
if (usage.input)
|
|
67
|
-
parts.push(`↑${
|
|
75
|
+
parts.push(`↑${formatTokens(usage.input)}`);
|
|
68
76
|
if (usage.output)
|
|
69
|
-
parts.push(`↓${
|
|
77
|
+
parts.push(`↓${formatTokens(usage.output)}`);
|
|
70
78
|
if (usage.cacheRead)
|
|
71
|
-
parts.push(`R${
|
|
79
|
+
parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
72
80
|
if (usage.cacheWrite)
|
|
73
|
-
parts.push(`W${
|
|
81
|
+
parts.push(`W${formatTokens(usage.cacheWrite)}`);
|
|
74
82
|
if (usage.cost)
|
|
75
83
|
parts.push(`$${(Number(usage.cost) || 0).toFixed(4)}`);
|
|
76
84
|
if (usage.contextTokens && usage.contextTokens > 0) {
|
|
77
|
-
parts.push(`ctx:${
|
|
85
|
+
parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
78
86
|
}
|
|
79
87
|
if (model)
|
|
80
88
|
parts.push(model);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gsd-pi",
|
|
3
|
-
"version": "2.35.0-dev.
|
|
3
|
+
"version": "2.35.0-dev.640d5c7",
|
|
4
4
|
"description": "GSD — Get Shit Done coding agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -50,7 +50,6 @@
|
|
|
50
50
|
"copy-themes": "node scripts/copy-themes.cjs",
|
|
51
51
|
"copy-export-html": "node scripts/copy-export-html.cjs",
|
|
52
52
|
"test:unit": "node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/*.test.ts src/resources/extensions/gsd/tests/*.test.mjs src/tests/*.test.ts",
|
|
53
|
-
"test:marketplace": "GSD_TEST_CLONE_MARKETPLACES=1 node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/claude-import-tui.test.ts src/resources/extensions/gsd/tests/plugin-importer-live.test.ts src/tests/marketplace-discovery.test.ts",
|
|
54
53
|
"test:coverage": "c8 --reporter=text --reporter=lcov --exclude='src/resources/extensions/gsd/tests/**' --exclude='src/tests/**' --exclude='scripts/**' --exclude='native/**' --exclude='node_modules/**' --check-coverage --statements=50 --lines=50 --branches=20 --functions=20 node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/*.test.ts src/resources/extensions/gsd/tests/*.test.mjs src/tests/*.test.ts",
|
|
55
54
|
"test:integration": "node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/*integration*.test.ts src/tests/integration/*.test.ts",
|
|
56
55
|
"test": "npm run test:unit && npm run test:integration",
|