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.
Files changed (88) hide show
  1. package/dist/cli.js +2 -7
  2. package/dist/resource-loader.d.ts +1 -1
  3. package/dist/resource-loader.js +1 -13
  4. package/dist/resources/extensions/async-jobs/await-tool.js +2 -0
  5. package/dist/resources/extensions/async-jobs/job-manager.js +6 -0
  6. package/dist/resources/extensions/bg-shell/output-formatter.js +19 -1
  7. package/dist/resources/extensions/bg-shell/process-manager.js +4 -0
  8. package/dist/resources/extensions/bg-shell/types.js +2 -0
  9. package/dist/resources/extensions/context7/index.js +0 -5
  10. package/dist/resources/extensions/get-secrets-from-user.js +30 -2
  11. package/dist/resources/extensions/google-search/index.js +0 -5
  12. package/dist/resources/extensions/gsd/auto-dispatch.js +1 -43
  13. package/dist/resources/extensions/gsd/auto-loop.js +1 -10
  14. package/dist/resources/extensions/gsd/auto-recovery.js +0 -35
  15. package/dist/resources/extensions/gsd/auto-start.js +2 -35
  16. package/dist/resources/extensions/gsd/auto.js +4 -59
  17. package/dist/resources/extensions/gsd/commands-handlers.js +2 -2
  18. package/dist/resources/extensions/gsd/doctor-environment.js +17 -26
  19. package/dist/resources/extensions/gsd/files.js +1 -9
  20. package/dist/resources/extensions/gsd/gitignore.js +7 -54
  21. package/dist/resources/extensions/gsd/guided-flow.js +1 -1
  22. package/dist/resources/extensions/gsd/health-widget.js +46 -97
  23. package/dist/resources/extensions/gsd/index.js +1 -10
  24. package/dist/resources/extensions/gsd/migrate-external.js +2 -55
  25. package/dist/resources/extensions/gsd/paths.js +7 -74
  26. package/dist/resources/extensions/gsd/post-unit-hooks.js +1 -4
  27. package/dist/resources/extensions/gsd/preferences-validation.js +1 -16
  28. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +0 -2
  29. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +0 -2
  30. package/dist/resources/extensions/gsd/session-lock.js +2 -26
  31. package/dist/resources/extensions/gsd/templates/plan.md +0 -8
  32. package/dist/resources/extensions/gsd/worktree-resolver.js +0 -12
  33. package/dist/resources/extensions/remote-questions/remote-command.js +22 -2
  34. package/dist/resources/extensions/shared/mod.js +1 -1
  35. package/dist/resources/extensions/shared/sanitize.js +0 -30
  36. package/dist/resources/extensions/shared/wizard-ui.js +478 -0
  37. package/dist/resources/extensions/subagent/index.js +14 -6
  38. package/package.json +1 -2
  39. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
  40. package/packages/pi-coding-agent/dist/core/resource-loader.js +2 -13
  41. package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
  42. package/packages/pi-coding-agent/src/core/resource-loader.ts +2 -13
  43. package/src/resources/extensions/async-jobs/await-tool.ts +2 -0
  44. package/src/resources/extensions/async-jobs/job-manager.ts +7 -0
  45. package/src/resources/extensions/bg-shell/output-formatter.ts +17 -0
  46. package/src/resources/extensions/bg-shell/process-manager.ts +4 -0
  47. package/src/resources/extensions/bg-shell/types.ts +12 -0
  48. package/src/resources/extensions/context7/index.ts +0 -7
  49. package/src/resources/extensions/get-secrets-from-user.ts +35 -2
  50. package/src/resources/extensions/google-search/index.ts +0 -7
  51. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -49
  52. package/src/resources/extensions/gsd/auto-loop.ts +1 -11
  53. package/src/resources/extensions/gsd/auto-recovery.ts +0 -39
  54. package/src/resources/extensions/gsd/auto-start.ts +2 -42
  55. package/src/resources/extensions/gsd/auto.ts +3 -61
  56. package/src/resources/extensions/gsd/commands-handlers.ts +2 -2
  57. package/src/resources/extensions/gsd/doctor-environment.ts +16 -26
  58. package/src/resources/extensions/gsd/files.ts +1 -10
  59. package/src/resources/extensions/gsd/gitignore.ts +7 -54
  60. package/src/resources/extensions/gsd/guided-flow.ts +1 -1
  61. package/src/resources/extensions/gsd/health-widget.ts +59 -103
  62. package/src/resources/extensions/gsd/index.ts +1 -10
  63. package/src/resources/extensions/gsd/migrate-external.ts +2 -47
  64. package/src/resources/extensions/gsd/paths.ts +7 -73
  65. package/src/resources/extensions/gsd/post-unit-hooks.ts +1 -5
  66. package/src/resources/extensions/gsd/preferences-validation.ts +1 -16
  67. package/src/resources/extensions/gsd/prompts/complete-milestone.md +0 -2
  68. package/src/resources/extensions/gsd/prompts/validate-milestone.md +0 -2
  69. package/src/resources/extensions/gsd/session-lock.ts +2 -29
  70. package/src/resources/extensions/gsd/templates/plan.md +0 -8
  71. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -12
  72. package/src/resources/extensions/gsd/tests/validate-directory.test.ts +0 -15
  73. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +0 -2
  74. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +0 -32
  75. package/src/resources/extensions/gsd/worktree-resolver.ts +0 -11
  76. package/src/resources/extensions/remote-questions/remote-command.ts +23 -2
  77. package/src/resources/extensions/shared/mod.ts +1 -1
  78. package/src/resources/extensions/shared/sanitize.ts +0 -36
  79. package/src/resources/extensions/shared/wizard-ui.ts +551 -0
  80. package/src/resources/extensions/subagent/index.ts +12 -6
  81. package/dist/resources/extensions/gsd/health-widget-core.js +0 -96
  82. package/dist/resources/extensions/gsd/roadmap-mutations.js +0 -55
  83. package/src/resources/extensions/gsd/health-widget-core.ts +0 -129
  84. package/src/resources/extensions/gsd/roadmap-mutations.ts +0 -66
  85. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +0 -214
  86. package/src/resources/extensions/gsd/tests/health-widget.test.ts +0 -158
  87. package/src/resources/extensions/gsd/tests/paths.test.ts +0 -113
  88. 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 { maskEditorLine, sanitizeError } from "../shared/mod.js";
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, maskEditorLine } from "./sanitize.js";
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(`↑${formatTokenCount(usage.input)}`);
75
+ parts.push(`↑${formatTokens(usage.input)}`);
68
76
  if (usage.output)
69
- parts.push(`↓${formatTokenCount(usage.output)}`);
77
+ parts.push(`↓${formatTokens(usage.output)}`);
70
78
  if (usage.cacheRead)
71
- parts.push(`R${formatTokenCount(usage.cacheRead)}`);
79
+ parts.push(`R${formatTokens(usage.cacheRead)}`);
72
80
  if (usage.cacheWrite)
73
- parts.push(`W${formatTokenCount(usage.cacheWrite)}`);
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:${formatTokenCount(usage.contextTokens)}`);
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.30eec3f",
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",