@oh-my-pi/pi-coding-agent 14.5.8 → 14.5.9
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 +22 -0
- package/package.json +7 -7
- package/src/config/settings-schema.ts +3 -3
- package/src/edit/modes/atom.lark +7 -5
- package/src/edit/modes/atom.ts +462 -56
- package/src/edit/modes/hashline.ts +21 -1
- package/src/lsp/index.ts +2 -4
- package/src/lsp/render.ts +0 -3
- package/src/lsp/types.ts +1 -4
- package/src/lsp/utils.ts +18 -14
- package/src/modes/controllers/command-controller.ts +17 -0
- package/src/modes/controllers/input-controller.ts +7 -1
- package/src/modes/interactive-mode.ts +30 -23
- package/src/modes/types.ts +4 -2
- package/src/modes/utils/context-usage.ts +294 -0
- package/src/prompts/tools/atom.md +99 -44
- package/src/prompts/tools/exit-plan-mode.md +5 -39
- package/src/prompts/tools/lsp.md +2 -3
- package/src/prompts/tools/{run-command.md → recipe.md} +1 -1
- package/src/prompts/tools/task.md +34 -147
- package/src/prompts/tools/todo-write.md +22 -64
- package/src/session/compaction/compaction.ts +35 -22
- package/src/session/session-dump-format.ts +1 -0
- package/src/slash-commands/builtin-registry.ts +12 -5
- package/src/tools/debug.ts +57 -70
- package/src/tools/index.ts +7 -7
- package/src/tools/{run-command → recipe}/index.ts +19 -19
- package/src/tools/recipe/render.ts +19 -0
- package/src/tools/{run-command → recipe}/runner.ts +28 -7
- package/src/tools/{run-command → recipe}/runners/pkg.ts +23 -53
- package/src/tools/renderers.ts +2 -2
- package/src/tools/run-command/render.ts +0 -18
- /package/src/tools/{run-command → recipe}/runners/cargo.ts +0 -0
- /package/src/tools/{run-command → recipe}/runners/index.ts +0 -0
- /package/src/tools/{run-command → recipe}/runners/just.ts +0 -0
- /package/src/tools/{run-command → recipe}/runners/make.ts +0 -0
- /package/src/tools/{run-command → recipe}/runners/task.ts +0 -0
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
getOpenAIResponsesHistoryPayload,
|
|
27
27
|
normalizeResponsesToolCallId,
|
|
28
28
|
} from "@oh-my-pi/pi-ai/utils";
|
|
29
|
+
import { countTokens } from "@oh-my-pi/pi-natives";
|
|
29
30
|
import { logger, prompt } from "@oh-my-pi/pi-utils";
|
|
30
31
|
import compactionShortSummaryPrompt from "../../prompts/compaction/compaction-short-summary.md" with { type: "text" };
|
|
31
32
|
import compactionSummaryPrompt from "../../prompts/compaction/compaction-summary.md" with { type: "text" };
|
|
@@ -218,7 +219,7 @@ export function shouldCompact(contextTokens: number, contextWindow: number, sett
|
|
|
218
219
|
return contextTokens > thresholdTokens;
|
|
219
220
|
}
|
|
220
221
|
|
|
221
|
-
function resolveThresholdTokens(contextWindow: number, settings: CompactionSettings): number {
|
|
222
|
+
export function resolveThresholdTokens(contextWindow: number, settings: CompactionSettings): number {
|
|
222
223
|
// Fixed token limit takes priority over percentage
|
|
223
224
|
const thresholdTokens = settings.thresholdTokens;
|
|
224
225
|
if (typeof thresholdTokens === "number" && Number.isFinite(thresholdTokens) && thresholdTokens > 0) {
|
|
@@ -240,67 +241,79 @@ function resolveThresholdTokens(contextWindow: number, settings: CompactionSetti
|
|
|
240
241
|
// ============================================================================
|
|
241
242
|
|
|
242
243
|
/**
|
|
243
|
-
*
|
|
244
|
-
*
|
|
244
|
+
* Image content has no tokenizer representation; charge a fixed estimate
|
|
245
|
+
* matching what providers typically bill for inline images.
|
|
246
|
+
*/
|
|
247
|
+
const IMAGE_TOKEN_ESTIMATE = 1200;
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Estimate token count for a message using cl100k_base via the native
|
|
251
|
+
* tokenizer. This is not Claude's first-party tokenizer (Anthropic doesn't
|
|
252
|
+
* publish one) but is within ~5–10% across English/code text.
|
|
245
253
|
*/
|
|
246
254
|
export function estimateTokens(message: AgentMessage): number {
|
|
247
|
-
|
|
255
|
+
const fragments: string[] = [];
|
|
256
|
+
let extra = 0;
|
|
248
257
|
|
|
249
258
|
switch (message.role) {
|
|
250
259
|
case "user": {
|
|
251
260
|
const content = (message as { content: string | Array<{ type: string; text?: string }> }).content;
|
|
252
261
|
if (typeof content === "string") {
|
|
253
|
-
|
|
262
|
+
fragments.push(content);
|
|
254
263
|
} else if (Array.isArray(content)) {
|
|
255
264
|
for (const block of content) {
|
|
256
265
|
if (block.type === "text" && block.text) {
|
|
257
|
-
|
|
266
|
+
fragments.push(block.text);
|
|
258
267
|
}
|
|
259
268
|
}
|
|
260
269
|
}
|
|
261
|
-
|
|
270
|
+
break;
|
|
262
271
|
}
|
|
263
272
|
case "assistant": {
|
|
264
273
|
const assistant = message as AssistantMessage;
|
|
265
274
|
for (const block of assistant.content) {
|
|
266
275
|
if (block.type === "text") {
|
|
267
|
-
|
|
276
|
+
fragments.push(block.text);
|
|
268
277
|
} else if (block.type === "thinking") {
|
|
269
|
-
|
|
278
|
+
fragments.push(block.thinking);
|
|
270
279
|
} else if (block.type === "toolCall") {
|
|
271
|
-
|
|
280
|
+
fragments.push(block.name);
|
|
281
|
+
fragments.push(JSON.stringify(block.arguments));
|
|
272
282
|
}
|
|
273
283
|
}
|
|
274
|
-
|
|
284
|
+
break;
|
|
275
285
|
}
|
|
276
286
|
case "hookMessage":
|
|
277
287
|
case "toolResult": {
|
|
278
288
|
if (typeof message.content === "string") {
|
|
279
|
-
|
|
289
|
+
fragments.push(message.content);
|
|
280
290
|
} else {
|
|
281
291
|
for (const block of message.content) {
|
|
282
292
|
if (block.type === "text" && block.text) {
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
chars += 4800; // Estimate images as 4000 chars, or 1200 tokens
|
|
293
|
+
fragments.push(block.text);
|
|
294
|
+
} else if (block.type === "image") {
|
|
295
|
+
extra += IMAGE_TOKEN_ESTIMATE;
|
|
287
296
|
}
|
|
288
297
|
}
|
|
289
298
|
}
|
|
290
|
-
|
|
299
|
+
break;
|
|
291
300
|
}
|
|
292
301
|
case "bashExecution": {
|
|
293
|
-
|
|
294
|
-
|
|
302
|
+
fragments.push(message.command);
|
|
303
|
+
fragments.push(message.output);
|
|
304
|
+
break;
|
|
295
305
|
}
|
|
296
306
|
case "branchSummary":
|
|
297
307
|
case "compactionSummary": {
|
|
298
|
-
|
|
299
|
-
|
|
308
|
+
fragments.push(message.summary);
|
|
309
|
+
break;
|
|
300
310
|
}
|
|
311
|
+
default:
|
|
312
|
+
return 0;
|
|
301
313
|
}
|
|
302
314
|
|
|
303
|
-
return
|
|
315
|
+
if (fragments.length === 0) return extra;
|
|
316
|
+
return extra + countTokens(fragments);
|
|
304
317
|
}
|
|
305
318
|
|
|
306
319
|
function estimateEntriesTokens(entries: SessionEntry[], startIndex: number, endIndex: number): number {
|
|
@@ -114,6 +114,7 @@ export function formatSessionDumpText(options: FormatSessionDumpTextOptions): st
|
|
|
114
114
|
if (c.type === "text") {
|
|
115
115
|
lines.push(c.text);
|
|
116
116
|
} else if (c.type === "thinking") {
|
|
117
|
+
if (c.thinking.trim().length === 0) continue;
|
|
117
118
|
lines.push("<thinking>");
|
|
118
119
|
lines.push(c.thinking);
|
|
119
120
|
lines.push("</thinking>\n");
|
|
@@ -123,11 +123,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
123
123
|
},
|
|
124
124
|
{
|
|
125
125
|
name: "loop",
|
|
126
|
-
description:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
await runtime.ctx.handleLoopCommand(command.args || undefined);
|
|
126
|
+
description:
|
|
127
|
+
"Toggle loop mode. While enabled, the next prompt you send re-submits after every yield. Esc cancels the current iteration; /loop again to disable.",
|
|
128
|
+
handle: async (_command, runtime) => {
|
|
129
|
+
await runtime.ctx.handleLoopCommand();
|
|
131
130
|
runtime.ctx.editor.setText("");
|
|
132
131
|
},
|
|
133
132
|
},
|
|
@@ -356,6 +355,14 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
356
355
|
runtime.ctx.editor.setText("");
|
|
357
356
|
},
|
|
358
357
|
},
|
|
358
|
+
{
|
|
359
|
+
name: "context",
|
|
360
|
+
description: "Show estimated context usage breakdown",
|
|
361
|
+
handle: (_command, runtime) => {
|
|
362
|
+
runtime.ctx.handleContextCommand();
|
|
363
|
+
runtime.ctx.editor.setText("");
|
|
364
|
+
},
|
|
365
|
+
},
|
|
359
366
|
{
|
|
360
367
|
name: "extensions",
|
|
361
368
|
aliases: ["status"],
|
package/src/tools/debug.ts
CHANGED
|
@@ -52,85 +52,72 @@ import { toolResult } from "./tool-result";
|
|
|
52
52
|
import { clampTimeout } from "./tool-timeouts";
|
|
53
53
|
|
|
54
54
|
const debugSchema = Type.Object({
|
|
55
|
-
action: StringEnum(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
),
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
),
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
function: Type.Optional(Type.String({ description: "function name", examples: ["main", "handle_request"] })),
|
|
97
|
-
name: Type.Optional(Type.String({ description: "variable or data name", examples: ["counter", "buffer"] })),
|
|
98
|
-
condition: Type.Optional(Type.String({ description: "breakpoint condition", examples: ["i == 10", "x > 0"] })),
|
|
99
|
-
hit_condition: Type.Optional(Type.String({ description: "hit condition" })),
|
|
100
|
-
expression: Type.Optional(Type.String({ description: "expression to evaluate", examples: ["x + 1", "obj.field"] })),
|
|
55
|
+
action: StringEnum([
|
|
56
|
+
"launch",
|
|
57
|
+
"attach",
|
|
58
|
+
"set_breakpoint",
|
|
59
|
+
"remove_breakpoint",
|
|
60
|
+
"set_instruction_breakpoint",
|
|
61
|
+
"remove_instruction_breakpoint",
|
|
62
|
+
"data_breakpoint_info",
|
|
63
|
+
"set_data_breakpoint",
|
|
64
|
+
"remove_data_breakpoint",
|
|
65
|
+
"continue",
|
|
66
|
+
"step_over",
|
|
67
|
+
"step_in",
|
|
68
|
+
"step_out",
|
|
69
|
+
"pause",
|
|
70
|
+
"evaluate",
|
|
71
|
+
"stack_trace",
|
|
72
|
+
"threads",
|
|
73
|
+
"scopes",
|
|
74
|
+
"variables",
|
|
75
|
+
"disassemble",
|
|
76
|
+
"read_memory",
|
|
77
|
+
"write_memory",
|
|
78
|
+
"modules",
|
|
79
|
+
"loaded_sources",
|
|
80
|
+
"custom_request",
|
|
81
|
+
"output",
|
|
82
|
+
"terminate",
|
|
83
|
+
"sessions",
|
|
84
|
+
]),
|
|
85
|
+
program: Type.Optional(Type.String({ description: "program path" })),
|
|
86
|
+
args: Type.Optional(Type.Array(Type.String(), { description: "program arguments" })),
|
|
87
|
+
adapter: Type.Optional(Type.String({ description: "debugger adapter (gdb, lldb-dap, debugpy, dlv)" })),
|
|
88
|
+
cwd: Type.Optional(Type.String()),
|
|
89
|
+
file: Type.Optional(Type.String({ description: "source file" })),
|
|
90
|
+
line: Type.Optional(Type.Number({ description: "source line" })),
|
|
91
|
+
function: Type.Optional(Type.String({ description: "function name" })),
|
|
92
|
+
name: Type.Optional(Type.String({ description: "variable or data name" })),
|
|
93
|
+
condition: Type.Optional(Type.String({ description: "breakpoint condition" })),
|
|
94
|
+
hit_condition: Type.Optional(Type.String()),
|
|
95
|
+
expression: Type.Optional(Type.String({ description: "expression to evaluate" })),
|
|
101
96
|
context: Type.Optional(
|
|
102
|
-
Type.String({ description: "evaluate context
|
|
97
|
+
Type.String({ description: "evaluate context: watch | repl | hover | variables | clipboard" }),
|
|
103
98
|
),
|
|
104
|
-
frame_id: Type.Optional(Type.Number(
|
|
99
|
+
frame_id: Type.Optional(Type.Number()),
|
|
105
100
|
scope_id: Type.Optional(Type.Number({ description: "scope variables reference" })),
|
|
106
101
|
variable_ref: Type.Optional(Type.Number({ description: "variable reference" })),
|
|
107
|
-
pid: Type.Optional(Type.Number({ description: "process id for attach"
|
|
108
|
-
port: Type.Optional(Type.Number({ description: "remote attach port"
|
|
109
|
-
host: Type.Optional(Type.String({ description: "remote attach host"
|
|
102
|
+
pid: Type.Optional(Type.Number({ description: "process id for attach" })),
|
|
103
|
+
port: Type.Optional(Type.Number({ description: "remote attach port" })),
|
|
104
|
+
host: Type.Optional(Type.String({ description: "remote attach host" })),
|
|
110
105
|
levels: Type.Optional(Type.Number({ description: "max stack frames" })),
|
|
111
|
-
memory_reference: Type.Optional(
|
|
112
|
-
|
|
113
|
-
),
|
|
114
|
-
|
|
115
|
-
instruction_count: Type.Optional(Type.Number({ description: "instructions to disassemble" })),
|
|
116
|
-
instruction_offset: Type.Optional(Type.Number({ description: "instruction offset" })),
|
|
106
|
+
memory_reference: Type.Optional(Type.String({ description: "memory reference or address" })),
|
|
107
|
+
instruction_reference: Type.Optional(Type.String()),
|
|
108
|
+
instruction_count: Type.Optional(Type.Number()),
|
|
109
|
+
instruction_offset: Type.Optional(Type.Number()),
|
|
117
110
|
count: Type.Optional(Type.Number({ description: "bytes to read" })),
|
|
118
111
|
data: Type.Optional(Type.String({ description: "base64 memory payload" })),
|
|
119
112
|
data_id: Type.Optional(Type.String({ description: "data breakpoint id" })),
|
|
120
|
-
access_type: Type.Optional(
|
|
121
|
-
StringEnum(["read", "write", "readWrite"], { description: "data breakpoint access type" }),
|
|
122
|
-
),
|
|
113
|
+
access_type: Type.Optional(StringEnum(["read", "write", "readWrite"])),
|
|
123
114
|
command: Type.Optional(Type.String({ description: "custom dap request command" })),
|
|
124
|
-
arguments: Type.Optional(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
),
|
|
129
|
-
|
|
130
|
-
resolve_symbols: Type.Optional(Type.Boolean({ description: "resolve symbols during disassembly" })),
|
|
131
|
-
allow_partial: Type.Optional(Type.Boolean({ description: "allow partial writes" })),
|
|
132
|
-
start_module: Type.Optional(Type.Number({ description: "modules start index" })),
|
|
133
|
-
module_count: Type.Optional(Type.Number({ description: "max modules to fetch" })),
|
|
115
|
+
arguments: Type.Optional(Type.Record(Type.String(), Type.Any(), { description: "custom request arguments" })),
|
|
116
|
+
offset: Type.Optional(Type.Number()),
|
|
117
|
+
resolve_symbols: Type.Optional(Type.Boolean()),
|
|
118
|
+
allow_partial: Type.Optional(Type.Boolean()),
|
|
119
|
+
start_module: Type.Optional(Type.Number()),
|
|
120
|
+
module_count: Type.Optional(Type.Number()),
|
|
134
121
|
timeout: Type.Optional(Type.Number({ description: "per-request timeout seconds" })),
|
|
135
122
|
});
|
|
136
123
|
|
package/src/tools/index.ts
CHANGED
|
@@ -37,11 +37,11 @@ import { NotebookTool } from "./notebook";
|
|
|
37
37
|
import { wrapToolWithMetaNotice } from "./output-meta";
|
|
38
38
|
import { PythonTool } from "./python";
|
|
39
39
|
import { ReadTool } from "./read";
|
|
40
|
+
import { RecipeTool } from "./recipe";
|
|
40
41
|
import { RenderMermaidTool } from "./render-mermaid";
|
|
41
42
|
import { createReportToolIssueTool, isAutoQaEnabled } from "./report-tool-issue";
|
|
42
43
|
import { ResolveTool } from "./resolve";
|
|
43
44
|
import { reportFindingTool } from "./review";
|
|
44
|
-
import { RunCommandTool } from "./run-command";
|
|
45
45
|
import { SearchTool } from "./search";
|
|
46
46
|
import { SearchToolBm25Tool } from "./search-tool-bm25";
|
|
47
47
|
import { loadSshTool } from "./ssh";
|
|
@@ -76,11 +76,11 @@ export * from "./job";
|
|
|
76
76
|
export * from "./notebook";
|
|
77
77
|
export * from "./python";
|
|
78
78
|
export * from "./read";
|
|
79
|
+
export * from "./recipe";
|
|
79
80
|
export * from "./render-mermaid";
|
|
80
81
|
export * from "./report-tool-issue";
|
|
81
82
|
export * from "./resolve";
|
|
82
83
|
export * from "./review";
|
|
83
|
-
export * from "./run-command";
|
|
84
84
|
export * from "./search";
|
|
85
85
|
export * from "./search-tool-bm25";
|
|
86
86
|
export * from "./ssh";
|
|
@@ -226,7 +226,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
226
226
|
rewind: RewindTool.createIf,
|
|
227
227
|
task: TaskTool.create,
|
|
228
228
|
job: JobTool.createIf,
|
|
229
|
-
|
|
229
|
+
recipe: RecipeTool.createIf,
|
|
230
230
|
irc: IrcTool.createIf,
|
|
231
231
|
todo_write: s => new TodoWriteTool(s),
|
|
232
232
|
web_search: s => new WebSearchTool(s),
|
|
@@ -375,10 +375,10 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
375
375
|
}
|
|
376
376
|
if (
|
|
377
377
|
requestedTools.includes("bash") &&
|
|
378
|
-
!requestedTools.includes("
|
|
379
|
-
session.settings.get("
|
|
378
|
+
!requestedTools.includes("recipe") &&
|
|
379
|
+
session.settings.get("recipe.enabled")
|
|
380
380
|
) {
|
|
381
|
-
requestedTools.push("
|
|
381
|
+
requestedTools.push("recipe");
|
|
382
382
|
}
|
|
383
383
|
}
|
|
384
384
|
const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
|
|
@@ -402,7 +402,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
402
402
|
if (name === "browser") return session.settings.get("browser.enabled");
|
|
403
403
|
if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
|
|
404
404
|
if (name === "irc") return session.settings.get("irc.enabled");
|
|
405
|
-
if (name === "
|
|
405
|
+
if (name === "recipe") return session.settings.get("recipe.enabled");
|
|
406
406
|
if (name === "task") {
|
|
407
407
|
const maxDepth = session.settings.get("task.maxRecursionDepth") ?? 2;
|
|
408
408
|
const currentDepth = session.taskDepth ?? 0;
|
|
@@ -4,43 +4,43 @@ import { prompt } from "@oh-my-pi/pi-utils";
|
|
|
4
4
|
import { type Static, Type } from "@sinclair/typebox";
|
|
5
5
|
import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
|
|
6
6
|
import type { Theme } from "../../modes/theme/theme";
|
|
7
|
-
import
|
|
7
|
+
import recipeDescription from "../../prompts/tools/recipe.md" with { type: "text" };
|
|
8
8
|
import type { ToolSession } from "..";
|
|
9
9
|
import { type BashRenderContext, BashTool, type BashToolDetails } from "../bash";
|
|
10
|
-
import {
|
|
10
|
+
import { createRecipeToolRenderer, type RecipeRenderArgs } from "./render";
|
|
11
11
|
import { buildPromptModel, type DetectedRunner, resolveCommand } from "./runner";
|
|
12
12
|
import { RUNNERS } from "./runners";
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const recipeSchema = Type.Object({
|
|
15
15
|
op: Type.String({
|
|
16
16
|
description: 'task name and args, e.g. "test" or "build --release"',
|
|
17
17
|
examples: ["test", "build --release", "pkg:test --watch"],
|
|
18
18
|
}),
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
type
|
|
21
|
+
type RecipeParams = Static<typeof recipeSchema>;
|
|
22
22
|
|
|
23
|
-
type
|
|
23
|
+
type RecipeRenderResult = {
|
|
24
24
|
content: Array<{ type: string; text?: string }>;
|
|
25
25
|
details?: BashToolDetails;
|
|
26
26
|
isError?: boolean;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
export class
|
|
30
|
-
readonly name = "
|
|
29
|
+
export class RecipeTool implements AgentTool<typeof recipeSchema, BashToolDetails, Theme> {
|
|
30
|
+
readonly name = "recipe";
|
|
31
31
|
readonly label = "Run";
|
|
32
32
|
readonly description: string;
|
|
33
|
-
readonly parameters =
|
|
33
|
+
readonly parameters = recipeSchema;
|
|
34
34
|
readonly strict = true;
|
|
35
35
|
readonly concurrency = "exclusive";
|
|
36
36
|
readonly mergeCallAndResult = true;
|
|
37
37
|
readonly inline = true;
|
|
38
|
-
readonly renderCall: (args:
|
|
38
|
+
readonly renderCall: (args: RecipeRenderArgs, options: RenderResultOptions, uiTheme: Theme) => Component;
|
|
39
39
|
readonly renderResult: (
|
|
40
|
-
result:
|
|
40
|
+
result: RecipeRenderResult,
|
|
41
41
|
options: RenderResultOptions & { renderContext?: BashRenderContext },
|
|
42
42
|
uiTheme: Theme,
|
|
43
|
-
args?:
|
|
43
|
+
args?: RecipeRenderArgs,
|
|
44
44
|
) => Component;
|
|
45
45
|
|
|
46
46
|
readonly #bash: BashTool;
|
|
@@ -49,30 +49,30 @@ export class RunCommandTool implements AgentTool<typeof runCommandSchema, BashTo
|
|
|
49
49
|
constructor(session: ToolSession, runners: DetectedRunner[]) {
|
|
50
50
|
this.#runners = runners;
|
|
51
51
|
this.#bash = new BashTool(session);
|
|
52
|
-
this.description = prompt.render(
|
|
53
|
-
const renderer =
|
|
52
|
+
this.description = prompt.render(recipeDescription, buildPromptModel(runners));
|
|
53
|
+
const renderer = createRecipeToolRenderer(runners);
|
|
54
54
|
this.renderCall = renderer.renderCall;
|
|
55
55
|
this.renderResult = renderer.renderResult;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
static async createIf(session: ToolSession): Promise<
|
|
59
|
-
if (!session.settings.get("
|
|
58
|
+
static async createIf(session: ToolSession): Promise<RecipeTool | null> {
|
|
59
|
+
if (!session.settings.get("recipe.enabled")) return null;
|
|
60
60
|
const detected = (await Promise.all(RUNNERS.map(runner => runner.detect(session.cwd)))).filter(
|
|
61
61
|
(runner): runner is DetectedRunner => runner !== null && runner.tasks.length > 0,
|
|
62
62
|
);
|
|
63
63
|
if (detected.length === 0) return null;
|
|
64
|
-
return new
|
|
64
|
+
return new RecipeTool(session, detected);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
async execute(
|
|
68
68
|
toolCallId: string,
|
|
69
|
-
{ op }:
|
|
69
|
+
{ op }: RecipeParams,
|
|
70
70
|
signal?: AbortSignal,
|
|
71
71
|
onUpdate?: AgentToolUpdateCallback<BashToolDetails>,
|
|
72
72
|
ctx?: AgentToolContext,
|
|
73
73
|
): Promise<AgentToolResult<BashToolDetails>> {
|
|
74
|
-
const command = resolveCommand(op, this.#runners);
|
|
75
|
-
return await this.#bash.execute(toolCallId, { command }, signal, onUpdate, ctx);
|
|
74
|
+
const { command, cwd } = resolveCommand(op, this.#runners);
|
|
75
|
+
return await this.#bash.execute(toolCallId, { command, cwd }, signal, onUpdate, ctx);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createShellRenderer } from "../bash";
|
|
2
|
+
import type { DetectedRunner } from "./runner";
|
|
3
|
+
import { commandFromOp, cwdFromOp, titleFromOp } from "./runner";
|
|
4
|
+
|
|
5
|
+
export interface RecipeRenderArgs {
|
|
6
|
+
op?: string;
|
|
7
|
+
__partialJson?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createRecipeToolRenderer(runners: DetectedRunner[]) {
|
|
12
|
+
return createShellRenderer<RecipeRenderArgs>({
|
|
13
|
+
resolveTitle: args => titleFromOp(args?.op, runners),
|
|
14
|
+
resolveCommand: args => commandFromOp(args?.op, runners),
|
|
15
|
+
resolveCwd: args => cwdFromOp(args?.op, runners),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const recipeToolRenderer = createRecipeToolRenderer([]);
|
|
@@ -9,6 +9,8 @@ export interface RunnerTask {
|
|
|
9
9
|
commandPrefix?: string;
|
|
10
10
|
/** Token passed to the runner command; defaults to `name`. Used when display names are namespaced. */
|
|
11
11
|
commandName?: string;
|
|
12
|
+
/** Working directory for the task, relative to the session cwd; absent means the runner's root cwd. */
|
|
13
|
+
cwd?: string;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export interface DetectedRunner {
|
|
@@ -39,16 +41,20 @@ interface PromptTaskModel {
|
|
|
39
41
|
paramSig?: string;
|
|
40
42
|
command?: string;
|
|
41
43
|
doc?: string;
|
|
44
|
+
cwd?: string;
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
const PROMPT_TASK_LIMIT = 20;
|
|
48
|
+
|
|
44
49
|
interface PromptRunnerModel {
|
|
45
50
|
id: string;
|
|
46
51
|
label: string;
|
|
47
52
|
commandPrefix: string;
|
|
48
53
|
tasks: PromptTaskModel[];
|
|
54
|
+
hiddenTaskCount?: number;
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
export interface
|
|
57
|
+
export interface RecipePromptModel {
|
|
52
58
|
[key: string]: unknown;
|
|
53
59
|
hasMultipleRunners: boolean;
|
|
54
60
|
ambiguityExampleRunner?: string;
|
|
@@ -101,7 +107,7 @@ function resolveRunnerAndTask(
|
|
|
101
107
|
): { runner: DetectedRunner; task: RunnerTask; tail: string } {
|
|
102
108
|
const { head, tail } = parseOp(op);
|
|
103
109
|
if (!head) {
|
|
104
|
-
throw new ToolError(`
|
|
110
|
+
throw new ToolError(`recipe op is empty. Available tasks:\n${formatAvailableTasks(runners)}`);
|
|
105
111
|
}
|
|
106
112
|
|
|
107
113
|
const colonIndex = head.indexOf(":");
|
|
@@ -136,12 +142,18 @@ function resolveRunnerAndTask(
|
|
|
136
142
|
);
|
|
137
143
|
}
|
|
138
144
|
|
|
139
|
-
export
|
|
145
|
+
export interface ResolvedTask {
|
|
146
|
+
command: string;
|
|
147
|
+
cwd?: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function resolveCommand(op: string, runners: DetectedRunner[]): ResolvedTask {
|
|
140
151
|
const { runner, task, tail } = resolveRunnerAndTask(op, runners);
|
|
141
|
-
|
|
152
|
+
const command = buildCommand(task.commandPrefix ?? runner.commandPrefix, task.commandName ?? task.name, tail);
|
|
153
|
+
return task.cwd ? { command, cwd: task.cwd } : { command };
|
|
142
154
|
}
|
|
143
155
|
|
|
144
|
-
export function
|
|
156
|
+
export function resolveTaskFromOp(op: string | undefined, runners: DetectedRunner[]): ResolvedTask | undefined {
|
|
145
157
|
if (!op) return undefined;
|
|
146
158
|
try {
|
|
147
159
|
return resolveCommand(op, runners);
|
|
@@ -150,6 +162,14 @@ export function commandFromOp(op: string | undefined, runners: DetectedRunner[])
|
|
|
150
162
|
}
|
|
151
163
|
}
|
|
152
164
|
|
|
165
|
+
export function commandFromOp(op: string | undefined, runners: DetectedRunner[]): string | undefined {
|
|
166
|
+
return resolveTaskFromOp(op, runners)?.command;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function cwdFromOp(op: string | undefined, runners: DetectedRunner[]): string | undefined {
|
|
170
|
+
return resolveTaskFromOp(op, runners)?.cwd;
|
|
171
|
+
}
|
|
172
|
+
|
|
153
173
|
export function titleFromOp(op: string | undefined, runners: DetectedRunner[]): string {
|
|
154
174
|
if (!op) return "Run";
|
|
155
175
|
const { head } = parseOp(op);
|
|
@@ -177,7 +197,7 @@ function findAmbiguityExample(runners: DetectedRunner[]): { runner: string; task
|
|
|
177
197
|
return firstRunner && firstTask ? { runner: firstRunner.id, task: firstTask.name } : undefined;
|
|
178
198
|
}
|
|
179
199
|
|
|
180
|
-
export function buildPromptModel(runners: DetectedRunner[]):
|
|
200
|
+
export function buildPromptModel(runners: DetectedRunner[]): RecipePromptModel {
|
|
181
201
|
const ambiguityExample = findAmbiguityExample(runners);
|
|
182
202
|
return {
|
|
183
203
|
hasMultipleRunners: runners.length > 1,
|
|
@@ -187,11 +207,12 @@ export function buildPromptModel(runners: DetectedRunner[]): RunCommandPromptMod
|
|
|
187
207
|
id: runner.id,
|
|
188
208
|
label: runner.label,
|
|
189
209
|
commandPrefix: runner.commandPrefix,
|
|
190
|
-
tasks: runner.tasks.map(task => ({
|
|
210
|
+
tasks: runner.tasks.slice(0, PROMPT_TASK_LIMIT).map(task => ({
|
|
191
211
|
name: task.name,
|
|
192
212
|
paramSig: task.parameters.length > 0 ? task.parameters.join(" ") : undefined,
|
|
193
213
|
command: buildCommand(task.commandPrefix ?? runner.commandPrefix, task.commandName ?? task.name, ""),
|
|
194
214
|
doc: task.doc,
|
|
215
|
+
cwd: task.cwd,
|
|
195
216
|
})),
|
|
196
217
|
})),
|
|
197
218
|
};
|