@mariozechner/pi-coding-agent 0.49.0 → 0.49.2

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 (81) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +14 -2
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +6 -0
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/export-html/template.css +19 -0
  7. package/dist/core/export-html/template.js +70 -5
  8. package/dist/core/extensions/index.d.ts +1 -1
  9. package/dist/core/extensions/index.d.ts.map +1 -1
  10. package/dist/core/extensions/index.js.map +1 -1
  11. package/dist/core/extensions/runner.d.ts +2 -2
  12. package/dist/core/extensions/runner.d.ts.map +1 -1
  13. package/dist/core/extensions/runner.js +49 -24
  14. package/dist/core/extensions/runner.js.map +1 -1
  15. package/dist/core/extensions/types.d.ts +10 -3
  16. package/dist/core/extensions/types.d.ts.map +1 -1
  17. package/dist/core/extensions/types.js.map +1 -1
  18. package/dist/core/model-registry.d.ts +2 -0
  19. package/dist/core/model-registry.d.ts.map +1 -1
  20. package/dist/core/model-registry.js +36 -5
  21. package/dist/core/model-registry.js.map +1 -1
  22. package/dist/core/sdk.d.ts.map +1 -1
  23. package/dist/core/sdk.js +9 -1
  24. package/dist/core/sdk.js.map +1 -1
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/main.d.ts.map +1 -1
  29. package/dist/main.js +2 -1
  30. package/dist/main.js.map +1 -1
  31. package/dist/modes/interactive/components/extension-input.d.ts +5 -2
  32. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  33. package/dist/modes/interactive/components/extension-input.js +9 -0
  34. package/dist/modes/interactive/components/extension-input.js.map +1 -1
  35. package/dist/modes/interactive/components/login-dialog.d.ts +5 -2
  36. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  37. package/dist/modes/interactive/components/login-dialog.js +9 -0
  38. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  39. package/dist/modes/interactive/components/model-selector.d.ts +14 -2
  40. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  41. package/dist/modes/interactive/components/model-selector.js +94 -39
  42. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  43. package/dist/modes/interactive/components/scoped-models-selector.d.ts +5 -2
  44. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  45. package/dist/modes/interactive/components/scoped-models-selector.js +9 -0
  46. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  47. package/dist/modes/interactive/components/session-selector.d.ts +23 -5
  48. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  49. package/dist/modes/interactive/components/session-selector.js +327 -55
  50. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  51. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  52. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  53. package/dist/modes/interactive/components/settings-selector.js +10 -0
  54. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  55. package/dist/modes/interactive/components/tree-selector.d.ts +5 -2
  56. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  57. package/dist/modes/interactive/components/tree-selector.js +23 -0
  58. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  59. package/dist/modes/interactive/interactive-mode.d.ts +5 -2
  60. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  61. package/dist/modes/interactive/interactive-mode.js +54 -29
  62. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  63. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  64. package/dist/modes/rpc/rpc-mode.js +2 -1
  65. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  66. package/dist/modes/rpc/rpc-types.d.ts +1 -0
  67. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  68. package/dist/modes/rpc/rpc-types.js.map +1 -1
  69. package/dist/utils/photon.d.ts +3 -2
  70. package/dist/utils/photon.d.ts.map +1 -1
  71. package/dist/utils/photon.js +85 -3
  72. package/dist/utils/photon.js.map +1 -1
  73. package/docs/extensions.md +5 -3
  74. package/docs/session.md +6 -0
  75. package/docs/tui.md +36 -3
  76. package/examples/extensions/README.md +1 -0
  77. package/examples/extensions/question.ts +9 -22
  78. package/examples/extensions/widget-placement.ts +17 -0
  79. package/examples/extensions/with-deps/package-lock.json +2 -2
  80. package/examples/extensions/with-deps/package.json +1 -1
  81. package/package.json +5 -5
@@ -8,12 +8,89 @@
8
8
  * The challenge: photon-node's CJS entry uses fs.readFileSync(__dirname + '/photon_rs_bg.wasm')
