@oh-my-pi/pi-coding-agent 14.5.6 → 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 +26 -0
- package/package.json +7 -7
- package/src/config/model-registry.ts +23 -1
- package/src/config/settings-schema.ts +24 -1
- package/src/config/settings.ts +16 -0
- package/src/edit/modes/atom.ts +3 -5
- package/src/modes/components/hook-editor.ts +2 -2
- package/src/modes/components/settings-defs.ts +10 -0
- package/src/modes/components/status-line/presets.ts +7 -7
- package/src/modes/components/status-line/segments.ts +16 -10
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/components/status-line-segment-editor.ts +1 -1
- package/src/modes/components/status-line.ts +6 -0
- package/src/modes/controllers/event-controller.ts +14 -9
- package/src/modes/controllers/input-controller.ts +15 -0
- package/src/modes/interactive-mode.ts +72 -0
- package/src/modes/theme/defaults/dark-poimandres.json +1 -0
- package/src/modes/theme/defaults/light-poimandres.json +1 -0
- package/src/modes/theme/theme.ts +6 -0
- package/src/modes/types.ts +5 -0
- package/src/prompts/tools/run-command.md +16 -0
- package/src/slash-commands/builtin-registry.ts +10 -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/src/modes/types.ts
CHANGED
|
@@ -86,6 +86,8 @@ export interface InteractiveModeContext {
|
|
|
86
86
|
toolOutputExpanded: boolean;
|
|
87
87
|
todoExpanded: boolean;
|
|
88
88
|
planModeEnabled: boolean;
|
|
89
|
+
loopModeEnabled: boolean;
|
|
90
|
+
loopPrompt?: string;
|
|
89
91
|
planModePlanFilePath?: string;
|
|
90
92
|
hideThinkingBlock: boolean;
|
|
91
93
|
pendingImages: ImageContent[];
|
|
@@ -106,6 +108,7 @@ export interface InteractiveModeContext {
|
|
|
106
108
|
unsubscribe?: () => void;
|
|
107
109
|
onInputCallback?: (input: SubmittedUserInput) => void;
|
|
108
110
|
optimisticUserMessageSignature: string | undefined;
|
|
111
|
+
locallySubmittedUserSignatures: Set<string>;
|
|
109
112
|
lastSigintTime: number;
|
|
110
113
|
lastEscapeTime: number;
|
|
111
114
|
shutdownRequested: boolean;
|
|
@@ -233,6 +236,8 @@ export interface InteractiveModeContext {
|
|
|
233
236
|
openExternalEditor(): void;
|
|
234
237
|
registerExtensionShortcuts(): void;
|
|
235
238
|
handlePlanModeCommand(initialPrompt?: string): Promise<void>;
|
|
239
|
+
handleLoopCommand(prompt?: string): Promise<void>;
|
|
240
|
+
disableLoopMode(options?: { silent?: boolean }): void;
|
|
236
241
|
handleExitPlanModeTool(details: ExitPlanModeDetails): Promise<void>;
|
|
237
242
|
|
|
238
243
|
// Hook UI methods
|
|
@@ -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}}
|
|
@@ -121,6 +121,16 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
121
121
|
runtime.ctx.editor.setText("");
|
|
122
122
|
},
|
|
123
123
|
},
|
|
124
|
+
{
|
|
125
|
+
name: "loop",
|
|
126
|
+
description: "Loop the agent: re-submit the same prompt every time it yields (Esc to stop)",
|
|
127
|
+
inlineHint: "<prompt>",
|
|
128
|
+
allowArgs: true,
|
|
129
|
+
handle: async (command, runtime) => {
|
|
130
|
+
await runtime.ctx.handleLoopCommand(command.args || undefined);
|
|
131
|
+
runtime.ctx.editor.setText("");
|
|
132
|
+
},
|
|
133
|
+
},
|
|
124
134
|
{
|
|
125
135
|
name: "model",
|
|
126
136
|
aliases: ["models"],
|
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
|
+
});
|
package/src/tools/index.ts
CHANGED
|
@@ -41,6 +41,7 @@ import { RenderMermaidTool } from "./render-mermaid";
|
|
|
41
41
|
import { createReportToolIssueTool, isAutoQaEnabled } from "./report-tool-issue";
|
|
42
42
|
import { ResolveTool } from "./resolve";
|
|
43
43
|
import { reportFindingTool } from "./review";
|
|
44
|
+
import { RunCommandTool } from "./run-command";
|
|
44
45
|
import { SearchTool } from "./search";
|
|
45
46
|
import { SearchToolBm25Tool } from "./search-tool-bm25";
|
|
46
47
|
import { loadSshTool } from "./ssh";
|
|
@@ -79,6 +80,7 @@ export * from "./render-mermaid";
|
|
|
79
80
|
export * from "./report-tool-issue";
|
|
80
81
|
export * from "./resolve";
|
|
81
82
|
export * from "./review";
|
|
83
|
+
export * from "./run-command";
|
|
82
84
|
export * from "./search";
|
|
83
85
|
export * from "./search-tool-bm25";
|
|
84
86
|
export * from "./ssh";
|
|
@@ -224,6 +226,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
224
226
|
rewind: RewindTool.createIf,
|
|
225
227
|
task: TaskTool.create,
|
|
226
228
|
job: JobTool.createIf,
|
|
229
|
+
run_command: RunCommandTool.createIf,
|
|
227
230
|
irc: IrcTool.createIf,
|
|
228
231
|
todo_write: s => new TodoWriteTool(s),
|
|
229
232
|
web_search: s => new WebSearchTool(s),
|
|
@@ -370,6 +373,13 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
370
373
|
) {
|
|
371
374
|
requestedTools.push("ast_edit");
|
|
372
375
|
}
|
|
376
|
+
if (
|
|
377
|
+
requestedTools.includes("bash") &&
|
|
378
|
+
!requestedTools.includes("run_command") &&
|
|
379
|
+
session.settings.get("runCommand.enabled")
|
|
380
|
+
) {
|
|
381
|
+
requestedTools.push("run_command");
|
|
382
|
+
}
|
|
373
383
|
}
|
|
374
384
|
const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
|
|
375
385
|
const isToolAllowed = (name: string) => {
|
|
@@ -392,6 +402,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
392
402
|
if (name === "browser") return session.settings.get("browser.enabled");
|
|
393
403
|
if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
|
|
394
404
|
if (name === "irc") return session.settings.get("irc.enabled");
|
|
405
|
+
if (name === "run_command") return session.settings.get("runCommand.enabled");
|
|
395
406
|
if (name === "task") {
|
|
396
407
|
const maxDepth = session.settings.get("task.maxRecursionDepth") ?? 2;
|
|
397
408
|
const currentDepth = session.taskDepth ?? 0;
|
package/src/tools/renderers.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { notebookToolRenderer } from "./notebook";
|
|
|
24
24
|
import { pythonToolRenderer } from "./python";
|
|
25
25
|
import { readToolRenderer } from "./read";
|
|
26
26
|
import { resolveToolRenderer } from "./resolve";
|
|
27
|
+
import { runCommandToolRenderer } from "./run-command/render";
|
|
27
28
|
import { searchToolRenderer } from "./search";
|
|
28
29
|
import { searchToolBm25Renderer } from "./search-tool-bm25";
|
|
29
30
|
import { sshToolRenderer } from "./ssh";
|
|
@@ -48,6 +49,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
|
|
|
48
49
|
ast_grep: astGrepToolRenderer as ToolRenderer,
|
|
49
50
|
ast_edit: astEditToolRenderer as ToolRenderer,
|
|
50
51
|
bash: bashToolRenderer as ToolRenderer,
|
|
52
|
+
run_command: runCommandToolRenderer as ToolRenderer,
|
|
51
53
|
debug: debugToolRenderer as ToolRenderer,
|
|
52
54
|
python: pythonToolRenderer as ToolRenderer,
|
|
53
55
|
calc: calculatorToolRenderer as ToolRenderer,
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import { prompt } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
5
|
+
import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
|
|
6
|
+
import type { Theme } from "../../modes/theme/theme";
|
|
7
|
+
import runCommandDescription from "../../prompts/tools/run-command.md" with { type: "text" };
|
|
8
|
+
import type { ToolSession } from "..";
|
|
9
|
+
import { type BashRenderContext, BashTool, type BashToolDetails } from "../bash";
|
|
10
|
+
import { createRunCommandToolRenderer, type RunCommandRenderArgs } from "./render";
|
|
11
|
+
import { buildPromptModel, type DetectedRunner, resolveCommand } from "./runner";
|
|
12
|
+
import { RUNNERS } from "./runners";
|
|
13
|
+
|
|
14
|
+
const runCommandSchema = Type.Object({
|
|
15
|
+
op: Type.String({
|
|
16
|
+
description: 'task name and args, e.g. "test" or "build --release"',
|
|
17
|
+
examples: ["test", "build --release", "pkg:test --watch"],
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
type RunCommandParams = Static<typeof runCommandSchema>;
|
|
22
|
+
|
|
23
|
+
type RunCommandRenderResult = {
|
|
24
|
+
content: Array<{ type: string; text?: string }>;
|
|
25
|
+
details?: BashToolDetails;
|
|
26
|
+
isError?: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export class RunCommandTool implements AgentTool<typeof runCommandSchema, BashToolDetails, Theme> {
|
|
30
|
+
readonly name = "run_command";
|
|
31
|
+
readonly label = "Run";
|
|
32
|
+
readonly description: string;
|
|
33
|
+
readonly parameters = runCommandSchema;
|
|
34
|
+
readonly strict = true;
|
|
35
|
+
readonly concurrency = "exclusive";
|
|
36
|
+
readonly mergeCallAndResult = true;
|
|
37
|
+
readonly inline = true;
|
|
38
|
+
readonly renderCall: (args: RunCommandRenderArgs, options: RenderResultOptions, uiTheme: Theme) => Component;
|
|
39
|
+
readonly renderResult: (
|
|
40
|
+
result: RunCommandRenderResult,
|
|
41
|
+
options: RenderResultOptions & { renderContext?: BashRenderContext },
|
|
42
|
+
uiTheme: Theme,
|
|
43
|
+
args?: RunCommandRenderArgs,
|
|
44
|
+
) => Component;
|
|
45
|
+
|
|
46
|
+
readonly #bash: BashTool;
|
|
47
|
+
readonly #runners: DetectedRunner[];
|
|
48
|
+
|
|
49
|
+
constructor(session: ToolSession, runners: DetectedRunner[]) {
|
|
50
|
+
this.#runners = runners;
|
|
51
|
+
this.#bash = new BashTool(session);
|
|
52
|
+
this.description = prompt.render(runCommandDescription, buildPromptModel(runners));
|
|
53
|
+
const renderer = createRunCommandToolRenderer(runners);
|
|
54
|
+
this.renderCall = renderer.renderCall;
|
|
55
|
+
this.renderResult = renderer.renderResult;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static async createIf(session: ToolSession): Promise<RunCommandTool | null> {
|
|
59
|
+
if (!session.settings.get("runCommand.enabled")) return null;
|
|
60
|
+
const detected = (await Promise.all(RUNNERS.map(runner => runner.detect(session.cwd)))).filter(
|
|
61
|
+
(runner): runner is DetectedRunner => runner !== null && runner.tasks.length > 0,
|
|
62
|
+
);
|
|
63
|
+
if (detected.length === 0) return null;
|
|
64
|
+
return new RunCommandTool(session, detected);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async execute(
|
|
68
|
+
toolCallId: string,
|
|
69
|
+
{ op }: RunCommandParams,
|
|
70
|
+
signal?: AbortSignal,
|
|
71
|
+
onUpdate?: AgentToolUpdateCallback<BashToolDetails>,
|
|
72
|
+
ctx?: AgentToolContext,
|
|
73
|
+
): Promise<AgentToolResult<BashToolDetails>> {
|
|
74
|
+
const command = resolveCommand(op, this.#runners);
|
|
75
|
+
return await this.#bash.execute(toolCallId, { command }, signal, onUpdate, ctx);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export * from "./runner";
|
|
80
|
+
export { tasksFromCargoMetadata } from "./runners/cargo";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createShellRenderer } from "../bash";
|
|
2
|
+
import type { DetectedRunner } from "./runner";
|
|
3
|
+
import { commandFromOp, titleFromOp } from "./runner";
|
|
4
|
+
|
|
5
|
+
export interface RunCommandRenderArgs {
|
|
6
|
+
op?: string;
|
|
7
|
+
__partialJson?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createRunCommandToolRenderer(runners: DetectedRunner[]) {
|
|
12
|
+
return createShellRenderer<RunCommandRenderArgs>({
|
|
13
|
+
resolveTitle: args => titleFromOp(args?.op, runners),
|
|
14
|
+
resolveCommand: args => commandFromOp(args?.op, runners),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const runCommandToolRenderer = createRunCommandToolRenderer([]);
|