zidane 5.5.5 → 5.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +7 -1
  2. package/dist/{agent-CMAklak7.d.ts → agent-Dtnvs5ee.d.ts} +91 -2
  3. package/dist/agent-Dtnvs5ee.d.ts.map +1 -0
  4. package/dist/chat.d.ts +204 -15
  5. package/dist/chat.d.ts.map +1 -1
  6. package/dist/chat.js +3 -3
  7. package/dist/{errors-C5VSakmT.js → errors-DdZXnyXE.js} +38 -2
  8. package/dist/errors-DdZXnyXE.js.map +1 -0
  9. package/dist/{index-CF5QwBiz.d.ts → index-DHeHe04L.d.ts} +2 -2
  10. package/dist/{index-CF5QwBiz.d.ts.map → index-DHeHe04L.d.ts.map} +1 -1
  11. package/dist/{index-kroGomhj.d.ts → index-DX8De0nl.d.ts} +23 -2
  12. package/dist/index-DX8De0nl.d.ts.map +1 -0
  13. package/dist/index.d.ts +4 -4
  14. package/dist/index.js +10 -10
  15. package/dist/{interpolate-Cvjy8gpk.js → interpolate-j5V-wcAQ.js} +2 -2
  16. package/dist/{interpolate-Cvjy8gpk.js.map → interpolate-j5V-wcAQ.js.map} +1 -1
  17. package/dist/{login-B_kfoGMP.js → login-BOj03nVe.js} +5 -4
  18. package/dist/login-BOj03nVe.js.map +1 -0
  19. package/dist/{mcp-BE43Viwi.js → mcp-ngMS0S6N.js} +2 -2
  20. package/dist/{mcp-BE43Viwi.js.map → mcp-ngMS0S6N.js.map} +1 -1
  21. package/dist/mcp.d.ts +1 -1
  22. package/dist/mcp.js +1 -1
  23. package/dist/{messages-BBWakTN6.js → messages-B5k4DAXy.js} +2 -2
  24. package/dist/{messages-BBWakTN6.js.map → messages-B5k4DAXy.js.map} +1 -1
  25. package/dist/{presets-BDvBZuYI.js → presets-CTSij3yV.js} +2 -2
  26. package/dist/{presets-BDvBZuYI.js.map → presets-CTSij3yV.js.map} +1 -1
  27. package/dist/presets.d.ts +2 -2
  28. package/dist/presets.js +1 -1
  29. package/dist/{providers-CsUyN_FJ.js → providers-CaJE2ToS.js} +3 -3
  30. package/dist/{providers-CsUyN_FJ.js.map → providers-CaJE2ToS.js.map} +1 -1
  31. package/dist/providers.d.ts +1 -1
  32. package/dist/providers.js +2 -2
  33. package/dist/restate.d.ts +1 -1
  34. package/dist/session/sqlite.d.ts +1 -1
  35. package/dist/session/sqlite.d.ts.map +1 -1
  36. package/dist/session/sqlite.js +226 -51
  37. package/dist/session/sqlite.js.map +1 -1
  38. package/dist/{session-DzfRacU_.js → session-BoEW_wCR.js} +2 -2
  39. package/dist/{session-DzfRacU_.js.map → session-BoEW_wCR.js.map} +1 -1
  40. package/dist/session.d.ts +1 -1
  41. package/dist/session.js +2 -2
  42. package/dist/skills.d.ts +2 -2
  43. package/dist/skills.js +1 -1
  44. package/dist/{tools-Bbd0Ivwn.js → tools-CslsHpKb.js} +156 -16
  45. package/dist/tools-CslsHpKb.js.map +1 -0
  46. package/dist/tools.d.ts +2 -2
  47. package/dist/tools.js +1 -1
  48. package/dist/{transcript-anchors-C7CtKPPo.d.ts → transcript-anchors-CwoKNW6Y.d.ts} +74 -5
  49. package/dist/transcript-anchors-CwoKNW6Y.d.ts.map +1 -0
  50. package/dist/tui.d.ts +24 -5
  51. package/dist/tui.d.ts.map +1 -1
  52. package/dist/tui.js +1280 -333
  53. package/dist/tui.js.map +1 -1
  54. package/dist/{turn-operations-rYyU2Qyq.js → turn-operations-B8ySajUl.js} +687 -86
  55. package/dist/turn-operations-B8ySajUl.js.map +1 -0
  56. package/dist/types-oKPBdCmL.js.map +1 -1
  57. package/dist/types.d.ts +3 -3
  58. package/dist/types.js +2 -2
  59. package/docs/ARCHITECTURE.md +5 -2
  60. package/docs/CHAT.md +10 -3
  61. package/docs/RESTATE.md +190 -0
  62. package/docs/SKILL.md +27 -2
  63. package/docs/TUI.md +4 -3
  64. package/package.json +2 -1
  65. package/dist/agent-CMAklak7.d.ts.map +0 -1
  66. package/dist/errors-C5VSakmT.js.map +0 -1
  67. package/dist/index-kroGomhj.d.ts.map +0 -1
  68. package/dist/login-B_kfoGMP.js.map +0 -1
  69. package/dist/tools-Bbd0Ivwn.js.map +0 -1
  70. package/dist/transcript-anchors-C7CtKPPo.d.ts.map +0 -1
  71. package/dist/turn-operations-rYyU2Qyq.js.map +0 -1
package/dist/tui.js CHANGED
@@ -1,30 +1,37 @@
1
- import { $ as useMcpAuthDispatch, $n as tryOpenBrowser, A as getSafelist, At as resolveChipColor, B as supportsOAuth, Br as accentColor, Bt as useDiscoveryOptional, Cn as mergeApprovalAndBodyOutcomes, Ct as SETTINGS_CHOICES, D as useSafeModeQueue, Dn as stripEditOutcomesAnnotation, Dr as getContextWindow, Dt as useSettings, E as useSafeModeActions, En as rewriteMultiEditHeader, Et as clampFps, F as suggestSafelistEntry, Fn as matchesBinding, H as filterModelCatalog, Hn as uniqueSkillNamesFromReferences, Ht as useConfig, Jt as isEditErrorResult, K as discoverProjectMcps, Kr as TODO_STATUS_GLYPHS, Kt as deriveSessionTitle, L as splitPromptSegments, Lt as createDiscoverySlot, Nn as ensureKeybindingsFile, Nr as piIdOf, On as summarizeOutcomes, Q as McpAuthProvider, Qn as buildLinearRamp, Qt as listSessionMeta, R as formatPathForCwd, Rt as DiscoveryProvider, St as DEFAULT_SETTINGS, T as SafeModeProvider, Tn as resolveApprovalForPayload, Tt as SettingsProvider, U as indexOfEntry, Ut as resolveConfig, V as buildModelCatalog, Vn as createSkillsCompletionProvider, Vt as ConfigProvider, W as buildMcpServers, Wn as createFilesCompletionProvider, Wt as EDIT_TOOL_NAMES, Xn as useCompletion, Xt as isVisible, Y as createFileMcpCredentialStore, Yt as isTurnHighlighted, Zn as blendHsl, Zt as lastContextSizeFromTurns, _ as turnContextSize, _n as previewEditPayload, _t as truncateTrailing, a as computeTurnAnchors, at as InteractionsProvider, b as defaultSkillScanPaths, bt as listProjectFiles, c as formatToolCall, cn as turnSelectionOwnership, ct as createInteractionTools, d as useSelectStyle, dn as buildContextualDiff, dt as pendingInteractionsFromTurns, en as marginTopFor, et as useMcpAuthState, f as useSurfaces, fn as buildUnifiedDiff, fr as shouldAutoCompact, g as finalizeStreamingMarkdownForOwner, gn as filetypeFromPath, gt as hintsLength, h as finalizeStreamingMarkdown, hn as extractEditPayload, ht as clipHintsToWidth, i as turnAsText, in as sumRunCosts, j as isOnSafelist, jt as resolveTheme, k as addToSafelist, kn as findGitRoot, kr as modelSupportsReasoning, l as ThemeProvider, ln as updateToolEventOutcomes, m as useTheme, mi as buildPlanSystem, mt as useInteractionsQueue, n as deleteTurnSafely, ni as useActiveTodos, nn as selectableTurnIds, nr as buildUpdateHint, o as TOOL_DISPLAY, on as toolCallPreview, pi as buildBuildSystem, pr as detectAuth, pt as useInteractionsActions, qt as eventsFromTurns, r as truncateTurnsAt, rn as stripSpawnTokensLine, rr as useUpdateCheck, rt as splitMarkdownCodeBlocks, s as displayNameFor, sn as toolResultText, st as buildResumedToolResultsTurn, tr as bootTick, tt as getMcpAuthStatus, u as useColors, ut as makeRequestInteraction, v as useStreamBuffer, w as writeSessionExport, wn as parseEditOutcomesFromResult, wt as SETTINGS_TOGGLES, x as discoverProjectSkills, xn as buildEditOutcomesAnnotation, xt as useEnabledToggleSet, y as buildSkillsConfig, yn as summarizeEditPayload, yr as setProviderCredential, yt as generateSessionTitle, z as runOAuthLogin, zt as useDiscovery } from "./turn-operations-rYyU2Qyq.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-Bbd0Ivwn.js";
3
- import { s as errorMessage } from "./errors-C5VSakmT.js";
4
- import { s as McpOAuthProvider, t as connectMcpServers } from "./mcp-BE43Viwi.js";
5
- import { C as summaryToTurn, a as selectFilesFromSession, r as buildPostCompactAttachments, s as compactConversation, t as loginMcpServer } from "./login-B_kfoGMP.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";
4
+ import { c as errorMessage } from "./errors-DdZXnyXE.js";
5
+ import { s as McpOAuthProvider, t as connectMcpServers } from "./mcp-ngMS0S6N.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
- import { n as loadSession, t as createSession } from "./session-DzfRacU_.js";
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.
@@ -4637,6 +5206,282 @@ function OptionList({ items, initialCursor, onPick }) {
4637
5206
  });
