zidane 5.6.0 → 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 (45) hide show
  1. package/dist/{agent-B26FuGew.d.ts → agent-Dtnvs5ee.d.ts} +2 -1
  2. package/dist/agent-Dtnvs5ee.d.ts.map +1 -0
  3. package/dist/chat.d.ts +112 -11
  4. package/dist/chat.d.ts.map +1 -1
  5. package/dist/chat.js +3 -3
  6. package/dist/{index-CE7z_11T.d.ts → index-DHeHe04L.d.ts} +2 -2
  7. package/dist/{index-CE7z_11T.d.ts.map → index-DHeHe04L.d.ts.map} +1 -1
  8. package/dist/{index-CROWxXo9.d.ts → index-DX8De0nl.d.ts} +2 -2
  9. package/dist/index-DX8De0nl.d.ts.map +1 -0
  10. package/dist/index.d.ts +3 -3
  11. package/dist/index.js +3 -3
  12. package/dist/{login-D5lQWoFx.js → login-BOj03nVe.js} +4 -3
  13. package/dist/login-BOj03nVe.js.map +1 -0
  14. package/dist/mcp.d.ts +1 -1
  15. package/dist/{presets-BDCthpyD.js → presets-CTSij3yV.js} +2 -2
  16. package/dist/{presets-BDCthpyD.js.map → presets-CTSij3yV.js.map} +1 -1
  17. package/dist/presets.d.ts +2 -2
  18. package/dist/presets.js +1 -1
  19. package/dist/providers.d.ts +1 -1
  20. package/dist/restate.d.ts +1 -1
  21. package/dist/session/sqlite.d.ts +1 -1
  22. package/dist/session.d.ts +1 -1
  23. package/dist/skills.d.ts +2 -2
  24. package/dist/{tools-Co3VYhgM.js → tools-CslsHpKb.js} +3 -2
  25. package/dist/tools-CslsHpKb.js.map +1 -0
  26. package/dist/tools.d.ts +2 -2
  27. package/dist/tools.js +1 -1
  28. package/dist/{transcript-anchors-CTTeQJzy.d.ts → transcript-anchors-CwoKNW6Y.d.ts} +74 -5
  29. package/dist/transcript-anchors-CwoKNW6Y.d.ts.map +1 -0
  30. package/dist/tui.d.ts +24 -5
  31. package/dist/tui.d.ts.map +1 -1
  32. package/dist/tui.js +851 -253
  33. package/dist/tui.js.map +1 -1
  34. package/dist/{turn-operations-fhinWY4m.js → turn-operations-B8ySajUl.js} +572 -79
  35. package/dist/turn-operations-B8ySajUl.js.map +1 -0
  36. package/dist/types-oKPBdCmL.js.map +1 -1
  37. package/dist/types.d.ts +2 -2
  38. package/docs/TUI.md +1 -0
  39. package/package.json +2 -1
  40. package/dist/agent-B26FuGew.d.ts.map +0 -1
  41. package/dist/index-CROWxXo9.d.ts.map +0 -1
  42. package/dist/login-D5lQWoFx.js.map +0 -1
  43. package/dist/tools-Co3VYhgM.js.map +0 -1
  44. package/dist/transcript-anchors-CTTeQJzy.d.ts.map +0 -1
  45. package/dist/turn-operations-fhinWY4m.js.map +0 -1
package/dist/tui.js CHANGED
@@ -1,30 +1,37 @@
1
- import { $n as blendHsl, $t as lastContextSizeFromTurns, A as getSafelist, An as summarizeOutcomes, Ar as getContextWindow, B as oauthUsesManualCodePaste, Bt as DiscoveryProvider, Cn as buildEditOutcomesAnnotation, Ct as useEnabledToggleSet, D as useSafeModeQueue, Dn as resolveApprovalForPayload, Dt as SettingsProvider, E as useSafeModeActions, En as parseEditOutcomesFromResult, Et as SETTINGS_TOGGLES, F as suggestSafelistEntry, Fn as ensureKeybindingsFile, G as indexOfEntry, Gt as resolveConfig, H as supportsOAuth, Ht as useDiscoveryOptional, Ir as piIdOf, J as discoverProjectMcps, Jt as deriveSessionTitle, K as buildMcpServers, Kn as createFilesCompletionProvider, Kt as EDIT_TOOL_NAMES, L as splitPromptSegments, Ln as matchesBinding, Mr as modelSupportsReasoning, Mt as resolveChipColor, Nt as resolveTheme, On as rewriteMultiEditHeader, Ot as clampFps, Qn as useCompletion, Qt as isVisible, R as formatPathForCwd, Sr as setProviderCredential, St as listProjectFiles, T as SafeModeProvider, Tn as mergeApprovalAndBodyOutcomes, Tt as SETTINGS_CHOICES, U as buildModelCatalog, Un as createSkillsCompletionProvider, Ur as accentColor, Ut as ConfigProvider, V as runOAuthLogin, Vt as useDiscovery, W as filterModelCatalog, Wn as uniqueSkillNamesFromReferences, Wt as useConfig, Xt as isEditErrorResult, Yr as TODO_STATUS_GLYPHS, Yt as eventsFromTurns, Z as createFileMcpCredentialStore, Zt as isTurnHighlighted, _ as turnContextSize, _i as buildPlanSystem, _n as extractEditPayload, _t as clipHintsToWidth, a as computeTurnAnchors, ai as useActiveTodos, an as stripSpawnTokensLine, ar as useUpdateCheck, at as splitMarkdownCodeBlocks, b as defaultSkillScanPaths, c as formatToolCall, cn as toolCallPreview, d as useSelectStyle, dn as updateToolEventOutcomes, en as listSessionMeta, er as buildLinearRamp, et as McpAuthProvider, f as useSurfaces, ft as makeRequestInteraction, g as finalizeStreamingMarkdownForOwner, gi as buildBuildSystem, gr as detectAuth, gt as useInteractionsQueue, h as finalizeStreamingMarkdown, hr as shouldAutoCompact, ht as useInteractionsActions, i as turnAsText, in as selectableTurnIds, ir as buildUpdateHint, j as isOnSafelist, jn as findGitRoot, k as addToSafelist, kn as stripEditOutcomesAnnotation, kt as useSettings, l as ThemeProvider, ln as toolResultText, lt as buildResumedToolResultsTurn, m as useTheme, mn as buildUnifiedDiff, mr as AUTO_COMPACT_MIN_GROWTH_FRACTION, n as deleteTurnSafely, nn as marginTopFor, nt as useMcpAuthState, o as TOOL_DISPLAY, on as sumRunCosts, pn as buildContextualDiff, pt as pendingInteractionsFromTurns, r as truncateTurnsAt, rr as bootTick, rt as getMcpAuthStatus, s as displayNameFor, st as InteractionsProvider, tr as tryOpenBrowser, tt as useMcpAuthDispatch, u as useColors, un as turnSelectionOwnership, ut as createInteractionTools, v as useStreamBuffer, vn as filetypeFromPath, vt as hintsLength, w as writeSessionExport, wt as DEFAULT_SETTINGS, x as discoverProjectSkills, xn as summarizeEditPayload, xt as generateSessionTitle, y as buildSkillsConfig, yn as previewEditPayload, yt as truncateTrailing, z as fetchOAuthRedirect, zt as createDiscoverySlot } from "./turn-operations-fhinWY4m.js";
2
- import { A as resolvePersistDir, B as formatTaskStatus, H as previewLine, I as ageString, L as compactPath, O as cleanupPersistedSession, R as fmtTokens, U as shortId, V as formatTaskSummary, j as resolveTasksDir, p as createAgent, z as formatDuration } from "./tools-Co3VYhgM.js";
1
+ import { $r as TODO_STATUS_GLYPHS, $t as isEditErrorResult, A as getSafelist, An as resolveApprovalForPayload, At as SettingsProvider, B as oauthUsesManualCodePaste, Bn as keybindingsPath, Br as piIdOf, Ct as buildHints, D as useSafeModeQueue, Dt as DEFAULT_SETTINGS, E as useSafeModeActions, En as buildEditOutcomesAnnotation, Er as setProviderCredential, Et as useEnabledToggleSet, F as suggestSafelistEntry, Ft as resolveChipColor, G as indexOfEntry, Gt as useDiscoveryOptional, H as supportsOAuth, Ht as createDiscoverySlot, In as KEYBINDING_DEFS, Ir as modelSupportsReasoning, It as resolveTheme, J as discoverProjectMcps, Jn as uniqueSkillNamesFromReferences, Jt as resolveConfig, K as buildMcpServers, Kt as ConfigProvider, L as splitPromptSegments, Mn as stripEditOutcomesAnnotation, Mt as useSettings, Nn as summarizeOutcomes, On as mergeApprovalAndBodyOutcomes, Ot as SETTINGS_CHOICES, Pn as findGitRoot, Pr as getContextWindow, Qt as eventsFromTurns, R as formatPathForCwd, Rn as ensureKeybindingsFile, Si as envSection, Sn as previewEditPayload, St as generateSessionTitle, T as SafeModeProvider, Tt as listProjectFiles, U as buildModelCatalog, Ut as DiscoveryProvider, V as runOAuthLogin, Vn as matchesBinding, W as filterModelCatalog, Wt as useDiscovery, Xn as createFilesCompletionProvider, Yt as EDIT_TOOL_NAMES, Z as createFileMcpCredentialStore, Zt as deriveSessionTitle, _ as turnContextSize, _n as buildUnifiedDiff, _t as EMPTY_HINTS, a as computeTurnAnchors, an as marginTopFor, ar as tryOpenBrowser, at as splitMarkdownCodeBlocks, b as defaultSkillScanPaths, bi as buildBuildSystem, bn as extractEditPayload, br as detectAuth, bt as truncateTrailing, c as formatToolCall, cn as stripSpawnTokensLine, cr as buildUpdateHint, d as useSelectStyle, dn as toolCallPreview, en as isTurnHighlighted, et as McpAuthProvider, f as useSurfaces, fn as toolResultText, ft as makeRequestInteraction, g as finalizeStreamingMarkdownForOwner, gn as buildContextualDiff, gt as useInteractionsQueue, h as finalizeStreamingMarkdown, ht as useInteractionsActions, i as turnAsText, ir as buildLinearRamp, j as isOnSafelist, jn as rewriteMultiEditHeader, jt as clampFps, k as addToSafelist, kn as parseEditOutcomesFromResult, kt as SETTINGS_TOGGLES, l as ThemeProvider, li as useActiveTodos, ln as sumRunCosts, lr as useUpdateCheck, lt as buildResumedToolResultsTurn, m as useTheme, mn as updateToolEventOutcomes, n as deleteTurnSafely, nn as lastContextSizeFromTurns, nr as useCompletion, nt as useMcpAuthState, o as TOOL_DISPLAY, pn as turnSelectionOwnership, pt as pendingInteractionsFromTurns, qn as createSkillsCompletionProvider, qr as accentColor, qt as useConfig, r as truncateTurnsAt, rn as listSessionMeta, rr as blendHsl, rt as getMcpAuthStatus, s as displayNameFor, sn as selectableTurnIds, sr as bootTick, st as InteractionsProvider, tn as isVisible, tt as useMcpAuthDispatch, u as useColors, ut as createInteractionTools, v as useStreamBuffer, vr as AUTO_COMPACT_MIN_GROWTH_FRACTION, vt as clipHintsToWidth, w as writeSessionExport, wn as summarizeEditPayload, x as discoverProjectSkills, xi as buildPlanSystem, xn as filetypeFromPath, y as buildSkillsConfig, yr as shouldAutoCompact, yt as hintsLength, z as fetchOAuthRedirect, zn as formatBindingForDisplay } from "./turn-operations-B8ySajUl.js";
2
+ import { A as resolvePersistDir, B as formatTaskStatus, H as previewLine, I as ageString, L as compactPath, O as cleanupPersistedSession, R as fmtTokens, U as shortId, V as formatTaskSummary, j as resolveTasksDir, p as createAgent, z as formatDuration } from "./tools-CslsHpKb.js";
3
+ import { n as createProcessContext } from "./contexts-BOtMvzli.js";
3
4
  import { c as errorMessage } from "./errors-DdZXnyXE.js";
