@oh-my-pi/pi-coding-agent 16.1.15 → 16.1.17
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 +78 -0
- package/dist/cli.js +3531 -3812
- package/dist/types/auto-thinking/classifier.d.ts +4 -2
- package/dist/types/cli/args.d.ts +2 -5
- package/dist/types/cli/flag-tables.d.ts +2 -2
- package/dist/types/cli/session-picker.d.ts +4 -2
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/config/model-discovery.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +12 -1
- package/dist/types/eval/agent-bridge.d.ts +19 -0
- package/dist/types/eval/js/shared/helpers.d.ts +1 -13
- package/dist/types/eval/js/shared/types.d.ts +1 -1
- package/dist/types/eval/js/worker-protocol.d.ts +1 -1
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +22 -7
- package/dist/types/extensibility/plugins/loader.d.ts +16 -9
- package/dist/types/extensibility/tool-event-input.d.ts +2 -0
- package/dist/types/hindsight/content.d.ts +2 -10
- package/dist/types/internal-urls/local-protocol.d.ts +18 -1
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -0
- package/dist/types/modes/components/plugin-settings.d.ts +5 -0
- package/dist/types/modes/components/session-selector.d.ts +25 -0
- package/dist/types/modes/components/status-line/types.d.ts +1 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +14 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +11 -0
- package/dist/types/session/agent-session.d.ts +6 -6
- package/dist/types/session/provider-image-budget.d.ts +3 -0
- package/dist/types/session/session-context.d.ts +6 -5
- package/dist/types/task/isolation-runner.d.ts +128 -0
- package/dist/types/task/parallel.d.ts +4 -0
- package/dist/types/task/worktree.d.ts +14 -1
- package/dist/types/thinking.d.ts +23 -1
- package/dist/types/tiny/title-client.d.ts +2 -2
- package/dist/types/tools/eval-render.d.ts +3 -0
- package/dist/types/tools/eval.d.ts +11 -17
- package/dist/types/tools/todo.d.ts +26 -28
- package/dist/types/tui/output-block.d.ts +8 -0
- package/dist/types/utils/image-resize.d.ts +2 -0
- package/dist/types/utils/tools-manager.d.ts +2 -0
- package/dist/types/web/search/providers/exa.d.ts +2 -0
- package/package.json +12 -12
- package/scripts/build-binary.ts +18 -4
- package/src/auto-thinking/classifier.ts +7 -2
- package/src/cli/args.ts +4 -5
- package/src/cli/flag-tables.ts +3 -3
- package/src/cli/gallery-fixtures/interaction.ts +6 -9
- package/src/cli/gallery-fixtures/shell.ts +15 -23
- package/src/cli/profile-alias.ts +38 -7
- package/src/cli/session-picker.ts +17 -3
- package/src/cli/usage-cli.ts +5 -1
- package/src/commands/launch.ts +3 -3
- package/src/config/model-discovery.ts +59 -8
- package/src/config/model-registry.ts +74 -3
- package/src/config/settings-schema.ts +13 -1
- package/src/discovery/omp-extension-roots.ts +1 -3
- package/src/edit/renderer.ts +34 -12
- package/src/eval/__tests__/agent-bridge.test.ts +462 -3
- package/src/eval/__tests__/helpers-local-roots.test.ts +2 -5
- package/src/eval/__tests__/julia-prelude.test.ts +1 -30
- package/src/eval/__tests__/prelude-agent.test.ts +42 -8
- package/src/eval/agent-bridge.ts +301 -71
- package/src/eval/jl/prelude.jl +32 -227
- package/src/eval/jl/runner.jl +38 -12
- package/src/eval/js/shared/helpers.ts +1 -114
- package/src/eval/js/shared/prelude.txt +13 -27
- package/src/eval/js/shared/runtime.ts +0 -6
- package/src/eval/js/shared/types.ts +1 -1
- package/src/eval/js/worker-protocol.ts +1 -1
- package/src/eval/py/__tests__/prelude.test.ts +13 -0
- package/src/eval/py/executor.ts +1 -1
- package/src/eval/py/prelude.py +47 -105
- package/src/eval/py/runner.py +0 -6
- package/src/eval/rb/prelude.rb +21 -189
- package/src/eval/rb/runner.rb +116 -9
- package/src/export/html/tool-views.generated.js +29 -29
- package/src/extensibility/extensions/wrapper.ts +3 -2
- package/src/extensibility/hooks/tool-wrapper.ts +4 -3
- package/src/extensibility/plugins/legacy-pi-compat.ts +40 -8
- package/src/extensibility/plugins/loader.ts +71 -23
- package/src/extensibility/plugins/marketplace/manager.ts +134 -0
- package/src/extensibility/tool-event-input.ts +57 -0
- package/src/hindsight/content.ts +21 -12
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/local-protocol.ts +100 -53
- package/src/main.ts +15 -4
- package/src/mcp/tool-bridge.ts +27 -2
- package/src/mnemopi/state.ts +5 -2
- package/src/modes/acp/acp-event-mapper.ts +7 -2
- package/src/modes/components/agent-transcript-viewer.ts +193 -41
- package/src/modes/components/chat-transcript-builder.ts +6 -0
- package/src/modes/components/custom-editor.test.ts +18 -1
- package/src/modes/components/custom-editor.ts +77 -45
- package/src/modes/components/hook-editor.ts +15 -2
- package/src/modes/components/plugin-settings.ts +7 -1
- package/src/modes/components/session-selector.ts +143 -29
- package/src/modes/components/settings-selector.ts +2 -2
- package/src/modes/components/status-line/component.ts +52 -8
- package/src/modes/components/status-line/segments.ts +5 -1
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/components/welcome.ts +12 -14
- package/src/modes/controllers/command-controller.ts +21 -5
- package/src/modes/controllers/input-controller.ts +115 -3
- package/src/modes/controllers/selector-controller.ts +19 -1
- package/src/modes/interactive-mode.ts +3 -3
- package/src/modes/rpc/rpc-mode.ts +6 -0
- package/src/modes/utils/copy-targets.ts +7 -2
- package/src/modes/utils/ui-helpers.ts +3 -3
- package/src/prompts/system/system-prompt.md +3 -3
- package/src/prompts/system/workflow-notice.md +3 -3
- package/src/prompts/tools/bash.md +16 -0
- package/src/prompts/tools/eval.md +19 -19
- package/src/prompts/tools/todo.md +1 -1
- package/src/sdk.ts +8 -10
- package/src/session/agent-session.ts +422 -97
- package/src/session/provider-image-budget.ts +86 -0
- package/src/session/session-context.ts +14 -7
- package/src/session/session-storage.ts +24 -2
- package/src/session/snapcompact-inline.ts +19 -3
- package/src/slash-commands/builtin-registry.ts +0 -22
- package/src/slash-commands/helpers/usage-report.ts +9 -1
- package/src/task/index.ts +61 -207
- package/src/task/isolation-runner.ts +354 -0
- package/src/task/parallel.ts +6 -1
- package/src/task/worktree.ts +46 -9
- package/src/thinking.ts +29 -2
- package/src/tiny/title-client.ts +75 -21
- package/src/tools/ask.ts +44 -38
- package/src/tools/bash.ts +9 -2
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/eval-render.ts +34 -27
- package/src/tools/eval.ts +100 -103
- package/src/tools/index.ts +8 -1
- package/src/tools/read.ts +136 -60
- package/src/tools/todo.ts +60 -64
- package/src/tui/code-cell.ts +1 -1
- package/src/tui/output-block.ts +11 -0
- package/src/utils/image-resize.ts +30 -0
- package/src/utils/tools-manager.ts +67 -10
- package/src/web/search/providers/exa.ts +85 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Context,
|
|
3
|
+
DeveloperMessage,
|
|
4
|
+
ImageContent,
|
|
5
|
+
Model,
|
|
6
|
+
TextContent,
|
|
7
|
+
ToolResultMessage,
|
|
8
|
+
UserMessage,
|
|
9
|
+
} from "@oh-my-pi/pi-ai";
|
|
10
|
+
import { providerImageBudget } from "@oh-my-pi/snapcompact";
|
|
11
|
+
|
|
12
|
+
const TOOL_RESULT_IMAGE_OMISSION: TextContent = {
|
|
13
|
+
type: "text",
|
|
14
|
+
text: "[image omitted: provider image limit]",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function countImages(context: Context): number {
|
|
18
|
+
let count = 0;
|
|
19
|
+
for (const message of context.messages) {
|
|
20
|
+
if (!Array.isArray(message.content)) continue;
|
|
21
|
+
for (const part of message.content) {
|
|
22
|
+
if (part.type === "image") count++;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return count;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function clampContent(
|
|
29
|
+
content: readonly (TextContent | ImageContent)[],
|
|
30
|
+
state: { remainingDrops: number },
|
|
31
|
+
): (TextContent | ImageContent)[] | undefined {
|
|
32
|
+
let changed = false;
|
|
33
|
+
const clamped: (TextContent | ImageContent)[] = [];
|
|
34
|
+
for (const part of content) {
|
|
35
|
+
if (part.type === "image" && state.remainingDrops > 0) {
|
|
36
|
+
state.remainingDrops--;
|
|
37
|
+
changed = true;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
clamped.push(part);
|
|
41
|
+
}
|
|
42
|
+
return changed ? clamped : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function clampUserMessage(message: UserMessage, state: { remainingDrops: number }): UserMessage {
|
|
46
|
+
if (!Array.isArray(message.content) || state.remainingDrops <= 0) return message;
|
|
47
|
+
const content = clampContent(message.content, state);
|
|
48
|
+
return content ? { ...message, content } : message;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function clampDeveloperMessage(message: DeveloperMessage, state: { remainingDrops: number }): DeveloperMessage {
|
|
52
|
+
if (!Array.isArray(message.content) || state.remainingDrops <= 0) return message;
|
|
53
|
+
const content = clampContent(message.content, state);
|
|
54
|
+
return content ? { ...message, content } : message;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function clampToolResultMessage(message: ToolResultMessage, state: { remainingDrops: number }): ToolResultMessage {
|
|
58
|
+
if (state.remainingDrops <= 0) return message;
|
|
59
|
+
const content = clampContent(message.content, state);
|
|
60
|
+
if (!content) return message;
|
|
61
|
+
return { ...message, content: content.length > 0 ? content : [TOOL_RESULT_IMAGE_OMISSION] };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Drops oldest transient image blocks so outgoing vision requests fit the active provider's image cap. */
|
|
65
|
+
export function clampProviderContextImages(context: Context, model: Model): Context {
|
|
66
|
+
if (!model.input.includes("image")) return context;
|
|
67
|
+
const limit = providerImageBudget(model.provider);
|
|
68
|
+
const totalImages = countImages(context);
|
|
69
|
+
if (totalImages <= limit) return context;
|
|
70
|
+
|
|
71
|
+
const state = { remainingDrops: totalImages - limit };
|
|
72
|
+
const messages = context.messages.map(message => {
|
|
73
|
+
switch (message.role) {
|
|
74
|
+
case "user":
|
|
75
|
+
return clampUserMessage(message, state);
|
|
76
|
+
case "developer":
|
|
77
|
+
return clampDeveloperMessage(message, state);
|
|
78
|
+
case "toolResult":
|
|
79
|
+
return clampToolResultMessage(message, state);
|
|
80
|
+
case "assistant":
|
|
81
|
+
return message;
|
|
82
|
+
}
|
|
83
|
+
return message;
|
|
84
|
+
});
|
|
85
|
+
return { ...context, messages };
|
|
86
|
+
}
|
|
@@ -62,13 +62,14 @@ export function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEnt
|
|
|
62
62
|
|
|
63
63
|
export interface BuildSessionContextOptions {
|
|
64
64
|
/**
|
|
65
|
-
* Build the
|
|
66
|
-
* every path entry
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* result to a provider.
|
|
65
|
+
* Build the display transcript instead of the LLM context. By default this
|
|
66
|
+
* preserves every path entry with compactions inline; set
|
|
67
|
+
* `collapseCompactedHistory` for the live TUI surface to render only the
|
|
68
|
+
* latest compacted tail.
|
|
70
69
|
*/
|
|
71
70
|
transcript?: boolean;
|
|
71
|
+
/** In transcript mode, elide entries replaced by the latest compaction. */
|
|
72
|
+
collapseCompactedHistory?: boolean;
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
/**
|
|
@@ -255,7 +256,7 @@ export function buildSessionContext(
|
|
|
255
256
|
}
|
|
256
257
|
};
|
|
257
258
|
|
|
258
|
-
if (options?.transcript) {
|
|
259
|
+
if (options?.transcript && !options.collapseCompactedHistory) {
|
|
259
260
|
// Display transcript: every entry in chronological order. Compactions do
|
|
260
261
|
// not erase prior history here — each renders inline (as a divider in the
|
|
261
262
|
// TUI) at the point it fired, with any snapcompact frames re-attached so
|
|
@@ -294,6 +295,7 @@ export function buildSessionContext(
|
|
|
294
295
|
})();
|
|
295
296
|
const remoteReplacementHistory = providerPayload?.items;
|
|
296
297
|
|
|
298
|
+
if (options?.transcript) handleEntryResetTracking(compaction);
|
|
297
299
|
// Emit summary first; re-attach any archived snapcompact frames so the
|
|
298
300
|
// model can keep reading the archived history after every context rebuild.
|
|
299
301
|
const snapcompactArchive = snapcompact.getPreservedArchive(compaction.preserveData);
|
|
@@ -312,7 +314,12 @@ export function buildSessionContext(
|
|
|
312
314
|
// Find compaction index in path
|
|
313
315
|
const compactionIdx = path.findIndex(e => e.type === "compaction" && e.id === compaction.id);
|
|
314
316
|
|
|
315
|
-
|
|
317
|
+
// The remote replacement payload (OpenAI remote compaction) carries the
|
|
318
|
+
// kept turns for the LLM context only; it is not rendered as visible
|
|
319
|
+
// messages. The collapsed display transcript must still emit the kept
|
|
320
|
+
// SessionEntry rows so a remotely-compacted session keeps its recent
|
|
321
|
+
// turns visible instead of showing only the summary and post-compaction.
|
|
322
|
+
if (!remoteReplacementHistory || options?.transcript) {
|
|
316
323
|
// Emit kept messages (before compaction, starting from firstKeptEntryId)
|
|
317
324
|
let foundFirstKept = false;
|
|
318
325
|
for (let i = 0; i < compactionIdx; i++) {
|
|
@@ -137,8 +137,30 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
writeTextSync(fpath: string, content: string): void {
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
const dir = path.dirname(fpath);
|
|
141
|
+
this.ensureDirSync(dir);
|
|
142
|
+
const tempPath = path.join(dir, `.${path.basename(fpath)}.${Snowflake.next()}.tmp`);
|
|
143
|
+
try {
|
|
144
|
+
fs.writeFileSync(tempPath, content);
|
|
145
|
+
fs.renameSync(tempPath, fpath);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
try {
|
|
148
|
+
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
|
|
149
|
+
} catch (cleanupErr) {
|
|
150
|
+
if (!isEnoent(cleanupErr)) {
|
|
151
|
+
logger.warn("Failed to remove session rewrite temp file", {
|
|
152
|
+
sessionFile: fpath,
|
|
153
|
+
tempPath,
|
|
154
|
+
error: toError(cleanupErr).message,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (hasFsCode(err, "EPERM")) {
|
|
159
|
+
fs.writeFileSync(fpath, content);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
throw toError(err);
|
|
163
|
+
}
|
|
142
164
|
}
|
|
143
165
|
|
|
144
166
|
statSync(path: string): SessionStorageStat {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { countTokens } from "@oh-my-pi/pi-agent-core";
|
|
18
18
|
import type { Context, ImageContent, Model, TextContent, ToolResultMessage, UserMessage } from "@oh-my-pi/pi-ai";
|
|
19
|
+
import { isPersonalGitHubCopilotBaseUrl } from "@oh-my-pi/pi-catalog/wire/github-copilot";
|
|
19
20
|
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
20
21
|
import contextFramesNote from "../prompts/system/snapcompact-context-frames-note.md" with { type: "text" };
|
|
21
22
|
import contextStub from "../prompts/system/snapcompact-context-stub.md" with { type: "text" };
|
|
@@ -77,6 +78,19 @@ function passesSavingsGate(frames: number, shape: snapcompact.Shape, textTokens:
|
|
|
77
78
|
return frames * shape.frameTokenEstimate <= textTokens * SAVINGS_MARGIN;
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
/**
|
|
82
|
+
* The model is vision-capable for the endpoint we're actually about to hit.
|
|
83
|
+
* GitHub Copilot business and enterprise hosts respond `400 vision is not
|
|
84
|
+
* supported` on image inputs (issue #3387), so even if a stale cached spec
|
|
85
|
+
* still advertises `["text","image"]` we MUST not rasterize transcripts when
|
|
86
|
+
* the resolved `baseUrl` is non-personal.
|
|
87
|
+
*/
|
|
88
|
+
function canSendImages(model: Model): boolean {
|
|
89
|
+
if (!model.input.includes("image")) return false;
|
|
90
|
+
if (model.provider === "github-copilot" && !isPersonalGitHubCopilotBaseUrl(model.baseUrl)) return false;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
80
94
|
interface SystemPromptImageTarget {
|
|
81
95
|
scope: Exclude<SnapcompactSystemPromptMode, "none">;
|
|
82
96
|
text: string;
|
|
@@ -277,7 +291,7 @@ export function estimateInlineSavings(input: {
|
|
|
277
291
|
messages: readonly InlineMessageView[];
|
|
278
292
|
}): SnapcompactSavingsEstimate {
|
|
279
293
|
const { options, model } = input;
|
|
280
|
-
if (!model
|
|
294
|
+
if (!model || !canSendImages(model)) {
|
|
281
295
|
return { visionCapable: false, savedTokens: 0 };
|
|
282
296
|
}
|
|
283
297
|
|
|
@@ -416,8 +430,10 @@ export class SnapcompactInlineTransformer {
|
|
|
416
430
|
|
|
417
431
|
async transform(context: Context, model: Model): Promise<Context> {
|
|
418
432
|
// Vision gate: providers silently DROP images on text-only models —
|
|
419
|
-
// rendering would lose the content entirely.
|
|
420
|
-
|
|
433
|
+
// rendering would lose the content entirely. Also short-circuits when the
|
|
434
|
+
// resolved endpoint rejects vision regardless of the model's input list
|
|
435
|
+
// (issue #3387: Copilot business endpoint).
|
|
436
|
+
if (!canSendImages(model)) return context;
|
|
421
437
|
|
|
422
438
|
const shape = snapcompact.resolveShape(model, this.options.shape);
|
|
423
439
|
const budget = snapcompact.providerImageBudget(model.provider) - countContextImages(context);
|
|
@@ -239,11 +239,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
239
239
|
return "Plan: off";
|
|
240
240
|
},
|
|
241
241
|
handleTui: async (command, runtime) => {
|
|
242
|
-
const hadArgs = !!command.args;
|
|
243
242
|
await runtime.ctx.handlePlanModeCommand(command.args || undefined);
|
|
244
|
-
if (hadArgs) {
|
|
245
|
-
runtime.ctx.editor.addToHistory(command.text);
|
|
246
|
-
}
|
|
247
243
|
runtime.ctx.editor.setText("");
|
|
248
244
|
},
|
|
249
245
|
},
|
|
@@ -277,11 +273,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
277
273
|
return state ? `Goal: ${state.goal.status} (${shortDetail(state.goal.objective)})` : "Goal: off";
|
|
278
274
|
},
|
|
279
275
|
handleTui: async (command, runtime) => {
|
|
280
|
-
const hadArgs = !!command.args;
|
|
281
276
|
await runtime.ctx.handleGoalModeCommand(command.args || undefined);
|
|
282
|
-
if (hadArgs) {
|
|
283
|
-
runtime.ctx.editor.addToHistory(command.text);
|
|
284
|
-
}
|
|
285
277
|
runtime.ctx.editor.setText("");
|
|
286
278
|
},
|
|
287
279
|
},
|
|
@@ -1270,7 +1262,6 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1270
1262
|
allowArgs: true,
|
|
1271
1263
|
handle: handleMcpAcp,
|
|
1272
1264
|
handleTui: async (command, runtime) => {
|
|
1273
|
-
runtime.ctx.editor.addToHistory(command.text);
|
|
1274
1265
|
runtime.ctx.editor.setText("");
|
|
1275
1266
|
await runtime.ctx.handleMCPCommand(command.text);
|
|
1276
1267
|
},
|
|
@@ -1293,7 +1284,6 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1293
1284
|
allowArgs: true,
|
|
1294
1285
|
handle: handleSshAcp,
|
|
1295
1286
|
handleTui: async (command, runtime) => {
|
|
1296
|
-
runtime.ctx.editor.addToHistory(command.text);
|
|
1297
1287
|
runtime.ctx.editor.setText("");
|
|
1298
1288
|
await runtime.ctx.handleSSHCommand(command.text);
|
|
1299
1289
|
},
|
|
@@ -1436,9 +1426,6 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1436
1426
|
allowArgs: true,
|
|
1437
1427
|
handleTui: async (command, runtime) => {
|
|
1438
1428
|
const question = command.text.slice(`/${command.name}`.length).trim();
|
|
1439
|
-
if (question) {
|
|
1440
|
-
runtime.ctx.editor.addToHistory(command.text);
|
|
1441
|
-
}
|
|
1442
1429
|
runtime.ctx.editor.setText("");
|
|
1443
1430
|
await runtime.ctx.handleBtwCommand(question);
|
|
1444
1431
|
},
|
|
@@ -1450,9 +1437,6 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1450
1437
|
allowArgs: true,
|
|
1451
1438
|
handleTui: async (command, runtime) => {
|
|
1452
1439
|
const work = command.text.slice(`/${command.name}`.length).trim();
|
|
1453
|
-
if (work) {
|
|
1454
|
-
runtime.ctx.editor.addToHistory(command.text);
|
|
1455
|
-
}
|
|
1456
1440
|
runtime.ctx.editor.setText("");
|
|
1457
1441
|
await runtime.ctx.handleTanCommand(work);
|
|
1458
1442
|
},
|
|
@@ -1464,9 +1448,6 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1464
1448
|
allowArgs: true,
|
|
1465
1449
|
handleTui: async (command, runtime) => {
|
|
1466
1450
|
const complaint = command.text.slice(`/${command.name}`.length).trim();
|
|
1467
|
-
if (complaint) {
|
|
1468
|
-
runtime.ctx.editor.addToHistory(command.text);
|
|
1469
|
-
}
|
|
1470
1451
|
runtime.ctx.editor.setText("");
|
|
1471
1452
|
await runtime.ctx.handleOmfgCommand(complaint);
|
|
1472
1453
|
},
|
|
@@ -1558,7 +1539,6 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1558
1539
|
}
|
|
1559
1540
|
},
|
|
1560
1541
|
handleTui: async (command, runtime) => {
|
|
1561
|
-
runtime.ctx.editor.addToHistory(command.text);
|
|
1562
1542
|
runtime.ctx.editor.setText("");
|
|
1563
1543
|
await runtime.ctx.handleMemoryCommand(command.text);
|
|
1564
1544
|
},
|
|
@@ -1586,7 +1566,6 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1586
1566
|
runtime.ctx.editor.setText("");
|
|
1587
1567
|
return;
|
|
1588
1568
|
}
|
|
1589
|
-
runtime.ctx.editor.addToHistory(command.text);
|
|
1590
1569
|
runtime.ctx.editor.setText("");
|
|
1591
1570
|
await runtime.ctx.handleRenameCommand(title);
|
|
1592
1571
|
},
|
|
@@ -1629,7 +1608,6 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1629
1608
|
runtime.ctx.editor.setText("");
|
|
1630
1609
|
return;
|
|
1631
1610
|
}
|
|
1632
|
-
runtime.ctx.editor.addToHistory(command.text);
|
|
1633
1611
|
runtime.ctx.editor.setText("");
|
|
1634
1612
|
await runtime.ctx.handleMoveCommand(targetPath);
|
|
1635
1613
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { UsageLimit, UsageReport } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
2
3
|
import type { OAuthAccountIdentity } from "../../session/auth-storage";
|
|
3
4
|
import type { SlashCommandRuntime } from "../types";
|
|
4
5
|
import { reportMatchesActiveAccount } from "./active-oauth-account";
|
|
@@ -52,6 +53,10 @@ function renderUsageReports(
|
|
|
52
53
|
)) {
|
|
53
54
|
lines.push("", formatProviderName(provider));
|
|
54
55
|
const activeAccount = resolveActiveAccount?.(provider);
|
|
56
|
+
// Provider-wide disclaimers render once per provider, not per limit.
|
|
57
|
+
const providerNotes = [...new Set(providerReports.flatMap(report => report.notes ?? []))];
|
|
58
|
+
for (const note of providerNotes)
|
|
59
|
+
lines.push(` ${sanitizeText(note.replace(/[\r\n]+/g, " ").replace(/\t/g, " "))}`);
|
|
55
60
|
for (const report of providerReports) {
|
|
56
61
|
const inUse = reportMatchesActiveAccount(report, activeAccount);
|
|
57
62
|
const savedResets = report.resetCredits?.availableCount ?? 0;
|
|
@@ -83,7 +88,10 @@ function renderUsageReports(
|
|
|
83
88
|
if (limit.window?.resetsAt && limit.window.resetsAt > nowMs) {
|
|
84
89
|
lines.push(` resets in ${formatDuration(limit.window.resetsAt - nowMs)}`);
|
|
85
90
|
}
|
|
86
|
-
if (limit.notes && limit.notes.length > 0)
|
|
91
|
+
if (limit.notes && limit.notes.length > 0)
|
|
92
|
+
lines.push(
|
|
93
|
+
` ${limit.notes.map(n => sanitizeText(n.replace(/[\r\n]+/g, " ").replace(/\t/g, " "))).join(" • ")}`,
|
|
94
|
+
);
|
|
87
95
|
}
|
|
88
96
|
}
|
|
89
97
|
}
|
package/src/task/index.ts
CHANGED
|
@@ -47,29 +47,22 @@ import type { AsyncJobManager } from "../async";
|
|
|
47
47
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
48
48
|
import { loadOverallPlanReference } from "../plan-mode/plan-handoff";
|
|
49
49
|
import { AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
|
|
50
|
-
import { generateCommitMessage } from "../utils/commit-message-generator";
|
|
51
|
-
import * as git from "../utils/git";
|
|
52
50
|
import { type DiscoveryResult, discoverAgents, getAgent } from "./discovery";
|
|
53
51
|
import { runSubprocess } from "./executor";
|
|
52
|
+
import {
|
|
53
|
+
applyEligibleNestedPatches,
|
|
54
|
+
type IsolationContext,
|
|
55
|
+
makeIsolationCommitMessage,
|
|
56
|
+
mergeIsolatedChanges,
|
|
57
|
+
prepareIsolationContext,
|
|
58
|
+
runIsolatedSubprocess,
|
|
59
|
+
} from "./isolation-runner";
|
|
54
60
|
import { generateTaskName } from "./name-generator";
|
|
55
61
|
import { AgentOutputManager } from "./output-manager";
|
|
56
62
|
import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
|
|
57
63
|
import { renderResult, renderCall as renderTaskCall } from "./render";
|
|
58
64
|
import { repairTaskParams } from "./repair-args";
|
|
59
|
-
import {
|
|
60
|
-
applyNestedPatches,
|
|
61
|
-
captureBaseline,
|
|
62
|
-
captureDeltaPatch,
|
|
63
|
-
cleanupIsolation,
|
|
64
|
-
cleanupTaskBranches,
|
|
65
|
-
commitToBranch,
|
|
66
|
-
ensureIsolation,
|
|
67
|
-
getRepoRoot,
|
|
68
|
-
type IsolationHandle,
|
|
69
|
-
mergeTaskBranches,
|
|
70
|
-
parseIsolationMode,
|
|
71
|
-
type WorktreeBaseline,
|
|
72
|
-
} from "./worktree";
|
|
65
|
+
import { parseIsolationMode } from "./worktree";
|
|
73
66
|
|
|
74
67
|
function renderSubagentUserPrompt(assignment: string): string {
|
|
75
68
|
return prompt.render(subagentUserPromptTemplate, {
|
|
@@ -1047,7 +1040,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1047
1040
|
const isolationRequested = "isolated" in params ? params.isolated === true : false;
|
|
1048
1041
|
const isIsolated = isolationMode !== "none" && isolationRequested;
|
|
1049
1042
|
const mergeMode = this.session.settings.get("task.isolation.merge");
|
|
1050
|
-
const commitStyle = this.session.settings.get("task.isolation.commits");
|
|
1051
1043
|
const taskDepth = this.session.taskDepth ?? 0;
|
|
1052
1044
|
const subagentLspEnabled = (this.session.enableLsp ?? true) && this.session.settings.get("task.enableLsp");
|
|
1053
1045
|
|
|
@@ -1118,12 +1110,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1118
1110
|
// structured output go through eval agent(prompt, schema).
|
|
1119
1111
|
const effectiveOutputSchema = effectiveAgent.output ?? this.session.outputSchema;
|
|
1120
1112
|
|
|
1121
|
-
let
|
|
1122
|
-
let baseline: WorktreeBaseline | null = null;
|
|
1113
|
+
let isolationContext: IsolationContext | null = null;
|
|
1123
1114
|
if (isIsolated) {
|
|
1124
1115
|
try {
|
|
1125
|
-
|
|
1126
|
-
baseline = await captureBaseline(repoRoot);
|
|
1116
|
+
isolationContext = await prepareIsolationContext(this.session.cwd);
|
|
1127
1117
|
} catch (err) {
|
|
1128
1118
|
const message = err instanceof Error ? err.message : String(err);
|
|
1129
1119
|
return {
|
|
@@ -1132,6 +1122,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1132
1122
|
};
|
|
1133
1123
|
}
|
|
1134
1124
|
}
|
|
1125
|
+
const repoRoot = isolationContext?.repoRoot ?? null;
|
|
1135
1126
|
|
|
1136
1127
|
const preferredIsolationBackend = parseIsolationMode(isolationMode);
|
|
1137
1128
|
|
|
@@ -1251,17 +1242,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1251
1242
|
};
|
|
1252
1243
|
emitProgress();
|
|
1253
1244
|
|
|
1254
|
-
const buildCommitMessageFn = ()
|
|
1255
|
-
commitStyle === "ai" && this.session.modelRegistry
|
|
1256
|
-
? async (diff: string) => {
|
|
1257
|
-
return generateCommitMessage(
|
|
1258
|
-
diff,
|
|
1259
|
-
this.session.modelRegistry!,
|
|
1260
|
-
this.session.settings,
|
|
1261
|
-
this.session.getSessionId?.() ?? undefined,
|
|
1262
|
-
);
|
|
1263
|
-
}
|
|
1264
|
-
: undefined;
|
|
1245
|
+
const buildCommitMessageFn = makeIsolationCommitMessage(this.session);
|
|
1265
1246
|
|
|
1266
1247
|
const sharedRunOptions = {
|
|
1267
1248
|
cwd: this.session.cwd,
|
|
@@ -1321,192 +1302,65 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1321
1302
|
if (!isIsolated) {
|
|
1322
1303
|
return runSubprocess(sharedRunOptions);
|
|
1323
1304
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
let isolationHandle: IsolationHandle | undefined;
|
|
1327
|
-
try {
|
|
1328
|
-
if (!repoRoot || !baseline) {
|
|
1329
|
-
throw new Error("Isolated task execution not initialized.");
|
|
1330
|
-
}
|
|
1331
|
-
const taskBaseline = structuredClone(baseline);
|
|
1332
|
-
|
|
1333
|
-
isolationHandle = await ensureIsolation(repoRoot, agentId, preferredIsolationBackend);
|
|
1334
|
-
const isolationDir = isolationHandle.mergedDir;
|
|
1335
|
-
|
|
1336
|
-
// Isolated runs re-discover extensions/custom tools inside the
|
|
1337
|
-
// worktree instead of reusing the parent's source paths.
|
|
1338
|
-
const result = await runSubprocess({
|
|
1339
|
-
...sharedRunOptions,
|
|
1340
|
-
worktree: isolationDir,
|
|
1341
|
-
preloadedExtensionPaths: undefined,
|
|
1342
|
-
preloadedCustomToolPaths: undefined,
|
|
1343
|
-
});
|
|
1344
|
-
if (mergeMode === "branch" && result.exitCode === 0) {
|
|
1345
|
-
try {
|
|
1346
|
-
const commitResult = await commitToBranch(
|
|
1347
|
-
isolationDir,
|
|
1348
|
-
taskBaseline,
|
|
1349
|
-
agentId,
|
|
1350
|
-
params.description,
|
|
1351
|
-
buildCommitMessageFn(),
|
|
1352
|
-
);
|
|
1353
|
-
return {
|
|
1354
|
-
...result,
|
|
1355
|
-
branchName: commitResult?.branchName,
|
|
1356
|
-
nestedPatches: commitResult?.nestedPatches,
|
|
1357
|
-
};
|
|
1358
|
-
} catch (mergeErr) {
|
|
1359
|
-
// Agent succeeded but branch commit failed — clean up stale branch
|
|
1360
|
-
const branchName = `omp/task/${agentId}`;
|
|
1361
|
-
await git.branch.tryDelete(repoRoot, branchName);
|
|
1362
|
-
const msg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
|
|
1363
|
-
return { ...result, error: `Merge failed: ${msg}` };
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
if (result.exitCode === 0) {
|
|
1367
|
-
try {
|
|
1368
|
-
const delta = await captureDeltaPatch(isolationDir, taskBaseline);
|
|
1369
|
-
const patchPath = path.join(effectiveArtifactsDir, `${agentId}.patch`);
|
|
1370
|
-
await Bun.write(patchPath, delta.rootPatch);
|
|
1371
|
-
return {
|
|
1372
|
-
...result,
|
|
1373
|
-
patchPath,
|
|
1374
|
-
nestedPatches: delta.nestedPatches,
|
|
1375
|
-
};
|
|
1376
|
-
} catch (patchErr) {
|
|
1377
|
-
const msg = patchErr instanceof Error ? patchErr.message : String(patchErr);
|
|
1378
|
-
return { ...result, error: `Patch capture failed: ${msg}` };
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
return result;
|
|
1382
|
-
} catch (err) {
|
|
1383
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1384
|
-
return {
|
|
1385
|
-
index: spawnIndex,
|
|
1386
|
-
id: agentId,
|
|
1387
|
-
agent: agent.name,
|
|
1388
|
-
agentSource: agent.source,
|
|
1389
|
-
task: renderSubagentUserPrompt(assignment),
|
|
1390
|
-
assignment,
|
|
1391
|
-
description: params.description,
|
|
1392
|
-
exitCode: 1,
|
|
1393
|
-
output: "",
|
|
1394
|
-
stderr: message,
|
|
1395
|
-
truncated: false,
|
|
1396
|
-
durationMs: Date.now() - taskStart,
|
|
1397
|
-
tokens: 0,
|
|
1398
|
-
requests: 0,
|
|
1399
|
-
modelOverride,
|
|
1400
|
-
error: message,
|
|
1401
|
-
};
|
|
1402
|
-
} finally {
|
|
1403
|
-
if (isolationHandle) {
|
|
1404
|
-
await cleanupIsolation(isolationHandle);
|
|
1405
|
-
}
|
|
1305
|
+
if (!isolationContext) {
|
|
1306
|
+
throw new Error("Isolated task execution not initialized.");
|
|
1406
1307
|
}
|
|
1308
|
+
const taskStart = Date.now();
|
|
1309
|
+
return runIsolatedSubprocess({
|
|
1310
|
+
baseOptions: sharedRunOptions,
|
|
1311
|
+
context: isolationContext,
|
|
1312
|
+
preferredBackend: preferredIsolationBackend,
|
|
1313
|
+
agentId,
|
|
1314
|
+
mergeMode,
|
|
1315
|
+
artifactsDir: effectiveArtifactsDir,
|
|
1316
|
+
description: params.description,
|
|
1317
|
+
buildCommitMessage: buildCommitMessageFn,
|
|
1318
|
+
buildFailureResult: err => {
|
|
1319
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1320
|
+
return {
|
|
1321
|
+
index: spawnIndex,
|
|
1322
|
+
id: agentId,
|
|
1323
|
+
agent: agent.name,
|
|
1324
|
+
agentSource: agent.source,
|
|
1325
|
+
task: renderSubagentUserPrompt(assignment),
|
|
1326
|
+
assignment,
|
|
1327
|
+
description: params.description,
|
|
1328
|
+
exitCode: 1,
|
|
1329
|
+
output: "",
|
|
1330
|
+
stderr: message,
|
|
1331
|
+
truncated: false,
|
|
1332
|
+
durationMs: Date.now() - taskStart,
|
|
1333
|
+
tokens: 0,
|
|
1334
|
+
requests: 0,
|
|
1335
|
+
modelOverride,
|
|
1336
|
+
error: message,
|
|
1337
|
+
};
|
|
1338
|
+
},
|
|
1339
|
+
});
|
|
1407
1340
|
};
|
|
1408
1341
|
|
|
1409
1342
|
const result = await runTask();
|
|
1410
1343
|
|
|
1411
1344
|
let mergeSummary = "";
|
|
1412
1345
|
let changesApplied: boolean | null = null;
|
|
1413
|
-
let hadAnyChanges = false;
|
|
1414
1346
|
let mergedBranchForNestedPatches = false;
|
|
1415
1347
|
if (isIsolated && repoRoot) {
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
mergeSummary = "\n\nNo changes to apply.";
|
|
1421
|
-
} else {
|
|
1422
|
-
const mergeResult = await mergeTaskBranches(repoRoot, [
|
|
1423
|
-
{ branchName: result.branchName, taskId: result.id, description: result.description },
|
|
1424
|
-
]);
|
|
1425
|
-
mergedBranchForNestedPatches = mergeResult.merged.includes(result.branchName);
|
|
1426
|
-
changesApplied = mergeResult.failed.length === 0;
|
|
1427
|
-
hadAnyChanges = changesApplied && mergeResult.merged.length > 0;
|
|
1428
|
-
|
|
1429
|
-
if (changesApplied) {
|
|
1430
|
-
mergeSummary = hadAnyChanges
|
|
1431
|
-
? `\n\nMerged branch: ${result.branchName}`
|
|
1432
|
-
: "\n\nNo changes to apply.";
|
|
1433
|
-
} else {
|
|
1434
|
-
const conflictPart = mergeResult.conflict ? `\nConflict: ${mergeResult.conflict}` : "";
|
|
1435
|
-
mergeSummary = `\n\n<system-notification>Branch merge failed: ${result.branchName}.${conflictPart}\nThe unmerged branch remains for manual resolution.</system-notification>`;
|
|
1436
|
-
}
|
|
1437
|
-
if (mergeResult.stashConflict) {
|
|
1438
|
-
mergeSummary += `\n\n<system-notification>${mergeResult.stashConflict}</system-notification>`;
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
// Clean up the merged branch (keep failed ones for manual resolution)
|
|
1442
|
-
if (changesApplied) {
|
|
1443
|
-
await cleanupTaskBranches(repoRoot, [result.branchName]);
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
} else {
|
|
1447
|
-
// Patch mode: apply the patch from a successful run. A failed or
|
|
1448
|
-
// aborted run has nothing to apply and must not block the result.
|
|
1449
|
-
const succeeded = result.exitCode === 0 && !result.error && !result.aborted;
|
|
1450
|
-
if (!succeeded) {
|
|
1451
|
-
changesApplied = true;
|
|
1452
|
-
hadAnyChanges = false;
|
|
1453
|
-
} else if (!result.patchPath) {
|
|
1454
|
-
changesApplied = false;
|
|
1455
|
-
hadAnyChanges = false;
|
|
1456
|
-
} else {
|
|
1457
|
-
const patchText = await Bun.file(result.patchPath).text();
|
|
1458
|
-
if (!patchText.trim()) {
|
|
1459
|
-
changesApplied = true;
|
|
1460
|
-
hadAnyChanges = false;
|
|
1461
|
-
} else {
|
|
1462
|
-
const normalized = patchText.endsWith("\n") ? patchText : `${patchText}\n`;
|
|
1463
|
-
changesApplied = await git.patch.canApplyText(repoRoot, normalized);
|
|
1464
|
-
if (changesApplied) {
|
|
1465
|
-
try {
|
|
1466
|
-
await git.patch.applyText(repoRoot, normalized);
|
|
1467
|
-
hadAnyChanges = true;
|
|
1468
|
-
} catch {
|
|
1469
|
-
changesApplied = false;
|
|
1470
|
-
hadAnyChanges = false;
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
if (changesApplied) {
|
|
1477
|
-
mergeSummary = hadAnyChanges ? "\n\nApplied patches: yes" : "\n\nNo changes to apply.";
|
|
1478
|
-
} else {
|
|
1479
|
-
const notification =
|
|
1480
|
-
"<system-notification>Patches were not applied and must be handled manually.</system-notification>";
|
|
1481
|
-
const patchList = result.patchPath ? `\n\nPatch artifact:\n- ${result.patchPath}` : "";
|
|
1482
|
-
mergeSummary = `\n\n${notification}${patchList}`;
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
} catch (mergeErr) {
|
|
1486
|
-
const msg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
|
|
1487
|
-
changesApplied = false;
|
|
1488
|
-
hadAnyChanges = false;
|
|
1489
|
-
mergeSummary = `\n\n<system-notification>Merge phase failed: ${msg}\nTask outputs are preserved but changes were not applied.</system-notification>`;
|
|
1490
|
-
}
|
|
1348
|
+
const outcome = await mergeIsolatedChanges({ result, repoRoot, mergeMode });
|
|
1349
|
+
mergeSummary = outcome.summary;
|
|
1350
|
+
changesApplied = outcome.changesApplied;
|
|
1351
|
+
mergedBranchForNestedPatches = outcome.mergedBranchForNestedPatches;
|
|
1491
1352
|
}
|
|
1492
1353
|
|
|
1493
|
-
// Apply nested repo patches (separate from parent git)
|
|
1494
|
-
if (isIsolated && repoRoot
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
await applyNestedPatches(repoRoot, nestedPatches, buildCommitMessageFn());
|
|
1504
|
-
} catch {
|
|
1505
|
-
// Nested patch failures are non-fatal to the parent merge
|
|
1506
|
-
mergeSummary +=
|
|
1507
|
-
"\n\n<system-notification>Some nested repository patches failed to apply.</system-notification>";
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1354
|
+
// Apply nested repo patches (separate from parent git).
|
|
1355
|
+
if (isIsolated && repoRoot) {
|
|
1356
|
+
mergeSummary += await applyEligibleNestedPatches({
|
|
1357
|
+
result,
|
|
1358
|
+
repoRoot,
|
|
1359
|
+
mergeMode,
|
|
1360
|
+
changesApplied,
|
|
1361
|
+
mergedBranchForNestedPatches,
|
|
1362
|
+
commitMessage: buildCommitMessageFn(),
|
|
1363
|
+
});
|
|
1510
1364
|
}
|
|
1511
1365
|
|
|
1512
1366
|
// Cleanup temp directory if used
|