4638
5207
  }
4639
5208
  //#endregion
5209
+ //#region src/tui/oauth-url-block.tsx
5210
+ /** @jsxImportSource @opentui/react */
5211
+ /**
5212
+ * Long URL rendered as N single-row OSC 8 hyperlinks instead of one
5213
+ * wrapped hyperlink.
5214
+ *
5215
+ * Why split: OpenTUI packs a `linkId` per cell into the attribute
5216
+ * bitfield, but its renderer emits an OSC 8 open/close pair per visual
5217
+ * row without the spec's `id=` parameter. Terminals (notably iTerm2)
5218
+ * need a matching `id=` to stitch hyperlink fragments across rows —
5219
+ * without it, only one row of a wrapped link ends up clickable. We
5220
+ * pre-chunk the URL into rows that fit on one line and render each as
5221
+ * its own intact `<a href>` with `wrapMode="none"`, so the terminal
5222
+ * never sees a wrapped hyperlink and clicking any row opens the full
5223
+ * URL.
5224
+ *
5225
+ * Width: caller passes a hard cap (its container's content-area width).
5226
+ * We further clamp by the live terminal width minus `chromeWidth` so a
5227
+ * narrow terminal still chunks short enough to avoid forced wrap by
5228
+ * the layout engine. `chromeWidth` is the container's border + padding
5229
+ * budget — default 14 fits a typical modal; pass a smaller value for
5230
+ * less-padded containers (e.g. 6 for the auth wizard panel).
5231
+ */
5232
+ function OAuthUrlBlock({ url, fg, maxLineWidth, chromeWidth = 14 }) {
5233
+ const { width: termWidth } = useTerminalDimensions();
5234
+ return /* @__PURE__ */ jsx(Fragment, { children: chunkString(url, Math.max(20, Math.min(maxLineWidth, termWidth - chromeWidth))).map((line, i) => /* @__PURE__ */ jsx("text", {
5235
+ wrapMode: "none",
5236
+ fg,
5237
+ children: /* @__PURE__ */ jsx("a", {
5238
+ href: url,
5239
+ children: line
5240
+ })
5241
+ }, i)) });
5242
+ }
5243
+ function chunkString(s, n) {
5244
+ if (s.length <= n) return [s];
5245
+ const out = [];
5246
+ for (let i = 0; i < s.length; i += n) out.push(s.slice(i, i + n));
5247
+ return out;
5248
+ }
5249
+ //#endregion
5250
+ //#region src/tui/oauth-auth-block.tsx
5251
+ /** Keystroke shown next to the open-browser button. */
5252
+ const OPEN_BROWSER_KEY = "ctrl+b";
5253
+ /**
5254
+ * Unified affordance for surfacing an OAuth authorization URL in the TUI:
5255
+ *
5256
+ * 1. A prominent OSC 8 hyperlink button — "Click here to open auth URL in
5257
+ * browser". Honored by every terminal that supports clickable links
5258
+ * (iTerm2, Kitty, WezTerm, Ghostty, Alacritty, …). Lands on the user's
5259
+ * LOCAL terminal even over SSH because OSC 8 is interpreted client-side.
5260
+ * 2. The full URL rendered greyed below, chunked into per-row hyperlinks
5261
+ * via {@link OAuthUrlBlock}. Backup channel for terminals without OSC 8,
5262
+ * and for users who'd rather copy via drag-select than click.
5263
+ * 3. Optional paste-back input. When `paste` is provided, an `<input>`
5264
+ * renders below the URL so the user can paste the FULL redirect URL
5265
+ * their browser ended up at after authorizing — useful when zidane runs
5266
+ * over SSH and the browser-side redirect to loopback can't reach the
5267
+ * remote callback server. The caller's `onSubmit` typically pipes the
5268
+ * pasted value through {@link fetchOAuthRedirect} so the in-process
5269
+ * server receives the request and the OAuth promise resolves through
5270
+ * the same happy path a real browser would have taken.
5271
+ *
5272
+ * The component owns presentation only — keyboard focus, input ref, and
5273
+ * the submit handler stay with the caller so the affordance composes
5274
+ * cleanly with whatever picker / wizard / modal it lives in.
5275
+ */
5276
+ function OAuthAuthBlock({ authUrl, maxLineWidth, chromeWidth = 14, paste }) {
5277
+ const COLOR = useColors();
5278
+ return /* @__PURE__ */ jsxs("box", {
5279
+ style: {
5280
+ flexDirection: "column",
5281
+ gap: 1
5282
+ },
5283
+ children: [
5284
+ /* @__PURE__ */ jsx(OpenBrowserButton, { authUrl }),
5285
+ /* @__PURE__ */ jsxs("box", {
5286
+ style: { flexDirection: "column" },
5287
+ children: [/* @__PURE__ */ jsx("text", {
5288
+ fg: COLOR.mute,
5289
+ children: "or copy the URL manually:"
5290
+ }), /* @__PURE__ */ jsx(OAuthUrlBlock, {
5291
+ url: authUrl,
5292
+ fg: COLOR.dim,
5293
+ maxLineWidth,
5294
+ chromeWidth
5295
+ })]
5296
+ }),
5297
+ paste && /* @__PURE__ */ jsxs("box", {
5298
+ style: { flexDirection: "column" },
5299
+ children: [
5300
+ /* @__PURE__ */ jsx("text", {
5301
+ fg: COLOR.mute,
5302
+ children: "or — if the browser couldn't reach this machine (SSH, firewall) — paste the URL it tried to redirect to:"
5303
+ }),
5304
+ paste.hint && /* @__PURE__ */ jsx("text", {
5305
+ fg: toneColor(paste.hint.tone, COLOR),
5306
+ children: paste.hint.text
5307
+ }),
5308
+ /* @__PURE__ */ jsx("box", {
5309
+ style: {
5310
+ border: true,
5311
+ borderColor: paste.focused ? COLOR.borderActive : COLOR.border,
5312
+ paddingLeft: 1,
5313
+ paddingRight: 1,
5314
+ height: 3
5315
+ },
5316
+ children: /* @__PURE__ */ jsx("input", {
5317
+ ref: paste.inputRef,
5318
+ focused: paste.focused,
5319
+ placeholder: paste.placeholder ?? "paste redirect URL and press enter…",
5320
+ onSubmit: paste.onSubmit,
5321
+ style: { flexGrow: 1 }
5322
+ })
5323
+ })
5324
+ ]
5325
+ })
5326
+ ]
5327
+ });
5328
+ }
5329
+ /**
5330
+ * Real button: a bordered, brand-colored box hosting a labeled keystroke
5331
+ * hint. Pressing `ctrl+b` (anywhere this block is mounted) calls
5332
+ * {@link tryOpenBrowser} to launch the URL via the OS handler AND
5333
+ * {@link writeToClipboard} so the URL also lands in the user's clipboard
5334
+ * (OSC 52 → works over SSH, native helper → works locally). Both fire
5335
+ * because either alone can fail silently — `open`/`xdg-open` are no-ops
5336
+ * on a headless box, and OSC 52 is disabled on some terminals — and the
5337
+ * combination covers both failure modes without prompting again.
5338
+ *
5339
+ * The visible label is also wrapped in an OSC 8 hyperlink so a mouse
5340
+ * click in a supporting terminal (iTerm2, Kitty, WezTerm, …) reaches the
5341
+ * same browser. The keybind is the authoritative path — it doesn't
5342
+ * depend on terminal capability — and the hyperlink is the bonus.
5343
+ */
5344
+ function OpenBrowserButton({ authUrl }) {
5345
+ const COLOR = useColors();
5346
+ const [feedback, setFeedback] = useState(null);
5347
+ const feedbackTimer = useRef(null);
5348
+ const trigger = useCallback(() => {
5349
+ tryOpenBrowser(authUrl);
5350
+ setFeedback(writeToClipboard(authUrl) ? "opened in browser · URL copied to clipboard" : "opened in browser");
5351
+ if (feedbackTimer.current) clearTimeout(feedbackTimer.current);
5352
+ feedbackTimer.current = setTimeout(setFeedback, 4e3, null);
5353
+ }, [authUrl]);
5354
+ useKeyboard((key) => {
5355
+ if (key.ctrl && key.name === "b") {
5356
+ key.preventDefault();
5357
+ trigger();
5358
+ }
5359
+ });
5360
+ useEffect(() => () => {
5361
+ if (feedbackTimer.current) clearTimeout(feedbackTimer.current);
5362
+ }, []);
5363
+ return /* @__PURE__ */ jsxs("box", {
5364
+ style: { flexDirection: "column" },
5365
+ children: [/* @__PURE__ */ jsx("box", {
5366
+ style: {
5367
+ border: true,
5368
+ borderColor: COLOR.brand,
5369
+ paddingLeft: 1,
5370
+ paddingRight: 1,
5371
+ alignSelf: "flex-start"
5372
+ },
5373
+ children: /* @__PURE__ */ jsxs("text", {
5374
+ wrapMode: "none",
5375
+ children: [
5376
+ /* @__PURE__ */ jsx("a", {
5377
+ href: authUrl,
5378
+ fg: COLOR.brand,
5379
+ children: "↗ Open auth URL in browser"
5380
+ }),
5381
+ /* @__PURE__ */ jsx("span", {
5382
+ fg: COLOR.mute,
5383
+ children: " "
5384
+ }),
5385
+ /* @__PURE__ */ jsx("span", {
5386
+ fg: COLOR.warn,
5387
+ children: OPEN_BROWSER_KEY
5388
+ }),
5389
+ /* @__PURE__ */ jsx("span", {
5390
+ fg: COLOR.mute,
5391
+ children: " open · click also works"
5392
+ })
5393
+ ]
5394
+ })
5395
+ }), feedback && /* @__PURE__ */ jsx("text", {
5396
+ fg: COLOR.accent,
5397
+ children: `✓ ${feedback}`
5398
+ })]
5399
+ });
5400
+ }
5401
+ function toneColor(tone, COLOR) {
5402
+ switch (tone) {
5403
+ case "error": return COLOR.error;
5404
+ case "accent": return COLOR.accent;
5405
+ case "dim": return COLOR.dim;
5406
+ }
5407
+ }
5408
+ /**
5409
+ * Self-contained MCP authorizing panel:
5410
+ *
5411
+ * - Renders the {@link OAuthAuthBlock} for the URL + paste input.
5412
+ * - Owns the input ref, the submit handler, and the hint state.
5413
+ * - Auto-fetches the pasted URL via {@link fetchOAuthRedirect} so the
5414
+ * MCP SDK's loopback callback server resolves the OAuth promise
5415
+ * through the same code path a real browser-redirect would have
5416
+ * taken — works over SSH where the browser can't reach the remote
5417
+ * callback server directly.
5418
+ *
5419
+ * `inputFocused` is forwarded as the input's `focused` prop AND read by
5420
+ * the parent picker's `useKeyboard` (via a ref) to suppress single-key
5421
+ * shortcuts (`l` / `o` / `r`) while the user is typing into the input.
5422
+ */
5423
+ function McpAuthorizingPanel({ serverName, authUrl, maxLineWidth, chromeWidth, inputFocused }) {
5424
+ const COLOR = useColors();
5425
+ const inputRef = useRef(null);
5426
+ const [hint, setHint] = useState(null);
5427
+ const onSubmit = useCallback(() => {
5428
+ const value = inputRef.current?.value?.trim() ?? "";
5429
+ if (!value) return;
5430
+ setHint({
5431
+ text: "submitting redirect URL…",
5432
+ tone: "dim"
5433
+ });
5434
+ (async () => {
5435
+ try {
5436
+ const result = await fetchOAuthRedirect(value);
5437
+ if (result.status >= 200 && result.status < 300) setHint({
5438
+ text: "redirect accepted — finalizing…",
5439
+ tone: "accent"
5440
+ });
5441
+ else setHint({
5442
+ text: `callback server rejected the URL (${result.status}${result.message ? `: ${result.message}` : ""}). Cancel and retry.`,
5443
+ tone: "error"
5444
+ });
5445
+ } catch (err) {
5446
+ setHint({
5447
+ text: errorMessage(err),
5448
+ tone: "error"
5449
+ });
5450
+ }
5451
+ })();
5452
+ }, []);
5453
+ return /* @__PURE__ */ jsxs("box", {
5454
+ style: {
5455
+ flexDirection: "column",
5456
+ border: ["top"],
5457
+ borderColor: COLOR.border,
5458
+ paddingTop: 1
5459
+ },
5460
+ children: [
5461
+ /* @__PURE__ */ jsx("text", {
5462
+ fg: COLOR.brand,
5463
+ children: `Authorizing ${serverName}`
5464
+ }),
5465
+ /* @__PURE__ */ jsx("text", {
5466
+ fg: COLOR.dim,
5467
+ children: "Your browser should have opened — complete the login, then return here."
5468
+ }),
5469
+ /* @__PURE__ */ jsx(OAuthAuthBlock, {
5470
+ authUrl,
5471
+ maxLineWidth,
5472
+ chromeWidth,
5473
+ paste: {
5474
+ inputRef,
5475
+ focused: inputFocused,
5476
+ onSubmit,
5477
+ placeholder: "paste redirect URL and press enter…",
5478
+ hint: hint ?? void 0
5479
+ }
5480
+ })
5481
+ ]
5482
+ });
5483
+ }
5484
+ //#endregion
4640
5485
  //#region src/tui/todo-indicator.tsx
