zidane 5.6.7 → 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-8mn3PIaa.d.ts → index-Cf-131kQ.d.ts} +2 -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-Btpliwct.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-BXmWG3kd.js → presets-DAA0NaK_.js} +2 -2
  21. package/dist/{presets-BXmWG3kd.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-FerA0zSl.js → tools-B9aQpZVx.js} +6 -4
  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-C5Sp1Snh.d.ts → transcript-anchors-CoSZb1PE.d.ts} +42 -4
  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 +703 -148
  44. package/dist/tui.js.map +1 -1
  45. package/dist/{turn-operations-D-OQYUgS.js → turn-operations-D2rHrTQv.js} +375 -14
  46. package/dist/turn-operations-D2rHrTQv.js.map +1 -0
  47. package/dist/types.d.ts +2 -2
  48. package/docs/CHAT.md +4 -1
  49. package/docs/SKILL.md +1 -0
  50. package/docs/TUI.md +1 -0
  51. package/package.json +1 -1
  52. package/dist/agent-D70rr6Uk.d.ts.map +0 -1
  53. package/dist/index-8mn3PIaa.d.ts.map +0 -1
  54. package/dist/login-Btpliwct.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-FerA0zSl.js.map +0 -1
  58. package/dist/transcript-anchors-C5Sp1Snh.d.ts.map +0 -1
  59. package/dist/turn-operations-D-OQYUgS.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-D-OQYUgS.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-FerA0zSl.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-Btpliwct.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
@@ -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", {
@@ -8533,53 +8730,150 @@ function McpsList({ items, enabledSet, totalCount, cursor, highlightBg, query, e
8533
8730
  })
8534
8731
  ]
8535
8732
  });
