kritya 0.8.25-beta → 0.8.26-beta
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/dist/agent/loop.js +9 -0
- package/dist/agent/toolExecutor.js +10 -1
- package/dist/commands/registry.js +4 -0
- package/dist/ui/AiDisclosurePrompt.js +1 -1
- package/dist/ui/App.js +15 -4
- package/dist/ui/StatusLine.js +10 -8
- package/dist/ui/TranscriptItem.js +1 -1
- package/dist/ui/useAgent.js +80 -2
- package/package.json +1 -1
package/dist/agent/loop.js
CHANGED
|
@@ -45,6 +45,15 @@ export class Agent {
|
|
|
45
45
|
dryRunMode = false;
|
|
46
46
|
/** When true, file-edit tools auto-approve without prompting (see ACCEPT_EDITS_TOOL_NAMES). */
|
|
47
47
|
acceptEdits = false;
|
|
48
|
+
/**
|
|
49
|
+
* When true, EVERY tool call auto-approves without prompting — including
|
|
50
|
+
* `shell` and classifyDanger-flagged destructive commands, unlike
|
|
51
|
+
* `acceptEdits`. Only reachable from the UI when the sandbox is active
|
|
52
|
+
* (see sandboxAvailable in shell/sandbox.ts), since that's the safety net
|
|
53
|
+
* this mode leans on instead of a human in the loop. Deny rules and the
|
|
54
|
+
* kill switch still apply regardless of this flag.
|
|
55
|
+
*/
|
|
56
|
+
bypassMode = false;
|
|
48
57
|
/**
|
|
49
58
|
* Whether a human can see and respond to a permission prompt right now.
|
|
50
59
|
* True by default (the CLI and its subagents render one); headless runs
|
|
@@ -236,8 +236,17 @@ export class ToolExecutor {
|
|
|
236
236
|
danger === null &&
|
|
237
237
|
tool.requiresPermission &&
|
|
238
238
|
ACCEPT_EDITS_TOOL_NAMES.has(tool.name);
|
|
239
|
+
// Unlike autoApproveEdit, this covers every tool — including `shell` and
|
|
240
|
+
// classifyDanger-flagged destructive commands — since bypassMode leans on
|
|
241
|
+
// the sandbox (not a human) to contain the blast radius. planMode/dryRunMode
|
|
242
|
+
// already returned above, so this can't apply while either is on.
|
|
243
|
+
const autoApproveBypass = host.bypassMode;
|
|
239
244
|
let source;
|
|
240
|
-
if (
|
|
245
|
+
if (autoApproveBypass) {
|
|
246
|
+
host.onAutoApprove?.();
|
|
247
|
+
source = "bypass-mode";
|
|
248
|
+
}
|
|
249
|
+
else if (autoApproveEdit) {
|
|
241
250
|
host.onAutoApprove?.();
|
|
242
251
|
source = "accept-edits";
|
|
243
252
|
}
|
|
@@ -232,6 +232,10 @@ async function runPhase(ctx, phase) {
|
|
|
232
232
|
ctx.agent.acceptEdits = false;
|
|
233
233
|
ctx.setAcceptEdits(false);
|
|
234
234
|
}
|
|
235
|
+
if (wantPlanMode && ctx.bypassMode) {
|
|
236
|
+
ctx.agent.bypassMode = false;
|
|
237
|
+
ctx.setBypassMode(false);
|
|
238
|
+
}
|
|
235
239
|
saveProjectState(ctx.workspace, project.name, phase);
|
|
236
240
|
const artifact = artifactPath(project.name, phase);
|
|
237
241
|
ctx.addItem({
|
|
@@ -19,5 +19,5 @@ export function AiDisclosurePrompt({ onDismiss }) {
|
|
|
19
19
|
const id = setInterval(() => setBlinkOn((v) => !v), BLINK_INTERVAL_MS);
|
|
20
20
|
return () => clearInterval(id);
|
|
21
21
|
}, []);
|
|
22
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [_jsx(Text, { children: "kritya is an AI agent" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "
|
|
22
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [_jsx(Text, { children: "kritya is an AI agent" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "It reads files, edits your project, and runs commands \u2014 driven by an AI model, not a human. You approve changes before they happen." }) }), _jsx(Text, { dimColor: true, children: "Details: /help, SECURITY.md (EU AI Act Art. 50)." }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Shown once. ", _jsx(Text, { color: "yellow", children: blinkOn ? "AI Disclosure" : " " })] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "Press Enter to continue\u2026" }) })] }));
|
|
23
23
|
}
|
package/dist/ui/App.js
CHANGED
|
@@ -125,7 +125,7 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
125
125
|
.catch(() => { });
|
|
126
126
|
}, [workspace]);
|
|
127
127
|
useEffect(refreshFileList, [refreshFileList]);
|
|
128
|
-
const { items, addItem, phase, setPhase, stream, thinking, activity, setActivity, runningPhase, setRunningPhase, workflow, refreshWorkflow, permission, elicitation, inFlight, model, provider, servedModel, totalUsage, totalCost, tasks, setTasks, ctxPct, setCtxPct, tokenBudget, budgetPct, budgetUsed, budgetStopped, resetBudget, setBudgetLimit, branch, planMode, setPlanMode, dryRunMode, killed, killReason, engageKill, releaseKill, acceptEdits, setAcceptEdits, autoApprovedCount, cycleMode, onAcceptEditsConfirm, abortRef, setModelEverywhere, setProviderEverywhere, costReport, runAgent, runWebSearch, onPermissionDecision, onElicitationDecision, onResumeSelect, requestPermission, requestElicitation, persistenceWarningCount, } = useAgent({
|
|
128
|
+
const { items, addItem, phase, setPhase, stream, thinking, activity, setActivity, runningPhase, setRunningPhase, workflow, refreshWorkflow, permission, elicitation, inFlight, model, provider, servedModel, totalUsage, totalCost, tasks, setTasks, ctxPct, setCtxPct, tokenBudget, budgetPct, budgetUsed, budgetStopped, resetBudget, setBudgetLimit, branch, planMode, setPlanMode, dryRunMode, killed, killReason, engageKill, releaseKill, acceptEdits, setAcceptEdits, bypassMode, setBypassMode, autoApprovedCount, cycleMode, onAcceptEditsConfirm, onBypassModeConfirm, abortRef, setModelEverywhere, setProviderEverywhere, costReport, runAgent, runWebSearch, onPermissionDecision, onElicitationDecision, onResumeSelect, requestPermission, requestElicitation, persistenceWarningCount, } = useAgent({
|
|
129
129
|
agent,
|
|
130
130
|
workspace,
|
|
131
131
|
modelRef,
|
|
@@ -258,6 +258,15 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
258
258
|
onAcceptEditsConfirm(false);
|
|
259
259
|
return;
|
|
260
260
|
}
|
|
261
|
+
// First-time auto/bypass-mode confirmation: Yes proceeds, anything else cancels.
|
|
262
|
+
if (phase === "confirmBypassMode") {
|
|
263
|
+
const c = _input.toLowerCase();
|
|
264
|
+
if (c === "y" || key.return)
|
|
265
|
+
onBypassModeConfirm(true);
|
|
266
|
+
else if (c === "n" || key.escape)
|
|
267
|
+
onBypassModeConfirm(false);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
261
270
|
// History recall with ↑/↓ when no autocomplete popup is open.
|
|
262
271
|
if (phase === "input" && !suggestions.length && !fileSuggestions.length) {
|
|
263
272
|
const hist = inputHistory.current;
|
|
@@ -320,6 +329,8 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
320
329
|
planMode,
|
|
321
330
|
acceptEdits,
|
|
322
331
|
setAcceptEdits,
|
|
332
|
+
bypassMode,
|
|
333
|
+
setBypassMode,
|
|
323
334
|
tokenBudget,
|
|
324
335
|
budgetPct,
|
|
325
336
|
budgetUsed,
|
|
@@ -444,7 +455,7 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
444
455
|
addItem({ kind: "info", text: `Attached ${images.length} image(s).` });
|
|
445
456
|
await runAgent(await expandMentions(text), images);
|
|
446
457
|
};
|
|
447
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: items, children: (item) => _jsx(TranscriptItem, { item: item, verbose: verbose, stdout: stdout }, item.id) }, staticKey), stream ? (_jsx(Box, { marginBottom: 1, children: _jsx(Markdown, { text: tailForViewport(stream, terminalColumns(stdout), terminalRows(stdout)), streaming: true }) })) : null, tasks.length > 0 && (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "blue", paddingX: 1, width: Math.min(terminalColumns(stdout) - 2, Math.max(...tasks.map((t) => stringWidth(t.text))) + 6), children: tasks.map((t, i) => (_jsxs(Text, { color: t.status === "done" ? "green" : t.status === "in_progress" ? "yellow" : undefined, dimColor: t.status === "pending", children: [t.status === "done" ? "☑" : t.status === "in_progress" ? "◐" : "☐", " ", t.text] }, i))) })), phase === "working" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Spinner, { label:
|
|
458
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: items, children: (item) => _jsx(TranscriptItem, { item: item, verbose: verbose, stdout: stdout }, item.id) }, staticKey), firstLaunch && items.length <= 1 && phase === "input" && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, paddingLeft: 2, children: [_jsx(Text, { dimColor: true, children: "Try asking:" }), _jsx(Text, { dimColor: true, children: " \u00B7 Explain this codebase" }), _jsx(Text, { dimColor: true, children: " \u00B7 Find and fix failing tests" }), _jsx(Text, { dimColor: true, children: " \u00B7 Review my uncommitted changes" }), _jsx(Text, { dimColor: true, children: " \u00B7 Add a new feature" }), _jsx(Text, { dimColor: true, children: " \u00B7 /help for commands" })] })), stream ? (_jsx(Box, { marginBottom: 1, children: _jsx(Markdown, { text: tailForViewport(stream, terminalColumns(stdout), terminalRows(stdout)), streaming: true }) })) : null, tasks.length > 0 && (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "blue", paddingX: 1, width: Math.min(terminalColumns(stdout) - 2, Math.max(...tasks.map((t) => stringWidth(t.text))) + 6), children: tasks.map((t, i) => (_jsxs(Text, { color: t.status === "done" ? "green" : t.status === "in_progress" ? "yellow" : undefined, dimColor: t.status === "pending", children: [t.status === "done" ? "☑" : t.status === "in_progress" ? "◐" : "☐", " ", t.text] }, i))) })), phase === "working" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Spinner, { label:
|
|
448
459
|
// The workflow prefix rides on every branch, not just "thinking":
|
|
449
460
|
// during a phase you want to know which phase a tool call belongs
|
|
450
461
|
// to just as much as which phase is being reasoned about.
|
|
@@ -471,7 +482,7 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
471
482
|
}
|
|
472
483
|
addItem({ kind: "user", text: `${text} (queued)` });
|
|
473
484
|
void expandMentions(text).then((t) => agent.queueSteer(t));
|
|
474
|
-
}, placeholder: "steer the agent\u2026 (Enter to queue)" })] })] })), phase === "permission" && permission && (_jsx(PermissionPrompt, { toolName: permission.toolName, summary: permission.summary, diff: permission.diff, warning: permission.warning, onDecision: onPermissionDecision })), phase === "elicitation" && elicitation && (_jsx(ElicitationPrompt, { message: elicitation.message, fields: elicitation.fields, onDecision: onElicitationDecision })), phase === "confirmMode" && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: "Switch to accept-edits mode?" }), _jsx(Text, { children: "File writes and edits will auto-approve without asking." }), _jsx(Text, { dimColor: true, children: "Destructive shell commands (rm -rf, force-push, etc.) always still ask, in every mode." }), _jsx(Text, { dimColor: true, children: "Shift+Tab again moves to dry-run mode; once more back to normal." }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "green", children: "Yes (y)" }), " \u00B7 No (n/Esc)"] }) })] })), phase === "model" && (_jsx(ModelPicker, { current: model, customModels: config.customModels ?? [], onSelect: (id) => {
|
|
485
|
+
}, placeholder: "steer the agent\u2026 (Enter to queue)" })] })] })), phase === "permission" && permission && (_jsx(PermissionPrompt, { toolName: permission.toolName, summary: permission.summary, diff: permission.diff, warning: permission.warning, onDecision: onPermissionDecision })), phase === "elicitation" && elicitation && (_jsx(ElicitationPrompt, { message: elicitation.message, fields: elicitation.fields, onDecision: onElicitationDecision })), phase === "confirmMode" && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: "Switch to accept-edits mode?" }), _jsx(Text, { children: "File writes and edits will auto-approve without asking." }), _jsx(Text, { dimColor: true, children: "Destructive shell commands (rm -rf, force-push, etc.) always still ask, in every mode." }), _jsx(Text, { dimColor: true, children: "Shift+Tab again moves to dry-run mode; once more back to normal." }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "green", children: "Yes (y)" }), " \u00B7 No (n/Esc)"] }) })] })), phase === "confirmBypassMode" && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Switch to auto mode? (sandbox-gated)" }), _jsx(Text, { children: "EVERY tool call will auto-approve, including destructive shell commands (rm -rf, force-push, etc.) \u2014 nothing will ask first." }), _jsx(Text, { dimColor: true, children: "The sandbox \u2014 not a human \u2014 is what contains any damage this mode causes." }), _jsx(Text, { dimColor: true, children: "Shift+Tab again goes back to normal." }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "red", children: "Yes (y)" }), " \u00B7 No (n/Esc)"] }) })] })), phase === "model" && (_jsx(ModelPicker, { current: model, customModels: config.customModels ?? [], onSelect: (id) => {
|
|
475
486
|
setPhase("input");
|
|
476
487
|
setModelEverywhere(id);
|
|
477
488
|
}, onCancel: () => setPhase("input") })), phase === "resume" && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "magenta", children: ["Resume a session ", _jsx(Text, { dimColor: true, children: "(type to search \u00B7 Esc for a fresh one)" })] }), resumeFilter ? (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "filter: " }), resumeFilter] })) : null, _jsx(SelectList, { items: (resumeSessions ?? [])
|
|
@@ -496,7 +507,7 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
496
507
|
lastCategory = c.category;
|
|
497
508
|
return (_jsxs(Fragment, { children: [showHeader && _jsx(Text, { dimColor: true, children: c.category ?? "Other" }), _jsxs(Text, { color: i === selectedCmd ? "cyan" : undefined, children: [i === selectedCmd ? "❯ " : " ", _jsx(Text, { bold: i === selectedCmd, children: c.name }), _jsxs(Text, { dimColor: true, children: [" \u2014 ", c.description] })] })] }, c.name));
|
|
498
509
|
});
|
|
499
|
-
})(), _jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u00B7 Tab/Enter select \u00B7 Enter again to run" })] })), fileSuggestions.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [fileSuggestions.map((f, i) => (_jsxs(Text, { color: i === selectedFile ? "cyan" : undefined, children: [i === selectedFile ? "❯ " : " ", f] }, f))), _jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u00B7 Tab/Enter attach file" })] }))] })), _jsx(Box, { children: _jsx(StatusLine, { killed: killed, killReason: killReason, dryRunMode: dryRunMode, planMode: planMode, acceptEdits: acceptEdits, autoApprovedCount: autoApprovedCount, provider: provider, model: servedModel && servedModel !== model
|
|
510
|
+
})(), _jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u00B7 Tab/Enter select \u00B7 Enter again to run" })] })), fileSuggestions.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [fileSuggestions.map((f, i) => (_jsxs(Text, { color: i === selectedFile ? "cyan" : undefined, children: [i === selectedFile ? "❯ " : " ", f] }, f))), _jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u00B7 Tab/Enter attach file" })] }))] })), _jsx(Box, { children: _jsx(StatusLine, { killed: killed, killReason: killReason, dryRunMode: dryRunMode, planMode: planMode, acceptEdits: acceptEdits, bypassMode: bypassMode, autoApprovedCount: autoApprovedCount, provider: provider, model: servedModel && servedModel !== model
|
|
500
511
|
? `${model}-${modelDisplaySlug(servedModel)}`
|
|
501
512
|
: model, workflow: workflow, branch: branch, budgetPct: budgetPct, budgetStopped: budgetStopped, totalCost: totalCost, sandboxActive: (config.sandboxExec ?? defaultSandboxMode()) !== "off" && sandboxAvailable(), persistenceWarningCount: persistenceWarningCount, privacyMode: privacyMode }) })] }));
|
|
502
513
|
}
|
package/dist/ui/StatusLine.js
CHANGED
|
@@ -8,12 +8,14 @@ import { displayModelId } from "../config/models.js";
|
|
|
8
8
|
* moved to the on-demand `/status` command — see CommandContext.statusReport
|
|
9
9
|
* in src/commands/registry.ts.
|
|
10
10
|
*/
|
|
11
|
-
export function StatusLine({ killed, killReason, dryRunMode, planMode, acceptEdits, autoApprovedCount, provider, model, workflow, branch, budgetPct, budgetStopped, totalCost, sandboxActive, persistenceWarningCount, privacyMode, }) {
|
|
12
|
-
return (_jsxs(Text, { dimColor: true, children: [killed ? (_jsxs(Text, { bold: true, color: "red", children: ["\u26D4 KILLED", killReason ? ` (${killReason})` : "", " \u00B7", " "] })) : (""), _jsxs(Text, { color: dryRunMode || planMode ? "cyan" : acceptEdits ? "green" : undefined, children: ["mode:", " ",
|
|
13
|
-
?
|
|
14
|
-
:
|
|
15
|
-
? "
|
|
16
|
-
:
|
|
17
|
-
?
|
|
18
|
-
:
|
|
11
|
+
export function StatusLine({ killed, killReason, dryRunMode, planMode, acceptEdits, bypassMode, autoApprovedCount, provider, model, workflow, branch, budgetPct, budgetStopped, totalCost, sandboxActive, persistenceWarningCount, privacyMode, }) {
|
|
12
|
+
return (_jsxs(Text, { dimColor: true, children: [killed ? (_jsxs(Text, { bold: true, color: "red", children: ["\u26D4 KILLED", killReason ? ` (${killReason})` : "", " \u00B7", " "] })) : (""), _jsxs(Text, { color: bypassMode ? "red" : dryRunMode || planMode ? "cyan" : acceptEdits ? "green" : undefined, children: ["mode:", " ", bypassMode
|
|
13
|
+
? `auto (${autoApprovedCount} auto)`
|
|
14
|
+
: dryRunMode
|
|
15
|
+
? "dry-run"
|
|
16
|
+
: planMode
|
|
17
|
+
? "plan"
|
|
18
|
+
: acceptEdits
|
|
19
|
+
? `accept-edits (${autoApprovedCount} auto)`
|
|
20
|
+
: "default", " · "] }), displayModelId(provider, model), _jsxs(Text, { color: sandboxActive ? "green" : "red", children: [" ", "\u00B7 ", sandboxActive ? "🔒 sandbox:active" : "🔓 sandbox:inactive"] }), workflow ? (_jsxs(Text, { color: "magenta", children: [" ", "\u00B7 \u2691 ", workflow.name, ":", workflow.phase] })) : (""), branch ? ` · ⎇ ${branch}` : "", budgetStopped || budgetPct >= 80 ? (_jsxs(Text, { color: budgetStopped ? "red" : "yellow", children: [" \u00B7 budget ", budgetPct, "%"] })) : (""), totalCost > 0 ? ` · $${totalCost.toFixed(4)}` : "", persistenceWarningCount > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 \u26A0 persistence warnings: ", persistenceWarningCount] })) : (""), privacyMode ? _jsx(Text, { color: "cyan", children: " \u00B7 privacy:on" }) : "", " · /diff review · /undo revert · Ctrl+K stop · /status for details"] }));
|
|
19
21
|
}
|
|
@@ -9,5 +9,5 @@ export function TranscriptItem({ item, verbose, stdout }) {
|
|
|
9
9
|
return (_jsxs(Box, { marginBottom: item.kind === "tool" ? 0 : 1, flexDirection: "column", children: [item.kind === "user" && (_jsxs(Text, { children: [_jsxs(Text, { bold: true, color: "green", children: ["\u276F", " "] }), item.text] })), item.kind === "assistant" && _jsx(Markdown, { text: item.text }), item.kind === "tool" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: [item.error ? _jsx(Text, { color: "red", children: "\u2717" }) : _jsx(Text, { color: "green", children: "\u2713" }), " ", item.summary, item.resultSummary && !verbose ? ` — ${item.resultSummary}` : ""] }), item.output && item.output.trim() && (item.resultSummary === undefined || verbose) && (_jsx(Text, { dimColor: true, children: toolOutputPreview(item.output, verbose, Math.max(20, terminalColumns(stdout) - 6), item.error)
|
|
10
10
|
.split("\n")
|
|
11
11
|
.map((l) => ` ${l}`)
|
|
12
|
-
.join("\n") }))] })), item.kind === "info" && _jsx(Text, { dimColor: true, children: item.text }), item.kind === "banner" && _jsx(Banner, { subtitle: item.subtitle, compact: item.compact })] }));
|
|
12
|
+
.join("\n") }))] })), item.kind === "info" && _jsx(Text, { dimColor: true, children: item.text }), item.kind === "banner" && _jsx(Banner, { subtitle: item.subtitle, compact: item.compact }), item.kind === "summary" && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: "\u2713 Completed" }), item.files.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Changed:" }), item.files.map((f) => (_jsxs(Text, { children: [" ", f] }, f)))] })) : (_jsx(Text, { dimColor: true, children: "No files changed." })), _jsxs(Text, { dimColor: true, children: ["Next: ", item.nextStep] })] }))] }));
|
|
13
13
|
}
|
package/dist/ui/useAgent.js
CHANGED
|
@@ -7,12 +7,27 @@ import { ProviderClient, RetryExhaustedError, friendlyProviderErrorHint, } from
|
|
|
7
7
|
import { createSwitchyardClient } from "../provider/switchyardClient.js";
|
|
8
8
|
import { SWITCHYARD_ROUTE_ID, resolveEffectiveModel } from "../provider/switchyardSidecar.js";
|
|
9
9
|
import { KillSwitchError } from "../agent/killSwitch.js";
|
|
10
|
+
import { defaultSandboxMode, sandboxAvailable } from "../shell/sandbox.js";
|
|
10
11
|
import { loadProjectState, nextPhase, PHASE_COMMAND, } from "../agent/workflow.js";
|
|
11
12
|
import { tavilySearch } from "../tools/webSearch.js";
|
|
12
13
|
import { killActiveNotice, useKillSwitch } from "./useKillSwitch.js";
|
|
13
14
|
import { useUsageBudget } from "./useUsageBudget.js";
|
|
14
15
|
import { useSessionResume } from "./useSessionResume.js";
|
|
15
16
|
import { activePersistenceWarnings, onPersistenceWarning } from "../config/debug.js";
|
|
17
|
+
/** Tool names whose successful calls mutate a file the user would want to review. */
|
|
18
|
+
const FILE_MUTATING_TOOLS = new Set([
|
|
19
|
+
"edit_file",
|
|
20
|
+
"write_file",
|
|
21
|
+
"edit_notebook",
|
|
22
|
+
"edit_spreadsheet",
|
|
23
|
+
"edit_pdf",
|
|
24
|
+
"write_document",
|
|
25
|
+
]);
|
|
26
|
+
/** Pulls the path out of a tool summary like "Edit src/foo.ts" or "Write src/foo.ts (12 bytes)". */
|
|
27
|
+
function pathFromToolSummary(summary) {
|
|
28
|
+
const match = summary.match(/^\S+\s+(\S+)/);
|
|
29
|
+
return match?.[1];
|
|
30
|
+
}
|
|
16
31
|
function initialToolStatus(name) {
|
|
17
32
|
if (name.startsWith("mcp_"))
|
|
18
33
|
return "contacting MCP server…";
|
|
@@ -102,8 +117,13 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
102
117
|
* workflow's own `planMode`. */
|
|
103
118
|
const [dryRunMode, setDryRunMode] = useState(false);
|
|
104
119
|
const [acceptEdits, setAcceptEdits] = useState(false);
|
|
120
|
+
const [bypassMode, setBypassMode] = useState(false);
|
|
105
121
|
const [autoApprovedCount, setAutoApprovedCount] = useState(0);
|
|
106
122
|
const hasConfirmedAcceptEdits = useRef(false);
|
|
123
|
+
const hasConfirmedBypassMode = useRef(false);
|
|
124
|
+
/** Whether auto/bypass mode is even reachable — requires a real sandbox
|
|
125
|
+
* backend on this platform (Windows never has one; see sandboxAvailable). */
|
|
126
|
+
const sandboxActive = (config.sandboxExec ?? defaultSandboxMode()) !== "off" && sandboxAvailable();
|
|
107
127
|
const abortRef = useRef(null);
|
|
108
128
|
/** Tool calls currently running, keyed by call id — more than one when a
|
|
109
129
|
* turn's read-only calls are dispatched in parallel. Rendered as live rows. */
|
|
@@ -192,12 +212,44 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
192
212
|
"more for normal.",
|
|
193
213
|
});
|
|
194
214
|
};
|
|
195
|
-
|
|
215
|
+
const enterBypassMode = () => {
|
|
216
|
+
hasConfirmedBypassMode.current = true;
|
|
217
|
+
agent.bypassMode = true;
|
|
218
|
+
setBypassMode(true);
|
|
219
|
+
setAutoApprovedCount(0);
|
|
220
|
+
addItem({
|
|
221
|
+
kind: "info",
|
|
222
|
+
text: "Auto mode ON — EVERY tool call auto-approves, including destructive shell commands " +
|
|
223
|
+
"(rm -rf, force-push, etc.). Relies on the sandbox, not a human, to contain damage. " +
|
|
224
|
+
"Shift+Tab again for normal.",
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
228
|
+
* Cycles normal → accept-edits → dry-run → auto (only when the sandbox is
|
|
229
|
+
* active) → normal. First entry into accept-edits or auto mode this session
|
|
230
|
+
* pauses on a confirmation instead of switching immediately. When the
|
|
231
|
+
* sandbox isn't active, auto mode is skipped and dry-run goes straight back
|
|
232
|
+
* to normal, same as before this mode existed.
|
|
233
|
+
*/
|
|
196
234
|
const cycleMode = () => {
|
|
235
|
+
if (bypassMode) {
|
|
236
|
+
agent.bypassMode = false;
|
|
237
|
+
setBypassMode(false);
|
|
238
|
+
addItem({ kind: "info", text: "Auto mode OFF — back to normal, prompts resume." });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
197
241
|
if (dryRunMode) {
|
|
198
242
|
agent.dryRunMode = false;
|
|
199
243
|
setDryRunMode(false);
|
|
200
|
-
|
|
244
|
+
if (!sandboxActive) {
|
|
245
|
+
addItem({ kind: "info", text: "Dry-run mode OFF — the agent can make changes again." });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (!hasConfirmedBypassMode.current) {
|
|
249
|
+
setPhase("confirmBypassMode");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
enterBypassMode();
|
|
201
253
|
return;
|
|
202
254
|
}
|
|
203
255
|
if (acceptEdits) {
|
|
@@ -222,6 +274,13 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
222
274
|
if (confirmed)
|
|
223
275
|
enterAcceptEdits();
|
|
224
276
|
};
|
|
277
|
+
const onBypassModeConfirm = (confirmed) => {
|
|
278
|
+
setPhase("input");
|
|
279
|
+
if (confirmed)
|
|
280
|
+
enterBypassMode();
|
|
281
|
+
else
|
|
282
|
+
addItem({ kind: "info", text: "Auto mode declined — back to normal." });
|
|
283
|
+
};
|
|
225
284
|
const setModelEverywhere = (id) => {
|
|
226
285
|
modelRef.current = id;
|
|
227
286
|
setModel(id);
|
|
@@ -387,6 +446,8 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
387
446
|
const ac = new AbortController();
|
|
388
447
|
abortRef.current = ac;
|
|
389
448
|
setInFlight([]);
|
|
449
|
+
const changedFiles = new Set();
|
|
450
|
+
let turnSucceeded = false;
|
|
390
451
|
try {
|
|
391
452
|
await agent.runTurn(text, {
|
|
392
453
|
onTextDelta: (delta) => {
|
|
@@ -408,6 +469,11 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
408
469
|
},
|
|
409
470
|
onToolEnd: (id, name, summary, preview, isError, resultSummary) => {
|
|
410
471
|
setInFlight((prev) => prev.filter((t) => t.id !== id));
|
|
472
|
+
if (!isError && FILE_MUTATING_TOOLS.has(name)) {
|
|
473
|
+
const p = pathFromToolSummary(summary);
|
|
474
|
+
if (p)
|
|
475
|
+
changedFiles.add(p);
|
|
476
|
+
}
|
|
411
477
|
if (name !== "update_tasks")
|
|
412
478
|
addItem({
|
|
413
479
|
kind: "tool",
|
|
@@ -439,6 +505,7 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
439
505
|
recordUsage(usage);
|
|
440
506
|
},
|
|
441
507
|
}, ac.signal, images);
|
|
508
|
+
turnSucceeded = true;
|
|
442
509
|
}
|
|
443
510
|
catch (err) {
|
|
444
511
|
const isAbort = (err instanceof Error && err.name === "AbortError") ||
|
|
@@ -476,6 +543,13 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
476
543
|
addItem({ kind: "info", text });
|
|
477
544
|
}
|
|
478
545
|
finally {
|
|
546
|
+
if (turnSucceeded && changedFiles.size > 0) {
|
|
547
|
+
addItem({
|
|
548
|
+
kind: "summary",
|
|
549
|
+
files: [...changedFiles],
|
|
550
|
+
nextStep: "review with /diff · revert with /undo",
|
|
551
|
+
});
|
|
552
|
+
}
|
|
479
553
|
abortRef.current = null;
|
|
480
554
|
setInFlight([]);
|
|
481
555
|
setStream("");
|
|
@@ -554,9 +628,13 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
554
628
|
releaseKill,
|
|
555
629
|
acceptEdits,
|
|
556
630
|
setAcceptEdits,
|
|
631
|
+
bypassMode,
|
|
632
|
+
setBypassMode,
|
|
633
|
+
sandboxActive,
|
|
557
634
|
autoApprovedCount,
|
|
558
635
|
cycleMode,
|
|
559
636
|
onAcceptEditsConfirm,
|
|
637
|
+
onBypassModeConfirm,
|
|
560
638
|
abortRef,
|
|
561
639
|
setModelEverywhere,
|
|
562
640
|
setProviderEverywhere,
|
package/package.json
CHANGED