pi-ui-extend 0.1.13 → 0.1.17

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 (111) hide show
  1. package/README.md +1 -1
  2. package/dist/app/app.d.ts +7 -0
  3. package/dist/app/app.js +102 -17
  4. package/dist/app/commands/command-controller.js +2 -0
  5. package/dist/app/commands/command-host.d.ts +5 -0
  6. package/dist/app/commands/command-model-actions.d.ts +2 -0
  7. package/dist/app/commands/command-model-actions.js +40 -4
  8. package/dist/app/commands/command-navigation-actions.d.ts +9 -0
  9. package/dist/app/commands/command-navigation-actions.js +62 -0
  10. package/dist/app/commands/command-registry.d.ts +2 -0
  11. package/dist/app/commands/command-registry.js +16 -0
  12. package/dist/app/constants.d.ts +0 -1
  13. package/dist/app/constants.js +0 -1
  14. package/dist/app/extensions/extension-ui-controller.d.ts +16 -5
  15. package/dist/app/extensions/extension-ui-controller.js +99 -61
  16. package/dist/app/icons.d.ts +1 -0
  17. package/dist/app/icons.js +2 -0
  18. package/dist/app/input/input-action-controller.d.ts +2 -0
  19. package/dist/app/input/input-action-controller.js +8 -1
  20. package/dist/app/logger.d.ts +25 -0
  21. package/dist/app/logger.js +90 -0
  22. package/dist/app/model/model-usage-status.js +30 -15
  23. package/dist/app/popup/menu-items-controller.d.ts +4 -0
  24. package/dist/app/popup/menu-items-controller.js +68 -6
  25. package/dist/app/popup/popup-action-controller.d.ts +2 -1
  26. package/dist/app/popup/popup-action-controller.js +7 -4
  27. package/dist/app/popup/popup-menu-controller.d.ts +36 -23
  28. package/dist/app/popup/popup-menu-controller.js +97 -326
  29. package/dist/app/rendering/conversation-entry-renderer.js +3 -3
  30. package/dist/app/rendering/conversation-viewport.d.ts +10 -2
  31. package/dist/app/rendering/conversation-viewport.js +157 -16
  32. package/dist/app/rendering/editor-panels.js +22 -9
  33. package/dist/app/rendering/popup-menu-renderer.d.ts +62 -0
  34. package/dist/app/rendering/popup-menu-renderer.js +405 -0
  35. package/dist/app/rendering/render-controller.js +30 -28
  36. package/dist/app/rendering/render-text.js +5 -2
  37. package/dist/app/rendering/status-line-renderer.d.ts +8 -1
  38. package/dist/app/rendering/status-line-renderer.js +217 -117
  39. package/dist/app/rendering/toast-controller.d.ts +12 -3
  40. package/dist/app/rendering/toast-controller.js +70 -12
  41. package/dist/app/runtime.d.ts +2 -1
  42. package/dist/app/runtime.js +20 -10
  43. package/dist/app/screen/mouse-controller.d.ts +2 -2
  44. package/dist/app/screen/mouse-controller.js +27 -48
  45. package/dist/app/screen/screen-styler.d.ts +1 -1
  46. package/dist/app/screen/screen-styler.js +9 -7
  47. package/dist/app/screen/scroll-controller.d.ts +12 -9
  48. package/dist/app/screen/scroll-controller.js +56 -45
  49. package/dist/app/screen/status-controller.js +2 -1
  50. package/dist/app/session/lazy-session-manager.d.ts +11 -0
  51. package/dist/app/session/lazy-session-manager.js +539 -0
  52. package/dist/app/session/pix-system-message.d.ts +16 -0
  53. package/dist/app/session/pix-system-message.js +64 -0
  54. package/dist/app/session/request-history.d.ts +4 -0
  55. package/dist/app/session/request-history.js +11 -0
  56. package/dist/app/session/session-event-controller.d.ts +11 -0
  57. package/dist/app/session/session-event-controller.js +58 -2
  58. package/dist/app/session/session-history.d.ts +18 -0
  59. package/dist/app/session/session-history.js +72 -3
  60. package/dist/app/session/session-lifecycle-controller.d.ts +6 -2
  61. package/dist/app/session/session-lifecycle-controller.js +7 -2
  62. package/dist/app/session/session-search.js +10 -0
  63. package/dist/app/session/tabs-controller.d.ts +17 -5
  64. package/dist/app/session/tabs-controller.js +308 -29
  65. package/dist/app/todo/todo-model.d.ts +4 -2
  66. package/dist/app/todo/todo-model.js +23 -13
  67. package/dist/app/types.d.ts +17 -6
  68. package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
  69. package/dist/app/workspace/workspace-actions-controller.js +12 -0
  70. package/dist/config.d.ts +6 -1
  71. package/dist/config.js +82 -25
  72. package/dist/default-pix-config.js +4 -0
  73. package/dist/fuzzy.d.ts +2 -0
  74. package/dist/fuzzy.js +27 -7
  75. package/dist/input-editor.d.ts +9 -0
  76. package/dist/input-editor.js +52 -0
  77. package/dist/schemas/pi-tools-suite-schema.d.ts +1 -0
  78. package/dist/schemas/pi-tools-suite-schema.js +1 -0
  79. package/dist/schemas/pix-schema.d.ts +3 -1
  80. package/dist/schemas/pix-schema.js +6 -4
  81. package/dist/terminal-width.d.ts +2 -0
  82. package/dist/terminal-width.js +64 -3
  83. package/dist/theme.js +6 -6
  84. package/dist/ui.d.ts +8 -0
  85. package/external/pi-tools-suite/README.md +3 -2
  86. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +52 -8
  87. package/external/pi-tools-suite/src/antigravity-auth/commands.ts +3 -41
  88. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +0 -2
  89. package/external/pi-tools-suite/src/antigravity-auth/index.ts +11 -18
  90. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +129 -61
  91. package/external/pi-tools-suite/src/antigravity-auth/status.ts +82 -3
  92. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +20 -7
  93. package/external/pi-tools-suite/src/antigravity-auth/types.ts +21 -0
  94. package/external/pi-tools-suite/src/config.ts +8 -0
  95. package/external/pi-tools-suite/src/dcp/index.ts +16 -1
  96. package/external/pi-tools-suite/src/dcp/state.ts +35 -0
  97. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +3 -0
  98. package/external/pi-tools-suite/src/todo/index.ts +123 -14
  99. package/external/pi-tools-suite/src/todo/state/persistence.ts +0 -1
  100. package/external/pi-tools-suite/src/todo/state/state-reducer.ts +26 -43
  101. package/external/pi-tools-suite/src/todo/todo.ts +12 -23
  102. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +34 -16
  103. package/external/pi-tools-suite/src/todo/tool/types.ts +7 -28
  104. package/external/pi-tools-suite/src/todo/view/format.ts +2 -3
  105. package/external/pi-tools-suite/src/tool-descriptions.ts +6 -4
  106. package/external/pi-tools-suite/src/usage/index.ts +5 -2
  107. package/external/pi-tools-suite/src/usage/lib/google.ts +53 -40
  108. package/external/pi-tools-suite/src/usage/lib/types.ts +12 -2
  109. package/package.json +1 -1
  110. package/schemas/pi-tools-suite.json +4 -0
  111. package/schemas/pix.json +11 -2