9
9
  * which bakes the build machine's absolute path into Bun compiled binaries.
10
10
  *
11
- * Solution: Lazy-load photon via dynamic import and gracefully handle failures.
12
- * Image processing functions have fallbacks that return original images when photon isn't available.
11
+ * Solution:
12
+ * 1. Patch fs.readFileSync to redirect missing photon_rs_bg.wasm reads
13
+ * 2. Copy photon_rs_bg.wasm next to the executable in build:binary
13
14
  */
15
+ import { createRequire } from "module";
16
+ import * as path from "path";
17
+ import { fileURLToPath } from "url";
18
+ const require = createRequire(import.meta.url);
19
+ const fs = require("fs");
20
+ const WASM_FILENAME = "photon_rs_bg.wasm";
14
21
  // Lazy-loaded photon module
15
22
  let photonModule = null;
16
23
  let loadPromise = null;
24
+ function pathOrNull(file) {
25
+ if (typeof file === "string") {
26
+ return file;
27
+ }
28
+ if (file instanceof URL) {
29
+ return fileURLToPath(file);
30
+ }
31
+ return null;
32
+ }
33
+ function getFallbackWasmPaths() {
34
+ const execDir = path.dirname(process.execPath);
35
+ return [
36
+ path.join(execDir, WASM_FILENAME),
37
+ path.join(execDir, "photon", WASM_FILENAME),
38
+ path.join(process.cwd(), WASM_FILENAME),
39
+ ];
40
+ }
41
+ function patchPhotonWasmRead() {
42
+ const originalReadFileSync = fs.readFileSync.bind(fs);
43
+ const fallbackPaths = getFallbackWasmPaths();
44
+ const mutableFs = fs;
45
+ const patchedReadFileSync = ((...args) => {
46
+ const [file, options] = args;
47
+ const resolvedPath = pathOrNull(file);
48
+ if (resolvedPath?.endsWith(WASM_FILENAME)) {
49
+ try {
50
+ return originalReadFileSync(...args);
51
+ }
52
+ catch (error) {
53
+ const err = error;
54
+ if (err?.code && err.code !== "ENOENT") {
55
+ throw error;
56
+ }
57
+ for (const fallbackPath of fallbackPaths) {
58
+ if (!fs.existsSync(fallbackPath)) {
59
+ continue;
60
+ }
61
+ if (options === undefined) {
62
+ return originalReadFileSync(fallbackPath);
63
+ }
64
+ return originalReadFileSync(fallbackPath, options);
65
+ }
66
+ throw error;
67
+ }
68
+ }
69
+ return originalReadFileSync(...args);
70
+ });
71
+ try {
72
+ mutableFs.readFileSync = patchedReadFileSync;
73
+ }
74
+ catch {
75
+ Object.defineProperty(fs, "readFileSync", {
76
+ value: patchedReadFileSync,
77
+ writable: true,
78
+ configurable: true,
79
+ });
80
+ }
81
+ return () => {
82
+ try {
83
+ mutableFs.readFileSync = originalReadFileSync;
84
+ }
85
+ catch {
86
+ Object.defineProperty(fs, "readFileSync", {
87
+ value: originalReadFileSync,
88
+ writable: true,
89
+ configurable: true,
90
+ });
91
+ }
92
+ };
93
+ }
17
94
  /**
18
95
  * Load the photon module asynchronously.
19
96
  * Returns cached module on subsequent calls.
@@ -26,13 +103,18 @@ export async function loadPhoton() {
26
103
  return loadPromise;
27
104
  }
28
105
  loadPromise = (async () => {
106
+ const restoreReadFileSync = patchPhotonWasmRead();
29
107
  try {
30
108
  photonModule = await import("@silvia-odwyer/photon-node");
109
+ return photonModule;
31
110
  }
32
111
  catch {
33
112
  photonModule = null;
113
+ return photonModule;
114
+ }
115
+ finally {
116
+ restoreReadFileSync();
34
117
  }
35
- return photonModule;
36
118
  })();
37
119
  return loadPromise;
38
120
  }
@@ -1 +1 @@
1
- {"version":3,"file":"photon.js","sourceRoot":"","sources":["../../src/utils/photon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,4BAA4B;AAC5B,IAAI,YAAY,GAAuD,IAAI,CAAC;AAC5E,IAAI,WAAW,GAAuE,IAAI,CAAC;AAE3F;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,GAAgE;IAC/F,IAAI,YAAY,EAAE,CAAC;QAClB,OAAO,YAAY,CAAC;IACrB,CAAC;IAED,IAAI,WAAW,EAAE,CAAC;QACjB,OAAO,WAAW,CAAC;IACpB,CAAC;IAED,WAAW,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;QAC1B,IAAI,CAAC;YACJ,YAAY,GAAG,MAAM,MAAM,CAAC,4BAA4B,CAAC,CAAC;QAC3D,CAAC;QAAC,MAAM,CAAC;YACR,YAAY,GAAG,IAAI,CAAC;QACrB,CAAC;QACD,OAAO,YAAY,CAAC;IAAA,CACpB,CAAC,EAAE,CAAC;IAEL,OAAO,WAAW,CAAC;AAAA,CACnB","sourcesContent":["/**\n * Photon image processing wrapper.\n *\n * This module provides a unified interface to @silvia-odwyer/photon-node that works in:\n * 1. Node.js (development, npm run build)\n * 2. Bun compiled binaries (standalone distribution)\n *\n * The challenge: photon-node's CJS entry uses fs.readFileSync(__dirname + '/photon_rs_bg.wasm')\n * which bakes the build machine's absolute path into Bun compiled binaries.\n *\n * Solution: Lazy-load photon via dynamic import and gracefully handle failures.\n * Image processing functions have fallbacks that return original images when photon isn't available.\n */\n\n// Re-export types from the main package\nexport type { PhotonImage as PhotonImageType } from \"@silvia-odwyer/photon-node\";\n\n// Lazy-loaded photon module\nlet photonModule: typeof import(\"@silvia-odwyer/photon-node\") | null = null;\nlet loadPromise: Promise<typeof import(\"@silvia-odwyer/photon-node\") | null> | null = null;\n\n/**\n * Load the photon module asynchronously.\n * Returns cached module on subsequent calls.\n */\nexport async function loadPhoton(): Promise<typeof import(\"@silvia-odwyer/photon-node\") | null> {\n\tif (photonModule) {\n\t\treturn photonModule;\n\t}\n\n\tif (loadPromise) {\n\t\treturn loadPromise;\n\t}\n\n\tloadPromise = (async () => {\n\t\ttry {\n\t\t\tphotonModule = await import(\"@silvia-odwyer/photon-node\");\n\t\t} catch {\n\t\t\tphotonModule = null;\n\t\t}\n\t\treturn photonModule;\n\t})();\n\n\treturn loadPromise;\n}\n"]}
1
+ {"version":3,"file":"photon.js","sourceRoot":"","sources":["../../src/utils/photon.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Photon image processing wrapper.\n *\n * This module provides a unified interface to @silvia-odwyer/photon-node that works in:\n * 1. Node.js (development, npm run build)\n * 2. Bun compiled binaries (standalone distribution)\n *\n * The challenge: photon-node's CJS entry uses fs.readFileSync(__dirname + '/photon_rs_bg.wasm')\n * which bakes the build machine's absolute path into Bun compiled binaries.\n *\n * Solution:\n * 1. Patch fs.readFileSync to redirect missing photon_rs_bg.wasm reads\n * 2. Copy photon_rs_bg.wasm next to the executable in build:binary\n */\n\nimport type { PathOrFileDescriptor } from \"fs\";\nimport { createRequire } from \"module\";\nimport * as path from \"path\";\nimport { fileURLToPath } from \"url\";\n\nconst require = createRequire(import.meta.url);\nconst fs = require(\"fs\") as typeof import(\"fs\");\n\n// Re-export types from the main package\nexport type { PhotonImage as PhotonImageType } from \"@silvia-odwyer/photon-node\";\n\ntype ReadFileSync = typeof fs.readFileSync;\n\nconst WASM_FILENAME = \"photon_rs_bg.wasm\";\n\n// Lazy-loaded photon module\nlet photonModule: typeof import(\"@silvia-odwyer/photon-node\") | null = null;\nlet loadPromise: Promise<typeof import(\"@silvia-odwyer/photon-node\") | null> | null = null;\n\nfunction pathOrNull(file: PathOrFileDescriptor): string | null {\n\tif (typeof file === \"string\") {\n\t\treturn file;\n\t}\n\tif (file instanceof URL) {\n\t\treturn fileURLToPath(file);\n\t}\n\treturn null;\n}\n\nfunction getFallbackWasmPaths(): string[] {\n\tconst execDir = path.dirname(process.execPath);\n\treturn [\n\t\tpath.join(execDir, WASM_FILENAME),\n\t\tpath.join(execDir, \"photon\", WASM_FILENAME),\n\t\tpath.join(process.cwd(), WASM_FILENAME),\n\t];\n}\n\nfunction patchPhotonWasmRead(): () => void {\n\tconst originalReadFileSync: ReadFileSync = fs.readFileSync.bind(fs);\n\tconst fallbackPaths = getFallbackWasmPaths();\n\tconst mutableFs = fs as { readFileSync: ReadFileSync };\n\n\tconst patchedReadFileSync: ReadFileSync = ((...args: Parameters<ReadFileSync>) => {\n\t\tconst [file, options] = args;\n\t\tconst resolvedPath = pathOrNull(file);\n\n\t\tif (resolvedPath?.endsWith(WASM_FILENAME)) {\n\t\t\ttry {\n\t\t\t\treturn originalReadFileSync(...args);\n\t\t\t} catch (error) {\n\t\t\t\tconst err = error as NodeJS.ErrnoException;\n\t\t\t\tif (err?.code && err.code !== \"ENOENT\") {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\n\t\t\t\tfor (const fallbackPath of fallbackPaths) {\n\t\t\t\t\tif (!fs.existsSync(fallbackPath)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (options === undefined) {\n\t\t\t\t\t\treturn originalReadFileSync(fallbackPath);\n\t\t\t\t\t}\n\t\t\t\t\treturn originalReadFileSync(fallbackPath, options);\n\t\t\t\t}\n\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\n\t\treturn originalReadFileSync(...args);\n\t}) as ReadFileSync;\n\n\ttry {\n\t\tmutableFs.readFileSync = patchedReadFileSync;\n\t} catch {\n\t\tObject.defineProperty(fs, \"readFileSync\", {\n\t\t\tvalue: patchedReadFileSync,\n\t\t\twritable: true,\n\t\t\tconfigurable: true,\n\t\t});\n\t}\n\n\treturn () => {\n\t\ttry {\n\t\t\tmutableFs.readFileSync = originalReadFileSync;\n\t\t} catch {\n\t\t\tObject.defineProperty(fs, \"readFileSync\", {\n\t\t\t\tvalue: originalReadFileSync,\n\t\t\t\twritable: true,\n\t\t\t\tconfigurable: true,\n\t\t\t});\n\t\t}\n\t};\n}\n\n/**\n * Load the photon module asynchronously.\n * Returns cached module on subsequent calls.\n */\nexport async function loadPhoton(): Promise<typeof import(\"@silvia-odwyer/photon-node\") | null> {\n\tif (photonModule) {\n\t\treturn photonModule;\n\t}\n\n\tif (loadPromise) {\n\t\treturn loadPromise;\n\t}\n\n\tloadPromise = (async () => {\n\t\tconst restoreReadFileSync = patchPhotonWasmRead();\n\t\ttry {\n\t\t\tphotonModule = await import(\"@silvia-odwyer/photon-node\");\n\t\t\treturn photonModule;\n\t\t} catch {\n\t\t\tphotonModule = null;\n\t\t\treturn photonModule;\n\t\t} finally {\n\t\t\trestoreReadFileSync();\n\t\t}\n\t})();\n\n\treturn loadPromise;\n}\n"]}
@@ -184,7 +184,7 @@ export default function (pi: ExtensionAPI) {
184
184
  const ok = await ctx.ui.confirm("Title", "Are you sure?");
185
185
  ctx.ui.notify("Done!", "success");
186
186
  ctx.ui.setStatus("my-ext", "Processing..."); // Footer status
187
- ctx.ui.setWidget("my-ext", ["Line 1", "Line 2"]); // Widget above editor
187
+ ctx.ui.setWidget("my-ext", ["Line 1", "Line 2"]); // Widget above editor (default)
188
188
  });
