zidane 5.6.5 → 5.6.8

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 (59) hide show
  1. package/dist/{agent-D70rr6Uk.d.ts → agent-CZEvtmJk.d.ts} +45 -2
  2. package/dist/agent-CZEvtmJk.d.ts.map +1 -0
  3. package/dist/chat.d.ts +261 -5
  4. package/dist/chat.d.ts.map +1 -1
  5. package/dist/chat.js +3 -3
  6. package/dist/{index-BjwwNjQd.d.ts → index-BBH6XWFt.d.ts} +2 -2
  7. package/dist/{index-BjwwNjQd.d.ts.map → index-BBH6XWFt.d.ts.map} +1 -1
  8. package/dist/{index-DKpXHp1c.d.ts → index-Cf-131kQ.d.ts} +25 -2
  9. package/dist/index-Cf-131kQ.d.ts.map +1 -0
  10. package/dist/index.d.ts +3 -3
  11. package/dist/index.js +7 -7
  12. package/dist/{login-YckkS-Bq.js → login-CwLWX6vP.js} +17 -37
  13. package/dist/login-CwLWX6vP.js.map +1 -0
  14. package/dist/{mcp-ngMS0S6N.js → mcp-Wzf0qxaj.js} +120 -26
  15. package/dist/mcp-Wzf0qxaj.js.map +1 -0
  16. package/dist/mcp.d.ts +1 -1
  17. package/dist/mcp.js +1 -1
  18. package/dist/{messages-B5k4DAXy.js → messages-BfmXLDT4.js} +56 -2
  19. package/dist/messages-BfmXLDT4.js.map +1 -0
  20. package/dist/{presets-ZT3TJDEb.js → presets-DAA0NaK_.js} +2 -2
  21. package/dist/{presets-ZT3TJDEb.js.map → presets-DAA0NaK_.js.map} +1 -1
  22. package/dist/presets.d.ts +2 -2
  23. package/dist/presets.js +1 -1
  24. package/dist/{providers-CaJE2ToS.js → providers-C_ahnRBS.js} +2 -2
  25. package/dist/{providers-CaJE2ToS.js.map → providers-C_ahnRBS.js.map} +1 -1
  26. package/dist/providers.d.ts +1 -1
  27. package/dist/providers.js +2 -2
  28. package/dist/restate.d.ts +1 -1
  29. package/dist/session/sqlite.d.ts +1 -1
  30. package/dist/{session-BoEW_wCR.js → session-PUzXZlG6.js} +2 -2
  31. package/dist/{session-BoEW_wCR.js.map → session-PUzXZlG6.js.map} +1 -1
  32. package/dist/session.d.ts +1 -1
  33. package/dist/session.js +2 -2
  34. package/dist/skills.d.ts +2 -2
  35. package/dist/{tools-Bgx8OBqK.js → tools-B9aQpZVx.js} +77 -12
  36. package/dist/tools-B9aQpZVx.js.map +1 -0
  37. package/dist/tools.d.ts +2 -2
  38. package/dist/tools.js +1 -1
  39. package/dist/{transcript-anchors-CGA0h4AT.d.ts → transcript-anchors-CoSZb1PE.d.ts} +65 -9
  40. package/dist/transcript-anchors-CoSZb1PE.d.ts.map +1 -0
  41. package/dist/tui.d.ts +47 -14
  42. package/dist/tui.d.ts.map +1 -1
  43. package/dist/tui.js +716 -151
  44. package/dist/tui.js.map +1 -1
  45. package/dist/{turn-operations-b2ICQ1Y5.js → turn-operations-D2rHrTQv.js} +410 -20
  46. package/dist/turn-operations-D2rHrTQv.js.map +1 -0
  47. package/dist/types.d.ts +2 -2
  48. package/docs/CHAT.md +11 -8
  49. package/docs/SKILL.md +1 -0
  50. package/docs/TUI.md +2 -1
  51. package/package.json +1 -1
  52. package/dist/agent-D70rr6Uk.d.ts.map +0 -1
  53. package/dist/index-DKpXHp1c.d.ts.map +0 -1
  54. package/dist/login-YckkS-Bq.js.map +0 -1
  55. package/dist/mcp-ngMS0S6N.js.map +0 -1
  56. package/dist/messages-B5k4DAXy.js.map +0 -1
  57. package/dist/tools-Bgx8OBqK.js.map +0 -1
  58. package/dist/transcript-anchors-CGA0h4AT.d.ts.map +0 -1
  59. package/dist/turn-operations-b2ICQ1Y5.js.map +0 -1
package/dist/tui.js CHANGED
@@ -1,11 +1,11 @@
1
- import { A as getSafelist, Ar as setProviderCredential, At as SETTINGS_TOGGLES, B as oauthUsesManualCodePaste, Bn as KEYBINDING_KEY_COL_WIDTH, Br as modelSupportsReasoning, Cn as extractEditPayload, Cr as shouldAutoCompact, Ct as buildHints, D as useSafeModeQueue, Di as buildBuildSystem, Dn as summarizeEditPayload, Dt as DEFAULT_SETTINGS, E as useSafeModeActions, Et as useEnabledToggleSet, F as suggestSafelistEntry, Fn as stripEditOutcomesAnnotation, G as indexOfEntry, Gn as matchesBinding, Gr as discoverAgentsMd, Gt as useDiscovery, H as supportsOAuth, Hn as formatBindingForDisplay, In as summarizeOutcomes, It as resolveChipColor, J as discoverProjectMcps, Jt as useConfig, K as buildMcpServers, Kt as useDiscoveryOptional, L as splitPromptSegments, Lt as resolveTheme, Mn as parseEditOutcomesFromResult, Mt as clampFps, Nn as resolveApprovalForPayload, Nt as useSettings, Oi as buildPlanSystem, Ot as SETTINGS_CATEGORIES, Pn as rewriteMultiEditHeader, Qn as uniqueSkillNamesFromReferences, Qt as EDIT_TOOL_NAMES, R as formatPathForCwd, Rn as KEYBINDING_DEFS, Rr as getContextWindow, Sr as AUTO_COMPACT_MIN_GROWTH_FRACTION, St as generateSessionTitle, T as SafeModeProvider, Tn as previewEditPayload, Tt as listProjectFiles, U as buildModelCatalog, Un as groupBindings, Ut as createDiscoverySlot, V as runOAuthLogin, Vn as ensureKeybindingsFile, W as filterModelCatalog, Wn as keybindingsPath, Wr as piIdOf, Wt as DiscoveryProvider, Yt as resolveConfig, Z as createFileMcpCredentialStore, Zn as createSkillsCompletionProvider, _ as turnContextSize, _n as updateToolEventOutcomes, _t as EMPTY_HINTS, a as computeTurnAnchors, an as lastContextSizeFromTurns, at as splitMarkdownCodeBlocks, b as defaultSkillScanPaths, bn as buildUnifiedDiff, bt as truncateTrailing, c as formatToolCall, cn as marginTopFor, cr as buildLinearRamp, d as useSelectStyle, dn as stripSpawnTokensLine, dr as bootTick, ei as accentColor, en as deriveSessionTitle, er as createFilesCompletionProvider, et as McpAuthProvider, f as useSurfaces, fn as sumRunCosts, fr as buildUpdateHint, ft as makeRequestInteraction, g as finalizeStreamingMarkdownForOwner, gi as useActiveTodos, gn as turnSelectionOwnership, gt as useInteractionsQueue, h as finalizeStreamingMarkdown, hn as toolResultText, ht as useInteractionsActions, i as turnAsText, in as isVisible, j as isOnSafelist, jn as mergeApprovalAndBodyOutcomes, jt as SettingsProvider, k as addToSafelist, ki as envSection, kn as buildEditOutcomesAnnotation, kt as SETTINGS_CHOICES, l as ThemeProvider, lr as tryOpenBrowser, lt as buildResumedToolResultsTurn, m as useTheme, mn as toolCallPreview, n as deleteTurnSafely, nn as isEditErrorResult, nt as useMcpAuthState, o as TOOL_DISPLAY, oi as TODO_STATUS_GLYPHS, on as listSessionMeta, or as useCompletion, pr as useUpdateCheck, pt as pendingInteractionsFromTurns, qr as findGitRoot, qt as ConfigProvider, r as truncateTurnsAt, rn as isTurnHighlighted, rt as getMcpAuthStatus, s as displayNameFor, sr as blendHsl, st as InteractionsProvider, tn as eventsFromTurns, tt as useMcpAuthDispatch, u as useColors, un as selectableTurnIds, ut as createInteractionTools, v as useStreamBuffer, vt as clipHintsToWidth, w as writeSessionExport, wn as filetypeFromPath, wr as detectAuth, x as discoverProjectSkills, y as buildSkillsConfig, yn as buildContextualDiff, yt as hintsLength, z as fetchOAuthRedirect } from "./turn-operations-b2ICQ1Y5.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-Bgx8OBqK.js";
1
+ import { $n as ensureKeybindingsFile, A as getSafelist, At as clipHintsToWidth, B as oauthUsesManualCodePaste, Bi as buildPlanSystem, Bt as SETTINGS_CATEGORIES, Cn as stripSpawnTokensLine, Cr as bootTick, D as useSafeModeQueue, Dn as toolResultText, Dt as useInteractionsActions, E as useSafeModeActions, En as toolCallPreview, F as suggestSafelistEntry, Fn as extractEditPayload, Fr as shouldAutoCompact, Ft as buildHints, G as indexOfEntry, Gn as resolveApprovalForPayload, Gt as useSettings, H as supportsOAuth, Hr as setProviderCredential, Ht as SETTINGS_TOGGLES, In as filetypeFromPath, Ir as detectAuth, J as discoverProjectMcps, Jn as summarizeOutcomes, Jt as resolveChipColor, K as buildMcpServers, Kn as rewriteMultiEditHeader, L as splitPromptSegments, Ln as previewEditPayload, Lt as listProjectFiles, Mn as buildUnifiedDiff, Mt as truncateTrailing, Oi as useActiveTodos, On as turnSelectionOwnership, Ot as useInteractionsQueue, Pr as AUTO_COMPACT_MIN_GROWTH_FRACTION, Pt as generateSessionTitle, Q as loadMcpToolsCache, Qn as KEYBINDING_KEY_COL_WIDTH, Qr as modelSupportsReasoning, R as formatPathForCwd, Rt as useEnabledToggleSet, Sn as selectableTurnIds, St as createInteractionTools, T as SafeModeProvider, Tr as useUpdateCheck, Tt as pendingInteractionsFromTurns, U as buildModelCatalog, Un as mergeApprovalAndBodyOutcomes, Ut as SettingsProvider, V as runOAuthLogin, Vi as envSection, Vn as buildEditOutcomesAnnotation, Vt as SETTINGS_CHOICES, W as filterModelCatalog, Wn as parseEditOutcomesFromResult, Wt as clampFps, Xn as KEYBINDING_DEFS, Xr as getContextWindow, Yt as resolveTheme, _ as turnContextSize, _n as lastContextSizeFromTurns, _t as splitMarkdownCodeBlocks, a as computeTurnAnchors, ai as findGitRoot, an as ConfigProvider, b as defaultSkillScanPaths, bn as marginTopFor, br as buildLinearRamp, c as formatToolCall, ct as parentServerName, d as useSelectStyle, er as formatBindingForDisplay, et as refreshMcpToolsCatalog, f as useSurfaces, fi as accentColor, fn as deriveSessionTitle, fr as createFilesCompletionProvider, ft as McpAuthProvider, g as finalizeStreamingMarkdownForOwner, gn as isVisible, h as finalizeStreamingMarkdown, hn as isTurnHighlighted, ht as getMcpAuthStatus, i as turnAsText, in as useDiscoveryOptional, it as useMcpToolToggleMap, j as isOnSafelist, jn as buildContextualDiff, jt as hintsLength, k as addToSafelist, kn as updateToolEventOutcomes, kt as EMPTY_HINTS, l as ThemeProvider, lr as createSkillsCompletionProvider, lt as createFileMcpCredentialStore, m as useTheme, mn as isEditErrorResult, mt as useMcpAuthState, n as deleteTurnSafely, ni as piIdOf, nn as DiscoveryProvider, nr as keybindingsPath, nt as subscribeMcpToolsCache, o as TOOL_DISPLAY, on as useConfig, ot as buildVisibleMcpRows, pn as eventsFromTurns, pt as useMcpAuthDispatch, qn as stripEditOutcomesAnnotation, r as truncateTurnsAt, ri as discoverAgentsMd, rn as useDiscovery, rr as matchesBinding, s as displayNameFor, sn as resolveConfig, st as indexOfServerRow, tn as createDiscoverySlot, tr as groupBindings, u as useColors, un as EDIT_TOOL_NAMES, ur as uniqueSkillNamesFromReferences, v as useStreamBuffer, vi as TODO_STATUS_GLYPHS, vn as listSessionMeta, vr as useCompletion, w as writeSessionExport, wn as sumRunCosts, wr as buildUpdateHint, wt as makeRequestInteraction, x as discoverProjectSkills, xr as tryOpenBrowser, xt as buildResumedToolResultsTurn, y as buildSkillsConfig, yr as blendHsl, yt as InteractionsProvider, z as fetchOAuthRedirect, zi as buildBuildSystem, zn as summarizeEditPayload, zt as DEFAULT_SETTINGS } from "./turn-operations-D2rHrTQv.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-B9aQpZVx.js";
3
3
  import { n as createProcessContext } from "./contexts-BOtMvzli.js";
