@rmrdeveloper/sideroom-pi 5.0.1 → 7.0.0

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 (42) hide show
  1. package/README.md +115 -81
  2. package/{src/assets → assets}/artifacts/GUIDELINES_TEMPLATE.md +40 -11
  3. package/extensions/ask/execute.ts +56 -0
  4. package/extensions/ask/index.ts +78 -0
  5. package/extensions/ask/model.ts +196 -0
  6. package/extensions/ask/ui.ts +445 -0
  7. package/extensions/guidelines/index.ts +13 -0
  8. package/extensions/guidelines/prompt.ts +26 -0
  9. package/extensions/modified-files/index.ts +45 -0
  10. package/extensions/modified-files/model.ts +74 -0
  11. package/extensions/modified-files/session.ts +200 -0
  12. package/extensions/modified-files/ui.ts +207 -0
  13. package/extensions/todo/execute.ts +69 -0
  14. package/extensions/todo/guards.ts +107 -0
  15. package/extensions/todo/index.ts +91 -0
  16. package/extensions/todo/model.ts +327 -0
  17. package/extensions/todo/session.ts +166 -0
  18. package/extensions/todo/ui.ts +41 -0
  19. package/package.json +28 -24
  20. package/skills/sideroom-guidelines/SKILL.md +77 -0
  21. package/skills/sideroom-guidelines/references/languages/go.md +27 -0
  22. package/skills/sideroom-guidelines/references/languages/java.md +24 -0
  23. package/skills/sideroom-guidelines/references/languages/php-laravel.md +26 -0
  24. package/skills/sideroom-guidelines/references/languages/python.md +21 -0
  25. package/skills/sideroom-guidelines/references/languages/rust.md +19 -0
  26. package/skills/sideroom-guidelines/references/languages/typescript.md +30 -0
  27. package/dist/pi-extension.js +0 -1175
  28. package/skills/sideroom-critic/SKILL.md +0 -32
  29. package/skills/sideroom-grilling/SKILL.md +0 -81
  30. package/skills/sideroom-spec/SKILL.md +0 -38
  31. package/skills/sideroom-transcribe-audio/SKILL.md +0 -38
  32. package/skills/sideroom-transcribe-audio/scripts/transcribe.py +0 -40
  33. package/src/assets/agents/sideroom-code-reviewer.md +0 -30
  34. package/src/assets/agents/sideroom-fixer.md +0 -37
  35. package/src/assets/agents/sideroom-implementer.md +0 -39
  36. package/src/assets/agents/sideroom-planner.md +0 -31
  37. package/src/assets/agents/sideroom-verifier.md +0 -32
  38. package/src/assets/artifacts/guidelines/java.md +0 -211
  39. package/src/assets/artifacts/guidelines/javascript.md +0 -235
  40. package/src/assets/artifacts/guidelines/php-laravel.md +0 -210
  41. package/src/assets/artifacts/guidelines/python.md +0 -227
  42. package/src/assets/artifacts/guidelines/typescript.md +0 -231
