@oh-my-pi/pi-coding-agent 14.0.5 → 14.1.1
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 +120 -0
- package/package.json +8 -8
- package/src/async/index.ts +1 -0
- package/src/async/job-manager.ts +43 -10
- package/src/async/support.ts +5 -0
- package/src/cli/list-models.ts +96 -57
- package/src/commit/agentic/tools/analyze-file.ts +1 -2
- package/src/commit/model-selection.ts +16 -13
- package/src/config/mcp-schema.json +1 -1
- package/src/config/model-equivalence.ts +675 -0
- package/src/config/model-registry.ts +242 -45
- package/src/config/model-resolver.ts +282 -65
- package/src/config/settings-schema.ts +27 -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.css +82 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +614 -97
- 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 +4 -4
- package/src/internal-urls/jobs-protocol.ts +2 -1
- package/src/ipy/executor.ts +447 -52
- package/src/ipy/kernel.ts +39 -13
- package/src/lsp/client.ts +55 -1
- package/src/lsp/index.ts +8 -0
- package/src/lsp/types.ts +6 -0
- package/src/main.ts +6 -2
- package/src/memories/index.ts +7 -6
- package/src/modes/acp/acp-agent.ts +4 -1
- package/src/modes/components/bash-execution.ts +16 -4
- package/src/modes/components/model-selector.ts +221 -64
- 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 +42 -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 +17 -6
- 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/system/system-prompt.md +5 -1
- package/src/prompts/tools/bash.md +16 -1
- package/src/prompts/tools/cancel-job.md +1 -1
- 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 +12 -3
- package/src/prompts/tools/read.md +9 -0
- package/src/prompts/tools/task.md +2 -2
- package/src/prompts/tools/vim.md +98 -0
- package/src/prompts/tools/write.md +1 -0
- package/src/sdk.ts +758 -725
- package/src/session/agent-session.ts +187 -40
- package/src/session/session-manager.ts +50 -4
- package/src/slash-commands/builtin-registry.ts +17 -0
- package/src/task/executor.ts +9 -5
- package/src/task/index.ts +3 -5
- package/src/task/types.ts +2 -2
- package/src/tools/bash.ts +240 -57
- package/src/tools/cancel-job.ts +2 -1
- package/src/tools/find.ts +5 -2
- package/src/tools/grep.ts +77 -8
- package/src/tools/index.ts +48 -19
- package/src/tools/inspect-image.ts +1 -1
- package/src/tools/{await-tool.ts → poll-tool.ts} +38 -31
- package/src/tools/python.ts +293 -278
- package/src/tools/read.ts +218 -1
- package/src/tools/sqlite-reader.ts +623 -0
- 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/tools/write.ts +187 -1
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/edit-mode.ts +2 -1
- package/src/utils/git.ts +24 -1
- package/src/utils/session-color.ts +55 -0
- package/src/utils/title-generator.ts +16 -7
- 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
|
@@ -9,6 +9,7 @@ import { theme } from "../../modes/theme/theme";
|
|
|
9
9
|
import type { AgentSession } from "../../session/agent-session";
|
|
10
10
|
import { calculatePromptTokens } from "../../session/compaction/compaction";
|
|
11
11
|
import * as git from "../../utils/git";
|
|
12
|
+
import { getSessionAccentAnsi, getSessionAccentHexForTitle } from "../../utils/session-color";
|
|
12
13
|
import { sanitizeStatusText } from "../shared";
|
|
13
14
|
import {
|
|
14
15
|
canReuseCachedPr,
|
|
@@ -471,7 +472,12 @@ export class StatusLineComponent implements Component {
|
|
|
471
472
|
leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
|
|
472
473
|
rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
|
|
473
474
|
const gapWidth = Math.max(1, topFillWidth - leftWidth - rightWidth);
|
|
474
|
-
const
|
|
475
|
+
const accentHex = getSessionAccentHexForTitle(
|
|
476
|
+
this.session.sessionManager?.getSessionName(),
|
|
477
|
+
this.session.sessionManager?.titleSource,
|
|
478
|
+
);
|
|
479
|
+
const gapColor = getSessionAccentAnsi(accentHex) ?? theme.getFgAnsi("border");
|
|
480
|
+
const gapFill = `${gapColor}${theme.boxRound.horizontal.repeat(gapWidth)}\x1b[39m`;
|
|
475
481
|
return leftGroup + gapFill + rightGroup;
|
|
476
482
|
}
|
|
477
483
|
|
|
@@ -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));
|
|
@@ -1002,6 +1025,14 @@ function formatAccountLabel(limit: UsageLimit, report: UsageReport, index: numbe
|
|
|
1002
1025
|
return `account ${index + 1}`;
|
|
1003
1026
|
}
|
|
1004
1027
|
|
|
1028
|
+
function formatUnlimitedReportLabel(report: UsageReport, index: number): string {
|
|
1029
|
+
const email = report.metadata?.email as string | undefined;
|
|
1030
|
+
if (email) return email;
|
|
1031
|
+
const accountId = report.metadata?.accountId as string | undefined;
|
|
1032
|
+
if (accountId) return accountId;
|
|
1033
|
+
return `account ${index + 1}`;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1005
1036
|
function formatResetShort(limit: UsageLimit, nowMs: number): string | undefined {
|
|
1006
1037
|
if (limit.window?.resetsAt !== undefined) {
|
|
1007
1038
|
return formatDuration(limit.window.resetsAt - nowMs);
|
|
@@ -1188,6 +1219,16 @@ function renderUsageReports(reports: UsageReport[], uiTheme: typeof theme, nowMs
|
|
|
1188
1219
|
}
|
|
1189
1220
|
}
|
|
1190
1221
|
|
|
1222
|
+
// Render accounts with no rate limits (e.g. business/enterprise plans).
|
|
1223
|
+
const unlimitedReports = providerReports.filter(report => report.limits.length === 0);
|
|
1224
|
+
for (const report of unlimitedReports) {
|
|
1225
|
+
const label = formatUnlimitedReportLabel(report, 0);
|
|
1226
|
+
const tier = report.metadata?.planType as string | undefined;
|
|
1227
|
+
const tierSuffix = tier ? ` ${uiTheme.fg("dim", `(${tier})`)}` : "";
|
|
1228
|
+
lines.push(
|
|
1229
|
+
`${uiTheme.fg("success", uiTheme.status.success)} ${label}${tierSuffix} ${uiTheme.fg("dim", "-- no limits")}`,
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1191
1232
|
// No per-provider footer; global header shows last check.
|
|
1192
1233
|
}
|
|
1193
1234
|
|
|
@@ -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(),
|
|
@@ -7,6 +7,7 @@ import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
|
7
7
|
import { getAgentDbPath, getConfigDirName, getProjectDir } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import { invalidate as invalidateFsCache } from "../../capability/fs";
|
|
9
9
|
import { getRoleInfo } from "../../config/model-registry";
|
|
10
|
+
import { formatModelSelectorValue } from "../../config/model-resolver";
|
|
10
11
|
import { settings } from "../../config/settings";
|
|
11
12
|
import { DebugSelectorComponent } from "../../debug";
|
|
12
13
|
import { disableProvider, enableProvider } from "../../discovery";
|
|
@@ -387,31 +388,38 @@ export class SelectorController {
|
|
|
387
388
|
this.ctx.settings,
|
|
388
389
|
this.ctx.session.modelRegistry,
|
|
389
390
|
this.ctx.session.scopedModels,
|
|
390
|
-
async (model, role, thinkingLevel) => {
|
|
391
|
+
async (model, role, thinkingLevel, selector) => {
|
|
391
392
|
try {
|
|
392
393
|
if (role === null) {
|
|
393
394
|
// Temporary: update agent state but don't persist to settings
|
|
394
395
|
await this.ctx.session.setModelTemporary(model);
|
|
395
396
|
this.ctx.statusLine.invalidate();
|
|
396
397
|
this.ctx.updateEditorBorderColor();
|
|
397
|
-
this.ctx.showStatus(`Temporary model: ${model.id}`);
|
|
398
|
+
this.ctx.showStatus(`Temporary model: ${selector ?? model.id}`);
|
|
398
399
|
done();
|
|
399
400
|
this.ctx.ui.requestRender();
|
|
400
401
|
} else if (role === "default") {
|
|
401
402
|
// Default: update agent state and persist
|
|
402
|
-
await this.ctx.session.setModel(model, role
|
|
403
|
+
await this.ctx.session.setModel(model, role, {
|
|
404
|
+
selector,
|
|
405
|
+
thinkingLevel,
|
|
406
|
+
});
|
|
403
407
|
if (thinkingLevel && thinkingLevel !== ThinkingLevel.Inherit) {
|
|
404
408
|
this.ctx.session.setThinkingLevel(thinkingLevel);
|
|
405
409
|
}
|
|
406
410
|
this.ctx.statusLine.invalidate();
|
|
407
411
|
this.ctx.updateEditorBorderColor();
|
|
408
|
-
this.ctx.showStatus(`Default model: ${model.id}`);
|
|
412
|
+
this.ctx.showStatus(`Default model: ${selector ?? model.id}`);
|
|
409
413
|
// Don't call done() - selector stays open for role assignment
|
|
410
414
|
} else {
|
|
411
415
|
// Other roles (smol, slow): just update settings, not current model
|
|
416
|
+
this.ctx.settings.setModelRole(
|
|
417
|
+
role,
|
|
418
|
+
formatModelSelectorValue(selector ?? `${model.provider}/${model.id}`, thinkingLevel),
|
|
419
|
+
);
|
|
412
420
|
const roleInfo = getRoleInfo(role, settings);
|
|
413
421
|
const roleLabel = roleInfo?.name ?? role;
|
|
414
|
-
this.ctx.showStatus(`${roleLabel} model: ${model.id}`);
|
|
422
|
+
this.ctx.showStatus(`${roleLabel} model: ${selector ?? model.id}`);
|
|
415
423
|
// Don't call done() - selector stays open
|
|
416
424
|
}
|
|
417
425
|
} catch (error) {
|
|
@@ -739,8 +747,9 @@ export class SelectorController {
|
|
|
739
747
|
const sessionManager = this.ctx.sessionManager as {
|
|
740
748
|
getSessionName?: () => string | undefined;
|
|
741
749
|
getCwd: () => string;
|
|
750
|
+
titleSource?: "auto" | "user" | undefined;
|
|
742
751
|
};
|
|
743
|
-
setSessionTerminalTitle(sessionManager.getSessionName?.(), sessionManager.getCwd());
|
|
752
|
+
setSessionTerminalTitle(sessionManager.getSessionName?.(), sessionManager.getCwd(), sessionManager.titleSource);
|
|
744
753
|
}
|
|
745
754
|
|
|
746
755
|
async #detachActiveSessionBeforeDeletion(sessionPath: string): Promise<boolean> {
|
|
@@ -759,6 +768,7 @@ export class SelectorController {
|
|
|
759
768
|
this.ctx.statusLine.invalidate();
|
|
760
769
|
this.ctx.statusLine.setSessionStartTime(Date.now());
|
|
761
770
|
this.ctx.updateEditorTopBorder();
|
|
771
|
+
this.ctx.updateEditorBorderColor();
|
|
762
772
|
this.ctx.renderInitialMessages();
|
|
763
773
|
await this.ctx.reloadTodos();
|
|
764
774
|
this.ctx.ui.requestRender();
|
|
@@ -771,6 +781,7 @@ export class SelectorController {
|
|
|
771
781
|
// Switch session via AgentSession (emits hook and tool session events)
|
|
772
782
|
await this.ctx.session.switchSession(sessionPath);
|
|
773
783
|
this.#refreshSessionTerminalTitle();
|
|
784
|
+
this.ctx.updateEditorBorderColor();
|
|
774
785
|
|
|
775
786
|
// Clear and re-render the chat
|
|
776
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
|
}
|