pi-ui-extend 0.1.11 → 0.1.15

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 (122) hide show
  1. package/README.md +1 -1
  2. package/dist/app/app.d.ts +5 -0
  3. package/dist/app/app.js +88 -16
  4. package/dist/app/cli/install.d.ts +14 -0
  5. package/dist/app/cli/install.js +19 -7
  6. package/dist/app/cli/startup-info.js +5 -2
  7. package/dist/app/cli/update.d.ts +7 -0
  8. package/dist/app/cli/update.js +11 -3
  9. package/dist/app/commands/command-controller.js +1 -0
  10. package/dist/app/commands/command-host.d.ts +3 -0
  11. package/dist/app/commands/command-model-actions.d.ts +2 -0
  12. package/dist/app/commands/command-model-actions.js +40 -4
  13. package/dist/app/commands/command-navigation-actions.js +3 -0
  14. package/dist/app/commands/command-registry.d.ts +1 -0
  15. package/dist/app/commands/command-registry.js +8 -0
  16. package/dist/app/commands/shell-command.d.ts +7 -0
  17. package/dist/app/commands/shell-command.js +12 -4
  18. package/dist/app/extensions/extension-ui-controller.d.ts +16 -5
  19. package/dist/app/extensions/extension-ui-controller.js +99 -61
  20. package/dist/app/icons.d.ts +1 -0
  21. package/dist/app/icons.js +2 -0
  22. package/dist/app/input/input-action-controller.d.ts +1 -0
  23. package/dist/app/input/input-action-controller.js +8 -2
  24. package/dist/app/input/prompt-enhancer-controller.d.ts +7 -1
  25. package/dist/app/input/prompt-enhancer-controller.js +12 -3
  26. package/dist/app/input/voice-controller.d.ts +49 -1
  27. package/dist/app/input/voice-controller.js +16 -5
  28. package/dist/app/logger.d.ts +25 -0
  29. package/dist/app/logger.js +90 -0
  30. package/dist/app/model/model-usage-status.js +30 -15
  31. package/dist/app/popup/menu-items-controller.d.ts +2 -0
  32. package/dist/app/popup/menu-items-controller.js +45 -6
  33. package/dist/app/popup/popup-action-controller.d.ts +2 -1
  34. package/dist/app/popup/popup-action-controller.js +7 -4
  35. package/dist/app/popup/popup-menu-controller.d.ts +36 -23
  36. package/dist/app/popup/popup-menu-controller.js +68 -322
  37. package/dist/app/rendering/conversation-entry-renderer.js +4 -13
  38. package/dist/app/rendering/conversation-viewport.d.ts +10 -2
  39. package/dist/app/rendering/conversation-viewport.js +157 -16
  40. package/dist/app/rendering/editor-panels.js +4 -2
  41. package/dist/app/rendering/popup-menu-renderer.d.ts +50 -0
  42. package/dist/app/rendering/popup-menu-renderer.js +307 -0
  43. package/dist/app/rendering/render-controller.js +5 -13
  44. package/dist/app/rendering/status-line-renderer.d.ts +1 -1
  45. package/dist/app/rendering/status-line-renderer.js +30 -35
  46. package/dist/app/rendering/toast-controller.d.ts +11 -3
  47. package/dist/app/rendering/toast-controller.js +53 -12
  48. package/dist/app/rendering/toast-renderer.js +10 -13
  49. package/dist/app/rendering/tool-block-renderer.d.ts +1 -0
  50. package/dist/app/rendering/tool-block-renderer.js +3 -2
  51. package/dist/app/runtime.d.ts +2 -1
  52. package/dist/app/runtime.js +20 -10
  53. package/dist/app/screen/clipboard.d.ts +9 -0
  54. package/dist/app/screen/clipboard.js +19 -6
  55. package/dist/app/screen/file-link-opener.d.ts +8 -0
  56. package/dist/app/screen/file-link-opener.js +11 -3
  57. package/dist/app/screen/file-links.js +3 -3
  58. package/dist/app/screen/image-opener.d.ts +12 -0
  59. package/dist/app/screen/image-opener.js +13 -5
  60. package/dist/app/screen/mouse-controller.d.ts +2 -2
  61. package/dist/app/screen/mouse-controller.js +27 -48
  62. package/dist/app/screen/screen-styler.d.ts +1 -1
  63. package/dist/app/screen/screen-styler.js +9 -7
  64. package/dist/app/screen/scroll-controller.d.ts +11 -9
  65. package/dist/app/screen/scroll-controller.js +50 -45
  66. package/dist/app/session/lazy-session-manager.d.ts +11 -0
  67. package/dist/app/session/lazy-session-manager.js +539 -0
  68. package/dist/app/session/pix-system-message.d.ts +16 -0
  69. package/dist/app/session/pix-system-message.js +64 -0
  70. package/dist/app/session/queued-message-controller.js +5 -1
  71. package/dist/app/session/session-event-controller.d.ts +11 -0
  72. package/dist/app/session/session-event-controller.js +58 -2
  73. package/dist/app/session/session-history.d.ts +18 -0
  74. package/dist/app/session/session-history.js +72 -3
  75. package/dist/app/session/session-lifecycle-controller.d.ts +6 -2
  76. package/dist/app/session/session-lifecycle-controller.js +7 -2
  77. package/dist/app/session/tabs-controller.d.ts +13 -1
  78. package/dist/app/session/tabs-controller.js +248 -27
  79. package/dist/app/terminal/nerd-font-controller.d.ts +16 -0
  80. package/dist/app/terminal/nerd-font-controller.js +20 -12
  81. package/dist/app/todo/todo-model.d.ts +3 -1
  82. package/dist/app/todo/todo-model.js +14 -2
  83. package/dist/app/types.d.ts +5 -2
  84. package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
  85. package/dist/app/workspace/workspace-actions-controller.js +12 -0
  86. package/dist/config.d.ts +5 -1
  87. package/dist/config.js +73 -25
  88. package/dist/default-pix-config.js +9 -6
  89. package/dist/schemas/index.d.ts +5 -0
  90. package/dist/schemas/index.js +5 -0
  91. package/dist/schemas/pi-tools-suite-schema.d.ts +178 -0
  92. package/dist/schemas/pi-tools-suite-schema.js +219 -0
  93. package/dist/schemas/pix-schema.d.ts +66 -0
  94. package/dist/schemas/pix-schema.js +92 -0
  95. package/dist/terminal-width.d.ts +2 -0
  96. package/dist/terminal-width.js +134 -56
  97. package/external/pi-tools-suite/README.md +1 -0
  98. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +12 -3
  99. package/external/pi-tools-suite/src/antigravity-auth/commands.ts +2 -4
  100. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +2 -2
  101. package/external/pi-tools-suite/src/antigravity-auth/index.ts +8 -2
  102. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +102 -50
  103. package/external/pi-tools-suite/src/antigravity-auth/status.ts +81 -2
  104. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +29 -8
  105. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +3 -0
  106. package/external/pi-tools-suite/src/config.ts +8 -0
  107. package/external/pi-tools-suite/src/dcp/index.ts +16 -1
  108. package/external/pi-tools-suite/src/dcp/state.ts +35 -0
  109. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +4 -0
  110. package/external/pi-tools-suite/src/todo/index.ts +185 -13
  111. package/external/pi-tools-suite/src/todo/state/selectors.ts +4 -0
  112. package/external/pi-tools-suite/src/todo/state/state-reducer.ts +23 -10
  113. package/external/pi-tools-suite/src/todo/todo.ts +12 -11
  114. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +33 -6
  115. package/external/pi-tools-suite/src/todo/tool/types.ts +9 -1
  116. package/external/pi-tools-suite/src/todo/view/format.ts +2 -1
  117. package/external/pi-tools-suite/src/tool-descriptions.ts +2 -1
  118. package/external/pi-tools-suite/src/usage/index.ts +5 -2
  119. package/external/pi-tools-suite/src/usage/lib/google.ts +6 -13
  120. package/package.json +12 -3
  121. package/schemas/pi-tools-suite.json +885 -0
  122. package/schemas/pix.json +302 -0