4641
5486
  /**
4642
5487
  * Layout budget when truncating the content. The bar never wraps —
@@ -5085,9 +5930,15 @@ function EnterApiKeyStep({ descriptor, error, onSubmit }) {
5085
5930
  });
5086
5931
  }
5087
5932
  function OAuthRunningStep({ descriptor, dataDir, onSuccess, onError }) {
5933
+ const usesManualPaste = oauthUsesManualCodePaste(descriptor);
5088
5934
  const [url, setUrl] = useState(null);
5089
- const [status, setStatus] = useState("starting browser…");
5090
- const COLOR = useColors();
5935
+ const [status, setStatus] = useState(usesManualPaste ? "opening browser…" : "starting browser…");
5936
+ const [pending, setPending] = useState(null);
5937
+ const [pasteHint, setPasteHint] = useState(null);
5938
+ const focused = useModalAwareFocus();
5939
+ const inputRef = useRef(null);
5940
+ const pendingRef = useRef(null);
5941
+ pendingRef.current = pending;
5091
5942
  useEffect(() => {
5092
5943
  const ac = new AbortController();
5093
5944
  let cancelled = false;
@@ -5097,8 +5948,22 @@ function OAuthRunningStep({ descriptor, dataDir, onSuccess, onError }) {
5097
5948
  onUrl: (loginUrl) => {
5098
5949
  if (cancelled) return;
5099
5950
  setUrl(loginUrl);
5100
- setStatus("waiting for browser callback…");
5951
+ setStatus(usesManualPaste ? "complete the login in your browser, then paste the code below" : "waiting for browser callback…");
5101
5952
  },
5953
+ onPrompt: (prompt) => new Promise((resolve, reject) => {
5954
+ if (cancelled) {
5955
+ reject(/* @__PURE__ */ new Error("OAuth flow cancelled"));
5956
+ return;
5957
+ }
5958
+ pendingRef.current?.reject(/* @__PURE__ */ new Error("superseded by a newer OAuth prompt"));
5959
+ setPending({
5960
+ message: prompt.message,
5961
+ placeholder: prompt.placeholder,
5962
+ allowEmpty: prompt.allowEmpty,
5963
+ resolve,
5964
+ reject
5965
+ });
5966
+ }),
5102
5967
  onProgress: (message) => {
5103
5968
  if (!cancelled) setStatus(message);
5104
5969
  },