189
189
 
190
190
  // Register tools, commands, shortcuts, flags
@@ -1366,7 +1366,7 @@ Extensions can interact with users via `ctx.ui` methods and customize how messag
1366
1366
  - Settings toggles (SettingsList)
1367
1367
  - Status indicators (setStatus)
1368
1368
  - Working message during streaming (setWorkingMessage)
1369
- - Widgets above editor (setWidget)
1369
+ - Widgets above/below editor (setWidget)
1370
1370
  - Custom footers (setFooter)
1371
1371
 
1372
1372
  ### Dialogs
@@ -1456,8 +1456,10 @@ ctx.ui.setStatus("my-ext", undefined); // Clear
1456
1456
  ctx.ui.setWorkingMessage("Thinking deeply...");
1457
1457
  ctx.ui.setWorkingMessage(); // Restore default
1458
1458
 
1459
- // Widget above editor (string array or factory function)
1459
+ // Widget above editor (default)
1460
1460
  ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
1461
+ // Widget below editor
1462
+ ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"], { placement: "belowEditor" });
1461
1463
  ctx.ui.setWidget("my-widget", (tui, theme) => new Text(theme.fg("accent", "Custom"), 0, 0));
1462
1464
  ctx.ui.setWidget("my-widget", undefined); // Clear
