@oh-my-pi/pi-coding-agent 14.1.0 → 14.1.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 +79 -0
- package/package.json +8 -8
- package/src/async/job-manager.ts +43 -10
- package/src/commit/agentic/tools/analyze-file.ts +1 -2
- package/src/config/mcp-schema.json +1 -1
- package/src/config/model-equivalence.ts +1 -0
- package/src/config/model-registry.ts +63 -34
- package/src/config/model-resolver.ts +111 -15
- package/src/config/settings-schema.ts +4 -3
- package/src/config/settings.ts +1 -1
- package/src/cursor.ts +64 -23
- package/src/edit/index.ts +254 -89
- package/src/edit/modes/chunk.ts +336 -57
- package/src/edit/modes/hashline.ts +51 -26
- package/src/edit/modes/patch.ts +16 -10
- package/src/edit/modes/replace.ts +15 -7
- package/src/edit/renderer.ts +248 -94
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +6 -4
- package/src/extensibility/custom-tools/types.ts +0 -3
- package/src/extensibility/extensions/loader.ts +16 -0
- package/src/extensibility/extensions/runner.ts +2 -7
- package/src/extensibility/extensions/types.ts +8 -4
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/ipy/executor.ts +447 -52
- package/src/ipy/kernel.ts +39 -13
- package/src/lsp/client.ts +54 -0
- package/src/lsp/index.ts +8 -0
- package/src/lsp/types.ts +6 -0
- package/src/main.ts +0 -1
- package/src/modes/acp/acp-agent.ts +4 -1
- package/src/modes/components/bash-execution.ts +16 -4
- package/src/modes/components/status-line/presets.ts +17 -6
- package/src/modes/components/status-line/segments.ts +15 -0
- package/src/modes/components/status-line-segment-editor.ts +1 -0
- package/src/modes/components/status-line.ts +7 -1
- package/src/modes/components/tool-execution.ts +145 -75
- package/src/modes/controllers/command-controller.ts +24 -1
- package/src/modes/controllers/event-controller.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +28 -5
- package/src/modes/controllers/input-controller.ts +9 -3
- package/src/modes/controllers/selector-controller.ts +4 -1
- package/src/modes/interactive-mode.ts +19 -3
- package/src/modes/print-mode.ts +13 -4
- package/src/modes/prompt-action-autocomplete.ts +3 -5
- package/src/modes/rpc/rpc-mode.ts +8 -2
- package/src/modes/shared.ts +2 -2
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/chunk-edit.md +191 -163
- package/src/prompts/tools/hashline.md +11 -11
- package/src/prompts/tools/patch.md +10 -5
- package/src/prompts/tools/{await.md → poll.md} +1 -1
- package/src/prompts/tools/read-chunk.md +3 -3
- package/src/prompts/tools/task.md +2 -2
- package/src/prompts/tools/vim.md +98 -0
- package/src/sdk.ts +754 -724
- package/src/session/agent-session.ts +164 -34
- package/src/session/session-manager.ts +50 -4
- package/src/slash-commands/builtin-registry.ts +17 -0
- package/src/task/executor.ts +4 -4
- package/src/task/index.ts +3 -5
- package/src/task/types.ts +2 -2
- package/src/tools/bash.ts +26 -8
- package/src/tools/find.ts +5 -2
- package/src/tools/grep.ts +77 -8
- package/src/tools/index.ts +48 -19
- package/src/tools/{await-tool.ts → poll-tool.ts} +36 -30
- package/src/tools/python.ts +293 -278
- package/src/tools/submit-result.ts +5 -2
- package/src/tools/todo-write.ts +8 -2
- package/src/tools/vim.ts +966 -0
- package/src/utils/edit-mode.ts +2 -1
- package/src/utils/session-color.ts +55 -0
- package/src/utils/title-generator.ts +15 -6
- package/src/vim/buffer.ts +309 -0
- package/src/vim/commands.ts +382 -0
- package/src/vim/engine.ts +2426 -0
- package/src/vim/parser.ts +151 -0
- package/src/vim/render.ts +252 -0
- package/src/vim/types.ts +197 -0
|
@@ -81,6 +81,7 @@ export interface ToolExecutionHandle {
|
|
|
81
81
|
export class ToolExecutionComponent extends Container {
|
|
82
82
|
#contentBox: Box; // Used for custom tools and bash visual truncation
|
|
83
83
|
#contentText: Text; // For built-in tools (with its own padding/bg)
|
|
84
|
+
#multiFileBoxes: (Box | Spacer)[] = []; // Extra boxes for multi-file edit results
|
|
84
85
|
#imageComponents: Image[] = [];
|
|
85
86
|
#imageSpacers: Spacer[] = [];
|
|
86
87
|
#toolName: string;
|
|
@@ -126,17 +127,18 @@ export class ToolExecutionComponent extends Container {
|
|
|
126
127
|
tool: AgentTool | undefined,
|
|
127
128
|
ui: TUI,
|
|
128
129
|
cwd: string = getProjectDir(),
|
|
130
|
+
_toolCallId?: string,
|
|
129
131
|
) {
|
|
130
132
|
super();
|
|
131
133
|
this.#toolName = toolName;
|
|
132
134
|
this.#toolLabel = tool?.label ?? toolName;
|
|
133
|
-
this.#args = cloneToolArgs(args);
|
|
134
135
|
this.#showImages = options.showImages ?? true;
|
|
135
136
|
this.#editFuzzyThreshold = options.editFuzzyThreshold;
|
|
136
137
|
this.#editAllowFuzzy = options.editAllowFuzzy;
|
|
137
138
|
this.#tool = tool;
|
|
138
139
|
this.#ui = ui;
|
|
139
140
|
this.#cwd = cwd;
|
|
141
|
+
this.#args = cloneToolArgs(args);
|
|
140
142
|
|
|
141
143
|
this.addChild(new Spacer(1));
|
|
142
144
|
|
|
@@ -179,12 +181,32 @@ export class ToolExecutionComponent extends Container {
|
|
|
179
181
|
#maybeComputeEditDiff(): void {
|
|
180
182
|
if (this.#toolName !== "edit") return;
|
|
181
183
|
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
+
const edits = this.#args?.edits;
|
|
185
|
+
if (!Array.isArray(edits) || edits.length === 0) return;
|
|
186
|
+
|
|
187
|
+
const first = edits[0];
|
|
188
|
+
if (!first || typeof first !== "object") return;
|
|
189
|
+
|
|
190
|
+
// Detect mode from first edit entry shape and compute preview for first file
|
|
191
|
+
if ("old_text" in first && "new_text" in first) {
|
|
192
|
+
// Replace mode
|
|
193
|
+
const { path, old_text: oldText, new_text: newText, all } = first;
|
|
194
|
+
if (!path || oldText === undefined || newText === undefined) return;
|
|
184
195
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
196
|
+
const argsKey = JSON.stringify({ path, oldText, newText, all });
|
|
197
|
+
if (this.#editDiffArgsKey === argsKey) return;
|
|
198
|
+
this.#editDiffArgsKey = argsKey;
|
|
199
|
+
|
|
200
|
+
computeEditDiff(path, oldText, newText, this.#cwd, true, all, this.#editFuzzyThreshold).then(result => {
|
|
201
|
+
if (this.#editDiffArgsKey === argsKey) {
|
|
202
|
+
this.#editDiffPreview = result;
|
|
203
|
+
this.#updateDisplay();
|
|
204
|
+
this.#ui.requestRender();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
} else if ("path" in first && ("diff" in first || ("op" in first && !("content" in first)))) {
|
|
208
|
+
// Patch mode (has diff or op without content — chunk edits always have content)
|
|
209
|
+
const { path, op, rename, diff } = first;
|
|
188
210
|
if (!path) return;
|
|
189
211
|
|
|
190
212
|
const argsKey = JSON.stringify({ path, op, rename, diff });
|
|
@@ -201,49 +223,26 @@ export class ToolExecutionComponent extends Container {
|
|
|
201
223
|
this.#ui.requestRender();
|
|
202
224
|
}
|
|
203
225
|
});
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
226
|
+
} else if ("loc" in first && "path" in first) {
|
|
227
|
+
// Hashline mode — group edits by path, preview first file
|
|
228
|
+
const path = first.path;
|
|
229
|
+
if (!path) return;
|
|
230
|
+
const fileEdits = edits.filter((e: any) => e.path === path);
|
|
231
|
+
const move = this.#args?.move;
|
|
232
|
+
|
|
233
|
+
const argsKey = JSON.stringify({ path, edits: fileEdits, move });
|
|
210
234
|
if (this.#editDiffArgsKey === argsKey) return;
|
|
211
235
|
this.#editDiffArgsKey = argsKey;
|
|
212
236
|
|
|
213
|
-
computeHashlineDiff({ path, edits, move }, this.#cwd).then(result => {
|
|
237
|
+
computeHashlineDiff({ path, edits: fileEdits, move }, this.#cwd).then(result => {
|
|
214
238
|
if (this.#editDiffArgsKey === argsKey) {
|
|
215
239
|
this.#editDiffPreview = result;
|
|
216
240
|
this.#updateDisplay();
|
|
217
241
|
this.#ui.requestRender();
|
|
218
242
|
}
|
|
219
243
|
});
|
|
220
|
-
return;
|
|
221
244
|
}
|
|
222
|
-
|
|
223
|
-
const oldText = this.#args?.old_text;
|
|
224
|
-
const newText = this.#args?.new_text;
|
|
225
|
-
const all = this.#args?.all;
|
|
226
|
-
|
|
227
|
-
// Need all three params to compute diff
|
|
228
|
-
if (!path || oldText === undefined || newText === undefined) return;
|
|
229
|
-
|
|
230
|
-
// Create a key to track which args this computation is for
|
|
231
|
-
const argsKey = JSON.stringify({ path, oldText, newText, all });
|
|
232
|
-
|
|
233
|
-
// Skip if we already computed for these exact args
|
|
234
|
-
if (this.#editDiffArgsKey === argsKey) return;
|
|
235
|
-
|
|
236
|
-
this.#editDiffArgsKey = argsKey;
|
|
237
|
-
|
|
238
|
-
// Compute diff async
|
|
239
|
-
computeEditDiff(path, oldText, newText, this.#cwd, true, all, this.#editFuzzyThreshold).then(result => {
|
|
240
|
-
// Only update if args haven't changed since we started
|
|
241
|
-
if (this.#editDiffArgsKey === argsKey) {
|
|
242
|
-
this.#editDiffPreview = result;
|
|
243
|
-
this.#updateDisplay();
|
|
244
|
-
this.#ui.requestRender();
|
|
245
|
-
}
|
|
246
|
-
});
|
|
245
|
+
// Chunk mode edits don't have a pre-execution diff preview
|
|
247
246
|
}
|
|
248
247
|
|
|
249
248
|
updateResult(
|
|
@@ -443,51 +442,122 @@ export class ToolExecutionComponent extends Container {
|
|
|
443
442
|
} else if (this.#toolName in toolRenderers) {
|
|
444
443
|
// Built-in tools with renderers
|
|
445
444
|
const renderer = toolRenderers[this.#toolName];
|
|
446
|
-
// Inline renderers skip background styling
|
|
447
|
-
this.#contentBox.setBgFn(renderer.inline ? undefined : bgFn);
|
|
448
|
-
this.#contentBox.clear();
|
|
449
445
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
446
|
+
// Clean up previous multi-file boxes
|
|
447
|
+
for (const box of this.#multiFileBoxes) {
|
|
448
|
+
this.removeChild(box);
|
|
449
|
+
}
|
|
450
|
+
this.#multiFileBoxes = [];
|
|
451
|
+
|
|
452
|
+
// Check for multi-file edit results
|
|
453
|
+
const perFileResults = this.#result?.details?.perFileResults as
|
|
454
|
+
| Array<{ path: string; isError?: boolean }>
|
|
455
|
+
| undefined;
|
|
456
|
+
if (perFileResults && perFileResults.length > 1) {
|
|
457
|
+
// Multi-file: render each file as its own Box (identical to separate tool calls)
|
|
458
|
+
this.#contentBox.setBgFn(undefined);
|
|
459
|
+
this.#contentBox.clear();
|
|
460
|
+
|
|
461
|
+
const renderContext = this.#buildRenderContext();
|
|
462
|
+
this.#renderState.renderContext = renderContext;
|
|
463
|
+
|
|
464
|
+
for (let i = 0; i < perFileResults.length; i++) {
|
|
465
|
+
const fileResult = perFileResults[i];
|
|
466
|
+
if (i > 0) {
|
|
467
|
+
const spacer = new Spacer(1);
|
|
468
|
+
this.#multiFileBoxes.push(spacer);
|
|
469
|
+
this.addChild(spacer);
|
|
457
470
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
471
|
+
const fileBgFn = fileResult.isError
|
|
472
|
+
? (text: string) => theme.bg("toolErrorBg", text)
|
|
473
|
+
: (text: string) => theme.bg("toolSuccessBg", text);
|
|
474
|
+
const fileBox = new Box(1, 1, fileBgFn);
|
|
475
|
+
try {
|
|
476
|
+
const resultComponent = renderer.renderResult(
|
|
477
|
+
{ content: [], details: fileResult, isError: fileResult.isError },
|
|
478
|
+
this.#renderState,
|
|
479
|
+
theme,
|
|
480
|
+
);
|
|
481
|
+
if (resultComponent) {
|
|
482
|
+
fileBox.addChild(ensureInvalidate(resultComponent));
|
|
483
|
+
}
|
|
484
|
+
} catch (err) {
|
|
485
|
+
logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
|
|
486
|
+
}
|
|
487
|
+
this.#multiFileBoxes.push(fileBox);
|
|
488
|
+
this.addChild(fileBox);
|
|
462
489
|
}
|
|
463
|
-
}
|
|
464
490
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
491
|
+
// Show pending indicator for remaining files
|
|
492
|
+
const totalFiles = this.#args?.edits
|
|
493
|
+
? new Set((this.#args.edits as any[]).map((e: any) => e?.path).filter(Boolean)).size
|
|
494
|
+
: 0;
|
|
495
|
+
const remaining = Math.max(0, totalFiles - perFileResults.length);
|
|
496
|
+
if (remaining > 0 && this.#isPartial) {
|
|
497
|
+
const pendingSpacer = new Spacer(1);
|
|
498
|
+
this.#multiFileBoxes.push(pendingSpacer);
|
|
499
|
+
this.addChild(pendingSpacer);
|
|
500
|
+
const pendingBox = new Box(1, 1, (text: string) => theme.bg("toolPendingBg", text));
|
|
501
|
+
const pendingText = renderStatusLine(
|
|
473
502
|
{
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
503
|
+
icon: "pending",
|
|
504
|
+
title: "Edit",
|
|
505
|
+
description: theme.fg("dim", `${remaining} more file${remaining > 1 ? "s" : ""} pending…`),
|
|
477
506
|
},
|
|
478
|
-
this.#renderState,
|
|
479
507
|
theme,
|
|
480
|
-
this.#getCallArgsForRender(),
|
|
481
508
|
);
|
|
482
|
-
|
|
483
|
-
|
|
509
|
+
pendingBox.addChild(new Text(pendingText, 0, 0));
|
|
510
|
+
this.#multiFileBoxes.push(pendingBox);
|
|
511
|
+
this.addChild(pendingBox);
|
|
512
|
+
}
|
|
513
|
+
} else {
|
|
514
|
+
// Single-file or no result: standard rendering
|
|
515
|
+
// Inline renderers skip background styling
|
|
516
|
+
this.#contentBox.setBgFn(renderer.inline ? undefined : bgFn);
|
|
517
|
+
this.#contentBox.clear();
|
|
518
|
+
|
|
519
|
+
const shouldRenderCall = !this.#result || !renderer.mergeCallAndResult;
|
|
520
|
+
if (shouldRenderCall) {
|
|
521
|
+
// Render call component
|
|
522
|
+
try {
|
|
523
|
+
const callComponent = renderer.renderCall(this.#getCallArgsForRender(), this.#renderState, theme);
|
|
524
|
+
if (callComponent) {
|
|
525
|
+
this.#contentBox.addChild(ensureInvalidate(callComponent));
|
|
526
|
+
}
|
|
527
|
+
} catch (err) {
|
|
528
|
+
logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
|
|
529
|
+
// Fall back to default on error
|
|
530
|
+
this.#contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
|
|
484
531
|
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Render result component if we have a result
|
|
535
|
+
if (this.#result) {
|
|
536
|
+
try {
|
|
537
|
+
// Build render context for tools that need extra state
|
|
538
|
+
const renderContext = this.#buildRenderContext();
|
|
539
|
+
this.#renderState.renderContext = renderContext;
|
|
540
|
+
|
|
541
|
+
const resultComponent = renderer.renderResult(
|
|
542
|
+
{
|
|
543
|
+
content: this.#result.content as any,
|
|
544
|
+
details: this.#result.details,
|
|
545
|
+
isError: this.#result.isError,
|
|
546
|
+
},
|
|
547
|
+
this.#renderState,
|
|
548
|
+
theme,
|
|
549
|
+
this.#getCallArgsForRender(),
|
|
550
|
+
);
|
|
551
|
+
if (resultComponent) {
|
|
552
|
+
this.#contentBox.addChild(ensureInvalidate(resultComponent));
|
|
553
|
+
}
|
|
554
|
+
} catch (err) {
|
|
555
|
+
logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
|
|
556
|
+
// Fall back to showing raw output on error
|
|
557
|
+
const output = this.#getTextOutput();
|
|
558
|
+
if (output) {
|
|
559
|
+
this.#contentBox.addChild(new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
|
|
560
|
+
}
|
|
491
561
|
}
|
|
492
562
|
}
|
|
493
563
|
}
|
|
@@ -588,11 +588,16 @@ export class CommandController {
|
|
|
588
588
|
}
|
|
589
589
|
await this.ctx.session.newSession();
|
|
590
590
|
this.ctx.resetObserverRegistry();
|
|
591
|
-
setSessionTerminalTitle(
|
|
591
|
+
setSessionTerminalTitle(
|
|
592
|
+
this.ctx.sessionManager.getSessionName(),
|
|
593
|
+
this.ctx.sessionManager.getCwd(),
|
|
594
|
+
this.ctx.sessionManager.titleSource,
|
|
595
|
+
);
|
|
592
596
|
|
|
593
597
|
this.ctx.statusLine.invalidate();
|
|
594
598
|
this.ctx.statusLine.setSessionStartTime(Date.now());
|
|
595
599
|
this.ctx.updateEditorTopBorder();
|
|
600
|
+
this.ctx.updateEditorBorderColor();
|
|
596
601
|
this.ctx.ui.requestRender();
|
|
597
602
|
|
|
598
603
|
this.ctx.chatContainer.clear();
|
|
@@ -686,6 +691,23 @@ export class CommandController {
|
|
|
686
691
|
}
|
|
687
692
|
}
|
|
688
693
|
|
|
694
|
+
async handleRenameCommand(title: string): Promise<void> {
|
|
695
|
+
try {
|
|
696
|
+
const stored = await this.ctx.sessionManager.setSessionName(title, "user");
|
|
697
|
+
if (!stored) {
|
|
698
|
+
this.ctx.showError("Session name cannot be empty.");
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
const name = this.ctx.sessionManager.getSessionName()!;
|
|
702
|
+
setSessionTerminalTitle(name, this.ctx.sessionManager.getCwd(), this.ctx.sessionManager.titleSource);
|
|
703
|
+
this.ctx.statusLine.invalidate();
|
|
704
|
+
this.ctx.updateEditorBorderColor();
|
|
705
|
+
this.ctx.showStatus(`Session renamed to "${name}".`);
|
|
706
|
+
} catch (err) {
|
|
707
|
+
this.ctx.showError(`Rename failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
689
711
|
async handleBashCommand(command: string, excludeFromContext = false): Promise<void> {
|
|
690
712
|
const isDeferred = this.ctx.session.isStreaming;
|
|
691
713
|
this.ctx.bashComponent = new BashExecutionComponent(command, this.ctx.ui, excludeFromContext);
|
|
@@ -869,6 +891,7 @@ export class CommandController {
|
|
|
869
891
|
|
|
870
892
|
this.ctx.statusLine.invalidate();
|
|
871
893
|
this.ctx.updateEditorTopBorder();
|
|
894
|
+
this.ctx.updateEditorBorderColor();
|
|
872
895
|
await this.ctx.reloadTodos();
|
|
873
896
|
|
|
874
897
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
@@ -244,6 +244,7 @@ export class EventController {
|
|
|
244
244
|
tool,
|
|
245
245
|
this.ctx.ui,
|
|
246
246
|
this.ctx.sessionManager.getCwd(),
|
|
247
|
+
content.id,
|
|
247
248
|
);
|
|
248
249
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
249
250
|
this.ctx.chatContainer.addChild(component);
|
|
@@ -333,6 +334,7 @@ export class EventController {
|
|
|
333
334
|
tool,
|
|
334
335
|
this.ctx.ui,
|
|
335
336
|
this.ctx.sessionManager.getCwd(),
|
|
337
|
+
event.toolCallId,
|
|
336
338
|
);
|
|
337
339
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
338
340
|
this.ctx.chatContainer.addChild(component);
|
|
@@ -638,7 +640,8 @@ export class EventController {
|
|
|
638
640
|
if (this.ctx.isBackgrounded === false) return;
|
|
639
641
|
const notify = settings.get("completion.notify");
|
|
640
642
|
if (notify === "off") return;
|
|
641
|
-
const title =
|
|
643
|
+
const title =
|
|
644
|
+
this.ctx.sessionManager.titleSource === "auto" ? undefined : this.ctx.sessionManager.getSessionName();
|
|
642
645
|
const message = title ? `${title}: Complete` : "Complete";
|
|
643
646
|
TERMINAL.sendNotification(message);
|
|
644
647
|
}
|
|
@@ -120,10 +120,18 @@ export class ExtensionUiController {
|
|
|
120
120
|
getThinkingLevel: () => this.ctx.session.thinkingLevel,
|
|
121
121
|
setThinkingLevel: level => this.ctx.session.setThinkingLevel(level),
|
|
122
122
|
getCommands: () => [],
|
|
123
|
+
getSessionName: () => this.ctx.sessionManager.getSessionName(),
|
|
124
|
+
setSessionName: async name => {
|
|
125
|
+
await this.ctx.sessionManager.setSessionName(name, "user");
|
|
126
|
+
setSessionTerminalTitle(
|
|
127
|
+
this.ctx.sessionManager.getSessionName(),
|
|
128
|
+
this.ctx.sessionManager.getCwd(),
|
|
129
|
+
this.ctx.sessionManager.titleSource,
|
|
130
|
+
);
|
|
131
|
+
},
|
|
123
132
|
};
|
|
124
133
|
const contextActions: ExtensionContextActions = {
|
|
125
134
|
getModel: () => this.ctx.session.model,
|
|
126
|
-
getSearchDb: () => this.ctx.session.searchDb,
|
|
127
135
|
isIdle: () => !this.ctx.session.isStreaming,
|
|
128
136
|
abort: () => this.ctx.session.abort(),
|
|
129
137
|
hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
|
|
@@ -164,7 +172,11 @@ export class ExtensionUiController {
|
|
|
164
172
|
if (!success) {
|
|
165
173
|
return { cancelled: true };
|
|
166
174
|
}
|
|
167
|
-
setSessionTerminalTitle(
|
|
175
|
+
setSessionTerminalTitle(
|
|
176
|
+
this.ctx.sessionManager.getSessionName(),
|
|
177
|
+
this.ctx.sessionManager.getCwd(),
|
|
178
|
+
this.ctx.sessionManager.titleSource,
|
|
179
|
+
);
|
|
168
180
|
|
|
169
181
|
// Call setup callback if provided
|
|
170
182
|
if (options?.setup) {
|
|
@@ -242,7 +254,11 @@ export class ExtensionUiController {
|
|
|
242
254
|
if (!result) {
|
|
243
255
|
return { cancelled: true };
|
|
244
256
|
}
|
|
245
|
-
setSessionTerminalTitle(
|
|
257
|
+
setSessionTerminalTitle(
|
|
258
|
+
this.ctx.sessionManager.getSessionName(),
|
|
259
|
+
this.ctx.sessionManager.getCwd(),
|
|
260
|
+
this.ctx.sessionManager.titleSource,
|
|
261
|
+
);
|
|
246
262
|
this.ctx.chatContainer.clear();
|
|
247
263
|
this.ctx.renderInitialMessages();
|
|
248
264
|
await this.ctx.reloadTodos();
|
|
@@ -382,10 +398,18 @@ export class ExtensionUiController {
|
|
|
382
398
|
getThinkingLevel: () => this.ctx.session.thinkingLevel,
|
|
383
399
|
setThinkingLevel: (level, persist) => this.ctx.session.setThinkingLevel(level, persist),
|
|
384
400
|
getCommands: () => [],
|
|
401
|
+
getSessionName: () => this.ctx.sessionManager.getSessionName(),
|
|
402
|
+
setSessionName: async name => {
|
|
403
|
+
await this.ctx.sessionManager.setSessionName(name, "user");
|
|
404
|
+
setSessionTerminalTitle(
|
|
405
|
+
this.ctx.sessionManager.getSessionName(),
|
|
406
|
+
this.ctx.sessionManager.getCwd(),
|
|
407
|
+
this.ctx.sessionManager.titleSource,
|
|
408
|
+
);
|
|
409
|
+
},
|
|
385
410
|
};
|
|
386
411
|
const contextActions: ExtensionContextActions = {
|
|
387
412
|
getModel: () => this.ctx.session.model,
|
|
388
|
-
getSearchDb: () => this.ctx.session.searchDb,
|
|
389
413
|
isIdle: () => !this.ctx.session.isStreaming,
|
|
390
414
|
abort: () => this.ctx.session.abort(),
|
|
391
415
|
hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
|
|
@@ -583,7 +607,6 @@ export class ExtensionUiController {
|
|
|
583
607
|
sessionManager: this.ctx.session.sessionManager,
|
|
584
608
|
modelRegistry: this.ctx.session.modelRegistry,
|
|
585
609
|
model: this.ctx.session.model,
|
|
586
|
-
searchDb: this.ctx.session.searchDb,
|
|
587
610
|
isIdle: () => !this.ctx.session.isStreaming,
|
|
588
611
|
hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
|
|
589
612
|
hasQueuedMessages: () => this.ctx.session.queuedMessageCount > 0,
|
|
@@ -342,8 +342,15 @@ export class InputController {
|
|
|
342
342
|
generateSessionTitle(text, registry, this.ctx.settings, this.ctx.session.sessionId, this.ctx.session.model)
|
|
343
343
|
.then(async title => {
|
|
344
344
|
if (title) {
|
|
345
|
-
await this.ctx.sessionManager.setSessionName(title);
|
|
346
|
-
|
|
345
|
+
const applied = await this.ctx.sessionManager.setSessionName(title, "auto");
|
|
346
|
+
if (applied) {
|
|
347
|
+
setSessionTerminalTitle(
|
|
348
|
+
this.ctx.sessionManager.getSessionName()!,
|
|
349
|
+
this.ctx.sessionManager.getCwd(),
|
|
350
|
+
this.ctx.sessionManager.titleSource,
|
|
351
|
+
);
|
|
352
|
+
this.ctx.updateEditorBorderColor();
|
|
353
|
+
}
|
|
347
354
|
}
|
|
348
355
|
})
|
|
349
356
|
.catch(() => {});
|
|
@@ -557,7 +564,6 @@ export class InputController {
|
|
|
557
564
|
return createPromptActionAutocompleteProvider({
|
|
558
565
|
commands,
|
|
559
566
|
basePath,
|
|
560
|
-
searchDb: this.ctx.session.searchDb,
|
|
561
567
|
keybindings: this.ctx.keybindings,
|
|
562
568
|
copyCurrentLine: () => this.handleCopyCurrentLine(),
|
|
563
569
|
copyPrompt: () => this.handleCopyPrompt(),
|
|
@@ -747,8 +747,9 @@ export class SelectorController {
|
|
|
747
747
|
const sessionManager = this.ctx.sessionManager as {
|
|
748
748
|
getSessionName?: () => string | undefined;
|
|
749
749
|
getCwd: () => string;
|
|
750
|
+
titleSource?: "auto" | "user" | undefined;
|
|
750
751
|
};
|
|
751
|
-
setSessionTerminalTitle(sessionManager.getSessionName?.(), sessionManager.getCwd());
|
|
752
|
+
setSessionTerminalTitle(sessionManager.getSessionName?.(), sessionManager.getCwd(), sessionManager.titleSource);
|
|
752
753
|
}
|
|
753
754
|
|
|
754
755
|
async #detachActiveSessionBeforeDeletion(sessionPath: string): Promise<boolean> {
|
|
@@ -767,6 +768,7 @@ export class SelectorController {
|
|
|
767
768
|
this.ctx.statusLine.invalidate();
|
|
768
769
|
this.ctx.statusLine.setSessionStartTime(Date.now());
|
|
769
770
|
this.ctx.updateEditorTopBorder();
|
|
771
|
+
this.ctx.updateEditorBorderColor();
|
|
770
772
|
this.ctx.renderInitialMessages();
|
|
771
773
|
await this.ctx.reloadTodos();
|
|
772
774
|
this.ctx.ui.requestRender();
|
|
@@ -779,6 +781,7 @@ export class SelectorController {
|
|
|
779
781
|
// Switch session via AgentSession (emits hook and tool session events)
|
|
780
782
|
await this.ctx.session.switchSession(sessionPath);
|
|
781
783
|
this.#refreshSessionTerminalTitle();
|
|
784
|
+
this.ctx.updateEditorBorderColor();
|
|
782
785
|
|
|
783
786
|
// Clear and re-render the chat
|
|
784
787
|
this.ctx.chatContainer.clear();
|
|
@@ -39,6 +39,7 @@ import { STTController, type SttState } from "../stt";
|
|
|
39
39
|
import type { ExitPlanModeDetails, LspStartupServerInfo } from "../tools";
|
|
40
40
|
import type { EventBus } from "../utils/event-bus";
|
|
41
41
|
import { getEditorCommand, openInEditor } from "../utils/external-editor";
|
|
42
|
+
import { getSessionAccentAnsi, getSessionAccentHexForTitle } from "../utils/session-color";
|
|
42
43
|
import { popTerminalTitle, pushTerminalTitle, setSessionTerminalTitle } from "../utils/title-generator";
|
|
43
44
|
import type { AssistantMessageComponent } from "./components/assistant-message";
|
|
44
45
|
import type { BashExecutionComponent } from "./components/bash-execution";
|
|
@@ -392,7 +393,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
392
393
|
// Start the UI
|
|
393
394
|
this.ui.start();
|
|
394
395
|
pushTerminalTitle();
|
|
395
|
-
setSessionTerminalTitle(
|
|
396
|
+
setSessionTerminalTitle(
|
|
397
|
+
this.sessionManager.getSessionName(),
|
|
398
|
+
this.sessionManager.getCwd(),
|
|
399
|
+
this.sessionManager.titleSource,
|
|
400
|
+
);
|
|
401
|
+
this.updateEditorBorderColor();
|
|
396
402
|
this.#syncEditorMaxHeight();
|
|
397
403
|
this.isInitialized = true;
|
|
398
404
|
this.ui.requestRender(true);
|
|
@@ -531,8 +537,14 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
531
537
|
} else if (this.isPythonMode) {
|
|
532
538
|
this.editor.borderColor = theme.getPythonModeBorderColor();
|
|
533
539
|
} else {
|
|
534
|
-
const
|
|
535
|
-
|
|
540
|
+
const hex = getSessionAccentHexForTitle(this.sessionManager.getSessionName(), this.sessionManager.titleSource);
|
|
541
|
+
const ansi = getSessionAccentAnsi(hex);
|
|
542
|
+
if (ansi) {
|
|
543
|
+
this.editor.borderColor = (str: string) => `${ansi}${str}\x1b[39m`;
|
|
544
|
+
} else {
|
|
545
|
+
const level = this.session.thinkingLevel ?? ThinkingLevel.Off;
|
|
546
|
+
this.editor.borderColor = theme.getThinkingBorderColor(level);
|
|
547
|
+
}
|
|
536
548
|
}
|
|
537
549
|
this.updateEditorTopBorder();
|
|
538
550
|
this.ui.requestRender();
|
|
@@ -1302,6 +1314,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1302
1314
|
return this.#commandController.handleMoveCommand(targetPath);
|
|
1303
1315
|
}
|
|
1304
1316
|
|
|
1317
|
+
handleRenameCommand(title: string): Promise<void> {
|
|
1318
|
+
return this.#commandController.handleRenameCommand(title);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1305
1321
|
handleMemoryCommand(text: string): Promise<void> {
|
|
1306
1322
|
return this.#commandController.handleMemoryCommand(text);
|
|
1307
1323
|
}
|
package/src/modes/print-mode.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - `omp --mode json "prompt"` - JSON event stream
|
|
7
7
|
*/
|
|
8
8
|
import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import { sanitizeText } from "@oh-my-pi/pi-natives";
|
|
9
10
|
import type { AgentSession } from "../session/agent-session";
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -72,11 +73,14 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
72
73
|
},
|
|
73
74
|
getThinkingLevel: () => session.thinkingLevel,
|
|
74
75
|
setThinkingLevel: level => session.setThinkingLevel(level),
|
|
76
|
+
getSessionName: () => session.sessionManager.getSessionName(),
|
|
77
|
+
setSessionName: async name => {
|
|
78
|
+
await session.sessionManager.setSessionName(name, "user");
|
|
79
|
+
},
|
|
75
80
|
},
|
|
76
81
|
// ExtensionContextActions
|
|
77
82
|
{
|
|
78
83
|
getModel: () => session.model,
|
|
79
|
-
getSearchDb: () => session.searchDb,
|
|
80
84
|
isIdle: () => !session.isStreaming,
|
|
81
85
|
abort: () => session.abort(),
|
|
82
86
|
hasPendingMessages: () => session.queuedMessageCount > 0,
|
|
@@ -166,14 +170,19 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
166
170
|
|
|
167
171
|
// Check for error/aborted
|
|
168
172
|
if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") {
|
|
169
|
-
|
|
170
|
-
process.
|
|
173
|
+
const errorLine = sanitizeText(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);
|
|
174
|
+
const flushed = process.stderr.write(`${errorLine}\n`);
|
|
175
|
+
if (flushed) {
|
|
176
|
+
process.exit(1);
|
|
177
|
+
} else {
|
|
178
|
+
process.stderr.once("drain", () => process.exit(1));
|
|
179
|
+
}
|
|
171
180
|
}
|
|
172
181
|
|
|
173
182
|
// Output text content
|
|
174
183
|
for (const content of assistantMsg.content) {
|
|
175
184
|
if (content.type === "text") {
|
|
176
|
-
process.stdout.write(`${content.text}\n`);
|
|
185
|
+
process.stdout.write(`${sanitizeText(content.text)}\n`);
|
|
177
186
|
}
|
|
178
187
|
}
|
|
179
188
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { SearchDb } from "@oh-my-pi/pi-natives";
|
|
2
1
|
import {
|
|
3
2
|
type AutocompleteItem,
|
|
4
3
|
type AutocompleteProvider,
|
|
@@ -24,7 +23,6 @@ interface PromptActionAutocompleteItem extends AutocompleteItem {
|
|
|
24
23
|
interface PromptActionAutocompleteOptions {
|
|
25
24
|
commands: SlashCommand[];
|
|
26
25
|
basePath: string;
|
|
27
|
-
searchDb?: SearchDb;
|
|
28
26
|
keybindings: KeybindingsManager;
|
|
29
27
|
copyCurrentLine: () => void;
|
|
30
28
|
copyPrompt: () => void;
|
|
@@ -92,8 +90,8 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
92
90
|
#baseProvider: CombinedAutocompleteProvider;
|
|
93
91
|
#actions: PromptActionDefinition[];
|
|
94
92
|
|
|
95
|
-
constructor(commands: SlashCommand[], basePath: string, actions: PromptActionDefinition[]
|
|
96
|
-
this.#baseProvider = new CombinedAutocompleteProvider(commands, basePath
|
|
93
|
+
constructor(commands: SlashCommand[], basePath: string, actions: PromptActionDefinition[]) {
|
|
94
|
+
this.#baseProvider = new CombinedAutocompleteProvider(commands, basePath);
|
|
97
95
|
this.#actions = actions;
|
|
98
96
|
}
|
|
99
97
|
|
|
@@ -229,5 +227,5 @@ export function createPromptActionAutocompleteProvider(
|
|
|
229
227
|
},
|
|
230
228
|
];
|
|
231
229
|
|
|
232
|
-
return new PromptActionAutocompleteProvider(options.commands, options.basePath, actions
|
|
230
|
+
return new PromptActionAutocompleteProvider(options.commands, options.basePath, actions);
|
|
233
231
|
}
|
|
@@ -440,11 +440,14 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
440
440
|
},
|
|
441
441
|
getThinkingLevel: () => session.thinkingLevel,
|
|
442
442
|
setThinkingLevel: level => session.setThinkingLevel(level),
|
|
443
|
+
getSessionName: () => session.sessionManager.getSessionName(),
|
|
444
|
+
setSessionName: async name => {
|
|
445
|
+
await session.sessionManager.setSessionName(name, "user");
|
|
446
|
+
},
|
|
443
447
|
},
|
|
444
448
|
// ExtensionContextActions
|
|
445
449
|
{
|
|
446
450
|
getModel: () => session.agent.state.model,
|
|
447
|
-
getSearchDb: () => session.searchDb,
|
|
448
451
|
isIdle: () => !session.isStreaming,
|
|
449
452
|
abort: () => session.abort(),
|
|
450
453
|
hasPendingMessages: () => session.queuedMessageCount > 0,
|
|
@@ -751,7 +754,10 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
751
754
|
if (!name) {
|
|
752
755
|
return error(id, "set_session_name", "Session name cannot be empty");
|
|
753
756
|
}
|
|
754
|
-
session.setSessionName(name);
|
|
757
|
+
const applied = await session.setSessionName(name, "user");
|
|
758
|
+
if (!applied) {
|
|
759
|
+
return error(id, "set_session_name", "Session name cannot be empty");
|
|
760
|
+
}
|
|
755
761
|
return success(id, "set_session_name");
|
|
756
762
|
}
|
|
757
763
|
|
package/src/modes/shared.ts
CHANGED
|
@@ -5,10 +5,10 @@ import { theme } from "./theme/theme";
|
|
|
5
5
|
// Text Sanitization
|
|
6
6
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
7
7
|
|
|
8
|
-
/** Sanitize text for display in a single-line status.
|
|
8
|
+
/** Sanitize text for display in a single-line status. Strips C0/C1 control characters (including ANSI ESC), collapses whitespace, trims. */
|
|
9
9
|
export function sanitizeStatusText(text: string): string {
|
|
10
10
|
return text
|
|
11
|
-
.replace(/[\
|
|
11
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
|
|
12
12
|
.replace(/ +/g, " ")
|
|
13
13
|
.trim();
|
|
14
14
|
}
|
package/src/modes/types.ts
CHANGED
|
@@ -187,6 +187,7 @@ export interface InteractiveModeContext {
|
|
|
187
187
|
handleCompactCommand(customInstructions?: string): Promise<void>;
|
|
188
188
|
handleHandoffCommand(customInstructions?: string): Promise<void>;
|
|
189
189
|
handleMoveCommand(targetPath: string): Promise<void>;
|
|
190
|
+
handleRenameCommand(title: string): Promise<void>;
|
|
190
191
|
handleMemoryCommand(text: string): Promise<void>;
|
|
191
192
|
handleSTTToggle(): Promise<void>;
|
|
192
193
|
executeCompaction(customInstructionsOrOptions?: string | CompactOptions, isAuto?: boolean): Promise<void>;
|