4
5
  import { s as McpOAuthProvider, t as connectMcpServers } from "./mcp-ngMS0S6N.js";
5
- import { C as summaryToTurn, a as selectFilesFromSession, r as buildPostCompactAttachments, s as compactConversation, t as loginMcpServer } from "./login-D5lQWoFx.js";
6
+ import { C as summaryToTurn, a as selectFilesFromSession, r as buildPostCompactAttachments, s as compactConversation, t as loginMcpServer } from "./login-BOj03nVe.js";
6
7
  import { n as formatTokenUsage } from "./stats-Lc3zL3RM.js";
7
8
  import { n as loadSession, t as createSession } from "./session-BoEW_wCR.js";
8
9
  import { createTuiStore } from "./session/sqlite.js";
10
+ import { basename, join, relative } from "node:path";
9
11
  import { homedir } from "node:os";
10
12
  import { spawn } from "node:child_process";
11
13
  import * as fs from "node:fs";
14
+ import { readFileSync, readdirSync, statSync } from "node:fs";
12
15
  import { Buffer } from "node:buffer";
13
16
  import { createContext, createElement, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
14
17
  import { BoxRenderable, CodeRenderable, RGBA, SyntaxStyle, TextRenderable, addDefaultParsers, createCliRenderer, decodePasteBytes, defaultTextareaKeyBindings, getTreeSitterClient, stripAnsiSequences } from "@opentui/core";
15
18
  import { createRoot, useKeyboard, useRenderer, useSelectionHandler, useTerminalDimensions } from "@opentui/react";
16
19
  import { Fragment, jsx, jsxs } from "@opentui/react/jsx-runtime";
20
+ import { Fzf, byLengthAsc } from "fzf";
17
21
  //#region src/tui/modal.tsx
18
22
  const ModalContext = createContext(null);
19
23
  function ModalRoot({ children }) {
20
24
  const [active, setActive] = useState(null);
25
+ const [lockCount, setLockCount] = useState(0);
21
26
  const api = useMemo(() => ({
22
27
  open: (node) => setActive(node),
23
28
  close: () => setActive(null),
29
+ lock: () => setLockCount((c) => c + 1),
30
+ unlock: () => setLockCount((c) => Math.max(0, c - 1)),
24
31
  get isOpen() {
25
- return active !== null;
32
+ return active !== null || lockCount > 0;
26
33
  }
27
- }), [active]);
34
+ }), [active, lockCount]);
28
35
  return /* @__PURE__ */ jsxs(ModalContext.Provider, {
29
36
  value: api,
30
37
  children: [/* @__PURE__ */ jsx("box", {
@@ -1341,7 +1348,8 @@ function EventLineImpl({ event, depthOffset = 0, hideChildLabel = false, selecte
1341
1348
  case "separator": return /* @__PURE__ */ jsx("text", { children: " " });
1342
1349
  case "user-prompt": return /* @__PURE__ */ jsx(UserPromptBlock, {
1343
1350
  text: safeText,
1344
- refs: event.refs
1351
+ refs: event.refs,
1352
+ attachments: event.attachments
1345
1353
  });
1346
1354
  case "info": return /* @__PURE__ */ jsx("box", {
1347
1355
  style: row,
@@ -1608,7 +1616,7 @@ function CompactSummaryBlock({ event, indent }) {
1608
1616
  */
1609
1617
  /** Prompt chevron rendered ahead of every user-prompt block. */
1610
1618
  const USER_PROMPT_PREFIX = "❯ ";
1611
- function UserPromptBlock({ text, refs }) {
1619
+ function UserPromptBlock({ text, refs, attachments }) {
1612
1620
  const COLOR = useColors();
1613
1621
  const SURFACE = useSurfaces();
1614
1622
  const boxStyle = {
@@ -1617,17 +1625,54 @@ function UserPromptBlock({ text, refs }) {
1617
1625
  paddingLeft: 1,
1618
1626
  paddingRight: 1
1619
1627
  };
1620
- if (!refs || refs.length === 0) return /* @__PURE__ */ jsx("box", {
1628
+ const attachmentChips = attachments && attachments.length > 0 ? /* @__PURE__ */ jsx("box", {
1629
+ style: {
1630
+ flexDirection: "row",
1631
+ flexWrap: "wrap",
1632
+ paddingTop: text.length > 0 ? 0 : 0
1633
+ },
1634
+ children: attachments.map((att, idx) => {
1635
+ const sz = att.size;
1636
+ const label = sz < 1024 ? `${sz}B` : sz < 1024 * 1024 ? `${(sz / 1024).toFixed(1)}KB` : `${(sz / (1024 * 1024)).toFixed(1)}MB`;
1637
+ const icon = att.mediaType.startsWith("image/") ? "🖼" : "📎";
1638
+ const chipColor = resolveChipColor(SURFACE.chips, "file");
1639
+ return /* @__PURE__ */ jsxs("text", { children: [
1640
+ idx > 0 ? " " : "",
1641
+ icon,
1642
+ /* @__PURE__ */ jsxs("span", {
1643
+ fg: chipColor.fg,
1644
+ bg: chipColor.bg,
1645
+ children: [
1646
+ " ",
1647
+ att.name,
1648
+ " ",
1649
+ "(",
1650
+ label,
1651
+ ")",
1652
+ " "
1653
+ ]
1654
+ })
1655
+ ] }, `att-${idx}`);
1656
+ })
1657
+ }) : null;
1658
+ if (!refs || refs.length === 0) return /* @__PURE__ */ jsxs("box", {
1621
1659
  style: boxStyle,
1622
- children: /* @__PURE__ */ jsxs("text", { children: [/* @__PURE__ */ jsx("span", {
1623
- fg: COLOR.brand,
1624
- children: USER_PROMPT_PREFIX
1625
- }), text] })
1660
+ children: [
1661
+ text.length > 0 && /* @__PURE__ */ jsxs("text", { children: [/* @__PURE__ */ jsx("span", {
1662
+ fg: COLOR.brand,
1663
+ children: USER_PROMPT_PREFIX
1664
+ }), text] }),
1665
+ !text.length && attachmentChips && /* @__PURE__ */ jsx("text", {
1666
+ fg: COLOR.brand,
1667
+ children: USER_PROMPT_PREFIX
1668
+ }),
1669
+ attachmentChips
1670
+ ]
1626
1671
  });
1627
1672
  const segments = splitPromptSegments(text, refs);
1628
- return /* @__PURE__ */ jsx("box", {
1673
+ return /* @__PURE__ */ jsxs("box", {
1629
1674
  style: boxStyle,
1630
- children: /* @__PURE__ */ jsxs("box", {
1675
+ children: [/* @__PURE__ */ jsxs("box", {
1631
1676
  style: {
1632
1677
  flexDirection: "row",
1633
1678
  flexWrap: "wrap"
@@ -1644,7 +1689,7 @@ function UserPromptBlock({ text, refs }) {
1644
1689
  children: seg.text
1645
1690
  }, i);
1646
1691
  })]
1647
- })
1692
+ }), attachmentChips]
1648
1693
  });
1649
1694
  }
1650
1695
  /** Click-feedback delay before `[copied]` reverts to `[copy]`, in ms. */
@@ -2444,6 +2489,316 @@ function TodoInProgressList({ input, dim }) {
2444
2489
  });
2445
2490
  }
2446
2491
  //#endregion
2492
+ //#region src/tui/cwd-picker.tsx
2493
+ const VISIBLE_ROWS$1 = 12;
2494
+ const HOME = homedir();
2495
+ const MAX_ENTRIES = 5e4;
2496
+ const MAX_DEPTH = 5;
2497
+ const SKIP_DIRS = new Set([
2498
+ "node_modules",
2499
+ ".git",
2500
+ ".hg",
2501
+ ".svn",
2502
+ "__pycache__",
2503
+ ".cache",
2504
+ ".npm",
2505
+ ".yarn",
2506
+ "dist",
2507
+ "build",
2508
+ ".next",
2509
+ ".nuxt",
2510
+ "coverage",
2511
+ ".venv",
2512
+ "venv",
2513
+ ".tox",
2514
+ "vendor",
2515
+ "target",
2516
+ ".gradle",
2517
+ ".idea",
2518
+ ".vscode"
2519
+ ]);
2520
+ function walkDirs(base) {
2521
+ const results = [];
2522
+ const queue = [{
2523
+ dir: base,
2524
+ depth: 0
2525
+ }];
2526
+ while (queue.length > 0 && results.length < MAX_ENTRIES) {
2527
+ const { dir, depth } = queue.shift();
2528
+ let entries;
2529
+ try {
2530
+ entries = readdirSync(dir, { withFileTypes: true });
2531
+ } catch {
2532
+ continue;
2533
+ }
2534
+ for (const e of entries) {
2535
+ if (results.length >= MAX_ENTRIES) break;
2536
+ if (e.name.startsWith(".") || SKIP_DIRS.has(e.name)) continue;
2537
+ try {
2538
+ const full = join(dir, e.name);
2539
+ if (e.isDirectory() || e.isSymbolicLink() && statSync(full).isDirectory()) {
2540
+ results.push({
2541
+ rel: relative(base, full),
2542
+ path: full
2543
+ });
2544
+ if (depth + 1 < MAX_DEPTH) queue.push({
2545
+ dir: full,
2546
+ depth: depth + 1
2547
+ });
2548
+ }
2549
+ } catch {}
2550
+ }
2551
+ }
2552
+ return results;
2553
+ }
2554
+ function compactHome(p) {
2555
+ return p === HOME ? "~" : p.startsWith(`${HOME}/`) ? `~${p.slice(HOME.length)}` : p;
2556
+ }
2557
+ function highlightName(name, positions, hlColor, dimColor) {
2558
+ const spans = [];
2559
+ let run = "";
2560
+ let runHL = false;
2561
+ for (let i = 0; i < name.length; i++) {
2562
+ const hl = positions.has(i);
2563
+ if (hl !== runHL && run) {
2564
+ spans.push({
2565
+ text: run,
2566
+ color: runHL ? hlColor : dimColor
2567
+ });
2568
+ run = "";
2569
+ }
2570
+ run += name[i];
2571
+ runHL = hl;
2572
+ }
2573
+ if (run) spans.push({
2574
+ text: run,
2575
+ color: runHL ? hlColor : dimColor
2576
+ });
2577
+ return spans;
2578
+ }
2579
+ function CwdPickerModal({ currentCwd, onPick }) {
2580
+ const COLOR = useColors();
2581
+ const SURFACE = useSurfaces();
2582
+ const inputRef = useRef(null);
2583
+ const [query, setQuery] = useState("");
2584
+ const [browsePath, setBrowsePath] = useState(HOME);
2585
+ const [selectedIdx, setSelectedIdx] = useState(0);
2586
+ useEffect(() => {
2587
+ inputRef.current?.focus();
2588
+ }, []);
2589
+ const dirs = useMemo(() => walkDirs(browsePath), [browsePath]);
2590
+ const fzf = useMemo(() => new Fzf(dirs, {
2591
+ selector: (d) => d.rel,
2592
+ tiebreakers: [byLengthAsc],
2593
+ limit: 200
2594
+ }), [dirs]);
2595
+ const results = useMemo(() => {
2596
+ if (!query) return dirs.slice(0, 200).map((item) => ({
2597
+ item,
2598
+ start: 0,
2599
+ end: 0,
2600
+ score: 0,
2601
+ positions: /* @__PURE__ */ new Set()
2602
+ }));
2603
+ return fzf.find(query);
2604
+ }, [
2605
+ fzf,
2606
+ dirs,
2607
+ query
2608
+ ]);
2609
+ const safeIndex = results.length === 0 ? 0 : Math.min(selectedIdx, results.length - 1);
2610
+ const handleQueryChange = useCallback((next) => {
2611
+ setQuery(next);
2612
+ setSelectedIdx(0);
2613
+ }, []);
2614
+ const commit = () => {
2615
+ const row = results[safeIndex];
2616
+ if (row) onPick(row.item.path);
2617
+ };
2618
+ const drillDown = () => {
2619
+ const row = results[safeIndex];
2620
+ if (row) {
2621
+ setBrowsePath(row.item.path);
2622
+ setQuery("");
2623
+ setSelectedIdx(0);
2624
+ }
2625
+ };
2626
+ const goUp = () => {
2627
+ const parent = join(browsePath, "..");
2628
+ if (parent !== browsePath) {
2629
+ setBrowsePath(parent);
2630
+ setQuery("");
2631
+ setSelectedIdx(0);
2632
+ }
2633
+ };
2634
+ const viewport = useMemo(() => {
2635
+ if (results.length <= VISIBLE_ROWS$1) return {
2636
+ start: 0,
2637
+ slice: results
2638
+ };
2639
+ const half = Math.floor(VISIBLE_ROWS$1 / 2);
2640
+ let start = Math.max(0, safeIndex - half);
2641
+ if (start + VISIBLE_ROWS$1 > results.length) start = results.length - VISIBLE_ROWS$1;
2642
+ return {
2643
+ start,
2644
+ slice: results.slice(start, start + VISIBLE_ROWS$1)
2645
+ };
2646
+ }, [results, safeIndex]);
2647
+ useKeyboard((key) => {
2648
+ if (key.name === "up") {
2649
+ setSelectedIdx((i) => results.length === 0 ? i : ((i - 1) % results.length + results.length) % results.length);
2650
+ return;
2651
+ }
2652
+ if (key.name === "down") {
2653
+ setSelectedIdx((i) => results.length === 0 ? i : (i + 1) % results.length);
2654
+ return;
2655
+ }
2656
+ if (key.name === "tab") {
2657
+ drillDown();
2658
+ return;
2659
+ }
2660
+ if (key.name === "left" && !query) {
2661
+ goUp();
2662
+ return;
2663
+ }
2664
+ if (key.name === "right" && !query) {
2665
+ drillDown();
2666
+ return;
2667
+ }
2668
+ if (key.name === "return") commit();
2669
+ });
2670
+ return /* @__PURE__ */ jsxs(Modal, {
2671
+ title: "change directory",
2672
+ maxWidth: 80,
2673
+ children: [
2674
+ /* @__PURE__ */ jsxs("text", {
2675
+ fg: COLOR.dim,
2676
+ children: [
2677
+ /* @__PURE__ */ jsx("span", {
2678
+ fg: COLOR.mute,
2679
+ children: "browsing "
2680
+ }),
2681
+ /* @__PURE__ */ jsx("span", {
2682
+ fg: COLOR.brand,
2683
+ children: compactHome(browsePath)
2684
+ }),
2685
+ /* @__PURE__ */ jsx("span", {
2686
+ fg: COLOR.mute,
2687
+ children: ` · ${dirs.length} dirs`
2688
+ })
2689
+ ]
2690
+ }),
2691
+ /* @__PURE__ */ jsx("box", {
2692
+ style: {
2693
+ border: true,
2694
+ borderColor: COLOR.borderActive,
2695
+ paddingLeft: 1,
2696
+ paddingRight: 1,
2697
+ height: 3
2698
+ },
2699
+ children: /* @__PURE__ */ jsx("input", {
2700
+ ref: inputRef,
2701
+ focused: true,
2702
+ placeholder: "fuzzy search directories…",
2703
+ onInput: handleQueryChange,
2704
+ onSubmit: () => {},
2705
+ style: { flexGrow: 1 }
2706
+ })
2707
+ }),
2708
+ /* @__PURE__ */ jsx("box", {
2709
+ style: {
2710
+ flexDirection: "column",
2711
+ height: VISIBLE_ROWS$1,
2712
+ flexShrink: 0
2713
+ },
2714
+ children: results.length === 0 ? /* @__PURE__ */ jsxs("text", {
2715
+ fg: COLOR.dim,
2716
+ children: [/* @__PURE__ */ jsx("span", {
2717
+ fg: COLOR.mute,
2718
+ children: "no directories match "
2719
+ }), /* @__PURE__ */ jsx("span", {
2720
+ fg: COLOR.warn,
2721
+ children: query.trim()
2722
+ })]
2723
+ }) : viewport.slice.map((result, i) => {
2724
+ const focused = viewport.start + i === safeIndex;
2725
+ const current = result.item.path === currentCwd;
2726
+ const marker = current ? "●" : " ";
2727
+ const nameColor = focused ? COLOR.brand : COLOR.dim;
2728
+ const spans = query ? highlightName(result.item.rel, result.positions, COLOR.warn, nameColor) : [{
2729
+ text: result.item.rel,
2730
+ color: nameColor
2731
+ }];
2732
+ return /* @__PURE__ */ jsx("box", {
2733
+ style: {
2734
+ height: 1,
2735
+ paddingLeft: 1,
2736
+ paddingRight: 1,
2737
+ flexShrink: 0,
2738
+ backgroundColor: focused ? SURFACE.selection : void 0
2739
+ },
2740
+ children: /* @__PURE__ */ jsxs("text", {
2741
+ wrapMode: "none",
2742
+ children: [
2743
+ /* @__PURE__ */ jsx("span", {
2744
+ fg: current ? COLOR.brand : COLOR.mute,
2745
+ children: marker
2746
+ }),
2747
+ /* @__PURE__ */ jsx("span", {
2748
+ fg: COLOR.mute,
2749
+ children: " "
2750
+ }),
2751
+ spans.map((s, si) => /* @__PURE__ */ jsx("span", {
2752
+ fg: s.color,
2753
+ children: s.text
2754
+ }, si)),
2755
+ /* @__PURE__ */ jsx("span", {
2756
+ fg: COLOR.mute,
2757
+ children: "/"
2758
+ })
2759
+ ]
2760
+ })
2761
+ }, result.item.path);
2762
+ })
2763
+ }),
2764
+ /* @__PURE__ */ jsxs("text", {
2765
+ fg: COLOR.dim,
2766
+ children: [
2767
+ /* @__PURE__ */ jsx("span", {
2768
+ fg: COLOR.warn,
2769
+ children: "↑↓"
2770
+ }),
2771
+ " navigate · ",
2772
+ /* @__PURE__ */ jsx("span", {
2773
+ fg: COLOR.warn,
2774
+ children: "tab"
2775
+ }),
2776
+ " enter dir · ",
2777
+ /* @__PURE__ */ jsx("span", {
2778
+ fg: COLOR.warn,
2779
+ children: "←"
2780
+ }),
2781
+ " parent · ",
2782
+ /* @__PURE__ */ jsx("span", {
2783
+ fg: COLOR.warn,
2784
+ children: "↵"
2785
+ }),
2786
+ " select · ",
2787
+ /* @__PURE__ */ jsx("span", {
2788
+ fg: COLOR.warn,
2789
+ children: "esc"
2790
+ }),
2791
+ " close · ",
2792
+ /* @__PURE__ */ jsx("span", {
2793
+ fg: COLOR.mute,
2794
+ children: `${results.length} match${results.length === 1 ? "" : "es"}`
2795
+ })
2796
+ ]
2797
+ })
2798
+ ]
2799
+ });
2800
+ }
2801
+ //#endregion
2447
2802
  //#region src/tui/discovery-shell.tsx
2448
2803
  /**
2449
2804
  * SWR throttles. `files` is short so a long-open `@` popover picks up
@@ -2786,6 +3141,220 @@ function EffortRow({ level, isCurrent, isFocused, highlightBg }) {
2786
3141
  });
2787
3142
  }
2788
3143
  //#endregion
3144
+ //#region src/tui/keybindings-modal.tsx
3145
+ /**
3146
+ * Fixed column width for the rendered key — derived once from
3147
+ * {@link KEYBINDING_DEFS} so adding an action with a wider default spec
3148
+ * (`ctrl+shift+x`, etc.) automatically grows the column instead of
3149
+ * truncating the label. Falls back to a sensible minimum so empty /
3150
+ * one-glyph defaults don't collapse the layout.
3151
+ */
3152
+ const KEY_COL_WIDTH = (() => {
3153
+ let max = 8;
3154
+ for (const def of KEYBINDING_DEFS) {
3155
+ const width = formatBindingForDisplay(def.default).length;
3156
+ if (width > max) max = width;
3157
+ }
3158
+ return max + 2;
3159
+ })();
3160
+ function KeybindingsModal({ bindings, filePath, onEditFile, onClose }) {
3161
+ const COLOR = useColors();
3162
+ const SURFACE = useSurfaces();
3163
+ const { height: termHeight } = useTerminalDimensions();
3164
+ const scrollRef = useRef(null);
3165
+ const sections = useMemo(() => groupBindings(bindings), [bindings]);
3166
+ useKeyboard((key) => {
3167
+ if (key.name === "return") onEditFile();
3168
+ });
3169
+ const idealHeight = Math.floor((termHeight - 4) * .7);
3170
+ const maxHeight = Math.max(18, Math.min(40, idealHeight));
3171
+ const totalCount = KEYBINDING_DEFS.length;
3172
+ return /* @__PURE__ */ jsxs(Modal, {
3173
+ title: "keybindings",
3174
+ bottomTitle: "↵ edit file · esc close",
3175
+ rightTitle: /* @__PURE__ */ jsx(CountsBadge$1, { count: totalCount }),
3176
+ maxWidth: 104,
3177
+ minWidth: 64,
3178
+ maxHeight,
3179
+ onClose,
3180
+ children: [/* @__PURE__ */ jsx("box", {
3181
+ style: {
3182
+ flexDirection: "column",
3183
+ flexGrow: 1,
3184
+ flexShrink: 1,
3185
+ overflow: "hidden"
3186
+ },
3187
+ children: /* @__PURE__ */ jsx("scrollbox", {
3188
+ ref: scrollRef,
3189
+ focusable: false,
3190
+ stickyScroll: false,
3191
+ style: {
3192
+ flexGrow: 1,
3193
+ flexShrink: 1
3194
+ },
3195
+ children: sections.map((section, sectionIdx) => /* @__PURE__ */ jsxs("box", {
3196
+ style: {
3197
+ flexDirection: "column",
3198
+ flexShrink: 0,
3199
+ marginTop: sectionIdx === 0 ? 0 : 1
3200
+ },
3201
+ children: [/* @__PURE__ */ jsx(SectionHeader, { label: section.group }), section.rows.map((row) => /* @__PURE__ */ jsx(BindingRow, {
3202
+ def: row.def,
3203
+ spec: row.spec
3204
+ }, row.def.action))]
3205
+ }, section.group))
3206
+ })
3207
+ }), /* @__PURE__ */ jsx(EditFileButton, {
3208
+ filePath,
3209
+ highlightBg: SURFACE.selection,
3210
+ brand: COLOR.brand,
3211
+ mute: COLOR.mute,
3212
+ dim: COLOR.dim
3213
+ })]
3214
+ });
3215
+ }
3216
+ function SectionHeader({ label }) {
3217
+ return /* @__PURE__ */ jsx("box", {
3218
+ style: {
3219
+ flexShrink: 0,
3220
+ paddingLeft: 1,
3221
+ paddingRight: 1
3222
+ },
3223
+ children: /* @__PURE__ */ jsx("text", {
3224
+ wrapMode: "none",
3225
+ children: /* @__PURE__ */ jsx("span", {
3226
+ fg: useColors().brand,
3227
+ children: label
3228
+ })
3229
+ })
3230
+ });
3231
+ }
3232
+ function BindingRow({ def, spec }) {
3233
+ const COLOR = useColors();
3234
+ const display = formatBindingForDisplay(spec);
3235
+ const keyText = (display || "—").padEnd(KEY_COL_WIDTH, " ");
3236
+ return /* @__PURE__ */ jsxs("box", {
3237
+ style: {
3238
+ flexDirection: "column",
3239
+ flexShrink: 0,
3240
+ paddingLeft: 3,
3241
+ paddingRight: 1
3242
+ },
3243
+ children: [/* @__PURE__ */ jsxs("text", {
3244
+ wrapMode: "none",
3245
+ children: [/* @__PURE__ */ jsx("span", {
3246
+ fg: display ? COLOR.warn : COLOR.mute,
3247
+ children: keyText
3248
+ }), /* @__PURE__ */ jsx("span", {
3249
+ fg: COLOR.dim,
3250
+ children: def.label
3251
+ })]
3252
+ }), /* @__PURE__ */ jsx("box", {
3253
+ style: {
3254
+ flexShrink: 0,
3255
+ paddingLeft: KEY_COL_WIDTH
3256
+ },
3257
+ children: /* @__PURE__ */ jsx("text", {
3258
+ wrapMode: "word",
3259
+ fg: COLOR.mute,
3260
+ children: def.description
3261
+ })
3262
+ })]
3263
+ });
3264
+ }
3265
+ function EditFileButton({ filePath, highlightBg, brand, mute, dim }) {
3266
+ const display = filePath ? compactPath(filePath) : "~/.zidane/keybindings.json";
3267
+ return /* @__PURE__ */ jsxs("box", {
3268
+ style: {
3269
+ flexShrink: 0,
3270
+ flexDirection: "column",
3271
+ paddingLeft: 1,
3272
+ paddingRight: 1,
3273
+ backgroundColor: highlightBg
3274
+ },
3275
+ children: [/* @__PURE__ */ jsxs("text", {
3276
+ wrapMode: "none",
3277
+ children: [
3278
+ /* @__PURE__ */ jsx("span", {
3279
+ fg: brand,
3280
+ children: "▶ "
3281
+ }),
3282
+ /* @__PURE__ */ jsx("span", {
3283
+ fg: brand,
3284
+ children: "Edit keybindings file"
3285
+ }),
3286
+ /* @__PURE__ */ jsx("span", {
3287
+ fg: mute,
3288
+ children: " "
3289
+ }),
3290
+ /* @__PURE__ */ jsx("span", {
3291
+ fg: brand,
3292
+ children: "›"
3293
+ })
3294
+ ]
3295
+ }), /* @__PURE__ */ jsxs("text", {
3296
+ wrapMode: "none",
3297
+ children: [/* @__PURE__ */ jsx("span", {
3298
+ fg: mute,
3299
+ children: " "
3300
+ }), /* @__PURE__ */ jsx("span", {
3301
+ fg: dim,
3302
+ children: `${display} — restart to apply changes`
3303
+ })]
3304
+ })]
3305
+ });
3306
+ }
3307
+ function CountsBadge$1({ count }) {
3308
+ const COLOR = useColors();
3309
+ return /* @__PURE__ */ jsxs("text", {
3310
+ wrapMode: "none",
3311
+ children: [
3312
+ /* @__PURE__ */ jsx("span", {
3313
+ fg: COLOR.mute,
3314
+ children: " "
3315
+ }),
3316
+ /* @__PURE__ */ jsx("span", {
3317
+ fg: COLOR.accent,
3318
+ children: String(count)
3319
+ }),
3320
+ /* @__PURE__ */ jsx("span", {
3321
+ fg: COLOR.mute,
3322
+ children: ` action${count === 1 ? "" : "s"} `
3323
+ })
3324
+ ]
3325
+ });
3326
+ }
3327
+ /**
3328
+ * Walk `KEYBINDING_DEFS` in order and bucket rows into contiguous
3329
+ * sections by `group`. Preserves catalog order — if two actions in the
3330
+ * same group are split by an entry from another group, they'd render
3331
+ * as two separate sections with the same header (catalog order wins
3332
+ * over "merge same-group entries" so the on-screen story matches the
3333
+ * on-disk file).
3334
+ */
3335
+ function groupBindings(bindings) {
3336
+ const sections = [];
3337
+ for (const def of KEYBINDING_DEFS) {
3338
+ const last = sections[sections.length - 1];
3339
+ const spec = bindings[def.action] ?? "";
3340
+ if (last && last.group === def.group) {
3341
+ last.rows.push({
3342
+ def,
3343
+ spec
3344
+ });
3345
+ continue;
3346
+ }
3347
+ sections.push({
3348
+ group: def.group,
3349
+ rows: [{
3350
+ def,
3351
+ spec
3352
+ }]
3353
+ });
3354
+ }
3355
+ return sections;
3356
+ }
3357
+ //#endregion
2789
3358
  //#region src/tui/model-picker.tsx
2790
3359
  /**
2791
3360
  * Cross-provider, searchable model picker.
@@ -5848,7 +6417,61 @@ function renderProjectLabel(rowProject, currentProject, COLOR) {
5848
6417
  /** Visible content lines: 1 minimum, 5 maximum (textarea scrolls past 5). */
5849
6418
  const MIN_CONTENT_LINES = 1;
5850
6419
  const MAX_CONTENT_LINES = 5;
5851
- const CWD_DISPLAY = compactPath(process.cwd());
6420
+ const IMAGE_MEDIA_TYPES = {
6421
+ png: "image/png",
6422
+ jpg: "image/jpeg",
6423
+ jpeg: "image/jpeg",
6424
+ gif: "image/gif",
6425
+ webp: "image/webp",
6426
+ svg: "image/svg+xml",
6427
+ bmp: "image/bmp"
6428
+ };
6429
+ const MIME_BY_EXT = {
6430
+ txt: "text/plain",
6431
+ md: "text/markdown",
6432
+ json: "application/json",
6433
+ yaml: "text/yaml",
6434
+ yml: "text/yaml",
6435
+ toml: "text/plain",
6436
+ xml: "application/xml",
6437
+ html: "text/html",
6438
+ htm: "text/html",
6439
+ css: "text/css",
6440
+ csv: "text/csv",
6441
+ tsv: "text/tab-separated-values",
6442
+ js: "text/javascript",
6443
+ mjs: "text/javascript",
6444
+ cjs: "text/javascript",
6445
+ ts: "text/typescript",
6446
+ mts: "text/typescript",
6447
+ cts: "text/typescript",
6448
+ tsx: "text/typescript",
6449
+ jsx: "text/javascript",
6450
+ py: "text/x-python",
6451
+ rb: "text/x-ruby",
6452
+ rs: "text/x-rust",
6453
+ go: "text/x-go",
6454
+ java: "text/x-java",
6455
+ c: "text/x-c",
6456
+ h: "text/x-c",
6457
+ cpp: "text/x-c++",
6458
+ hpp: "text/x-c++",
6459
+ sh: "text/x-shellscript",
6460
+ bash: "text/x-shellscript",
6461
+ zsh: "text/x-shellscript",
6462
+ fish: "text/x-shellscript",
6463
+ sql: "text/x-sql",
6464
+ graphql: "text/x-graphql",
6465
+ pdf: "application/pdf",
6466
+ zip: "application/zip",
6467
+ tar: "application/x-tar",
6468
+ gz: "application/gzip",
6469
+ log: "text/plain",
6470
+ env: "text/plain",
6471
+ cfg: "text/plain",
6472
+ ini: "text/plain",
6473
+ conf: "text/plain"
6474
+ };
5852
6475
  /**
5853
6476
  * Stable empty-array reference — keeps `queuedMessages` default referentially
5854
6477
  * stable across renders so memoized children don't bust their deps. Declared
@@ -5856,10 +6479,10 @@ const CWD_DISPLAY = compactPath(process.cwd());
5856
6479
  * default-value reference at the prop destructure.
5857
6480
  */
5858
6481
  const EMPTY_QUEUED_MESSAGES = [];
5859
- function ChatScreen({ events, busy, compacting = false, queuedMessages = EMPTY_QUEUED_MESSAGES, queueSelectionIndex = null, queueShortcuts, onEnterQueueFromEmptyPrompt, settings, onSubmit, session, pending, onApproval, pendingInteraction, onInteraction, completionProviders, onPopupOpenChange, selectedTurnId, promptTriggerHints, liveSession = null }) {
6482
+ function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EMPTY_QUEUED_MESSAGES, queueSelectionIndex = null, queueShortcuts, onEnterQueueFromEmptyPrompt, settings, onSubmit, session, pending, onApproval, pendingInteraction, onInteraction, completionProviders, onPopupOpenChange, selectedTurnId, promptTriggerHints, liveSession = null }) {
5860
6483
  const COLOR = useColors();
5861
6484
  const titleText = session?.title ?? "untitled";
5862
- const showSessionShortcut = !!session && !busy && !pending && !pendingInteraction;
6485
+ const showSessionShortcut = !!session && !busy && !pending && !pendingInteraction && settings.uiMode !== "minimal";
5863
6486
  const userMessageCount = useMemo(() => events.filter((e) => e.kind === "user-prompt").length, [events]);
5864
6487
  const { width: termWidth } = useTerminalDimensions();
5865
6488
  const hasStatusIcon = busy || compacting;
@@ -5895,7 +6518,7 @@ function ChatScreen({ events, busy, compacting = false, queuedMessages = EMPTY_Q
5895
6518
  const statsLen = segments.reduce((sum, s) => sum + s.text.length, 0);
5896
6519
  const cwdBudget = Math.max(0, termWidth - 4 - titleText.length - iconReserve - OVERLAY_RESERVED - statsLen - 3);
5897
6520
  if (cwdBudget >= 6) segments.unshift({
5898
- text: compactPath(CWD_DISPLAY, cwdBudget),
6521
+ text: compactPath(cwd, cwdBudget),
5899
6522
  color: COLOR.dim
5900
6523
  }, {
5901
6524
  text: " · ",
@@ -5903,6 +6526,7 @@ function ChatScreen({ events, busy, compacting = false, queuedMessages = EMPTY_Q
5903
6526
  });
5904
6527
  return segments;
5905
6528
  }, [
6529
+ cwd,
5906
6530
  session,
5907
6531
  userMessageCount,
5908
6532
  COLOR,
@@ -5921,8 +6545,8 @@ function ChatScreen({ events, busy, compacting = false, queuedMessages = EMPTY_Q
5921
6545
  const modal = useModal();
5922
6546
  useEffect(() => {
5923
6547
  if (!fileEditPending) return;
5924
- modal.open(/* @__PURE__ */ jsx(Fragment, {}));
5925
- return () => modal.close();
6548
+ modal.lock();
6549
+ return () => modal.unlock();
5926
6550
  }, [fileEditPending, modal]);
5927
6551
  return /* @__PURE__ */ jsxs("box", {
5928
6552
  style: {
@@ -6281,44 +6905,16 @@ function QueuedMessagesBlock({ messages, selectionIndex, shortcuts }) {
6281
6905
  function previewText(text) {
6282
6906
  return text.replace(/\n+/g, " ↵ ");
6283
6907
  }
6284
- const KEY_GLYPHS = {
6285
- up: "↑",
6286
- down: "↓",
6287
- left: "←",
6288
- right: "→",
6289
- return: "↵",
6290
- enter: "↵",
6291
- delete: "⌫",
6292
- backspace: "⌫",
6293
- escape: "esc",
6294
- space: "␣",
6295
- tab: "⇥"
6296
- };
6297
- /**
6298
- * Render a binding spec (`"ctrl+return"`, `"backspace"`, `"delete"`) as a
6299
- * compact display string the user recognizes at a glance. Substitutes
6300
- * arrow / enter / backspace glyphs for their verbose names so the hint
6301
- * row stays narrow; modifier names + plain keys pass through unchanged.
6302
- *
6303
- * Kept pure / local: every consumer in this file would otherwise embed
6304
- * the same `replace` chain, and a future `keybindings.json` override
6305
- * (e.g. `"backspace"` for drop) renders correctly without code edits.
6306
- */
6307
- function formatBindingForDisplay(spec) {
6308
- const segments = spec.toLowerCase().split("+");
6309
- const key = segments.pop() ?? "";
6310
- const modifiers = segments.join("+");
6311
- const glyph = KEY_GLYPHS[key] ?? key;
6312
- return modifiers ? `${modifiers}+${glyph}` : glyph;
6313
- }
6314
6908
  /** Stable empty providers reference — avoids `useCompletion` rerun on every render when no providers are wired. */
6315
6909
  const EMPTY_PROVIDERS = [];
6316
6910
  function PromptBlock({ userPrompts, onSubmit, completionProviders, onPopupOpenChange, selectMode = null, triggerHints, busy = false, onEnterQueueFromEmpty }) {
6317
6911
  const focused = useModalAwareFocus();
6318
6912
  const COLOR = useColors();
6913
+ const SURFACE = useSurfaces();
6319
6914
  const textareaRef = useRef(null);
6320
6915
  /** Auto-grow: visible content rows the textarea currently occupies (clamped 1..5). */
6321
6916
  const [contentLines, setContentLines] = useState(MIN_CONTENT_LINES);
6917
+ const [attachments, setAttachments] = useState([]);
6322
6918
  /**
6323
6919
  * Mirror of the textarea buffer + cursor, updated on every `onContentChange`.
6324
6920
  * Drives `useCompletion` — the textarea stays uncontrolled (we don't push
@@ -6358,8 +6954,8 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, onPopupOpenCh
6358
6954
  }, []);
6359
6955
  const submit = useCallback(() => {
6360
6956
  const value = textareaRef.current?.plainText ?? "";
6361
- if (!value.trim()) return;
6362
- onSubmit(value, completion.references);
6957
+ if (!value.trim() && attachments.length === 0) return;
6958
+ onSubmit(value, completion.references, attachments);
6363
6959
  textareaRef.current?.clear();
6364
6960
  historyRef.current = null;
6365
6961
  setBufferState({
@@ -6367,7 +6963,12 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, onPopupOpenCh
6367
6963
  cursor: 0
6368
6964
  });
6369
6965
  setContentLines(MIN_CONTENT_LINES);
6370
- }, [onSubmit, completion.references]);
6966
+ setAttachments([]);
6967
+ }, [
6968
+ onSubmit,
6969
+ completion.references,
6970
+ attachments
6971
+ ]);
6371
6972
  const commitCompletion = useCallback(() => {
6372
6973
  const result = completion.commit();
6373
6974
  if (!result) return false;
@@ -6399,6 +7000,32 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, onPopupOpenCh
6399
7000
  if (!ta) return;
6400
7001
  const normalized = stripAnsiSequences(decodePasteBytes(event.bytes)).replace(/\r\n?/g, "\n");
6401
7002
  if (normalized.length === 0) return;
7003
+ if (!normalized.includes("\n")) {
7004
+ const trimmed = normalized.trim().replace(/^["']|["']$/g, "");
7005
+ let resolved = trimmed.startsWith("file://") ? decodeURIComponent(trimmed.slice(7)) : trimmed;
7006
+ if (resolved.startsWith("~/")) resolved = (process.env.HOME ?? "") + resolved.slice(1);
7007
+ if (resolved.length > 0 && (resolved.startsWith("/") || resolved.startsWith("."))) try {
7008
+ if (statSync(resolved).isFile()) {
7009
+ const buf = readFileSync(resolved);
7010
+ const ext = (resolved.split(".").pop() ?? "").toLowerCase();
7011
+ const mediaType = IMAGE_MEDIA_TYPES[ext] ?? MIME_BY_EXT[ext] ?? "application/octet-stream";
7012
+ setAttachments((prev) => [...prev, {
7013
+ name: basename(resolved),
7014
+ content: buf,
7015
+ mediaType
7016
+ }]);
7017
+ return;
7018
+ }
7019
+ } catch {}
7020
+ }
7021
+ if (normalized.includes("\n")) {
7022
+ setAttachments((prev) => [...prev, {
7023
+ name: "paste.txt",
7024
+ content: Buffer.from(normalized, "utf-8"),
7025
+ mediaType: "text/plain"
7026
+ }]);
7027
+ return;
7028
+ }
6402
7029
  const parts = normalized.split("\n");
6403
7030
  for (let i = 0; i < parts.length; i++) {
6404
7031
  if (i > 0) ta.newLine();
@@ -6478,7 +7105,15 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, onPopupOpenCh
6478
7105
  const buffer = textareaRef.current;
6479
7106
  if (!buffer) return;
6480
7107
  if (event.name === "up") {
6481
- if (buffer.cursorOffset !== 0) return;
7108
+ if (buffer.cursorOffset !== 0) {
7109
+ const { row } = buffer.editorView.getCursor();
7110
+ if (row === 0) {
7111
+ event.preventDefault();
7112
+ buffer.cursorOffset = 0;
7113
+ syncBuffer();
7114
+ }
7115
+ return;
7116
+ }
6482
7117
  event.preventDefault();
6483
7118
  if (buffer.plainText.length === 0 && onEnterQueueFromEmpty?.()) return;
6484
7119
  cycleHistory(-1);
@@ -6492,7 +7127,8 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, onPopupOpenCh
6492
7127
  completion,
6493
7128
  commitCompletion,
6494
7129
  cycleHistory,
6495
- onEnterQueueFromEmpty
7130
+ onEnterQueueFromEmpty,
7131
+ syncBuffer
6496
7132
  ]);
6497
7133
  const boxHeight = Math.min(MAX_CONTENT_LINES, contentLines) + 2;
6498
7134
  return /* @__PURE__ */ jsxs("box", {
@@ -6505,36 +7141,71 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, onPopupOpenCh
6505
7141
  flexDirection: "column",
6506
7142
  flexShrink: 0
6507
7143
  },
6508
- children: [/* @__PURE__ */ jsx("box", {
6509
- style: {
6510
- border: true,
6511
- borderColor: selectMode ? COLOR.warn : COLOR.borderActive,
6512
- paddingLeft: 1,
6513
- paddingRight: 1,
6514
- height: boxHeight,
6515
- flexDirection: "column"
6516
- },
6517
- children: /* @__PURE__ */ jsx("textarea", {
6518
- ref: textareaRef,
6519
- focused: focused && !selectMode,
6520
- keyBindings: TEXTAREA_BINDINGS,
6521
- wrapMode: "word",
6522
- placeholder: selectMode === "turn" ? "— turn-select mode — press ⎋ to resume typing —" : selectMode === "queue" ? "— queue-select mode — press ⎋ to resume typing —" : "Ask zidane…",
6523
- syntaxStyle: chipStyle,
7144
+ children: [
7145
+ attachments.length > 0 && /* @__PURE__ */ jsx("box", {
6524
7146
  style: {
6525
- flexGrow: 1,
6526
- height: "100%"
7147
+ flexDirection: "row",
7148
+ flexWrap: "wrap",
7149
+ paddingLeft: 1,
7150
+ paddingRight: 1,
7151
+ paddingBottom: 0
7152
+ },
7153
+ children: attachments.map((att, idx) => {
7154
+ const sz = att.content.length;
7155
+ const label = sz < 1024 ? `${sz}B` : sz < 1024 * 1024 ? `${(sz / 1024).toFixed(1)}KB` : `${(sz / (1024 * 1024)).toFixed(1)}MB`;
7156
+ const icon = att.mediaType.startsWith("image/") ? "🖼" : "📎";
7157
+ const chipColor = resolveChipColor(SURFACE.chips, "file");
7158
+ return /* @__PURE__ */ jsxs("text", { children: [
7159
+ idx > 0 ? " " : "",
7160
+ icon,
7161
+ /* @__PURE__ */ jsxs("span", {
7162
+ fg: chipColor.fg,
7163
+ bg: chipColor.bg,
7164
+ children: [
7165
+ " ",
7166
+ att.name,
7167
+ " ",
7168
+ "(",
7169
+ label,
7170
+ ")",
7171
+ " "
7172
+ ]
7173
+ })
7174
+ ] }, `${att.name}-${idx}`);
7175
+ })
7176
+ }),
7177
+ /* @__PURE__ */ jsx("box", {
7178
+ style: {
7179
+ border: true,
7180
+ borderColor: selectMode ? COLOR.warn : COLOR.borderActive,
7181
+ paddingLeft: 1,
7182
+ paddingRight: 1,
7183
+ height: boxHeight,
7184
+ flexDirection: "column"
6527
7185
  },
6528
- onSubmit: submit,
6529
- onContentChange: syncBuffer,
6530
- onKeyDown,
6531
- onPaste
7186
+ children: /* @__PURE__ */ jsx("textarea", {
7187
+ ref: textareaRef,
7188
+ focused: focused && !selectMode,
7189
+ keyBindings: TEXTAREA_BINDINGS,
7190
+ wrapMode: "word",
7191
+ placeholder: selectMode === "turn" ? "— turn-select mode — press ⎋ to resume typing —" : selectMode === "queue" ? "— queue-select mode — press ⎋ to resume typing —" : "Ask zidane…",
7192
+ syntaxStyle: chipStyle,
7193
+ style: {
7194
+ flexGrow: 1,
7195
+ height: "100%"
7196
+ },
7197
+ onSubmit: submit,
7198
+ onContentChange: syncBuffer,
7199
+ onKeyDown,
7200
+ onPaste
7201
+ })
7202
+ }),
7203
+ /* @__PURE__ */ jsx(PromptHints, {
7204
+ selectMode,
7205
+ triggerHints,
7206
+ busy
6532
7207
  })
6533
- }), /* @__PURE__ */ jsx(PromptHints, {
6534
- selectMode,
6535
- triggerHints,
6536
- busy
6537
- })]
7208
+ ]
6538
7209
  }), !selectMode && /* @__PURE__ */ jsx("box", {
6539
7210
  style: {
6540
7211
  position: "absolute",
@@ -6638,11 +7309,13 @@ const PROMPT_HINTS_SELECT_QUEUE = [{
6638
7309
  */
6639
7310
  function PromptHints({ selectMode, triggerHints, busy = false }) {
6640
7311
  const COLOR = useColors();
7312
+ const { settings } = useSettings();
6641
7313
  const { width: termWidth } = useTerminalDimensions();
6642
- const primary = selectMode === "turn" ? PROMPT_HINTS_SELECT_TURN : selectMode === "queue" ? PROMPT_HINTS_SELECT_QUEUE : busy ? PROMPT_HINTS_BUSY : PROMPT_HINTS_NORMAL;
7314
+ const minimalResting = !selectMode && !busy && settings.uiMode === "minimal";
7315
+ const primary = selectMode === "turn" ? PROMPT_HINTS_SELECT_TURN : selectMode === "queue" ? PROMPT_HINTS_SELECT_QUEUE : busy ? PROMPT_HINTS_BUSY : minimalResting ? EMPTY_HINTS : PROMPT_HINTS_NORMAL;
6643
7316
  const hints = useMemo(() => {
6644
7317
  const budget = Math.max(0, termWidth - 5);
6645
- const tooLongCombined = !!triggerHints && triggerHints.length > 0 && !selectMode && hintsLength(primary) + 3 + hintsLength(triggerHints) > budget;
7318
+ const tooLongCombined = !!triggerHints && triggerHints.length > 0 && !selectMode && primary.length > 0 && hintsLength(primary) + 3 + hintsLength(triggerHints) > budget;
6646
7319
  return clipHintsToWidth(selectMode || !triggerHints || triggerHints.length === 0 || tooLongCombined ? primary : [...primary, ...triggerHints], budget);
6647
7320
  }, [
6648
7321
  selectMode,
@@ -8939,7 +9612,15 @@ function AppShell() {
8939
9612
  useEffect(() => {
8940
9613
  smoothStreamingRef.current = settings.smoothStreaming;
8941
9614
  }, [settings.smoothStreaming]);
8942
- const [projectDir] = useState(() => findGitRoot(process.cwd()) ?? process.cwd());
9615
+ const [projectDir, setProjectDir] = useState(() => findGitRoot(process.cwd()) ?? process.cwd());
9616
+ const [cwd, setCwdRaw] = useState(process.cwd);
9617
+ const cwdRef = useRef(cwd);
9618
+ cwdRef.current = cwd;
9619
+ const setCwd = useCallback((next) => {
9620
+ process.chdir(next);
9621
+ setCwdRaw(next);
9622
+ setProjectDir(findGitRoot(next) ?? next);
9623
+ }, []);
8943
9624
  const safelistRef = useRef(null);
8944
9625
  const readSafelist = useCallback(() => {
8945
9626
  if (safelistRef.current === null) safelistRef.current = getSafelist(dataDir, projectDir);
@@ -8971,9 +9652,8 @@ function AppShell() {
8971
9652
  const ensureSkillsCatalogRef = useRef(ensureSkillsCatalog);
8972
9653
  ensureSkillsCatalogRef.current = ensureSkillsCatalog;
8973
9654
  const formatFilePathForCwd = useMemo(() => {
8974
- const cwd = process.cwd();
8975
9655
  return (entry) => formatPathForCwd(entry.path, projectDir, cwd);
8976
- }, [projectDir]);
9656
+ }, [projectDir, cwd]);
8977
9657
  const completionProviders = useMemo(() => [createSkillsCompletionProvider({
8978
9658
  getCatalog: () => skillsCatalogRef.current,
8979
9659
  getEnabled: () => enabledSkillsRef.current,
@@ -9142,6 +9822,25 @@ function AppShell() {
9142
9822
  queueSelectionIndexRef.current = queueSelectionIndex;
9143
9823
  const agentRef = useRef(null);
9144
9824
  const sessionRef = useRef(null);
9825
+ useEffect(() => {
9826
+ const handle = agentRef.current?.handle;
9827
+ if (handle) handle.cwd = cwd;
9828
+ }, [cwd]);
9829
+ useEffect(() => {
9830
+ const agent = agentRef.current;
9831
+ if (!agent) return;
9832
+ const profile = pickedAgentRef.current;
9833
+ if (profile.id !== "build" && profile.id !== "plan") return;
9834
+ return agent.hooks.hook("system:transform", (ctx) => {
9835
+ const allowInteraction = allowInteractionRef.current !== false;
9836
+ const freshEnv = envSection({
9837
+ cwd,
9838
+ ...projectDir !== cwd ? { projectRoot: projectDir } : {},
9839
+ allowInteraction
9840
+ });
9841
+ ctx.system = ctx.system.replace(/<env>[\s\S]*?<\/env>/, freshEnv);
9842
+ });
9843
+ }, [cwd, projectDir]);
9145
9844
  /**
9146
9845
  * Live registry of in-flight tool calls — populated by `tool:before`
9147
9846
  * (and `child:tool:before`), drained by `tool:after` / `tool:error` /
@@ -9287,7 +9986,7 @@ function AppShell() {
9287
9986
  });
9288
9987
  const allowInteraction = allowInteractionRef.current !== false;
9289
9988
  const interactionTools = allowInteraction ? createInteractionTools({ requestInteraction: makeRequestInteraction(interactions) }) : {};
9290
- const actualCwd = process.cwd();
9989
+ const actualCwd = cwdRef.current;
9291
9990
  const envOpts = {
9292
9991
  cwd: actualCwd,
9293
9992
  ...projectDir !== actualCwd ? { projectRoot: projectDir } : {},
@@ -9323,6 +10022,7 @@ function AppShell() {
9323
10022
  ...persistBehavior,
9324
10023
  tasksDir
9325
10024
  },
10025
+ execution: createProcessContext({ cwd: actualCwd }),
9326
10026
  provider: descriptor.factory(),
9327
10027
  session,
9328
10028
  mcpConnector: (configs) => connectMcpServers(configs, void 0, agent.hooks, { buildAuthProvider: (cfg) => new McpOAuthProvider({
@@ -10147,7 +10847,7 @@ function AppShell() {
10147
10847
  * busy-state lifecycle so the title spinner stays mounted continuously
10148
10848
  * across back-to-back queued messages.
10149
10849
  */
10150
- const runSingleMessage = useCallback(async (prompt, references) => {
10850
+ const runSingleMessage = useCallback(async (prompt, references, attachments) => {
10151
10851
  const agent = agentRef.current;
10152
10852
  const session = sessionRef.current;
10153
10853
  if (!agent || !session || !picked) return;
@@ -10160,10 +10860,16 @@ function AppShell() {
10160
10860
  end: r.end,
10161
10861
  providerId: r.providerId
10162
10862
  }));
10863
+ const attachmentMeta = attachments.map((a) => ({
10864
+ name: a.name,
10865
+ mediaType: a.mediaType,
10866
+ size: a.content.length
10867
+ }));
10163
10868
  stream.appendImmediate({
10164
10869
  kind: "user-prompt",
10165
10870
  text: prompt,
10166
- ...refSpans.length > 0 ? { refs: refSpans } : {}
10871
+ ...refSpans.length > 0 ? { refs: refSpans } : {},
10872
+ ...attachmentMeta.length > 0 ? { attachments: attachmentMeta } : {}
10167
10873
  });
10168
10874
  if (autoCompactInFlightRef.current) await autoCompactInFlightRef.current.catch(() => {});
10169
10875
  const newRefs = uniqueSkillNamesFromReferences(references);
@@ -10174,9 +10880,34 @@ function AppShell() {
10174
10880
  debugLog(`activateSkill("${name}")`, err);
10175
10881
  }
10176
10882
  try {
10883
+ const runPrompt = attachments.length === 0 ? prompt : [...prompt.length > 0 ? [{
10884
+ type: "text",
10885
+ text: prompt
10886
+ }] : [], ...attachments.map((att) => {
10887
+ if (att.mediaType.startsWith("image/")) return {
10888
+ type: "image",
10889
+ mediaType: att.mediaType,
10890
+ data: att.content.toString("base64"),
10891
+ name: att.name
10892
+ };
10893
+ if (att.mediaType.startsWith("text/")) return {
10894
+ type: "document",
10895
+ mediaType: att.mediaType,
10896
+ data: att.content.toString("utf-8"),
10897
+ encoding: "text",
10898
+ name: att.name
10899
+ };
10900
+ return {
10901
+ type: "document",
10902
+ mediaType: att.mediaType,
10903
+ data: att.content.toString("base64"),
10904
+ encoding: "base64",
10905
+ name: att.name
10906
+ };
10907
+ })];
10177
10908
  await agent.run({
10178
10909
  model: picked.model,
10179
- prompt,
10910
+ prompt: runPrompt,
10180
10911
  ...picked.effort ? { thinking: picked.effort } : {}
10181
10912
  });
10182
10913
  await session.save().catch((err) => debugLog("session.save failed", err));
@@ -10198,15 +10929,16 @@ function AppShell() {
10198
10929
  stream.flushAndUpdate(finalizeStreamingMarkdown);
10199
10930
  }
10200
10931
  }, [picked, stream]);
10201
- const onSubmitPrompt = useCallback((prompt, references) => {
10202
- if (!prompt.trim()) return;
10932
+ const onSubmitPrompt = useCallback((prompt, references, attachments) => {
10933
+ if (!prompt.trim() && attachments.length === 0) return;
10203
10934
  const agent = agentRef.current;
10204
10935
  const session = sessionRef.current;
10205
10936
  if (!agent || !session || !picked) return;
10206
10937
  if (runningRef.current) {
10207
10938
  messageQueueRef.current = [...messageQueueRef.current, {
10208
10939
  prompt,
10209
- references
10940
+ references,
10941
+ attachments
10210
10942
  }];
10211
10943
  setMessageQueue(messageQueueRef.current.slice());
10212
10944
  return;
@@ -10215,7 +10947,7 @@ function AppShell() {
10215
10947
  (async () => {
10216
10948
  setBusy(true);
10217
10949
  try {
10218
- await runSingleMessage(prompt, references);
10950
+ await runSingleMessage(prompt, references, attachments);
10219
10951
  while (messageQueueRef.current.length > 0) {
10220
10952
  const next = messageQueueRef.current[0];
10221
10953
  messageQueueRef.current = messageQueueRef.current.slice(1);
@@ -10226,7 +10958,7 @@ function AppShell() {
10226
10958
  if (prev <= 0) return null;
10227
10959
  return prev - 1;
10228
10960
  });
10229
- await runSingleMessage(next.prompt, next.references);
10961
+ await runSingleMessage(next.prompt, next.references, next.attachments);
10230
10962
  }
10231
10963
  } finally {
10232
10964
  setBusy(false);
@@ -10852,6 +11584,15 @@ function AppShell() {
10852
11584
  }));
10853
11585
  return;
10854
11586
  }
11587
+ if (matchesBinding(key, keybindings.openKeybindings) && screen !== "auth") {
11588
+ modal.open(/* @__PURE__ */ jsx(KeybindingsModal, {
11589
+ bindings: keybindings,
11590
+ filePath: keybindingsPath(config.paths.userDir),
11591
+ onEditFile: onOpenKeybindingsFile,
11592
+ onClose: () => modal.close()
11593
+ }));
11594
+ return;
11595
+ }
10855
11596
  if (matchesBinding(key, keybindings.enterSelectTurnMode) && screen === "chat" && !busy && !pendingApproval && !pendingInteraction) {
10856
11597
  enterSelectMode();
10857
11598
  return;
@@ -10883,6 +11624,16 @@ function AppShell() {
10883
11624
  }));
10884
11625
  return;
10885
11626
  }
11627
+ if (matchesBinding(key, keybindings.changeCwd) && screen !== "auth") {
11628
+ modal.open(/* @__PURE__ */ jsx(CwdPickerModal, {
11629
+ currentCwd: cwdRef.current,
11630
+ onPick: (dir) => {
11631
+ setCwd(dir);
11632
+ modal.close();
11633
+ }
11634
+ }));
11635
+ return;
11636
+ }
10886
11637
  if (key.name !== "escape") return;
10887
11638
  if (busy || pendingApproval) return onAbort();
10888
11639
  if (popupOpenRef.current) return;
@@ -10917,6 +11668,7 @@ function AppShell() {
10917
11668
  pendingInteractionResumed: pendingInteractionIsResumed,
10918
11669
  currentSession,
10919
11670
  hasMultipleAgents,
11671
+ uiMode: settings.uiMode,
10920
11672
  modelLabel: picked?.model ?? null,
10921
11673
  modelColor: COLOR.model,
10922
11674
  effortLabel: modelHasReasoning ? picked?.effort ?? "medium" : null,
@@ -10937,6 +11689,7 @@ function AppShell() {
10937
11689
  pendingInteractionIsResumed,
10938
11690
  currentSession,
10939
11691
  hasMultipleAgents,
11692
+ settings.uiMode,
10940
11693
  picked,
10941
11694
  pickedAgent,
10942
11695
  COLOR,
@@ -11016,6 +11769,7 @@ function AppShell() {
11016
11769
  currentProjectRoot: projectDir
11017
11770
  }),
11018
11771
  screen === "chat" && /* @__PURE__ */ jsx(ChatScreen, {
11772
+ cwd,
11019
11773
  events,
11020
11774
  busy,
11021
11775
  compacting,
@@ -11055,162 +11809,6 @@ function effortForModel(descriptor, modelId, remembered) {
11055
11809
  if (!modelSupportsReasoning(descriptor, modelId)) return void 0;
11056
11810
  return remembered?.[modelId] ?? "medium";
11057
11811
  }
11058
- /**
11059
- * Build the footer's shortcut hints for the current screen. On the chat
11060
- * screen the model id rides next to its `ctrl+m` shortcut and the agent
11061
- * label rides next to `shift+tab`, each in its accent color — the bar
11062
- * doubles as the status display without needing separate badges. When
11063
- * the active model exposes reasoning, the `ctrl+m` hint grows a
11064
- * secondary `/n` chord with the current effort label, surfacing the
11065
- * effort picker as a discoverable, in-place affordance.
11066
- */
11067
- function buildHints({ screen, busy, pending, pendingInteractionLive, pendingInteractionResumed, currentSession, hasMultipleAgents, modelLabel, modelColor, effortLabel, effortColor, effortKeyColor, agentLabel, agentColor, keybindings, inFlightToolCount, activeSkillCount, skillsChipColor, updateHint }) {
11068
- if (pending) return [
11069
- {
11070
- key: "↑↓",
11071
- label: "navigate"
11072
- },
11073
- {
11074
- key: "↵",
11075
- label: "select"
11076
- },
11077
- {
11078
- key: "esc",
11079
- label: "abort run"
11080
- }
11081
- ];
11082
- if (pendingInteractionLive) return [
11083
- {
11084
- key: "↑↓",
11085
- label: "navigate"
11086
- },
11087
- {
11088
- key: "↵",
11089
- label: "select"
11090
- },
11091
- {
11092
- key: "esc",
11093
- label: "abort run"
11094
- }
11095
- ];
11096
- if (pendingInteractionResumed) return [
11097
- {
11098
- key: "↑↓",
11099
- label: "navigate"
11100
- },
11101
- {
11102
- key: "↵",
11103
- label: "select"
11104
- },
11105
- {
11106
- key: "esc",
11107
- label: "leave for later"
11108
- }
11109
- ];
11110
- if (busy) {
11111
- const baseBusyHints = [];
11112
- if (inFlightToolCount > 0) baseBusyHints.push({
11113
- key: keybindings.cancelToolCall,
11114
- label: inFlightToolCount === 1 ? "cancel" : `cancel (${inFlightToolCount})`
11115
- });
11116
- baseBusyHints.push({
11117
- key: "esc",
11118
- label: "abort"
11119
- });
11120
- return baseBusyHints;
11121
- }
11122
- if (screen === "auth") return [
11123
- {
11124
- key: "↑↓",
11125
- label: "navigate"
11126
- },
11127
- {
11128
- key: "↵",
11129
- label: "select"
11130
- },
11131
- {
11132
- key: "esc",
11133
- label: "exit"
11134
- }
11135
- ];
11136
- if (screen === "sessions") return [
11137
- {
11138
- key: "↑↓",
11139
- label: "navigate"
11140
- },
11141
- {
11142
- key: "↵",
11143
- label: "open"
11144
- },
11145
- {
11146
- key: keybindings.openSessionDetails,
11147
- label: "session"
11148
- },
11149
- {
11150
- key: keybindings.openSettings,
11151
- label: "settings"
11152
- },
11153
- {
11154
- key: "esc",
11155
- label: currentSession ? "back" : "exit"
11156
- }
11157
- ];
11158
- const modelHint = modelLabel ? {
11159
- key: keybindings.openModelPicker,
11160
- label: modelLabel,
11161
- labelColor: modelColor,
11162
- ...effortLabel ? { extra: {
11163
- key: shortChord(keybindings.openEffortPicker),
11164
- keyColor: effortKeyColor,
11165
- label: effortLabel,
11166
- labelColor: effortColor
11167
- } } : {}
11168
- } : null;
11169
- const skillsChip = activeSkillCount > 0 ? {
11170
- key: "✦",
11171
- keyColor: skillsChipColor,
11172
- label: activeSkillCount === 1 ? "1 skill" : `${activeSkillCount} skills`,
11173
- labelColor: skillsChipColor
11174
- } : null;
11175
- const cancelTaskChip = inFlightToolCount > 0 ? {
11176
- key: keybindings.cancelToolCall,
11177
- label: inFlightToolCount === 1 ? "cancel task" : `cancel task (${inFlightToolCount})`
11178
- } : null;
11179
- return [
11180
- ...hasMultipleAgents ? [{
11181
- key: keybindings.cycleAgent,
11182
- label: agentLabel,
11183
- labelColor: agentColor
11184
- }] : [],
11185
- ...modelHint ? [modelHint] : [],
11186
- ...skillsChip ? [skillsChip] : [],
11187
- ...cancelTaskChip ? [cancelTaskChip] : [],
11188
- ...currentSession ? [{
11189
- key: keybindings.openSessionDetails,
11190
- label: "session"
11191
- }] : [],
11192
- {
11193
- key: keybindings.openSettings,
11194
- label: "settings"
11195
- },
11196
- {
11197
- key: "esc",
11198
- label: "sessions"
11199
- },
11200
- ...updateHint ? [updateHint] : []
11201
- ];
11202
- }
11203
- /**
11204
- * Shorten a binding spec for display as a "chord continuation" — the
11205
- * `/n` after `ctrl+m model` in the chat footer. We only strip the
11206
- * `ctrl+` prefix so user-customized chords (`alt+n`, `meta+shift+n`)
11207
- * still render in full. Falls back to the verbatim spec when no
11208
- * `ctrl+` prefix is found, which keeps the visual contract honest:
11209
- * the rendered key always matches the bound trigger.
11210
- */
11211
- function shortChord(spec) {
11212
- return spec.startsWith("ctrl+") ? `/${spec.slice(5)}` : spec;
11213
- }
11214
11812
  //#endregion
11215
11813
  //#region src/tui/tree-sitter.ts
11216
11814
  /**