@oh-my-pi/pi-coding-agent 11.2.3 → 11.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +100 -0
- package/examples/extensions/plan-mode.ts +1 -1
- package/examples/hooks/qna.ts +1 -1
- package/examples/hooks/status-line.ts +1 -1
- package/examples/sdk/11-sessions.ts +1 -1
- package/package.json +8 -8
- package/src/cli/args.ts +9 -6
- package/src/cli/update-cli.ts +2 -2
- package/src/commands/index/index.ts +2 -5
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/changelog/index.ts +2 -2
- package/src/config/keybindings.ts +16 -1
- package/src/config/model-registry.ts +25 -20
- package/src/config/model-resolver.ts +8 -8
- package/src/config/resolve-config-value.ts +92 -0
- package/src/config/settings-schema.ts +9 -0
- package/src/config.ts +14 -1
- package/src/export/html/template.css +7 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +33 -16
- package/src/extensibility/custom-commands/bundled/review/index.ts +1 -1
- package/src/extensibility/extensions/index.ts +18 -0
- package/src/extensibility/extensions/loader.ts +15 -0
- package/src/extensibility/extensions/runner.ts +78 -1
- package/src/extensibility/extensions/types.ts +131 -5
- package/src/extensibility/extensions/wrapper.ts +1 -1
- package/src/extensibility/plugins/git-url.ts +270 -0
- package/src/extensibility/plugins/index.ts +2 -0
- package/src/extensibility/slash-commands.ts +45 -0
- package/src/index.ts +7 -0
- package/src/lsp/render.ts +50 -43
- package/src/lsp/utils.ts +2 -2
- package/src/main.ts +11 -10
- package/src/mcp/transports/stdio.ts +3 -5
- package/src/modes/components/custom-message.ts +0 -8
- package/src/modes/components/diff.ts +1 -7
- package/src/modes/components/footer.ts +4 -4
- package/src/modes/components/model-selector.ts +4 -0
- package/src/modes/components/todo-display.ts +13 -3
- package/src/modes/components/tool-execution.ts +30 -16
- package/src/modes/components/tree-selector.ts +50 -19
- package/src/modes/controllers/event-controller.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +34 -2
- package/src/modes/controllers/input-controller.ts +47 -33
- package/src/modes/controllers/selector-controller.ts +10 -15
- package/src/modes/interactive-mode.ts +50 -38
- package/src/modes/print-mode.ts +6 -0
- package/src/modes/rpc/rpc-client.ts +4 -4
- package/src/modes/rpc/rpc-mode.ts +17 -2
- package/src/modes/rpc/rpc-types.ts +2 -2
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +3 -1
- package/src/patch/applicator.ts +2 -3
- package/src/patch/fuzzy.ts +1 -1
- package/src/patch/shared.ts +74 -61
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/task.md +6 -0
- package/src/sdk.ts +15 -11
- package/src/session/agent-session.ts +72 -23
- package/src/session/auth-storage.ts +2 -1
- package/src/session/blob-store.ts +105 -0
- package/src/session/session-manager.ts +107 -44
- package/src/task/executor.ts +19 -9
- package/src/task/render.ts +80 -58
- package/src/tools/ask.ts +28 -5
- package/src/tools/bash.ts +47 -39
- package/src/tools/browser.ts +248 -26
- package/src/tools/calculator.ts +42 -23
- package/src/tools/fetch.ts +33 -16
- package/src/tools/find.ts +57 -22
- package/src/tools/grep.ts +54 -25
- package/src/tools/index.ts +5 -5
- package/src/tools/notebook.ts +19 -6
- package/src/tools/path-utils.ts +26 -1
- package/src/tools/python.ts +20 -14
- package/src/tools/read.ts +21 -8
- package/src/tools/render-utils.ts +5 -45
- package/src/tools/ssh.ts +59 -53
- package/src/tools/submit-result.ts +2 -2
- package/src/tools/todo-write.ts +32 -14
- package/src/tools/truncate.ts +1 -1
- package/src/tools/write.ts +39 -24
- package/src/tui/output-block.ts +61 -3
- package/src/tui/tree-list.ts +4 -4
- package/src/tui/utils.ts +71 -1
- package/src/utils/frontmatter.ts +1 -1
- package/src/utils/title-generator.ts +1 -1
- package/src/utils/tools-manager.ts +18 -2
- package/src/web/scrapers/osv.ts +4 -1
- package/src/web/scrapers/youtube.ts +1 -1
- package/src/web/search/index.ts +1 -1
- package/src/web/search/render.ts +96 -90
|
@@ -55,6 +55,7 @@ class TreeList implements Component {
|
|
|
55
55
|
private toolCallMap: Map<string, ToolCallInfo> = new Map();
|
|
56
56
|
private multipleRoots = false;
|
|
57
57
|
private activePathIds: Set<string> = new Set();
|
|
58
|
+
private lastSelectedId: string | null = null;
|
|
58
59
|
|
|
59
60
|
public onSelect?: (entryId: string) => void;
|
|
60
61
|
public onCancel?: () => void;
|
|
@@ -64,19 +65,17 @@ class TreeList implements Component {
|
|
|
64
65
|
tree: SessionTreeNode[],
|
|
65
66
|
private readonly currentLeafId: string | null,
|
|
66
67
|
private readonly maxVisibleLines: number,
|
|
68
|
+
initialSelectedId?: string,
|
|
67
69
|
) {
|
|
68
70
|
this.multipleRoots = tree.length > 1;
|
|
69
71
|
this.flatNodes = this.flattenTree(tree);
|
|
70
72
|
this.buildActivePath();
|
|
71
73
|
this.applyFilter();
|
|
72
74
|
|
|
73
|
-
// Start with current leaf
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
} else {
|
|
78
|
-
this.selectedIndex = Math.max(0, this.filteredNodes.length - 1);
|
|
79
|
-
}
|
|
75
|
+
// Start with initialSelectedId if provided, otherwise current leaf
|
|
76
|
+
const targetId = initialSelectedId ?? currentLeafId;
|
|
77
|
+
this.selectedIndex = this.findNearestVisibleIndex(targetId);
|
|
78
|
+
this.lastSelectedId = this.filteredNodes[this.selectedIndex]?.node.entry.id ?? null;
|
|
80
79
|
}
|
|
81
80
|
|
|
82
81
|
/** Build the set of entry IDs on the path from root to current leaf */
|
|
@@ -100,6 +99,36 @@ class TreeList implements Component {
|
|
|
100
99
|
}
|
|
101
100
|
}
|
|
102
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Find the index of the nearest visible entry, walking up the parent chain if needed.
|
|
104
|
+
* Returns the index in filteredNodes, or the last index as fallback.
|
|
105
|
+
*/
|
|
106
|
+
private findNearestVisibleIndex(entryId: string | null): number {
|
|
107
|
+
if (this.filteredNodes.length === 0) return 0;
|
|
108
|
+
|
|
109
|
+
// Build a map for parent lookup
|
|
110
|
+
const entryMap = new Map<string, FlatNode>();
|
|
111
|
+
for (const flatNode of this.flatNodes) {
|
|
112
|
+
entryMap.set(flatNode.node.entry.id, flatNode);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Build a map of visible entry IDs to their indices in filteredNodes
|
|
116
|
+
const visibleIdToIndex = new Map<string, number>(this.filteredNodes.map((node, i) => [node.node.entry.id, i]));
|
|
117
|
+
|
|
118
|
+
// Walk from entryId up to root, looking for a visible entry
|
|
119
|
+
let currentId = entryId;
|
|
120
|
+
while (currentId !== null) {
|
|
121
|
+
const index = visibleIdToIndex.get(currentId);
|
|
122
|
+
if (index !== undefined) return index;
|
|
123
|
+
const node = entryMap.get(currentId);
|
|
124
|
+
if (!node) break;
|
|
125
|
+
currentId = node.node.entry.parentId ?? null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Fallback: last visible entry
|
|
129
|
+
return this.filteredNodes.length - 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
103
132
|
private flattenTree(roots: SessionTreeNode[]): FlatNode[] {
|
|
104
133
|
const result: FlatNode[] = [];
|
|
105
134
|
this.toolCallMap.clear();
|
|
@@ -231,8 +260,11 @@ class TreeList implements Component {
|
|
|
231
260
|
}
|
|
232
261
|
|
|
233
262
|
private applyFilter(): void {
|
|
234
|
-
//
|
|
235
|
-
|
|
263
|
+
// Update lastSelectedId only when we have a valid selection (non-empty list)
|
|
264
|
+
// This preserves the selection when switching through empty filter results
|
|
265
|
+
if (this.filteredNodes.length > 0) {
|
|
266
|
+
this.lastSelectedId = this.filteredNodes[this.selectedIndex]?.node.entry.id ?? this.lastSelectedId;
|
|
267
|
+
}
|
|
236
268
|
|
|
237
269
|
const searchTokens = this.searchQuery.toLowerCase().split(/\s+/).filter(Boolean);
|
|
238
270
|
|
|
@@ -295,18 +327,17 @@ class TreeList implements Component {
|
|
|
295
327
|
return true;
|
|
296
328
|
});
|
|
297
329
|
|
|
298
|
-
// Try to preserve cursor on the same node
|
|
299
|
-
if (
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
330
|
+
// Try to preserve cursor on the same node, or find nearest visible ancestor
|
|
331
|
+
if (this.lastSelectedId) {
|
|
332
|
+
this.selectedIndex = this.findNearestVisibleIndex(this.lastSelectedId);
|
|
333
|
+
} else if (this.selectedIndex >= this.filteredNodes.length) {
|
|
334
|
+
// Clamp index if out of bounds
|
|
335
|
+
this.selectedIndex = Math.max(0, this.filteredNodes.length - 1);
|
|
305
336
|
}
|
|
306
337
|
|
|
307
|
-
//
|
|
308
|
-
if (this.
|
|
309
|
-
this.
|
|
338
|
+
// Update lastSelectedId to the actual selection (may have changed due to parent walk)
|
|
339
|
+
if (this.filteredNodes.length > 0) {
|
|
340
|
+
this.lastSelectedId = this.filteredNodes[this.selectedIndex]?.node.entry.id ?? this.lastSelectedId;
|
|
310
341
|
}
|
|
311
342
|
}
|
|
312
343
|
|
|
@@ -97,6 +97,7 @@ export class EventController {
|
|
|
97
97
|
this.ctx.ui.requestRender();
|
|
98
98
|
} else if (event.message.role === "assistant") {
|
|
99
99
|
this.lastThinkingCount = 0;
|
|
100
|
+
this.resetReadGroup();
|
|
100
101
|
this.ctx.streamingComponent = new AssistantMessageComponent(undefined, this.ctx.hideThinkingBlock);
|
|
101
102
|
this.ctx.streamingMessage = event.message;
|
|
102
103
|
this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
|
|
@@ -64,6 +64,8 @@ export class ExtensionUiController {
|
|
|
64
64
|
setFooter: () => {},
|
|
65
65
|
setHeader: () => {},
|
|
66
66
|
setEditorComponent: () => {},
|
|
67
|
+
getToolsExpanded: () => this.ctx.toolOutputExpanded,
|
|
68
|
+
setToolsExpanded: expanded => this.ctx.setToolsExpanded(expanded),
|
|
67
69
|
};
|
|
68
70
|
this.ctx.setToolUIContext(uiContext, true);
|
|
69
71
|
|
|
@@ -114,6 +116,7 @@ export class ExtensionUiController {
|
|
|
114
116
|
},
|
|
115
117
|
getThinkingLevel: () => this.ctx.session.thinkingLevel,
|
|
116
118
|
setThinkingLevel: level => this.ctx.session.setThinkingLevel(level),
|
|
119
|
+
getCommands: () => [],
|
|
117
120
|
};
|
|
118
121
|
const contextActions: ExtensionContextActions = {
|
|
119
122
|
getModel: () => this.ctx.session.model,
|
|
@@ -130,6 +133,7 @@ export class ExtensionUiController {
|
|
|
130
133
|
instructionsOrOptions && typeof instructionsOrOptions === "object" ? instructionsOrOptions : undefined;
|
|
131
134
|
await this.ctx.session.compact(instructions, options);
|
|
132
135
|
},
|
|
136
|
+
getSystemPrompt: () => this.ctx.session.systemPrompt,
|
|
133
137
|
};
|
|
134
138
|
const commandActions: ExtensionCommandContextActions = {
|
|
135
139
|
getContextUsage: () => this.ctx.session.getContextUsage(),
|
|
@@ -195,7 +199,7 @@ export class ExtensionUiController {
|
|
|
195
199
|
this.ctx.chatContainer.clear();
|
|
196
200
|
this.ctx.renderInitialMessages();
|
|
197
201
|
await this.ctx.reloadTodos();
|
|
198
|
-
if (result.editorText) {
|
|
202
|
+
if (result.editorText && !this.ctx.editor.getText().trim()) {
|
|
199
203
|
this.ctx.editor.setText(result.editorText);
|
|
200
204
|
}
|
|
201
205
|
this.ctx.showStatus("Navigated to selected point");
|
|
@@ -212,6 +216,16 @@ export class ExtensionUiController {
|
|
|
212
216
|
}
|
|
213
217
|
await this.ctx.executeCompaction(instructionsOrOptions, false);
|
|
214
218
|
},
|
|
219
|
+
switchSession: async sessionPath => {
|
|
220
|
+
const result = await this.ctx.session.switchSession(sessionPath);
|
|
221
|
+
if (!result) {
|
|
222
|
+
return { cancelled: true };
|
|
223
|
+
}
|
|
224
|
+
this.ctx.chatContainer.clear();
|
|
225
|
+
this.ctx.renderInitialMessages();
|
|
226
|
+
await this.ctx.reloadTodos();
|
|
227
|
+
return { cancelled: false };
|
|
228
|
+
},
|
|
215
229
|
};
|
|
216
230
|
|
|
217
231
|
extensionRunner.initialize(actions, contextActions, commandActions, uiContext);
|
|
@@ -283,6 +297,7 @@ export class ExtensionUiController {
|
|
|
283
297
|
},
|
|
284
298
|
getThinkingLevel: () => this.ctx.session.thinkingLevel,
|
|
285
299
|
setThinkingLevel: (level, persist) => this.ctx.session.setThinkingLevel(level, persist),
|
|
300
|
+
getCommands: () => [],
|
|
286
301
|
};
|
|
287
302
|
const contextActions: ExtensionContextActions = {
|
|
288
303
|
getModel: () => this.ctx.session.model,
|
|
@@ -299,6 +314,7 @@ export class ExtensionUiController {
|
|
|
299
314
|
instructionsOrOptions && typeof instructionsOrOptions === "object" ? instructionsOrOptions : undefined;
|
|
300
315
|
await this.ctx.session.compact(instructions, options);
|
|
301
316
|
},
|
|
317
|
+
getSystemPrompt: () => this.ctx.session.systemPrompt,
|
|
302
318
|
};
|
|
303
319
|
const commandActions: ExtensionCommandContextActions = {
|
|
304
320
|
getContextUsage: () => this.ctx.session.getContextUsage(),
|
|
@@ -373,7 +389,7 @@ export class ExtensionUiController {
|
|
|
373
389
|
this.ctx.chatContainer.clear();
|
|
374
390
|
this.ctx.renderInitialMessages();
|
|
375
391
|
await this.ctx.reloadTodos();
|
|
376
|
-
if (result.editorText) {
|
|
392
|
+
if (result.editorText && !this.ctx.editor.getText().trim()) {
|
|
377
393
|
this.ctx.editor.setText(result.editorText);
|
|
378
394
|
}
|
|
379
395
|
this.ctx.showStatus("Navigated to selected point");
|
|
@@ -390,6 +406,19 @@ export class ExtensionUiController {
|
|
|
390
406
|
}
|
|
391
407
|
await this.ctx.executeCompaction(instructionsOrOptions, false);
|
|
392
408
|
},
|
|
409
|
+
switchSession: async sessionPath => {
|
|
410
|
+
if (this.ctx.isBackgrounded) {
|
|
411
|
+
return { cancelled: true };
|
|
412
|
+
}
|
|
413
|
+
const result = await this.ctx.session.switchSession(sessionPath);
|
|
414
|
+
if (!result) {
|
|
415
|
+
return { cancelled: true };
|
|
416
|
+
}
|
|
417
|
+
this.ctx.chatContainer.clear();
|
|
418
|
+
this.ctx.renderInitialMessages();
|
|
419
|
+
await this.ctx.reloadTodos();
|
|
420
|
+
return { cancelled: false };
|
|
421
|
+
},
|
|
393
422
|
};
|
|
394
423
|
|
|
395
424
|
extensionRunner.initialize(actions, contextActions, commandActions, uiContext);
|
|
@@ -418,6 +447,8 @@ export class ExtensionUiController {
|
|
|
418
447
|
setFooter: () => {},
|
|
419
448
|
setHeader: () => {},
|
|
420
449
|
setEditorComponent: () => {},
|
|
450
|
+
getToolsExpanded: () => false,
|
|
451
|
+
setToolsExpanded: () => {},
|
|
421
452
|
};
|
|
422
453
|
}
|
|
423
454
|
|
|
@@ -461,6 +492,7 @@ export class ExtensionUiController {
|
|
|
461
492
|
shutdown: () => {
|
|
462
493
|
// Signal shutdown request
|
|
463
494
|
},
|
|
495
|
+
getSystemPrompt: () => this.ctx.session.systemPrompt,
|
|
464
496
|
});
|
|
465
497
|
} catch (err) {
|
|
466
498
|
this.showToolError(registeredTool.definition.name, err instanceof Error ? err.message : String(err));
|
|
@@ -97,6 +97,22 @@ export class InputController {
|
|
|
97
97
|
this.ctx.editor.setCustomKeyHandler(key, () => void this.ctx.handlePlanModeCommand());
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
for (const key of this.ctx.keybindings.getKeys("newSession")) {
|
|
101
|
+
this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.handleClearCommand());
|
|
102
|
+
}
|
|
103
|
+
for (const key of this.ctx.keybindings.getKeys("tree")) {
|
|
104
|
+
this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showTreeSelector());
|
|
105
|
+
}
|
|
106
|
+
for (const key of this.ctx.keybindings.getKeys("fork")) {
|
|
107
|
+
this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showUserMessageSelector());
|
|
108
|
+
}
|
|
109
|
+
for (const key of this.ctx.keybindings.getKeys("resume")) {
|
|
110
|
+
this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showSessionSelector());
|
|
111
|
+
}
|
|
112
|
+
for (const key of this.ctx.keybindings.getKeys("followUp")) {
|
|
113
|
+
this.ctx.editor.setCustomKeyHandler(key, () => void this.handleFollowUp());
|
|
114
|
+
}
|
|
115
|
+
|
|
100
116
|
this.ctx.editor.onChange = (text: string) => {
|
|
101
117
|
const wasBashMode = this.ctx.isBashMode;
|
|
102
118
|
const wasPythonMode = this.ctx.isPythonMode;
|
|
@@ -107,37 +123,6 @@ export class InputController {
|
|
|
107
123
|
this.ctx.updateEditorBorderColor();
|
|
108
124
|
}
|
|
109
125
|
};
|
|
110
|
-
|
|
111
|
-
this.ctx.editor.onAltEnter = async (text: string) => {
|
|
112
|
-
const trimmedText = text.trim();
|
|
113
|
-
|
|
114
|
-
// Queue follow-up messages while compaction is running
|
|
115
|
-
if (this.ctx.session.isCompacting) {
|
|
116
|
-
if (!trimmedText) {
|
|
117
|
-
this.ctx.editor.handleInput("\n");
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
this.ctx.queueCompactionMessage(trimmedText, "followUp");
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Alt+Enter queues a follow-up message while streaming
|
|
125
|
-
if (this.ctx.session.isStreaming) {
|
|
126
|
-
if (!trimmedText) {
|
|
127
|
-
this.ctx.editor.handleInput("\n");
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
this.ctx.editor.addToHistory(trimmedText);
|
|
131
|
-
this.ctx.editor.setText("");
|
|
132
|
-
await this.ctx.session.prompt(trimmedText, { streamingBehavior: "followUp" });
|
|
133
|
-
this.ctx.updatePendingMessagesDisplay();
|
|
134
|
-
this.ctx.ui.requestRender();
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Default behavior: insert a new line
|
|
139
|
-
this.ctx.editor.handleInput("\n");
|
|
140
|
-
};
|
|
141
126
|
}
|
|
142
127
|
|
|
143
128
|
setupEditorSubmitHandler(): void {
|
|
@@ -519,6 +504,31 @@ export class InputController {
|
|
|
519
504
|
}
|
|
520
505
|
}
|
|
521
506
|
|
|
507
|
+
/** Send editor text as a follow-up message (queued behind current stream). */
|
|
508
|
+
async handleFollowUp(): Promise<void> {
|
|
509
|
+
const text = this.ctx.editor.getText().trim();
|
|
510
|
+
if (!text) return;
|
|
511
|
+
|
|
512
|
+
if (this.ctx.session.isCompacting) {
|
|
513
|
+
this.ctx.queueCompactionMessage(text, "followUp");
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (this.ctx.session.isStreaming) {
|
|
518
|
+
this.ctx.editor.addToHistory(text);
|
|
519
|
+
this.ctx.editor.setText("");
|
|
520
|
+
await this.ctx.session.prompt(text, { streamingBehavior: "followUp" });
|
|
521
|
+
this.ctx.updatePendingMessagesDisplay();
|
|
522
|
+
this.ctx.ui.requestRender();
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Not streaming — just submit normally
|
|
527
|
+
this.ctx.editor.addToHistory(text);
|
|
528
|
+
this.ctx.editor.setText("");
|
|
529
|
+
await this.ctx.session.prompt(text);
|
|
530
|
+
}
|
|
531
|
+
|
|
522
532
|
restoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number {
|
|
523
533
|
const { steering, followUp } = this.ctx.session.clearQueue();
|
|
524
534
|
const allQueued = [...steering, ...followUp];
|
|
@@ -681,10 +691,14 @@ export class InputController {
|
|
|
681
691
|
}
|
|
682
692
|
|
|
683
693
|
toggleToolOutputExpansion(): void {
|
|
684
|
-
this.
|
|
694
|
+
this.setToolsExpanded(!this.ctx.toolOutputExpanded);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
setToolsExpanded(expanded: boolean): void {
|
|
698
|
+
this.ctx.toolOutputExpanded = expanded;
|
|
685
699
|
for (const child of this.ctx.chatContainer.children) {
|
|
686
700
|
if (isExpandable(child)) {
|
|
687
|
-
child.setExpanded(
|
|
701
|
+
child.setExpanded(expanded);
|
|
688
702
|
}
|
|
689
703
|
}
|
|
690
704
|
this.ctx.ui.requestRender();
|
|
@@ -84,7 +84,7 @@ export class SelectorController {
|
|
|
84
84
|
},
|
|
85
85
|
getStatusLinePreview: () => {
|
|
86
86
|
// Return the rendered status line for inline preview
|
|
87
|
-
const width = this.ctx.ui.
|
|
87
|
+
const width = this.ctx.ui.terminal.columns;
|
|
88
88
|
return this.ctx.statusLine.getTopBorder(width).content;
|
|
89
89
|
},
|
|
90
90
|
onPluginsChanged: () => {
|
|
@@ -185,6 +185,10 @@ export class SelectorController {
|
|
|
185
185
|
this.ctx.updateEditorBorderColor();
|
|
186
186
|
break;
|
|
187
187
|
|
|
188
|
+
case "clearOnShrink":
|
|
189
|
+
this.ctx.ui.setClearOnShrink(value as boolean);
|
|
190
|
+
break;
|
|
191
|
+
|
|
188
192
|
// Settings with UI side effects
|
|
189
193
|
case "showImages":
|
|
190
194
|
for (const child of this.ctx.chatContainer.children) {
|
|
@@ -354,15 +358,6 @@ export class SelectorController {
|
|
|
354
358
|
const tree = this.ctx.sessionManager.getTree();
|
|
355
359
|
const realLeafId = this.ctx.sessionManager.getLeafId();
|
|
356
360
|
|
|
357
|
-
// Find the visible leaf for display (skip metadata entries like labels)
|
|
358
|
-
let visibleLeafId = realLeafId;
|
|
359
|
-
while (visibleLeafId) {
|
|
360
|
-
const entry = this.ctx.sessionManager.getEntry(visibleLeafId);
|
|
361
|
-
if (!entry) break;
|
|
362
|
-
if (entry.type !== "label" && entry.type !== "custom") break;
|
|
363
|
-
visibleLeafId = entry.parentId ?? null;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
361
|
if (tree.length === 0) {
|
|
367
362
|
this.ctx.showStatus("No entries in session");
|
|
368
363
|
return;
|
|
@@ -371,11 +366,11 @@ export class SelectorController {
|
|
|
371
366
|
this.showSelector(done => {
|
|
372
367
|
const selector = new TreeSelectorComponent(
|
|
373
368
|
tree,
|
|
374
|
-
|
|
369
|
+
realLeafId,
|
|
375
370
|
this.ctx.ui.terminal.rows,
|
|
376
371
|
async entryId => {
|
|
377
|
-
// Selecting the
|
|
378
|
-
if (entryId ===
|
|
372
|
+
// Selecting the current leaf is a no-op (already there)
|
|
373
|
+
if (entryId === realLeafId) {
|
|
379
374
|
done();
|
|
380
375
|
this.ctx.showStatus("Already at this point");
|
|
381
376
|
return;
|
|
@@ -458,7 +453,7 @@ export class SelectorController {
|
|
|
458
453
|
this.ctx.chatContainer.clear();
|
|
459
454
|
this.ctx.renderInitialMessages();
|
|
460
455
|
await this.ctx.reloadTodos();
|
|
461
|
-
if (result.editorText) {
|
|
456
|
+
if (result.editorText && !this.ctx.editor.getText().trim()) {
|
|
462
457
|
this.ctx.editor.setText(result.editorText);
|
|
463
458
|
}
|
|
464
459
|
this.ctx.showStatus("Navigated to selected point");
|
|
@@ -552,7 +547,7 @@ export class SelectorController {
|
|
|
552
547
|
done();
|
|
553
548
|
|
|
554
549
|
if (mode === "login") {
|
|
555
|
-
this.ctx.showStatus(`Logging in to ${providerId}
|
|
550
|
+
this.ctx.showStatus(`Logging in to ${providerId}…`);
|
|
556
551
|
|
|
557
552
|
try {
|
|
558
553
|
await this.ctx.session.modelRegistry.authStorage.login(providerId as OAuthProvider, {
|
|
@@ -17,12 +17,13 @@ import {
|
|
|
17
17
|
} from "@oh-my-pi/pi-tui";
|
|
18
18
|
import { $env, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
|
|
19
19
|
import chalk from "chalk";
|
|
20
|
+
import { APP_NAME } from "../config";
|
|
20
21
|
import { KeybindingsManager } from "../config/keybindings";
|
|
21
22
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
22
23
|
import { type Settings, settings } from "../config/settings";
|
|
23
24
|
import type { ExtensionUIContext, ExtensionUIDialogOptions } from "../extensibility/extensions";
|
|
24
25
|
import type { CompactOptions } from "../extensibility/extensions/types";
|
|
25
|
-
import { loadSlashCommands } from "../extensibility/slash-commands";
|
|
26
|
+
import { BUILTIN_SLASH_COMMANDS, loadSlashCommands } from "../extensibility/slash-commands";
|
|
26
27
|
import { resolvePlanUrlToPath } from "../internal-urls";
|
|
27
28
|
import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
|
|
28
29
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
@@ -173,6 +174,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
173
174
|
this.mcpManager = mcpManager;
|
|
174
175
|
|
|
175
176
|
this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"));
|
|
177
|
+
this.ui.setClearOnShrink(settings.get("clearOnShrink"));
|
|
176
178
|
setMermaidRenderCallback(() => this.ui.requestRender());
|
|
177
179
|
this.chatContainer = new Container();
|
|
178
180
|
this.pendingMessagesContainer = new Container();
|
|
@@ -199,39 +201,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
199
201
|
|
|
200
202
|
this.hideThinkingBlock = settings.get("hideThinkingBlock");
|
|
201
203
|
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
{ name: "model", description: "Select model (opens selector UI)" },
|
|
207
|
-
{ name: "export", description: "Export session to HTML file" },
|
|
208
|
-
{ name: "dump", description: "Copy session transcript to clipboard" },
|
|
209
|
-
{ name: "share", description: "Share session as a secret GitHub gist" },
|
|
210
|
-
{ name: "browser", description: "Toggle browser headless vs visible mode" },
|
|
211
|
-
{ name: "copy", description: "Copy last agent message to clipboard" },
|
|
212
|
-
{ name: "session", description: "Show session info and stats" },
|
|
213
|
-
{ name: "usage", description: "Show provider usage and limits" },
|
|
214
|
-
{ name: "extensions", description: "Open Extension Control Center dashboard" },
|
|
215
|
-
{ name: "status", description: "Alias for /extensions" },
|
|
216
|
-
{ name: "changelog", description: "Show changelog entries" },
|
|
217
|
-
{ name: "hotkeys", description: "Show all keyboard shortcuts" },
|
|
218
|
-
{ name: "branch", description: "Create a new branch from a previous message" },
|
|
219
|
-
{ name: "tree", description: "Navigate session tree (switch branches)" },
|
|
220
|
-
{ name: "login", description: "Login with OAuth provider" },
|
|
221
|
-
{ name: "logout", description: "Logout from OAuth provider" },
|
|
222
|
-
{ name: "new", description: "Start a new session" },
|
|
223
|
-
{ name: "fork", description: "Duplicate current session into a new session" },
|
|
224
|
-
{ name: "compact", description: "Manually compact the session context" },
|
|
225
|
-
{ name: "handoff", description: "Hand off the session context to a new session" },
|
|
226
|
-
{ name: "background", description: "Detach UI and continue running in background" },
|
|
227
|
-
{ name: "bg", description: "Alias for /background" },
|
|
228
|
-
{ name: "resume", description: "Resume a different session" },
|
|
229
|
-
{ name: "debug", description: "Write debug log (TUI state and messages)" },
|
|
230
|
-
{ name: "exit", description: "Exit the application" },
|
|
231
|
-
];
|
|
232
|
-
|
|
233
|
-
// Convert hook commands to SlashCommand format
|
|
234
|
-
const hookCommands: SlashCommand[] = (this.session.extensionRunner?.getRegisteredCommands() ?? []).map(cmd => ({
|
|
204
|
+
const builtinCommandNames = new Set(BUILTIN_SLASH_COMMANDS.map(c => c.name));
|
|
205
|
+
const hookCommands: SlashCommand[] = (
|
|
206
|
+
this.session.extensionRunner?.getRegisteredCommands(builtinCommandNames) ?? []
|
|
207
|
+
).map(cmd => ({
|
|
235
208
|
name: cmd.name,
|
|
236
209
|
description: cmd.description ?? "(hook command)",
|
|
237
210
|
getArgumentCompletions: cmd.getArgumentCompletions,
|
|
@@ -254,7 +227,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
254
227
|
}
|
|
255
228
|
|
|
256
229
|
// Store pending commands for init() where file commands are loaded async
|
|
257
|
-
this.pendingSlashCommands = [...
|
|
230
|
+
this.pendingSlashCommands = [...BUILTIN_SLASH_COMMANDS, ...hookCommands, ...customCommands, ...skillCommandList];
|
|
258
231
|
|
|
259
232
|
this.uiHelpers = new UiHelpers(this);
|
|
260
233
|
this.extensionUiController = new ExtensionUiController(this);
|
|
@@ -373,6 +346,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
373
346
|
// Initialize hooks with TUI-based UI context
|
|
374
347
|
await this.initHooksAndCustomTools();
|
|
375
348
|
|
|
349
|
+
// Restore mode from session (e.g. plan mode on resume)
|
|
350
|
+
await this.restoreModeFromSession();
|
|
351
|
+
|
|
376
352
|
// Subscribe to agent events
|
|
377
353
|
this.subscribeToAgent();
|
|
378
354
|
|
|
@@ -416,7 +392,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
416
392
|
}
|
|
417
393
|
|
|
418
394
|
updateEditorTopBorder(): void {
|
|
419
|
-
const width = this.ui.
|
|
395
|
+
const width = this.ui.terminal.columns;
|
|
420
396
|
const topBorder = this.statusLine.getTopBorder(width);
|
|
421
397
|
this.editor.setTopBorder(topBorder);
|
|
422
398
|
}
|
|
@@ -561,6 +537,19 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
561
537
|
}
|
|
562
538
|
}
|
|
563
539
|
|
|
540
|
+
/** Restore mode state from session entries on resume (e.g. plan mode). */
|
|
541
|
+
private async restoreModeFromSession(): Promise<void> {
|
|
542
|
+
const sessionContext = this.sessionManager.buildSessionContext();
|
|
543
|
+
if (sessionContext.mode === "plan") {
|
|
544
|
+
const planFilePath = sessionContext.modeData?.planFilePath as string | undefined;
|
|
545
|
+
await this.enterPlanMode({ planFilePath });
|
|
546
|
+
} else if (sessionContext.mode === "plan_paused") {
|
|
547
|
+
this.planModePaused = true;
|
|
548
|
+
this.planModeHasEntered = true;
|
|
549
|
+
this.updatePlanModeStatus();
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
564
553
|
private async enterPlanMode(options?: {
|
|
565
554
|
planFilePath?: string;
|
|
566
555
|
workflow?: "parallel" | "iterative";
|
|
@@ -594,6 +583,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
594
583
|
this.planModeHasEntered = true;
|
|
595
584
|
await this.applyPlanModeModel();
|
|
596
585
|
this.updatePlanModeStatus();
|
|
586
|
+
this.sessionManager.appendModeChange("plan", { planFilePath });
|
|
597
587
|
this.showStatus(`Plan mode enabled. Plan file: ${planFilePath}`);
|
|
598
588
|
}
|
|
599
589
|
|
|
@@ -621,8 +611,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
621
611
|
this.planModePreviousTools = undefined;
|
|
622
612
|
this.planModePreviousModel = undefined;
|
|
623
613
|
this.updatePlanModeStatus();
|
|
614
|
+
const paused = options?.paused ?? false;
|
|
615
|
+
this.sessionManager.appendModeChange(paused ? "plan_paused" : "none");
|
|
624
616
|
if (!options?.silent) {
|
|
625
|
-
this.showStatus(
|
|
617
|
+
this.showStatus(paused ? "Plan mode paused." : "Plan mode disabled.");
|
|
626
618
|
}
|
|
627
619
|
}
|
|
628
620
|
|
|
@@ -735,10 +727,26 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
735
727
|
await this.session.emitCustomToolSessionEvent("shutdown");
|
|
736
728
|
|
|
737
729
|
if (this.isInitialized) {
|
|
738
|
-
|
|
730
|
+
this.ui.requestRender(true);
|
|
739
731
|
}
|
|
740
732
|
|
|
733
|
+
// Wait for any pending renders to complete
|
|
734
|
+
// requestRender() uses process.nextTick(), so we wait one tick
|
|
735
|
+
await new Promise(resolve => process.nextTick(resolve));
|
|
736
|
+
|
|
737
|
+
// Drain any in-flight Kitty key release events before stopping.
|
|
738
|
+
// This prevents escape sequences from leaking to the parent shell over slow SSH.
|
|
739
|
+
await this.ui.terminal.drainInput(1000);
|
|
740
|
+
|
|
741
741
|
this.stop();
|
|
742
|
+
|
|
743
|
+
// Print resumption hint if this is a persisted session
|
|
744
|
+
const sessionId = this.sessionManager.getSessionId();
|
|
745
|
+
const sessionFile = this.sessionManager.getSessionFile();
|
|
746
|
+
if (sessionId && sessionFile) {
|
|
747
|
+
process.stderr.write(`\n${chalk.dim(`Resume this session with ${APP_NAME} --resume ${sessionId}`)}\n`);
|
|
748
|
+
}
|
|
749
|
+
|
|
742
750
|
await postmortem.quit(0);
|
|
743
751
|
}
|
|
744
752
|
|
|
@@ -1011,6 +1019,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1011
1019
|
this.inputController.toggleToolOutputExpansion();
|
|
1012
1020
|
}
|
|
1013
1021
|
|
|
1022
|
+
setToolsExpanded(expanded: boolean): void {
|
|
1023
|
+
this.inputController.setToolsExpanded(expanded);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1014
1026
|
toggleThinkingBlockVisibility(): void {
|
|
1015
1027
|
this.inputController.toggleThinkingBlockVisibility();
|
|
1016
1028
|
}
|
package/src/modes/print-mode.ts
CHANGED
|
@@ -63,6 +63,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
63
63
|
getActiveTools: () => session.getActiveToolNames(),
|
|
64
64
|
getAllTools: () => session.getAllToolNames(),
|
|
65
65
|
setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
|
|
66
|
+
getCommands: () => [],
|
|
66
67
|
setModel: async model => {
|
|
67
68
|
const key = await session.modelRegistry.getApiKey(model);
|
|
68
69
|
if (!key) return false;
|
|
@@ -80,6 +81,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
80
81
|
hasPendingMessages: () => session.queuedMessageCount > 0,
|
|
81
82
|
shutdown: () => {},
|
|
82
83
|
getContextUsage: () => session.getContextUsage(),
|
|
84
|
+
getSystemPrompt: () => session.systemPrompt,
|
|
83
85
|
compact: async instructionsOrOptions => {
|
|
84
86
|
const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
|
|
85
87
|
const options =
|
|
@@ -108,6 +110,10 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
108
110
|
const result = await session.navigateTree(targetId, { summarize: options?.summarize });
|
|
109
111
|
return { cancelled: result.cancelled };
|
|
110
112
|
},
|
|
113
|
+
switchSession: async sessionPath => {
|
|
114
|
+
const success = await session.switchSession(sessionPath);
|
|
115
|
+
return { cancelled: !success };
|
|
116
|
+
},
|
|
111
117
|
compact: async instructionsOrOptions => {
|
|
112
118
|
const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
|
|
113
119
|
const options =
|
|
@@ -189,15 +189,15 @@ export class RpcClient {
|
|
|
189
189
|
/**
|
|
190
190
|
* Queue a steering message to interrupt the agent mid-run.
|
|
191
191
|
*/
|
|
192
|
-
async steer(message: string): Promise<void> {
|
|
193
|
-
await this.send({ type: "steer", message });
|
|
192
|
+
async steer(message: string, images?: ImageContent[]): Promise<void> {
|
|
193
|
+
await this.send({ type: "steer", message, images });
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
/**
|
|
197
197
|
* Queue a follow-up message to be processed after the agent finishes.
|
|
198
198
|
*/
|
|
199
|
-
async followUp(message: string): Promise<void> {
|
|
200
|
-
await this.send({ type: "follow_up", message });
|
|
199
|
+
async followUp(message: string, images?: ImageContent[]): Promise<void> {
|
|
200
|
+
await this.send({ type: "follow_up", message, images });
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
/**
|