8536
- if (items.length === 0) return /* @__PURE__ */ jsxs("box", {
8733
+ if (filteredCount === 0) return /* @__PURE__ */ jsxs("box", {
8537
8734
  style: { flexDirection: "column" },
8538
8735
  children: [renderMcpErrors(errors, home, COLOR.warn), /* @__PURE__ */ jsx(EmptyRow, { label: `no servers match "${query}"` })]
8539
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
+ }
8540
8754
  return /* @__PURE__ */ jsxs("box", {
8541
8755
  style: { flexDirection: "column" },
8542
- children: [renderMcpErrors(errors, home, COLOR.warn), items.map((entry, i) => {
8543
- const focused = i === cursor;
8544
- const bg = focused ? highlightBg : void 0;
8545
- const name = entry.config.name;
8546
- const enabled = enabledSet.has(name);
8547
- const status = getMcpAuthStatus(authState, name);
8548
- return /* @__PURE__ */ jsxs("box", {
8549
- id: anchorIdFor(i),
8550
- style: {
8551
- flexDirection: "column",
8552
- flexShrink: 0,
8553
- paddingLeft: 1,
8554
- paddingRight: 1,
8555
- backgroundColor: bg
8556
- },
8557
- children: [/* @__PURE__ */ jsxs("text", {
8558
- wrapMode: "none",
8559
- children: [
8560
- /* @__PURE__ */ jsx("span", {
8561
- fg: focused ? COLOR.brand : COLOR.mute,
8562
- children: focused ? "▶ " : " "
8563
- }),
8564
- /* @__PURE__ */ jsx("span", {
8565
- fg: enabled ? COLOR.accent : COLOR.mute,
8566
- children: enabled ? "[✓] " : "[ ] "
8567
- }),
8568
- /* @__PURE__ */ jsx("span", {
8569
- fg: focused ? COLOR.brand : COLOR.dim,
8570
- children: name
8571
- }),
8572
- renderInlineMcpBadge(status, COLOR)
8573
- ]
8574
- }), /* @__PURE__ */ jsx("text", {
8575
- wrapMode: "none",
8576
- fg: COLOR.mute,
8577
- children: `${COL_TITLE}${mcpDetail(entry)}`
8578
- })]
8579
- }, name);
8580
- })]
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}`))]
8581
8792
  });
8582
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
+ }
8583
8877
  function KeybindingsList({ bindings, query }) {
8584
8878
  const sections = useMemo(() => groupBindings(bindings), [bindings]);
8585
8879
  const filteredSections = useMemo(() => {
@@ -8937,21 +9231,21 @@ function renderMcpDetailPanel(entry, status, COLOR) {
8937
9231
  function renderInlineMcpBadge(status, COLOR) {
8938
9232
  switch (status.kind) {
8939
9233
  case "idle": return null;
8940
- case "authed": return /* @__PURE__ */ jsxs("span", {
9234
+ case "authed": return /* @__PURE__ */ jsx("span", {
8941
9235
  fg: COLOR.accent,
8942
- children: [" ", "✓ authed"]
9236
+ children: " · ✓ authed"
8943
9237
  });
8944
- case "needs-auth": return /* @__PURE__ */ jsxs("span", {
9238
+ case "needs-auth": return /* @__PURE__ */ jsx("span", {
8945
9239
  fg: COLOR.warn,
8946
- children: [" ", "! needs login"]
9240
+ children: " · ! needs login"
8947
9241
  });
8948
- case "authorizing": return /* @__PURE__ */ jsxs("span", {
9242
+ case "authorizing": return /* @__PURE__ */ jsx("span", {
8949
9243
  fg: COLOR.warn,
8950
- children: [" ", "… authorizing"]
9244
+ children: " · … authorizing"
8951
9245
  });
8952
- case "error": return /* @__PURE__ */ jsxs("span", {
9246
+ case "error": return /* @__PURE__ */ jsx("span", {
8953
9247
  fg: COLOR.error,
8954
- children: [" ", "✗ login failed"]
9248
+ children: " · ✗ login failed"
8955
9249
  });
8956
9250
  }
8957
9251
  }
@@ -8965,10 +9259,11 @@ function renderMcpErrors(errors, home, warnColor) {
8965
9259
  }, err.path))
8966
9260
  });
8967
9261
  }
8968
- function Hints({ activeTab, focusedMcp, focusedMcpStatus, canRefreshSkills, canRefreshMcps }) {
9262
+ function Hints({ activeTab, focusedMcp, focusedMcpRow, focusedMcpStatus, canRefreshSkills, canRefreshMcps }) {
8969
9263
  const COLOR = useColors();
8970
- const showLogin = activeTab === "mcps" && !!focusedMcp && !!focusedMcpStatus && canLogin$1(focusedMcp, focusedMcpStatus);
8971
- 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);
8972
9267
  const showCancel = activeTab === "mcps" && focusedMcpStatus?.kind === "authorizing";
8973
9268
  const showRefresh = activeTab === "skills" && canRefreshSkills || activeTab === "mcps" && canRefreshMcps;
8974
9269
  return /* @__PURE__ */ jsxs("text", {
@@ -8989,6 +9284,14 @@ function Hints({ activeTab, focusedMcp, focusedMcpStatus, canRefreshSkills, canR
8989
9284
  children: "↵"
8990
9285
  }),
8991
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
+ ] }),
8992
9295
  showLogin && /* @__PURE__ */ jsxs("span", { children: [
8993
9296
  " · ",
8994
9297
  /* @__PURE__ */ jsx("span", {
@@ -9044,13 +9347,15 @@ function generalCorpus(item) {
9044
9347
  function skillCorpus(s) {
9045
9348
  return `${s.name} ${s.description ?? ""}`.toLowerCase();
9046
9349
  }
9047
- function mcpCorpus(m) {
9350
+ function mcpCorpus(m, toolsByServer) {
9351
+ const toolCorpus = (toolsByServer[m.config.name] ?? []).map((t) => `${t.name} ${t.description ?? ""}`).join(" ");
9048
9352
  return [
9049
9353
  m.config.name,
9050
9354
  m.config.transport,
9051
9355
  m.config.command ?? "",
9052
9356
  m.config.url ?? "",
9053
- (m.config.args ?? []).join(" ")
9357
+ (m.config.args ?? []).join(" "),
9358
+ toolCorpus
9054
9359
  ].join(" ").toLowerCase();
9055
9360
  }
9056
9361
  function keybindingCorpus(row) {
@@ -9897,6 +10202,8 @@ function AppShell() {
9897
10202
  mcpsCatalogRef.current = mcpsCatalog;
9898
10203
  const enabledMcpsRef = useRef(settings.enabledMcps);
9899
10204
  enabledMcpsRef.current = settings.enabledMcps;
10205
+ const disabledMcpToolsRef = useRef(settings.disabledMcpTools);
10206
+ disabledMcpToolsRef.current = settings.disabledMcpTools;
9900
10207
  const filesCatalogRef = useRef(filesCatalog);
9901
10208
  filesCatalogRef.current = filesCatalog;
9902
10209
  const ensureFilesCatalogRef = useRef(ensureFilesCatalog);
@@ -10234,7 +10541,8 @@ function AppShell() {
10234
10541
  });
10235
10542
  const projectMcps = buildMcpServers({
10236
10543
  discovered: mcpsCatalogRef.current,
10237
- enabled: enabledMcpsRef.current
10544
+ enabled: enabledMcpsRef.current,
10545
+ ...disabledMcpToolsRef.current ? { disabledTools: disabledMcpToolsRef.current } : {}
10238
10546
  });
10239
10547
  const allowInteraction = allowInteractionRef.current !== false;
10240
10548
  const interactionTools = allowInteraction ? createInteractionTools({ requestInteraction: makeRequestInteraction(interactions) }) : {};
@@ -10399,6 +10707,15 @@ function AppShell() {
10399
10707
  reason
10400
10708
  });
10401
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
+ });
10402
10719
  agent.hooks.hook("mcp:auth:url", ({ name, url }) => {
10403
10720
  dispatchAuthRef.current({
10404
10721
  type: "auth-url",
@@ -10634,6 +10951,7 @@ function AppShell() {
10634
10951
  agent.hooks.hook("agent:done", () => {
10635
10952
  pendingAnnotationsRef.current.clear();
10636
10953
  });
10954
+ subscribeMcpToolsCache(agent.hooks, { dataDir });
10637
10955
  return agent;
10638
10956
  }, [
10639
10957
  providerRegistry,
@@ -11311,6 +11629,24 @@ function AppShell() {
11311
11629
  const onCancelLoginMcp = useCallback((name) => {
11312
11630
  mcpLoginAbortsRef.current.get(name)?.abort();
11313
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]);
11314
11650
  const onOpenKeybindingsFile = useCallback(() => {
11315
11651
  modal.close();
11316
11652
  (async () => {
@@ -11817,7 +12153,8 @@ function AppShell() {
11817
12153
  onLogoutMcp,
11818
12154
  onCancelLoginMcp,
11819
12155
  onRefreshSkills,
11820
- onRefreshMcps
12156
+ onRefreshMcps,
12157
+ onRefreshMcpTools
11821
12158
  }
11822
12159
  }));
11823
12160
  return;
@@ -12287,55 +12624,100 @@ function initTreeSitterWorker() {
12287
12624
  //#endregion
12288
12625
  //#region src/tui/mcps-settings.tsx
12289
12626
  /**
12290
- * 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:
12291
12629
  *
12292
- * [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
12293
12635
  *
12294
12636
  * Keybindings:
12295
12637
  *
12296
- * ↑ / ↓ / k / j navigate
12297
- * enter / space toggle enabled/disabled
12298
- * l login (needs-auth or error rows; opens browser)
12299
- * o logout (authed rows; wipes stored tokens)
12300
- * 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
12301
12650
  *
12302
- * Status badge legend:
12651
+ * Status badge legend (server row only):
12303
12652
  *
12304
12653
  * ✓ authed tokens stored + bootstrap connected
12305
- * ! needs login bootstrap needs OAuth, no tokens
12654
+ * ! needs login bootstrap needs OAuth, no tokens
12306
12655
  * … authorizing interactive login in flight
12307
12656
  * ✗ <error> login attempt failed
12308
12657
  *
12309
- * The state behind the badges is read via `useMcpAuthState()` so updates
12310
- * propagate live while the modal is open a `mcp:auth:url` arriving from
12311
- * 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.
12312
12663
  *
12313
12664
  * Errors (`DiscoveryError[]`) surface in a warn-colored preamble so a
12314
12665
  * broken `mcps.json` is loud rather than invisible.
12315
12666
  */
12316
- function McpsSettingsModal({ catalog, errors, onLogin, onLogout, onCancelLogin, onRefresh }) {
12667
+ function McpsSettingsModal({ catalog, errors, toolsByServer, onLogin, onLogout, onCancelLogin, onRefresh }) {
12317
12668
  const COLOR = useColors();
12318
12669
  const home = homedir();
12319
12670
  const authState = useMcpAuthState();
12320
- const { enabledSet, toggle } = useEnabledToggleSet({
12671
+ const { enabledSet: serverEnabledSet, toggle: toggleServer } = useEnabledToggleSet({
12321
12672
  catalog,
12322
12673
  keyOf: (d) => d.config.name,
12323
12674
  settingKey: "enabledMcps"
12324
12675
  });
12676
+ const toolToggleMap = useMcpToolToggleMap({ catalogByServer: toolsByServer });
12677
+ const [expanded, setExpanded] = useState(() => /* @__PURE__ */ new Set());
12325
12678
  const [cursor, setCursorRaw] = useState(0);
12679
+ const rows = useMemo(() => buildVisibleMcpRows(catalog, expanded, toolsByServer), [
12680
+ catalog,
12681
+ expanded,
12682
+ toolsByServer
12683
+ ]);
12326
12684
  const moveCursor = useCallback((delta) => setCursorRaw((prev) => {
12327
- if (catalog.length === 0) return prev;
12328
- return ((prev + delta) % catalog.length + catalog.length) % catalog.length;
12329
- }), [catalog.length]);
12330
- const safeCursor = Math.min(cursor, Math.max(0, catalog.length - 1));
12331
- 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;
12332
12692
  const focusedStatus = focusedEntry ? getMcpAuthStatus(authState, focusedEntry.config.name) : void 0;
12333
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]);
12334
12717
  useKeyboard((key) => {
12335
12718
  if (key.name === "escape" && focusedEntry) {
12336
- const name = focusedEntry.config.name;
12337
- if (getMcpAuthStatus(authState, name).kind === "authorizing") {
12338
- onCancelLogin(name);
12719
+ if (getMcpAuthStatus(authState, focusedEntry.config.name).kind === "authorizing") {
12720
+ onCancelLogin(focusedEntry.config.name);
12339
12721
  return;
12340
12722
  }
12341
12723
  }
@@ -12348,21 +12730,36 @@ function McpsSettingsModal({ catalog, errors, onLogin, onLogout, onCancelLogin,
12348
12730
  moveCursor(1);
12349
12731
  return;
12350
12732
  }
12351
- if (catalog.length === 0) return;
12352
- const entry = catalog[safeCursor];
12353
- if (!entry) return;
12354
- const name = entry.config.name;
12355
- 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
+ }
12356
12750
  if (key.name === "return" || key.name === "space") {
12357
- 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);
12358
12753
  return;
12359
12754
  }
12360
- if (key.name === "l" && canLogin(entry, status)) {
12361
- 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);
12362
12759
  return;
12363
12760
  }
12364
12761
  if (key.name === "o" && canLogout(status)) {
12365
- onLogout(name);
12762
+ onLogout(focusedRow.entry.config.name);
12366
12763
  return;
12367
12764
  }
12368
12765
  if (key.name === "r" && onRefresh) onRefresh();
@@ -12445,46 +12842,185 @@ function McpsSettingsModal({ catalog, errors, onLogin, onLogout, onCancelLogin,
12445
12842
  })
12446
12843
  ]
12447
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
+ }
12448
12862
  return /* @__PURE__ */ jsxs(Modal, {
12449
- title: ` mcp servers · ${enabledSet.size} / ${catalog.length} enabled `,
12863
+ title: ` mcp servers · ${serverEnabledSet.size} / ${catalog.length} enabled `,
12450
12864
  children: [
12451
12865
  renderErrors(errors, home, COLOR.warn),
12452
12866
  /* @__PURE__ */ jsx("box", {
12453
12867
  style: { flexDirection: "column" },
12454
- children: catalog.map((entry, i) => {
12455
- const focused = i === safeCursor;
12456
- const name = entry.config.name;
12457
- const enabled = enabledSet.has(name);
12458
- const status = getMcpAuthStatus(authState, name);
12459
- return /* @__PURE__ */ jsxs("text", {
12460
- fg: focused ? COLOR.brand : COLOR.dim,
12461
- children: [
12462
- /* @__PURE__ */ jsx("span", {
12463
- fg: focused ? COLOR.brand : COLOR.mute,
12464
- children: focused ? "▶ " : " "
12465
- }),
12466
- /* @__PURE__ */ jsx("span", {
12467
- fg: enabled ? COLOR.accent : COLOR.mute,
12468
- children: enabled ? "[✓] " : "[ ] "
12469
- }),
12470
- /* @__PURE__ */ jsx("span", {
12471
- fg: focused ? COLOR.brand : COLOR.dim,
12472
- children: name
12473
- }),
12474
- /* @__PURE__ */ jsxs("span", {
12475
- fg: COLOR.mute,
12476
- children: [" ", detailFor(entry)]
12477
- }),
12478
- renderInlineBadge(status, COLOR)
12479
- ]
12480
- }, name);
12481
- })
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}`))
12482
12890
  }),
