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.
@@ -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 (autoApproveEdit) {
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: "You are interacting with an AI system: kritya reads/writes files and runs shell commands here, acting on an AI model's output \u2014 not a human." }) }), _jsx(Text, { children: "Mutating actions ask for approval first." }), _jsx(Text, { 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" }) })] }));
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
  }
@@ -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:", " ", dryRunMode
13
- ? "dry-run"
14
- : planMode
15
- ? "plan"
16
- : acceptEdits
17
- ? `accept-edits (${autoApprovedCount} auto)`
18
- : "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" }) : "", " · /status for details"] }));
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
  }
@@ -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
- /** Cycles normal → accept-edits → dry-run → normal. First entry into accept-edits this session pauses on a confirmation instead of switching immediately. */
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
- addItem({ kind: "info", text: "Dry-run mode OFF — the agent can make changes again." });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kritya",
3
- "version": "0.8.25-beta",
3
+ "version": "0.8.26-beta",
4
4
  "description": "Kritya — a lean, provider-agnostic terminal coding agent (NVIDIA, OpenAI, OpenRouter, Groq, Ollama, and more)",
5
5
  "type": "module",
6
6
  "publishConfig": {