package/README.md CHANGED
@@ -5,26 +5,31 @@
5
5
  [![npm license](https://img.shields.io/npm/l/@rmrdeveloper/sideroom-pi?label=license)](https://www.npmjs.com/package/@rmrdeveloper/sideroom-pi)
6
6
 
7
7
  Sideroom Pi is a global [Pi package](https://pi.dev/docs/latest/packages). It
8
- registers the `/sideroom` command inside Pi and runs planner, implementer,
9
- reviewer, verifier, and fixer as isolated Pi SDK sessions.
8
+ registers two parent-agent tools: `sideroom_ask`, a TUI questionnaire for
9
+ clarifying decisions, and `sideroom_todo`, a live work board shown above the
10
+ editor. It also shows a session-scoped list of files successfully edited by Pi,
11
+ and steers write/edit toward packaged coding guidelines. It writes no `.pi`
12
+ configuration, task graph, or other Sideroom files into the repository you are
13
+ working in.
10
14
 
11
- It creates no `.pi` configuration, task graph, harness state, or other files
12
- in the repository being changed.
15
+ ## Quick path
13
16
 
14
- ## Install
17
+ 1. Install globally from npm. Pi writes this installation to your user
18
+ settings, so do not use `-l`.
15
19
 
16
- Install Sideroom globally from npm. Pi writes this installation to your user
17
- settings, so do not use `-l`.
20
+ ```bash
21
+ pi install npm:@rmrdeveloper/sideroom-pi
22
+ ```
18
23
 
19
- ```bash
20
- pi install npm:@rmrdeveloper/sideroom-pi
21
- ```
24
+ 2. Start Pi in the repository you want to work in.
25
+ 3. Ask the parent agent a question that needs a decision or a visible work
26
+ board. It can call `sideroom_ask` or `sideroom_todo` in the TUI.
22
27
 
23
- For a reproducible environment, install a specific release instead. A pinned
24
- version does not move when you run Pi's package update command.
28
+ For a reproducible environment, pin a release. A pinned version does not move
29
+ when you run Pi's package update command.
25
30
 
26
31
  ```bash
27
- pi install npm:@rmrdeveloper/sideroom-pi@5.0.0
32
+ pi install npm:@rmrdeveloper/sideroom-pi@7.0.0
28
33
  ```
29
34
 
30
35
  To update an unpinned installation:
@@ -33,74 +38,91 @@ To update an unpinned installation:
33
38
  pi update --extension npm:@rmrdeveloper/sideroom-pi
34
39
  ```
35
40
 
36
- Start Pi from the repository you want to work in, then invoke the extension:
37
-
38
- ```text
39
- /sideroom "Add a health endpoint"
40
- /sideroom --language php-laravel "Add invoice export"
41
- /sideroom --read-only "Review the retry behavior"
42
- ```
43
-
44
- Pi extension commands are necessarily slash commands, so `/sideroom` is the
45
- single command; there is no subcommand or separate binary to run. When the
46
- request is omitted, Pi asks for it using its own UI.
47
-
48
- Supported guideline variants are `typescript` (default), `javascript`,
49
- `php-laravel`, `python`, and `java`. `--read-only` removes write-capable tools
50
- from every role. `--max-fix-passes <number>` changes the default limit of two
51
- repair passes.
52
-
53
- ## Provenance and isolation
54
-
55
- `/sideroom` is dispatched to the extension before Pi expands skills or sends
56
- the command to the interactive agent. The extension then creates direct,
57
- in-memory SDK sessions for the five Sideroom roles. Those child sessions load
58
- only this package's skills and explicit policy; global Pi skills, extensions,
59
- prompt templates, and global context files are excluded.
60
-
61
- At the end of every run, Pi displays a `Sideroom completed` or `Sideroom
62
- failed` message with its provenance and the completed roles. It also records
63
- the same trace in Pi's session history as `sideroom:run`; this is Pi session
64
- metadata, never a file in the target repository.
65
-
66
- The packaged `sideroom-grilling` skill runs as the design-decision gate. Its
67
- questions use Pi's native decision UI. During a run, a persistent Pi widget
68
- shows the active phase, the waiting-for-answer state, and the direct-SDK
69
- provenance. Selecting a recommendation accepts it; selecting the alternate
70
- option opens a custom-answer field. The widget is removed automatically when a
71
- run completes or fails; the final Pi session message remains as the durable
72
- record. It is not a request to any global skill with the same name.
73
-
74
- ## Content and guidelines
75
-
76
- The five role prompts are in `src/assets/agents/`. The shared policy is
77
- `src/assets/artifacts/GUIDELINES_TEMPLATE.md`; the language layers live in
78
- `src/assets/artifacts/guidelines/`. Implementer and fixer receive those
79
- policies before their write gate and must read them before every code-writing
80
- tool call.
81
-
82
- Reusable Pi skills ship in `skills/`: `sideroom-grilling`, `sideroom-critic`,
83
- `sideroom-spec`, and `sideroom-transcribe-audio`. Pi loads them directly through
84
- the package manifest.
85
- The transcription skill needs `uv`, Python, and `ffmpeg` only when explicitly
86
- used.
41
+ ## Details
42
+
43
+ | Topic | Decision |
44
+ | --- | --- |
45
+ | Surface | Two LLM-callable tools: `sideroom_ask` and `sideroom_todo`. No `/sideroom` command. |
46
+ | Questionnaire | One `sideroom_ask` call is one 1–N question batch; every question has `recommendationIndex`. |
47
+ | Always-on rows | The questionnaire UI adds Out of scope and a custom answer. Do not send those options. |
48
+ | Work board | `sideroom_todo propose` replaces a visible board; `update` patches items by id. |
49
+ | Headless | `sideroom_todo update` works in print, JSON, and RPC. `propose` and `sideroom_ask` return `Error: UI not available` there. |
50
+ | Edited files | Successful `write` and `edit` results appear below an active work board; `F8` opens the full list. |
51
+ | Session state | The work board and edited-file history live in the Pi session branch, not in the target repository. |
52
+ | Coding guidelines | A short system-prompt reminder points at the `sideroom-guidelines` skill. Language deltas load on demand. |
53
+ | Seed artifact | `assets/artifacts/GUIDELINES_TEMPLATE.md` is the canonical seed; it is not dumped into the prompt. |
54
+
55
+ ## Tool
56
+
57
+ `sideroom_ask` follows Pi's questionnaire UI: a simple list for one question,
58
+ tabs plus Submit for several. Write prompts, tab labels, and option copy in
59
+ the language the user is speaking; ids, option values, and TUI chrome stay
60
+ English. Each question needs:
61
+
62
+ - `id`
63
+ - `prompt`
64
+ - at least two `{ value, label, description? }` options
65
+ - `recommendationIndex` pointing at the recommended option
66
+ - optional `label` for the tab bar (defaults to `Q1`, `Q2`, …)
67
+
68
+ The recommended option is marked in the list. Escape cancels the batch.
69
+
70
+ ## Work board
71
+
72
+ `sideroom_todo` keeps an ordered, display-only board above the editor. The
73
+ agent maintains it and the user steers through chat. It never creates a
74
+ repository task file or a `/todos` command.
75
+
76
+ - `propose` replaces the full board and is available only in the interactive
77
+ TUI.
78
+ - `update` applies one or more `{ id, status?, content? }` patches and works in
79
+ every Pi mode.
80
+ - Items have stable string ids, short content, and one of `pending`,
81
+ `in_progress`, `completed`, or `cancelled`.
82
+ - While items are pending, exactly one item must be `in_progress`. Complete the
83
+ current item and start the next one in the same update.
84
+ - Snapshots are stored in the active session branch. The board is rebuilt after
85
+ session navigation and compaction.
86
+
87
+ ## Edited files
88
+
89
+ The package automatically records successful Pi `write` and `edit` calls for
90
+ the active session. It does not infer filesystem changes from shell commands,
91
+ Git, people, or subagents. The compact list is shown below the work board when
92
+ one is active and contains at most five recent paths.
93
+
94
+ - `F8` opens or closes the extended, scrollable list; press `R` there
95
+ to clear the current session's history.
96
+ - Every path is an OSC 8 `file://` link. Your terminal determines whether that
97
+ is a click, Ctrl+click, or another modifier-assisted action.
98
+ - Paths outside the active project are labelled `external`.
99
+ - A new session starts with a new list; the current session's history survives
100
+ reload, tree navigation, and compaction.
101
+
102
+ ## Coding guidelines
103
+
104
+ Before `write` or `edit`, the parent agent is reminded to read the packaged
105
+ `sideroom-guidelines` skill. That skill holds the shared Do/Don't table. For
106
+ `.java`, `.php`, `.ts`/`.tsx`, `.py`, `.go`, and `.rs`, the agent also reads one
107
+ matching file under `skills/sideroom-guidelines/references/languages/`. Other
108
+ languages follow the shared table only.
87
109
 
88
110
  ## Development
89
111
 
90
112
  ```bash
91
113
  npm install
92
114
  npm run check
93
- npm run build
94
115
  ```
95
116
 
96
- Biome is the sole formatter and linter. Runtime code uses Pi's SDK only; Pi
97
- provides `@earendil-works/pi-coding-agent` as a peer dependency.
117
+ Biome is the sole formatter and linter. Pi loads TypeScript from `extensions/`
118
+ directly; do not bundle. Pi provides `@earendil-works/pi-coding-agent`,
119
+ `@earendil-works/pi-tui`, and `typebox` as peer dependencies.
98
120
 
99
121
  ## Releases and commits
100
122
 
101
123
  Commit messages must follow the Conventional Commits format. The existing
102
124
  Husky `commit-msg` hook validates each local commit with Commitlint, for
103
- example `feat: add a pipeline summary` or `fix: validate malformed findings`.
125
+ example `feat: mark the recommended sideroom_ask option`.
104
126
 
105
127
  Every user-visible package change needs a Changeset. Create one with:
106
128
 
@@ -108,21 +130,33 @@ Every user-visible package change needs a Changeset. Create one with:
108
130
  npm run changeset
109
131
  ```
110
132
 
111
- Review pending release work with `npm run changeset:status`. On the release
112
- branch, run `npm run version-packages`, review and commit the updated
113
- `package.json`, `package-lock.json`, and `CHANGELOG.md`, then publish
114
- deliberately with `npm run release`. The repository does not publish
115
- automatically from CI.
133
+ Review pending release work with `npm run changeset:status`. When Changesets
134
+ are merged to `main`, the `Release` workflow creates or updates a reviewable
135
+ Changesets version PR. That PR contains the generated `package.json`,
136
+ `package-lock.json`, and `CHANGELOG.md` updates. Merge the version PR only after
137
+ review; its merge runs `npm ci` and `npm run check`, then uses
138
+ npm 11.5.1 and npm Trusted Publishing (GitHub OIDC) to publish. Changesets then
139
+ pushes the release Git tag and creates the GitHub Release automatically.
140
+
141
+ Never run `npm publish`, create release tags, or create GitHub Releases locally.
142
+ Publishing is allowed only through the merged Changesets version PR and the
143
+ `Release` workflow. The npm package's Trusted Publisher must be configured for
144
+ this GitHub repository, the workflow filename `release.yml` (not its path),
145
+ and the `npm` environment; configure the matching GitHub `npm` environment
146
+ before the first release.
116
147
 
117
148
  ## Layout
118
149
 
119
150
  ```text
120
- src/
121
- pi-extension.ts Pi extension entry point for /sideroom
122
- pi-command.ts slash-command parsing
123
- app.ts Pi pipeline assembly
124
- core/ orchestration, roles, contracts, and content catalog
125
- runtimes/pi.ts direct Pi SDK adapter for isolated child sessions
126
- assets/ role prompts and coding guidelines
127
- skills/ package-provided Pi skills
151
+ extensions/
152
+ ask/ sideroom_ask (index.ts, execute.ts, model.ts, ui.ts)
153
+ todo/ sideroom_todo (index.ts, execute.ts, session.ts,
154
+ guards.ts, model.ts, ui.ts)
155
+ modified-files/ session-backed edited-file tracker and view
156
+ guidelines/ short write/edit reminder
157
+ skills/sideroom-guidelines/ on-demand checklist and language deltas
158
+ assets/artifacts/ GUIDELINES_TEMPLATE.md seed
128
159
  ```
160
+
161
+ Add another tool as `extensions/<name>/index.ts`. Pi discovers
162
+ `extensions/*/index.ts`; helpers in that folder are not loaded as extensions.
@@ -8,6 +8,7 @@ Each row is expanded, with examples, in the matching section below.
8
8
 
9
9
  | Do | Don't |
10
10
  | -------------------------------------------------- | ------------------------------------------------------------------------------------------- |
11
+ | Use braces around every `if` body | Write an unbraced, single-statement conditional body |
11
12
  | Early return on bad input | Pyramid `if/else` nesting |
12
13
  | Explicit error, fail now | Multiple fallbacks that hide the real failure |
13
14
  | One responsibility per unit | Validate + transform + persist + notify in one place |
@@ -55,13 +56,26 @@ function getDiscount(user) {
55
56
 
56
57
  // Good — guard clauses, happy path at the end, shallow indent
57
58
  function getDiscount(user) {
58
- if (!user) return 0;
59
- if (!user.isActive) return 0;
60
- if (!user.hasSubscription) return 0;
59
+ if (!user) {
60
+ return 0;
61
+ }
62
+ if (!user.isActive) {
63
+ return 0;
64
+ }
65
+ if (!user.hasSubscription) {
66
+ return 0;
67
+ }
61
68
  return 0.2;
62
69
  }
63
70
  ```
64
71
 
72
+ ### Braced conditionals
73
+
74
+ Every `if` and `else` body must use braces in languages that support them,
75
+ including a one-line guard clause. Never rely on indentation alone to bind a
76
+ statement to a conditional. In Python, which has no braces, use a complete
77
+ indented suite and never a one-line conditional body.
78
+
65
79
  ### Fail fast
66
80
 
67
81
  Invalid input, impossible state, or a broken dependency should fail immediately with a clear error. Prefer that over chains of fallbacks, silent defaults, or "keep going somehow."
@@ -78,7 +92,9 @@ function getShippingCost(order) {
78
92
  // Good — fails immediately with a clear error
79
93
  function getShippingCost(order) {
80
94
  const zone = ZONES[order.zoneId];
81
- if (!zone) throw new Error(`Unknown zoneId: ${order.zoneId}`);
95
+ if (!zone) {
96
+ throw new Error(`Unknown zoneId: ${order.zoneId}`);
97
+ }
82
98
  return zone.baseCost;
83
99
  }
84
100
  ```
@@ -90,7 +106,9 @@ A function, class, or module has one reason to change. If it does two jobs, spli
90
106
  ```js
91
107
  // Bad — validates, transforms, persists, and notifies all in one place
92
108
  async function saveUser(data) {
93
- if (!data.email) throw new Error('email required');
109
+ if (!data.email) {
110
+ throw new Error('email required');
111
+ }
94
112
  const normalized = { ...data, email: data.email.trim().toLowerCase() };
95
113
  await db.users.insert(normalized);
96
114
  await mailer.send(normalized.email, 'welcome');
@@ -98,7 +116,9 @@ async function saveUser(data) {
98
116
 
99
117
  // Good — each unit has a single reason to change
100
118
  function validateUser(data) {
101
- if (!data.email) throw new Error('email required');
119
+ if (!data.email) {
120
+ throw new Error('email required');
121
+ }
102
122
  }
103
123
  function normalizeUser(data) {
104
124
  return { ...data, email: data.email.trim().toLowerCase() };
@@ -284,14 +304,22 @@ Pick one convention and apply it consistently: e.g. `undefined` for "not yet set
284
304
  ```js
285
305
  // Bad — null is overloaded to mean three different things
286
306
  function getDiscount(user) {
287
- if (!user) return null; // no user
288
- if (!user.plan) return null; // no plan
289
- if (user.plan.discount === 0) return null; // legitimately zero discount
307
+ if (!user) {
308
+ return null; // no user
309
+ }
310
+ if (!user.plan) {
311
+ return null; // no plan
312
+ }
313
+ if (user.plan.discount === 0) {
314
+ return null; // legitimately zero discount
315
+ }
290
316
  }
291
317
 
292
318
  // Good — each case is explicit, zero is a real value
293
319
  function getDiscount(user) {
294
- if (!user || !user.plan) return 0;
320
+ if (!user || !user.plan) {
321
+ return 0;
322
+ }
295
323
  return user.plan.discount;
296
324
  }
297
325
  ```
@@ -439,7 +467,8 @@ class UpdateProfileRequest extends FormRequest {
439
467
  }
440
468
  class ProfileService {
441
469
  public function update(array $data) {
442
- if (empty($data['email'])) { // redundant re-validation
470
+ // Redundant re-validation.
471
+ if (empty($data['email'])) {
443
472
  throw new \InvalidArgumentException('email required');
444
473
  }
445
474
  $this->user->update($data);
@@ -0,0 +1,56 @@
1
+ import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
2
+ import {
3
+ type AskParams,
4
+ type AskQuestion,
5
+ type AskResult,
6
+ formatAnswerLines,
7
+ parseAskParams,
8
+ UI_UNAVAILABLE,
9
+ } from './model.ts';
10
+ import { runAskUi } from './ui.ts';
11
+
12
+ interface AskToolResult {
13
+ readonly content: [{ readonly type: 'text'; readonly text: string }];
14
+ readonly details: AskResult;
15
+ }
16
+
17
+ export async function executeAsk(
18
+ params: AskParams,
19
+ ctx: Pick<ExtensionContext, 'mode' | 'ui'>,
20
+ ): Promise<AskToolResult> {
21
+ const parsed = parseAskParams(params);
22
+ if (!parsed.ok) {
23
+ return cancelledResult(parsed.message);
24
+ }
25
+ if (ctx.mode !== 'tui') {
26
+ return cancelledResult(UI_UNAVAILABLE, parsed.questions);
27
+ }
28
+
29
+ const result = await runAskUi(ctx.ui, parsed.questions);
30
+ if (result.cancelled) {
31
+ return {
32
+ content: [{ type: 'text', text: 'User cancelled the questionnaire' }],
33
+ details: result,
34
+ };
35
+ }
36
+
37
+ return {
38
+ content: [
39
+ {
40
+ type: 'text',
41
+ text: formatAnswerLines(parsed.questions, result.answers).join('\n'),
42
+ },
43
+ ],
44
+ details: result,
45
+ };
46
+ }
47
+
48
+ function cancelledResult(
49
+ message: string,
50
+ questions: readonly AskQuestion[] = [],
51
+ ): AskToolResult {
52
+ return {
53
+ content: [{ type: 'text', text: message }],
54
+ details: { questions, answers: [], cancelled: true },
55
+ };
56
+ }
@@ -0,0 +1,78 @@
1
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
+ import { Text } from '@earendil-works/pi-tui';
3
+ import { executeAsk } from './execute.ts';
4
+ import { AskParamsSchema, type AskResult, TOOL_NAME } from './model.ts';
5
+
6
+ export const ASK_DESCRIPTION =
7
+ 'Ask the user one or more questions in the language they are speaking. Use for clarifying requirements, getting preferences, or confirming decisions. Each question must include a recommended option. For a single question, shows a simple option list. For multiple questions, shows a tab-based interface.';
8
+
9
+ export const ASK_PROMPT_SNIPPET =
10
+ 'Ask the user one or more questions with a recommended option, in their language.';
11
+
12
+ export const ASK_PROMPT_GUIDELINES = [
13
+ 'Use sideroom_ask to clarify requirements, preferences, or decisions with the user.',
14
+ 'Each sideroom_ask question needs an id, a prompt, at least two options, and a recommendationIndex pointing at the recommended option.',
15
+ 'Write every sideroom_ask prompt, tab label, and option label or description in the language the user is speaking. Keep ids, option values, and tool code in English.',
16
+ 'sideroom_ask always adds Out of scope and a custom answer; do not include those options yourself.',
17
+ 'One sideroom_ask call is one batch; call it again if another round of questions is needed.',
18
+ 'sideroom_ask only works in the interactive TUI; it returns an error in print, JSON, or RPC modes.',
19
+ ];
20
+
21
+ // Pi loads extensions/*/index.ts through export default.
22
+ export default function registerAsk(pi: ExtensionAPI): void {
23
+ pi.registerTool({
24
+ name: TOOL_NAME,
25
+ label: 'Sideroom Ask',
26
+ description: ASK_DESCRIPTION,
27
+ promptSnippet: ASK_PROMPT_SNIPPET,
28
+ promptGuidelines: ASK_PROMPT_GUIDELINES,
29
+ parameters: AskParamsSchema,
30
+ executionMode: 'sequential',
31
+ execute: async (_toolCallId, params, _signal, _onUpdate, ctx) =>
32
+ executeAsk(params, ctx),
33
+ renderCall(args, theme) {
34
+ const questions = args.questions;
35
+ const count = questions.length;
36
+ const labels = questions
37
+ .map(
38
+ (question, index) =>
39
+ question.label || question.id || `Q${String(index + 1)}`,
40
+ )
41
+ .join(', ');
42
+ let text = theme.fg('toolTitle', theme.bold(`${TOOL_NAME} `));
43
+ text += theme.fg(
44
+ 'muted',
45
+ `${String(count)} question${count !== 1 ? 's' : ''}`,
46
+ );
47
+ if (labels.length > 0) {
48
+ text += theme.fg('dim', ` (${labels})`);
49
+ }
50
+ return new Text(text, 0, 0);
51
+ },
52
+ renderResult(result, _options, theme) {
53
+ const details = result.details as AskResult | undefined;
54
+ if (details === undefined) {
55
+ const text = result.content[0];
56
+ return new Text(text?.type === 'text' ? text.text : '', 0, 0);
57
+ }
58
+ if (details.cancelled) {
59
+ return new Text(theme.fg('warning', 'Cancelled'), 0, 0);
60
+ }
61
+ const lines = details.answers.map((answer) => {
62
+ const mark = theme.fg('success', '✓ ');
63
+ const id = theme.fg('accent', answer.id);
64
+ if (answer.outOfScope) {
65
+ return `${mark}${id}: ${theme.fg('muted', '(out of scope)')}`;
66
+ }
67
+ if (answer.wasCustom) {
68
+ return `${mark}${id}: ${theme.fg('muted', '(wrote) ')}${answer.label}`;
69
+ }
70
+ if (answer.index !== undefined) {
71
+ return `${mark}${id}: ${String(answer.index)}. ${answer.label}`;
72
+ }
73
+ return `${mark}${id}: ${answer.label}`;
74
+ });
75
+ return new Text(lines.join('\n'), 0, 0);
76
+ },
77
+ });
78
+ }