@@ -32,6 +32,14 @@ export function createSlashCommands(actions, host) {
32
32
  allowArguments: true,
33
33
  run: (argumentsText) => actions.runAutocompleteSlashCommand(argumentsText),
34
34
  },
35
+ {
36
+ name: "no-context-files",
37
+ description: "Set project AGENTS.md/CLAUDE.md loading off or on",
38
+ kind: "builtin",
39
+ keywords: ["context", "agents", "claude", "project", "config"],
40
+ allowArguments: true,
41
+ run: (argumentsText) => actions.runNoContextFilesSlashCommand(argumentsText),
42
+ },
35
43
  {
36
44
  name: "scoped-models",
37
45
  description: "Show or set models used by the model selector/cycling",
@@ -177,6 +185,14 @@ export function createSlashCommands(actions, host) {
177
185
  allowArguments: true,
178
186
  run: (argumentsText) => actions.runJumpCommand(argumentsText),
179
187
  },
188
+ {
189
+ name: "history",
190
+ description: "Search command history and restore a match",
191
+ kind: "builtin",
192
+ keywords: ["command", "request", "prompt", "find", "recent"],
193
+ allowArguments: true,
194
+ run: (argumentsText) => actions.runHistoryCommand(argumentsText),
195
+ },
180
196
  {
181
197
  name: "search",
182
198
  description: "Search sessions and open a match in a new tab",
@@ -42,7 +42,6 @@ export declare const GIT_BRANCH_CACHE_MS = 30000;
42
42
  export declare const TODO_TOOL_NAME = "todo";
43
43
  export declare const TODO_ACTIONS: readonly ["create", "update", "batch_create", "batch_update", "list", "get", "delete", "clear", "export", "import"];
44
44
  export declare const TODO_STATUSES: readonly ["pending", "in_progress", "deferred", "completed", "deleted"];
45
- export declare const TODO_PRIORITIES: readonly ["low", "medium", "high", "urgent"];
46
45
  export declare const SUBAGENT_STATUSES: readonly ["planned", "running", "retrying", "done", "failed", "stopped"];
47
46
  export declare const SUBAGENT_ACTIVE_STATUSES: readonly ["planned", "running", "retrying"];
48
47
  export declare const SUBAGENT_TERMINAL_STATUSES: readonly ["done", "failed", "stopped"];
@@ -90,7 +90,6 @@ export const TODO_ACTIONS = [
90
90
  "import",
91
91
  ];
92
92
  export const TODO_STATUSES = ["pending", "in_progress", "deferred", "completed", "deleted"];
93
- export const TODO_PRIORITIES = ["low", "medium", "high", "urgent"];
94
93
  export const SUBAGENT_STATUSES = ["planned", "running", "retrying", "done", "failed", "stopped"];
95
94
  export const SUBAGENT_ACTIVE_STATUSES = ["planned", "running", "retrying"];
96
95
  export const SUBAGENT_TERMINAL_STATUSES = ["done", "failed", "stopped"];
@@ -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?.();
@@ -12,6 +12,7 @@ declare const NERD_FONT_ICONS: {
12
12
  readonly deleted: "󰅙";
13
13
  readonly deferred: "󰍷";
14
14
  readonly info: "󰋼";
15
+ readonly lightbulb: "󰌵";
15
16
  readonly microphone: "󰍬";
16
17
  readonly plus: "󰐕";
17
18
  readonly pause: "󰏤";
package/dist/app/icons.js CHANGED
@@ -18,6 +18,7 @@ const NERD_FONT_ICONS = {
18
18
  deleted: "\u{f0159}",
19
19
  deferred: "\u{f0377}",
20
20
  info: "\u{f02fc}",
21
+ lightbulb: "\u{f0335}",
21
22
  microphone: "\u{f036c}",
22
23
  plus: "\u{f0415}",
23
24
  pause: "\u{f03e4}",
@@ -44,6 +45,7 @@ const FALLBACK_ICONS = {
44
45
  deleted: "×",
45
46
  deferred: "↷",
46
47
  info: "i",
48
+ lightbulb: "💡",
47
49
  microphone: "m",
48
50
  plus: "+",
49
51
  pause: "⏸",
@@ -19,6 +19,7 @@ export type AppInputActionControllerHost = {
19
19
  addEntry(entry: Entry): void;
20
20
  addSessionAbortedEntry(): void;
21
21
  showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
22
+ dismissActiveDialog?(): boolean;
22
23
  stopVoiceInput(): Promise<void>;
23
24
  isShellCommandRunning(): boolean;
24
25
  runChatShellCommand(command: string): Promise<InteractiveShellCommandResult>;
@@ -39,6 +40,7 @@ export declare class AppInputActionController {
39
40
  queueInputFromEditor(): Promise<void>;
40
41
  handleInterrupt(): Promise<void>;
41
42
  handleEscape(): Promise<void>;
43
+ private closeActiveGlobalUi;
42
44
  private abortStreamingSession;
43
45
  private restoreSessionState;
44
46
  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,13 @@ export class AppInputActionController {
76
78
  await this.abortStreamingSession(runtime, { stopIfAlreadyAborting: false });
77
79
  return;
78
80
  }
79
- if (this.popupMenus.syncActivePopupMenu())
81
+ }
82
+ closeActiveGlobalUi() {
83
+ if (this.popupMenus.syncActivePopupMenu()) {
80
84
  this.popupMenus.cancelActivePopupMenu();
85
+ return true;
86
+ }
87
+ return this.host.dismissActiveDialog?.() ?? false;
81
88
  }
82
89
  async abortStreamingSession(runtime, options) {
83
90
  const session = runtime.session;
@@ -0,0 +1,25 @@
1
+ export declare const PIX_LOG_MAX_LINES = 1000;
2
+ export type PixLogLevel = "debug" | "info" | "warn" | "error";
3
+ export type PixLogDetails = Record<string, unknown>;
4
+ export type PixFileLoggerOptions = {
5
+ logPath?: string;
6
+ maxLines?: number;
7
+ };
8
+ export declare function getPixLogPath(homeDir?: string): string;
9
+ export declare class PixFileLogger {
10
+ readonly logPath: string;
11
+ private readonly maxLines;
12
+ private pending;
13
+ constructor(options?: PixFileLoggerOptions);
14
+ log(level: PixLogLevel, event: string, details?: PixLogDetails): Promise<void>;
15
+ debug(event: string, details?: PixLogDetails): Promise<void>;
16
+ info(event: string, details?: PixLogDetails): Promise<void>;
17
+ warn(event: string, details?: PixLogDetails): Promise<void>;
18
+ error(event: string, details?: PixLogDetails): Promise<void>;
19
+ flush(): Promise<void>;
20
+ private formatLine;
21
+ private writeLine;
22
+ }
23
+ export declare const pixLogger: PixFileLogger;
24
+ export declare function logPixEvent(level: PixLogLevel, event: string, details?: PixLogDetails): void;
25
+ export declare function trimLogFile(logPath: string, maxLines?: number): Promise<void>;
@@ -0,0 +1,90 @@
1
+ import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ export const PIX_LOG_MAX_LINES = 1000;
5
+ export function getPixLogPath(homeDir = homedir()) {
6
+ return join(homeDir, ".config", "pi", "pix.log");
7
+ }
8
+ export class PixFileLogger {
9
+ logPath;
10
+ maxLines;
11
+ pending = Promise.resolve();
12
+ constructor(options = {}) {
13
+ this.logPath = options.logPath ?? getPixLogPath();
14
+ this.maxLines = Math.max(1, Math.floor(options.maxLines ?? PIX_LOG_MAX_LINES));
15
+ }
16
+ log(level, event, details = {}) {
17
+ const line = this.formatLine(level, event, details);
18
+ this.pending = this.pending
19
+ .then(() => this.writeLine(line))
20
+ .catch(() => undefined);
21
+ return this.pending;
22
+ }
23
+ debug(event, details) {
24
+ return this.log("debug", event, details);
25
+ }
26
+ info(event, details) {
27
+ return this.log("info", event, details);
28
+ }
29
+ warn(event, details) {
30
+ return this.log("warn", event, details);
31
+ }
32
+ error(event, details) {
33
+ return this.log("error", event, details);
34
+ }
35
+ async flush() {
36
+ await this.pending;
37
+ }
38
+ formatLine(level, event, details) {
39
+ return `${new Date().toISOString()} ${level.toUpperCase()} ${sanitizeToken(event)} ${safeJson(details)}\n`;
40
+ }
41
+ async writeLine(line) {
42
+ await mkdir(dirname(this.logPath), { recursive: true });
43
+ await appendFile(this.logPath, line, "utf8");
44
+ await trimLogFile(this.logPath, this.maxLines);
45
+ }
46
+ }
47
+ export const pixLogger = new PixFileLogger();
48
+ export function logPixEvent(level, event, details) {
49
+ void pixLogger.log(level, event, details);
50
+ }
51
+ export async function trimLogFile(logPath, maxLines = PIX_LOG_MAX_LINES) {
52
+ const text = await readFile(logPath, "utf8").catch((error) => {
53
+ if (isNodeError(error) && error.code === "ENOENT")
54
+ return undefined;
55
+ throw error;
56
+ });
57
+ if (text === undefined)
58
+ return;
59
+ const lines = text.split(/\r?\n/u);
60
+ if (lines.at(-1) === "")
61
+ lines.pop();
62
+ if (lines.length <= maxLines)
63
+ return;
64
+ await writeFile(logPath, `${lines.slice(-maxLines).join("\n")}\n`, "utf8");
65
+ }
66
+ function sanitizeToken(value) {
67
+ return value
68
+ .replace(/[\t\r\n]+/gu, " ")
69
+ .replace(/\s+/gu, "_")
70
+ .replace(/[^\w:.-]+/gu, "_")
71
+ .replace(/^_+|_+$/gu, "")
72
+ .slice(0, 120) || "event";
73
+ }
74
+ function safeJson(value) {
75
+ try {
76
+ return JSON.stringify(value, (_key, item) => {
77
+ if (item instanceof Error)
78
+ return { name: item.name, message: item.message, stack: item.stack };
79
+ if (typeof item === "bigint")
80
+ return item.toString();
81
+ return item;
82
+ });
83
+ }
84
+ catch (error) {
85
+ return JSON.stringify({ logSerializationError: error instanceof Error ? error.message : String(error) });
86
+ }
87
+ }
88
+ function isNodeError(error) {
89
+ return error instanceof Error && "code" in error;
90
+ }