4
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-YckkS-Bq.js";
5
+ import { s as McpOAuthProvider, t as connectMcpServers } from "./mcp-Wzf0qxaj.js";
6
+ import { C as summaryToTurn, a as selectFilesFromSession, r as buildPostCompactAttachments, s as compactConversation, t as loginMcpServer } from "./login-CwLWX6vP.js";
7
7
  import { n as formatTokenUsage } from "./stats-Lc3zL3RM.js";
8
- import { n as loadSession, t as createSession } from "./session-BoEW_wCR.js";
8
+ import { n as loadSession, t as createSession } from "./session-PUzXZlG6.js";
9
9
  import { createTuiStore } from "./session/sqlite.js";
10
10
  import { basename, join, relative } from "node:path";
11
11
  import { homedir } from "node:os";
@@ -1967,11 +1967,40 @@ function makeMarkdownRenderNode(bag) {
1967
1967
  wrapper.marginTop = topMargin;
1968
1968
  return wrapper;
1969
1969
  }
1970
+ if (inner instanceof CodeRenderable) pinContentTrim(inner);
1970
1971
  inner.marginTop = topMargin;
1971
1972
  return inner;
1972
1973
  };
1973
1974
  }
1974
1975
  /**
1976
+ * Install an instance-level `content` accessor on a prose
1977
+ * {@link CodeRenderable} that strips trailing newlines before
1978
+ * delegating to the prototype setter. Runs once per block on
1979
+ * creation; subsequent markdown delta passes call
1980
+ * `applyMarkdownCodeRenderable` which writes `token.raw` directly into
1981
+ * `.content` and re-enters our setter, so updates stay trimmed too.
1982
+ *
1983
+ * Idempotent — re-applies cleanly if a block is recreated on a
1984
+ * type-change (which destroys + re-creates the renderable anyway).
1985
+ */
1986
+ function pinContentTrim(renderable) {
1987
+ const desc = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(renderable), "content");
1988
+ if (!desc?.set || !desc.get) return;
1989
+ const protoSet = desc.set;
1990
+ const protoGet = desc.get;
1991
+ Object.defineProperty(renderable, "content", {
1992
+ get() {
1993
+ return protoGet.call(this);
1994
+ },
1995
+ set(value) {
1996
+ const next = typeof value === "string" ? value.replace(/\n+$/, "") : value;
1997
+ protoSet.call(this, next);
1998
+ },
1999
+ configurable: true
2000
+ });
2001
+ renderable.content = renderable.content;
2002
+ }
2003
+ /**
1975
2004
  * Pull the trailing block index out of an OpenTUI renderable id. Returns
1976
2005
  * `0` for any id that doesn't match the expected `…-block-N` shape — the
1977
2006
  * spacing rule then treats unparseable ids as "first block" (no
@@ -2017,7 +2046,7 @@ MarkdownBlock.displayName = "MarkdownBlock";
2017
2046
  const TOOL_RESULT_MAX_LINES = 6;
2018
2047
  function ToolResultBlock({ text, indent }) {
2019
2048
  const COLOR = useColors();
2020
- const rawLines = text.split("\n");
2049
+ const rawLines = stripAnsiSequences(text).split("\n");
2021
2050
  let end = rawLines.length;
2022
2051
  while (end > 0 && rawLines[end - 1].trim() === "") end--;
2023
2052
  if (end === 0) return null;
@@ -2428,6 +2457,7 @@ function ToolCallBlock({ event, display, dim }) {
2428
2457
  });
2429
2458
  return /* @__PURE__ */ jsxs("text", {
2430
2459
  fg: dim ? COLOR.dim : COLOR.model,
2460
+ wrapMode: "none",
2431
2461
  children: [
2432
2462
  /* @__PURE__ */ jsx("span", {
2433
2463
  fg: COLOR.mute,
@@ -2807,6 +2837,7 @@ function CwdPickerModal({ currentCwd, onPick }) {
2807
2837
  */
2808
2838
  const FILES_REFRESH_THROTTLE_MS = 3e3;
2809
2839
  const SKILLS_REFRESH_THROTTLE_MS = 3e4;
2840
+ const MCP_TOOLS_CACHE_POLL_MS = 4e3;
2810
2841
  function debugLog$1(...args) {
2811
2842
  if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/tui] ${args.map(errorMessage).join(" ")}\n`);
2812
2843
  }
@@ -2822,6 +2853,7 @@ function DiscoveryShell({ children }) {
2822
2853
  const [mcpsCatalog, setMcpsCatalog] = useState([]);
2823
2854
  const [mcpsErrors, setMcpsErrors] = useState([]);
2824
2855
  const [filesCatalog, setFilesCatalog] = useState([]);
2856
+ const [mcpToolsByServer, setMcpToolsByServer] = useState(() => readToolsByServer(dataDir));
2825
2857
  const filesSlotRef = useRef(null);
2826
2858
  const skillsSlotRef = useRef(null);
2827
2859
  useEffect(() => {
@@ -2923,11 +2955,22 @@ function DiscoveryShell({ children }) {
2923
2955
  config.prefix,
2924
2956
  mcpCredentialStore
2925
2957
  ]);
2958
+ useEffect(() => {
2959
+ const id = setInterval(() => {
2960
+ try {
2961
+ setMcpToolsByServer(readToolsByServer(dataDir));
2962
+ } catch (err) {
2963
+ debugLog$1("mcp-tools-cache refresh failed", err);
2964
+ }
2965
+ }, MCP_TOOLS_CACHE_POLL_MS);
2966
+ return () => clearInterval(id);
2967
+ }, [dataDir]);
2926
2968
  return /* @__PURE__ */ jsx(DiscoveryProvider, {
2927
2969
  value: useMemo(() => ({
2928
2970
  skillsCatalog,
2929
2971
  mcpsCatalog,
2930
2972
  mcpsErrors,
2973
+ mcpToolsByServer,
2931
2974
  filesCatalog,
2932
2975
  refreshSkills,
2933
2976
  refreshMcps,
@@ -2938,6 +2981,7 @@ function DiscoveryShell({ children }) {
2938
2981
  skillsCatalog,
2939
2982
  mcpsCatalog,
2940
2983
  mcpsErrors,
2984
+ mcpToolsByServer,
2941
2985
  filesCatalog,
2942
2986
  refreshSkills,
2943
2987
  refreshMcps,
@@ -2948,6 +2992,19 @@ function DiscoveryShell({ children }) {
2948
2992
  children
2949
2993
  });
2950
2994
  }
2995
+ /**
2996
+ * Read the on-disk MCP tools cache into a `serverName → tools` map.
2997
+ * Strips the per-entry timestamp / transport metadata that the cache
2998
+ * file keeps for diagnostics; consumers downstream of the context
2999
+ * only need the schemas. Best-effort — a corrupt cache produces an
3000
+ * empty map and the next bootstrap re-populates.
3001
+ */
3002
+ function readToolsByServer(dataDir) {
3003
+ const cache = loadMcpToolsCache({ dataDir });
3004
+ const out = {};
3005
+ for (const [name, entry] of Object.entries(cache)) out[name] = entry.tools;
3006
+ return out;
3007
+ }
2951
3008
  //#endregion
2952
3009
  //#region src/tui/effort-picker.tsx
2953
3010
  const BASE_LEVELS = [
@@ -7964,6 +8021,13 @@ function statusColor(status, COLOR) {
7964
8021
  }
7965
8022
  //#endregion
7966
8023
  //#region src/tui/settings-modal.tsx
8024
+ /**
8025
+ * Stable empty default for `mcpToolsByServer` — using a fresh `{}` per
8026
+ * render would invalidate `useMemo` deps that reference it (and
8027
+ * needlessly re-run filtered / visible-row computation when the cache
8028
+ * is cold).
8029
+ */
8030
+ const EMPTY_TOOLS = Object.freeze({});
7967
8031
  const TAB_LABELS = {
7968
8032
  ...Object.fromEntries(SETTINGS_CATEGORIES.map((c) => [c.id, c.label])),
7969
8033
  keybindings: "Keybindings",
@@ -7979,6 +8043,29 @@ function anchorIdFor(index) {
7979
8043
  }
7980
8044
  const COL_TITLE = " ";
7981
8045
  const SPACER_CHECKBOX_WIDTH = " ";
8046
+ /**
8047
+ * Truncate `s` to `maxCols` terminal columns, appending an ellipsis
8048
+ * when it overflows. Used by every settings list to keep long
8049
+ * descriptions on a single line (`wrapMode="none"` clips silently
8050
+ * mid-word without an indicator; this gives the reader a clear "more
8051
+ * text was here" signal instead).
8052
+ *
8053
+ * Whitespace is normalised first so embedded `\n` in descriptions
8054
+ * doesn't blow up the budget — single space as separator everywhere.
8055
+ *
8056
+ * Column counting assumes 1 col per code point. Most descriptions in
8057
+ * this UI are ASCII; a CJK or wide-emoji description would over-count
8058
+ * its width by ~1 col per wide char but that just slightly under-
8059
+ * truncates rather than overflowing the row, which is the failure
8060
+ * mode we care about.
8061
+ */
8062
+ function truncateToCols(s, maxCols) {
8063
+ const clean = s.replace(/\s+/g, " ").trim();
8064
+ if (maxCols <= 0) return "";
8065
+ if (clean.length <= maxCols) return clean;
8066
+ if (maxCols === 1) return "…";
8067
+ return `${clean.slice(0, maxCols - 1)}…`;
8068
+ }
7982
8069
  function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCatalogProp, mcpsErrors: mcpsErrorsProp, keybindings, keybindingsPath, authentication, actions } = {}) {
7983
8070
  const COLOR = useColors();
7984
8071
  const SURFACE = useSurfaces();
@@ -7986,10 +8073,13 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
7986
8073
  const authState = useMcpAuthState();
7987
8074
  const inputRef = useRef(null);
7988
8075
  const scrollboxRef = useRef(null);
8076
+ const { width: termWidth } = useTerminalDimensions();
8077
+ const contentCols = Math.max(40, Math.min(160, termWidth - 4) - 2 - 4 - 2 - 1);
7989
8078
  const discovery = useDiscoveryOptional();
7990
8079
  const skillsCatalog = discovery?.skillsCatalog ?? skillsCatalogProp ?? [];
7991
8080
  const mcpsCatalog = discovery?.mcpsCatalog ?? mcpsCatalogProp ?? [];
7992
8081
  const mcpsErrors = discovery?.mcpsErrors ?? mcpsErrorsProp;
8082
+ const mcpToolsByServer = discovery?.mcpToolsByServer ?? EMPTY_TOOLS;
7993
8083
  const skillsToggle = useEnabledToggleSet({
7994
8084
  catalog: skillsCatalog,
7995
8085
  keyOf: (s) => s.name,
@@ -8000,6 +8090,10 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
8000
8090
  keyOf: (m) => m.config.name,
8001
8091
  settingKey: "enabledMcps"
8002
8092
  });
8093
+ const mcpToolToggleMap = useMcpToolToggleMap({ catalogByServer: mcpToolsByServer });
8094
+ const [expandedMcps, setExpandedMcps] = useState(() => /* @__PURE__ */ new Set());
8095
+ const onRefreshMcpToolsRef = useRef(actions?.onRefreshMcpTools);
8096
+ onRefreshMcpToolsRef.current = actions?.onRefreshMcpTools;
8003
8097
  const tabOrder = useMemo(() => {
8004
8098
  const tabs = [...SETTINGS_CATEGORIES.map((c) => c.id)];
8005
8099
  if (keybindings) tabs.push("keybindings");
@@ -8013,6 +8107,10 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
8013
8107
  useEffect(() => {
8014
8108
  if (!tabOrder.includes(activeTab)) setActiveTab(tabOrder[0]);
8015
8109
  }, [tabOrder, activeTab]);
8110
+ useEffect(() => {
8111
+ if (activeTab !== "mcps") return;
8112
+ onRefreshMcpToolsRef.current?.();
8113
+ }, [activeTab]);
8016
8114
  useEffect(() => {
8017
8115
  inputRef.current?.focus();
8018
8116
  }, []);
@@ -8061,13 +8159,22 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
8061
8159
  return buckets;
8062
8160
  }, [generalItems, query]);
8063
8161
  const filteredSkills = useMemo(() => skillsCatalog.filter((s) => matchesQuery(skillCorpus(s), query)), [skillsCatalog, query]);
8064
- const filteredMcps = useMemo(() => mcpsCatalog.filter((m) => matchesQuery(mcpCorpus(m), query)), [mcpsCatalog, query]);
8162
+ const filteredMcps = useMemo(() => mcpsCatalog.filter((m) => matchesQuery(mcpCorpus(m, mcpToolsByServer), query)), [
8163
+ mcpsCatalog,
8164
+ query,
8165
+ mcpToolsByServer
8166
+ ]);
8167
+ const mcpVisibleRows = useMemo(() => buildVisibleMcpRows(filteredMcps, expandedMcps, mcpToolsByServer), [
8168
+ filteredMcps,
8169
+ expandedMcps,
8170
+ mcpToolsByServer
8171
+ ]);
8065
8172
  const filteredProviders = useMemo(() => (authentication?.providers ?? []).filter((p) => matchesQuery(providerCorpus(p), query)), [authentication?.providers, query]);
8066
8173
  const authRowCount = filteredProviders.length + (authentication ? 1 : 0);
8067
8174
  const filteredSize = {
8068
8175
  ...Object.fromEntries(SETTINGS_CATEGORIES.map((c) => [c.id, filteredByCategory[c.id].length])),
8069
8176
  skills: filteredSkills.length,
8070
- mcps: filteredMcps.length,
8177
+ mcps: mcpVisibleRows.length,
8071
8178
  keybindings: 0,
8072
8179
  authentication: authRowCount
8073
8180
  };
@@ -8075,7 +8182,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
8075
8182
  const moveCursor = useCallback((delta) => setCursorByTab((prev) => {
8076
8183
  const size = filteredSize[activeTab];
8077
8184
  if (size === 0) return prev;
8078
- const next = ((prev[activeTab] + delta) % size + size) % size;
8185
+ const next = ((Math.min(prev[activeTab], size - 1) + delta) % size + size) % size;
8079
8186
  return {
8080
8187
  ...prev,
8081
8188
  [activeTab]: next
@@ -8088,19 +8195,86 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
8088
8195
  [activeTab]: 0
8089
8196
  }));
8090
8197
  }, [activeTab]);
8198
+ const expandFocusedMcpRow = useCallback(() => {
8199
+ const row = mcpVisibleRows[cursor];
8200
+ if (!row || row.kind !== "server") return;
8201
+ const name = row.entry.config.name;
8202
+ setExpandedMcps((prev) => {
8203
+ if (prev.has(name)) return prev;
8204
+ const next = new Set(prev);
8205
+ next.add(name);
8206
+ return next;
8207
+ });
8208
+ }, [mcpVisibleRows, cursor]);
8209
+ const collapseFocusedMcpRow = useCallback(() => {
8210
+ const row = mcpVisibleRows[cursor];
8211
+ if (!row) return;
8212
+ const parentName = parentServerName(row);
8213
+ if (row.kind !== "server") {
8214
+ const parentIndex = indexOfServerRow(mcpVisibleRows, parentName);
8215
+ if (parentIndex >= 0) setCursorByTab((prev) => ({
8216
+ ...prev,
8217
+ mcps: parentIndex
8218
+ }));
8219
+ }
8220
+ setExpandedMcps((prev) => {
8221
+ if (!prev.has(parentName)) return prev;
8222
+ const next = new Set(prev);
8223
+ next.delete(parentName);
8224
+ return next;
8225
+ });
8226
+ }, [mcpVisibleRows, cursor]);
8227
+ /**
8228
+ * Single `tab` press toggles between expanded / collapsed based on
8229
+ * the focused row's current state. The split helpers above remain
8230
+ * useful for `→` / `←` (a "tree-style" navigation contract some
8231
+ * users expect — expand-only / collapse-only). Toggle is the
8232
+ * default that the footer hint advertises.
8233
+ */
8234
+ const toggleExpandFocusedMcpRow = useCallback(() => {
8235
+ const row = mcpVisibleRows[cursor];
8236
+ if (!row) return;
8237
+ const parentName = parentServerName(row);
8238
+ if (expandedMcps.has(parentName)) collapseFocusedMcpRow();
8239
+ else expandFocusedMcpRow();
8240
+ }, [
8241
+ mcpVisibleRows,
8242
+ cursor,
8243
+ expandedMcps,
8244
+ collapseFocusedMcpRow,
8245
+ expandFocusedMcpRow
8246
+ ]);
8247
+ const mcpRowCount = mcpVisibleRows.length;
8091
8248
  useEffect(() => {
8092
8249
  if (activeTab === "keybindings") return;
8093
8250
  const sb = scrollboxRef.current;
8094
8251
  if (!sb) return;
8095
- const handle = requestAnimationFrame(() => {
8096
- sb.scrollChildIntoView(anchorIdFor(cursor));
8252
+ let inner = 0;
8253
+ const outer = requestAnimationFrame(() => {
8254
+ inner = requestAnimationFrame(() => {
8255
+ sb.scrollChildIntoView(anchorIdFor(cursor));
8256
+ });
8097
8257
  });
8098
- return () => cancelAnimationFrame(handle);
8258
+ return () => {
8259
+ cancelAnimationFrame(outer);
8260
+ if (inner) cancelAnimationFrame(inner);
8261
+ };
8099
8262
  }, [
8100
8263
  cursor,
8101
8264
  activeTab,
8102
- query
8265
+ query,
8266
+ mcpRowCount
8103
8267
  ]);
8268
+ useEffect(() => {
8269
+ if (mcpRowCount === 0) return;
8270
+ setCursorByTab((prev) => {
8271
+ if (prev.mcps < mcpRowCount) return prev;
8272
+ return {
8273
+ ...prev,
8274
+ mcps: mcpRowCount - 1
8275
+ };
8276
+ });
8277
+ }, [mcpRowCount]);
8104
8278
  useEffect(() => {
8105
8279
  if (activeTab !== "keybindings") return;
8106
8280
  const sb = scrollboxRef.current;
@@ -8110,7 +8284,8 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
8110
8284
  });
8111
8285
  return () => cancelAnimationFrame(handle);
8112
8286
  }, [activeTab]);
8113
- const focusedMcp = filteredMcps[cursor];
8287
+ const focusedMcpRow = activeTab === "mcps" ? mcpVisibleRows[cursor] : void 0;
8288
+ const focusedMcp = focusedMcpRow ? mcpsCatalog.find((m) => m.config.name === parentServerName(focusedMcpRow)) : void 0;
8114
8289
  const focusedMcpStatus = focusedMcp ? getMcpAuthStatus(authState, focusedMcp.config.name) : void 0;
8115
8290
  const oauthPasteActive = activeTab === "mcps" && focusedMcpStatus?.kind === "authorizing" && !!focusedMcpStatus.url;
8116
8291
  useKeyboard((key) => {
@@ -8155,6 +8330,10 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
8155
8330
  });
8156
8331
  return;
8157
8332
  }
8333
+ if (activeTab === "mcps" && key.name === "tab") {
8334
+ toggleExpandFocusedMcpRow();
8335
+ return;
8336
+ }
8158
8337
  if (key.name === "return") {
8159
8338
  if (isCategoryTab(activeTab)) {
8160
8339
  const it = filteredByCategory[activeTab][cursor];
@@ -8174,8 +8353,10 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
8174
8353
  return;
8175
8354
  }
8176
8355
  if (activeTab === "mcps") {
8177
- const m = filteredMcps[cursor];
8178
- if (m) mcpsToggle.toggle(m.config.name);
8356
+ const row = mcpVisibleRows[cursor];
8357
+ if (!row) return;
8358
+ if (row.kind === "server") mcpsToggle.toggle(row.entry.config.name);
8359
+ else if (row.kind === "tool") mcpToolToggleMap[row.serverName]?.toggle(row.tool.name);
8179
8360
  return;
8180
8361
  }
8181
8362
  if (activeTab === "keybindings") {
@@ -8198,11 +8379,12 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
8198
8379
  return;
8199
8380
  }
8200
8381
  if (activeTab === "mcps" && focusedMcp && focusedMcpStatus) {
8201
- if (key.ctrl && key.name === "l" && canLogin$1(focusedMcp, focusedMcpStatus)) {
8382
+ const onServerRow = focusedMcpRow?.kind === "server";
8383
+ if (onServerRow && key.ctrl && key.name === "l" && canLogin$1(focusedMcp, focusedMcpStatus)) {
8202
8384
  actions?.onLoginMcp?.(focusedMcp.config.name);
8203
8385
  return;
8204
8386
  }
8205
- if (key.ctrl && key.name === "o" && canLogout$1(focusedMcpStatus)) {
8387
+ if (onServerRow && key.ctrl && key.name === "o" && canLogout$1(focusedMcpStatus)) {
8206
8388
  actions?.onLogoutMcp?.(focusedMcp.config.name);
8207
8389
  return;
8208
8390
  }
@@ -8213,7 +8395,10 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
8213
8395
  actions.onRefreshSkills();
8214
8396
  return;
8215
8397
  }
8216
- if (activeTab === "mcps" && actions?.onRefreshMcps) actions.onRefreshMcps();
8398
+ if (activeTab === "mcps") {
8399
+ if (actions?.onRefreshMcps) actions.onRefreshMcps();
8400
+ if (actions?.onRefreshMcpTools) actions.onRefreshMcpTools();
8401
+ }
8217
8402
  }
8218
8403
  });
8219
8404
  const totalsByCategory = useMemo(() => {
@@ -8277,12 +8462,14 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
8277
8462
  minHeight: 4
8278
8463
  },
8279
8464
  stickyScroll: false,
8465
+ viewportCulling: false,
8280
8466
  children: [
8281
8467
  isCategoryTab(activeTab) && /* @__PURE__ */ jsx(GeneralList, {
8282
8468
  items: filteredByCategory[activeTab],
8283
8469
  cursor,
8284
8470
  highlightBg: SURFACE.selection,
8285
- query
8471
+ query,
8472
+ contentCols
8286
8473
  }),
8287
8474
  activeTab === "skills" && /* @__PURE__ */ jsx(SkillsList, {
8288
8475
  items: filteredSkills,
@@ -8290,16 +8477,22 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
8290
8477
  totalCount: skillsCatalog.length,
8291
8478
  cursor,
8292
8479
  highlightBg: SURFACE.selection,
8293
- query
8480
+ query,
8481
+ contentCols
8294
8482
  }),
8295
8483
  activeTab === "mcps" && /* @__PURE__ */ jsx(McpsList, {
8296
- items: filteredMcps,
8484
+ rows: mcpVisibleRows,
8485
+ filteredCount: filteredMcps.length,
8297
8486
  enabledSet: mcpsToggle.enabledSet,
8487
+ toolToggleMap: mcpToolToggleMap,
8488
+ toolsByServer: mcpToolsByServer,
8489
+ expanded: expandedMcps,
8298
8490
  totalCount: mcpsCatalog.length,
8299
8491
  cursor,
8300
8492
  highlightBg: SURFACE.selection,
8301
8493
  query,
8302
8494
  errors: mcpsErrors,
8495
+ contentCols,
8303
8496
  authState
8304
8497
  }),
8305
8498
  activeTab === "keybindings" && keybindings && /* @__PURE__ */ jsx(KeybindingsList, {
@@ -8331,6 +8524,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
8331
8524
  children: /* @__PURE__ */ jsx(Hints, {
8332
8525
  activeTab,
8333
8526
  focusedMcp,
8527
+ focusedMcpRow,
8334
8528
  focusedMcpStatus,
8335
8529
  canRefreshSkills: !!actions?.onRefreshSkills,
8336
8530
  canRefreshMcps: !!actions?.onRefreshMcps
@@ -8374,9 +8568,10 @@ function TabStrip({ order, active, counts }) {
8374
8568
  })
8375
8569
  });
8376
8570
  }
8377
- function GeneralList({ items, cursor, highlightBg, query }) {
8571
+ function GeneralList({ items, cursor, highlightBg, query, contentCols }) {
8378
8572
  const { settings } = useSettings();
8379
8573
  if (items.length === 0) return /* @__PURE__ */ jsx(EmptyRow, { label: query ? `no settings match "${query}"` : "no settings" });
8574
+ const descCols = Math.max(0, contentCols - 1 - 1 - 6);
8380
8575
  return /* @__PURE__ */ jsx("box", {
8381
8576
  style: { flexDirection: "column" },
8382
8577
  children: items.map((item, i) => {
@@ -8386,7 +8581,7 @@ function GeneralList({ items, cursor, highlightBg, query }) {
8386
8581
  if (item.kind === "toggle") return /* @__PURE__ */ jsx(ToggleRow, {
8387
8582
  id,
8388
8583
  label: item.label,
8389
- description: item.description,
8584
+ description: truncateToCols(item.description, descCols),
8390
8585
  enabled: settings[item.key],
8391
8586
  focused,
8392
8587
  bg
@@ -8397,7 +8592,7 @@ function GeneralList({ items, cursor, highlightBg, query }) {
8397
8592
  return /* @__PURE__ */ jsx(ChoiceRow, {
8398
8593
  id,
8399
8594
  label: item.label,
8400
- description: item.description,
8595
+ description: truncateToCols(item.description, descCols),
8401
8596
  value: opt?.label ?? String(current),
8402
8597
  cyclable: item.options.length > 1,
8403
8598
  focused,
@@ -8407,14 +8602,14 @@ function GeneralList({ items, cursor, highlightBg, query }) {
8407
8602
  return /* @__PURE__ */ jsx(ActionRow$1, {
8408
8603
  id,
8409
8604
  label: item.label,
8410
- description: item.description,
8605
+ description: truncateToCols(item.description, descCols),
8411
8606
  focused,
8412
8607
  bg
8413
8608
  }, item.id);
8414
8609
  })
8415
8610
  });
8416
8611
  }
8417
- function SkillsList({ items, enabledSet, totalCount, cursor, highlightBg, query }) {
8612
+ function SkillsList({ items, enabledSet, totalCount, cursor, highlightBg, query, contentCols }) {
8418
8613
  const COLOR = useColors();
8419
8614
  if (totalCount === 0) return /* @__PURE__ */ jsxs("box", {
8420
8615
  style: { flexDirection: "column" },
@@ -8449,12 +8644,14 @@ function SkillsList({ items, enabledSet, totalCount, cursor, highlightBg, query
8449
8644
  })]
8450
8645
  });
8451
8646
  if (items.length === 0) return /* @__PURE__ */ jsx(EmptyRow, { label: `no skills match "${query}"` });
8647
+ const descCols = Math.max(0, contentCols - 1 - 1 - 6);
8452
8648
  return /* @__PURE__ */ jsx("box", {
8453
8649
  style: { flexDirection: "column" },
8454
8650
  children: items.map((entry, i) => {
8455
8651
  const focused = i === cursor;
8456
8652
  const bg = focused ? highlightBg : void 0;
8457
8653
  const enabled = enabledSet.has(entry.name);
8654
+ const description = truncateToCols(entry.description ?? "", descCols);
8458
8655
  return /* @__PURE__ */ jsxs("box", {
8459
8656
  id: anchorIdFor(i),
8460
8657
  style: {
@@ -8483,13 +8680,13 @@ function SkillsList({ items, enabledSet, totalCount, cursor, highlightBg, query
8483
8680
  }), /* @__PURE__ */ jsx("text", {
8484
8681
  wrapMode: "none",
8485
8682
  fg: COLOR.mute,
8486
- children: `${COL_TITLE}${entry.description ?? ""}`
8683
+ children: `${COL_TITLE}${description}`
8487
8684
  })]
8488
8685
  }, entry.name);
8489
8686
  })
8490
8687
  });
8491
8688
  }
8492
- function McpsList({ items, enabledSet, totalCount, cursor, highlightBg, query, errors, authState }) {
8689
+ function McpsList({ rows, filteredCount, enabledSet, toolToggleMap, toolsByServer, expanded, totalCount, cursor, highlightBg, query, errors, authState, contentCols }) {
8493
8690
  const COLOR = useColors();
8494
8691
  const home = homedir();
8495
8692
  if (totalCount === 0) return /* @__PURE__ */ jsxs("box", {
@@ -8508,7 +8705,12 @@ function McpsList({ items, enabledSet, totalCount, cursor, highlightBg, query, e
8508
8705
  fg: COLOR.model,
8509
8706
  children: " mcps.json "
8510
8707
  }),
8511
- "into",
8708
+ "(or",
8709
+ /* @__PURE__ */ jsx("span", {
8710
+ fg: COLOR.model,
8711
+ children: " mcp.json "
8712
+ }),
8713
+ ") into",
8512
8714
  /* @__PURE__ */ jsx("span", {
8513
8715
  fg: COLOR.model,
8514
8716
  children: " .zidane/ "
@@ -8528,53 +8730,150 @@ function McpsList({ items, enabledSet, totalCount, cursor, highlightBg, query, e
8528
8730
  })
8529
8731
  ]
8530
8732
  });
8531
- if (items.length === 0) return /* @__PURE__ */ jsxs("box", {
8733
+ if (filteredCount === 0) return /* @__PURE__ */ jsxs("box", {
8532
8734
  style: { flexDirection: "column" },
8533
8735
  children: [renderMcpErrors(errors, home, COLOR.warn), /* @__PURE__ */ jsx(EmptyRow, { label: `no servers match "${query}"` })]
8534
8736
  });
8737
+ const groups = [];
8738
+ for (let i = 0; i < rows.length; i++) {
8739
+ const row = rows[i];
8740
+ if (row.kind === "server") {
8741
+ groups.push({
8742
+ serverIndex: i,
8743
+ server: row,
8744
+ children: []
8745
+ });
8746
+ continue;
8747
+ }
8748
+ const last = groups[groups.length - 1];
8749
+ if (last) last.children.push({
8750
+ row,
8751
+ index: i
8752
+ });
8753
+ }
8535
8754
  return /* @__PURE__ */ jsxs("box", {
8536
8755
  style: { flexDirection: "column" },
8537
- children: [renderMcpErrors(errors, home, COLOR.warn), items.map((entry, i) => {
8538
- const focused = i === cursor;
8539
- const bg = focused ? highlightBg : void 0;
8540
- const name = entry.config.name;
8541
- const enabled = enabledSet.has(name);
8542
- const status = getMcpAuthStatus(authState, name);
8543
- return /* @__PURE__ */ jsxs("box", {
8544
- id: anchorIdFor(i),
8545
- style: {
8546
- flexDirection: "column",
8547
- flexShrink: 0,
8548
- paddingLeft: 1,
8549
- paddingRight: 1,
8550
- backgroundColor: bg
8551
- },
8552
- children: [/* @__PURE__ */ jsxs("text", {
8553
- wrapMode: "none",
8554
- children: [
8555
- /* @__PURE__ */ jsx("span", {
8556
- fg: focused ? COLOR.brand : COLOR.mute,
8557
- children: focused ? "▶ " : " "
8558
- }),
8559
- /* @__PURE__ */ jsx("span", {
8560
- fg: enabled ? COLOR.accent : COLOR.mute,
8561
- children: enabled ? "[✓] " : "[ ] "
8562
- }),
8563
- /* @__PURE__ */ jsx("span", {
8564
- fg: focused ? COLOR.brand : COLOR.dim,
8565
- children: name
8566
- }),
8567
- renderInlineMcpBadge(status, COLOR)
8568
- ]
8569
- }), /* @__PURE__ */ jsx("text", {
8570
- wrapMode: "none",
8571
- fg: COLOR.mute,
8572
- children: `${COL_TITLE}${mcpDetail(entry)}`
8573
- })]
8574
- }, name);
8575
- })]
8756
+ children: [renderMcpErrors(errors, home, COLOR.warn), groups.map((group) => /* @__PURE__ */ jsxs("box", {
8757
+ style: { flexDirection: "column" },
8758
+ children: [renderServerListRow({
8759
+ entry: group.server.entry,
8760
+ focused: group.serverIndex === cursor,
8761
+ bg: group.serverIndex === cursor ? highlightBg : void 0,
8762
+ enabled: enabledSet.has(group.server.entry.config.name),
8763
+ expanded: expanded.has(group.server.entry.config.name),
8764
+ toolEnabledSet: toolToggleMap[group.server.entry.config.name]?.enabledSet,
8765
+ toolTotal: toolsByServer[group.server.entry.config.name]?.length,
8766
+ anchorId: anchorIdFor(group.serverIndex),
8767
+ status: getMcpAuthStatus(authState, group.server.entry.config.name),
8768
+ COLOR,
8769
+ contentCols
8770
+ }), group.children.map(({ row, index }) => {
8771
+ const focused = index === cursor;
8772
+ const bg = focused ? highlightBg : void 0;
8773
+ if (row.kind === "tool") return renderToolListRow({
8774
+ serverName: row.serverName,
8775
+ tool: row.tool,
8776
+ focused,
8777
+ bg,
8778
+ enabled: toolToggleMap[row.serverName]?.enabledSet.has(row.tool.name) ?? true,
8779
+ anchorId: anchorIdFor(index),
8780
+ COLOR,
8781
+ contentCols
8782
+ });
8783
+ return renderEmptyToolsListRow({
8784
+ serverName: row.serverName,
8785
+ anchorId: anchorIdFor(index),
8786
+ focused,
8787
+ bg,
8788
+ COLOR
8789
+ });
8790
+ })]
8791
+ }, `group:${group.server.entry.config.name}`))]
8576
8792
  });
8577
8793
  }
8794
+ function renderServerListRow({ entry, focused, bg, enabled, expanded, toolEnabledSet, toolTotal, anchorId, status, COLOR, contentCols }) {
8795
+ const name = entry.config.name;
8796
+ const chevron = expanded ? "▾" : "▸";
8797
+ const toolCountChip = toolEnabledSet && typeof toolTotal === "number" ? ` (${toolEnabledSet.size}/${toolTotal})` : "";
8798
+ const detailRaw = mcpDetail(entry);
8799
+ const detailBudget = Math.max(0, contentCols - 2 - 2 - 4 - name.length - toolCountChip.length - 10 - 3);
8800
+ const detail = detailRaw ? truncateToCols(detailRaw, detailBudget) : "";
8801
+ return /* @__PURE__ */ jsxs("text", {
8802
+ id: anchorId,
8803
+ wrapMode: "none",
8804
+ bg,
8805
+ children: [
8806
+ /* @__PURE__ */ jsx("span", {
8807
+ fg: focused ? COLOR.brand : COLOR.mute,
8808
+ children: focused ? "▶ " : " "
8809
+ }),
8810
+ /* @__PURE__ */ jsx("span", {
8811
+ fg: focused ? COLOR.brand : COLOR.mute,
8812
+ children: `${chevron} `
8813
+ }),
8814
+ /* @__PURE__ */ jsx("span", {
8815
+ fg: enabled ? COLOR.accent : COLOR.mute,
8816
+ children: enabled ? "[✓] " : "[ ] "
8817
+ }),
8818
+ /* @__PURE__ */ jsx("span", {
8819
+ fg: focused ? COLOR.brand : COLOR.dim,
8820
+ children: name
8821
+ }),
8822
+ toolCountChip && /* @__PURE__ */ jsx("span", {
8823
+ fg: COLOR.mute,
8824
+ children: toolCountChip
8825
+ }),
8826
+ renderInlineMcpBadge(status, COLOR),
8827
+ detail && /* @__PURE__ */ jsx("span", {
8828
+ fg: COLOR.mute,
8829
+ children: ` · ${detail}`
8830
+ })
8831
+ ]
8832
+ }, `server:${name}`);
8833
+ }
8834
+ const TOOL_ROW_INDENT$1 = " ";
8835
+ const TOOL_ROW_FOCUS_INDENT$1 = " ▶ ";
8836
+ function renderToolListRow({ serverName, tool, focused, bg, enabled, anchorId, COLOR, contentCols }) {
8837
+ const descBudget = Math.max(0, contentCols - 6 - 4 - tool.name.length - 2);
8838
+ const description = truncateToCols(tool.description ?? "", descBudget);
8839
+ return /* @__PURE__ */ jsxs("text", {
8840
+ id: anchorId,
8841
+ wrapMode: "none",
8842
+ bg,
8843
+ children: [
8844
+ /* @__PURE__ */ jsx("span", {
8845
+ fg: focused ? COLOR.brand : COLOR.mute,
8846
+ children: focused ? TOOL_ROW_FOCUS_INDENT$1 : TOOL_ROW_INDENT$1
8847
+ }),
8848
+ /* @__PURE__ */ jsx("span", {
8849
+ fg: enabled ? COLOR.accent : COLOR.mute,
8850
+ children: enabled ? "[✓] " : "[ ] "
8851
+ }),
8852
+ /* @__PURE__ */ jsx("span", {
8853
+ fg: focused ? COLOR.brand : COLOR.dim,
8854
+ children: tool.name
8855
+ }),
8856
+ description && /* @__PURE__ */ jsxs("span", {
8857
+ fg: focused ? COLOR.dim : COLOR.mute,
8858
+ children: [" ", description]
8859
+ })
8860
+ ]
8861
+ }, `tool:${serverName}/${tool.name}`);
8862
+ }
8863
+ function renderEmptyToolsListRow({ serverName, anchorId, focused, bg, COLOR }) {
8864
+ return /* @__PURE__ */ jsxs("text", {
8865
+ id: anchorId,
8866
+ wrapMode: "none",
8867
+ bg,
8868
+ children: [/* @__PURE__ */ jsx("span", {
8869
+ fg: focused ? COLOR.brand : COLOR.mute,
8870
+ children: focused ? TOOL_ROW_FOCUS_INDENT$1 : TOOL_ROW_INDENT$1
8871
+ }), /* @__PURE__ */ jsx("span", {
8872
+ fg: COLOR.mute,
8873
+ children: "⟲ No cached tools yet. Send any prompt with this server enabled to populate."
8874
+ })]
8875
+ }, `tool-empty:${serverName}`);
8876
+ }
8578
8877
  function KeybindingsList({ bindings, query }) {
8579
8878
  const sections = useMemo(() => groupBindings(bindings), [bindings]);
8580
8879
  const filteredSections = useMemo(() => {
@@ -8932,21 +9231,21 @@ function renderMcpDetailPanel(entry, status, COLOR) {
8932
9231
  function renderInlineMcpBadge(status, COLOR) {
8933
9232
  switch (status.kind) {
8934
9233
  case "idle": return null;
8935
- case "authed": return /* @__PURE__ */ jsxs("span", {
9234
+ case "authed": return /* @__PURE__ */ jsx("span", {
8936
9235
  fg: COLOR.accent,
8937
- children: [" ", "✓ authed"]
9236
+ children: " · ✓ authed"
8938
9237
  });
8939
- case "needs-auth": return /* @__PURE__ */ jsxs("span", {
9238
+ case "needs-auth": return /* @__PURE__ */ jsx("span", {
8940
9239
  fg: COLOR.warn,
8941
- children: [" ", "! needs login"]
9240
+ children: " · ! needs login"
8942
9241
  });
8943
- case "authorizing": return /* @__PURE__ */ jsxs("span", {
9242
+ case "authorizing": return /* @__PURE__ */ jsx("span", {
8944
9243
  fg: COLOR.warn,
8945
- children: [" ", "… authorizing"]
9244
+ children: " · … authorizing"
8946
9245
  });
8947
- case "error": return /* @__PURE__ */ jsxs("span", {
9246
+ case "error": return /* @__PURE__ */ jsx("span", {
8948
9247
  fg: COLOR.error,
8949
- children: [" ", "✗ login failed"]
9248
+ children: " · ✗ login failed"
8950
9249
  });
8951
9250
  }
8952
9251
  }
@@ -8960,10 +9259,11 @@ function renderMcpErrors(errors, home, warnColor) {
8960
9259
  }, err.path))
8961
9260
  });
8962
9261
  }
8963
- function Hints({ activeTab, focusedMcp, focusedMcpStatus, canRefreshSkills, canRefreshMcps }) {
9262
+ function Hints({ activeTab, focusedMcp, focusedMcpRow, focusedMcpStatus, canRefreshSkills, canRefreshMcps }) {
8964
9263
  const COLOR = useColors();
8965
- const showLogin = activeTab === "mcps" && !!focusedMcp && !!focusedMcpStatus && canLogin$1(focusedMcp, focusedMcpStatus);
8966
- const showLogout = activeTab === "mcps" && !!focusedMcpStatus && canLogout$1(focusedMcpStatus);
9264
+ const onServerRow = focusedMcpRow?.kind === "server";
9265
+ const showLogin = activeTab === "mcps" && onServerRow && !!focusedMcp && !!focusedMcpStatus && canLogin$1(focusedMcp, focusedMcpStatus);
9266
+ const showLogout = activeTab === "mcps" && onServerRow && !!focusedMcpStatus && canLogout$1(focusedMcpStatus);
8967
9267
  const showCancel = activeTab === "mcps" && focusedMcpStatus?.kind === "authorizing";
8968
9268
  const showRefresh = activeTab === "skills" && canRefreshSkills || activeTab === "mcps" && canRefreshMcps;
8969
9269
  return /* @__PURE__ */ jsxs("text", {
@@ -8984,6 +9284,14 @@ function Hints({ activeTab, focusedMcp, focusedMcpStatus, canRefreshSkills, canR
8984
9284
  children: "↵"
8985
9285
  }),
8986
9286
  isCategoryTab(activeTab) ? " toggle/cycle/select" : activeTab === "keybindings" ? " edit file" : activeTab === "authentication" ? " switch / re-authenticate" : " toggle",
9287
+ activeTab === "mcps" && focusedMcpRow && /* @__PURE__ */ jsxs("span", { children: [
9288
+ " · ",
9289
+ /* @__PURE__ */ jsx("span", {
9290
+ fg: COLOR.warn,
9291
+ children: "tab"
9292
+ }),
9293
+ focusedMcpRow.kind === "server" ? " expand/collapse" : " collapse"
9294
+ ] }),
8987
9295
  showLogin && /* @__PURE__ */ jsxs("span", { children: [
8988
9296
  " · ",
8989
9297
  /* @__PURE__ */ jsx("span", {
@@ -9039,13 +9347,15 @@ function generalCorpus(item) {
9039
9347
  function skillCorpus(s) {
9040
9348
  return `${s.name} ${s.description ?? ""}`.toLowerCase();
9041
9349
  }
9042
- function mcpCorpus(m) {
9350
+ function mcpCorpus(m, toolsByServer) {
9351
+ const toolCorpus = (toolsByServer[m.config.name] ?? []).map((t) => `${t.name} ${t.description ?? ""}`).join(" ");
9043
9352
  return [
9044
9353
  m.config.name,
9045
9354
  m.config.transport,
9046
9355
  m.config.command ?? "",
9047
9356
  m.config.url ?? "",
9048
- (m.config.args ?? []).join(" ")
9357
+ (m.config.args ?? []).join(" "),
9358
+ toolCorpus
9049
9359
  ].join(" ").toLowerCase();
9050
9360
  }
9051
9361
  function keybindingCorpus(row) {
@@ -9892,6 +10202,8 @@ function AppShell() {
9892
10202
  mcpsCatalogRef.current = mcpsCatalog;
9893
10203
  const enabledMcpsRef = useRef(settings.enabledMcps);
9894
10204
  enabledMcpsRef.current = settings.enabledMcps;
10205
+ const disabledMcpToolsRef = useRef(settings.disabledMcpTools);
10206
+ disabledMcpToolsRef.current = settings.disabledMcpTools;
9895
10207
  const filesCatalogRef = useRef(filesCatalog);
9896
10208
  filesCatalogRef.current = filesCatalog;
9897
10209
  const ensureFilesCatalogRef = useRef(ensureFilesCatalog);
@@ -10229,7 +10541,8 @@ function AppShell() {
10229
10541
  });
10230
10542
  const projectMcps = buildMcpServers({
10231
10543
  discovered: mcpsCatalogRef.current,
10232
- enabled: enabledMcpsRef.current
10544
+ enabled: enabledMcpsRef.current,
10545
+ ...disabledMcpToolsRef.current ? { disabledTools: disabledMcpToolsRef.current } : {}
10233
10546
  });
10234
10547
  const allowInteraction = allowInteractionRef.current !== false;
10235
10548
  const interactionTools = allowInteraction ? createInteractionTools({ requestInteraction: makeRequestInteraction(interactions) }) : {};
@@ -10394,6 +10707,15 @@ function AppShell() {
10394
10707
  reason
10395
10708
  });
10396
10709
  });
10710
+ agent.hooks.hook("mcp:bootstrap:end", (ctx) => {
10711
+ if (ctx.ok) return;
10712
+ if (mcpsCatalogRef.current.find((d) => d.config.name === ctx.name)?.config.auth !== "oauth") return;
10713
+ dispatchAuthRef.current({
10714
+ type: "auth-error",
10715
+ name: ctx.name,
10716
+ error: ctx.error.message
10717
+ });
10718
+ });
10397
10719
  agent.hooks.hook("mcp:auth:url", ({ name, url }) => {
10398
10720
  dispatchAuthRef.current({
10399
10721
  type: "auth-url",
@@ -10629,6 +10951,7 @@ function AppShell() {
10629
10951
  agent.hooks.hook("agent:done", () => {
10630
10952
  pendingAnnotationsRef.current.clear();
10631
10953
  });
10954
+ subscribeMcpToolsCache(agent.hooks, { dataDir });
10632
10955
  return agent;
10633
10956
  }, [
10634
10957
  providerRegistry,
@@ -11306,6 +11629,24 @@ function AppShell() {
11306
11629
  const onCancelLoginMcp = useCallback((name) => {
11307
11630
  mcpLoginAbortsRef.current.get(name)?.abort();
11308
11631
  }, []);
11632
+ const onRefreshMcpTools = useCallback(async () => {
11633
+ const agent = agentRef.current;
11634
+ if (!agent) return;
11635
+ const servers = mcpsCatalogRef.current.map((d) => d.config);
11636
+ if (servers.length === 0) return;
11637
+ try {
11638
+ await refreshMcpToolsCatalog({
11639
+ servers,
11640
+ hooks: agent.hooks,
11641
+ buildAuthProvider: (cfg) => new McpOAuthProvider({
11642
+ name: cfg.name,
11643
+ store: mcpCredentialStore
11644
+ })
11645
+ });
11646
+ } catch (err) {
11647
+ debugLog("refreshMcpToolsCatalog failed", err);
11648
+ }
11649
+ }, [mcpCredentialStore]);
11309
11650
  const onOpenKeybindingsFile = useCallback(() => {
11310
11651
  modal.close();
11311
11652
  (async () => {
@@ -11812,7 +12153,8 @@ function AppShell() {
11812
12153
  onLogoutMcp,
11813
12154
  onCancelLoginMcp,
11814
12155
  onRefreshSkills,
11815
- onRefreshMcps
12156
+ onRefreshMcps,
12157
+ onRefreshMcpTools
11816
12158
  }
11817
12159
  }));
11818
12160
  return;
@@ -12282,55 +12624,100 @@ function initTreeSitterWorker() {
12282
12624
  //#endregion
12283
12625
  //#region src/tui/mcps-settings.tsx
12284
12626
  /**
12285
- * MCP server picker. Shows discovered entries with three columns:
12627
+ * MCP server picker. Hierarchical: each server is one row, and an
12628
+ * expanded server reveals one row per advertised tool. The full surface:
12286
12629
  *
12287
- * [enabled?] <name> <transport · detail> <auth status>
12630
+ *[] linear stdio · npx -y mcp-remote … (5/7) ✓ authed
12631
+ * [✓] create_issue Create a new Linear issue
12632
+ * [✓] list_issues Query issues by status / assignee
12633
+ * [ ] update_issue Update fields on an existing issue
12634
+ * ▶ [✓] chrome-devtools stdio · npx -y chrome-devtools-mcp@latest
12288
12635
  *
12289
12636
  * Keybindings:
12290
12637
  *
12291
- * ↑ / ↓ / k / j navigate
12292
- * enter / space toggle enabled/disabled
12293
- * l login (needs-auth or error rows; opens browser)
12294
- * o logout (authed rows; wipes stored tokens)
12295
- * esc close also cancels an in-flight `authorizing` row
12638
+ * ↑ / ↓ / k / j navigate (over servers AND visible tool rows)
12639
+ * / tab expand the focused server
12640
+ * / shift+tab collapse on a tool row, also snaps the cursor
12641
+ * back to the parent server
12642
+ * enter / space toggle server toggles `enabledMcps`; tool toggles
12643
+ * `disabledMcpTools[serverName]`
12644
+ * l login (server rows only — needs-auth / error)
12645
+ * o logout (server rows only — authed / error /
12646
+ * authorizing)
12647
+ * r refresh discovery
12648
+ * esc close — also cancels an in-flight `authorizing`
12649
+ * row
12296
12650
  *
12297
- * Status badge legend:
12651
+ * Status badge legend (server row only):
12298
12652
  *
12299
12653
  * ✓ authed tokens stored + bootstrap connected
12300
- * ! needs login bootstrap needs OAuth, no tokens
12654
+ * ! needs login bootstrap needs OAuth, no tokens
12301
12655
  * … authorizing interactive login in flight
12302
12656
  * ✗ <error> login attempt failed
12303
12657
  *
12304
- * The state behind the badges is read via `useMcpAuthState()` so updates
12305
- * propagate live while the modal is open a `mcp:auth:url` arriving from
12306
- * an in-flight login flips the row to `authorizing` without re-opening.
12658
+ * The (N/M) chip on a server header counts ENABLED / TOTAL advertised
12659
+ * tools, drawn only when the tool catalog has loaded. Without a catalog
12660
+ * (server never bootstrapped this profile, cache cold) the chip is
12661
+ * suppressed and the empty-state row under the expanded server explains
12662
+ * how to populate.
12307
12663
  *
12308
12664
  * Errors (`DiscoveryError[]`) surface in a warn-colored preamble so a
12309
12665
  * broken `mcps.json` is loud rather than invisible.
12310
12666
  */
12311
- function McpsSettingsModal({ catalog, errors, onLogin, onLogout, onCancelLogin, onRefresh }) {
12667
+ function McpsSettingsModal({ catalog, errors, toolsByServer, onLogin, onLogout, onCancelLogin, onRefresh }) {
12312
12668
  const COLOR = useColors();
12313
12669
  const home = homedir();
12314
12670
  const authState = useMcpAuthState();
12315
- const { enabledSet, toggle } = useEnabledToggleSet({
12671
+ const { enabledSet: serverEnabledSet, toggle: toggleServer } = useEnabledToggleSet({
12316
12672
  catalog,
12317
12673
  keyOf: (d) => d.config.name,
12318
12674
  settingKey: "enabledMcps"
12319
12675
  });
12676
+ const toolToggleMap = useMcpToolToggleMap({ catalogByServer: toolsByServer });
12677
+ const [expanded, setExpanded] = useState(() => /* @__PURE__ */ new Set());
12320
12678
  const [cursor, setCursorRaw] = useState(0);
12679
+ const rows = useMemo(() => buildVisibleMcpRows(catalog, expanded, toolsByServer), [
12680
+ catalog,
12681
+ expanded,
12682
+ toolsByServer
12683
+ ]);
12321
12684
  const moveCursor = useCallback((delta) => setCursorRaw((prev) => {
12322
- if (catalog.length === 0) return prev;
12323
- return ((prev + delta) % catalog.length + catalog.length) % catalog.length;
12324
- }), [catalog.length]);
12325
- const safeCursor = Math.min(cursor, Math.max(0, catalog.length - 1));
12326
- const focusedEntry = catalog[safeCursor];
12685
+ if (rows.length === 0) return prev;
12686
+ return ((prev + delta) % rows.length + rows.length) % rows.length;
12687
+ }), [rows.length]);
12688
+ const safeCursor = Math.min(cursor, Math.max(0, rows.length - 1));
12689
+ const focusedRow = rows[safeCursor];
12690
+ const focusedServerName = focusedRow ? focusedRow.kind === "server" ? focusedRow.entry.config.name : focusedRow.serverName : void 0;
12691
+ const focusedEntry = focusedServerName ? catalog.find((c) => c.config.name === focusedServerName) : void 0;
12327
12692
  const focusedStatus = focusedEntry ? getMcpAuthStatus(authState, focusedEntry.config.name) : void 0;
12328
12693
  const inputActive = focusedStatus?.kind === "authorizing" && !!focusedStatus.url;
12694
+ const collapseFocused = useCallback(() => {
12695
+ if (!focusedRow) return;
12696
+ const parentName = focusedRow.kind === "server" ? focusedRow.entry.config.name : focusedRow.serverName;
12697
+ if (focusedRow.kind !== "server") {
12698
+ const parentIndex = indexOfServerRow(rows, parentName);
12699
+ if (parentIndex >= 0) setCursorRaw(parentIndex);
12700
+ }
12701
+ setExpanded((prev) => {
12702
+ if (!prev.has(parentName)) return prev;
12703
+ const next = new Set(prev);
12704
+ next.delete(parentName);
12705
+ return next;
12706
+ });
12707
+ }, [focusedRow, rows]);
12708
+ const expandFocused = useCallback(() => {
12709
+ if (!focusedRow || focusedRow.kind !== "server") return;
12710
+ setExpanded((prev) => {
12711
+ if (prev.has(focusedRow.entry.config.name)) return prev;
12712
+ const next = new Set(prev);
12713
+ next.add(focusedRow.entry.config.name);
12714
+ return next;
12715
+ });
12716
+ }, [focusedRow]);
12329
12717
  useKeyboard((key) => {
12330
12718
  if (key.name === "escape" && focusedEntry) {
12331
- const name = focusedEntry.config.name;
12332
- if (getMcpAuthStatus(authState, name).kind === "authorizing") {
12333
- onCancelLogin(name);
12719
+ if (getMcpAuthStatus(authState, focusedEntry.config.name).kind === "authorizing") {
12720
+ onCancelLogin(focusedEntry.config.name);
12334
12721
  return;
12335
12722
  }
12336
12723
  }
@@ -12343,21 +12730,36 @@ function McpsSettingsModal({ catalog, errors, onLogin, onLogout, onCancelLogin,
12343
12730
  moveCursor(1);
12344
12731
  return;
12345
12732
  }
12346
- if (catalog.length === 0) return;
12347
- const entry = catalog[safeCursor];
12348
- if (!entry) return;
12349
- const name = entry.config.name;
12350
- const status = getMcpAuthStatus(authState, name);
12733
+ if (rows.length === 0) return;
12734
+ if (!focusedRow) return;
12735
+ if (key.name === "tab") {
12736
+ const row = rows[safeCursor];
12737
+ const parentName = row ? parentServerName(row) : void 0;
12738
+ if (parentName && expanded.has(parentName)) collapseFocused();
12739
+ else expandFocused();
12740
+ return;
12741
+ }
12742
+ if (key.name === "right") {
12743
+ expandFocused();
12744
+ return;
12745
+ }
12746
+ if (key.name === "left") {
12747
+ collapseFocused();
12748
+ return;
12749
+ }
12351
12750
  if (key.name === "return" || key.name === "space") {
12352
- toggle(name);
12751
+ if (focusedRow.kind === "server") toggleServer(focusedRow.entry.config.name);
12752
+ else if (focusedRow.kind === "tool") toolToggleMap[focusedRow.serverName]?.toggle(focusedRow.tool.name);
12353
12753
  return;
12354
12754
  }
12355
- if (key.name === "l" && canLogin(entry, status)) {
12356
- onLogin(name);
12755
+ if (focusedRow.kind !== "server") return;
12756
+ const status = getMcpAuthStatus(authState, focusedRow.entry.config.name);
12757
+ if (key.name === "l" && canLogin(focusedRow.entry, status)) {
12758
+ onLogin(focusedRow.entry.config.name);
12357
12759
  return;
12358
12760
  }
12359
12761
  if (key.name === "o" && canLogout(status)) {
12360
- onLogout(name);
12762
+ onLogout(focusedRow.entry.config.name);
12361
12763
  return;
12362
12764
  }
12363
12765
  if (key.name === "r" && onRefresh) onRefresh();
@@ -12378,7 +12780,12 @@ function McpsSettingsModal({ catalog, errors, onLogin, onLogout, onCancelLogin,
12378
12780
  fg: COLOR.model,
12379
12781
  children: " mcps.json "
12380
12782
  }),
12381
- "into",
12783
+ "(or",
12784
+ /* @__PURE__ */ jsx("span", {
12785
+ fg: COLOR.model,
12786
+ children: " mcp.json "
12787
+ }),
12788
+ ") into",
12382
12789
  /* @__PURE__ */ jsx("span", {
12383
12790
  fg: COLOR.model,
12384
12791
  children: " .zidane/ "
@@ -12435,46 +12842,185 @@ function McpsSettingsModal({ catalog, errors, onLogin, onLogout, onCancelLogin,
12435
12842
  })
12436
12843
  ]
12437
12844
  });
12845
+ const groups = [];
12846
+ for (let i = 0; i < rows.length; i++) {
12847
+ const row = rows[i];
12848
+ if (row.kind === "server") {
12849
+ groups.push({
12850
+ serverIndex: i,
12851
+ server: row,
12852
+ children: []
12853
+ });
12854
+ continue;
12855
+ }
12856
+ const last = groups[groups.length - 1];
12857
+ if (last) last.children.push({
12858
+ row,
12859
+ index: i
12860
+ });
12861
+ }
12438
12862
  return /* @__PURE__ */ jsxs(Modal, {
12439
- title: ` mcp servers · ${enabledSet.size} / ${catalog.length} enabled `,
12863
+ title: ` mcp servers · ${serverEnabledSet.size} / ${catalog.length} enabled `,
12440
12864
  children: [
12441
12865
  renderErrors(errors, home, COLOR.warn),
12442
12866
  /* @__PURE__ */ jsx("box", {
12443
12867
  style: { flexDirection: "column" },
12444
- children: catalog.map((entry, i) => {
12445
- const focused = i === safeCursor;
12446
- const name = entry.config.name;
12447
- const enabled = enabledSet.has(name);
12448
- const status = getMcpAuthStatus(authState, name);
12449
- return /* @__PURE__ */ jsxs("text", {
12450
- fg: focused ? COLOR.brand : COLOR.dim,
12451
- children: [
12452
- /* @__PURE__ */ jsx("span", {
12453
- fg: focused ? COLOR.brand : COLOR.mute,
12454
- children: focused ? "▶ " : " "
12455
- }),
12456
- /* @__PURE__ */ jsx("span", {
12457
- fg: enabled ? COLOR.accent : COLOR.mute,
12458
- children: enabled ? "[✓] " : "[ ] "
12459
- }),
12460
- /* @__PURE__ */ jsx("span", {
12461
- fg: focused ? COLOR.brand : COLOR.dim,
12462
- children: name
12463
- }),
12464
- /* @__PURE__ */ jsxs("span", {
12465
- fg: COLOR.mute,
12466
- children: [" ", detailFor(entry)]
12467
- }),
12468
- renderInlineBadge(status, COLOR)
12469
- ]
12470
- }, name);
12471
- })
12868
+ children: groups.map((group) => /* @__PURE__ */ jsxs("box", {
12869
+ style: { flexDirection: "column" },
12870
+ children: [renderRow({
12871
+ row: group.server,
12872
+ focused: group.serverIndex === safeCursor,
12873
+ serverEnabledSet,
12874
+ toolEnabledSet: toolToggleMap[group.server.entry.config.name]?.enabledSet,
12875
+ toolCatalog: toolsByServer,
12876
+ authState,
12877
+ expanded,
12878
+ COLOR
12879
+ }), group.children.map(({ row, index }) => renderRow({
12880
+ row,
12881
+ focused: index === safeCursor,
12882
+ serverEnabledSet,
12883
+ toolEnabledSet: toolToggleMap[parentServerName(row)]?.enabledSet,
12884
+ toolCatalog: toolsByServer,
12885
+ authState,
12886
+ expanded,
12887
+ COLOR
12888
+ }))]
12889
+ }, `group:${group.server.entry.config.name}`))
12472
12890
  }),
12473
12891
  focusedEntry && focusedStatus && renderDetailPanel(focusedEntry, focusedStatus, COLOR),
12474
- renderActionHints(focusedEntry, focusedStatus, !!onRefresh, COLOR)
12892
+ renderActionHints({
12893
+ focusedRow,
12894
+ focusedEntry,
12895
+ focusedStatus,
12896
+ expanded,
12897
+ showRefresh: !!onRefresh,
12898
+ COLOR
12899
+ })
12475
12900
  ]
12476
12901
  });
12477
12902
  }
12903
+ function renderRow({ row, focused, serverEnabledSet, toolEnabledSet, toolCatalog, authState, expanded, COLOR }) {
12904
+ if (row.kind === "server") return renderServerRow({
12905
+ entry: row.entry,
12906
+ focused,
12907
+ serverEnabledSet,
12908
+ toolEnabledSet,
12909
+ toolCatalog,
12910
+ authState,
12911
+ expanded,
12912
+ COLOR
12913
+ });
12914
+ if (row.kind === "tool") return renderToolRow({
12915
+ row,
12916
+ focused,
12917
+ toolEnabledSet,
12918
+ COLOR
12919
+ });
12920
+ return renderEmptyToolsRow({
12921
+ row,
12922
+ focused,
12923
+ COLOR
12924
+ });
12925
+ }
12926
+ function renderServerRow({ entry, focused, serverEnabledSet, toolEnabledSet, toolCatalog, authState, expanded, COLOR }) {
12927
+ const name = entry.config.name;
12928
+ const enabled = serverEnabledSet.has(name);
12929
+ const status = getMcpAuthStatus(authState, name);
12930
+ const cached = toolCatalog[name];
12931
+ const chevron = expanded.has(name) ? "▼" : "◀";
12932
+ const toolCountChip = cached && toolEnabledSet ? ` (${toolEnabledSet.size}/${cached.length})` : "";
12933
+ return /* @__PURE__ */ jsxs("box", {
12934
+ style: {
12935
+ flexDirection: "row",
12936
+ height: 1,
12937
+ paddingRight: 1
12938
+ },
12939
+ children: [
12940
+ /* @__PURE__ */ jsxs("text", {
12941
+ wrapMode: "none",
12942
+ fg: focused ? COLOR.brand : COLOR.dim,
12943
+ children: [
12944
+ /* @__PURE__ */ jsx("span", {
12945
+ fg: focused ? COLOR.brand : COLOR.mute,
12946
+ children: focused ? "▶ " : " "
12947
+ }),
12948
+ /* @__PURE__ */ jsx("span", {
12949
+ fg: enabled ? COLOR.accent : COLOR.mute,
12950
+ children: enabled ? "[✓] " : "[ ] "
12951
+ }),
12952
+ /* @__PURE__ */ jsx("span", {
12953
+ fg: focused ? COLOR.brand : COLOR.dim,
12954
+ children: name
12955
+ }),
12956
+ /* @__PURE__ */ jsxs("span", {
12957
+ fg: COLOR.mute,
12958
+ children: [" ", detailFor(entry)]
12959
+ }),
12960
+ toolCountChip && /* @__PURE__ */ jsx("span", {
12961
+ fg: COLOR.mute,
12962
+ children: toolCountChip
12963
+ }),
12964
+ renderInlineBadge(status, COLOR)
12965
+ ]
12966
+ }),
12967
+ /* @__PURE__ */ jsx("box", { style: { flexGrow: 1 } }),
12968
+ /* @__PURE__ */ jsx("text", {
12969
+ wrapMode: "none",
12970
+ children: /* @__PURE__ */ jsx("span", {
12971
+ fg: focused ? COLOR.brand : COLOR.mute,
12972
+ children: chevron
12973
+ })
12974
+ })
12975
+ ]
12976
+ }, `server:${name}`);
12977
+ }
12978
+ const TOOL_ROW_INDENT = " ";
12979
+ const TOOL_ROW_FOCUS_INDENT = " ▶ ";
12980
+ function renderToolRow({ row, focused, toolEnabledSet, COLOR }) {
12981
+ const enabled = toolEnabledSet?.has(row.tool.name) ?? true;
12982
+ const description = trimToWidth(row.tool.description ?? "", 60);
12983
+ return /* @__PURE__ */ jsxs("text", {
12984
+ fg: focused ? COLOR.brand : COLOR.dim,
12985
+ children: [
12986
+ /* @__PURE__ */ jsx("span", {
12987
+ fg: focused ? COLOR.brand : COLOR.mute,
12988
+ children: focused ? TOOL_ROW_FOCUS_INDENT : TOOL_ROW_INDENT
12989
+ }),
12990
+ /* @__PURE__ */ jsx("span", {
12991
+ fg: enabled ? COLOR.accent : COLOR.mute,
12992
+ children: enabled ? "[✓] " : "[ ] "
12993
+ }),
12994
+ /* @__PURE__ */ jsx("span", {
12995
+ fg: focused ? COLOR.brand : COLOR.dim,
12996
+ children: row.tool.name
12997
+ }),
12998
+ description && /* @__PURE__ */ jsxs("span", {
12999
+ fg: COLOR.mute,
13000
+ children: [" ", description]
13001
+ })
13002
+ ]
13003
+ }, `tool:${row.serverName}/${row.tool.name}`);
13004
+ }
13005
+ function renderEmptyToolsRow({ row, focused, COLOR }) {
13006
+ return /* @__PURE__ */ jsxs("text", { children: [/* @__PURE__ */ jsx("span", {
13007
+ fg: focused ? COLOR.brand : COLOR.mute,
13008
+ children: focused ? TOOL_ROW_FOCUS_INDENT : TOOL_ROW_INDENT
13009
+ }), /* @__PURE__ */ jsx("span", {
13010
+ fg: COLOR.mute,
13011
+ children: "⟲ No cached tools yet. Send any prompt with this server enabled to populate."
13012
+ })] }, `tool-empty:${row.serverName}`);
13013
+ }
13014
+ /**
13015
+ * Cap a single-line description at `max` columns, suffixing `…` if it
13016
+ * overflows. Renderer uses one column per char; surrogate-pair safety
13017
+ * isn't a concern here because tool descriptions are ASCII in practice.
13018
+ */
13019
+ function trimToWidth(s, max) {
13020
+ const clean = s.replace(/\s+/g, " ").trim();
13021
+ if (clean.length <= max) return clean;
13022
+ return `${clean.slice(0, Math.max(0, max - 1))}…`;
13023
+ }
12478
13024
  function detailFor(entry) {
12479
13025
  const transport = entry.config.transport;
12480
13026
  return `${transport} · ${transport === "stdio" ? entry.config.command ?? "" : entry.config.url ?? ""}`;
@@ -12580,11 +13126,14 @@ function renderDetailPanel(entry, status, COLOR) {
12580
13126
  });
12581
13127
  return null;
12582
13128
  }
12583
- function renderActionHints(entry, status, showRefresh, COLOR) {
12584
- const effectiveStatus = status ?? { kind: "idle" };
12585
- const canL = entry ? canLogin(entry, effectiveStatus) : false;
12586
- const canO = canLogout(effectiveStatus);
13129
+ function renderActionHints({ focusedRow, focusedEntry, focusedStatus, expanded, showRefresh, COLOR }) {
13130
+ const effectiveStatus = focusedStatus ?? { kind: "idle" };
13131
+ const onServerRow = focusedRow?.kind === "server";
13132
+ const canL = onServerRow && focusedEntry ? canLogin(focusedEntry, effectiveStatus) : false;
13133
+ const canO = onServerRow ? canLogout(effectiveStatus) : false;
12587
13134
  const canCancel = effectiveStatus.kind === "authorizing";
13135
+ const canExpand = onServerRow && focusedEntry ? !expanded.has(focusedEntry.config.name) : false;
13136
+ const canCollapse = focusedRow?.kind === "tool" || (onServerRow && focusedEntry ? expanded.has(focusedEntry.config.name) : false);
12588
13137
  return /* @__PURE__ */ jsxs("text", {
12589
13138
  fg: COLOR.mute,
12590
13139
  children: [
@@ -12598,6 +13147,22 @@ function renderActionHints(entry, status, showRefresh, COLOR) {
12598
13147
  children: "↵"
12599
13148
  }),
12600
13149
  " toggle",
13150
+ canExpand && /* @__PURE__ */ jsxs("span", { children: [
13151
+ " · ",
13152
+ /* @__PURE__ */ jsx("span", {
13153
+ fg: COLOR.warn,
13154
+ children: "→"
13155
+ }),
13156
+ " expand"
13157
+ ] }),
13158
+ canCollapse && /* @__PURE__ */ jsxs("span", { children: [
13159
+ " · ",
13160
+ /* @__PURE__ */ jsx("span", {
13161
+ fg: COLOR.warn,
13162
+ children: "←"
13163
+ }),
13164
+ " collapse"
13165
+ ] }),
12601
13166
  canL && /* @__PURE__ */ jsxs("span", { children: [
12602
13167
  " · ",
12603
13168
  /* @__PURE__ */ jsx("span", {