@oh-my-pi/pi-coding-agent 13.14.2 → 13.15.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.
- package/CHANGELOG.md +140 -0
- package/package.json +10 -8
- package/src/autoresearch/command-initialize.md +34 -0
- package/src/autoresearch/command-resume.md +17 -0
- package/src/autoresearch/contract.ts +332 -0
- package/src/autoresearch/dashboard.ts +447 -0
- package/src/autoresearch/git.ts +243 -0
- package/src/autoresearch/helpers.ts +458 -0
- package/src/autoresearch/index.ts +693 -0
- package/src/autoresearch/prompt.md +227 -0
- package/src/autoresearch/resume-message.md +16 -0
- package/src/autoresearch/state.ts +386 -0
- package/src/autoresearch/tools/init-experiment.ts +310 -0
- package/src/autoresearch/tools/log-experiment.ts +833 -0
- package/src/autoresearch/tools/run-experiment.ts +640 -0
- package/src/autoresearch/types.ts +218 -0
- package/src/cli/args.ts +8 -2
- package/src/cli/initial-message.ts +58 -0
- package/src/config/keybindings.ts +417 -212
- package/src/config/model-registry.ts +1 -0
- package/src/config/model-resolver.ts +57 -9
- package/src/config/settings-schema.ts +38 -10
- package/src/config/settings.ts +1 -4
- package/src/export/html/template.css +43 -13
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.html +1 -0
- package/src/export/html/template.js +107 -0
- package/src/extensibility/extensions/types.ts +31 -8
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/lsp/index.ts +1 -1
- package/src/main.ts +44 -44
- package/src/mcp/oauth-discovery.ts +1 -1
- package/src/modes/acp/acp-agent.ts +957 -0
- package/src/modes/acp/acp-event-mapper.ts +531 -0
- package/src/modes/acp/acp-mode.ts +13 -0
- package/src/modes/acp/index.ts +2 -0
- package/src/modes/components/agent-dashboard.ts +5 -4
- package/src/modes/components/custom-editor.ts +47 -47
- package/src/modes/components/extensions/extension-dashboard.ts +2 -1
- package/src/modes/components/history-search.ts +2 -1
- package/src/modes/components/hook-editor.ts +2 -1
- package/src/modes/components/hook-input.ts +8 -7
- package/src/modes/components/hook-selector.ts +15 -10
- package/src/modes/components/keybinding-hints.ts +9 -9
- package/src/modes/components/login-dialog.ts +3 -3
- package/src/modes/components/mcp-add-wizard.ts +2 -1
- package/src/modes/components/model-selector.ts +14 -3
- package/src/modes/components/oauth-selector.ts +2 -1
- package/src/modes/components/session-selector.ts +2 -1
- package/src/modes/components/settings-selector.ts +2 -1
- package/src/modes/components/status-line-segment-editor.ts +2 -1
- package/src/modes/components/tree-selector.ts +3 -2
- package/src/modes/components/user-message-selector.ts +3 -8
- package/src/modes/components/user-message.ts +16 -0
- package/src/modes/controllers/extension-ui-controller.ts +89 -4
- package/src/modes/controllers/input-controller.ts +29 -23
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/modes/index.ts +1 -0
- package/src/modes/interactive-mode.ts +17 -5
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/prompt-action-autocomplete.ts +7 -7
- package/src/modes/rpc/rpc-mode.ts +7 -2
- package/src/modes/rpc/rpc-types.ts +1 -0
- package/src/modes/theme/theme.ts +53 -44
- package/src/modes/types.ts +9 -2
- package/src/modes/utils/hotkeys-markdown.ts +19 -19
- package/src/modes/utils/keybinding-matchers.ts +21 -0
- package/src/modes/utils/ui-helpers.ts +1 -1
- package/src/patch/hashline.ts +139 -127
- package/src/patch/index.ts +77 -59
- package/src/patch/shared.ts +19 -11
- package/src/prompts/tools/hashline.md +43 -116
- package/src/sdk.ts +34 -17
- package/src/session/agent-session.ts +123 -30
- package/src/session/session-manager.ts +32 -31
- package/src/tools/ask.ts +56 -30
- package/src/tools/bash-interceptor.ts +1 -39
- package/src/tools/bash-skill-urls.ts +1 -1
- package/src/tools/browser.ts +1 -1
- package/src/tools/gemini-image.ts +1 -1
- package/src/tools/resolve.ts +1 -1
- package/src/utils/child-process.ts +88 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Component, OverlayHandle, TUI } from "@oh-my-pi/pi-tui";
|
|
2
|
-
import { Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { Container, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
3
3
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { KeybindingsManager } from "../../config/keybindings";
|
|
5
5
|
import type {
|
|
@@ -9,6 +9,9 @@ import type {
|
|
|
9
9
|
ExtensionError,
|
|
10
10
|
ExtensionUIContext,
|
|
11
11
|
ExtensionUIDialogOptions,
|
|
12
|
+
ExtensionUiComponent,
|
|
13
|
+
ExtensionWidgetContent,
|
|
14
|
+
ExtensionWidgetOptions,
|
|
12
15
|
TerminalInputHandler,
|
|
13
16
|
} from "../../extensibility/extensions";
|
|
14
17
|
import { HookEditorComponent } from "../../modes/components/hook-editor";
|
|
@@ -18,8 +21,12 @@ import { getAvailableThemesWithPaths, getThemeByName, setTheme, type Theme, them
|
|
|
18
21
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
19
22
|
import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
|
|
20
23
|
|
|
24
|
+
const MAX_WIDGET_LINES = 10;
|
|
25
|
+
|
|
21
26
|
export class ExtensionUiController {
|
|
22
27
|
#extensionTerminalInputUnsubscribers = new Set<() => void>();
|
|
28
|
+
#hookWidgetsAbove = new Map<string, ExtensionUiComponent>();
|
|
29
|
+
#hookWidgetsBelow = new Map<string, ExtensionUiComponent>();
|
|
23
30
|
constructor(private ctx: InteractiveModeContext) {}
|
|
24
31
|
|
|
25
32
|
/**
|
|
@@ -35,7 +42,7 @@ export class ExtensionUiController {
|
|
|
35
42
|
onTerminalInput: handler => this.addExtensionTerminalInputListener(handler),
|
|
36
43
|
setStatus: (key, text) => this.setHookStatus(key, text),
|
|
37
44
|
setWorkingMessage: message => this.ctx.setWorkingMessage(message),
|
|
38
|
-
setWidget: (key, content) => this.setHookWidget(key, content),
|
|
45
|
+
setWidget: (key, content, options) => this.setHookWidget(key, content, options),
|
|
39
46
|
setTitle: title => setTerminalTitle(title),
|
|
40
47
|
custom: (factory, options) => this.showHookCustom(factory, options),
|
|
41
48
|
setEditorText: text => this.ctx.editor.setText(text),
|
|
@@ -150,6 +157,7 @@ export class ExtensionUiController {
|
|
|
150
157
|
|
|
151
158
|
// Create new session
|
|
152
159
|
this.clearExtensionTerminalInputListeners();
|
|
160
|
+
this.clearHookWidgets();
|
|
153
161
|
const success = await this.ctx.session.newSession({ parentSession: options?.parentSession });
|
|
154
162
|
if (!success) {
|
|
155
163
|
return { cancelled: true };
|
|
@@ -227,6 +235,7 @@ export class ExtensionUiController {
|
|
|
227
235
|
await this.ctx.executeCompaction(instructionsOrOptions, false);
|
|
228
236
|
},
|
|
229
237
|
switchSession: async sessionPath => {
|
|
238
|
+
this.clearHookWidgets();
|
|
230
239
|
const result = await this.ctx.session.switchSession(sessionPath);
|
|
231
240
|
if (!result) {
|
|
232
241
|
return { cancelled: true };
|
|
@@ -252,11 +261,73 @@ export class ExtensionUiController {
|
|
|
252
261
|
});
|
|
253
262
|
}
|
|
254
263
|
|
|
255
|
-
setHookWidget(key: string, content:
|
|
256
|
-
|
|
264
|
+
setHookWidget(key: string, content: ExtensionWidgetContent, options?: ExtensionWidgetOptions): void {
|
|
265
|
+
const placement = options?.placement ?? "aboveEditor";
|
|
266
|
+
this.#removeHookWidget(this.#hookWidgetsAbove, key);
|
|
267
|
+
this.#removeHookWidget(this.#hookWidgetsBelow, key);
|
|
268
|
+
|
|
269
|
+
if (content === undefined) {
|
|
270
|
+
this.#rebuildHookWidgets();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const target = placement === "belowEditor" ? this.#hookWidgetsBelow : this.#hookWidgetsAbove;
|
|
275
|
+
target.set(key, this.#createHookWidget(content));
|
|
276
|
+
this.#rebuildHookWidgets();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#removeHookWidget(widgets: Map<string, ExtensionUiComponent>, key: string): void {
|
|
280
|
+
const existing = widgets.get(key);
|
|
281
|
+
existing?.dispose?.();
|
|
282
|
+
widgets.delete(key);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#createHookWidget(content: ExtensionWidgetContent): ExtensionUiComponent {
|
|
286
|
+
if (Array.isArray(content)) {
|
|
287
|
+
const container = new Container();
|
|
288
|
+
for (const line of content.slice(0, MAX_WIDGET_LINES)) {
|
|
289
|
+
container.addChild(new Text(line, 1, 0));
|
|
290
|
+
}
|
|
291
|
+
if (content.length > MAX_WIDGET_LINES) {
|
|
292
|
+
container.addChild(new Text(theme.fg("muted", "... (widget truncated)"), 1, 0));
|
|
293
|
+
}
|
|
294
|
+
return container;
|
|
295
|
+
}
|
|
296
|
+
if (content === undefined) {
|
|
297
|
+
throw new Error("Widget content missing");
|
|
298
|
+
}
|
|
299
|
+
return content(this.ctx.ui, theme);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#rebuildHookWidgets(): void {
|
|
303
|
+
this.#renderHookWidgetContainer(this.ctx.hookWidgetContainerAbove, this.#hookWidgetsAbove, true, true);
|
|
304
|
+
this.#renderHookWidgetContainer(this.ctx.hookWidgetContainerBelow, this.#hookWidgetsBelow, false, false);
|
|
257
305
|
this.ctx.ui.requestRender();
|
|
258
306
|
}
|
|
259
307
|
|
|
308
|
+
#renderHookWidgetContainer(
|
|
309
|
+
container: Container,
|
|
310
|
+
widgets: Map<string, ExtensionUiComponent>,
|
|
311
|
+
spacerWhenEmpty: boolean,
|
|
312
|
+
leadingSpacer: boolean,
|
|
313
|
+
): void {
|
|
314
|
+
container.clear();
|
|
315
|
+
|
|
316
|
+
if (widgets.size === 0) {
|
|
317
|
+
if (spacerWhenEmpty) {
|
|
318
|
+
container.addChild(new Spacer(1));
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (leadingSpacer) {
|
|
324
|
+
container.addChild(new Spacer(1));
|
|
325
|
+
}
|
|
326
|
+
for (const widget of widgets.values()) {
|
|
327
|
+
container.addChild(widget);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
260
331
|
initializeHookRunner(uiContext: ExtensionUIContext, _hasUI: boolean): void {
|
|
261
332
|
const extensionRunner = this.ctx.session.extensionRunner;
|
|
262
333
|
if (!extensionRunner) {
|
|
@@ -353,6 +424,7 @@ export class ExtensionUiController {
|
|
|
353
424
|
|
|
354
425
|
// Create new session
|
|
355
426
|
this.clearExtensionTerminalInputListeners();
|
|
427
|
+
this.clearHookWidgets();
|
|
356
428
|
const success = await this.ctx.session.newSession({ parentSession: options?.parentSession });
|
|
357
429
|
if (!success) {
|
|
358
430
|
return { cancelled: true };
|
|
@@ -432,6 +504,7 @@ export class ExtensionUiController {
|
|
|
432
504
|
if (this.ctx.isBackgrounded) {
|
|
433
505
|
return { cancelled: true };
|
|
434
506
|
}
|
|
507
|
+
this.clearHookWidgets();
|
|
435
508
|
const result = await this.ctx.session.switchSession(sessionPath);
|
|
436
509
|
if (!result) {
|
|
437
510
|
return { cancelled: true };
|
|
@@ -828,6 +901,18 @@ export class ExtensionUiController {
|
|
|
828
901
|
};
|
|
829
902
|
}
|
|
830
903
|
|
|
904
|
+
clearHookWidgets(): void {
|
|
905
|
+
for (const widget of this.#hookWidgetsAbove.values()) {
|
|
906
|
+
widget.dispose?.();
|
|
907
|
+
}
|
|
908
|
+
for (const widget of this.#hookWidgetsBelow.values()) {
|
|
909
|
+
widget.dispose?.();
|
|
910
|
+
}
|
|
911
|
+
this.#hookWidgetsAbove.clear();
|
|
912
|
+
this.#hookWidgetsBelow.clear();
|
|
913
|
+
this.#rebuildHookWidgets();
|
|
914
|
+
}
|
|
915
|
+
|
|
831
916
|
clearExtensionTerminalInputListeners(): void {
|
|
832
917
|
for (const unsubscribe of this.#extensionTerminalInputUnsubscribers) {
|
|
833
918
|
unsubscribe();
|
|
@@ -26,7 +26,7 @@ export class InputController {
|
|
|
26
26
|
constructor(private ctx: InteractiveModeContext) {}
|
|
27
27
|
|
|
28
28
|
setupKeyHandlers(): void {
|
|
29
|
-
this.ctx.editor.setActionKeys("interrupt", this.ctx.keybindings.getKeys("interrupt"));
|
|
29
|
+
this.ctx.editor.setActionKeys("app.interrupt", this.ctx.keybindings.getKeys("app.interrupt"));
|
|
30
30
|
this.ctx.editor.shouldBypassAutocompleteOnEscape = () =>
|
|
31
31
|
Boolean(
|
|
32
32
|
this.ctx.loadingAnimation ||
|
|
@@ -83,68 +83,74 @@ export class InputController {
|
|
|
83
83
|
}
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
-
this.ctx.editor.setActionKeys("clear", this.ctx.keybindings.getKeys("clear"));
|
|
86
|
+
this.ctx.editor.setActionKeys("app.clear", this.ctx.keybindings.getKeys("app.clear"));
|
|
87
87
|
this.ctx.editor.onClear = () => this.handleCtrlC();
|
|
88
|
-
this.ctx.editor.setActionKeys("exit", this.ctx.keybindings.getKeys("exit"));
|
|
88
|
+
this.ctx.editor.setActionKeys("app.exit", this.ctx.keybindings.getKeys("app.exit"));
|
|
89
89
|
this.ctx.editor.onExit = () => this.handleCtrlD();
|
|
90
|
-
this.ctx.editor.setActionKeys("suspend", this.ctx.keybindings.getKeys("suspend"));
|
|
90
|
+
this.ctx.editor.setActionKeys("app.suspend", this.ctx.keybindings.getKeys("app.suspend"));
|
|
91
91
|
this.ctx.editor.onSuspend = () => this.handleCtrlZ();
|
|
92
|
-
this.ctx.editor.setActionKeys("
|
|
92
|
+
this.ctx.editor.setActionKeys("app.thinking.cycle", this.ctx.keybindings.getKeys("app.thinking.cycle"));
|
|
93
93
|
this.ctx.editor.onCycleThinkingLevel = () => this.cycleThinkingLevel();
|
|
94
|
-
this.ctx.editor.setActionKeys("
|
|
94
|
+
this.ctx.editor.setActionKeys("app.model.cycleForward", this.ctx.keybindings.getKeys("app.model.cycleForward"));
|
|
95
95
|
this.ctx.editor.onCycleModelForward = () => this.cycleRoleModel();
|
|
96
|
-
this.ctx.editor.setActionKeys("
|
|
96
|
+
this.ctx.editor.setActionKeys("app.model.cycleBackward", this.ctx.keybindings.getKeys("app.model.cycleBackward"));
|
|
97
97
|
this.ctx.editor.onCycleModelBackward = () => this.cycleRoleModel({ temporary: true });
|
|
98
98
|
this.ctx.editor.onQuickSelectModel = () => this.ctx.showModelSelector({ temporaryOnly: true });
|
|
99
99
|
|
|
100
100
|
// Global debug handler on TUI (works regardless of focus)
|
|
101
101
|
this.ctx.ui.onDebug = () => this.ctx.showDebugSelector();
|
|
102
|
-
this.ctx.editor.setActionKeys("
|
|
102
|
+
this.ctx.editor.setActionKeys("app.model.select", this.ctx.keybindings.getKeys("app.model.select"));
|
|
103
103
|
this.ctx.editor.onSelectModel = () => this.ctx.showModelSelector();
|
|
104
|
-
this.ctx.editor.setActionKeys("
|
|
104
|
+
this.ctx.editor.setActionKeys("app.history.search", this.ctx.keybindings.getKeys("app.history.search"));
|
|
105
105
|
this.ctx.editor.onHistorySearch = () => this.ctx.showHistorySearch();
|
|
106
|
-
this.ctx.editor.setActionKeys("
|
|
106
|
+
this.ctx.editor.setActionKeys("app.thinking.toggle", this.ctx.keybindings.getKeys("app.thinking.toggle"));
|
|
107
107
|
this.ctx.editor.onToggleThinking = () => this.ctx.toggleThinkingBlockVisibility();
|
|
108
|
-
this.ctx.editor.setActionKeys("
|
|
108
|
+
this.ctx.editor.setActionKeys("app.editor.external", this.ctx.keybindings.getKeys("app.editor.external"));
|
|
109
109
|
this.ctx.editor.onExternalEditor = () => void this.openExternalEditor();
|
|
110
110
|
this.ctx.editor.onShowHotkeys = () => this.ctx.handleHotkeysCommand();
|
|
111
|
-
this.ctx.editor.setActionKeys(
|
|
111
|
+
this.ctx.editor.setActionKeys(
|
|
112
|
+
"app.clipboard.pasteImage",
|
|
113
|
+
this.ctx.keybindings.getKeys("app.clipboard.pasteImage"),
|
|
114
|
+
);
|
|
112
115
|
this.ctx.editor.onPasteImage = () => this.handleImagePaste();
|
|
113
|
-
this.ctx.editor.setActionKeys(
|
|
116
|
+
this.ctx.editor.setActionKeys(
|
|
117
|
+
"app.clipboard.copyPrompt",
|
|
118
|
+
this.ctx.keybindings.getKeys("app.clipboard.copyPrompt"),
|
|
119
|
+
);
|
|
114
120
|
this.ctx.editor.onCopyPrompt = () => this.handleCopyPrompt();
|
|
115
|
-
this.ctx.editor.setActionKeys("
|
|
121
|
+
this.ctx.editor.setActionKeys("app.tools.expand", this.ctx.keybindings.getKeys("app.tools.expand"));
|
|
116
122
|
this.ctx.editor.onExpandTools = () => this.toggleToolOutputExpansion();
|
|
117
|
-
this.ctx.editor.setActionKeys("dequeue", this.ctx.keybindings.getKeys("dequeue"));
|
|
123
|
+
this.ctx.editor.setActionKeys("app.message.dequeue", this.ctx.keybindings.getKeys("app.message.dequeue"));
|
|
118
124
|
this.ctx.editor.onDequeue = () => this.handleDequeue();
|
|
119
125
|
|
|
120
126
|
this.ctx.editor.clearCustomKeyHandlers();
|
|
121
127
|
// Wire up extension shortcuts
|
|
122
128
|
this.registerExtensionShortcuts();
|
|
123
129
|
|
|
124
|
-
const planModeKeys = this.ctx.keybindings.getKeys("
|
|
130
|
+
const planModeKeys = this.ctx.keybindings.getKeys("app.plan.toggle");
|
|
125
131
|
for (const key of planModeKeys) {
|
|
126
132
|
this.ctx.editor.setCustomKeyHandler(key, () => void this.ctx.handlePlanModeCommand());
|
|
127
133
|
}
|
|
128
134
|
|
|
129
|
-
for (const key of this.ctx.keybindings.getKeys("
|
|
135
|
+
for (const key of this.ctx.keybindings.getKeys("app.session.new")) {
|
|
130
136
|
this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.handleClearCommand());
|
|
131
137
|
}
|
|
132
|
-
for (const key of this.ctx.keybindings.getKeys("tree")) {
|
|
138
|
+
for (const key of this.ctx.keybindings.getKeys("app.session.tree")) {
|
|
133
139
|
this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showTreeSelector());
|
|
134
140
|
}
|
|
135
|
-
for (const key of this.ctx.keybindings.getKeys("fork")) {
|
|
141
|
+
for (const key of this.ctx.keybindings.getKeys("app.session.fork")) {
|
|
136
142
|
this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showUserMessageSelector());
|
|
137
143
|
}
|
|
138
|
-
for (const key of this.ctx.keybindings.getKeys("resume")) {
|
|
144
|
+
for (const key of this.ctx.keybindings.getKeys("app.session.resume")) {
|
|
139
145
|
this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showSessionSelector());
|
|
140
146
|
}
|
|
141
|
-
for (const key of this.ctx.keybindings.getKeys("followUp")) {
|
|
147
|
+
for (const key of this.ctx.keybindings.getKeys("app.message.followUp")) {
|
|
142
148
|
this.ctx.editor.setCustomKeyHandler(key, () => void this.handleFollowUp());
|
|
143
149
|
}
|
|
144
|
-
for (const key of this.ctx.keybindings.getKeys("
|
|
150
|
+
for (const key of this.ctx.keybindings.getKeys("app.stt.toggle")) {
|
|
145
151
|
this.ctx.editor.setCustomKeyHandler(key, () => void this.ctx.handleSTTToggle());
|
|
146
152
|
}
|
|
147
|
-
for (const key of this.ctx.keybindings.getKeys("copyLine")) {
|
|
153
|
+
for (const key of this.ctx.keybindings.getKeys("app.clipboard.copyLine")) {
|
|
148
154
|
this.ctx.editor.setCustomKeyHandler(key, () => this.handleCopyCurrentLine());
|
|
149
155
|
}
|
|
150
156
|
|
|
@@ -660,7 +660,7 @@ export class MCPCommandController {
|
|
|
660
660
|
}
|
|
661
661
|
|
|
662
662
|
async #removeManagedOAuthCredential(credentialId: string | undefined): Promise<void> {
|
|
663
|
-
if (!credentialId
|
|
663
|
+
if (!credentialId?.startsWith("mcp_oauth_")) return;
|
|
664
664
|
await this.ctx.session.modelRegistry.authStorage.remove(credentialId);
|
|
665
665
|
}
|
|
666
666
|
|
package/src/modes/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { postmortem } from "@oh-my-pi/pi-utils";
|
|
|
4
4
|
/**
|
|
5
5
|
* Run modes for the coding agent.
|
|
6
6
|
*/
|
|
7
|
+
export { runAcpMode } from "./acp";
|
|
7
8
|
export { InteractiveMode, type InteractiveModeOptions } from "./interactive-mode";
|
|
8
9
|
export { type PrintModeOptions, runPrintMode } from "./print-mode";
|
|
9
10
|
export { type ModelInfo, RpcClient, type RpcClientOptions, type RpcEventListener } from "./rpc/rpc-client";
|
|
@@ -12,7 +12,12 @@ import chalk from "chalk";
|
|
|
12
12
|
import { KeybindingsManager } from "../config/keybindings";
|
|
13
13
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
14
14
|
import { type Settings, settings } from "../config/settings";
|
|
15
|
-
import type {
|
|
15
|
+
import type {
|
|
16
|
+
ExtensionUIContext,
|
|
17
|
+
ExtensionUIDialogOptions,
|
|
18
|
+
ExtensionWidgetContent,
|
|
19
|
+
ExtensionWidgetOptions,
|
|
20
|
+
} from "../extensibility/extensions";
|
|
16
21
|
import type { CompactOptions } from "../extensibility/extensions/types";
|
|
17
22
|
import { BUILTIN_SLASH_COMMANDS, loadSlashCommands } from "../extensibility/slash-commands";
|
|
18
23
|
import { resolveLocalUrlToPath } from "../internal-urls";
|
|
@@ -93,6 +98,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
93
98
|
btwContainer: Container;
|
|
94
99
|
editor: CustomEditor;
|
|
95
100
|
editorContainer: Container;
|
|
101
|
+
hookWidgetContainerAbove: Container;
|
|
102
|
+
hookWidgetContainerBelow: Container;
|
|
96
103
|
statusLine: StatusLineComponent;
|
|
97
104
|
|
|
98
105
|
isInitialized = false;
|
|
@@ -216,6 +223,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
216
223
|
} catch (error) {
|
|
217
224
|
logger.warn("History storage unavailable", { error: String(error) });
|
|
218
225
|
}
|
|
226
|
+
this.hookWidgetContainerAbove = new Container();
|
|
227
|
+
this.hookWidgetContainerAbove.addChild(new Spacer(1));
|
|
228
|
+
this.hookWidgetContainerBelow = new Container();
|
|
219
229
|
this.editorContainer = new Container();
|
|
220
230
|
this.editorContainer.addChild(this.editor);
|
|
221
231
|
this.statusLine = new StatusLineComponent(session);
|
|
@@ -263,7 +273,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
263
273
|
async init(): Promise<void> {
|
|
264
274
|
if (this.isInitialized) return;
|
|
265
275
|
|
|
266
|
-
this.keybindings =
|
|
276
|
+
this.keybindings = logger.time("InteractiveMode.init:keybindings", () => KeybindingsManager.create());
|
|
267
277
|
|
|
268
278
|
// Register session manager flush for signal handlers (SIGINT, SIGTERM, SIGHUP)
|
|
269
279
|
this.#cleanupUnsubscribe = postmortem.register("session-manager-flush", () => this.sessionManager.flush());
|
|
@@ -329,8 +339,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
329
339
|
this.ui.addChild(this.todoContainer);
|
|
330
340
|
this.ui.addChild(this.btwContainer);
|
|
331
341
|
this.ui.addChild(this.statusLine); // Only renders hook statuses (main status in editor border)
|
|
332
|
-
this.ui.addChild(
|
|
342
|
+
this.ui.addChild(this.hookWidgetContainerAbove);
|
|
333
343
|
this.ui.addChild(this.editorContainer);
|
|
344
|
+
this.ui.addChild(this.hookWidgetContainerBelow);
|
|
334
345
|
this.ui.setFocus(this.editor);
|
|
335
346
|
|
|
336
347
|
this.#inputController.setupKeyHandlers();
|
|
@@ -837,6 +848,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
837
848
|
this.#sttController = undefined;
|
|
838
849
|
}
|
|
839
850
|
this.#extensionUiController.clearExtensionTerminalInputListeners();
|
|
851
|
+
this.#extensionUiController.clearHookWidgets();
|
|
840
852
|
this.statusLine.dispose();
|
|
841
853
|
if (this.#resizeHandler) {
|
|
842
854
|
process.stdout.removeListener("resize", this.#resizeHandler);
|
|
@@ -1359,8 +1371,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1359
1371
|
return this.#extensionUiController.emitCustomToolSessionEvent(reason, previousSessionFile);
|
|
1360
1372
|
}
|
|
1361
1373
|
|
|
1362
|
-
setHookWidget(key: string, content:
|
|
1363
|
-
this.#extensionUiController.setHookWidget(key, content);
|
|
1374
|
+
setHookWidget(key: string, content: ExtensionWidgetContent, options?: ExtensionWidgetOptions): void {
|
|
1375
|
+
this.#extensionUiController.setHookWidget(key, content, options);
|
|
1364
1376
|
}
|
|
1365
1377
|
|
|
1366
1378
|
setHookStatus(key: string, text: string | undefined): void {
|
package/src/modes/print-mode.ts
CHANGED
|
@@ -146,7 +146,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
146
146
|
});
|
|
147
147
|
|
|
148
148
|
// Send initial message with attachments
|
|
149
|
-
if (initialMessage) {
|
|
149
|
+
if (initialMessage !== undefined) {
|
|
150
150
|
await session.prompt(initialMessage, { images: initialImages });
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
type AutocompleteItem,
|
|
3
3
|
type AutocompleteProvider,
|
|
4
4
|
CombinedAutocompleteProvider,
|
|
5
|
-
|
|
5
|
+
getKeybindings,
|
|
6
6
|
type SlashCommand,
|
|
7
7
|
} from "@oh-my-pi/pi-tui";
|
|
8
8
|
import { formatKeyHints, type KeybindingsManager } from "../config/keybindings";
|
|
@@ -174,26 +174,26 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
174
174
|
export function createPromptActionAutocompleteProvider(
|
|
175
175
|
options: PromptActionAutocompleteOptions,
|
|
176
176
|
): PromptActionAutocompleteProvider {
|
|
177
|
-
const editorKeybindings =
|
|
177
|
+
const editorKeybindings = getKeybindings();
|
|
178
178
|
const actions: PromptActionDefinition[] = [
|
|
179
179
|
{
|
|
180
180
|
id: "copy-line",
|
|
181
181
|
label: "Copy current line",
|
|
182
|
-
description: formatKeyHints(options.keybindings.getKeys("copyLine")),
|
|
182
|
+
description: formatKeyHints(options.keybindings.getKeys("app.clipboard.copyLine")),
|
|
183
183
|
keywords: ["copy", "line", "clipboard", "current"],
|
|
184
184
|
execute: options.copyCurrentLine,
|
|
185
185
|
},
|
|
186
186
|
{
|
|
187
187
|
id: "copy-prompt",
|
|
188
188
|
label: "Copy whole prompt",
|
|
189
|
-
description: formatKeyHints(options.keybindings.getKeys("copyPrompt")),
|
|
189
|
+
description: formatKeyHints(options.keybindings.getKeys("app.clipboard.copyPrompt")),
|
|
190
190
|
keywords: ["copy", "prompt", "clipboard", "message"],
|
|
191
191
|
execute: options.copyPrompt,
|
|
192
192
|
},
|
|
193
193
|
{
|
|
194
194
|
id: "undo",
|
|
195
195
|
label: "Undo",
|
|
196
|
-
description: formatKeyHints(editorKeybindings.getKeys("undo")),
|
|
196
|
+
description: formatKeyHints(editorKeybindings.getKeys("tui.editor.undo")),
|
|
197
197
|
keywords: ["undo", "revert", "edit", "history"],
|
|
198
198
|
execute: options.undo,
|
|
199
199
|
},
|
|
@@ -214,14 +214,14 @@ export function createPromptActionAutocompleteProvider(
|
|
|
214
214
|
{
|
|
215
215
|
id: "cursor-line-start",
|
|
216
216
|
label: "Move cursor to beginning of line",
|
|
217
|
-
description: formatKeyHints(editorKeybindings.getKeys("cursorLineStart")),
|
|
217
|
+
description: formatKeyHints(editorKeybindings.getKeys("tui.editor.cursorLineStart")),
|
|
218
218
|
keywords: ["move", "cursor", "line", "start", "beginning", "home"],
|
|
219
219
|
execute: options.moveCursorToLineStart,
|
|
220
220
|
},
|
|
221
221
|
{
|
|
222
222
|
id: "cursor-line-end",
|
|
223
223
|
label: "Move cursor to end of line",
|
|
224
|
-
description: formatKeyHints(editorKeybindings.getKeys("cursorLineEnd")),
|
|
224
|
+
description: formatKeyHints(editorKeybindings.getKeys("tui.editor.cursorLineEnd")),
|
|
225
225
|
keywords: ["move", "cursor", "line", "end"],
|
|
226
226
|
execute: options.moveCursorToLineEnd,
|
|
227
227
|
},
|
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
* - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
|
|
12
12
|
*/
|
|
13
13
|
import { readJsonl, Snowflake } from "@oh-my-pi/pi-utils";
|
|
14
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
ExtensionUIContext,
|
|
16
|
+
ExtensionUIDialogOptions,
|
|
17
|
+
ExtensionWidgetOptions,
|
|
18
|
+
} from "../../extensibility/extensions";
|
|
15
19
|
import { type Theme, theme } from "../../modes/theme/theme";
|
|
16
20
|
import type { AgentSession } from "../../session/agent-session";
|
|
17
21
|
import type {
|
|
@@ -198,7 +202,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
198
202
|
// Not supported in RPC mode
|
|
199
203
|
}
|
|
200
204
|
|
|
201
|
-
setWidget(key: string, content: unknown): void {
|
|
205
|
+
setWidget(key: string, content: unknown, options?: ExtensionWidgetOptions): void {
|
|
202
206
|
// Only support string arrays in RPC mode - factory functions are ignored
|
|
203
207
|
if (content === undefined || Array.isArray(content)) {
|
|
204
208
|
this.output({
|
|
@@ -207,6 +211,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
207
211
|
method: "setWidget",
|
|
208
212
|
widgetKey: key,
|
|
209
213
|
widgetLines: content as string[] | undefined,
|
|
214
|
+
widgetPlacement: options?.placement,
|
|
210
215
|
} as RpcExtensionUIRequest);
|
|
211
216
|
}
|
|
212
217
|
// Component factories are not supported in RPC mode - would need TUI access
|
|
@@ -215,6 +215,7 @@ export type RpcExtensionUIRequest =
|
|
|
215
215
|
method: "setWidget";
|
|
216
216
|
widgetKey: string;
|
|
217
217
|
widgetLines: string[] | undefined;
|
|
218
|
+
widgetPlacement?: "aboveEditor" | "belowEditor";
|
|
218
219
|
}
|
|
219
220
|
| { type: "extension_ui_request"; id: string; method: "setTitle"; title: string }
|
|
220
221
|
| { type: "extension_ui_request"; id: string; method: "set_editor_text"; text: string };
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -1679,6 +1679,7 @@ export function getCurrentThemeName(): string | undefined {
|
|
|
1679
1679
|
var currentSymbolPresetOverride: SymbolPreset | undefined;
|
|
1680
1680
|
var currentColorBlindMode: boolean = false;
|
|
1681
1681
|
var themeWatcher: fs.FSWatcher | undefined;
|
|
1682
|
+
var themeReloadTimer: NodeJS.Timeout | undefined;
|
|
1682
1683
|
var sigwinchHandler: (() => void) | undefined;
|
|
1683
1684
|
var autoDetectedTheme: boolean = false;
|
|
1684
1685
|
var autoDarkTheme: string = "dark";
|
|
@@ -1888,11 +1889,7 @@ export function isValidSymbolPreset(preset: string): preset is SymbolPreset {
|
|
|
1888
1889
|
}
|
|
1889
1890
|
|
|
1890
1891
|
async function startThemeWatcher(): Promise<void> {
|
|
1891
|
-
|
|
1892
|
-
if (themeWatcher) {
|
|
1893
|
-
themeWatcher.close();
|
|
1894
|
-
themeWatcher = undefined;
|
|
1895
|
-
}
|
|
1892
|
+
stopThemeWatcher();
|
|
1896
1893
|
|
|
1897
1894
|
// Only watch if it's a custom theme (not built-in)
|
|
1898
1895
|
if (!currentThemeName || currentThemeName === "dark" || currentThemeName === "light") {
|
|
@@ -1900,54 +1897,62 @@ async function startThemeWatcher(): Promise<void> {
|
|
|
1900
1897
|
}
|
|
1901
1898
|
|
|
1902
1899
|
const customThemesDir = getCustomThemesDir();
|
|
1903
|
-
const
|
|
1900
|
+
const watchedThemeName = currentThemeName;
|
|
1901
|
+
const watchedFileName = `${watchedThemeName}.json`;
|
|
1902
|
+
const themeFile = path.join(customThemesDir, watchedFileName);
|
|
1904
1903
|
|
|
1905
1904
|
// Only watch if the file exists
|
|
1906
1905
|
if (!fs.existsSync(themeFile)) {
|
|
1907
1906
|
return;
|
|
1908
1907
|
}
|
|
1909
1908
|
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
.then(loadedTheme => {
|
|
1933
|
-
theme = loadedTheme;
|
|
1934
|
-
if (onThemeChangeCallback) {
|
|
1935
|
-
onThemeChangeCallback();
|
|
1936
|
-
}
|
|
1937
|
-
})
|
|
1938
|
-
.catch(err => {
|
|
1939
|
-
logger.debug("Theme reload error during rename fallback", { error: String(err) });
|
|
1940
|
-
});
|
|
1941
|
-
if (themeWatcher) {
|
|
1942
|
-
themeWatcher.close();
|
|
1943
|
-
themeWatcher = undefined;
|
|
1944
|
-
}
|
|
1909
|
+
const scheduleReload = () => {
|
|
1910
|
+
if (themeReloadTimer) {
|
|
1911
|
+
clearTimeout(themeReloadTimer);
|
|
1912
|
+
}
|
|
1913
|
+
themeReloadTimer = setTimeout(() => {
|
|
1914
|
+
themeReloadTimer = undefined;
|
|
1915
|
+
|
|
1916
|
+
// Ignore stale timers after switching themes or stopping the watcher
|
|
1917
|
+
if (currentThemeName !== watchedThemeName) {
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// Keep the last successfully loaded theme active if the file is temporarily missing
|
|
1922
|
+
if (!fs.existsSync(themeFile)) {
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
loadTheme(watchedThemeName, getCurrentThemeOptions())
|
|
1927
|
+
.then(loadedTheme => {
|
|
1928
|
+
theme = loadedTheme;
|
|
1929
|
+
if (onThemeChangeCallback) {
|
|
1930
|
+
onThemeChangeCallback();
|
|
1945
1931
|
}
|
|
1946
|
-
}
|
|
1932
|
+
})
|
|
1933
|
+
.catch(() => {
|
|
1934
|
+
// Ignore errors (file might be in invalid state while being edited)
|
|
1935
|
+
});
|
|
1936
|
+
}, 100);
|
|
1937
|
+
};
|
|
1938
|
+
|
|
1939
|
+
try {
|
|
1940
|
+
themeWatcher = fs.watch(customThemesDir, (_eventType, filename) => {
|
|
1941
|
+
if (currentThemeName !== watchedThemeName) {
|
|
1942
|
+
return;
|
|
1947
1943
|
}
|
|
1944
|
+
if (!filename) {
|
|
1945
|
+
scheduleReload();
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
const changedFile = String(filename);
|
|
1949
|
+
if (changedFile !== watchedFileName) {
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
scheduleReload();
|
|
1948
1953
|
});
|
|
1949
|
-
} catch
|
|
1950
|
-
|
|
1954
|
+
} catch {
|
|
1955
|
+
// Ignore errors starting watcher
|
|
1951
1956
|
}
|
|
1952
1957
|
}
|
|
1953
1958
|
|
|
@@ -2023,6 +2028,10 @@ function stopSigwinchListener(): void {
|
|
|
2023
2028
|
}
|
|
2024
2029
|
|
|
2025
2030
|
export function stopThemeWatcher(): void {
|
|
2031
|
+
if (themeReloadTimer) {
|
|
2032
|
+
clearTimeout(themeReloadTimer);
|
|
2033
|
+
themeReloadTimer = undefined;
|
|
2034
|
+
}
|
|
2026
2035
|
if (themeWatcher) {
|
|
2027
2036
|
themeWatcher.close();
|
|
2028
2037
|
themeWatcher = undefined;
|
package/src/modes/types.ts
CHANGED
|
@@ -3,7 +3,12 @@ import type { AssistantMessage, ImageContent, Message, UsageReport } from "@oh-m
|
|
|
3
3
|
import type { Component, Container, Loader, Spacer, Text, TUI } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import type { KeybindingsManager } from "../config/keybindings";
|
|
5
5
|
import type { Settings } from "../config/settings";
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
ExtensionUIContext,
|
|
8
|
+
ExtensionUIDialogOptions,
|
|
9
|
+
ExtensionWidgetContent,
|
|
10
|
+
ExtensionWidgetOptions,
|
|
11
|
+
} from "../extensibility/extensions";
|
|
7
12
|
import type { CompactOptions } from "../extensibility/extensions/types";
|
|
8
13
|
import type { MCPManager } from "../mcp";
|
|
9
14
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
@@ -59,6 +64,8 @@ export interface InteractiveModeContext {
|
|
|
59
64
|
btwContainer: Container;
|
|
60
65
|
editor: CustomEditor;
|
|
61
66
|
editorContainer: Container;
|
|
67
|
+
hookWidgetContainerAbove: Container;
|
|
68
|
+
hookWidgetContainerBelow: Container;
|
|
62
69
|
statusLine: StatusLineComponent;
|
|
63
70
|
|
|
64
71
|
// Session access
|
|
@@ -226,7 +233,7 @@ export interface InteractiveModeContext {
|
|
|
226
233
|
reason: "start" | "switch" | "branch" | "tree" | "shutdown",
|
|
227
234
|
previousSessionFile?: string,
|
|
228
235
|
): Promise<void>;
|
|
229
|
-
setHookWidget(key: string, content:
|
|
236
|
+
setHookWidget(key: string, content: ExtensionWidgetContent, options?: ExtensionWidgetOptions): void;
|
|
230
237
|
setHookStatus(key: string, text: string | undefined): void;
|
|
231
238
|
showHookSelector(
|
|
232
239
|
title: string,
|