@@ -1,3 +1,9 @@
1
+ import { spawn } from "node:child_process";
2
+ type ShellCommandDeps = {
3
+ spawn: typeof spawn;
4
+ waitForReturnToPix: () => Promise<void>;
5
+ };
6
+ export declare function setShellCommandTestDeps(overrides: Partial<ShellCommandDeps>): () => void;
1
7
  export type InteractiveShellCommandResult = {
2
8
  exitCode: number | null;
3
9
  signal: NodeJS.Signals | null;
@@ -25,3 +31,4 @@ export declare function shellCommandFromBangInput(text: string): string | undefi
25
31
  export declare function runChatShellCommand(command: string, cwd: string, handlers?: ChatShellCommandHandlers): RunningChatShellCommand;
26
32
  export declare function runInteractiveShellCommand(command: string, cwd: string): Promise<InteractiveShellCommandResult>;
27
33
  export declare function formatShellCommandEntry(command: string, result: InteractiveShellCommandResult, prefix?: string): string;
34
+ export {};
@@ -1,4 +1,12 @@
1
1
  import { spawn } from "node:child_process";
2
+ let deps = { spawn, waitForReturnToPix: waitForReturnToPixImpl };
3
+ export function setShellCommandTestDeps(overrides) {
4
+ const previous = deps;
5
+ deps = { ...deps, ...overrides };
6
+ return () => {
7
+ deps = previous;
8
+ };
9
+ }
2
10
  export function bangShellCommandFromInput(text) {
3
11
  const trimmed = text.trimStart();
4
12
  if (!trimmed.startsWith("!"))
@@ -12,7 +20,7 @@ export function shellCommandFromBangInput(text) {
12
20
  export function runChatShellCommand(command, cwd, handlers = {}) {
13
21
  let child;
14
22
  try {
15
- child = spawn(command, {
23
+ child = deps.spawn(command, {
16
24
  cwd,
17
25
  env: process.env,
18
26
  shell: shellOption(),
@@ -79,7 +87,7 @@ export async function runInteractiveShellCommand(command, cwd) {
79
87
  try {
80
88
  const result = await spawnShellCommand(command, cwd);
81
89
  process.stdout.write(`\n[pix] ${formatInteractiveShellResult(result)}\n`);
82
- await waitForReturnToPix();
90
+ await deps.waitForReturnToPix();
83
91
  return result;
84
92
  }
85
93
  finally {
@@ -93,7 +101,7 @@ export function formatShellCommandEntry(command, result, prefix = "!") {
93
101
  }
94
102
  async function spawnShellCommand(command, cwd) {
95
103
  try {
96
- const child = spawn(command, {
104
+ const child = deps.spawn(command, {
97
105
  cwd,
98
106
  env: process.env,
99
107
  shell: shellOption(),
@@ -160,7 +168,7 @@ function formatInteractiveShellResult(result) {
160
168
  return `terminated by ${result.signal}`;
161
169
  return `exit ${result.exitCode ?? 0}`;
162
170
  }
163
- async function waitForReturnToPix() {
171
+ async function waitForReturnToPixImpl() {
164
172
  if (!process.stdin.isTTY || !process.stdin.readable)
165
173
  return;
166
174
  process.stdout.write("[pix] Press Enter to return to pix…");
@@ -7,10 +7,14 @@ export type ExtensionTerminalInputResult = {
7
7
  };
8
8
  export type ExtensionUiControllerHost = {
9
9
  readonly theme: Theme;
10
+ activeExtensionUiScope?(): string | undefined;
10
11
  isRunning(): boolean;
11
12
  render(): void;
12
- showToast(message: string, kind?: ToastKind): void;
13
+ showToast(message: string, kind?: ToastKind, options?: {
14
+ scopeKey?: string;
15
+ }): void;
13
16
  readonly toastNotifier: ToastNotifier;
17
+ toastNotifierForScope?(scopeKey: string | undefined): ToastNotifier;
14
18
  readonly menuController: PixMenuController;
15
19
  setStatus(status: string): void;
16
20
  restoreSessionStatus(): void;
@@ -23,22 +27,24 @@ export declare class ExtensionUiController {
23
27
  private readonly host;
24
28
  private readonly extensionWidgets;
25
29
  private readonly terminalInputHandlers;
26
- private activeCustomUi;
27
- private readonly aboveInputRenderer;
30
+ private readonly activeCustomUis;
28
31
  constructor(host: ExtensionUiControllerHost);
29
32
  get widgets(): ReadonlyMap<string, ExtensionWidgetRegistration>;
30
33
  createExtensionTheme(): ExtensionWidgetTheme;
31
34
  setWidget(key: string, content: unknown, options?: {
32
35
  placement?: WidgetPlacement;
36
+ scopeKey?: string;
37
+ }): void;
38
+ clearWidgets(scopeKey?: string, options?: {
39
+ cancelCustomUi?: boolean;
33
40
  }): void;
34
- clearWidgets(): void;
35
41
  suppressWidget(key: string): void;
36
42
  handleTerminalInput(data: string): ExtensionTerminalInputResult;
37
43
  renderActiveCustomUi(width: number): string[] | undefined;
38
44
  activeCustomUiUsesEditor(): boolean;
39
45
  handleCustomUiMouse(event: ExtensionInputMouseEvent): boolean;
40
46
  widgetTuiHandle(): WidgetTuiHandle;
41
- createExtensionUIContext(): PixExtensionUIContext;
47
+ createExtensionUIContext(scopeKey?: string): PixExtensionUIContext;
42
48
  private setAboveInputWidget;
43
49
  private clearAboveInputWidget;
44
50
  private selectDialog;
@@ -53,5 +59,10 @@ export declare class ExtensionUiController {
53
59
  private cancelActiveCustomUi;
54
60
  private rejectActiveCustomUi;
55
61
  private finishActiveCustomUi;
62
+ private activeCustomUiForActiveScope;
63
+ private activeScopeKey;
64
+ private normalizeScopeKey;
65
+ private scopedWidgetKey;
66
+ private unscopedWidgetKey;
56
67
  private invalidateWidget;
57
68
  }
@@ -6,20 +6,15 @@ export class ExtensionUiController {
6
6
  host;
7
7
  extensionWidgets = new Map();
8
8
  terminalInputHandlers = new Set();
9
- activeCustomUi;
10
- aboveInputRenderer = {
11
- set: (key, content) => {
12
- this.setAboveInputWidget(key, content);
13
- },
14
- clear: (key) => {
15
- this.clearAboveInputWidget(key);
16
- },
17
- };
9
+ activeCustomUis = new Map();
18
10
  constructor(host) {
19
11
  this.host = host;
20
12
  }
21
13
  get widgets() {
22
- return this.extensionWidgets;
14
+ const activeScopeKey = this.activeScopeKey();
15
+ return new Map([...this.extensionWidgets.entries()]
16
+ .filter(([, widget]) => widget.scopeKey === activeScopeKey)
17
+ .map(([scopedKey, widget]) => [this.unscopedWidgetKey(scopedKey, widget.scopeKey), widget]));
23
18
  }
24
19
  createExtensionTheme() {
25
20
  const colors = this.host.theme.colors;
@@ -47,44 +42,54 @@ export class ExtensionUiController {
47
42
  };
48
43
  }
49
44
  setWidget(key, content, options) {
50
- const existing = this.extensionWidgets.get(key);
45
+ const scopeKey = this.normalizeScopeKey(options?.scopeKey);
46
+ const scopedKey = this.scopedWidgetKey(scopeKey, key);
47
+ const existing = this.extensionWidgets.get(scopedKey);
51
48
  if (existing)
52
49
  this.invalidateWidget(existing);
53
50
  if (content === undefined) {
54
- this.extensionWidgets.delete(key);
51
+ this.extensionWidgets.delete(scopedKey);
55
52
  if (this.host.isRunning())
56
53
  this.host.render();
57
54
  return;
58
55
  }
59
56
  if (!Array.isArray(content) && typeof content !== "function")
60
57
  return;
61
- this.extensionWidgets.set(key, {
58
+ this.extensionWidgets.set(scopedKey, {
62
59
  key,
60
+ scopeKey,
63
61
  placement: options?.placement === "belowEditor" ? "belowEditor" : "aboveEditor",
64
62
  content: content,
65
63
  });
66
64
  if (this.host.isRunning())
67
65
  this.host.render();
68
66
  }
69
- clearWidgets() {
70
- this.cancelActiveCustomUi();
71
- for (const widget of this.extensionWidgets.values())
67
+ clearWidgets(scopeKey, options = {}) {
68
+ const normalizedScopeKey = this.normalizeScopeKey(scopeKey);
69
+ if (options.cancelCustomUi !== false)
70
+ this.cancelActiveCustomUi(normalizedScopeKey);
71
+ for (const [key, widget] of this.extensionWidgets.entries()) {
72
+ if (widget.scopeKey !== normalizedScopeKey)
73
+ continue;
72
74
  this.invalidateWidget(widget);
73
- this.extensionWidgets.clear();
75
+ this.extensionWidgets.delete(key);
76
+ }
74
77
  }
75
78
  suppressWidget(key) {
76
- const widget = this.extensionWidgets.get(key);
79
+ const scopedKey = this.scopedWidgetKey(this.activeScopeKey(), key);
80
+ const widget = this.extensionWidgets.get(scopedKey);
77
81
  if (!widget)
78
82
  return;
79
83
  this.invalidateWidget(widget);
80
- this.extensionWidgets.delete(key);
84
+ this.extensionWidgets.delete(scopedKey);
81
85
  }
82
86
  handleTerminalInput(data) {
83
- if (this.activeCustomUi) {
87
+ const active = this.activeCustomUiForActiveScope();
88
+ if (active) {
84
89
  if (data === "\u0003")
85
90
  return { consume: false };
86
91
  try {
87
- const result = this.activeCustomUi.component.handleInput?.(data);
92
+ const result = active.component.handleInput?.(data);
88
93
  if (result && typeof result === "object") {
89
94
  return {
90
95
  consume: result.consume !== false,
@@ -98,7 +103,10 @@ export class ExtensionUiController {
98
103
  return { consume: true };
99
104
  }
100
105
  let current = data;
101
- for (const handler of [...this.terminalInputHandlers]) {
106
+ const activeScopeKey = this.activeScopeKey();
107
+ for (const { scopeKey, handler } of [...this.terminalInputHandlers]) {
108
+ if (scopeKey !== activeScopeKey)
109
+ continue;
102
110
  const result = handler(current);
103
111
  if (result?.data !== undefined)
104
112
  current = result.data;
@@ -108,7 +116,7 @@ export class ExtensionUiController {
108
116
  return current === data ? { consume: false } : { consume: false, data: current };
109
117
  }
110
118
  renderActiveCustomUi(width) {
111
- const active = this.activeCustomUi;
119
+ const active = this.activeCustomUiForActiveScope();
112
120
  if (!active)
113
121
  return undefined;
114
122
  try {
@@ -119,7 +127,7 @@ export class ExtensionUiController {
119
127
  }
120
128
  }
121
129
  activeCustomUiUsesEditor() {
122
- const active = this.activeCustomUi;
130
+ const active = this.activeCustomUiForActiveScope();
123
131
  if (!active)
124
132
  return false;
125
133
  try {
@@ -130,7 +138,7 @@ export class ExtensionUiController {
130
138
  }
131
139
  }
132
140
  handleCustomUiMouse(event) {
133
- const active = this.activeCustomUi;
141
+ const active = this.activeCustomUiForActiveScope();
134
142
  if (!active)
135
143
  return false;
136
144
  try {
@@ -142,13 +150,14 @@ export class ExtensionUiController {
142
150
  }
143
151
  }
144
152
  widgetTuiHandle() {
153
+ const activeScopeToastNotifier = this.host.toastNotifierForScope?.(this.activeScopeKey()) ?? this.host.toastNotifier;
145
154
  return {
146
155
  requestRender: () => {
147
156
  if (this.host.isRunning())
148
157
  this.host.render();
149
158
  },
150
- showToast: this.host.toastNotifier.show,
151
- toast: this.host.toastNotifier,
159
+ showToast: activeScopeToastNotifier.show,
160
+ toast: activeScopeToastNotifier,
152
161
  showMenu: this.host.menuController.show,
153
162
  menu: this.host.menuController,
154
163
  pix: {
@@ -157,9 +166,11 @@ export class ExtensionUiController {
157
166
  },
158
167
  };
159
168
  }
160
- createExtensionUIContext() {
169
+ createExtensionUIContext(scopeKey) {
170
+ const contextScopeKey = this.normalizeScopeKey(scopeKey);
171
+ const scopedToastNotifier = this.host.toastNotifierForScope?.(contextScopeKey) ?? this.host.toastNotifier;
161
172
  const notify = (message, type) => {
162
- this.host.showToast(message, isToastKind(type) ? type : "info");
173
+ this.host.showToast(message, isToastKind(type) ? type : "info", { scopeKey: contextScopeKey });
163
174
  };
164
175
  const extensionTheme = this.createExtensionTheme();
165
176
  const renderIfRunning = () => {
@@ -169,17 +180,24 @@ export class ExtensionUiController {
169
180
  return {
170
181
  select: async (title, options, opts) => await this.selectDialog(title, options, opts),
171
182
  confirm: async (title, message, opts) => await this.confirmDialog(title, message, opts),
172
- input: async (title, placeholder, opts) => await this.inputDialog(title, placeholder, opts),
183
+ input: async (title, placeholder, opts) => await this.inputDialog(title, placeholder, opts, contextScopeKey),
173
184
  notify,
174
- toast: this.host.toastNotifier,
175
- aboveInput: this.aboveInputRenderer,
185
+ toast: scopedToastNotifier,
186
+ aboveInput: {
187
+ set: (key, content) => {
188
+ this.setAboveInputWidget(key, content, contextScopeKey);
189
+ },
190
+ clear: (key) => {
191
+ this.clearAboveInputWidget(key, contextScopeKey);
192
+ },
193
+ },
176
194
  renderAboveInput: (key, content) => {
177
- this.setAboveInputWidget(key, content);
195
+ this.setAboveInputWidget(key, content, contextScopeKey);
178
196
  },
179
197
  showMenu: this.host.menuController.show,
180
198
  menu: this.host.menuController,
181
199
  onTerminalInput: (handler) => {
182
- const terminalInputHandler = handler;
200
+ const terminalInputHandler = { scopeKey: contextScopeKey, handler: handler };
183
201
  this.terminalInputHandlers.add(terminalInputHandler);
184
202
  return () => {
185
203
  this.terminalInputHandlers.delete(terminalInputHandler);
@@ -187,13 +205,13 @@ export class ExtensionUiController {
187
205
  },
188
206
  setStatus: (_key, text) => {
189
207
  if (text)
190
- this.host.showToast(text, "info");
208
+ this.host.showToast(text, "info", { scopeKey: contextScopeKey });
191
209
  this.host.restoreSessionStatus();
192
210
  renderIfRunning();
193
211
  },
194
212
  setWorkingMessage: (message) => {
195
213
  if (message)
196
- this.host.showToast(message, "info");
214
+ this.host.showToast(message, "info", { scopeKey: contextScopeKey });
197
215
  this.host.restoreSessionStatus();
198
216
  renderIfRunning();
199
217
  },
@@ -201,7 +219,7 @@ export class ExtensionUiController {
201
219
  setWorkingIndicator: () => undefined,
202
220
  setHiddenThinkingLabel: () => undefined,
203
221
  setWidget: ((key, content, options) => {
204
- this.setWidget(key, content, options);
222
+ this.setWidget(key, content, { ...options, scopeKey: contextScopeKey });
205
223
  }),
206
224
  setFooter: () => undefined,
207
225
  setHeader: () => undefined,
@@ -209,7 +227,7 @@ export class ExtensionUiController {
209
227
  process.title = title;
210
228
  renderIfRunning();
211
229
  },
212
- custom: (async (factory) => await this.showCustomUi(factory)),
230
+ custom: (async (factory) => await this.showCustomUi(factory, { scopeKey: contextScopeKey })),
213
231
  pasteToEditor: (text) => {
214
232
  this.host.setInput(text);
215
233
  renderIfRunning();
@@ -219,7 +237,7 @@ export class ExtensionUiController {
219
237
  renderIfRunning();
220
238
  },
221
239
  getEditorText: () => this.host.getInput(),
222
- editor: async (title, prefill) => await this.editorDialog(title, prefill),
240
+ editor: async (title, prefill) => await this.editorDialog(title, prefill, contextScopeKey),
223
241
  addAutocompleteProvider: () => undefined,
224
242
  setEditorComponent: () => undefined,
225
243
  getEditorComponent: () => undefined,
@@ -241,11 +259,11 @@ export class ExtensionUiController {
241
259
  },
242
260
  };
243
261
  }
244
- setAboveInputWidget(key, content) {
245
- this.setWidget(key, content, { placement: "aboveEditor" });
262
+ setAboveInputWidget(key, content, scopeKey = this.activeScopeKey()) {
263
+ this.setWidget(key, content, { placement: "aboveEditor", scopeKey });
246
264
  }
247
- clearAboveInputWidget(key) {
248
- this.setWidget(key, undefined, { placement: "aboveEditor" });
265
+ clearAboveInputWidget(key, scopeKey = this.activeScopeKey()) {
266
+ this.setWidget(key, undefined, { placement: "aboveEditor", scopeKey });
249
267
  }
250
268
  async selectDialog(title, options, opts) {
251
269
  if (opts?.signal?.aborted)
@@ -265,7 +283,7 @@ export class ExtensionUiController {
265
283
  });
266
284
  return selected === true;
267
285
  }
268
- async inputDialog(title, placeholder, opts) {
286
+ async inputDialog(title, placeholder, opts, scopeKey = this.activeScopeKey()) {
269
287
  if (opts?.signal?.aborted)
270
288
  return undefined;
271
289
  return await this.editorBackedDialog({
@@ -274,21 +292,21 @@ export class ExtensionUiController {
274
292
  mode: "input",
275
293
  ...(placeholder === undefined ? {} : { placeholder }),
276
294
  ...(opts === undefined ? {} : { opts }),
277
- });
295
+ }, scopeKey);
278
296
  }
279
- async editorDialog(title, prefill = "") {
297
+ async editorDialog(title, prefill = "", scopeKey = this.activeScopeKey()) {
280
298
  return await this.editorBackedDialog({
281
299
  title,
282
300
  initialValue: prefill,
283
301
  mode: "editor",
284
- });
302
+ }, scopeKey);
285
303
  }
286
- async editorBackedDialog(options) {
304
+ async editorBackedDialog(options, scopeKey = this.activeScopeKey()) {
287
305
  if (options.opts?.signal?.aborted)
288
306
  return undefined;
289
307
  if (!this.host.isRunning())
290
308
  return undefined;
291
- if (this.activeCustomUi)
309
+ if (this.activeCustomUis.has(scopeKey))
292
310
  throw new Error("Another extension custom UI is already active.");
293
311
  const savedInput = this.host.getInput();
294
312
  this.host.setInput(options.initialValue);
@@ -315,9 +333,9 @@ export class ExtensionUiController {
315
333
  return { consume: false, data };
316
334
  },
317
335
  };
318
- }, { savedInput });
336
+ }, { savedInput, scopeKey });
319
337
  return await this.withDialogAutoDismiss(promise, options.opts, () => {
320
- this.cancelActiveCustomUi();
338
+ this.cancelActiveCustomUi(scopeKey);
321
339
  });
322
340
  }
323
341
  renderEditorBackedDialog(options, width) {
@@ -356,7 +374,8 @@ export class ExtensionUiController {
356
374
  async showCustomUi(factory, options = {}) {
357
375
  if (!this.host.isRunning())
358
376
  return undefined;
359
- if (this.activeCustomUi)
377
+ const scopeKey = this.normalizeScopeKey(options.scopeKey);
378
+ if (this.activeCustomUis.has(scopeKey))
360
379
  throw new Error("Another extension custom UI is already active.");
361
380
  const savedInput = options.savedInput ?? this.host.getInput();
362
381
  return await new Promise((resolve, reject) => {
@@ -365,7 +384,7 @@ export class ExtensionUiController {
365
384
  if (settled)
366
385
  return;
367
386
  settled = true;
368
- this.finishActiveCustomUi(value, { resolve: true });
387
+ this.finishActiveCustomUi(scopeKey, value, { resolve: true });
369
388
  resolve(value);
370
389
  };
371
390
  void (async () => {
@@ -375,8 +394,9 @@ export class ExtensionUiController {
375
394
  component.dispose?.();
376
395
  return;
377
396
  }
378
- this.activeCustomUi = {
397
+ this.activeCustomUis.set(scopeKey, {
379
398
  key: CUSTOM_UI_WIDGET_KEY,
399
+ scopeKey,
380
400
  component,
381
401
  savedInput,
382
402
  resolve: (value) => {
@@ -391,7 +411,7 @@ export class ExtensionUiController {
391
411
  settled = true;
392
412
  reject(error);
393
413
  },
394
- };
414
+ });
395
415
  if (this.host.isRunning())
396
416
  this.host.render();
397
417
  }
@@ -404,17 +424,20 @@ export class ExtensionUiController {
404
424
  })();
405
425
  });
406
426
  }
407
- cancelActiveCustomUi() {
408
- this.finishActiveCustomUi(undefined, { resolve: true });
427
+ cancelActiveCustomUi(scopeKey = this.activeScopeKey()) {
428
+ this.finishActiveCustomUi(scopeKey, undefined, { resolve: true });
409
429
  }
410
430
  rejectActiveCustomUi(error) {
411
- this.finishActiveCustomUi(error, { resolve: false });
431
+ const active = this.activeCustomUiForActiveScope();
432
+ if (!active)
433
+ return;
434
+ this.finishActiveCustomUi(active.scopeKey, error, { resolve: false });
412
435
  }
413
- finishActiveCustomUi(value, options) {
414
- const active = this.activeCustomUi;
436
+ finishActiveCustomUi(scopeKey, value, options) {
437
+ const active = this.activeCustomUis.get(scopeKey);
415
438
  if (!active)
416
439
  return;
417
- this.activeCustomUi = undefined;
440
+ this.activeCustomUis.delete(scopeKey);
418
441
  if (this.host.getInput() !== active.savedInput)
419
442
  this.host.setInput(active.savedInput);
420
443
  try {
@@ -436,6 +459,21 @@ export class ExtensionUiController {
436
459
  if (this.host.isRunning())
437
460
  this.host.render();
438
461
  }
462
+ activeCustomUiForActiveScope() {
463
+ return this.activeCustomUis.get(this.activeScopeKey());
464
+ }
465
+ activeScopeKey() {
466
+ return this.normalizeScopeKey(this.host.activeExtensionUiScope?.());
467
+ }
468
+ normalizeScopeKey(scopeKey) {
469
+ return scopeKey ?? "";
470
+ }
471
+ scopedWidgetKey(scopeKey, key) {
472
+ return `${scopeKey.length}:${scopeKey}:${key}`;
473
+ }
474
+ unscopedWidgetKey(scopedKey, scopeKey) {
475
+ return scopedKey.slice(`${scopeKey.length}:${scopeKey}:`.length);
476
+ }
439
477
  invalidateWidget(widget) {
440
478
  try {
441
479
  widget.component?.dispose?.();
@@ -14,6 +14,7 @@ declare const NERD_FONT_ICONS: {
14
14
  readonly info: "󰋼";
15
15
  readonly microphone: "󰍬";
16
16
  readonly plus: "󰐕";
17
+ readonly pause: "󰏤";
17
18
  readonly record: "󰑊";
18
19
  readonly refresh: "󰑐";
19
20
  readonly volumeHigh: "󰕾";
package/dist/app/icons.js CHANGED
@@ -20,6 +20,7 @@ const NERD_FONT_ICONS = {
20
20
  info: "\u{f02fc}",
21
21
  microphone: "\u{f036c}",
22
22
  plus: "\u{f0415}",
23
+ pause: "\u{f03e4}",
23
24
  record: "\u{f044a}",
24
25
  refresh: "\u{f0450}",
25
26
  volumeHigh: "\u{f057e}",
@@ -45,6 +46,7 @@ const FALLBACK_ICONS = {
45
46
  info: "i",
46
47
  microphone: "m",
47
48
  plus: "+",
49
+ pause: "⏸",
48
50
  record: "●",
49
51
  refresh: "↻",
50
52
  volumeHigh: "♪",
@@ -39,6 +39,7 @@ export declare class AppInputActionController {
39
39
  queueInputFromEditor(): Promise<void>;
40
40
  handleInterrupt(): Promise<void>;
41
41
  handleEscape(): Promise<void>;
42
+ private closeActiveGlobalUi;
42
43
  private abortStreamingSession;
43
44
  private restoreSessionState;
44
45
  private sessionActivity;
@@ -63,6 +63,8 @@ export class AppInputActionController {
63
63
  await this.host.stop();
64
64
  }
65
65
  async handleEscape() {
66
+ if (this.closeActiveGlobalUi())
67
+ return;
66
68
  const session = this.host.runtime()?.session;
67
69
  if (session?.isCompacting) {
68
70
  this.host.setStatus("aborting compaction");
@@ -76,8 +78,12 @@ export class AppInputActionController {
76
78
  await this.abortStreamingSession(runtime, { stopIfAlreadyAborting: false });
77
79
  return;
78
80
  }
79
- if (this.popupMenus.syncActivePopupMenu())
80
- this.popupMenus.cancelActivePopupMenu();
81
+ }
82
+ closeActiveGlobalUi() {
83
+ if (!this.popupMenus.syncActivePopupMenu())
84
+ return false;
85
+ this.popupMenus.cancelActivePopupMenu();
86
+ return true;
81
87
  }
82
88
  async abortStreamingSession(runtime, options) {
83
89
  const session = runtime.session;
@@ -1,4 +1,4 @@
1
- import { type AgentSessionRuntime } from "@earendil-works/pi-coding-agent";
1
+ import { createAgentSessionFromServices, createAgentSessionServices, SessionManager, type AgentSessionRuntime } from "@earendil-works/pi-coding-agent";
2
2
  import type { PromptEnhancerConfig } from "../../config.js";
3
3
  import type { InputEditor } from "../../input-editor.js";
4
4
  import type { ToastNotifier } from "../../ui.js";
@@ -19,6 +19,12 @@ export type AppPromptEnhancerControllerHost = {
19
19
  render(): void;
20
20
  };
21
21
  type PromptEnhanceRunner = typeof enhancePromptWithPi;
22
+ type PromptEnhancerPiDeps = {
23
+ createAgentSessionServices: typeof createAgentSessionServices;
24
+ createAgentSessionFromServices: typeof createAgentSessionFromServices;
25
+ sessionManagerInMemory: typeof SessionManager.inMemory;
26
+ };
27
+ export declare function setPromptEnhancerPiTestDeps(overrides?: Partial<PromptEnhancerPiDeps>): void;
22
28
  type AppPromptEnhancerControllerOptions = {
23
29
  enhancePromptWithPi?: PromptEnhanceRunner;
24
30
  };
@@ -11,6 +11,15 @@ Do not add unsupported assumptions.
11
11
  Add useful constraints, acceptance criteria, and context requests when helpful.
12
12
  Output only the improved prompt. No commentary, no markdown fences.`;
13
13
  const PROMPT_ENHANCER_MIN_TEXT_LENGTH = 3;
14
+ const defaultPromptEnhancerPiDeps = {
15
+ createAgentSessionServices,
16
+ createAgentSessionFromServices,
17
+ sessionManagerInMemory: SessionManager.inMemory,
18
+ };
19
+ let promptEnhancerPiDeps = defaultPromptEnhancerPiDeps;
20
+ export function setPromptEnhancerPiTestDeps(overrides) {
21
+ promptEnhancerPiDeps = overrides ? { ...defaultPromptEnhancerPiDeps, ...overrides } : defaultPromptEnhancerPiDeps;
22
+ }
14
23
  export class AppPromptEnhancerController {
15
24
  host;
16
25
  enhancing = false;
@@ -115,7 +124,7 @@ export function promptEnhancerTextIsSufficient(text) {
115
124
  }
116
125
  async function enhancePromptWithPi(runtime, draft, config) {
117
126
  const parsedModel = parseModelRef(config.modelRef);
118
- const services = await createAgentSessionServices({
127
+ const services = await promptEnhancerPiDeps.createAgentSessionServices({
119
128
  cwd: runtime.cwd,
120
129
  agentDir: runtime.services.agentDir,
121
130
  authStorage: runtime.services.authStorage,
@@ -135,9 +144,9 @@ async function enhancePromptWithPi(runtime, draft, config) {
135
144
  if (!model) {
136
145
  throw new Error(modelNotFoundMessage(parsedModel.provider, parsedModel.modelId, services.modelRegistry.getAll()));
137
146
  }
138
- const { session } = await createAgentSessionFromServices({
147
+ const { session } = await promptEnhancerPiDeps.createAgentSessionFromServices({
139
148
  services,
140
- sessionManager: SessionManager.inMemory(runtime.cwd),
149
+ sessionManager: promptEnhancerPiDeps.sessionManagerInMemory(runtime.cwd),
141
150
  model,
142
151
  thinkingLevel: parsedModel.thinkingLevel ?? "minimal",
143
152
  noTools: "all",