12483
12891
  focusedEntry && focusedStatus && renderDetailPanel(focusedEntry, focusedStatus, COLOR),
12484
- renderActionHints(focusedEntry, focusedStatus, !!onRefresh, COLOR)
12892
+ renderActionHints({
12893
+ focusedRow,
12894
+ focusedEntry,
12895
+ focusedStatus,
12896
+ expanded,
12897
+ showRefresh: !!onRefresh,
12898
+ COLOR
12899
+ })
12485
12900
  ]
12486
12901
  });
12487
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
+ }
12488
13024
  function detailFor(entry) {
12489
13025
  const transport = entry.config.transport;
12490
13026
  return `${transport} · ${transport === "stdio" ? entry.config.command ?? "" : entry.config.url ?? ""}`;
@@ -12590,11 +13126,14 @@ function renderDetailPanel(entry, status, COLOR) {
12590
13126
  });
12591
13127
  return null;
12592
13128
  }
12593
- function renderActionHints(entry, status, showRefresh, COLOR) {
12594
- const effectiveStatus = status ?? { kind: "idle" };
12595
- const canL = entry ? canLogin(entry, effectiveStatus) : false;
12596
- 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;
12597
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);
12598
13137
  return /* @__PURE__ */ jsxs("text", {
12599
13138
  fg: COLOR.mute,
12600
13139
  children: [
@@ -12608,6 +13147,22 @@ function renderActionHints(entry, status, showRefresh, COLOR) {
12608
13147
  children: "↵"
12609
13148
  }),
12610
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
+ ] }),
12611
13166
  canL && /* @__PURE__ */ jsxs("span", { children: [
12612
13167
  " · ",
12613
13168
  /* @__PURE__ */ jsx("span", {