@@ -5117,31 +5982,70 @@ function OAuthRunningStep({ descriptor, dataDir, onSuccess, onError }) {
5117
5982
  })();
5118
5983
  return () => {
5119
5984
  cancelled = true;
5985
+ pendingRef.current?.reject(/* @__PURE__ */ new Error("OAuth flow cancelled"));
5986
+ pendingRef.current = null;
5120
5987
  ac.abort();
5121
5988
  };
5122
5989
  }, [
5123
5990
  descriptor,
5124
5991
  dataDir,
5125
5992
  onSuccess,
5126
- onError
5993
+ onError,
5994
+ usesManualPaste
5127
5995
  ]);
5996
+ const submitInput = useCallback(() => {
5997
+ const value = inputRef.current?.value?.trim() ?? "";
5998
+ const current = pendingRef.current;
5999
+ if (current) {
6000
+ if (!value && !current.allowEmpty) return;
6001
+ pendingRef.current = null;
6002
+ setPending(null);
6003
+ setStatus("exchanging code…");
6004
+ current.resolve(value);
6005
+ return;
6006
+ }
6007
+ if (!value) return;
6008
+ setPasteHint({
6009
+ text: "submitting redirect URL…",
6010
+ tone: "dim"
6011
+ });
6012
+ (async () => {
6013
+ try {
6014
+ const result = await fetchOAuthRedirect(value);
6015
+ if (result.status >= 200 && result.status < 300) {
6016
+ setPasteHint({
6017
+ text: "redirect accepted — exchanging code…",
6018
+ tone: "accent"
6019
+ });
6020
+ setStatus("exchanging code…");
6021
+ } else setPasteHint({
6022
+ text: `callback server rejected the URL (${result.status}${result.message ? `: ${result.message}` : ""}). Try again or restart the flow.`,
6023
+ tone: "error"
6024
+ });
6025
+ } catch (err) {
6026
+ setPasteHint({
6027
+ text: errorMessage(err),
6028
+ tone: "error"
6029
+ });
6030
+ }
6031
+ })();
6032
+ }, []);
5128
6033
  return /* @__PURE__ */ jsxs(WizardPanel, {
5129
6034
  title: `configure ${descriptor.label} — OAuth`,
5130
6035
  children: [
5131
6036
  /* @__PURE__ */ jsx(WizardEscHint, {}),
5132
- /* @__PURE__ */ jsx(Spinner, { label: status }),
5133
- url && /* @__PURE__ */ jsxs("box", {
5134
- style: {
5135
- flexDirection: "column",
5136
- gap: 0
5137
- },
5138
- children: [/* @__PURE__ */ jsx("text", {
5139
- fg: COLOR.dim,
5140
- children: "If the browser didn't open, visit:"
5141
- }), /* @__PURE__ */ jsx("text", {
5142
- fg: COLOR.model,
5143
- children: url
5144
- })]
6037
+ pending ? /* @__PURE__ */ jsx(Spinner, { label: pending.message }) : /* @__PURE__ */ jsx(Spinner, { label: status }),
6038
+ url && /* @__PURE__ */ jsx(OAuthAuthBlock, {
6039
+ authUrl: url,
6040
+ maxLineWidth: 200,
6041
+ chromeWidth: 6,
6042
+ paste: {
6043
+ inputRef,
6044
+ focused,
6045
+ onSubmit: submitInput,
6046
+ placeholder: pending?.placeholder ?? "paste redirect URL (or code) and press enter…",
6047
+ hint: pasteHint ?? void 0
6048
+ }
5145
6049
  })
5146
6050
  ]
5147
6051
  });
@@ -5513,7 +6417,61 @@ function renderProjectLabel(rowProject, currentProject, COLOR) {
5513
6417
  /** Visible content lines: 1 minimum, 5 maximum (textarea scrolls past 5). */
5514
6418
  const MIN_CONTENT_LINES = 1;
5515
6419
  const MAX_CONTENT_LINES = 5;
5516
- 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
+ };
5517
6475
  /**
5518
6476
  * Stable empty-array reference — keeps `queuedMessages` default referentially
5519
6477
  * stable across renders so memoized children don't bust their deps. Declared
@@ -5521,10 +6479,10 @@ const CWD_DISPLAY = compactPath(process.cwd());
5521
6479
  * default-value reference at the prop destructure.
5522
6480
  */
5523
6481
  const EMPTY_QUEUED_MESSAGES = [];
5524
- 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 }) {
5525
6483
  const COLOR = useColors();
5526
6484
  const titleText = session?.title ?? "untitled";
5527
- const showSessionShortcut = !!session && !busy && !pending && !pendingInteraction;
6485
+ const showSessionShortcut = !!session && !busy && !pending && !pendingInteraction && settings.uiMode !== "minimal";
5528
6486
  const userMessageCount = useMemo(() => events.filter((e) => e.kind === "user-prompt").length, [events]);
5529
6487
  const { width: termWidth } = useTerminalDimensions();
5530
6488
  const hasStatusIcon = busy || compacting;
@@ -5560,7 +6518,7 @@ function ChatScreen({ events, busy, compacting = false, queuedMessages = EMPTY_Q
5560
6518
  const statsLen = segments.reduce((sum, s) => sum + s.text.length, 0);
5561
6519
  const cwdBudget = Math.max(0, termWidth - 4 - titleText.length - iconReserve - OVERLAY_RESERVED - statsLen - 3);
5562
6520
  if (cwdBudget >= 6) segments.unshift({
5563
- text: compactPath(CWD_DISPLAY, cwdBudget),
6521
+ text: compactPath(cwd, cwdBudget),
5564
6522
  color: COLOR.dim
5565
6523
  }, {
5566
6524
  text: " · ",
@@ -5568,6 +6526,7 @@ function ChatScreen({ events, busy, compacting = false, queuedMessages = EMPTY_Q
5568
6526
  });
5569
6527
  return segments;
5570
6528
  }, [
6529
+ cwd,
5571
6530
  session,
5572
6531
  userMessageCount,
5573
6532
  COLOR,
@@ -5586,8 +6545,8 @@ function ChatScreen({ events, busy, compacting = false, queuedMessages = EMPTY_Q
5586
6545
  const modal = useModal();
5587
6546
  useEffect(() => {
5588
6547
  if (!fileEditPending) return;
5589
- modal.open(/* @__PURE__ */ jsx(Fragment, {}));
5590
- return () => modal.close();
6548
+ modal.lock();
6549
+ return () => modal.unlock();
5591
6550
  }, [fileEditPending, modal]);
5592
6551
  return /* @__PURE__ */ jsxs("box", {
5593
6552
  style: {
@@ -5946,44 +6905,16 @@ function QueuedMessagesBlock({ messages, selectionIndex, shortcuts }) {
5946
6905
  function previewText(text) {
5947
6906
  return text.replace(/\n+/g, " ↵ ");
5948
6907
  }
5949
- const KEY_GLYPHS = {
5950
- up: "↑",
5951
- down: "↓",
5952
- left: "←",
5953
- right: "→",
5954
- return: "↵",
5955
- enter: "↵",
5956
- delete: "⌫",
5957
- backspace: "⌫",
5958
- escape: "esc",
5959
- space: "␣",
5960
- tab: "⇥"
5961
- };
5962
- /**
5963
- * Render a binding spec (`"ctrl+return"`, `"backspace"`, `"delete"`) as a
5964
- * compact display string the user recognizes at a glance. Substitutes
5965
- * arrow / enter / backspace glyphs for their verbose names so the hint
5966
- * row stays narrow; modifier names + plain keys pass through unchanged.
5967
- *
5968
- * Kept pure / local: every consumer in this file would otherwise embed
5969
- * the same `replace` chain, and a future `keybindings.json` override
5970
- * (e.g. `"backspace"` for drop) renders correctly without code edits.
5971
- */
5972
- function formatBindingForDisplay(spec) {
5973
- const segments = spec.toLowerCase().split("+");
5974
- const key = segments.pop() ?? "";
5975
- const modifiers = segments.join("+");
5976
- const glyph = KEY_GLYPHS[key] ?? key;
5977
- return modifiers ? `${modifiers}+${glyph}` : glyph;
5978
- }
5979
6908
  /** Stable empty providers reference — avoids `useCompletion` rerun on every render when no providers are wired. */
5980
6909
  const EMPTY_PROVIDERS = [];
5981
6910
  function PromptBlock({ userPrompts, onSubmit, completionProviders, onPopupOpenChange, selectMode = null, triggerHints, busy = false, onEnterQueueFromEmpty }) {
5982
6911
  const focused = useModalAwareFocus();
5983
6912
  const COLOR = useColors();
6913
+ const SURFACE = useSurfaces();
5984
6914
  const textareaRef = useRef(null);
5985
6915
  /** Auto-grow: visible content rows the textarea currently occupies (clamped 1..5). */
5986
6916
  const [contentLines, setContentLines] = useState(MIN_CONTENT_LINES);
6917
+ const [attachments, setAttachments] = useState([]);
5987
6918
  /**
5988
6919
  * Mirror of the textarea buffer + cursor, updated on every `onContentChange`.
5989
6920
  * Drives `useCompletion` — the textarea stays uncontrolled (we don't push
@@ -6023,8 +6954,8 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, onPopupOpenCh
6023
6954
  }, []);
6024
6955
  const submit = useCallback(() => {
6025
6956
  const value = textareaRef.current?.plainText ?? "";
6026
- if (!value.trim()) return;
6027
- onSubmit(value, completion.references);
6957
+ if (!value.trim() && attachments.length === 0) return;
6958
+ onSubmit(value, completion.references, attachments);
6028
6959
  textareaRef.current?.clear();
6029
6960
  historyRef.current = null;
6030
6961
  setBufferState({
@@ -6032,7 +6963,12 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, onPopupOpenCh
6032
6963
  cursor: 0
6033
6964
  });
6034
6965
  setContentLines(MIN_CONTENT_LINES);
6035
- }, [onSubmit, completion.references]);
6966
+ setAttachments([]);
6967
+ }, [
6968
+ onSubmit,
6969
+ completion.references,
6970
+ attachments
6971
+ ]);
6036
6972
  const commitCompletion = useCallback(() => {
6037
6973
  const result = completion.commit();
6038
6974
  if (!result) return false;
@@ -6064,6 +7000,32 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, onPopupOpenCh
6064
7000
  if (!ta) return;
6065
7001
  const normalized = stripAnsiSequences(decodePasteBytes(event.bytes)).replace(/\r\n?/g, "\n");
6066
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
+ }
6067
7029
  const parts = normalized.split("\n");
6068
7030
  for (let i = 0; i < parts.length; i++) {
6069
7031
  if (i > 0) ta.newLine();
@@ -6143,7 +7105,15 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, onPopupOpenCh
6143
7105
  const buffer = textareaRef.current;
6144
7106
  if (!buffer) return;
6145
7107
  if (event.name === "up") {
6146
- 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
+ }
6147
7117
  event.preventDefault();
6148
7118
  if (buffer.plainText.length === 0 && onEnterQueueFromEmpty?.()) return;
6149
7119
  cycleHistory(-1);
@@ -6157,7 +7127,8 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, onPopupOpenCh
6157
7127
  completion,
6158
7128
  commitCompletion,
6159
7129
  cycleHistory,
6160
- onEnterQueueFromEmpty
7130
+ onEnterQueueFromEmpty,
7131
+ syncBuffer
6161
7132
  ]);
6162
7133
  const boxHeight = Math.min(MAX_CONTENT_LINES, contentLines) + 2;
6163
7134
  return /* @__PURE__ */ jsxs("box", {
@@ -6170,36 +7141,71 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, onPopupOpenCh
6170
7141
  flexDirection: "column",
6171
7142
  flexShrink: 0
6172
7143
  },
6173
- children: [/* @__PURE__ */ jsx("box", {
6174
- style: {
6175
- border: true,
6176
- borderColor: selectMode ? COLOR.warn : COLOR.borderActive,
6177
- paddingLeft: 1,
6178
- paddingRight: 1,
6179
- height: boxHeight,
6180
- flexDirection: "column"
6181
- },
6182
- children: /* @__PURE__ */ jsx("textarea", {
6183
- ref: textareaRef,
6184
- focused: focused && !selectMode,
6185
- keyBindings: TEXTAREA_BINDINGS,
6186
- wrapMode: "word",
6187
- placeholder: selectMode === "turn" ? "— turn-select mode — press ⎋ to resume typing —" : selectMode === "queue" ? "— queue-select mode — press ⎋ to resume typing —" : "Ask zidane…",
6188
- syntaxStyle: chipStyle,
7144
+ children: [
7145
+ attachments.length > 0 && /* @__PURE__ */ jsx("box", {
6189
7146
  style: {
6190
- flexGrow: 1,
6191
- height: "100%"
7147
+ flexDirection: "row",
7148
+ flexWrap: "wrap",
7149
+ paddingLeft: 1,
7150
+ paddingRight: 1,
7151
+ paddingBottom: 0
6192
7152
  },
6193
- onSubmit: submit,
6194
- onContentChange: syncBuffer,
6195
- onKeyDown,
6196
- onPaste
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"
7185
+ },
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
6197
7207
  })
6198
- }), /* @__PURE__ */ jsx(PromptHints, {
6199
- selectMode,
6200
- triggerHints,
6201
- busy
6202
- })]
7208
+ ]
6203
7209
  }), !selectMode && /* @__PURE__ */ jsx("box", {
6204
7210
  style: {
6205
7211
  position: "absolute",
@@ -6303,11 +7309,13 @@ const PROMPT_HINTS_SELECT_QUEUE = [{
6303
7309
  */
6304
7310
  function PromptHints({ selectMode, triggerHints, busy = false }) {
6305
7311
  const COLOR = useColors();
7312
+ const { settings } = useSettings();
6306
7313
  const { width: termWidth } = useTerminalDimensions();
6307
- 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;
6308
7316
  const hints = useMemo(() => {
6309
7317
  const budget = Math.max(0, termWidth - 5);
6310
- 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;
6311
7319
  return clipHintsToWidth(selectMode || !triggerHints || triggerHints.length === 0 || tooLongCombined ? primary : [...primary, ...triggerHints], budget);
6312
7320
  }, [
6313
7321
  selectMode,
@@ -7106,7 +8114,13 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
7106
8114
  ]);
7107
8115
  const focusedMcp = filteredMcps[cursor];
7108
8116
  const focusedMcpStatus = focusedMcp ? getMcpAuthStatus(authState, focusedMcp.config.name) : void 0;
8117
+ const oauthPasteActive = activeTab === "mcps" && focusedMcpStatus?.kind === "authorizing" && !!focusedMcpStatus.url;
7109
8118
  useKeyboard((key) => {
8119
+ if (key.name === "escape" && oauthPasteActive && focusedMcp) {
8120
+ actions?.onCancelLoginMcp?.(focusedMcp.config.name);
8121
+ return;
8122
+ }
8123
+ if (oauthPasteActive) return;
7110
8124
  if (!key.ctrl && !key.meta && !key.shift && (key.name === "left" || key.name === "right")) {
7111
8125
  key.preventDefault();
7112
8126
  const idx = TAB_ORDER.indexOf(activeTab);
@@ -7203,7 +8217,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
7203
8217
  },
7204
8218
  children: /* @__PURE__ */ jsx("input", {
7205
8219
  ref: inputRef,
7206
- focused: true,
8220
+ focused: !oauthPasteActive,
7207
8221
  placeholder: searchPlaceholder(activeTab),
7208
8222
  onInput: handleQueryChange,
7209
8223
  onSubmit: () => {},
@@ -7623,35 +8637,29 @@ function EmptyRow({ label }) {
7623
8637
  });
7624
8638
  }
7625
8639
  function renderMcpDetailPanel(entry, status, COLOR) {
7626
- if (status.kind === "authorizing") return /* @__PURE__ */ jsxs("box", {
7627
- style: {
7628
- flexDirection: "column",
7629
- border: ["top"],
7630
- borderColor: COLOR.border,
7631
- paddingTop: 1
7632
- },
7633
- children: [
7634
- /* @__PURE__ */ jsx("text", {
8640
+ if (status.kind === "authorizing") {
8641
+ if (!status.url) return /* @__PURE__ */ jsxs("box", {
8642
+ style: {
8643
+ flexDirection: "column",
8644
+ border: ["top"],
8645
+ borderColor: COLOR.border,
8646
+ paddingTop: 1
8647
+ },
8648
+ children: [/* @__PURE__ */ jsx("text", {
7635
8649
  fg: COLOR.brand,
7636
8650
  children: `Authorizing ${entry.config.name}`
7637
- }),
7638
- /* @__PURE__ */ jsx("text", {
7639
- fg: COLOR.dim,
7640
- children: "Your browser should have opened — complete the login, then return here."
7641
- }),
7642
- status.url && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("text", {
7643
- fg: COLOR.mute,
7644
- children: "If it didn't, click the URL below (or copy it):"
7645
8651
  }), /* @__PURE__ */ jsx("text", {
7646
- wrapMode: "char",
7647
- fg: COLOR.model,
7648
- children: /* @__PURE__ */ jsx("a", {
7649
- href: status.url,
7650
- children: status.url
7651
- })
7652
- })] })
7653
- ]
7654
- });
8652
+ fg: COLOR.dim,
8653
+ children: "Starting login flow…"
8654
+ })]
8655
+ });
8656
+ return /* @__PURE__ */ jsx(McpAuthorizingPanel, {
8657
+ serverName: entry.config.name,
8658
+ authUrl: status.url,
8659
+ maxLineWidth: 134,
8660
+ inputFocused: true
8661
+ });
8662
+ }
7655
8663
  if (status.kind === "error") return /* @__PURE__ */ jsxs("box", {
7656
8664
  style: {
7657
8665
  flexDirection: "column",
@@ -8591,11 +9599,28 @@ function AppShell() {
8591
9599
  useEffect(() => {
8592
9600
  autoCompactThresholdRef.current = settings.autoCompactThreshold;
8593
9601
  }, [settings.autoCompactThreshold]);
9602
+ /**
9603
+ * Post-compaction baseline for the hysteresis check in
9604
+ * {@link shouldAutoCompact}. Latched to the effective post-compact token
9605
+ * count whenever a compaction lands (see {@link onCompactSession}), reset
9606
+ * to `undefined` on session change so the first compaction in a fresh
9607
+ * session fires off the absolute threshold without any growth gating.
9608
+ * Pairs with {@link AUTO_COMPACT_MIN_GROWTH_FRACTION} in the predicate.
9609
+ */
9610
+ const lastCompactedInputTokensRef = useRef(void 0);
8594
9611
  const smoothStreamingRef = useRef(settings.smoothStreaming);
8595
9612
  useEffect(() => {
8596
9613
  smoothStreamingRef.current = settings.smoothStreaming;
8597
9614
  }, [settings.smoothStreaming]);
8598
- 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
+ }, []);
8599
9624
  const safelistRef = useRef(null);
8600
9625
  const readSafelist = useCallback(() => {
8601
9626
  if (safelistRef.current === null) safelistRef.current = getSafelist(dataDir, projectDir);
@@ -8627,9 +9652,8 @@ function AppShell() {
8627
9652
  const ensureSkillsCatalogRef = useRef(ensureSkillsCatalog);
8628
9653
  ensureSkillsCatalogRef.current = ensureSkillsCatalog;
8629
9654
  const formatFilePathForCwd = useMemo(() => {
8630
- const cwd = process.cwd();
8631
9655
  return (entry) => formatPathForCwd(entry.path, projectDir, cwd);
8632
- }, [projectDir]);
9656
+ }, [projectDir, cwd]);
8633
9657
  const completionProviders = useMemo(() => [createSkillsCompletionProvider({
8634
9658
  getCatalog: () => skillsCatalogRef.current,
8635
9659
  getEnabled: () => enabledSkillsRef.current,
@@ -8798,6 +9822,25 @@ function AppShell() {
8798
9822
  queueSelectionIndexRef.current = queueSelectionIndex;
8799
9823
  const agentRef = useRef(null);
8800
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]);
8801
9844
  /**
8802
9845
  * Live registry of in-flight tool calls — populated by `tool:before`
8803
9846
  * (and `child:tool:before`), drained by `tool:after` / `tool:error` /
@@ -8943,7 +9986,7 @@ function AppShell() {
8943
9986
  });
8944
9987
  const allowInteraction = allowInteractionRef.current !== false;
8945
9988
  const interactionTools = allowInteraction ? createInteractionTools({ requestInteraction: makeRequestInteraction(interactions) }) : {};
8946
- const actualCwd = process.cwd();
9989
+ const actualCwd = cwdRef.current;
8947
9990
  const envOpts = {
8948
9991
  cwd: actualCwd,
8949
9992
  ...projectDir !== actualCwd ? { projectRoot: projectDir } : {},
@@ -8979,6 +10022,7 @@ function AppShell() {
8979
10022
  ...persistBehavior,
8980
10023
  tasksDir
8981
10024
  },
10025
+ execution: createProcessContext({ cwd: actualCwd }),
8982
10026
  provider: descriptor.factory(),
8983
10027
  session,
8984
10028
  mcpConnector: (configs) => connectMcpServers(configs, void 0, agent.hooks, { buildAuthProvider: (cfg) => new McpOAuthProvider({
@@ -9470,6 +10514,7 @@ function AppShell() {
9470
10514
  const replayedTokens = lastContextSizeFromTurns(session.turns, session.runs);
9471
10515
  setLastInputTokens(replayedTokens);
9472
10516
  lastInputTokensRef.current = replayedTokens;
10517
+ lastCompactedInputTokensRef.current = void 0;
9473
10518
  setSessionCost(sumRunCosts(session.runs));
9474
10519
  setCurrentSession({
9475
10520
  id: session.id,
@@ -9802,7 +10847,7 @@ function AppShell() {
9802
10847
  * busy-state lifecycle so the title spinner stays mounted continuously
9803
10848
  * across back-to-back queued messages.
9804
10849
  */
9805
- const runSingleMessage = useCallback(async (prompt, references) => {
10850
+ const runSingleMessage = useCallback(async (prompt, references, attachments) => {
9806
10851
  const agent = agentRef.current;
9807
10852
  const session = sessionRef.current;
9808
10853
  if (!agent || !session || !picked) return;
@@ -9815,10 +10860,16 @@ function AppShell() {
9815
10860
  end: r.end,
9816
10861
  providerId: r.providerId
9817
10862
  }));
10863
+ const attachmentMeta = attachments.map((a) => ({
10864
+ name: a.name,
10865
+ mediaType: a.mediaType,
10866
+ size: a.content.length
10867
+ }));
9818
10868
  stream.appendImmediate({
9819
10869
  kind: "user-prompt",
9820
10870
  text: prompt,
9821
- ...refSpans.length > 0 ? { refs: refSpans } : {}
10871
+ ...refSpans.length > 0 ? { refs: refSpans } : {},
10872
+ ...attachmentMeta.length > 0 ? { attachments: attachmentMeta } : {}
9822
10873
  });
9823
10874
  if (autoCompactInFlightRef.current) await autoCompactInFlightRef.current.catch(() => {});
9824
10875
  const newRefs = uniqueSkillNamesFromReferences(references);
@@ -9829,9 +10880,34 @@ function AppShell() {
9829
10880
  debugLog(`activateSkill("${name}")`, err);
9830
10881
  }
9831
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
+ })];
9832
10908
  await agent.run({
9833
10909
  model: picked.model,
9834
- prompt,
10910
+ prompt: runPrompt,
9835
10911
  ...picked.effort ? { thinking: picked.effort } : {}
9836
10912
  });
9837
10913
  await session.save().catch((err) => debugLog("session.save failed", err));
@@ -9853,15 +10929,16 @@ function AppShell() {
9853
10929
  stream.flushAndUpdate(finalizeStreamingMarkdown);
9854
10930
  }
9855
10931
  }, [picked, stream]);
9856
- const onSubmitPrompt = useCallback((prompt, references) => {
9857
- if (!prompt.trim()) return;
10932
+ const onSubmitPrompt = useCallback((prompt, references, attachments) => {
10933
+ if (!prompt.trim() && attachments.length === 0) return;
9858
10934
  const agent = agentRef.current;
9859
10935
  const session = sessionRef.current;
9860
10936
  if (!agent || !session || !picked) return;
9861
10937
  if (runningRef.current) {
9862
10938
  messageQueueRef.current = [...messageQueueRef.current, {
9863
10939
  prompt,
9864
- references
10940
+ references,
10941
+ attachments
9865
10942
  }];
9866
10943
  setMessageQueue(messageQueueRef.current.slice());
9867
10944
  return;
@@ -9870,7 +10947,7 @@ function AppShell() {
9870
10947
  (async () => {
9871
10948
  setBusy(true);
9872
10949
  try {
9873
- await runSingleMessage(prompt, references);
10950
+ await runSingleMessage(prompt, references, attachments);
9874
10951
  while (messageQueueRef.current.length > 0) {
9875
10952
  const next = messageQueueRef.current[0];
9876
10953
  messageQueueRef.current = messageQueueRef.current.slice(1);
@@ -9881,7 +10958,7 @@ function AppShell() {
9881
10958
  if (prev <= 0) return null;
9882
10959
  return prev - 1;
9883
10960
  });
9884
- await runSingleMessage(next.prompt, next.references);
10961
+ await runSingleMessage(next.prompt, next.references, next.attachments);
9885
10962
  }
9886
10963
  } finally {
9887
10964
  setBusy(false);
@@ -10048,6 +11125,7 @@ function AppShell() {
10048
11125
  const replayedTokens = lastContextSizeFromTurns(session.turns, session.runs);
10049
11126
  setLastInputTokens(replayedTokens);
10050
11127
  lastInputTokensRef.current = replayedTokens;
11128
+ lastCompactedInputTokensRef.current = void 0;
10051
11129
  setSessionCost(sumRunCosts(session.runs));
10052
11130
  setCurrentSession((prev) => prev ? {
10053
11131
  ...prev,
@@ -10085,6 +11163,7 @@ function AppShell() {
10085
11163
  const replayedTokens = lastContextSizeFromTurns(session.turns, session.runs);
10086
11164
  setLastInputTokens(replayedTokens);
10087
11165
  lastInputTokensRef.current = replayedTokens;
11166
+ lastCompactedInputTokensRef.current = void 0;
10088
11167
  setCurrentSession((prev) => prev ? {
10089
11168
  ...prev,
10090
11169
  updatedAt: Date.now()
@@ -10241,6 +11320,7 @@ function AppShell() {
10241
11320
  setEvents(eventsFromTurns(liveSession.turns, liveSession.runs));
10242
11321
  setLastInputTokens(effectiveTokens);
10243
11322
  lastInputTokensRef.current = effectiveTokens;
11323
+ lastCompactedInputTokensRef.current = effectiveTokens;
10244
11324
  }
10245
11325
  setCurrentSession((prev) => prev && prev.id === sessionId ? {
10246
11326
  ...prev,
@@ -10283,7 +11363,9 @@ function AppShell() {
10283
11363
  threshold: autoCompactThresholdRef.current,
10284
11364
  inputTokens: lastInputTokensRef.current,
10285
11365
  rawContextWindow: rawWindow,
10286
- alreadyCompacting: !!autoCompactInFlightRef.current
11366
+ alreadyCompacting: !!autoCompactInFlightRef.current,
11367
+ ...lastCompactedInputTokensRef.current !== void 0 ? { lastCompactedInputTokens: lastCompactedInputTokensRef.current } : {},
11368
+ minGrowthFraction: AUTO_COMPACT_MIN_GROWTH_FRACTION
10287
11369
  });
10288
11370
  if (decision.kind !== "fire") return;
10289
11371
  const pct = Math.round(decision.usedFraction * 100);
@@ -10502,6 +11584,15 @@ function AppShell() {
10502
11584
  }));
10503
11585
  return;
10504
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
+ }
10505
11596
  if (matchesBinding(key, keybindings.enterSelectTurnMode) && screen === "chat" && !busy && !pendingApproval && !pendingInteraction) {
10506
11597
  enterSelectMode();
10507
11598
  return;
@@ -10533,6 +11624,16 @@ function AppShell() {
10533
11624
  }));
10534
11625
  return;
10535
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
+ }
10536
11637
  if (key.name !== "escape") return;
10537
11638
  if (busy || pendingApproval) return onAbort();
10538
11639
  if (popupOpenRef.current) return;
@@ -10567,6 +11668,7 @@ function AppShell() {
10567
11668
  pendingInteractionResumed: pendingInteractionIsResumed,
10568
11669
  currentSession,
10569
11670
  hasMultipleAgents,
11671
+ uiMode: settings.uiMode,
10570
11672
  modelLabel: picked?.model ?? null,
10571
11673
  modelColor: COLOR.model,
10572
11674
  effortLabel: modelHasReasoning ? picked?.effort ?? "medium" : null,
@@ -10587,6 +11689,7 @@ function AppShell() {
10587
11689
  pendingInteractionIsResumed,
10588
11690
  currentSession,
10589
11691
  hasMultipleAgents,
11692
+ settings.uiMode,
10590
11693
  picked,
10591
11694
  pickedAgent,
10592
11695
  COLOR,
@@ -10666,6 +11769,7 @@ function AppShell() {
10666
11769
  currentProjectRoot: projectDir
10667
11770
  }),
10668
11771
  screen === "chat" && /* @__PURE__ */ jsx(ChatScreen, {
11772
+ cwd,
10669
11773
  events,
10670
11774
  busy,
10671
11775
  compacting,
@@ -10705,162 +11809,6 @@ function effortForModel(descriptor, modelId, remembered) {
10705
11809
  if (!modelSupportsReasoning(descriptor, modelId)) return void 0;
10706
11810
  return remembered?.[modelId] ?? "medium";
10707
11811
  }
10708
- /**
10709
- * Build the footer's shortcut hints for the current screen. On the chat
10710
- * screen the model id rides next to its `ctrl+m` shortcut and the agent
10711
- * label rides next to `shift+tab`, each in its accent color — the bar
10712
- * doubles as the status display without needing separate badges. When
10713
- * the active model exposes reasoning, the `ctrl+m` hint grows a
10714
- * secondary `/n` chord with the current effort label, surfacing the
10715
- * effort picker as a discoverable, in-place affordance.
10716
- */
10717
- function buildHints({ screen, busy, pending, pendingInteractionLive, pendingInteractionResumed, currentSession, hasMultipleAgents, modelLabel, modelColor, effortLabel, effortColor, effortKeyColor, agentLabel, agentColor, keybindings, inFlightToolCount, activeSkillCount, skillsChipColor, updateHint }) {
10718
- if (pending) return [
10719
- {
10720
- key: "↑↓",
10721
- label: "navigate"
10722
- },
10723
- {
10724
- key: "↵",
10725
- label: "select"
10726
- },
10727
- {
10728
- key: "esc",
10729
- label: "abort run"
10730
- }
10731
- ];
10732
- if (pendingInteractionLive) return [
10733
- {
10734
- key: "↑↓",
10735
- label: "navigate"
10736
- },
10737
- {
10738
- key: "↵",
10739
- label: "select"
10740
- },
10741
- {
10742
- key: "esc",
10743
- label: "abort run"
10744
- }
10745
- ];
10746
- if (pendingInteractionResumed) return [
10747
- {
10748
- key: "↑↓",
10749
- label: "navigate"
10750
- },
10751
- {
10752
- key: "↵",
10753
- label: "select"
10754
- },
10755
- {
10756
- key: "esc",
10757
- label: "leave for later"
10758
- }
10759
- ];
10760
- if (busy) {
10761
- const baseBusyHints = [];
10762
- if (inFlightToolCount > 0) baseBusyHints.push({
10763
- key: keybindings.cancelToolCall,
10764
- label: inFlightToolCount === 1 ? "cancel" : `cancel (${inFlightToolCount})`
10765
- });
10766
- baseBusyHints.push({
10767
- key: "esc",
10768
- label: "abort"
10769
- });
10770
- return baseBusyHints;
10771
- }
10772
- if (screen === "auth") return [
10773
- {
10774
- key: "↑↓",
10775
- label: "navigate"
10776
- },
10777
- {
10778
- key: "↵",
10779
- label: "select"
10780
- },
10781
- {
10782
- key: "esc",
10783
- label: "exit"
10784
- }
10785
- ];
10786
- if (screen === "sessions") return [
10787
- {
10788
- key: "↑↓",
10789
- label: "navigate"
10790
- },
10791
- {
10792
- key: "↵",
10793
- label: "open"
10794
- },
10795
- {
10796
- key: keybindings.openSessionDetails,
10797
- label: "session"
10798
- },
10799
- {
10800
- key: keybindings.openSettings,
10801
- label: "settings"
10802
- },
10803
- {
10804
- key: "esc",
10805
- label: currentSession ? "back" : "exit"
10806
- }
10807
- ];
10808
- const modelHint = modelLabel ? {
10809
- key: keybindings.openModelPicker,
10810
- label: modelLabel,
10811
- labelColor: modelColor,
10812
- ...effortLabel ? { extra: {
10813
- key: shortChord(keybindings.openEffortPicker),
10814
- keyColor: effortKeyColor,
10815
- label: effortLabel,
10816
- labelColor: effortColor
10817
- } } : {}
10818
- } : null;
10819
- const skillsChip = activeSkillCount > 0 ? {
10820
- key: "✦",
10821
- keyColor: skillsChipColor,
10822
- label: activeSkillCount === 1 ? "1 skill" : `${activeSkillCount} skills`,
10823
- labelColor: skillsChipColor
10824
- } : null;
10825
- const cancelTaskChip = inFlightToolCount > 0 ? {
10826
- key: keybindings.cancelToolCall,
10827
- label: inFlightToolCount === 1 ? "cancel task" : `cancel task (${inFlightToolCount})`
10828
- } : null;
10829
- return [
10830
- ...hasMultipleAgents ? [{
10831
- key: keybindings.cycleAgent,
10832
- label: agentLabel,
10833
- labelColor: agentColor
10834
- }] : [],
10835
- ...modelHint ? [modelHint] : [],
10836
- ...skillsChip ? [skillsChip] : [],
10837
- ...cancelTaskChip ? [cancelTaskChip] : [],
10838
- ...currentSession ? [{
10839
- key: keybindings.openSessionDetails,
10840
- label: "session"
10841
- }] : [],
10842
- {
10843
- key: keybindings.openSettings,
10844
- label: "settings"
10845
- },
10846
- {
10847
- key: "esc",
10848
- label: "sessions"
10849
- },
10850
- ...updateHint ? [updateHint] : []
10851
- ];
10852
- }
10853
- /**
10854
- * Shorten a binding spec for display as a "chord continuation" — the
10855
- * `/n` after `ctrl+m model` in the chat footer. We only strip the
10856
- * `ctrl+` prefix so user-customized chords (`alt+n`, `meta+shift+n`)
10857
- * still render in full. Falls back to the verbatim spec when no
10858
- * `ctrl+` prefix is found, which keeps the visual contract honest:
10859
- * the rendered key always matches the bound trigger.
10860
- */
10861
- function shortChord(spec) {
10862
- return spec.startsWith("ctrl+") ? `/${spec.slice(5)}` : spec;
10863
- }
10864
11812
  //#endregion
10865
11813
  //#region src/tui/tree-sitter.ts
10866
11814
  /**
@@ -11104,7 +12052,18 @@ function McpsSettingsModal({ catalog, errors, onLogin, onLogout, onCancelLogin,
11104
12052
  return ((prev + delta) % catalog.length + catalog.length) % catalog.length;
11105
12053
  }), [catalog.length]);
11106
12054
  const safeCursor = Math.min(cursor, Math.max(0, catalog.length - 1));
12055
+ const focusedEntry = catalog[safeCursor];
12056
+ const focusedStatus = focusedEntry ? getMcpAuthStatus(authState, focusedEntry.config.name) : void 0;
12057
+ const inputActive = focusedStatus?.kind === "authorizing" && !!focusedStatus.url;
11107
12058
  useKeyboard((key) => {
12059
+ if (key.name === "escape" && focusedEntry) {
12060
+ const name = focusedEntry.config.name;
12061
+ if (getMcpAuthStatus(authState, name).kind === "authorizing") {
12062
+ onCancelLogin(name);
12063
+ return;
12064
+ }
12065
+ }
12066
+ if (inputActive) return;
11108
12067
  if (key.name === "up" || key.name === "k" || key.ctrl && key.name === "p") {
11109
12068
  moveCursor(-1);
11110
12069
  return;
@@ -11118,10 +12077,6 @@ function McpsSettingsModal({ catalog, errors, onLogin, onLogout, onCancelLogin,
11118
12077
  if (!entry) return;
11119
12078
  const name = entry.config.name;
11120
12079
  const status = getMcpAuthStatus(authState, name);
11121
- if (key.name === "escape" && status.kind === "authorizing") {
11122
- onCancelLogin(name);
11123
- return;
11124
- }
11125
12080
  if (key.name === "return" || key.name === "space") {
11126
12081
  toggle(name);
11127
12082
  return;
@@ -11209,8 +12164,6 @@ function McpsSettingsModal({ catalog, errors, onLogin, onLogout, onCancelLogin,
11209
12164
  })
11210
12165
  ]
11211
12166
  });
11212
- const focusedEntry = catalog[safeCursor];
11213
- const focusedStatus = focusedEntry ? getMcpAuthStatus(authState, focusedEntry.config.name) : void 0;
11214
12167
  return /* @__PURE__ */ jsxs(Modal, {
11215
12168
  title: ` mcp servers · ${enabledSet.size} / ${catalog.length} enabled `,
11216
12169
  children: [
@@ -11300,35 +12253,29 @@ function renderInlineBadge(status, COLOR) {
11300
12253
  * itself to the content automatically).
11301
12254
  */
11302
12255
  function renderDetailPanel(entry, status, COLOR) {
11303
- if (status.kind === "authorizing") return /* @__PURE__ */ jsxs("box", {
11304
- style: {
11305
- flexDirection: "column",
11306
- border: ["top"],
11307
- borderColor: COLOR.border,
11308
- paddingTop: 1
11309
- },
11310
- children: [
11311
- /* @__PURE__ */ jsx("text", {
12256
+ if (status.kind === "authorizing") {
12257
+ if (!status.url) return /* @__PURE__ */ jsxs("box", {
12258
+ style: {
12259
+ flexDirection: "column",
12260
+ border: ["top"],
12261
+ borderColor: COLOR.border,
12262
+ paddingTop: 1
12263
+ },
12264
+ children: [/* @__PURE__ */ jsx("text", {
11312
12265
  fg: COLOR.brand,
11313
12266
  children: `Authorizing ${entry.config.name}`
11314
- }),
11315
- /* @__PURE__ */ jsx("text", {
11316
- fg: COLOR.dim,
11317
- children: "Your browser should have opened — complete the login, then return here."
11318
- }),
11319
- status.url && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("text", {
11320
- fg: COLOR.mute,
11321
- children: "If it didn't, click the URL below (or copy it):"
11322
12267
  }), /* @__PURE__ */ jsx("text", {
11323
- wrapMode: "char",
11324
- fg: COLOR.model,
11325
- children: /* @__PURE__ */ jsx("a", {
11326
- href: status.url,
11327
- children: status.url
11328
- })
11329
- })] })
11330
- ]
11331
- });
12268
+ fg: COLOR.dim,
12269
+ children: "Starting login flow…"
12270
+ })]
12271
+ });
12272
+ return /* @__PURE__ */ jsx(McpAuthorizingPanel, {
12273
+ serverName: entry.config.name,
12274
+ authUrl: status.url,
12275
+ maxLineWidth: 86,
12276
+ inputFocused: true
12277
+ });
12278
+ }
11332
12279
  if (status.kind === "error") return /* @__PURE__ */ jsxs("box", {
11333
12280
  style: {
11334
12281
  flexDirection: "column",