1463
1465
 
package/docs/session.md CHANGED
@@ -10,6 +10,12 @@ Sessions are stored as JSONL (JSON Lines) files. Each line is a JSON object with
10
10
 
11
11
  Where `<path>` is the working directory with `/` replaced by `-`.
12
12
 
13
+ ## Deleting Sessions
14
+
15
+ Sessions can be removed by deleting their `.jsonl` files under `~/.pi/agent/sessions/`.
16
+
17
+ Pi also supports deleting sessions interactively from `/resume` (select a session and press `Ctrl+D`, then confirm). When available, pi uses the `trash` CLI to avoid permanent deletion.
18
+
13
19
  ## Session Version
14
20
 
15
21
  Sessions have a version field in the header:
package/docs/tui.md CHANGED
@@ -52,6 +52,36 @@ When a `Focusable` component has focus, TUI:
52
52
 
53
53
  This enables IME candidate windows to appear at the correct position for CJK input methods. The `Editor` and `Input` built-in components already implement this interface.
54
54
 
55
+ ### Container Components with Embedded Inputs
56
+
57
+ When a container component (dialog, selector, etc.) contains an `Input` or `Editor` child, the container must implement `Focusable` and propagate the focus state to the child. Otherwise, the hardware cursor won't be positioned correctly for IME input.
58
+
59
+ ```typescript
60
+ import { Container, type Focusable, Input } from "@mariozechner/pi-tui";
61
+
62
+ class SearchDialog extends Container implements Focusable {
63
+ private searchInput: Input;
64
+
65
+ // Focusable implementation - propagate to child input for IME cursor positioning
66
+ private _focused = false;
67
+ get focused(): boolean {
68
+ return this._focused;
69
+ }
70
+ set focused(value: boolean) {
71
+ this._focused = value;
72
+ this.searchInput.focused = value;
73
+ }
74
+
75
+ constructor() {
76
+ super();
77
+ this.searchInput = new Input();
78
+ this.addChild(this.searchInput);
79
+ }
80
+ }
81
+ ```
82
+
83
+ Without this propagation, typing with an IME (Chinese, Japanese, Korean, etc.) will show the candidate window in the wrong position on screen.
84
+
55
85
  ## Using Components
