@oh-my-pi/pi-tui 18.4.4 → 18.4.5
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 +16 -0
- package/dist/types/overlays/model-hub.d.ts +2 -0
- package/dist/types/render/render-utils.d.ts +5 -0
- package/dist/types/tools/edit.d.ts +2 -0
- package/dist/types/tools/task.d.ts +7 -0
- package/package.json +9 -9
- package/src/components/editor.ts +27 -2
- package/src/overlays/model-browser.ts +56 -12
- package/src/overlays/model-hub.ts +82 -29
- package/src/render/render-utils.ts +51 -3
- package/src/tools/edit.ts +23 -0
- package/src/tools/task.ts +29 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.4.5] - 2026-09-30
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added Factory Droid base-credit badges; models without a dollar-price reference no longer appear free ([#8577](https://github.com/can1357/oh-my-pi/pull/8577) by [@will-bogusz](https://github.com/will-bogusz), continued in [#13276](https://github.com/can1357/oh-my-pi/pull/13276) by [@DusKing1](https://github.com/DusKing1)).
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed home directories next to shell redirections, control operators or Markdown emphasis (`<`, `>`, `&`, `|`, `*`, `_`) leaking the full path in display-only text ([#11210](https://github.com/can1357/oh-my-pi/pull/11210) by [@DarkPhilosophy](https://github.com/DarkPhilosophy)).
|
|
14
|
+
- Fixed the subagent task card showing full home paths in tool intents and arguments, while keeping search patterns literal ([#11210](https://github.com/can1357/oh-my-pi/pull/11210) by [@DarkPhilosophy](https://github.com/DarkPhilosophy)).
|
|
15
|
+
- Fixed Enter doing nothing on a fully typed slash-command argument while its completion popup was open (e.g. `/mcp list`): it now submits instead of re-accepting the identical completion; subcommands that still need a required argument (e.g. `/mcp test`) keep inserting the subcommand so you can type it ([#13885](https://github.com/can1357/oh-my-pi/pull/13885) by [@H4vC](https://github.com/H4vC)).
|
|
16
|
+
|
|
5
17
|
## [18.4.4] - 2026-09-29
|
|
6
18
|
|
|
7
19
|
### Added
|
|
@@ -11,6 +23,10 @@
|
|
|
11
23
|
- `HookEditorComponent` accepts pasted images when constructed with `acceptImages`; the ask dialog returns them as `customInputImages` / `noteImages` ([#13774](https://github.com/can1357/oh-my-pi/pull/13774) by [@DrFaustus-vic](https://github.com/DrFaustus-vic))
|
|
12
24
|
- Added `formatFileMatches` and `FileMatchSection` to `tools/grouped-file-output` for rendering per-file grep/ast-grep matches in grouped or flat mode.
|
|
13
25
|
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- The model browser shows `varies`, `included`, or `pricing unknown` for models whose catalog declares that state, instead of labeling them `free` ([#11613](https://github.com/can1357/oh-my-pi/pull/11613) by [@will-bogusz](https://github.com/will-bogusz)).
|
|
29
|
+
|
|
14
30
|
## [18.4.3] - 2026-09-28
|
|
15
31
|
|
|
16
32
|
### Added
|
|
@@ -46,6 +46,8 @@ export interface ModelHubCallbacks {
|
|
|
46
46
|
onFallbackChainChange?: (role: string, chain: string[]) => void;
|
|
47
47
|
/** Locked provider activation: forward to the /login flow. */
|
|
48
48
|
onLoginRequest?: (providerId: string) => void;
|
|
49
|
+
/** Save the current role assignments and default thinking level as a named model preset. */
|
|
50
|
+
onSavePreset?: (name: string) => void;
|
|
49
51
|
/** Persist a new quick-switch cycle order (the ctrl+p role cycle). */
|
|
50
52
|
onCycleOrderChange?: (order: string[]) => void;
|
|
51
53
|
onCancel: () => void;
|
|
@@ -239,6 +239,11 @@ export declare function truncateDiffByHunk(diffText: string, maxHunks: number, m
|
|
|
239
239
|
export declare function shortenPath(filePath: unknown, homeDir?: string): string;
|
|
240
240
|
/** Shorten embedded home paths; normalize Windows separators unless the caller preserves native error text. */
|
|
241
241
|
export declare function shortenEmbeddedPaths(text: string, homeDir?: string, preserveSeparators?: boolean): string;
|
|
242
|
+
/**
|
|
243
|
+
* Shorten filesystem and command arguments without rewriting literal search patterns. A producer that
|
|
244
|
+
* names no argument key gets the general embedded-path shortening.
|
|
245
|
+
*/
|
|
246
|
+
export declare function shortenToolArgumentPaths(text: string, key: string | undefined, homeDir?: string): string;
|
|
242
247
|
/** Sanitize warning text before showing it in TUI, including embedded home paths. */
|
|
243
248
|
export declare function sanitizeDisplayWarning(text: string): string;
|
|
244
249
|
/** Sanitize and bound warning text before showing it in TUI. */
|
|
@@ -136,6 +136,8 @@ export interface EditRenderContext {
|
|
|
136
136
|
}
|
|
137
137
|
/** Render replacement text until a computed diff preview is available. */
|
|
138
138
|
export declare function renderStreamingFallback(editMode: EditMode, args: unknown, theme: Theme): string;
|
|
139
|
+
/** Extract display targets using the existing parsers for supported freeform edit modes. */
|
|
140
|
+
export declare function getEditInputPaths(input: string, resolvedMode?: EditMode): readonly string[];
|
|
139
141
|
/** Render edit previews and completed file diffs. */
|
|
140
142
|
export declare const editToolRenderer: {
|
|
141
143
|
mergeCallAndResult: true;
|
|
@@ -209,10 +209,17 @@ export interface AgentProgress {
|
|
|
209
209
|
lastIntent?: string;
|
|
210
210
|
currentTool?: string;
|
|
211
211
|
currentToolArgs?: string;
|
|
212
|
+
/** Argument key selected for the display preview, when known. */
|
|
213
|
+
currentToolArgsKey?: string;
|
|
214
|
+
/** Intent the model attached to the current call; undefined when that call carried none. */
|
|
215
|
+
currentToolIntent?: string;
|
|
212
216
|
currentToolStartMs?: number;
|
|
213
217
|
recentTools: Array<{
|
|
214
218
|
tool: string;
|
|
215
219
|
args: string;
|
|
220
|
+
argsKey?: string;
|
|
221
|
+
intent?: string;
|
|
222
|
+
isError?: boolean;
|
|
216
223
|
endMs: number;
|
|
217
224
|
}>;
|
|
218
225
|
recentOutput: string[];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "18.4.
|
|
4
|
+
"version": "18.4.5",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": {
|
|
@@ -41,14 +41,14 @@
|
|
|
41
41
|
"gen:glyphs": "bun scripts/gen-glyphs.ts"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@oh-my-pi/omptype": "18.4.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "18.4.
|
|
46
|
-
"@oh-my-pi/pi-ai": "18.4.
|
|
47
|
-
"@oh-my-pi/pi-catalog": "18.4.
|
|
48
|
-
"@oh-my-pi/pi-natives": "18.4.
|
|
49
|
-
"@oh-my-pi/pi-utils": "18.4.
|
|
50
|
-
"@oh-my-pi/pi-wire": "18.4.
|
|
51
|
-
"@oh-my-pi/snapcompact": "18.4.
|
|
44
|
+
"@oh-my-pi/omptype": "18.4.5",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "18.4.5",
|
|
46
|
+
"@oh-my-pi/pi-ai": "18.4.5",
|
|
47
|
+
"@oh-my-pi/pi-catalog": "18.4.5",
|
|
48
|
+
"@oh-my-pi/pi-natives": "18.4.5",
|
|
49
|
+
"@oh-my-pi/pi-utils": "18.4.5",
|
|
50
|
+
"@oh-my-pi/pi-wire": "18.4.5",
|
|
51
|
+
"@oh-my-pi/snapcompact": "18.4.5"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"kitty-vt-wasm": "^0.2.0"
|
package/src/components/editor.ts
CHANGED
|
@@ -1949,8 +1949,14 @@ export class Editor implements Component, Focusable {
|
|
|
1949
1949
|
const currentTextBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
1950
1950
|
// A narrowed `@` list can be empty while its refresh is pending; Enter
|
|
1951
1951
|
// then submits instead of waiting on the search.
|
|
1952
|
-
if (
|
|
1953
|
-
|
|
1952
|
+
if (
|
|
1953
|
+
!selected ||
|
|
1954
|
+
!this.#autocompletePrefixMatchesCursorText(currentTextBeforeCursor, selected) ||
|
|
1955
|
+
this.#selectedSlashArgumentIsAlreadyTyped(selected)
|
|
1956
|
+
) {
|
|
1957
|
+
// Stale, or accepting would only append whitespace to an already fully
|
|
1958
|
+
// typed slash-command argument (`/mcp list` + Enter): cancel and fall
|
|
1959
|
+
// through to normal submission instead of swallowing the keypress.
|
|
1954
1960
|
this.#cancelAutocomplete();
|
|
1955
1961
|
} else {
|
|
1956
1962
|
if (selected && this.#autocompleteProvider) {
|
|
@@ -4212,6 +4218,25 @@ export class Editor implements Component, Focusable {
|
|
|
4212
4218
|
return this.#autocompleteList?.getSelectedItem()?.value === SKILL_NAMESPACE;
|
|
4213
4219
|
}
|
|
4214
4220
|
|
|
4221
|
+
/**
|
|
4222
|
+
* Whether the selected completion for a submitted slash command's argument
|
|
4223
|
+
* only restates what the user already typed (e.g. `list ` for `/mcp list`).
|
|
4224
|
+
* Accepting it would change nothing visible, so Enter should submit.
|
|
4225
|
+
*
|
|
4226
|
+
* A selection whose usage hint still names a required `<arg>` outside any
|
|
4227
|
+
* optional `[...]` group (e.g. `test` with `<name>`) keeps Enter's accept
|
|
4228
|
+
* role, so the user continues into the argument instead of submitting a
|
|
4229
|
+
* command the handler can only reject.
|
|
4230
|
+
*/
|
|
4231
|
+
#selectedSlashArgumentIsAlreadyTyped(selected: AutocompleteItem): boolean {
|
|
4232
|
+
if (!this.#isInSubmittedSlashCommandContext()) return false;
|
|
4233
|
+
const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
|
|
4234
|
+
if (this.#state.cursorCol !== currentLine.length) return false;
|
|
4235
|
+
if (selected.hint?.replace(/\[[^\]]*\]/g, "").includes("<")) return false;
|
|
4236
|
+
const typed = this.#autocompletePrefix.trimEnd();
|
|
4237
|
+
return typed.length > 0 && selected.value.trimEnd() === typed;
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4215
4240
|
#isSlashCommandNameAutocompleteSelection(): boolean {
|
|
4216
4241
|
if (this.#autocompleteState !== "regular") {
|
|
4217
4242
|
return false;
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
11
11
|
import type { Model } from "@oh-my-pi/pi-ai";
|
|
12
12
|
import { buildModel } from "@oh-my-pi/pi-catalog/build";
|
|
13
|
-
import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
14
|
-
import type { ModelKind } from "@oh-my-pi/pi-catalog/types";
|
|
13
|
+
import { getModelPricingStatus, modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
14
|
+
import type { ModelKind, ModelPricingStatus } from "@oh-my-pi/pi-catalog/types";
|
|
15
15
|
import type { Component } from "../tui";
|
|
16
16
|
import { fuzzyRank } from "../fuzzy";
|
|
17
17
|
import { Input } from "../components/input";
|
|
@@ -619,10 +619,23 @@ function roleChipSpans(role: string, assignment: RoleAssignment, settings: Model
|
|
|
619
619
|
return spans;
|
|
620
620
|
}
|
|
621
621
|
|
|
622
|
-
/**
|
|
622
|
+
/**
|
|
623
|
+
* The non-rate pricing state a catalog rule declared for a zero-rate row
|
|
624
|
+
* (`pricing-status`), or `undefined` when the row publishes rates or declares
|
|
625
|
+
* nothing. Undeclared zero-rate rows keep the `free` convention below.
|
|
626
|
+
*/
|
|
627
|
+
function declaredPricingStatus(model: Model): Exclude<ModelPricingStatus, "fixed"> | undefined {
|
|
628
|
+
if (model.pricingStatus === undefined) return undefined;
|
|
629
|
+
const status = getModelPricingStatus(model);
|
|
630
|
+
return status === "fixed" ? undefined : status;
|
|
631
|
+
}
|
|
632
|
+
/** No token price and no subscription-credit charge (unless a pricing state is declared). */
|
|
623
633
|
function isFreeModel(model: Model): boolean {
|
|
634
|
+
const declared = declaredPricingStatus(model);
|
|
635
|
+
if (declared !== undefined) return declared === "free";
|
|
624
636
|
const cost = model.cost;
|
|
625
|
-
|
|
637
|
+
const credits = model.factoryDroidCredits;
|
|
638
|
+
return (!cost || (cost.input === 0 && cost.output === 0)) && (credits === undefined || credits === 0);
|
|
626
639
|
}
|
|
627
640
|
|
|
628
641
|
/** One per-million price leg: `3`, `0.25`, `12.5`; `?` when unknown. */
|
|
@@ -635,10 +648,37 @@ function formatCostLeg(n: number): string {
|
|
|
635
648
|
return s.includes(".") ? s.replace(/\.?0+$/, "") : s;
|
|
636
649
|
}
|
|
637
650
|
|
|
638
|
-
/**
|
|
651
|
+
/**
|
|
652
|
+
* Adds Factory Droid's `N×` base Standard Credits rate to a dollar price,
|
|
653
|
+
* replacing the price when the model has no dollar reference. Neither the
|
|
654
|
+
* reference price nor the base credit rate includes live promotions.
|
|
655
|
+
*/
|
|
656
|
+
function withCreditBadge(model: Model, price: string): string {
|
|
657
|
+
const credits = model.factoryDroidCredits;
|
|
658
|
+
if (credits === undefined) return price;
|
|
659
|
+
const badge = `${formatCostLeg(credits)}×`;
|
|
660
|
+
return model.cost.input !== 0 || model.cost.output !== 0 ? `${price} ${badge}` : badge;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const PRICING_STATUS_LABELS: Record<Exclude<ModelPricingStatus, "fixed">, { short: string; detail: string }> = {
|
|
664
|
+
free: { short: "free", detail: "free" },
|
|
665
|
+
included: { short: "included", detail: "included" },
|
|
666
|
+
variable: { short: "varies", detail: "price varies" },
|
|
667
|
+
unknown: { short: "unknown", detail: "pricing unknown" },
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
/** `$in/out` per-million cost pair with any credit badge; `free` when nothing is charged; a declared pricing state otherwise. */
|
|
639
671
|
function formatCostPair(model: Model): string {
|
|
672
|
+
const declared = declaredPricingStatus(model);
|
|
673
|
+
if (declared !== undefined) return PRICING_STATUS_LABELS[declared].short;
|
|
640
674
|
if (isFreeModel(model)) return "free";
|
|
641
|
-
return `$${formatCostLeg(model.cost.input)}/${formatCostLeg(model.cost.output)}
|
|
675
|
+
return withCreditBadge(model, `$${formatCostLeg(model.cost.input)}/${formatCostLeg(model.cost.output)}`);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** Detail-pane price fact: `$3/15 per M`, or the declared pricing state in words. */
|
|
679
|
+
function formatCostDetail(model: Model): string {
|
|
680
|
+
const declared = declaredPricingStatus(model);
|
|
681
|
+
return declared !== undefined ? PRICING_STATUS_LABELS[declared].detail : `${formatCostPair(model)} per M`;
|
|
642
682
|
}
|
|
643
683
|
|
|
644
684
|
/** Fact columns of a model picker (Stencil `NATIVE_REDESIGN.md` §4.7); the lowest priority hides first. */
|
|
@@ -649,18 +689,22 @@ export const MODEL_PICKER_COLUMNS: readonly TspPickerColumn[] = [
|
|
|
649
689
|
{ id: "price", head: "$/M", format: "price", priority: 3 },
|
|
650
690
|
];
|
|
651
691
|
|
|
652
|
-
/** `$3·15` price fact of a picker row
|
|
692
|
+
/** `$3·15` price fact of a picker row; `free` at zero cost; a declared pricing state otherwise. */
|
|
653
693
|
function pickerPrice(model: Model): string {
|
|
694
|
+
const declared = declaredPricingStatus(model);
|
|
695
|
+
if (declared !== undefined) return PRICING_STATUS_LABELS[declared].short;
|
|
654
696
|
if (isFreeModel(model)) return "free";
|
|
655
|
-
return `$${formatCostLeg(model.cost.input)}·${formatCostLeg(model.cost.output)}
|
|
697
|
+
return withCreditBadge(model, `$${formatCostLeg(model.cost.input)}·${formatCostLeg(model.cost.output)}`);
|
|
656
698
|
}
|
|
657
699
|
|
|
658
700
|
/** `$2 in · $10 out · $0.2 cache` for a model preview. */
|
|
659
701
|
function previewPrice(model: Model): string {
|
|
702
|
+
const declared = declaredPricingStatus(model);
|
|
703
|
+
if (declared !== undefined) return PRICING_STATUS_LABELS[declared].detail;
|
|
660
704
|
const cost = model.cost;
|
|
661
705
|
const parts = [`$${formatCostLeg(cost.input)} in`, `$${formatCostLeg(cost.output)} out`];
|
|
662
706
|
if (cost.cacheRead > 0) parts.push(`$${formatCostLeg(cost.cacheRead)} cache`);
|
|
663
|
-
return parts.join(" · ");
|
|
707
|
+
return withCreditBadge(model, parts.join(" · "));
|
|
664
708
|
}
|
|
665
709
|
|
|
666
710
|
/** The omp theme token of a thinking level's dot (`thinkingHigh`); none for inherit and auto. */
|
|
@@ -1301,7 +1345,7 @@ export class ModelBrowser implements Component {
|
|
|
1301
1345
|
if (model.isRecommended) facts.push("recommended");
|
|
1302
1346
|
if (model.contextWindow) facts.push(`${formatNumber(model.contextWindow).toLowerCase()} ctx`);
|
|
1303
1347
|
if (model.maxTokens) facts.push(`${formatNumber(model.maxTokens).toLowerCase()} out`);
|
|
1304
|
-
facts.push(
|
|
1348
|
+
facts.push(formatCostDetail(model));
|
|
1305
1349
|
if (model.reasoning) facts.push("reasoning");
|
|
1306
1350
|
if (model.input.includes("image")) facts.push("vision");
|
|
1307
1351
|
const intelligence = formatIntelligence(model);
|
|
@@ -1564,7 +1608,7 @@ export class ModelBrowser implements Component {
|
|
|
1564
1608
|
const facts: string[] = [];
|
|
1565
1609
|
if (model.contextWindow) facts.push(`${formatNumber(model.contextWindow).toLowerCase()} ctx`);
|
|
1566
1610
|
if (model.maxTokens) facts.push(`${formatNumber(model.maxTokens).toLowerCase()} out`);
|
|
1567
|
-
facts.push(
|
|
1611
|
+
facts.push(formatCostDetail(model));
|
|
1568
1612
|
if (model.reasoning) facts.push("reasoning");
|
|
1569
1613
|
if (model.input.includes("image")) facts.push("vision");
|
|
1570
1614
|
const intelligence = formatIntelligence(model);
|
|
@@ -1881,7 +1925,7 @@ export class ModelBrowser implements Component {
|
|
|
1881
1925
|
const facts: { k: TspText; v: TspText }[] = [];
|
|
1882
1926
|
if (ctx > 0) facts.push({ k: "ctx", v: formatNumber(ctx).toLowerCase() });
|
|
1883
1927
|
if (out > 0) facts.push({ k: "out", v: formatNumber(out).toLowerCase() });
|
|
1884
|
-
facts.push({ k: "price", v: isFreeModel(model) ? "free" :
|
|
1928
|
+
facts.push({ k: "price", v: isFreeModel(model) ? "free" : formatCostDetail(model) });
|
|
1885
1929
|
facts.push({ k: "reasoning", v: model.reasoning ? "yes" : "no" });
|
|
1886
1930
|
const children: NativeChild[] = [node("kv", { items: facts, layout: "inline" })];
|
|
1887
1931
|
const chips: NativeChild[] = [];
|
|
@@ -152,6 +152,8 @@ export interface ModelHubCallbacks {
|
|
|
152
152
|
onFallbackChainChange?: (role: string, chain: string[]) => void;
|
|
153
153
|
/** Locked provider activation: forward to the /login flow. */
|
|
154
154
|
onLoginRequest?: (providerId: string) => void;
|
|
155
|
+
/** Save the current role assignments and default thinking level as a named model preset. */
|
|
156
|
+
onSavePreset?: (name: string) => void;
|
|
155
157
|
/** Persist a new quick-switch cycle order (the ctrl+p role cycle). */
|
|
156
158
|
onCycleOrderChange?: (order: string[]) => void;
|
|
157
159
|
onCancel: () => void;
|
|
@@ -204,13 +206,14 @@ type StripState =
|
|
|
204
206
|
initialThinkingLevel?: ConfiguredThinkingLevel;
|
|
205
207
|
})
|
|
206
208
|
| {
|
|
207
|
-
/** Footer text input naming a new
|
|
208
|
-
kind: "
|
|
209
|
+
/** Footer text input naming a new role, or saving a model preset. */
|
|
210
|
+
kind: "name";
|
|
211
|
+
purpose: "role" | "preset";
|
|
209
212
|
input: Input;
|
|
210
213
|
};
|
|
211
214
|
|
|
212
215
|
/** A Roles-view command; keys and the picker's action bar both run {@link ModelHubComponent}'s `#runRolesAction`. */
|
|
213
|
-
type RolesAction = "pick" | "clear" | "fallback" | "cycle" | "earlier" | "later" | "new" | "thinking";
|
|
216
|
+
type RolesAction = "pick" | "clear" | "fallback" | "cycle" | "earlier" | "later" | "new" | "thinking" | "save";
|
|
214
217
|
|
|
215
218
|
/** Printable keys of the Roles view and the command each runs. */
|
|
216
219
|
const ROLES_ACTION_KEYS: Record<string, RolesAction> = {
|
|
@@ -221,6 +224,7 @@ const ROLES_ACTION_KEYS: Record<string, RolesAction> = {
|
|
|
221
224
|
"]": "later",
|
|
222
225
|
n: "new",
|
|
223
226
|
t: "thinking",
|
|
227
|
+
s: "save",
|
|
224
228
|
};
|
|
225
229
|
|
|
226
230
|
/** Picker fact columns of the Roles view. */
|
|
@@ -1429,7 +1433,7 @@ export class ModelHubComponent implements Component {
|
|
|
1429
1433
|
|
|
1430
1434
|
#activateStripChip(): void {
|
|
1431
1435
|
const strip = this.#strip;
|
|
1432
|
-
if (!strip || strip.kind === "
|
|
1436
|
+
if (!strip || strip.kind === "name") return;
|
|
1433
1437
|
const chip = strip.chips[strip.index];
|
|
1434
1438
|
if (!chip) return;
|
|
1435
1439
|
switch (chip.action) {
|
|
@@ -1654,15 +1658,26 @@ export class ModelHubComponent implements Component {
|
|
|
1654
1658
|
this.#refreshAfterMutation();
|
|
1655
1659
|
}
|
|
1656
1660
|
|
|
1657
|
-
/** Open the footer name input
|
|
1658
|
-
#
|
|
1659
|
-
this.#strip = { kind: "
|
|
1661
|
+
/** Open the footer name input: a new custom role, or saving the current setup as a model preset. */
|
|
1662
|
+
#openNameStrip(purpose: "role" | "preset"): void {
|
|
1663
|
+
this.#strip = { kind: "name", purpose, input: new Input() };
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
/** Validate and commit the name strip: a new role jumps into assigning it, a preset name saves it. */
|
|
1667
|
+
#submitNameStrip(): void {
|
|
1668
|
+
const strip = this.#strip;
|
|
1669
|
+
if (strip?.kind !== "name") return;
|
|
1670
|
+
if (strip.purpose === "preset") {
|
|
1671
|
+
this.#submitPresetName();
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
this.#submitRoleName();
|
|
1660
1675
|
}
|
|
1661
1676
|
|
|
1662
1677
|
/** Validate and commit the new-role name: jump straight into assigning it. */
|
|
1663
1678
|
#submitRoleName(): void {
|
|
1664
1679
|
const strip = this.#strip;
|
|
1665
|
-
if (strip?.kind !== "
|
|
1680
|
+
if (strip?.kind !== "name" || strip.purpose !== "role") return;
|
|
1666
1681
|
const name = strip.input.getValue().trim();
|
|
1667
1682
|
if (!/^[a-zA-Z][\w-]*$/.test(name)) return;
|
|
1668
1683
|
if (this.#visibleRoleIds().includes(name)) return;
|
|
@@ -1671,6 +1686,18 @@ export class ModelHubComponent implements Component {
|
|
|
1671
1686
|
this.#startAssign(name);
|
|
1672
1687
|
}
|
|
1673
1688
|
|
|
1689
|
+
/** Validate and commit the preset name: save the current setup under it (overwrites). */
|
|
1690
|
+
#submitPresetName(): void {
|
|
1691
|
+
const strip = this.#strip;
|
|
1692
|
+
if (strip?.kind !== "name" || strip.purpose !== "preset") return;
|
|
1693
|
+
const name = strip.input.getValue().trim();
|
|
1694
|
+
if (!/^[a-zA-Z][\w-]*$/.test(name)) return;
|
|
1695
|
+
this.#strip = null;
|
|
1696
|
+
this.#frame.chipRanges = [];
|
|
1697
|
+
this.#callbacks.onSavePreset?.(name);
|
|
1698
|
+
this.#refreshAfterMutation();
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1674
1701
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1675
1702
|
// Input
|
|
1676
1703
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -1841,9 +1868,9 @@ export class ModelHubComponent implements Component {
|
|
|
1841
1868
|
this.#closeStrip();
|
|
1842
1869
|
return;
|
|
1843
1870
|
}
|
|
1844
|
-
if (strip.kind === "
|
|
1871
|
+
if (strip.kind === "name") {
|
|
1845
1872
|
if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") {
|
|
1846
|
-
this.#
|
|
1873
|
+
this.#submitNameStrip();
|
|
1847
1874
|
return;
|
|
1848
1875
|
}
|
|
1849
1876
|
strip.input.handleInput(data);
|
|
@@ -1895,7 +1922,7 @@ export class ModelHubComponent implements Component {
|
|
|
1895
1922
|
this.#startAssignFallbackKey();
|
|
1896
1923
|
return;
|
|
1897
1924
|
case "newRole":
|
|
1898
|
-
this.#
|
|
1925
|
+
this.#openNameStrip("role");
|
|
1899
1926
|
return;
|
|
1900
1927
|
case "separator":
|
|
1901
1928
|
return;
|
|
@@ -2001,7 +2028,10 @@ export class ModelHubComponent implements Component {
|
|
|
2001
2028
|
return;
|
|
2002
2029
|
}
|
|
2003
2030
|
case "new":
|
|
2004
|
-
this.#
|
|
2031
|
+
this.#openNameStrip("role");
|
|
2032
|
+
return;
|
|
2033
|
+
case "save":
|
|
2034
|
+
if (this.#callbacks.onSavePreset) this.#openNameStrip("preset");
|
|
2005
2035
|
return;
|
|
2006
2036
|
case "thinking":
|
|
2007
2037
|
if (role) {
|
|
@@ -2036,7 +2066,7 @@ export class ModelHubComponent implements Component {
|
|
|
2036
2066
|
// Footer strip chips (columns stay in frame coordinates).
|
|
2037
2067
|
if (footerColumn !== undefined && this.#strip) {
|
|
2038
2068
|
const strip = this.#strip;
|
|
2039
|
-
if (event.leftClick && strip.kind !== "
|
|
2069
|
+
if (event.leftClick && strip.kind !== "name" && this.#frame.selectChipAt(strip, footerColumn)) {
|
|
2040
2070
|
this.#activateStripChip();
|
|
2041
2071
|
}
|
|
2042
2072
|
return true;
|
|
@@ -2472,7 +2502,10 @@ export class ModelHubComponent implements Component {
|
|
|
2472
2502
|
const altLeftRight = formatKeyHints(["alt+left", "alt+right"]);
|
|
2473
2503
|
const strip = this.#strip;
|
|
2474
2504
|
if (strip) {
|
|
2475
|
-
if (strip.kind === "
|
|
2505
|
+
if (strip.kind === "name") {
|
|
2506
|
+
if (strip.purpose === "preset") {
|
|
2507
|
+
return `${enter} save preset · ${cancel} cancel`;
|
|
2508
|
+
}
|
|
2476
2509
|
return `${enter} create + pick model · ${cancel} cancel`;
|
|
2477
2510
|
}
|
|
2478
2511
|
if (strip.kind === "role") return `${leftRight} choose · ${enter} assign/clear · ${cancel} cancel`;
|
|
@@ -2517,7 +2550,8 @@ export class ModelHubComponent implements Component {
|
|
|
2517
2550
|
// open — an assigned role whose model has thinking levels to offer.
|
|
2518
2551
|
const editable = row?.kind === "role" && this.#roleThinkingTarget(row.role) !== undefined;
|
|
2519
2552
|
const thinking = editable ? ` · ${formatKeyHint("t")} thinking` : "";
|
|
2520
|
-
|
|
2553
|
+
const savePreset = this.#callbacks.onSavePreset ? ` · ${formatKeyHint("s")} save preset` : "";
|
|
2554
|
+
return `${upDown} rows · ${enter} pick · ${formatKeyHint("f")} fallback · ${formatKeyHint("x")} clear${thinking} · ${formatKeyHint("c")} cycle · [/] reorder · ${formatKeyHint("n")} new${savePreset}`;
|
|
2521
2555
|
}
|
|
2522
2556
|
if (entry.kind === "provider" && entry.locked) {
|
|
2523
2557
|
return entry.oauth
|
|
@@ -2541,9 +2575,11 @@ export class ModelHubComponent implements Component {
|
|
|
2541
2575
|
}
|
|
2542
2576
|
|
|
2543
2577
|
#renderStrip(width: number, strip: StripState): string {
|
|
2544
|
-
if (strip.kind === "
|
|
2545
|
-
const
|
|
2546
|
-
const
|
|
2578
|
+
if (strip.kind === "name") {
|
|
2579
|
+
const preset = strip.purpose === "preset";
|
|
2580
|
+
const labelText = preset ? "Preset name:" : "New role name:";
|
|
2581
|
+
const label = theme.fg("accent", labelText);
|
|
2582
|
+
const inputWidth = Math.max(8, Math.min(32, width - visibleWidth(labelText) - 24));
|
|
2547
2583
|
const inputLine = strip.input.render(inputWidth)[0] ?? "";
|
|
2548
2584
|
return truncateToWidth(`${label} ${inputLine} ${theme.fg("dim", "(letters, digits, - and _)")}`, width);
|
|
2549
2585
|
}
|
|
@@ -2668,7 +2704,7 @@ export class ModelHubComponent implements Component {
|
|
|
2668
2704
|
// A chip click picks and applies it, like the footer mouse path.
|
|
2669
2705
|
const strip = this.#strip;
|
|
2670
2706
|
const index = Number(event.item);
|
|
2671
|
-
if (!strip || strip.kind === "
|
|
2707
|
+
if (!strip || strip.kind === "name" || !Number.isInteger(index) || !strip.chips[index]) return;
|
|
2672
2708
|
strip.index = index;
|
|
2673
2709
|
this.#activateStripChip();
|
|
2674
2710
|
break;
|
|
@@ -3184,8 +3220,13 @@ export class ModelHubComponent implements Component {
|
|
|
3184
3220
|
if (this.#assignmentPending) return [CLOSE_ACTION];
|
|
3185
3221
|
if (strip) {
|
|
3186
3222
|
const apply =
|
|
3187
|
-
strip.kind === "
|
|
3188
|
-
? pickerAction(
|
|
3223
|
+
strip.kind === "name"
|
|
3224
|
+
? pickerAction(
|
|
3225
|
+
strip.purpose === "preset" ? "presetName" : "roleName",
|
|
3226
|
+
strip.purpose === "preset" ? "Save preset" : "Create role",
|
|
3227
|
+
"enter",
|
|
3228
|
+
{ primary: true },
|
|
3229
|
+
)
|
|
3189
3230
|
: pickerAction(
|
|
3190
3231
|
"stripApply",
|
|
3191
3232
|
strip.kind === "thinking" ? "Apply" : strip.kind === "scope" ? "Save to scope" : "Assign / clear",
|
|
@@ -3220,6 +3261,7 @@ export class ModelHubComponent implements Component {
|
|
|
3220
3261
|
this.#roleThinkingTarget(row.role) ? roleAction("thinking", "Thinking", "t") : undefined,
|
|
3221
3262
|
roleAction("cycle", this.#cycleOrder().includes(row.role) ? "Leave cycle" : "Add to cycle", "c"),
|
|
3222
3263
|
roleAction("new", "New role", "n"),
|
|
3264
|
+
this.#callbacks.onSavePreset ? roleAction("save", "Save preset", "s") : undefined,
|
|
3223
3265
|
);
|
|
3224
3266
|
break;
|
|
3225
3267
|
}
|
|
@@ -3266,9 +3308,13 @@ export class ModelHubComponent implements Component {
|
|
|
3266
3308
|
|
|
3267
3309
|
/** The open strip as the picker's chip strip (role assignment, save scope, thinking level, new role name). */
|
|
3268
3310
|
#pickerStrip(strip: StripState): NonNullable<TspPickerProps["strip"]> {
|
|
3269
|
-
if (strip.kind === "
|
|
3311
|
+
if (strip.kind === "name") {
|
|
3270
3312
|
return {
|
|
3271
|
-
label: [
|
|
3313
|
+
label: [
|
|
3314
|
+
span(strip.purpose === "preset" ? "Preset name " : "New role name ", "muted"),
|
|
3315
|
+
span(strip.input.getValue(), "mono"),
|
|
3316
|
+
span("▏", "accent"),
|
|
3317
|
+
],
|
|
3272
3318
|
items: [],
|
|
3273
3319
|
};
|
|
3274
3320
|
}
|
|
@@ -3562,7 +3608,7 @@ export class ModelHubComponent implements Component {
|
|
|
3562
3608
|
case "strip": {
|
|
3563
3609
|
const strip = this.#strip;
|
|
3564
3610
|
const index = Number(value);
|
|
3565
|
-
if (!strip || strip.kind === "
|
|
3611
|
+
if (!strip || strip.kind === "name" || !Number.isInteger(index) || !strip.chips[index]) return;
|
|
3566
3612
|
strip.index = index;
|
|
3567
3613
|
this.#activateStripChip();
|
|
3568
3614
|
return;
|
|
@@ -3573,6 +3619,9 @@ export class ModelHubComponent implements Component {
|
|
|
3573
3619
|
case "roleName":
|
|
3574
3620
|
this.#submitRoleName();
|
|
3575
3621
|
return;
|
|
3622
|
+
case "presetName":
|
|
3623
|
+
this.#submitPresetName();
|
|
3624
|
+
return;
|
|
3576
3625
|
case "cancel":
|
|
3577
3626
|
if (this.#strip) this.#closeStrip();
|
|
3578
3627
|
return;
|
|
@@ -3622,16 +3671,17 @@ export class ModelHubComponent implements Component {
|
|
|
3622
3671
|
#describeStrip(): NativeNode | undefined {
|
|
3623
3672
|
const strip = this.#strip;
|
|
3624
3673
|
if (!strip) return undefined;
|
|
3625
|
-
if (strip.kind === "
|
|
3674
|
+
if (strip.kind === "name") {
|
|
3675
|
+
const preset = strip.purpose === "preset";
|
|
3626
3676
|
return node(
|
|
3627
3677
|
"row",
|
|
3628
3678
|
{ gap: "sm", align: "center" },
|
|
3629
3679
|
[
|
|
3630
|
-
text([span("New role name:", "accent")]),
|
|
3680
|
+
text([span(preset ? "Preset name:" : "New role name:", "accent")]),
|
|
3631
3681
|
col([strip.input], { grow: 1 }),
|
|
3632
3682
|
text([span("(letters, digits, - and _)", "dim")]),
|
|
3633
3683
|
],
|
|
3634
|
-
"roleName",
|
|
3684
|
+
preset ? "presetName" : "roleName",
|
|
3635
3685
|
);
|
|
3636
3686
|
}
|
|
3637
3687
|
let prefix: TspSpan[];
|
|
@@ -3674,8 +3724,10 @@ export class ModelHubComponent implements Component {
|
|
|
3674
3724
|
const strip = this.#strip;
|
|
3675
3725
|
if (strip) {
|
|
3676
3726
|
switch (strip.kind) {
|
|
3677
|
-
case "
|
|
3678
|
-
return
|
|
3727
|
+
case "name":
|
|
3728
|
+
return strip.purpose === "preset"
|
|
3729
|
+
? [keys("save preset", "enter"), cancel("cancel")]
|
|
3730
|
+
: [keys("create + pick model", "enter"), cancel("cancel")];
|
|
3679
3731
|
case "role":
|
|
3680
3732
|
return [keys("choose", "left", "right"), keys("assign/clear", "enter"), cancel("cancel")];
|
|
3681
3733
|
case "scope":
|
|
@@ -3741,6 +3793,7 @@ export class ModelHubComponent implements Component {
|
|
|
3741
3793
|
keys("cycle", "c"),
|
|
3742
3794
|
reorder,
|
|
3743
3795
|
keys("new", "n"),
|
|
3796
|
+
this.#callbacks.onSavePreset ? keys("save preset", "s") : undefined,
|
|
3744
3797
|
];
|
|
3745
3798
|
}
|
|
3746
3799
|
if (entry.kind === "provider" && entry.locked) {
|
|
@@ -909,10 +909,16 @@ function homePatternFor(homeDir: string, windowsStyle: boolean): HomePattern {
|
|
|
909
909
|
})
|
|
910
910
|
.join("[\\\\/]");
|
|
911
911
|
}
|
|
912
|
+
// `*` and `_` are also identifier and glob characters (`/home/me_old`, `src/**/home/me/**`), so they
|
|
913
|
+
// bound the home directory only as Markdown emphasis: opened at start or after whitespace, and
|
|
914
|
+
// closed before end, whitespace, or punctuation.
|
|
915
|
+
const end = `[\\\\/\\s"'\\x60)\\]},;:<>&|]`;
|
|
912
916
|
pattern = {
|
|
913
917
|
leading: new RegExp(`^${escapedHome}(?=$|[\\\\/])`, windowsStyle ? "i" : ""),
|
|
914
918
|
embedded: new RegExp(
|
|
915
|
-
`[a-zA-Z][a-zA-Z0-9+.-]*://[^\\s"'<>]
|
|
919
|
+
`[a-zA-Z][a-zA-Z0-9+.-]*://[^\\s"'<>]+` +
|
|
920
|
+
`|(^|[\\s"'\\x60([{=,:<>&|])(${escapedHome})(?=$|${end})` +
|
|
921
|
+
`|((?:^|\\s)[*_]{1,3})(${escapedHome})(?=$|${end}|[*_]{1,3}(?=$|[\\s.,;:!?)\\]}"'\\x60]))`,
|
|
916
922
|
windowsStyle ? "gi" : "g",
|
|
917
923
|
),
|
|
918
924
|
};
|
|
@@ -946,8 +952,13 @@ export function shortenEmbeddedPaths(text: string, homeDir?: string, preserveSep
|
|
|
946
952
|
const homePattern = homePatternFor(resolvedHome, windowsStyle);
|
|
947
953
|
const textWithShortenedHome = text.replace(
|
|
948
954
|
homePattern.embedded,
|
|
949
|
-
(
|
|
950
|
-
|
|
955
|
+
(
|
|
956
|
+
match,
|
|
957
|
+
boundary: string | undefined,
|
|
958
|
+
candidate: string | undefined,
|
|
959
|
+
emphasis: string | undefined,
|
|
960
|
+
emphasized: string | undefined,
|
|
961
|
+
) => (candidate !== undefined ? `${boundary}~` : emphasized !== undefined ? `${emphasis}~` : match),
|
|
951
962
|
);
|
|
952
963
|
if (preserveSeparators) return textWithShortenedHome;
|
|
953
964
|
return textWithShortenedHome
|
|
@@ -966,6 +977,43 @@ export function shortenEmbeddedPaths(text: string, homeDir?: string, preserveSep
|
|
|
966
977
|
.join(" ");
|
|
967
978
|
}
|
|
968
979
|
|
|
980
|
+
/**
|
|
981
|
+
* Shorten filesystem and command arguments without rewriting literal search patterns. A producer that
|
|
982
|
+
* names no argument key gets the general embedded-path shortening.
|
|
983
|
+
*/
|
|
984
|
+
export function shortenToolArgumentPaths(text: string, key: string | undefined, homeDir?: string): string {
|
|
985
|
+
if (key === "url") {
|
|
986
|
+
try {
|
|
987
|
+
const url = new URL(text);
|
|
988
|
+
if (url.protocol === "file:") {
|
|
989
|
+
let decodedPath = url.pathname;
|
|
990
|
+
try {
|
|
991
|
+
decodedPath = decodeURIComponent(decodedPath);
|
|
992
|
+
} catch {
|
|
993
|
+
/* Retain malformed percent escapes as literal path bytes. */
|
|
994
|
+
}
|
|
995
|
+
const filePath = url.hostname
|
|
996
|
+
? `//${url.hostname}${decodedPath}`
|
|
997
|
+
: /^[A-Za-z]:$/.test(decodedPath.slice(1, 3))
|
|
998
|
+
? decodedPath.slice(1)
|
|
999
|
+
: decodedPath;
|
|
1000
|
+
return shortenEmbeddedPaths(filePath, homeDir);
|
|
1001
|
+
}
|
|
1002
|
+
} catch {
|
|
1003
|
+
// Preserve malformed and non-file URL arguments verbatim.
|
|
1004
|
+
}
|
|
1005
|
+
return text;
|
|
1006
|
+
}
|
|
1007
|
+
return key === undefined ||
|
|
1008
|
+
key === "path" ||
|
|
1009
|
+
key === "file_path" ||
|
|
1010
|
+
key === "command" ||
|
|
1011
|
+
key === "task" ||
|
|
1012
|
+
key === "prompt"
|
|
1013
|
+
? shortenEmbeddedPaths(text, homeDir)
|
|
1014
|
+
: text;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
969
1017
|
/** Sanitize warning text before showing it in TUI, including embedded home paths. */
|
|
970
1018
|
export function sanitizeDisplayWarning(text: string): string {
|
|
971
1019
|
return shortenEmbeddedPaths(
|
package/src/tools/edit.ts
CHANGED
|
@@ -693,6 +693,29 @@ function getHashlineInputSections(input: string): HashlineInputEntry[] {
|
|
|
693
693
|
return entries;
|
|
694
694
|
}
|
|
695
695
|
|
|
696
|
+
/** Extract display targets using the existing parsers for supported freeform edit modes. */
|
|
697
|
+
export function getEditInputPaths(input: string, resolvedMode?: EditMode): readonly string[] {
|
|
698
|
+
const mode =
|
|
699
|
+
resolvedMode ??
|
|
700
|
+
(/^\*\*\* (?:Add|Update|Delete) File:/m.test(input)
|
|
701
|
+
? "apply_patch"
|
|
702
|
+
: /^[ \t]*\*{3}[ \t]+Edit[ \t]+File:/im.test(input)
|
|
703
|
+
? "sloppy"
|
|
704
|
+
: undefined);
|
|
705
|
+
try {
|
|
706
|
+
const entries =
|
|
707
|
+
mode && mode !== "hashline" ? inspectInputEntries({}, mode, input) : getHashlineInputSections(input);
|
|
708
|
+
const paths: string[] = [];
|
|
709
|
+
for (const entry of entries) {
|
|
710
|
+
if (entry.path) paths.push(entry.path);
|
|
711
|
+
if (entry.rename && entry.rename !== entry.path) paths.push(entry.rename);
|
|
712
|
+
}
|
|
713
|
+
return paths;
|
|
714
|
+
} catch {
|
|
715
|
+
return [];
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
696
719
|
function getHashlineInputRenderSummary(
|
|
697
720
|
args: EditRenderArgs,
|
|
698
721
|
editMode: EditMode | undefined,
|
package/src/tools/task.ts
CHANGED
|
@@ -33,7 +33,9 @@ import {
|
|
|
33
33
|
previewLine,
|
|
34
34
|
previewWindowRows,
|
|
35
35
|
replaceTabs,
|
|
36
|
+
shortenEmbeddedPaths,
|
|
36
37
|
shortenPath,
|
|
38
|
+
shortenToolArgumentPaths,
|
|
37
39
|
type ToolUIStatus,
|
|
38
40
|
TRUNCATE_LENGTHS,
|
|
39
41
|
truncateToWidth,
|
|
@@ -663,6 +665,11 @@ function renderRouteLine(route: string | undefined, continuePrefix: string, maxW
|
|
|
663
665
|
];
|
|
664
666
|
}
|
|
665
667
|
|
|
668
|
+
/** A tool call's own intent, else its argument, with home paths shortened as the subagent HUD does. */
|
|
669
|
+
function toolCallDetail(intent: string | undefined, args: string | undefined, argsKey: string | undefined): string {
|
|
670
|
+
return intent ? shortenEmbeddedPaths(intent) : shortenToolArgumentPaths(args ?? "", argsKey);
|
|
671
|
+
}
|
|
672
|
+
|
|
666
673
|
/**
|
|
667
674
|
* Render streaming progress for a single agent.
|
|
668
675
|
*/
|
|
@@ -723,7 +730,11 @@ function renderAgentProgress(
|
|
|
723
730
|
if (progress.status === "running") {
|
|
724
731
|
if (progress.currentTool) {
|
|
725
732
|
let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("muted", sanitizeText(progress.currentTool))}`;
|
|
726
|
-
const toolDetail =
|
|
733
|
+
const toolDetail = toolCallDetail(
|
|
734
|
+
progress.currentToolIntent,
|
|
735
|
+
progress.currentToolArgs,
|
|
736
|
+
progress.currentToolArgsKey,
|
|
737
|
+
);
|
|
727
738
|
if (toolDetail) {
|
|
728
739
|
toolLine += `: ${theme.fg("dim", previewLine(sanitizeText(toolDetail), 40))}`;
|
|
729
740
|
}
|
|
@@ -738,7 +749,7 @@ function renderAgentProgress(
|
|
|
738
749
|
// Show most recent completed tool when idle between tools
|
|
739
750
|
const recent = progress.recentTools[0];
|
|
740
751
|
let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("dim", sanitizeText(recent.tool))}`;
|
|
741
|
-
const toolDetail =
|
|
752
|
+
const toolDetail = toolCallDetail(recent.intent, recent.args, recent.argsKey);
|
|
742
753
|
if (toolDetail) {
|
|
743
754
|
toolLine += `: ${theme.fg("dim", previewLine(sanitizeText(toolDetail), 40))}`;
|
|
744
755
|
}
|
|
@@ -1795,7 +1806,10 @@ function describeProgressAgent(progress: AgentProgress, state: AgentDescribeStat
|
|
|
1795
1806
|
running && progress.currentTool
|
|
1796
1807
|
? {
|
|
1797
1808
|
name: plainText(progress.currentTool),
|
|
1798
|
-
intent:
|
|
1809
|
+
intent:
|
|
1810
|
+
plainText(
|
|
1811
|
+
toolCallDetail(progress.currentToolIntent, progress.currentToolArgs, progress.currentToolArgsKey),
|
|
1812
|
+
) || undefined,
|
|
1799
1813
|
age: progress.currentToolStartMs ? Math.max(0, nowMs - progress.currentToolStartMs) : undefined,
|
|
1800
1814
|
}
|
|
1801
1815
|
: null;
|
|
@@ -2252,8 +2266,19 @@ export interface AgentProgress {
|
|
|
2252
2266
|
lastIntent?: string;
|
|
2253
2267
|
currentTool?: string;
|
|
2254
2268
|
currentToolArgs?: string;
|
|
2269
|
+
/** Argument key selected for the display preview, when known. */
|
|
2270
|
+
currentToolArgsKey?: string;
|
|
2271
|
+
/** Intent the model attached to the current call; undefined when that call carried none. */
|
|
2272
|
+
currentToolIntent?: string;
|
|
2255
2273
|
currentToolStartMs?: number;
|
|
2256
|
-
recentTools: Array<{
|
|
2274
|
+
recentTools: Array<{
|
|
2275
|
+
tool: string;
|
|
2276
|
+
args: string;
|
|
2277
|
+
argsKey?: string;
|
|
2278
|
+
intent?: string;
|
|
2279
|
+
isError?: boolean;
|
|
2280
|
+
endMs: number;
|
|
2281
|
+
}>;
|
|
2257
2282
|
recentOutput: string[];
|
|
2258
2283
|
toolCount: number;
|
|
2259
2284
|
/** Count of assistant requests (assistant message_end events) across the run. Drives the soft request budget guard. */
|