@oh-my-pi/pi-coding-agent 14.5.7 → 14.5.8
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 +21 -0
- package/package.json +7 -7
- package/src/config/model-registry.ts +23 -1
- package/src/config/settings-schema.ts +23 -0
- package/src/modes/components/settings-defs.ts +10 -0
- package/src/modes/controllers/event-controller.ts +14 -9
- package/src/modes/controllers/input-controller.ts +6 -0
- package/src/modes/interactive-mode.ts +15 -1
- package/src/modes/types.ts +1 -0
- package/src/prompts/tools/run-command.md +16 -0
- package/src/tools/bash.ts +149 -115
- package/src/tools/index.ts +11 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/run-command/index.ts +80 -0
- package/src/tools/run-command/render.ts +18 -0
- package/src/tools/run-command/runner.ts +198 -0
- package/src/tools/run-command/runners/cargo.ts +131 -0
- package/src/tools/run-command/runners/index.ts +8 -0
- package/src/tools/run-command/runners/just.ts +73 -0
- package/src/tools/run-command/runners/make.ts +101 -0
- package/src/tools/run-command/runners/pkg.ts +195 -0
- package/src/tools/run-command/runners/task.ts +72 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [14.5.8] - 2026-04-29
|
|
6
|
+
### Breaking Changes
|
|
7
|
+
|
|
8
|
+
- Changed the task runner toggle from `just.enabled` to `runCommand.enabled`, so existing configurations using `just.enabled` must be migrated
|
|
9
|
+
- Removed the legacy `just` tool and replaced it with `run_command`
|
|
10
|
+
- Renamed the built-in tool API from `just` to `run_command`, so clients requesting/handling the old tool name must update
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Added a new `run_command` tool that runs project tasks via a single `op` argument, auto-detecting and supporting recipes from justfiles, `package.json` scripts (including workspace packages), Cargo bin/example/test targets, Makefiles, and Taskfiles
|
|
15
|
+
- Added support for explicit runner-qualified tasks via `run_command` with `runnerId:task` syntax in the prompt guidance
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Changed automatic tool availability so requesting `bash` can now auto-include `run_command` when a supported task runner manifest is detected in the working directory
|
|
20
|
+
- Changed task resolution to disambiguate identical task names across multiple runners and show runner-aware command execution errors
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Fixed editor draft being erased when a user message queued during streaming was eventually submitted; the queue/steer path now preserves any new prompt the user has typed since queuing, matching the existing optimistic-send protection.
|
|
25
|
+
|
|
5
26
|
## [14.5.7] - 2026-04-29
|
|
6
27
|
|
|
7
28
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "14.5.
|
|
4
|
+
"version": "14.5.8",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@agentclientprotocol/sdk": "0.20.0",
|
|
48
48
|
"@mozilla/readability": "^0.6.0",
|
|
49
|
-
"@oh-my-pi/omp-stats": "14.5.
|
|
50
|
-
"@oh-my-pi/pi-agent-core": "14.5.
|
|
51
|
-
"@oh-my-pi/pi-ai": "14.5.
|
|
52
|
-
"@oh-my-pi/pi-natives": "14.5.
|
|
53
|
-
"@oh-my-pi/pi-tui": "14.5.
|
|
54
|
-
"@oh-my-pi/pi-utils": "14.5.
|
|
49
|
+
"@oh-my-pi/omp-stats": "14.5.8",
|
|
50
|
+
"@oh-my-pi/pi-agent-core": "14.5.8",
|
|
51
|
+
"@oh-my-pi/pi-ai": "14.5.8",
|
|
52
|
+
"@oh-my-pi/pi-natives": "14.5.8",
|
|
53
|
+
"@oh-my-pi/pi-tui": "14.5.8",
|
|
54
|
+
"@oh-my-pi/pi-utils": "14.5.8",
|
|
55
55
|
"@puppeteer/browsers": "^2.13.0",
|
|
56
56
|
"@sinclair/typebox": "^0.34.49",
|
|
57
57
|
"@xterm/headless": "^6.0.0",
|
|
@@ -249,7 +249,7 @@ const ProviderDiscoverySchema = Type.Object({
|
|
|
249
249
|
type: Type.Union([Type.Literal("ollama"), Type.Literal("llama.cpp"), Type.Literal("lm-studio")]),
|
|
250
250
|
});
|
|
251
251
|
|
|
252
|
-
const ProviderAuthSchema = Type.Union([Type.Literal("apiKey"), Type.Literal("none")]);
|
|
252
|
+
const ProviderAuthSchema = Type.Union([Type.Literal("apiKey"), Type.Literal("none"), Type.Literal("oauth")]);
|
|
253
253
|
|
|
254
254
|
const ProviderConfigSchema = Type.Object({
|
|
255
255
|
baseUrl: Type.Optional(Type.String({ minLength: 1 })),
|
|
@@ -643,6 +643,7 @@ type CustomModelOverlay = {
|
|
|
643
643
|
compat?: Model<Api>["compat"];
|
|
644
644
|
contextPromotionTarget?: string;
|
|
645
645
|
premiumMultiplier?: number;
|
|
646
|
+
isOAuth?: boolean;
|
|
646
647
|
};
|
|
647
648
|
|
|
648
649
|
function mergeCustomModelHeaders(
|
|
@@ -661,6 +662,22 @@ function mergeCustomModelHeaders(
|
|
|
661
662
|
return headers;
|
|
662
663
|
}
|
|
663
664
|
|
|
665
|
+
/**
|
|
666
|
+
* Decide whether a custom-yaml model should force OAuth-style request shaping.
|
|
667
|
+
* - Explicit `auth: oauth` → force on.
|
|
668
|
+
* - Explicit `auth: apiKey` / `auth: none` → leave unset (auto-detect by key prefix).
|
|
669
|
+
* - No `auth` specified and `api: anthropic-messages` → default on. Custom Anthropic
|
|
670
|
+
* endpoints are typically Claude-Code-style proxies (e.g. CLIProxyAPI) that expect
|
|
671
|
+
* the cloaked request shape regardless of how the proxy itself is authenticated.
|
|
672
|
+
* - Otherwise → unset.
|
|
673
|
+
*/
|
|
674
|
+
function resolveCustomModelIsOAuth(api: Api, providerAuth: ProviderAuthMode | undefined): boolean | undefined {
|
|
675
|
+
if (providerAuth === "oauth") return true;
|
|
676
|
+
if (providerAuth !== undefined) return undefined;
|
|
677
|
+
if (api === "anthropic-messages") return true;
|
|
678
|
+
return undefined;
|
|
679
|
+
}
|
|
680
|
+
|
|
664
681
|
function buildCustomModelOverlay(
|
|
665
682
|
providerName: string,
|
|
666
683
|
providerBaseUrl: string,
|
|
@@ -669,6 +686,7 @@ function buildCustomModelOverlay(
|
|
|
669
686
|
providerApiKey: string | undefined,
|
|
670
687
|
authHeader: boolean | undefined,
|
|
671
688
|
providerCompat: Model<Api>["compat"] | undefined,
|
|
689
|
+
providerAuth: ProviderAuthMode | undefined,
|
|
672
690
|
modelDef: CustomModelDefinitionLike,
|
|
673
691
|
): CustomModelOverlay | undefined {
|
|
674
692
|
const api = modelDef.api ?? providerApi;
|
|
@@ -689,6 +707,7 @@ function buildCustomModelOverlay(
|
|
|
689
707
|
compat: mergeCompat(providerCompat, modelDef.compat),
|
|
690
708
|
contextPromotionTarget: modelDef.contextPromotionTarget,
|
|
691
709
|
premiumMultiplier: modelDef.premiumMultiplier,
|
|
710
|
+
isOAuth: resolveCustomModelIsOAuth(api, providerAuth),
|
|
692
711
|
};
|
|
693
712
|
}
|
|
694
713
|
|
|
@@ -720,6 +739,7 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
|
|
|
720
739
|
compat: resolvedModel.compat,
|
|
721
740
|
contextPromotionTarget: resolvedModel.contextPromotionTarget,
|
|
722
741
|
premiumMultiplier: resolvedModel.premiumMultiplier,
|
|
742
|
+
isOAuth: resolvedModel.isOAuth,
|
|
723
743
|
} as Model<Api>);
|
|
724
744
|
}
|
|
725
745
|
|
|
@@ -1735,6 +1755,7 @@ export class ModelRegistry {
|
|
|
1735
1755
|
providerConfig.apiKey,
|
|
1736
1756
|
providerConfig.authHeader,
|
|
1737
1757
|
providerConfig.compat,
|
|
1758
|
+
(providerConfig.auth as ProviderAuthMode | undefined) ?? undefined,
|
|
1738
1759
|
modelDef as CustomModelDefinitionLike,
|
|
1739
1760
|
);
|
|
1740
1761
|
if (!model) continue;
|
|
@@ -2069,6 +2090,7 @@ export class ModelRegistry {
|
|
|
2069
2090
|
config.apiKey,
|
|
2070
2091
|
config.authHeader,
|
|
2071
2092
|
config.compat,
|
|
2093
|
+
undefined,
|
|
2072
2094
|
modelDef as CustomModelDefinitionLike,
|
|
2073
2095
|
);
|
|
2074
2096
|
if (!overlay) {
|
|
@@ -608,6 +608,18 @@ export const SETTINGS_SCHEMA = {
|
|
|
608
608
|
},
|
|
609
609
|
},
|
|
610
610
|
|
|
611
|
+
"loop.mode": {
|
|
612
|
+
type: "enum",
|
|
613
|
+
values: ["prompt", "compact", "reset"] as const,
|
|
614
|
+
default: "prompt",
|
|
615
|
+
ui: {
|
|
616
|
+
tab: "interaction",
|
|
617
|
+
label: "Loop Mode",
|
|
618
|
+
description: "What happens between /loop iterations before re-submitting the prompt",
|
|
619
|
+
submenu: true,
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
|
|
611
623
|
// Input and startup
|
|
612
624
|
doubleEscapeAction: {
|
|
613
625
|
type: "enum",
|
|
@@ -1294,6 +1306,17 @@ export const SETTINGS_SCHEMA = {
|
|
|
1294
1306
|
},
|
|
1295
1307
|
},
|
|
1296
1308
|
|
|
1309
|
+
"runCommand.enabled": {
|
|
1310
|
+
type: "boolean",
|
|
1311
|
+
default: true,
|
|
1312
|
+
ui: {
|
|
1313
|
+
tab: "tools",
|
|
1314
|
+
label: "Run command",
|
|
1315
|
+
description:
|
|
1316
|
+
"Enable the run_command tool when a justfile / package.json / Cargo.toml / Makefile / Taskfile is present",
|
|
1317
|
+
},
|
|
1318
|
+
},
|
|
1319
|
+
|
|
1297
1320
|
"inspect_image.enabled": {
|
|
1298
1321
|
type: "boolean",
|
|
1299
1322
|
default: false,
|
|
@@ -450,6 +450,16 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
|
|
|
450
450
|
{ value: "none", label: "None", description: "Space only" },
|
|
451
451
|
{ value: "ascii", label: "ASCII", description: "Greater-than signs" },
|
|
452
452
|
],
|
|
453
|
+
// Loop mode
|
|
454
|
+
"loop.mode": [
|
|
455
|
+
{
|
|
456
|
+
value: "prompt",
|
|
457
|
+
label: "Prompt",
|
|
458
|
+
description: "Re-submit the prompt as a follow-up message (current behavior)",
|
|
459
|
+
},
|
|
460
|
+
{ value: "compact", label: "Compact", description: "Compact the session context, then re-submit the prompt" },
|
|
461
|
+
{ value: "reset", label: "Reset", description: "Start a new session, then re-submit the prompt" },
|
|
462
|
+
],
|
|
453
463
|
};
|
|
454
464
|
|
|
455
465
|
function createSubmenuSettingDef(base: Omit<SettingDef, "type" | "options">, provider: OptionProvider): SettingDef {
|
|
@@ -174,18 +174,23 @@ export class EventController {
|
|
|
174
174
|
|
|
175
175
|
this.#resetReadGroup();
|
|
176
176
|
const wasOptimistic = this.ctx.optimisticUserMessageSignature === signature;
|
|
177
|
+
const wasLocallySubmitted = this.ctx.locallySubmittedUserSignatures.delete(signature) || wasOptimistic;
|
|
177
178
|
if (!wasOptimistic) {
|
|
178
179
|
this.ctx.addMessageToChat(event.message);
|
|
179
180
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
181
|
+
if (wasOptimistic) {
|
|
182
|
+
this.ctx.optimisticUserMessageSignature = undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Clear the editor only when the submission did not originate from a
|
|
186
|
+
// local submission (optimistic or queued-while-streaming). Both local
|
|
187
|
+
// paths already cleared the editor at submit time; clearing again here
|
|
188
|
+
// would race with the user typing the next prompt while the previous
|
|
189
|
+
// large redraw lands and erase their in-progress draft (#783).
|
|
190
|
+
if (!event.message.synthetic) {
|
|
191
|
+
if (!wasLocallySubmitted) {
|
|
192
|
+
this.ctx.editor.setText("");
|
|
193
|
+
}
|
|
189
194
|
this.ctx.updatePendingMessagesDisplay();
|
|
190
195
|
}
|
|
191
196
|
this.ctx.ui.requestRender();
|
|
@@ -334,6 +334,11 @@ export class InputController {
|
|
|
334
334
|
this.ctx.editor.setText("");
|
|
335
335
|
const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
|
|
336
336
|
this.ctx.pendingImages = [];
|
|
337
|
+
// Record the signature so the queued message's eventual delivery
|
|
338
|
+
// (a user-role `message_start` event) leaves any draft the user has
|
|
339
|
+
// typed since queuing intact. Same protection as #783, applied to
|
|
340
|
+
// the streaming/queue path.
|
|
341
|
+
this.ctx.locallySubmittedUserSignatures.add(`${text}\u0000${images?.length ?? 0}`);
|
|
337
342
|
await this.ctx.session.prompt(text, { streamingBehavior: "steer", images });
|
|
338
343
|
this.ctx.updatePendingMessagesDisplay();
|
|
339
344
|
this.ctx.ui.requestRender();
|
|
@@ -443,6 +448,7 @@ export class InputController {
|
|
|
443
448
|
}
|
|
444
449
|
|
|
445
450
|
restoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number {
|
|
451
|
+
this.ctx.locallySubmittedUserSignatures.clear();
|
|
446
452
|
const { steering, followUp } = this.ctx.session.clearQueue();
|
|
447
453
|
const allQueued = [...steering, ...followUp];
|
|
448
454
|
if (allQueued.length === 0) {
|
|
@@ -170,6 +170,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
170
170
|
unsubscribe?: () => void;
|
|
171
171
|
onInputCallback?: (input: SubmittedUserInput) => void;
|
|
172
172
|
optimisticUserMessageSignature: string | undefined = undefined;
|
|
173
|
+
locallySubmittedUserSignatures: Set<string> = new Set();
|
|
173
174
|
#pendingSubmittedInput: SubmittedUserInput | undefined;
|
|
174
175
|
lastSigintTime = 0;
|
|
175
176
|
lastEscapeTime = 0;
|
|
@@ -497,14 +498,25 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
497
498
|
}
|
|
498
499
|
if (!this.loopModeEnabled || !this.loopPrompt) return;
|
|
499
500
|
const prompt = this.loopPrompt;
|
|
501
|
+
const loopAction = settings.get("loop.mode");
|
|
500
502
|
// Brief delay so the user has a chance to press Esc between iterations.
|
|
501
503
|
this.#loopAutoSubmitTimer = setTimeout(() => {
|
|
502
504
|
this.#loopAutoSubmitTimer = undefined;
|
|
503
505
|
if (!this.loopModeEnabled || !this.onInputCallback) return;
|
|
504
|
-
this
|
|
506
|
+
void this.#runLoopIteration(loopAction, prompt);
|
|
505
507
|
}, 800);
|
|
506
508
|
}
|
|
507
509
|
|
|
510
|
+
async #runLoopIteration(action: "prompt" | "compact" | "reset", prompt: string): Promise<void> {
|
|
511
|
+
if (action === "compact") {
|
|
512
|
+
await this.handleCompactCommand();
|
|
513
|
+
} else if (action === "reset") {
|
|
514
|
+
await this.handleClearCommand();
|
|
515
|
+
}
|
|
516
|
+
if (!this.loopModeEnabled || !this.onInputCallback) return;
|
|
517
|
+
this.onInputCallback(this.startPendingSubmission({ text: prompt }));
|
|
518
|
+
}
|
|
519
|
+
|
|
508
520
|
disableLoopMode(options?: { silent?: boolean }): void {
|
|
509
521
|
const wasEnabled = this.loopModeEnabled;
|
|
510
522
|
this.loopModeEnabled = false;
|
|
@@ -553,6 +565,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
553
565
|
};
|
|
554
566
|
this.#pendingSubmittedInput = submission;
|
|
555
567
|
this.optimisticUserMessageSignature = `${submission.text}\u0000${submission.images?.length ?? 0}`;
|
|
568
|
+
this.locallySubmittedUserSignatures.add(this.optimisticUserMessageSignature);
|
|
556
569
|
this.addMessageToChat({
|
|
557
570
|
role: "user",
|
|
558
571
|
content: [{ type: "text", text: submission.text }, ...(submission.images ?? [])],
|
|
@@ -574,6 +587,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
574
587
|
submission.cancelled = true;
|
|
575
588
|
this.#pendingSubmittedInput = undefined;
|
|
576
589
|
this.optimisticUserMessageSignature = undefined;
|
|
590
|
+
this.locallySubmittedUserSignatures.delete(`${submission.text}\u0000${submission.images?.length ?? 0}`);
|
|
577
591
|
this.#pendingWorkingMessage = undefined;
|
|
578
592
|
if (this.loadingAnimation) {
|
|
579
593
|
this.loadingAnimation.stop();
|
package/src/modes/types.ts
CHANGED
|
@@ -108,6 +108,7 @@ export interface InteractiveModeContext {
|
|
|
108
108
|
unsubscribe?: () => void;
|
|
109
109
|
onInputCallback?: (input: SubmittedUserInput) => void;
|
|
110
110
|
optimisticUserMessageSignature: string | undefined;
|
|
111
|
+
locallySubmittedUserSignatures: Set<string>;
|
|
111
112
|
lastSigintTime: number;
|
|
112
113
|
lastEscapeTime: number;
|
|
113
114
|
shutdownRequested: boolean;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Run a recipe / script / target from the project's task runners.
|
|
2
|
+
|
|
3
|
+
<instruction>
|
|
4
|
+
- `op` is a single string: task name plus any args, e.g. `{op: "test"}` or `{op: "build --release"}`.
|
|
5
|
+
- In monorepos, package and Cargo target tasks are namespaced with `/`, e.g. `{op: "pkg-a/test"}` or `{op: "crate/bin/server"}`.
|
|
6
|
+
{{#if hasMultipleRunners}}- When the same task name exists in more than one runner, prefix with the runner id, e.g. `{op: "{{ambiguityExampleRunner}}:{{ambiguityExampleTask}}"}`. The available runner ids are: {{#each runners}}`{{id}}`{{#unless @last}}, {{/unless}}{{/each}}.
|
|
7
|
+
{{/if}}- Runs in the session's cwd. Output and exit code are returned in the same shape as `bash`.
|
|
8
|
+
</instruction>
|
|
9
|
+
|
|
10
|
+
{{#each runners}}
|
|
11
|
+
<runner id="{{id}}" label="{{label}}" command="{{commandPrefix}}">
|
|
12
|
+
{{#each tasks}}
|
|
13
|
+
- `{{name}}{{#if paramSig}} {{paramSig}}{{/if}}`{{#if doc}} — {{doc}}{{/if}}{{#if command}} (`{{command}}`){{/if}}
|
|
14
|
+
{{/each}}
|
|
15
|
+
</runner>
|
|
16
|
+
{{/each}}
|
package/src/tools/bash.ts
CHANGED
|
@@ -222,15 +222,6 @@ function extractPartialBashEnv(partialJson: string | undefined): Record<string,
|
|
|
222
222
|
return Object.keys(env).length > 0 ? env : undefined;
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
function getBashEnvForDisplay(args: BashRenderArgs): Record<string, string> | undefined {
|
|
226
|
-
// During streaming, partial-json parsing often does not surface env values until the object closes.
|
|
227
|
-
// Recover them from the raw JSON buffer so the pending bash preview can show `NAME="..." cmd` immediately,
|
|
228
|
-
// instead of rendering only the command and making the env assignment appear at the very end.
|
|
229
|
-
const partialEnv = extractPartialBashEnv(args.__partialJson);
|
|
230
|
-
if (partialEnv && args.env) return { ...partialEnv, ...args.env };
|
|
231
|
-
return args.env ?? partialEnv;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
225
|
function formatTimeoutClampNotice(requestedTimeoutSec: number, effectiveTimeoutSec: number): string | undefined {
|
|
235
226
|
return requestedTimeoutSec !== effectiveTimeoutSec
|
|
236
227
|
? `Timeout clamped to ${effectiveTimeoutSec}s (requested ${requestedTimeoutSec}s; allowed range ${TOOL_TIMEOUTS.bash.min}-${TOOL_TIMEOUTS.bash.max}s).`
|
|
@@ -688,8 +679,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
688
679
|
// =============================================================================
|
|
689
680
|
// TUI Renderer
|
|
690
681
|
// =============================================================================
|
|
691
|
-
|
|
692
|
-
interface BashRenderArgs {
|
|
682
|
+
export interface BashRenderArgs {
|
|
693
683
|
command?: string;
|
|
694
684
|
env?: Record<string, string>;
|
|
695
685
|
timeout?: number;
|
|
@@ -698,7 +688,7 @@ interface BashRenderArgs {
|
|
|
698
688
|
[key: string]: unknown;
|
|
699
689
|
}
|
|
700
690
|
|
|
701
|
-
interface BashRenderContext {
|
|
691
|
+
export interface BashRenderContext {
|
|
702
692
|
/** Raw output text */
|
|
703
693
|
output?: string;
|
|
704
694
|
/** Whether output came from artifact storage */
|
|
@@ -711,7 +701,29 @@ interface BashRenderContext {
|
|
|
711
701
|
timeout?: number;
|
|
712
702
|
}
|
|
713
703
|
|
|
714
|
-
|
|
704
|
+
export interface ShellRendererConfig<TArgs> {
|
|
705
|
+
resolveTitle: (args: TArgs | undefined, options: RenderResultOptions) => string;
|
|
706
|
+
resolveCommand?: (args: TArgs | undefined) => string | undefined;
|
|
707
|
+
resolveCwd?: (args: TArgs | undefined) => string | undefined;
|
|
708
|
+
resolveEnv?: (args: TArgs | undefined) => Record<string, string> | undefined;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function getPartialJson<TArgs>(args: TArgs | undefined): string | undefined {
|
|
712
|
+
if (!args || typeof args !== "object" || !("__partialJson" in args)) return undefined;
|
|
713
|
+
const value = (args as { __partialJson?: unknown }).__partialJson;
|
|
714
|
+
return typeof value === "string" ? value : undefined;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
export function getBashEnvForDisplay(args: BashRenderArgs): Record<string, string> | undefined {
|
|
718
|
+
// During streaming, partial-json parsing often does not surface env values until the object closes.
|
|
719
|
+
// Recover them from the raw JSON buffer so the pending bash preview can show `NAME="..." cmd` immediately,
|
|
720
|
+
// instead of rendering only the command and making the env assignment appear at the very end.
|
|
721
|
+
const partialEnv = extractPartialBashEnv(args.__partialJson);
|
|
722
|
+
if (partialEnv && args.env) return { ...partialEnv, ...args.env };
|
|
723
|
+
return args.env ?? partialEnv;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
export function formatBashCommand(args: BashRenderArgs): string {
|
|
715
727
|
const command = replaceTabs(args.command || "…");
|
|
716
728
|
const prompt = "$";
|
|
717
729
|
const cwd = getProjectDir();
|
|
@@ -720,113 +732,135 @@ function formatBashCommand(args: BashRenderArgs): string {
|
|
|
720
732
|
return displayWorkdir ? `${prompt} cd ${displayWorkdir} && ${renderedCommand}` : `${prompt} ${renderedCommand}`;
|
|
721
733
|
}
|
|
722
734
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
+
function toBashRenderArgs<TArgs>(args: TArgs | undefined, config: ShellRendererConfig<TArgs>): BashRenderArgs {
|
|
736
|
+
return {
|
|
737
|
+
command: config.resolveCommand?.(args),
|
|
738
|
+
cwd: config.resolveCwd?.(args),
|
|
739
|
+
env: config.resolveEnv?.(args),
|
|
740
|
+
__partialJson: getPartialJson(args),
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
745
|
+
return {
|
|
746
|
+
renderCall(args: TArgs, options: RenderResultOptions, uiTheme: Theme): Component {
|
|
747
|
+
const renderArgs = toBashRenderArgs(args, config);
|
|
748
|
+
const cmdText = formatBashCommand(renderArgs);
|
|
749
|
+
const title = config.resolveTitle(args, options);
|
|
750
|
+
const text = renderStatusLine({ icon: "pending", title, description: cmdText }, uiTheme);
|
|
751
|
+
return new Text(text, 0, 0);
|
|
735
752
|
},
|
|
736
|
-
options: RenderResultOptions & { renderContext?: BashRenderContext },
|
|
737
|
-
uiTheme: Theme,
|
|
738
|
-
args?: BashRenderArgs,
|
|
739
|
-
): Component {
|
|
740
|
-
const cmdText = args ? formatBashCommand(args) : undefined;
|
|
741
|
-
const isError = result.isError === true;
|
|
742
|
-
const icon = options.isPartial ? "pending" : isError ? "error" : "success";
|
|
743
|
-
const header = renderStatusLine({ icon, title: "Bash" }, uiTheme);
|
|
744
|
-
const details = result.details;
|
|
745
|
-
const outputBlock = new CachedOutputBlock();
|
|
746
753
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
754
|
+
renderResult(
|
|
755
|
+
result: {
|
|
756
|
+
content: Array<{ type: string; text?: string }>;
|
|
757
|
+
details?: BashToolDetails;
|
|
758
|
+
isError?: boolean;
|
|
759
|
+
},
|
|
760
|
+
options: RenderResultOptions & { renderContext?: BashRenderContext },
|
|
761
|
+
uiTheme: Theme,
|
|
762
|
+
args?: TArgs,
|
|
763
|
+
): Component {
|
|
764
|
+
const renderArgs = toBashRenderArgs(args, config);
|
|
765
|
+
const cmdText = args ? formatBashCommand(renderArgs) : undefined;
|
|
766
|
+
const isError = result.isError === true;
|
|
767
|
+
const icon = options.isPartial ? "pending" : isError ? "error" : "success";
|
|
768
|
+
const title = config.resolveTitle(args, options);
|
|
769
|
+
const header = renderStatusLine({ icon, title }, uiTheme);
|
|
770
|
+
const details = result.details;
|
|
771
|
+
const outputBlock = new CachedOutputBlock();
|
|
772
|
+
|
|
773
|
+
return {
|
|
774
|
+
render: (width: number): string[] => {
|
|
775
|
+
// REACTIVE: read mutable options at render time
|
|
776
|
+
const { renderContext } = options;
|
|
777
|
+
const expanded = renderContext?.expanded ?? options.expanded;
|
|
778
|
+
const previewLines = renderContext?.previewLines ?? BASH_DEFAULT_PREVIEW_LINES;
|
|
779
|
+
|
|
780
|
+
// Get output from context (preferred) or fall back to result content
|
|
781
|
+
const output = renderContext?.output ?? result.content?.find(c => c.type === "text")?.text ?? "";
|
|
782
|
+
const displayOutput = output.trimEnd();
|
|
783
|
+
const showingFullOutput = expanded && renderContext?.isFullOutput === true;
|
|
784
|
+
|
|
785
|
+
// Build truncation warning
|
|
786
|
+
const timeoutSeconds = details?.timeoutSeconds ?? renderContext?.timeout;
|
|
787
|
+
const requestedTimeoutSeconds = details?.requestedTimeoutSeconds;
|
|
788
|
+
const timeoutLabel =
|
|
789
|
+
typeof timeoutSeconds === "number"
|
|
790
|
+
? requestedTimeoutSeconds !== undefined && requestedTimeoutSeconds !== timeoutSeconds
|
|
791
|
+
? `Timeout: ${timeoutSeconds}s (requested ${requestedTimeoutSeconds}s clamped)`
|
|
792
|
+
: `Timeout: ${timeoutSeconds}s`
|
|
793
|
+
: undefined;
|
|
794
|
+
const timeoutLine =
|
|
795
|
+
timeoutLabel !== undefined
|
|
796
|
+
? uiTheme.fg("dim", `${uiTheme.format.bracketLeft}${timeoutLabel}${uiTheme.format.bracketRight}`)
|
|
797
|
+
: undefined;
|
|
798
|
+
let warningLine: string | undefined;
|
|
799
|
+
if (details?.meta?.truncation && !showingFullOutput) {
|
|
800
|
+
warningLine = formatStyledTruncationWarning(details.meta, uiTheme) ?? undefined;
|
|
801
|
+
}
|
|
776
802
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
outputLines.push(
|
|
786
|
-
...rawOutputLines.map((line, index) =>
|
|
787
|
-
sixelLineMask?.[index] ? line : uiTheme.fg("toolOutput", replaceTabs(line)),
|
|
788
|
-
),
|
|
789
|
-
);
|
|
790
|
-
} else if (expanded) {
|
|
791
|
-
outputLines.push(...rawOutputLines.map(line => uiTheme.fg("toolOutput", replaceTabs(line))));
|
|
792
|
-
} else {
|
|
793
|
-
const styledOutput = rawOutputLines
|
|
794
|
-
.map(line => uiTheme.fg("toolOutput", replaceTabs(line)))
|
|
795
|
-
.join("\n");
|
|
796
|
-
const textContent = styledOutput;
|
|
797
|
-
const result = truncateToVisualLines(textContent, previewLines, width);
|
|
798
|
-
if (result.skippedCount > 0) {
|
|
803
|
+
const outputLines: string[] = [];
|
|
804
|
+
const hasOutput = displayOutput.trim().length > 0;
|
|
805
|
+
const rawOutputLines = displayOutput.split("\n");
|
|
806
|
+
const sixelLineMask =
|
|
807
|
+
TERMINAL.imageProtocol === ImageProtocol.Sixel ? getSixelLineMask(rawOutputLines) : undefined;
|
|
808
|
+
const hasSixelOutput = sixelLineMask?.some(Boolean) ?? false;
|
|
809
|
+
if (hasOutput) {
|
|
810
|
+
if (hasSixelOutput) {
|
|
799
811
|
outputLines.push(
|
|
800
|
-
|
|
801
|
-
"
|
|
802
|
-
`… (${result.skippedCount} earlier lines, showing ${result.visualLines.length} of ${result.skippedCount + result.visualLines.length}) (ctrl+o to expand)`,
|
|
812
|
+
...rawOutputLines.map((line, index) =>
|
|
813
|
+
sixelLineMask?.[index] ? line : uiTheme.fg("toolOutput", replaceTabs(line)),
|
|
803
814
|
),
|
|
804
815
|
);
|
|
816
|
+
} else if (expanded) {
|
|
817
|
+
outputLines.push(...rawOutputLines.map(line => uiTheme.fg("toolOutput", replaceTabs(line))));
|
|
818
|
+
} else {
|
|
819
|
+
const styledOutput = rawOutputLines
|
|
820
|
+
.map(line => uiTheme.fg("toolOutput", replaceTabs(line)))
|
|
821
|
+
.join("\n");
|
|
822
|
+
const textContent = styledOutput;
|
|
823
|
+
const result = truncateToVisualLines(textContent, previewLines, width);
|
|
824
|
+
if (result.skippedCount > 0) {
|
|
825
|
+
outputLines.push(
|
|
826
|
+
uiTheme.fg(
|
|
827
|
+
"dim",
|
|
828
|
+
`… (${result.skippedCount} earlier lines, showing ${result.visualLines.length} of ${result.skippedCount + result.visualLines.length}) (ctrl+o to expand)`,
|
|
829
|
+
),
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
outputLines.push(...result.visualLines);
|
|
805
833
|
}
|
|
806
|
-
outputLines.push(...result.visualLines);
|
|
807
834
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
}
|
|
835
|
+
if (timeoutLine) outputLines.push(timeoutLine);
|
|
836
|
+
if (warningLine) outputLines.push(warningLine);
|
|
837
|
+
|
|
838
|
+
return outputBlock.render(
|
|
839
|
+
{
|
|
840
|
+
header,
|
|
841
|
+
state: options.isPartial ? "pending" : isError ? "error" : "success",
|
|
842
|
+
sections: [
|
|
843
|
+
{ lines: cmdText ? [uiTheme.fg("dim", cmdText)] : [] },
|
|
844
|
+
{ label: uiTheme.fg("toolTitle", "Output"), lines: outputLines },
|
|
845
|
+
],
|
|
846
|
+
width,
|
|
847
|
+
},
|
|
848
|
+
uiTheme,
|
|
849
|
+
);
|
|
850
|
+
},
|
|
851
|
+
invalidate: () => {
|
|
852
|
+
outputBlock.invalidate();
|
|
853
|
+
},
|
|
854
|
+
};
|
|
855
|
+
},
|
|
856
|
+
mergeCallAndResult: true,
|
|
857
|
+
inline: true,
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
export const bashToolRenderer = createShellRenderer<BashRenderArgs>({
|
|
862
|
+
resolveTitle: () => "Bash",
|
|
863
|
+
resolveCommand: args => args?.command,
|
|
864
|
+
resolveCwd: args => args?.cwd,
|
|
865
|
+
resolveEnv: args => args?.env,
|
|
866
|
+
});
|