56
86
 
57
87
  **In hooks** via `ctx.ui.custom()`:
@@ -674,14 +704,17 @@ ctx.ui.setStatus("my-ext", undefined);
674
704
 
675
705
  **Examples:** [status-line.ts](../examples/extensions/status-line.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts)
676
706
 
677
- ### Pattern 5: Widget Above Editor
707
+ ### Pattern 5: Widgets Above/Below Editor
678
708
 
679
- Show persistent content above the input editor. Good for todo lists, progress.
709
+ Show persistent content above or below the input editor. Good for todo lists, progress.
680
710
 
681
711
  ```typescript
682
- // Simple string array
712
+ // Simple string array (above editor by default)
683
713
  ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
684
714
 
715
+ // Render below the editor
716
+ ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"], { placement: "belowEditor" });
717
+
685
718
  // Or with theme
686
719
  ctx.ui.setWidget("my-widget", (_tui, theme) => {
687
720
  const lines = items.map((item, i) =>
@@ -47,6 +47,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/
47
47
  | `handoff.ts` | Transfer context to a new focused session via `/handoff <goal>` |
48
48
  | `qna.ts` | Extracts questions from last response into editor via `ctx.ui.setEditorText()` |
49
49
  | `status-line.ts` | Shows turn progress in footer via `ctx.ui.setStatus()` with themed colors |
50
+ | `widget-placement.ts` | Shows widgets above and below the editor via `ctx.ui.setWidget()` placement |
50
51
  | `model-status.ts` | Shows model changes in status bar via `model_select` hook |
51
52
  | `snake.ts` | Snake game with custom UI, keyboard handling, and session persistence |
52
53
  | `send-user-message.ts` | Demonstrates `pi.sendUserMessage()` for sending user messages from extensions |
@@ -22,28 +22,17 @@ interface QuestionDetails {
22
22
  wasCustom?: boolean;
23
23
  }
24
24
 
25
- // Support both simple strings and objects with descriptions
26
- const OptionSchema = Type.Union([
27
- Type.String(),
28
- Type.Object({
29
- label: Type.String({ description: "Display label for the option" }),
30
- description: Type.Optional(Type.String({ description: "Optional description shown below label" })),
31
- }),
32
- ]);
25
+ // Options with labels and optional descriptions
26
+ const OptionSchema = Type.Object({
27
+ label: Type.String({ description: "Display label for the option" }),
28
+ description: Type.Optional(Type.String({ description: "Optional description shown below label" })),
29
+ });
33
30
 
34
31
  const QuestionParams = Type.Object({
35
32
  question: Type.String({ description: "The question to ask the user" }),
36
33
  options: Type.Array(OptionSchema, { description: "Options for the user to choose from" }),
37
34
  });
38
35
 
39
- // Normalize option to { label, description? }
40
- function normalizeOption(opt: string | { label: string; description?: string }): OptionWithDesc {
41
- if (typeof opt === "string") {
42
- return { label: opt };
43
- }
44
- return opt;
45
- }
46
-
47
36
  export default function question(pi: ExtensionAPI) {
48
37
  pi.registerTool({
49
38
  name: "question",
@@ -57,7 +46,7 @@ export default function question(pi: ExtensionAPI) {
57
46
  content: [{ type: "text", text: "Error: UI not available (running in non-interactive mode)" }],
58
47
  details: {
59
48
  question: params.question,
60
- options: params.options.map((o) => (typeof o === "string" ? o : o.label)),
49
+ options: params.options.map((o) => o.label),
61
50
  answer: null,
62
51
  } as QuestionDetails,
63
52
  };
@@ -70,9 +59,7 @@ export default function question(pi: ExtensionAPI) {
70
59
  };
71
60
  }
72
61
 
73
- // Normalize options
74
- const normalizedOptions = params.options.map(normalizeOption);
75
- const allOptions: DisplayOption[] = [...normalizedOptions, { label: "Type something.", isOther: true }];
62
+ const allOptions: DisplayOption[] = [...params.options, { label: "Type something.", isOther: true }];
76
63
 
77
64
  const result = await ctx.ui.custom<{ answer: string; wasCustom: boolean; index?: number } | null>(
78
65
  (tui, theme, _kb, done) => {
@@ -209,7 +196,7 @@ export default function question(pi: ExtensionAPI) {
209
196
  );
210
197
 
211
198
  // Build simple options list for details
212
- const simpleOptions = normalizedOptions.map((o) => o.label);
199
+ const simpleOptions = params.options.map((o) => o.label);
213
200
 
214
201
  if (!result) {
215
202
  return {
@@ -244,7 +231,7 @@ export default function question(pi: ExtensionAPI) {
244
231
  let text = theme.fg("toolTitle", theme.bold("question ")) + theme.fg("muted", args.question);
245
232
  const opts = Array.isArray(args.options) ? args.options : [];
246
233
  if (opts.length) {
247
- const labels = opts.map((o: string | { label: string }) => (typeof o === "string" ? o : o.label));
234
+ const labels = opts.map((o: OptionWithDesc) => o.label);
248
235
  const numbered = [...labels, "Type something."].map((o, i) => `${i + 1}. ${o}`);
249
236
  text += `\n${theme.fg("dim", ` Options: ${numbered.join(", ")}`)}`;
250
237
  }
@@ -0,0 +1,17 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+
3
+ const applyWidgets = (ctx: ExtensionContext) => {
4
+ if (!ctx.hasUI) return;
5
+ ctx.ui.setWidget("widget-above", ["Above editor widget"]);
6
+ ctx.ui.setWidget("widget-below", ["Below editor widget"], { placement: "belowEditor" });
7
+ };
8
+
9
+ export default function widgetPlacementExtension(pi: ExtensionAPI) {
10
+ pi.on("session_start", (_event, ctx) => {
11
+ applyWidgets(ctx);
12
+ });
13
+
14
+ pi.on("session_switch", (_event, ctx) => {
15
+ applyWidgets(ctx);
16
+ });
17
+ }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pi-extension-with-deps",
3
- "version": "1.13.0",
3
+ "version": "1.13.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pi-extension-with-deps",
9
- "version": "1.13.0",
9
+ "version": "1.13.2",
10
10
  "dependencies": {
11
11
  "ms": "^2.1.3"
12
12
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-with-deps",
3
3
  "private": true,
4
- "version": "1.13.0",
4
+ "version": "1.13.2",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mariozechner/pi-coding-agent",
3
- "version": "0.49.0",
3
+ "version": "0.49.2",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -33,16 +33,16 @@
33
33
  "build": "tsgo -p tsconfig.build.json && shx chmod +x dist/cli.js && npm run copy-assets",
34
34
  "build:binary": "npm run build && bun build --compile ./dist/cli.js --outfile dist/pi && npm run copy-binary-assets",
35
35
  "copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/",
36
- "copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/",
36
+ "copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/",
37
37
  "test": "vitest --run",
38
38
  "prepublishOnly": "npm run clean && npm run build"
39
39
  },
40
40
  "dependencies": {
41
41
  "@mariozechner/clipboard": "^0.3.0",
42
42
  "@mariozechner/jiti": "^2.6.2",
43
- "@mariozechner/pi-agent-core": "^0.49.0",
44
- "@mariozechner/pi-ai": "^0.49.0",
45
- "@mariozechner/pi-tui": "^0.49.0",
43
+ "@mariozechner/pi-agent-core": "^0.49.2",
44
+ "@mariozechner/pi-ai": "^0.49.2",
45
+ "@mariozechner/pi-tui": "^0.49.2",
46
46
  "@silvia-odwyer/photon-node": "^0.3.4",
47
47
  "chalk": "^5.5.0",
48
48
  "cli-highlight": "^2.1.11",