zidane 6.4.3 → 6.5.0

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 (104) hide show
  1. package/README.md +1 -0
  2. package/dist/{acp-Kr7LyExq.js → acp-B-AOovHz.js} +3 -3
  3. package/dist/{acp-Kr7LyExq.js.map → acp-B-AOovHz.js.map} +1 -1
  4. package/dist/acp-cli.js +3 -2
  5. package/dist/acp-cli.js.map +1 -1
  6. package/dist/acp.d.ts +1 -1
  7. package/dist/acp.js +1 -1
  8. package/dist/{agent-CEvzc1Ao.js → agent-SQ6rxfpT.js} +298 -110
  9. package/dist/agent-SQ6rxfpT.js.map +1 -0
  10. package/dist/agent.d.ts +1 -1
  11. package/dist/agent.js +2 -2
  12. package/dist/chat/pure.d.ts +3 -3
  13. package/dist/chat.d.ts +13 -44
  14. package/dist/chat.d.ts.map +1 -1
  15. package/dist/chat.js +5 -4
  16. package/dist/chat.js.map +1 -1
  17. package/dist/{context-transport-DbzJctSl.js → context-transport-CDBatvQc.js} +3 -3
  18. package/dist/{context-transport-DbzJctSl.js.map → context-transport-CDBatvQc.js.map} +1 -1
  19. package/dist/contexts/daytona.d.ts +1 -1
  20. package/dist/contexts/e2b.d.ts +1 -1
  21. package/dist/eval.d.ts +1 -1
  22. package/dist/extensions.d.ts +2 -2
  23. package/dist/extensions.js +2 -2
  24. package/dist/headless.d.ts +1 -1
  25. package/dist/headless.js +1 -1
  26. package/dist/{index-W_8010Lt.d.ts → index-CWBSrqI6.d.ts} +931 -3
  27. package/dist/index-CWBSrqI6.d.ts.map +1 -0
  28. package/dist/index.d.ts +2 -2
  29. package/dist/index.js +8 -7
  30. package/dist/index.js.map +1 -1
  31. package/dist/{login-Blkiy-58.js → login-XZDS4K2o.js} +3 -3
  32. package/dist/{login-Blkiy-58.js.map → login-XZDS4K2o.js.map} +1 -1
  33. package/dist/{mcp-CDN-m5lp.js → mcp-D27rP47I.js} +87 -6
  34. package/dist/mcp-D27rP47I.js.map +1 -0
  35. package/dist/mcp.d.ts +2 -2
  36. package/dist/mcp.js +3 -2
  37. package/dist/origin-pinned-fetch-CqG8VZIq.js +159 -0
  38. package/dist/origin-pinned-fetch-CqG8VZIq.js.map +1 -0
  39. package/dist/output/stream-json.d.ts +1 -1
  40. package/dist/output/terminal.d.ts +1 -1
  41. package/dist/plugins-2PMTVcp-.js +1174 -0
  42. package/dist/plugins-2PMTVcp-.js.map +1 -0
  43. package/dist/plugins.d.ts +2 -0
  44. package/dist/plugins.js +2 -0
  45. package/dist/presets.d.ts +1 -1
  46. package/dist/presets.js +1 -1
  47. package/dist/providers/anthropic.d.ts +1 -1
  48. package/dist/providers/arcee.d.ts +1 -1
  49. package/dist/providers/baseten.d.ts +1 -1
  50. package/dist/providers/cerebras.d.ts +1 -1
  51. package/dist/providers/local.d.ts +1 -1
  52. package/dist/providers/openai-compat.d.ts +1 -1
  53. package/dist/providers/openai.d.ts +1 -1
  54. package/dist/providers/openrouter.d.ts +1 -1
  55. package/dist/providers/xai.d.ts +1 -1
  56. package/dist/providers.d.ts +1 -1
  57. package/dist/restate.d.ts +52 -2
  58. package/dist/restate.d.ts.map +1 -1
  59. package/dist/restate.js +156 -36
  60. package/dist/restate.js.map +1 -1
  61. package/dist/session/sqlite.d.ts +1 -1
  62. package/dist/session.d.ts +1 -1
  63. package/dist/{resolve-CZJxds85.js → skills-Cp9vnBwd.js} +1430 -1006
  64. package/dist/skills-Cp9vnBwd.js.map +1 -0
  65. package/dist/skills.d.ts +1 -1
  66. package/dist/skills.js +1 -2
  67. package/dist/{stdio-DWQKYKad.js → stdio-O-m9WVkE.js} +2 -2
  68. package/dist/{stdio-DWQKYKad.js.map → stdio-O-m9WVkE.js.map} +1 -1
  69. package/dist/{stdio-loader-3uz37d_3.js → stdio-loader-BSGQmG4L.js} +2 -2
  70. package/dist/{stdio-loader-3uz37d_3.js.map → stdio-loader-BSGQmG4L.js.map} +1 -1
  71. package/dist/{tool-formatters-bhC2Xp8z.d.ts → tool-formatters-gFqQbzPY.d.ts} +2 -2
  72. package/dist/{tool-formatters-bhC2Xp8z.d.ts.map → tool-formatters-gFqQbzPY.d.ts.map} +1 -1
  73. package/dist/{read-state-Dt7UbVsd.js → tool-state-DTs-3ZHH.js} +218 -2
  74. package/dist/tool-state-DTs-3ZHH.js.map +1 -0
  75. package/dist/tools/fetch-url.d.ts +1 -1
  76. package/dist/tools/web-search.d.ts +1 -1
  77. package/dist/tools.d.ts +1 -1
  78. package/dist/tools.js +2 -2
  79. package/dist/{transcript-anchors-D7wVCZ2S.js → transcript-anchors-D4IxJifV.js} +42 -5
  80. package/dist/transcript-anchors-D4IxJifV.js.map +1 -0
  81. package/dist/{transcript-anchors-mcMLuXM1.d.ts → transcript-anchors-qCs3wZpu.d.ts} +98 -92
  82. package/dist/transcript-anchors-qCs3wZpu.d.ts.map +1 -0
  83. package/dist/tui.d.ts +38 -4
  84. package/dist/tui.d.ts.map +1 -1
  85. package/dist/tui.js +893 -148
  86. package/dist/tui.js.map +1 -1
  87. package/dist/{turn-operations-B5vVcG0Z.d.ts → turn-operations-M9-jUyDd.d.ts} +3 -3
  88. package/dist/{turn-operations-B5vVcG0Z.d.ts.map → turn-operations-M9-jUyDd.d.ts.map} +1 -1
  89. package/dist/types-BIarq1qE.js.map +1 -1
  90. package/dist/types.d.ts +2 -2
  91. package/docs/ARCHITECTURE.md +2 -0
  92. package/docs/PLUGINS.md +340 -0
  93. package/docs/RESTATE.md +133 -3
  94. package/docs/SKILL.md +24 -8
  95. package/package.json +6 -1
  96. package/dist/agent-CEvzc1Ao.js.map +0 -1
  97. package/dist/index-W_8010Lt.d.ts.map +0 -1
  98. package/dist/mcp-CDN-m5lp.js.map +0 -1
  99. package/dist/read-state-Dt7UbVsd.js.map +0 -1
  100. package/dist/resolve-CZJxds85.js.map +0 -1
  101. package/dist/skills-Dd6XCi4s.js +0 -420
  102. package/dist/skills-Dd6XCi4s.js.map +0 -1
  103. package/dist/transcript-anchors-D7wVCZ2S.js.map +0 -1
  104. package/dist/transcript-anchors-mcMLuXM1.d.ts.map +0 -1
package/dist/tui.js CHANGED
@@ -1,13 +1,14 @@
1
- import { $t as toggleChainEntry, A as redirectUriFromAuthUrl, At as resolveTheme, Bt as useDiscovery, Cr as sumRunCosts, Ct as SETTINGS_TOGGLES, D as formatPathForCwd, Dr as updateToolEventOutcomes, Er as toolResultText, Et as useSettings, F as loadMcpToolsCache, G as parentServerName, Gt as resolveStoragePaths, Ht as ConfigProvider, I as mcpToolsCachePath, Jt as chainsEqual, K as createFileMcpCredentialStore, Kn as pruneTodosByRun, Kt as buildRunModel, L as refreshMcpToolsCatalog, M as runOAuthLogin, N as supportsOAuth, Nn as accentColor, O as fetchOAuthRedirect, Or as usageByModel, Q as getMcpAuthStatus, Rt as createDiscoverySlot, Sr as stripSpawnTokensLine, St as SETTINGS_INPUTS, T as suggestSafelistEntries, Tn as discoverAgentsMd, Tr as toolCallPreview, Tt as clampFps, U as buildVisibleMcpRows, Ut as useConfig, V as useMcpToolToggleMap, Vt as useDiscoveryOptional, W as indexOfServerRow, Wt as resolveConfig, X as useMcpAuthDispatch, Xt as providerKeySetChanged, Y as McpAuthProvider, Yn as useActiveTodos, Yt as filterChainFallbacks, Z as useMcpAuthState, Zt as restoreChainFallbacks, _ as useSafeModeQueue, _r as listSessionMeta, _t as listProjectFiles, a as useSurfaces, an as removeMcpServer, b as getSafelist, bt as SETTINGS_CATEGORIES, c as useStreamBuffer, cr as envSection, d as discoverProjectSkills, dn as tryOpenBrowser, dt as EMPTY_HINTS, en as useCompletion, et as ASK_USER_TOOL, fr as compactSummaryEvent, ft as clipHintsToWidth, g as useSafeModeActions, gr as lastContextSizeFromTurns, h as SafeModeProvider, hn as useUpdateCheck, hr as eventsFromTurns, ht as buildHints, i as useSelectStyle, in as mcpsWritePath, it as createInteractionTools, j as oauthUsesManualCodePaste, kt as resolveChipColor, l as buildSkillsConfig, ln as discoverProjectMcps, lr as chatReferenceTokensForPrompt, lt as useInteractionsActions, m as writeSessionExport, mn as buildUpdateHint, mr as deriveSessionTitle, mt as truncateTrailing, n as ThemeProvider, nn as hasClaudeCodeSetup, nt as PRESENT_PLAN_TOOL, on as upsertMcpServer, or as buildBuildSystem, ot as makeRequestInteraction, pn as bootTick, pt as hintsLength, qt as chainPositionOf, r as useColors, rn as importFromClaudeCode, rt as buildResumedToolResultsTurn, s as useTheme, sn as buildMcpServers, sr as buildPlanSystem, st as pendingInteractionsFromTurns, t as computeTurnAnchors, tn as countClaudeSessionFiles, tt as InteractionsProvider, u as defaultSkillScanPaths, ur as createChatTurnsReadTool, ut as useInteractionsQueue, vr as loadState, vt as useEnabledToggleSet, wn as chatAutoCompactBehavior, wt as SettingsProvider, x as isOnSafelist, xr as serverToolResultSummary, xt as SETTINGS_CHOICES, y as addToSafelist, yr as marginTopFor, yt as DEFAULT_SETTINGS, z as subscribeMcpToolsCache, zn as TODO_STATUS_GLYPHS, zt as DiscoveryProvider } from "./transcript-anchors-D7wVCZ2S.js";
2
- import { $ as removeInstalledExtension, At as looksBinary, C as resolveTasksDir, E as selectFilesFromSession, Gt as KEYBINDING_KEY_COL_WIDTH, Jt as groupBindings, K as createExtensionPromptBridge, Kt as ensureKeybindingsFile, Mt as createModelSearchTool, O as compactConversation, Ot as tailTruncate, S as resolveSessionDir, Sn as effectiveContextWindow, V as summaryToTurn, Xt as matchesBinding, Y as installExtensionFromSpec, Yt as keybindingsPath, _ as rehomePersistedStubs, _n as resolveExtensionConfigValue, at as composeExtensionPreset, b as resolveMcpWarningsDir, c as applyCompactSummaryCutoff, et as updateInstalledExtension, ht as basic_default, ln as parseExtensionScanPathEnv, lt as partitionExtensionCatalog, m as cleanupPersistedSession, nt as discoverAndBuildExtensionRegistry, ot as disposeExtensionRegistry, pn as coerceExtensionConfigInput, q as ExtensionInstallError, qt as formatBindingForDisplay, rn as registerExtensionHostModules, sn as inlineLoadedExtension, st as filterExtensionRegistry, t as createAgent, tn as resolveExtensionKeybindings, tt as createExtensionHud, un as scanExtensionDirectories, ut as createExtensionHostRuntimeBridge, v as resolveLegacyPersistDir, vn as sanitizeExtensionConfigActionPatch, w as buildPostCompactAttachments, x as resolvePersistDir, xn as OUTPUT_RESERVE_TOKENS, xt as isSpawnTool, y as resolveLegacySessionDir } from "./agent-CEvzc1Ao.js";
1
+ import { $t as toggleChainEntry, A as redirectUriFromAuthUrl, At as resolveTheme, Bn as TODO_STATUS_GLYPHS, Bt as useDiscovery, Cr as stripSpawnTokensLine, Ct as SETTINGS_TOGGLES, D as formatPathForCwd, Dr as toolResultText, En as discoverAgentsMd, Er as toolCallPreview, Et as useSettings, F as loadMcpToolsCache, G as parentServerName, Gt as resolveStoragePaths, Ht as ConfigProvider, I as mcpToolsCachePath, Jt as chainsEqual, K as createFileMcpCredentialStore, Kt as buildRunModel, L as refreshMcpToolsCatalog, M as runOAuthLogin, N as supportsOAuth, O as fetchOAuthRedirect, Or as updateToolEventOutcomes, Pn as accentColor, Q as getMcpAuthStatus, Rt as createDiscoverySlot, Sr as serverToolResultSummary, St as SETTINGS_INPUTS, T as suggestSafelistEntries, Tn as chatAutoCompactBehavior, Tt as clampFps, U as buildVisibleMcpRows, Ut as useConfig, V as useMcpToolToggleMap, Vt as useDiscoveryOptional, W as indexOfServerRow, Wt as resolveConfig, X as useMcpAuthDispatch, Xn as useActiveTodos, Xt as providerKeySetChanged, Y as McpAuthProvider, Yt as filterChainFallbacks, Z as useMcpAuthState, Zt as restoreChainFallbacks, _ as useSafeModeQueue, _r as lastContextSizeFromTurns, _t as listProjectFiles, a as useSurfaces, an as removeMcpServer, b as getSafelist, br as marginTopFor, bt as SETTINGS_CATEGORIES, c as useStreamBuffer, cn as dedupMcpServersByName, cr as buildPlanSystem, d as discoverProjectSkills, dr as createChatTurnsReadTool, dt as EMPTY_HINTS, en as useCompletion, et as ASK_USER_TOOL, fn as tryOpenBrowser, ft as clipHintsToWidth, g as useSafeModeActions, gn as useUpdateCheck, gr as eventsFromTurns, h as SafeModeProvider, hn as buildUpdateHint, hr as deriveSessionTitle, ht as buildHints, i as useSelectStyle, in as mcpsWritePath, it as createInteractionTools, j as oauthUsesManualCodePaste, kr as usageByModel, kt as resolveChipColor, l as buildSkillsConfig, lr as envSection, lt as useInteractionsActions, m as writeSessionExport, mn as bootTick, mt as truncateTrailing, n as ThemeProvider, nn as hasClaudeCodeSetup, nt as PRESENT_PLAN_TOOL, on as upsertMcpServer, ot as makeRequestInteraction, pr as compactSummaryEvent, pt as hintsLength, qn as pruneTodosByRun, qt as chainPositionOf, r as useColors, rn as importFromClaudeCode, rt as buildResumedToolResultsTurn, s as useTheme, sn as buildMcpServers, sr as buildBuildSystem, st as pendingInteractionsFromTurns, t as computeTurnAnchors, tn as countClaudeSessionFiles, tt as InteractionsProvider, u as defaultSkillScanPaths, un as discoverProjectMcps, ur as chatReferenceTokensForPrompt, ut as useInteractionsQueue, vr as listSessionMeta, vt as useEnabledToggleSet, wr as sumRunCosts, wt as SettingsProvider, x as isOnSafelist, xt as SETTINGS_CHOICES, y as addToSafelist, yr as loadState, yt as DEFAULT_SETTINGS, z as subscribeMcpToolsCache, zt as DiscoveryProvider } from "./transcript-anchors-D4IxJifV.js";
2
+ import { $ as removeInstalledExtension, C as resolveTasksDir, Cn as effectiveContextWindow, E as selectFilesFromSession, Gt as KEYBINDING_KEY_COL_WIDTH, Jt as groupBindings, K as createExtensionPromptBridge, Kt as ensureKeybindingsFile, Nt as createModelSearchTool, O as compactConversation, S as resolveSessionDir, Sn as OUTPUT_RESERVE_TOKENS, St as isSpawnTool, V as summaryToTurn, Xt as matchesBinding, Y as installExtensionFromSpec, Yt as keybindingsPath, _ as rehomePersistedStubs, at as composeExtensionPreset, b as resolveMcpWarningsDir, c as applyCompactSummaryCutoff, ct as filterRegistryPlugins, dn as createDebugLog, dt as createExtensionHostRuntimeBridge, et as updateInstalledExtension, gt as basic_default, jt as looksBinary, kt as tailTruncate, ln as parseExtensionScanPathEnv, m as cleanupPersistedSession, mn as coerceExtensionConfigInput, nt as discoverAndBuildExtensionRegistry, ot as disposeExtensionRegistry, q as ExtensionInstallError, qt as formatBindingForDisplay, rn as registerExtensionHostModules, sn as inlineLoadedExtension, st as filterExtensionRegistry, t as createAgent, tn as resolveExtensionKeybindings, tt as createExtensionHud, un as scanExtensionDirectories, ut as partitionExtensionCatalog, v as resolveLegacyPersistDir, vn as resolveExtensionConfigValue, w as buildPostCompactAttachments, x as resolvePersistDir, y as resolveLegacySessionDir, yn as sanitizeExtensionConfigActionPatch } from "./agent-SQ6rxfpT.js";
3
3
  import { a as formatDuration, c as previewLine, i as fmtTokens, l as shortId, n as compactPath, o as formatTaskStatus, r as fmtCost, s as formatTaskSummary, t as ageString } from "./format-BF745pLL.js";
4
4
  import { n as appendCapped } from "./output-cap-C9zukcCj.js";
5
5
  import { i as killProcessGroup, n as SUPPORTS_PROCESS_GROUPS, r as createProcessContext } from "./contexts-DWVt7MvF.js";
6
6
  import { u as errorMessage } from "./errors-BwBxu3zD.js";
7
- import { M as findGitRoot } from "./resolve-CZJxds85.js";
7
+ import { R as projectUserPaths, c as SkillInstallError, l as installSkillFromSpec, z as findGitRoot } from "./skills-Cp9vnBwd.js";
8
+ import { n as defaultPluginDataRoot, r as loadAgentPlugin } from "./plugins-2PMTVcp-.js";
8
9
  import { a as createRingBufferSink, i as createLoggingHooks, l as logFileTimestamp, n as createFileSink, o as fanoutSink, r as createLogger, s as formatLogLine, t as consoleSink } from "./logger-DBX9uYOw.js";
9
10
  import { i as sniffImageMediaType } from "./media-sniff-UKijEaiQ.js";
10
- import { g as hasAuthorizationHeader, m as McpOAuthProvider, n as connectMcpServers, s as nonOwningMcpView } from "./mcp-CDN-m5lp.js";
11
+ import { _ as hasAuthorizationHeader, h as McpOAuthProvider, n as connectMcpServers, s as nonOwningMcpView } from "./mcp-D27rP47I.js";
11
12
  import { C as estimateRebilledCostUsd } from "./openai-compat-B3c_hzwf.js";
12
13
  import { replaceDynamicSection } from "./prompt.js";
13
14
  import { a as getExtensionSessionState, c as setExtensionSessionState, i as demoteStaleRunningStatus, o as loadSession, t as createSession } from "./session-cr9VdoEO.js";
@@ -16,18 +17,17 @@ import { o as withTimeout } from "./timeout-BDetbuN6.js";
16
17
  import { i as installCacheBreakLogger } from "./cache-telemetry-ehVdWsg2.js";
17
18
  import { a as generateStructuredData, c as shouldAutoGenerateTitle, f as commandBlockToEvent, g as parseCommandInvocation, h as executeCommand, n as patchInlineGhost, p as commandBlocksToContent, r as resolveInlineSuggestion, s as generateSessionTitle, t as createPathAutocompleteProvider } from "./inline-autocomplete-D0MPvBaS.js";
18
19
  import { s as resolveCompletionSubmit } from "./completion-core-BRo86R_L.js";
19
- import { i as installSkillFromSpec, r as SkillInstallError } from "./skills-Dd6XCi4s.js";
20
- import { a as ZIDANE_EXTENSION_SKILL_NAME, d as extensionScaffold, f as readSkillEditMarker, h as withZidaneSkillEditSkill, i as SKILL_EDIT_METADATA_KEY, l as editSessionMetadata, m as withZidaneExtensionSkill, o as ZIDANE_SKILL_EDIT_SKILL_NAME, t as loginMcpServer, u as extensionDirForCwd } from "./login-Blkiy-58.js";
20
+ import { a as ZIDANE_EXTENSION_SKILL_NAME, d as extensionScaffold, f as readSkillEditMarker, h as withZidaneSkillEditSkill, i as SKILL_EDIT_METADATA_KEY, l as editSessionMetadata, m as withZidaneExtensionSkill, o as ZIDANE_SKILL_EDIT_SKILL_NAME, t as loginMcpServer, u as extensionDirForCwd } from "./login-XZDS4K2o.js";
21
21
  import { C as restoreModelOptions, S as piIdOf, _ as modelOptionsFor, m as getContextWindow, p as enabledModelOptions, s as setProviderCredential, t as detectAuth, v as modelSupportsReasoning, w as renderDefaultCallbackPage } from "./auth-BzYbbdN_.js";
22
22
  import { createDockerContext } from "./contexts/docker.js";
23
23
  import { createTuiStore } from "./session/sqlite.js";
24
24
  import { $ as buildContextualDiff, A as createSkillsCompletionProvider, B as EDIT_TOOL_NAMES, C as chunkMarkdownForRender, D as splitMarkdownCodeBlocks, E as normalizeMarkdownCodeFences, G as isVisibleDuringSearch, H as isTurnHighlighted, I as createCommandsCompletionProvider, J as selectableTurnIds, K as promptHistoryFromEvents, L as blendHsl, N as createFilesCompletionProvider, O as stripHtmlCommentsOutsideCode, R as buildLinearRamp, T as codeTokenHasClosingFence, U as isUserBashEvent, V as isEditErrorResult, W as isVisible, X as streamEventSearchText, Y as selectionAfterTurnRemoval, Z as turnSelectionOwnership, _ as turnContextSize, _t as summarizeOutcomes, a as TOOL_DISPLAY, at as filetypeFromPath, b as filterModelCatalog, bt as createChatsCompletionProvider, ct as summarizeEditPayload, d as finalizeStreamingMarkdown, et as buildUnifiedDiff, f as finalizeStreamingMarkdownForOwner, ft as mergeApprovalAndBodyOutcomes, g as revealEarlierTranscriptEvents, gt as stripEditOutcomesAnnotation, h as replaceTranscriptTurnEvents, ht as rewriteMultiEditHeader, i as turnAsText, it as extractEditPayload, j as uniqueSkillNamesFromReferences, mt as resolveApprovalForPayload, n as deleteTurnSafely, o as displayNameFor, ot as previewEditPayload, p as hiddenEarlierTranscriptCount, pt as parseEditOutcomesFromResult, q as retagUserPromptEvents, r as truncateTurnsAt, s as formatToolCall, tt as clipDiffForRender, u as capTranscriptEvents, ut as buildEditOutcomesAnnotation, v as splitPromptSegments, w as closeDanglingCodeFence, y as buildModelCatalog, z as BASH_CALL_ID_PREFIX } from "./turn-operations-CAvi8NYt.js";
25
25
  import { a as searchHighlightRanges, c as searchSessionHistoryIndexAsync, i as highlightSegments, n as buildSessionSearchIndexAsync, r as filterSessionSearchCatalog, u as searchSessionIndexAsync } from "./history-search-C4bk1Wz_.js";
26
- import { basename, dirname, join, relative, resolve } from "node:path";
26
+ import { basename, delimiter, dirname, join, relative, resolve } from "node:path";
27
27
  import { Buffer } from "node:buffer";
28
28
  import * as fs from "node:fs";
29
29
  import { createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
30
- import { readFile, unlink } from "node:fs/promises";
30
+ import { readFile, readdir, stat, unlink } from "node:fs/promises";
31
31
  import { spawn, spawnSync } from "node:child_process";
32
32
  import { homedir, tmpdir } from "node:os";
33
33
  import { Fzf, byLengthAsc } from "fzf";
@@ -43,6 +43,144 @@ import * as OpenTuiReactJsxDevRuntime from "@opentui/react/jsx-dev-runtime";
43
43
  import * as OpenTuiReactJsxRuntime from "@opentui/react/jsx-runtime";
44
44
  import { Fragment, jsx as jsx$1, jsxs } from "@opentui/react/jsx-runtime";
45
45
  import * as ReactJsxDevRuntime from "react/jsx-dev-runtime";
46
+ //#region src/chat/plugins-discovery.ts
47
+ /** Subdirectory each search root is probed for. */
48
+ const PLUGINS_SUBPATH = "plugins";
49
+ /**
50
+ * Resolve the plugin search roots. First-found wins on name collision;
51
+ * earlier entries take precedence.
52
+ *
53
+ * Search order — see {@link projectUserPaths}:
54
+ *
55
+ * 1. `{project}/.agents/plugins`
56
+ * 2. `{project}/.zidane/plugins`
57
+ * 3. `~/.agents/plugins`
58
+ * 4. `~/.zidane/plugins`
59
+ *
60
+ * Non-existent paths are returned; the walk skips them.
61
+ *
62
+ * Unlike skills and MCP servers there is no foreign-agent or Ruler variant:
63
+ * Agent Plugins is a vendor-neutral format, so `.agents/plugins` IS the
64
+ * cross-tool location. Nothing to import from a competitor's directory.
65
+ */
66
+ function defaultPluginScanPaths(opts = {}) {
67
+ return projectUserPaths({
68
+ subPath: PLUGINS_SUBPATH,
69
+ ...opts
70
+ });
71
+ }
72
+ /**
73
+ * Parse a platform-delimited list of additional plugin scan roots. Mirrors
74
+ * `parseExtensionScanPathEnv` in `../extensions/discovery`:
75
+ *
76
+ * ZIDANE_PLUGIN_DIRS=/repo/plugins bun run tui
77
+ *
78
+ * Each path is a scan ROOT whose immediate children are plugin directories
79
+ * (`<root>/<name>/plugin.json`), not a single plugin directory.
80
+ *
81
+ * This exists because the four default roots are all dot-directories, and
82
+ * `.zidane/` is gitignored — so a repo has nowhere TRACKED to keep example or
83
+ * first-party plugins for local dogfooding. Extensions solved the same
84
+ * problem the same way.
85
+ */
86
+ function parsePluginScanPathEnv(value = process.env.ZIDANE_PLUGIN_DIRS) {
87
+ if (!value) return [];
88
+ return value.split(delimiter).map((p) => p.trim()).filter(Boolean);
89
+ }
90
+ /**
91
+ * Load every plugin reachable from the default search roots.
92
+ *
93
+ * Each immediate child directory of a root is one plugin — the same
94
+ * depth-1 rule the spec applies inside `skills/`. A child that isn't a
95
+ * directory, or that holds no `plugin.json`, is silently skipped: dropping a
96
+ * README next to your plugins is not an error.
97
+ *
98
+ * Dedup is by plugin NAME, not directory name, because the manifest owns
99
+ * identity — two directories called `tools/` shipping different plugins are
100
+ * two plugins, and one plugin vendored at two scopes is one plugin.
101
+ */
102
+ async function discoverProjectPlugins(opts = {}) {
103
+ const roots = [...defaultPluginScanPaths(opts), ...(opts.extraRoots ?? []).map((path) => ({
104
+ path,
105
+ source: "project"
106
+ }))].filter((p) => existsSync(p.path));
107
+ const dataRoot = defaultPluginDataRoot({
108
+ ...opts.prefix !== void 0 ? { prefix: opts.prefix } : {},
109
+ ...opts.home !== void 0 ? { home: opts.home } : {}
110
+ });
111
+ const plugins = [];
112
+ const errors = [];
113
+ const seen = /* @__PURE__ */ new Set();
114
+ for (const root of roots) {
115
+ if (opts.signal?.aborted) break;
116
+ let entries;
117
+ try {
118
+ entries = await readdir(root.path);
119
+ } catch (err) {
120
+ errors.push({
121
+ path: root.path,
122
+ source: root.source,
123
+ message: `Could not read plugin directory: ${err instanceof Error ? err.message : String(err)}`
124
+ });
125
+ continue;
126
+ }
127
+ for (const entry of entries.sort()) {
128
+ if (opts.signal?.aborted) break;
129
+ const dir = join(root.path, entry);
130
+ try {
131
+ if (!(await stat(dir)).isDirectory()) continue;
132
+ } catch {
133
+ continue;
134
+ }
135
+ const loaded = await loadAgentPlugin(dir, {
136
+ source: root.source,
137
+ dataRoot
138
+ });
139
+ if (!loaded.ok && loaded.diagnostics.some((d) => d.code === "manifest-missing")) continue;
140
+ const name = loaded.manifest?.name ?? basename(dir);
141
+ if (seen.has(name)) continue;
142
+ seen.add(name);
143
+ plugins.push({
144
+ name,
145
+ plugin: loaded,
146
+ source: root.source,
147
+ path: dir
148
+ });
149
+ }
150
+ }
151
+ return {
152
+ plugins,
153
+ errors
154
+ };
155
+ }
156
+ /**
157
+ * Map the user's enabled-plugin list onto the contributions a host merges
158
+ * into the agent.
159
+ *
160
+ * Conventions match `enabledSkills` / `enabledMcps`:
161
+ * - `undefined` → every discovered plugin is enabled (default)
162
+ * - `[]` → all off
163
+ * - `[names]` → allowlist
164
+ *
165
+ * A plugin that failed to load contributes nothing regardless of the
166
+ * allowlist — `ok: false` means components were never discovered.
167
+ */
168
+ function buildPluginContributions(opts) {
169
+ const allow = opts.enabled === void 0 ? null : new Set(opts.enabled);
170
+ const skills = [];
171
+ const mcpServers = [];
172
+ for (const entry of opts.discovered) {
173
+ if (!entry.plugin.ok) continue;
174
+ if (allow !== null && !allow.has(entry.name)) continue;
175
+ skills.push(...entry.plugin.skills);
176
+ mcpServers.push(...entry.plugin.mcpServers);
177
+ }
178
+ return {
179
+ skills,
180
+ mcpServers
181
+ };
182
+ }
183
+ //#endregion
46
184
  //#region src/mcp/warn-log.ts
47
185
  /**
48
186
  * Subscribe to `mcp:warn`
@@ -5620,9 +5758,7 @@ function useProjectLocation() {
5620
5758
  const FILES_REFRESH_THROTTLE_MS = 3e3;
5621
5759
  const SKILLS_REFRESH_THROTTLE_MS = 3e4;
5622
5760
  const MCP_TOOLS_CACHE_POLL_MS = 4e3;
5623
- function debugLog$1(...args) {
5624
- if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/tui] ${args.map(errorMessage).join(" ")}\n`);
5625
- }
5761
+ const debugLog$1 = createDebugLog("tui");
5626
5762
  function DiscoveryShell({ children }) {
5627
5763
  const config = useConfig();
5628
5764
  const { settings } = useSettings();
@@ -5635,6 +5771,7 @@ function DiscoveryShell({ children }) {
5635
5771
  const dataDir = config.paths.userDir;
5636
5772
  const mcpCredentialStore = useMemo(() => createFileMcpCredentialStore(dataDir), [dataDir]);
5637
5773
  const [skillsCatalog, setSkillsCatalog] = useState([]);
5774
+ const [pluginsCatalog, setPluginsCatalog] = useState([]);
5638
5775
  const [mcpsCatalog, setMcpsCatalog] = useState([]);
5639
5776
  const [mcpsErrors, setMcpsErrors] = useState([]);
5640
5777
  const [filesCatalog, setFilesCatalog] = useState([]);
@@ -5674,13 +5811,16 @@ function DiscoveryShell({ children }) {
5674
5811
  }, [mcpCredentialStore]);
5675
5812
  const filesSlotRef = useRef(null);
5676
5813
  const skillsSlotRef = useRef(null);
5814
+ const pluginsSlotRef = useRef(null);
5677
5815
  /** Last-published MCP discovery result — see the value-compare in `refreshMcps`. */
5678
5816
  const mcpsFingerprintRef = useRef(null);
5679
5817
  useEffect(() => {
5680
5818
  filesSlotRef.current?.abort();
5681
5819
  skillsSlotRef.current?.abort();
5820
+ pluginsSlotRef.current?.abort();
5682
5821
  setFilesCatalog([]);
5683
5822
  setSkillsCatalog([]);
5823
+ setPluginsCatalog([]);
5684
5824
  const filesSlot = createDiscoverySlot({
5685
5825
  throttleMs: FILES_REFRESH_THROTTLE_MS,
5686
5826
  walk: (signal) => listProjectFiles({
@@ -5709,9 +5849,30 @@ function DiscoveryShell({ children }) {
5709
5849
  debugLog$1(`discoverProjectSkills ${phase} failed`, err);
5710
5850
  }
5711
5851
  });
5852
+ const pluginsSlot = createDiscoverySlot({
5853
+ throttleMs: SKILLS_REFRESH_THROTTLE_MS,
5854
+ walk: async (signal) => {
5855
+ const { plugins, errors } = await discoverProjectPlugins({
5856
+ cwd: projectDir,
5857
+ prefix: config.prefix,
5858
+ ...config.pluginScanPaths.length > 0 ? { extraRoots: config.pluginScanPaths } : {},
5859
+ ...signal ? { signal } : {}
5860
+ });
5861
+ for (const error of errors) debugLog$1("discoverProjectPlugins", `${error.source}: ${error.path} — ${error.message}`);
5862
+ return plugins;
5863
+ },
5864
+ onLoad: (items) => {
5865
+ if (pluginsSlotRef.current === pluginsSlot) setPluginsCatalog(items);
5866
+ },
5867
+ onError: (err, phase) => {
5868
+ debugLog$1(`discoverProjectPlugins ${phase} failed`, err);
5869
+ }
5870
+ });
5712
5871
  filesSlotRef.current = filesSlot;
5713
5872
  skillsSlotRef.current = skillsSlot;
5873
+ pluginsSlotRef.current = pluginsSlot;
5714
5874
  skillsSlot.ensure().then((skills) => bootTick(`discovery:skills (${skills.length})`));
5875
+ pluginsSlot.ensure().then((plugins) => bootTick(`discovery:plugins (${plugins.length})`));
5715
5876
  try {
5716
5877
  const { servers, errors } = discoverProjectMcps({
5717
5878
  cwd: projectDir,
@@ -5733,6 +5894,7 @@ function DiscoveryShell({ children }) {
5733
5894
  return () => {
5734
5895
  filesSlot.abort();
5735
5896
  skillsSlot.abort();
5897
+ pluginsSlot.abort();
5736
5898
  };
5737
5899
  }, [
5738
5900
  projectDir,
@@ -5745,6 +5907,7 @@ function DiscoveryShell({ children }) {
5745
5907
  const ensureSkills = useCallback(() => skillsSlotRef.current?.ensure() ?? Promise.resolve([]), []);
5746
5908
  const refreshFiles = useCallback(() => filesSlotRef.current?.refresh() ?? Promise.resolve(), []);
5747
5909
  const refreshSkills = useCallback(() => skillsSlotRef.current?.refresh() ?? Promise.resolve(), []);
5910
+ const refreshPlugins = useCallback(() => pluginsSlotRef.current?.refresh() ?? Promise.resolve(), []);
5748
5911
  const refreshMcps = useCallback(() => {
5749
5912
  try {
5750
5913
  const { servers, errors } = discoverProjectMcps({
@@ -5791,22 +5954,26 @@ function DiscoveryShell({ children }) {
5791
5954
  return /* @__PURE__ */ jsx$1(DiscoveryProvider, {
5792
5955
  value: useMemo(() => ({
5793
5956
  skillsCatalog,
5957
+ pluginsCatalog,
5794
5958
  mcpsCatalog,
5795
5959
  mcpsErrors,
5796
5960
  mcpToolsByServer,
5797
5961
  filesCatalog,
5798
5962
  refreshSkills,
5963
+ refreshPlugins,
5799
5964
  refreshMcps,
5800
5965
  refreshFiles,
5801
5966
  ensureSkills,
5802
5967
  ensureFiles
5803
5968
  }), [
5804
5969
  skillsCatalog,
5970
+ pluginsCatalog,
5805
5971
  mcpsCatalog,
5806
5972
  mcpsErrors,
5807
5973
  mcpToolsByServer,
5808
5974
  filesCatalog,
5809
5975
  refreshSkills,
5976
+ refreshPlugins,
5810
5977
  refreshMcps,
5811
5978
  refreshFiles,
5812
5979
  ensureSkills,
@@ -8589,6 +8756,7 @@ function InteractionShell({ title, maxHeight, children }) {
8589
8756
  paddingRight: 1,
8590
8757
  flexDirection: "column",
8591
8758
  flexShrink: 0,
8759
+ overflow: "hidden",
8592
8760
  ...maxHeight !== void 0 ? { maxHeight } : {}
8593
8761
  },
8594
8762
  children
@@ -8817,6 +8985,25 @@ function PlanInteractionBlock({ request, onResolve }) {
8817
8985
  */
8818
8986
  const QUESTION_WIZARD_MAX_HEIGHT = "60%";
8819
8987
  /**
8988
+ * Rows the intro contributes to the wizard's content-sized height.
8989
+ *
8990
+ * A line count, plus the one-row bottom margin. Wrapping makes this a lower
8991
+ * bound, and long intros are capped: past {@link INTRO_ROWS_CAP} the panel would
8992
+ * hit its 60% ceiling anyway and the scrollbox takes over, so counting further
8993
+ * buys nothing. Zero when there is no intro, which keeps a bare confirm question
8994
+ * at its ~4-row size.
8995
+ */
8996
+ const INTRO_ROWS_CAP = 12;
8997
+ function introRows(intro) {
8998
+ if (!intro) return 0;
8999
+ let lines = 1;
9000
+ for (let i = 0; i < intro.length; i++) {
9001
+ if (intro[i] === "\n") lines++;
9002
+ if (lines > INTRO_ROWS_CAP) return 13;
9003
+ }
9004
+ return lines + 1;
9005
+ }
9006
+ /**
8820
9007
  * Estimated render height (rows) of one {@link QuestionRow}, mirroring its
8821
9008
  * actual layout: prompt line + optional description + 1-row bottom margin,
8822
9009
  * plus the status-dependent body — bordered text input (3) or textarea (5)
@@ -8900,12 +9087,23 @@ function QuestionInteractionBlock({ request, onResolve }) {
8900
9087
  });
8901
9088
  return () => cancelAnimationFrame(handle);
8902
9089
  }, [activeIndex, questions]);
8903
- const contentRows = useMemo(() => questions.reduce((sum, q, i) => sum + estimateQuestionRowHeight(q, i < activeIndex ? "done" : i === activeIndex ? "active" : "pending"), 0), [questions, activeIndex]);
9090
+ const contentRows = useMemo(() => introRows(intro) + questions.reduce((sum, q, i) => sum + estimateQuestionRowHeight(q, i < activeIndex ? "done" : i === activeIndex ? "active" : "pending"), 0), [
9091
+ intro,
9092
+ questions,
9093
+ activeIndex
9094
+ ]);
8904
9095
  return /* @__PURE__ */ jsxs(InteractionShell, {
8905
9096
  title: questions.length > 1 ? ` answer ${activeIndex + 1}/${questions.length} · esc to abort run ` : " answer · esc to abort run ",
8906
9097
  maxHeight: QUESTION_WIZARD_MAX_HEIGHT,
8907
- children: [
8908
- intro && /* @__PURE__ */ jsx$1("box", {
9098
+ children: [/* @__PURE__ */ jsxs("scrollbox", {
9099
+ ref: scrollboxRef,
9100
+ focusable: false,
9101
+ style: {
9102
+ height: contentRows,
9103
+ flexShrink: 1,
9104
+ minHeight: 0
9105
+ },
9106
+ children: [intro && /* @__PURE__ */ jsx$1("box", {
8909
9107
  style: {
8910
9108
  flexDirection: "column",
8911
9109
  marginBottom: 1,
@@ -8918,46 +9116,26 @@ function QuestionInteractionBlock({ request, onResolve }) {
8918
9116
  internalBlockMode: "coalesced",
8919
9117
  fg: COLOR.dim
8920
9118
  })
8921
- }),
8922
- /* @__PURE__ */ jsx$1("scrollbox", {
8923
- ref: scrollboxRef,
8924
- focusable: false,
8925
- style: {
8926
- height: contentRows,
8927
- flexShrink: 1
8928
- },
8929
- children: questions.map((q, i) => /* @__PURE__ */ jsx$1(QuestionRow, {
8930
- anchorId: anchorIdFor$1(q.id),
8931
- index: i,
8932
- question: q,
8933
- status: i < activeIndex ? "done" : i === activeIndex ? "active" : "pending",
8934
- answer: answers[q.id],
8935
- initialAnswer: i === activeIndex ? answers[q.id] : void 0,
8936
- onAnswer
8937
- }, q.id))
8938
- }),
8939
- questions.length > 1 && /* @__PURE__ */ jsxs("text", {
8940
- fg: COLOR.mute,
8941
- style: {
8942
- marginTop: 1,
8943
- flexShrink: 0
8944
- },
8945
- children: [canGoBack && /* @__PURE__ */ jsxs(Fragment, { children: [
8946
- /* @__PURE__ */ jsx$1("span", {
8947
- fg: COLOR.warn,
8948
- children: "shift+↹"
8949
- }),
8950
- " back",
8951
- /* @__PURE__ */ jsx$1("span", {
8952
- fg: COLOR.mute,
8953
- children: " · "
8954
- })
8955
- ] }), /* @__PURE__ */ jsx$1("span", {
8956
- fg: COLOR.mute,
8957
- children: `question ${activeIndex + 1} of ${questions.length}`
8958
- })]
8959
- })
8960
- ]
9119
+ }), questions.map((q, i) => /* @__PURE__ */ jsx$1(QuestionRow, {
9120
+ anchorId: anchorIdFor$1(q.id),
9121
+ index: i,
9122
+ question: q,
9123
+ status: i < activeIndex ? "done" : i === activeIndex ? "active" : "pending",
9124
+ answer: answers[q.id],
9125
+ initialAnswer: i === activeIndex ? answers[q.id] : void 0,
9126
+ onAnswer
9127
+ }, q.id))]
9128
+ }), canGoBack && /* @__PURE__ */ jsxs("text", {
9129
+ fg: COLOR.mute,
9130
+ style: {
9131
+ marginTop: 1,
9132
+ flexShrink: 0
9133
+ },
9134
+ children: [/* @__PURE__ */ jsx$1("span", {
9135
+ fg: COLOR.warn,
9136
+ children: "shift+↹"
9137
+ }), " back"]
9138
+ })]
8961
9139
  });
8962
9140
  }
8963
9141
  /**
@@ -13329,9 +13507,230 @@ function statusColor(status, COLOR) {
13329
13507
  }
13330
13508
  }
13331
13509
  //#endregion
13510
+ //#region src/chat/contribution-origin.ts
13511
+ /**
13512
+ * Union the three skill sources into the list the Skills tab shows, in the
13513
+ * precedence the AGENT applies — so the tab can never advertise a skill the
13514
+ * agent dropped, or attribute a name to the copy that lost.
13515
+ *
13516
+ * Precedence is extension > plugin > disk, and it is not arbitrary:
13517
+ * `resolveSkills` seeds its map in catalog order with later entries
13518
+ * overwriting earlier, `app.tsx` concatenates plugin skills BEFORE extension
13519
+ * ones, and catalog entries shadow anything discovered on disk. The reasoning
13520
+ * for extension-over-plugin lives at that concat: extension code is something
13521
+ * the user installed deliberately, whereas a project-scope plugin arrives with
13522
+ * a repo clone, so a cloned repo must not shadow a user's extension skill by
13523
+ * reusing its name.
13524
+ */
13525
+ function mergeSkillCatalog(opts) {
13526
+ const byName = /* @__PURE__ */ new Map();
13527
+ for (const skill of opts.disk ?? []) byName.set(skill.name, skill);
13528
+ for (const skill of opts.plugin ?? []) byName.set(skill.name, skill);
13529
+ for (const skill of opts.extension ?? []) byName.set(skill.name, skill);
13530
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
13531
+ }
13532
+ /**
13533
+ * Badge for a skill row, read straight off the skill's own metadata.
13534
+ *
13535
+ * `undefined` for a skill discovered on disk — no badge means "this came from a
13536
+ * directory you control", which is what makes a badge mean "something
13537
+ * contributed this".
13538
+ *
13539
+ * A plugin loaded BY an extension stamps both keys; the plugin wins, because
13540
+ * the plugin is what supplied the skill and the extension is only how it
13541
+ * arrived. `mcpOriginBadge` resolves the same collision the same way, so a row
13542
+ * in either tab reads identically for the same underlying situation.
13543
+ */
13544
+ function skillOriginBadge(skill) {
13545
+ const plugin = skill.metadata?.["zidane.plugin"];
13546
+ if (plugin) return `plugin:${plugin}`;
13547
+ const extension = skill.metadata?.["zidane.extension"];
13548
+ return extension ? `ext:${extension}` : void 0;
13549
+ }
13550
+ //#endregion
13551
+ //#region src/tui/plugin-list.ts
13552
+ /** Normalize a disk-discovered plugin. */
13553
+ function pluginEntryFromDisk(discovered) {
13554
+ return normalize({
13555
+ name: discovered.name,
13556
+ plugin: discovered.plugin,
13557
+ origin: {
13558
+ kind: "disk",
13559
+ scope: discovered.source,
13560
+ dir: discovered.path
13561
+ }
13562
+ });
13563
+ }
13564
+ /** Normalize a plugin an extension loaded. */
13565
+ function pluginEntryFromExtension(record) {
13566
+ return normalize({
13567
+ name: record.name,
13568
+ plugin: record.plugin,
13569
+ origin: {
13570
+ kind: "extension",
13571
+ extensionName: record.extensionName,
13572
+ dir: record.dir
13573
+ }
13574
+ });
13575
+ }
13576
+ function normalize(opts) {
13577
+ const { name, plugin, origin } = opts;
13578
+ return {
13579
+ name,
13580
+ ...plugin.manifest?.version !== void 0 ? { version: plugin.manifest.version } : {},
13581
+ ...plugin.manifest?.description !== void 0 ? { description: plugin.manifest.description } : {},
13582
+ origin,
13583
+ ok: plugin.ok,
13584
+ components: { ...plugin.components },
13585
+ contributions: {
13586
+ skills: plugin.skills.map((s) => ({
13587
+ name: s.name,
13588
+ ...s.description !== void 0 ? { description: s.description } : {}
13589
+ })),
13590
+ mcpServers: plugin.mcpServers.map((s) => ({
13591
+ name: s.name,
13592
+ transport: s.transport
13593
+ }))
13594
+ },
13595
+ diagnostics: plugin.diagnostics
13596
+ };
13597
+ }
13598
+ /**
13599
+ * Build the full plugin list from both sources, deduped and sorted by name.
13600
+ *
13601
+ * Disk entries win a name collision. An extension pointing at a plugin the
13602
+ * user also installed on disk is the same plugin; the disk copy is the one
13603
+ * with a stable path and a scope the user chose, so it's the one worth
13604
+ * showing.
13605
+ */
13606
+ function buildPluginList(opts) {
13607
+ const byName = /* @__PURE__ */ new Map();
13608
+ for (const discovered of opts.discovered ?? []) byName.set(discovered.name, pluginEntryFromDisk(discovered));
13609
+ for (const record of opts.fromExtensions ?? []) if (!byName.has(record.name)) byName.set(record.name, pluginEntryFromExtension(record));
13610
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
13611
+ }
13612
+ /**
13613
+ * One-line origin label for the detail row, e.g. `project: .agents/plugins/x`
13614
+ * or `via extension: my-toolkit`. `compact` shortens the directory (the
13615
+ * caller passes its `compactPath` so this module stays renderer-agnostic).
13616
+ */
13617
+ function pluginOriginLabel(origin, compact = (p) => p) {
13618
+ return origin.kind === "disk" ? `${origin.scope}: ${compact(origin.dir)}` : `via extension: ${origin.extensionName}`;
13619
+ }
13620
+ /**
13621
+ * Short component summary for the detail row, e.g. `3 skills, 1 server`.
13622
+ * Empty string when the plugin contributes nothing, so the caller can drop
13623
+ * the segment rather than print a bare `0 skills`.
13624
+ *
13625
+ * Counts what the PACKAGE ships, which is not always what ends up live: a
13626
+ * server whose name collides with one in `mcps.json` (or in a plugin merged
13627
+ * earlier) is dropped by `dedupMcpServers` at agent build, and a skill whose
13628
+ * name collides is shadowed by `resolveSkills`. Neither is knowable here —
13629
+ * this module only sees the plugin. The MCPs and Skills tabs are the authority
13630
+ * on what is actually loaded; collisions are reported at the merge, via
13631
+ * `name-collision-shadowed` for skills and a debug log for servers.
13632
+ */
13633
+ function pluginComponentSummary(entry) {
13634
+ const parts = [];
13635
+ const skills = entry.contributions.skills.length;
13636
+ const servers = entry.contributions.mcpServers.length;
13637
+ if (skills > 0) parts.push(`${skills} skill${skills === 1 ? "" : "s"}`);
13638
+ if (servers > 0) parts.push(`${servers} server${servers === 1 ? "" : "s"}`);
13639
+ return parts.join(", ");
13640
+ }
13641
+ /**
13642
+ * Whether an expanded row would have anything to show.
13643
+ *
13644
+ * Drives the disclosure chevron: a plugin that ships nothing must not offer an
13645
+ * expansion that opens to an empty box. A REJECTED plugin never expands either
13646
+ * — its components were never discovered, so there is nothing to list beyond
13647
+ * the failure its detail line already names.
13648
+ */
13649
+ function pluginHasContents(entry) {
13650
+ return entry.ok && (entry.contributions.skills.length > 0 || entry.contributions.mcpServers.length > 0);
13651
+ }
13652
+ /**
13653
+ * Why a rejected plugin failed, for the detail row. Uses the first error
13654
+ * diagnostic's code — the message itself is often long, and the code is the
13655
+ * part a user can search for.
13656
+ */
13657
+ function pluginRejectionReason(entry) {
13658
+ if (entry.ok) return void 0;
13659
+ const first = entry.diagnostics.find((d) => d.severity === "error");
13660
+ return first ? `rejected: ${first.code}` : "rejected";
13661
+ }
13662
+ /**
13663
+ * Kind badge label for an Extensions-tab row, rendered as a filled pill at the
13664
+ * END of the title line.
13665
+ *
13666
+ * It started as a fixed-width LEADING column, which pushed every name right by
13667
+ * its width — pure waste on a list that is overwhelmingly extensions, and it
13668
+ * broke the left alignment names had before plugins shared the tab. Trailing
13669
+ * keeps names flush and still answers "what is this row" without leaving the
13670
+ * line.
13671
+ *
13672
+ * The inner spaces are the pill's padding: a background color butted straight
13673
+ * against the glyphs reads as a highlight artifact rather than a badge.
13674
+ */
13675
+ const KIND_BADGE = {
13676
+ ext: " ext ",
13677
+ plugin: " plugin "
13678
+ };
13679
+ /**
13680
+ * Paint for the two kind pills, following the same shape as the theme's chip
13681
+ * colors (saturated `bg`, pre-paired legible `fg`).
13682
+ *
13683
+ * Derived from `ThemeColors` / `ThemeSurfaces` members that EVERY theme must
13684
+ * define rather than added as `surfaces.chips` entries. Two reasons: the chip
13685
+ * map is keyed by completion-provider id, so settings row kinds don't belong in
13686
+ * it; and a third-party theme that never opts in would fall back to
13687
+ * `chips.default` for both kinds, collapsing the distinction this badge exists
13688
+ * to draw.
13689
+ *
13690
+ * The hierarchy is deliberate. Extensions get the neutral `border` tier because
13691
+ * they're the majority — a saturated pill on nearly every row is noise. Plugins
13692
+ * get the `model` accent so they stand out of that field. `modal` as the plugin
13693
+ * foreground is what keeps this theme-safe: it's the panel the badge sits on,
13694
+ * so it contrasts against a saturated accent on dark AND light themes, which is
13695
+ * the same trick every shipped theme's chip map uses (`fg: base` / `fg: panel`
13696
+ * / `fg: butter`).
13697
+ */
13698
+ function kindBadgeStyle(kind, colors, surfaces) {
13699
+ return kind === "plugin" ? {
13700
+ bg: colors.model,
13701
+ fg: surfaces.modal
13702
+ } : {
13703
+ bg: colors.border,
13704
+ fg: colors.dim
13705
+ };
13706
+ }
13707
+ /**
13708
+ * Detail line for an Agent Plugin row: what it is, where it came from, and what
13709
+ * it contributes.
13710
+ *
13711
+ * A rejected plugin leads with its failure code instead of its description — a
13712
+ * plugin that didn't load has nothing useful to say about itself, and the code
13713
+ * is what the user can search for.
13714
+ *
13715
+ * `compact` shortens the directory (callers pass `displayPath`), keeping this
13716
+ * module free of path-presentation policy.
13717
+ */
13718
+ function pluginDetailLine(entry, compact = (p) => p) {
13719
+ const parts = [];
13720
+ const rejected = pluginRejectionReason(entry);
13721
+ if (rejected) parts.push(rejected);
13722
+ else if (entry.description) parts.push(entry.description);
13723
+ parts.push(`[${pluginOriginLabel(entry.origin, compact)}]`);
13724
+ const summary = pluginComponentSummary(entry);
13725
+ if (summary) parts.push(summary);
13726
+ return parts.join(" · ");
13727
+ }
13728
+ //#endregion
13332
13729
  //#region src/tui/extension-rows.ts
13333
13730
  function buildVisibleExtensionRows(opts) {
13334
13731
  const { errored, toggleable, configItemsByExt, expanded, queryActive, matchExtension, matchConfigItem } = opts;
13732
+ const plugins = opts.plugins ?? [];
13733
+ const matchPlugin = opts.matchPlugin ?? (() => true);
13335
13734
  const out = [];
13336
13735
  for (const ext of errored) {
13337
13736
  if (queryActive && !matchExtension(ext)) continue;
@@ -13340,10 +13739,39 @@ function buildVisibleExtensionRows(opts) {
13340
13739
  ext
13341
13740
  });
13342
13741
  }
13343
- for (const ext of toggleable) {
13742
+ for (const plugin of plugins) {
13743
+ if (plugin.ok) continue;
13744
+ if (queryActive && !matchPlugin(plugin)) continue;
13745
+ out.push({
13746
+ kind: "plugin",
13747
+ plugin,
13748
+ hasContents: false,
13749
+ expanded: false
13750
+ });
13751
+ }
13752
+ const body = [...toggleable.map((ext) => ({
13753
+ name: ext.name,
13754
+ ext
13755
+ })), ...plugins.filter((p) => p.ok).map((plugin) => ({
13756
+ name: plugin.name,
13757
+ plugin
13758
+ }))].sort((a, b) => a.name.localeCompare(b.name));
13759
+ for (const item of body) {
13760
+ if ("plugin" in item) {
13761
+ if (queryActive && !matchPlugin(item.plugin)) continue;
13762
+ const hasContents = pluginHasContents(item.plugin);
13763
+ out.push({
13764
+ kind: "plugin",
13765
+ plugin: item.plugin,
13766
+ hasContents,
13767
+ expanded: hasContents && (queryActive ? true : expanded.has(item.plugin.name))
13768
+ });
13769
+ continue;
13770
+ }
13771
+ const ext = item.ext;
13344
13772
  const items = Object.values(configItemsByExt[ext.name] ?? {});
13345
13773
  const headerMatches = !queryActive || matchExtension(ext);
13346
- const matchingItems = queryActive ? items.filter((item) => matchConfigItem(ext.name, item)) : items;
13774
+ const matchingItems = queryActive ? items.filter((configItem) => matchConfigItem(ext.name, configItem)) : items;
13347
13775
  if (queryActive && !headerMatches && matchingItems.length === 0) continue;
13348
13776
  const hasConfig = items.length > 0;
13349
13777
  const isExpanded = hasConfig && (queryActive ? true : expanded.has(ext.name));
@@ -13355,10 +13783,10 @@ function buildVisibleExtensionRows(opts) {
13355
13783
  });
13356
13784
  if (!isExpanded) continue;
13357
13785
  const rows = queryActive && !headerMatches ? matchingItems : items;
13358
- for (const item of rows) out.push({
13786
+ for (const configItem of rows) out.push({
13359
13787
  kind: "config",
13360
13788
  extName: ext.name,
13361
- item
13789
+ item: configItem
13362
13790
  });
13363
13791
  }
13364
13792
  return out;
@@ -13372,6 +13800,7 @@ function parentExtensionName(row) {
13372
13800
  if (!row) return void 0;
13373
13801
  if (row.kind === "extension") return row.ext.name;
13374
13802
  if (row.kind === "config") return row.extName;
13803
+ if (row.kind === "plugin") return row.hasContents ? row.plugin.name : void 0;
13375
13804
  }
13376
13805
  /**
13377
13806
  * Index of the `extension` header row for the given name. `-1` when absent.
@@ -14179,7 +14608,7 @@ const TAB_LABELS = {
14179
14608
  authentication: "Authentication",
14180
14609
  skills: "Skills",
14181
14610
  mcps: "MCP servers",
14182
- extensions: "Extensions",
14611
+ extensions: "Extensions & Plugins",
14183
14612
  logs: "Logs"
14184
14613
  };
14185
14614
  function isCategoryTab(id) {
@@ -14213,7 +14642,7 @@ function truncateToCols(s, maxCols) {
14213
14642
  if (maxCols === 1) return "…";
14214
14643
  return `${clean.slice(0, maxCols - 1)}…`;
14215
14644
  }
14216
- function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCatalogProp, mcpsErrors: mcpsErrorsProp, keybindings, keybindingsPath, extensionKeybindings, extensionThemes: extensionThemesProp, extensionsCatalog: extensionsCatalogProp, extensionsInactive: extensionsInactiveProp, extensionConfigCategories: extensionConfigCategoriesProp, extensionConfigItems: extensionConfigItemsProp, modelCatalog, logs, authentication, actions, version } = {}) {
14645
+ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCatalogProp, mcpsErrors: mcpsErrorsProp, keybindings, keybindingsPath, extensionKeybindings, extensionThemes: extensionThemesProp, extensionsCatalog: extensionsCatalogProp, extensionsInactive: extensionsInactiveProp, pluginsCatalog, pluginsFromExtensions, extensionContributedSkills, extensionConfigCategories: extensionConfigCategoriesProp, extensionConfigItems: extensionConfigItemsProp, modelCatalog, logs, authentication, actions, version } = {}) {
14217
14646
  const COLOR = useColors();
14218
14647
  const SURFACE = useSurfaces();
14219
14648
  const { settings, toggle: toggleBoolean, setSetting } = useSettings();
@@ -14225,12 +14654,45 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
14225
14654
  const { width: termWidth } = useTerminalDimensions();
14226
14655
  const contentCols = Math.max(40, termWidth - 2 - 4 - 2 - 1);
14227
14656
  const discovery = useDiscoveryOptional();
14228
- const skillsCatalog = discovery?.skillsCatalog ?? skillsCatalogProp ?? [];
14657
+ const [extSnapshot, setExtSnapshot] = useState(null);
14658
+ const diskSkillsCatalog = discovery?.skillsCatalog ?? skillsCatalogProp ?? [];
14659
+ /**
14660
+ * Plugins from the discovery CONTEXT, falling back to the open-time prop.
14661
+ *
14662
+ * `modal.open(<SettingsModal …/>)` builds this element once inside a key
14663
+ * handler, so every prop is frozen at open time. Anything that has to react
14664
+ * while the modal is open must come from context (live) or from a
14665
+ * ref-backed getter (`getExtensionsSnapshot`) — the same two rails
14666
+ * `skillsCatalog` / `mcpsCatalog` already ride.
14667
+ */
14668
+ const livePluginsCatalog = discovery?.pluginsCatalog ?? pluginsCatalog ?? [];
14669
+ /**
14670
+ * Skills contributed by disk plugins, derived HERE rather than passed in.
14671
+ *
14672
+ * Both inputs are live — the discovery context and `useSettings()` — so
14673
+ * toggling a plugin updates the Skills tab list and its count in the same
14674
+ * frame. As a prop this was computed in `AppShell` and captured at open
14675
+ * time, so a toggle changed the checkbox and nothing else.
14676
+ */
14677
+ const pluginContributedSkills = useMemo(() => buildPluginContributions({
14678
+ discovered: livePluginsCatalog,
14679
+ ...settings.enabledPlugins !== void 0 ? { enabled: settings.enabledPlugins } : {}
14680
+ }).skills, [livePluginsCatalog, settings.enabledPlugins]);
14681
+ const skillsCatalog = useMemo(() => mergeSkillCatalog({
14682
+ disk: diskSkillsCatalog,
14683
+ plugin: pluginContributedSkills,
14684
+ extension: extSnapshot?.skills ?? extensionContributedSkills ?? []
14685
+ }), [
14686
+ diskSkillsCatalog,
14687
+ pluginContributedSkills,
14688
+ extSnapshot,
14689
+ extensionContributedSkills
14690
+ ]);
14229
14691
  const mcpsCatalog = discovery?.mcpsCatalog ?? mcpsCatalogProp ?? [];
14230
14692
  const mcpsErrors = discovery?.mcpsErrors ?? mcpsErrorsProp;
14231
14693
  const mcpToolsByServer = discovery?.mcpToolsByServer ?? EMPTY_TOOLS;
14232
14694
  const skillsToggle = useEnabledToggleSet({
14233
- catalog: skillsCatalog,
14695
+ catalog: diskSkillsCatalog,
14234
14696
  keyOf: (s) => s.name,
14235
14697
  settingKey: "enabledSkills"
14236
14698
  });
@@ -14239,11 +14701,29 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
14239
14701
  keyOf: (m) => m.config.name,
14240
14702
  settingKey: "enabledMcps"
14241
14703
  });
14242
- const [extSnapshot, setExtSnapshot] = useState(null);
14243
14704
  const refreshExtensionRows = useCallback(() => {
14244
14705
  const snap = actions?.getExtensionsSnapshot?.();
14245
14706
  if (snap) setExtSnapshot(snap);
14246
14707
  }, [actions]);
14708
+ /**
14709
+ * Re-pull the registry snapshot whenever a toggle changes what it would
14710
+ * contain.
14711
+ *
14712
+ * The snapshot is a closure over refs on the `AppShell` side, so calling it
14713
+ * again reads current state — which is the only way registry-derived data
14714
+ * can react while this element's props stay frozen at open time. Without
14715
+ * this, disabling an extension or an extension-loaded plugin flipped its
14716
+ * checkbox and left its skills in the Skills tab.
14717
+ *
14718
+ * Cheap: reading a few refs and setting state that mostly compares equal.
14719
+ */
14720
+ useEffect(() => {
14721
+ refreshExtensionRows();
14722
+ }, [
14723
+ refreshExtensionRows,
14724
+ settings.enabledExtensions,
14725
+ settings.enabledPlugins
14726
+ ]);
14247
14727
  const extensionsInactiveAll = extSnapshot?.inactive ?? extensionsInactiveProp ?? [];
14248
14728
  const extensionsPartition = useMemo(() => partitionExtensionCatalog(extSnapshot?.catalog ?? extensionsCatalogProp ?? [], extensionsInactiveAll), [
14249
14729
  extSnapshot,
@@ -14258,6 +14738,20 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
14258
14738
  settingKey: "enabledExtensions",
14259
14739
  gcKeys: extensionsPartition.allNames
14260
14740
  });
14741
+ const pluginList = useMemo(() => buildPluginList({
14742
+ discovered: livePluginsCatalog,
14743
+ fromExtensions: extSnapshot?.plugins ?? pluginsFromExtensions ?? []
14744
+ }), [
14745
+ livePluginsCatalog,
14746
+ extSnapshot,
14747
+ pluginsFromExtensions
14748
+ ]);
14749
+ const pluginsToggle = useEnabledToggleSet({
14750
+ catalog: useMemo(() => pluginList.filter((p) => p.ok), [pluginList]),
14751
+ keyOf: (p) => p.name,
14752
+ settingKey: "enabledPlugins",
14753
+ gcKeys: pluginList.map((p) => p.name)
14754
+ });
14261
14755
  const extensionConfigCategories = extSnapshot?.configCategories ?? extensionConfigCategoriesProp ?? EMPTY_CONFIG_CATEGORIES;
14262
14756
  const extensionConfigItems = extSnapshot?.configItems ?? extensionConfigItemsProp ?? EMPTY_CONFIG_ITEMS;
14263
14757
  const configInputRef = useRef(null);
@@ -14705,14 +15199,17 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
14705
15199
  expanded: expandedExtensions,
14706
15200
  queryActive,
14707
15201
  matchExtension: (ext) => matchesTerms(extensionCorpus(ext), queryTerms),
14708
- matchConfigItem: (extName, item) => matchesTerms(extensionConfigItemCorpus(extName, item), queryTerms)
15202
+ matchConfigItem: (extName, item) => matchesTerms(extensionConfigItemCorpus(extName, item), queryTerms),
15203
+ plugins: pluginList,
15204
+ matchPlugin: (plugin) => matchesTerms(pluginCorpus(plugin), queryTerms)
14709
15205
  }), [
14710
15206
  extensionsErrored,
14711
15207
  extensionsCatalog,
14712
15208
  extensionConfigItems,
14713
15209
  expandedExtensions,
14714
15210
  queryActive,
14715
- queryTerms
15211
+ queryTerms,
15212
+ pluginList
14716
15213
  ]);
14717
15214
  const mcpVisibleRows = useMemo(() => buildVisibleMcpRows(filteredMcps, expandedMcps, mcpToolsByServer), [
14718
15215
  filteredMcps,
@@ -14887,6 +15384,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
14887
15384
  const row = extensionVisibleRows[cursor];
14888
15385
  if (!row) return void 0;
14889
15386
  if (row.kind === "extension" || row.kind === "errored") return row.ext;
15387
+ if (row.kind === "plugin") return void 0;
14890
15388
  return extensionsCatalog.find((e) => e.name === row.extName) ?? extensionsErrored.find((e) => e.name === row.extName);
14891
15389
  }, [
14892
15390
  activeTab,
@@ -14945,14 +15443,16 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
14945
15443
  }
14946
15444
  if (activeTab === "skills") {
14947
15445
  const s = filteredSkills[index];
14948
- if (s) skillsToggle.toggle(s.name);
15446
+ if (s && !skillOriginBadge(s)) skillsToggle.toggle(s.name);
14949
15447
  return;
14950
15448
  }
14951
15449
  if (activeTab === "extensions") {
14952
15450
  const row = extensionVisibleRows[index];
14953
15451
  if (!row) return;
14954
15452
  if (row.kind === "extension") extensionsToggle.toggle(row.ext.name);
14955
- else if (row.kind === "config") activateConfigRow(row.extName, row.item);
15453
+ else if (row.kind === "plugin") {
15454
+ if (row.plugin.ok) pluginsToggle.toggle(row.plugin.name);
15455
+ } else if (row.kind === "config") activateConfigRow(row.extName, row.item);
14956
15456
  return;
14957
15457
  }
14958
15458
  if (activeTab === "mcps") {
@@ -14984,6 +15484,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
14984
15484
  skillsToggle,
14985
15485
  extensionVisibleRows,
14986
15486
  extensionsToggle,
15487
+ pluginsToggle,
14987
15488
  activateConfigRow,
14988
15489
  mcpVisibleRows,
14989
15490
  mcpsToggle,
@@ -15044,6 +15545,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15044
15545
  return;
15045
15546
  }
15046
15547
  if (row?.kind === "config" && row.item.type === "boolean") return;
15548
+ if (row?.kind === "plugin") return;
15047
15549
  activateRowAt(index);
15048
15550
  return;
15049
15551
  }
@@ -15330,6 +15832,10 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15330
15832
  actions.onRefreshSkills();
15331
15833
  return;
15332
15834
  }
15835
+ if (activeTab === "extensions" && actions?.onRefreshPlugins) {
15836
+ actions.onRefreshPlugins();
15837
+ return;
15838
+ }
15333
15839
  if (activeTab === "mcps") {
15334
15840
  if (actions?.onRefreshMcps) actions.onRefreshMcps();
15335
15841
  fireMcpToolsRefresh();
@@ -15355,8 +15861,8 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15355
15861
  total: mcpsCatalog.length
15356
15862
  },
15357
15863
  extensions: {
15358
- current: extensionVisibleRows.filter((r) => r.kind === "extension" || r.kind === "errored").length,
15359
- total: extensionsCatalog.length + extensionsErrored.length
15864
+ current: extensionVisibleRows.filter((r) => r.kind === "extension" || r.kind === "errored" || r.kind === "plugin").length,
15865
+ total: extensionsCatalog.length + extensionsErrored.length + pluginList.length
15360
15866
  }
15361
15867
  };
15362
15868
  const editingInputItem = editingInputKey ? SETTINGS_INPUTS.find((inp) => inp.key === editingInputKey) ?? null : null;
@@ -15441,9 +15947,10 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15441
15947
  onRowClick: clickRowAt,
15442
15948
  rows: extensionVisibleRows,
15443
15949
  enabledSet: extensionsToggle.enabledSet,
15950
+ pluginEnabledSet: pluginsToggle.enabledSet,
15444
15951
  extensionConfig: settings.extensionConfig,
15445
15952
  categories: extensionConfigCategories,
15446
- totalCount: extensionsCatalog.length + extensionsErrored.length,
15953
+ totalCount: extensionsCatalog.length + extensionsErrored.length + pluginList.length,
15447
15954
  cursor,
15448
15955
  highlightBg: SURFACE.selection,
15449
15956
  query,
@@ -15598,6 +16105,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15598
16105
  focusedMcpStatus,
15599
16106
  canRefreshSkills: !!actions?.onRefreshSkills,
15600
16107
  canRefreshMcps: !!actions?.onRefreshMcps,
16108
+ canRefreshPlugins: !!actions?.onRefreshPlugins,
15601
16109
  canEditExtension: !!actions?.onEditExtension,
15602
16110
  canCreateExtension: !!actions?.onCreateExtension,
15603
16111
  canUpdateExtension: !!actions?.onUpdateExtension,
@@ -15771,57 +16279,89 @@ function GeneralList({ items, cursor, highlightBg, query, contentCols, editingKe
15771
16279
  });
15772
16280
  }
15773
16281
  const EXTENSION_CONFIG_INDENT = 3;
15774
- function ExtensionsList({ rows, enabledSet, extensionConfig, categories, totalCount, cursor, highlightBg, query, contentCols, editing, inputRef, onCommitEdit, modelCatalog, onApplyModelEdit, onCancelEdit, actionStatus, actionProbes, onRowClick }) {
16282
+ function ExtensionsList({ rows, enabledSet, pluginEnabledSet, extensionConfig, categories, totalCount, cursor, highlightBg, query, contentCols, editing, inputRef, onCommitEdit, modelCatalog, onApplyModelEdit, onCancelEdit, actionStatus, actionProbes, onRowClick }) {
15775
16283
  const COLOR = useColors();
16284
+ const SURFACE = useSurfaces();
16285
+ const extBadge = kindBadgeStyle("ext", COLOR, SURFACE);
16286
+ const pluginBadge = kindBadgeStyle("plugin", COLOR, SURFACE);
15776
16287
  const home = homedir();
15777
16288
  if (totalCount === 0) return /* @__PURE__ */ jsxs("box", {
15778
16289
  style: { flexDirection: "column" },
15779
- children: [/* @__PURE__ */ jsx$1("text", {
15780
- fg: COLOR.dim,
15781
- children: "No extensions discovered."
15782
- }), /* @__PURE__ */ jsxs("text", {
15783
- fg: COLOR.mute,
15784
- children: [
15785
- "Drop an",
15786
- /* @__PURE__ */ jsx$1("span", {
15787
- fg: COLOR.model,
15788
- children: " index.ts "
15789
- }),
15790
- "into",
15791
- /* @__PURE__ */ jsx$1("span", {
15792
- fg: COLOR.model,
15793
- children: " .zidane/extensions/<name>/ "
15794
- }),
15795
- "or",
15796
- /* @__PURE__ */ jsx$1("span", {
15797
- fg: COLOR.model,
15798
- children: " .agents/extensions/<name>/ "
15799
- }),
15800
- "(project or",
15801
- /* @__PURE__ */ jsx$1("span", {
15802
- fg: COLOR.model,
15803
- children: " ~/"
15804
- }),
15805
- "). Author with",
15806
- /* @__PURE__ */ jsx$1("span", {
15807
- fg: COLOR.model,
15808
- children: " defineExtension({ name, setup }) "
15809
- }),
15810
- "from",
15811
- /* @__PURE__ */ jsx$1("span", {
15812
- fg: COLOR.model,
15813
- children: " zidane "
15814
- }),
15815
- "— see",
15816
- /* @__PURE__ */ jsx$1("span", {
15817
- fg: COLOR.model,
15818
- children: " docs/EXTENSIONS.md "
15819
- }),
15820
- "."
15821
- ]
15822
- })]
16290
+ children: [
16291
+ /* @__PURE__ */ jsx$1("text", {
16292
+ fg: COLOR.dim,
16293
+ children: "No extensions or plugins discovered."
16294
+ }),
16295
+ /* @__PURE__ */ jsxs("text", {
16296
+ fg: COLOR.mute,
16297
+ children: [
16298
+ "Drop an",
16299
+ /* @__PURE__ */ jsx$1("span", {
16300
+ fg: COLOR.model,
16301
+ children: " index.ts "
16302
+ }),
16303
+ "into",
16304
+ /* @__PURE__ */ jsx$1("span", {
16305
+ fg: COLOR.model,
16306
+ children: " .zidane/extensions/<name>/ "
16307
+ }),
16308
+ "or",
16309
+ /* @__PURE__ */ jsx$1("span", {
16310
+ fg: COLOR.model,
16311
+ children: " .agents/extensions/<name>/ "
16312
+ }),
16313
+ "(project or",
16314
+ /* @__PURE__ */ jsx$1("span", {
16315
+ fg: COLOR.model,
16316
+ children: " ~/"
16317
+ }),
16318
+ "). Author with",
16319
+ /* @__PURE__ */ jsx$1("span", {
16320
+ fg: COLOR.model,
16321
+ children: " defineExtension({ name, setup }) "
16322
+ }),
16323
+ "from",
16324
+ /* @__PURE__ */ jsx$1("span", {
16325
+ fg: COLOR.model,
16326
+ children: " zidane "
16327
+ }),
16328
+ "— see",
16329
+ /* @__PURE__ */ jsx$1("span", {
16330
+ fg: COLOR.model,
16331
+ children: " docs/EXTENSIONS.md "
16332
+ }),
16333
+ "."
16334
+ ]
16335
+ }),
16336
+ /* @__PURE__ */ jsxs("text", {
16337
+ fg: COLOR.mute,
16338
+ children: [
16339
+ "For an Agent Plugin, drop a package with a",
16340
+ /* @__PURE__ */ jsx$1("span", {
16341
+ fg: COLOR.model,
16342
+ children: " plugin.json "
16343
+ }),
16344
+ "into",
16345
+ /* @__PURE__ */ jsx$1("span", {
16346
+ fg: COLOR.model,
16347
+ children: " .agents/plugins/<name>/ "
16348
+ }),
16349
+ "(project or",
16350
+ /* @__PURE__ */ jsx$1("span", {
16351
+ fg: COLOR.model,
16352
+ children: " ~/"
16353
+ }),
16354
+ ") — see",
16355
+ /* @__PURE__ */ jsx$1("span", {
16356
+ fg: COLOR.model,
16357
+ children: " docs/PLUGINS.md "
16358
+ }),
16359
+ "."
16360
+ ]
16361
+ })
16362
+ ]
15823
16363
  });
15824
- if (rows.length === 0) return /* @__PURE__ */ jsx$1(EmptyRow, { label: `no extensions match "${query}"` });
16364
+ if (rows.length === 0) return /* @__PURE__ */ jsx$1(EmptyRow, { label: `no extensions or plugins match "${query}"` });
15825
16365
  const descCols = Math.max(0, contentCols - 1 - 1 - 6);
15826
16366
  const groups = [];
15827
16367
  for (let i = 0; i < rows.length; i++) {
@@ -15834,6 +16374,16 @@ function ExtensionsList({ rows, enabledSet, extensionConfig, categories, totalCo
15834
16374
  });
15835
16375
  continue;
15836
16376
  }
16377
+ if (row.kind === "plugin") {
16378
+ groups.push({
16379
+ kind: "plugin",
16380
+ plugin: row.plugin,
16381
+ hasContents: row.hasContents,
16382
+ expanded: row.expanded,
16383
+ index: i
16384
+ });
16385
+ continue;
16386
+ }
15837
16387
  if (row.kind === "extension") {
15838
16388
  groups.push({
15839
16389
  kind: "extension",
@@ -15882,6 +16432,12 @@ function ExtensionsList({ rows, enabledSet, extensionConfig, categories, totalCo
15882
16432
  fg: focused ? COLOR.brand : COLOR.dim,
15883
16433
  children: group.ext.name
15884
16434
  }),
16435
+ /* @__PURE__ */ jsx$1("span", { children: " " }),
16436
+ /* @__PURE__ */ jsx$1("span", {
16437
+ fg: extBadge.fg,
16438
+ bg: extBadge.bg,
16439
+ children: KIND_BADGE.ext
16440
+ }),
15885
16441
  /* @__PURE__ */ jsx$1("span", {
15886
16442
  fg: COLOR.mute,
15887
16443
  children: ` ${where}`
@@ -15900,6 +16456,108 @@ function ExtensionsList({ rows, enabledSet, extensionConfig, categories, totalCo
15900
16456
  })]
15901
16457
  }, `err:${group.ext.name}`);
15902
16458
  }
16459
+ if (group.kind === "plugin") {
16460
+ const focused = group.index === cursor;
16461
+ const bg = focused ? highlightBg : void 0;
16462
+ const entry = group.plugin;
16463
+ const rejected = pluginRejectionReason(entry);
16464
+ const enabled = pluginEnabledSet.has(entry.name);
16465
+ const detail = truncateToCols(pluginDetailLine(entry, (p) => displayPath$1(p, home)), descCols);
16466
+ return /* @__PURE__ */ jsxs("box", {
16467
+ id: anchorIdFor(group.index),
16468
+ style: {
16469
+ flexDirection: "column",
16470
+ flexShrink: 0
16471
+ },
16472
+ children: [
16473
+ /* @__PURE__ */ jsxs("text", {
16474
+ wrapMode: "none",
16475
+ bg,
16476
+ onMouseDown: onRowClick && ((event) => {
16477
+ event.stopPropagation();
16478
+ event.preventDefault();
16479
+ onRowClick(group.index, inCheckboxZone(event, 4) ? "checkbox" : "row");
16480
+ }),
16481
+ children: [
16482
+ /* @__PURE__ */ jsx$1("span", {
16483
+ fg: focused ? COLOR.brand : COLOR.mute,
16484
+ children: focused ? "▶ " : " "
16485
+ }),
16486
+ rejected ? /* @__PURE__ */ jsx$1("span", {
16487
+ fg: COLOR.error,
16488
+ children: "✗ "
16489
+ }) : group.hasContents ? /* @__PURE__ */ jsx$1("span", {
16490
+ fg: COLOR.mute,
16491
+ children: group.expanded ? "▾ " : "▸ "
16492
+ }) : /* @__PURE__ */ jsx$1("span", {
16493
+ fg: COLOR.mute,
16494
+ children: " "
16495
+ }),
16496
+ rejected ? /* @__PURE__ */ jsx$1("span", {
16497
+ fg: COLOR.mute,
16498
+ children: " "
16499
+ }) : /* @__PURE__ */ jsx$1("span", {
16500
+ fg: enabled ? COLOR.accent : COLOR.mute,
16501
+ children: enabled ? "[✓] " : "[ ] "
16502
+ }),
16503
+ /* @__PURE__ */ jsx$1("span", {
16504
+ fg: focused ? COLOR.brand : COLOR.dim,
16505
+ children: entry.name
16506
+ }),
16507
+ entry.version && /* @__PURE__ */ jsx$1("span", {
16508
+ fg: COLOR.mute,
16509
+ children: ` v${entry.version}`
16510
+ }),
16511
+ /* @__PURE__ */ jsx$1("span", { children: " " }),
16512
+ /* @__PURE__ */ jsx$1("span", {
16513
+ fg: pluginBadge.fg,
16514
+ bg: pluginBadge.bg,
16515
+ children: KIND_BADGE.plugin
16516
+ })
16517
+ ]
16518
+ }),
16519
+ detail && /* @__PURE__ */ jsx$1("text", {
16520
+ wrapMode: "none",
16521
+ fg: rejected ? COLOR.error : COLOR.mute,
16522
+ children: `${COL_TITLE}${detail}`
16523
+ }),
16524
+ group.expanded && entry.contributions.skills.map((skill) => /* @__PURE__ */ jsxs("text", {
16525
+ wrapMode: "none",
16526
+ children: [
16527
+ /* @__PURE__ */ jsx$1("span", {
16528
+ fg: COLOR.mute,
16529
+ children: `${COL_TITLE} skill `
16530
+ }),
16531
+ /* @__PURE__ */ jsx$1("span", {
16532
+ fg: COLOR.dim,
16533
+ children: skill.name
16534
+ }),
16535
+ skill.description && /* @__PURE__ */ jsxs("span", {
16536
+ fg: COLOR.mute,
16537
+ children: [" ", truncateToCols(skill.description, Math.max(0, descCols - skill.name.length - 12))]
16538
+ })
16539
+ ]
16540
+ }, `skill:${skill.name}`)),
16541
+ group.expanded && entry.contributions.mcpServers.map((server) => /* @__PURE__ */ jsxs("text", {
16542
+ wrapMode: "none",
16543
+ children: [
16544
+ /* @__PURE__ */ jsx$1("span", {
16545
+ fg: COLOR.mute,
16546
+ children: `${COL_TITLE} server `
16547
+ }),
16548
+ /* @__PURE__ */ jsx$1("span", {
16549
+ fg: COLOR.dim,
16550
+ children: server.name
16551
+ }),
16552
+ /* @__PURE__ */ jsx$1("span", {
16553
+ fg: COLOR.mute,
16554
+ children: ` ${server.transport}`
16555
+ })
16556
+ ]
16557
+ }, `server:${server.name}`))
16558
+ ]
16559
+ }, `plugin:${entry.name}`);
16560
+ }
15903
16561
  const focused = group.index === cursor;
15904
16562
  const bg = focused ? highlightBg : void 0;
15905
16563
  const enabled = enabledSet.has(group.ext.name);
@@ -16018,6 +16676,12 @@ function ExtensionsList({ rows, enabledSet, extensionConfig, categories, totalCo
16018
16676
  group.ext.definition?.version && /* @__PURE__ */ jsx$1("span", {
16019
16677
  fg: COLOR.mute,
16020
16678
  children: ` v${group.ext.definition.version}`
16679
+ }),
16680
+ /* @__PURE__ */ jsx$1("span", { children: " " }),
16681
+ /* @__PURE__ */ jsx$1("span", {
16682
+ fg: extBadge.fg,
16683
+ bg: extBadge.bg,
16684
+ children: KIND_BADGE.ext
16021
16685
  })
16022
16686
  ]
16023
16687
  }),
@@ -16261,6 +16925,7 @@ function ConfigModelPickerRow({ id, label, description, catalog, multi, initial,
16261
16925
  }
16262
16926
  function SkillsList({ items, enabledSet, totalCount, cursor, highlightBg, query, contentCols, onRowClick }) {
16263
16927
  const COLOR = useColors();
16928
+ const contributedBadge = kindBadgeStyle("plugin", COLOR, useSurfaces());
16264
16929
  if (totalCount === 0) return /* @__PURE__ */ jsxs("box", {
16265
16930
  style: { flexDirection: "column" },
16266
16931
  children: [/* @__PURE__ */ jsx$1("text", {
@@ -16303,6 +16968,7 @@ function SkillsList({ items, enabledSet, totalCount, cursor, highlightBg, query,
16303
16968
  const enabled = enabledSet.has(entry.name);
16304
16969
  const description = truncateToCols(entry.description ?? "", descCols);
16305
16970
  const origin = skillOriginLabel(entry);
16971
+ const badge = skillOriginBadge(entry);
16306
16972
  return /* @__PURE__ */ jsxs("box", {
16307
16973
  onMouseDown: onRowClick && ((event) => {
16308
16974
  event.stopPropagation();
@@ -16322,14 +16988,17 @@ function SkillsList({ items, enabledSet, totalCount, cursor, highlightBg, query,
16322
16988
  onMouseDown: onRowClick && ((event) => {
16323
16989
  event.stopPropagation();
16324
16990
  event.preventDefault();
16325
- onRowClick(i, inCheckboxZone(event, 2) ? "checkbox" : "row");
16991
+ onRowClick(i, !badge && inCheckboxZone(event, 2) ? "checkbox" : "row");
16326
16992
  }),
16327
16993
  children: [
16328
16994
  /* @__PURE__ */ jsx$1("span", {
16329
16995
  fg: focused ? COLOR.brand : COLOR.mute,
16330
16996
  children: focused ? "▶ " : " "
16331
16997
  }),
16332
- /* @__PURE__ */ jsx$1("span", {
16998
+ badge ? /* @__PURE__ */ jsx$1("span", {
16999
+ fg: COLOR.mute,
17000
+ children: " "
17001
+ }) : /* @__PURE__ */ jsx$1("span", {
16333
17002
  fg: enabled ? COLOR.accent : COLOR.mute,
16334
17003
  children: enabled ? "[✓] " : "[ ] "
16335
17004
  }),
@@ -16337,7 +17006,11 @@ function SkillsList({ items, enabledSet, totalCount, cursor, highlightBg, query,
16337
17006
  fg: focused ? COLOR.brand : COLOR.dim,
16338
17007
  children: entry.name
16339
17008
  }),
16340
- origin && /* @__PURE__ */ jsxs("span", {
17009
+ badge ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", { children: " " }), /* @__PURE__ */ jsx$1("span", {
17010
+ fg: contributedBadge.fg,
17011
+ bg: contributedBadge.bg,
17012
+ children: ` ${badge} `
17013
+ })] }) : origin && /* @__PURE__ */ jsxs("span", {
16341
17014
  fg: COLOR.mute,
16342
17015
  children: [" · ", origin]
16343
17016
  })
@@ -17040,13 +17713,13 @@ function renderMcpErrors(errors, home, warnColor, dimColor) {
17040
17713
  }, err.path))
17041
17714
  });
17042
17715
  }
17043
- function Hints({ activeTab, authorizing = false, focusedMcp, focusedMcpRow, focusedMcpStatus, canRefreshSkills, canRefreshMcps, canEditExtension, canCreateExtension, canUpdateExtension, canRemoveExtension, canInstallExtension, canInstallSkill, canEditSkill, canAddMcp, canRemoveSkill, canRemoveMcp, focusedExtensionExpandable, focusedSecretClearable = false }) {
17716
+ function Hints({ activeTab, authorizing = false, focusedMcp, focusedMcpRow, focusedMcpStatus, canRefreshSkills, canRefreshMcps, canRefreshPlugins, canEditExtension, canCreateExtension, canUpdateExtension, canRemoveExtension, canInstallExtension, canInstallSkill, canEditSkill, canAddMcp, canRemoveSkill, canRemoveMcp, focusedExtensionExpandable, focusedSecretClearable = false }) {
17044
17717
  const COLOR = useColors();
17045
17718
  const onServerRow = focusedMcpRow?.kind === "server";
17046
17719
  const showLogin = activeTab === "mcps" && onServerRow && !!focusedMcp && !!focusedMcpStatus && canLogin$1(focusedMcp, focusedMcpStatus);
17047
17720
  const showLogout = activeTab === "mcps" && onServerRow && !!focusedMcpStatus && canLogout$1(focusedMcpStatus);
17048
17721
  const showCancel = activeTab === "mcps" && authorizing;
17049
- const showRefresh = activeTab === "skills" && canRefreshSkills || activeTab === "mcps" && canRefreshMcps;
17722
+ const showRefresh = activeTab === "skills" && canRefreshSkills || activeTab === "mcps" && canRefreshMcps || activeTab === "extensions" && canRefreshPlugins;
17050
17723
  return /* @__PURE__ */ jsx$1(HintRow, { hints: [
17051
17724
  {
17052
17725
  key: "←→",
@@ -17172,6 +17845,21 @@ function extensionCorpus(e) {
17172
17845
  ].join(" ").toLowerCase();
17173
17846
  }
17174
17847
  /**
17848
+ * Search corpus for an Agent Plugin row. Includes the literal word `plugin`
17849
+ * and the origin, so typing `plugin` narrows to plugins and typing an
17850
+ * extension's name surfaces the plugins it loaded.
17851
+ */
17852
+ function pluginCorpus(p) {
17853
+ return [
17854
+ "plugin",
17855
+ p.name,
17856
+ p.version ?? "",
17857
+ p.description ?? "",
17858
+ p.origin.kind === "disk" ? p.origin.scope : p.origin.extensionName,
17859
+ p.origin.dir
17860
+ ].join(" ").toLowerCase();
17861
+ }
17862
+ /**
17175
17863
  * What a config row shows at rest. Secrets are masked (last 4 kept so the user
17176
17864
  * can tell WHICH token is stored), unset model rows read `(unset)`.
17177
17865
  *
@@ -18123,7 +18811,7 @@ const SESSION_SEARCH_INDEX_CACHE_MAX_BYTES = 64 * 1024 * 1024;
18123
18811
  */
18124
18812
  /** How long the `⚡ cache break` footer toast stays visible after a break. */
18125
18813
  const CACHE_BREAK_CHIP_MS = 8e3;
18126
- const debugLog = process.env.ZIDANE_DEBUG ? (label, err) => console.error(`[zidane/tui] ${label}:`, err) : () => {};
18814
+ const debugLog = createDebugLog("tui");
18127
18815
  /**
18128
18816
  * Launch the user's preferred editor on `path`. Tries `$EDITOR` /
18129
18817
  * `$VISUAL` first (the standard *nix convention for "open this
@@ -18209,21 +18897,6 @@ function reduceEditsByOutcomes(edits, outcomes) {
18209
18897
  }
18210
18898
  return out;
18211
18899
  }
18212
- /**
18213
- * Dedup an MCP-server list by `name`. First entry wins — used at
18214
- * agent build to give project-level `mcps.json` precedence over any
18215
- * extension-contributed servers that happen to share a name.
18216
- */
18217
- function dedupMcpServers(servers) {
18218
- const seen = /* @__PURE__ */ new Set();
18219
- const out = [];
18220
- for (const server of servers) {
18221
- if (seen.has(server.name)) continue;
18222
- seen.add(server.name);
18223
- out.push(server);
18224
- }
18225
- return out;
18226
- }
18227
18900
  const ExtensionRegistryContext = createContext(null);
18228
18901
  function useExtensionRegistryState() {
18229
18902
  const ctx = useContext(ExtensionRegistryContext);
@@ -18485,7 +19158,7 @@ function AppShell() {
18485
19158
  const sessionSafelistRef = useRef(/* @__PURE__ */ new Set());
18486
19159
  const streamRef = useRef(null);
18487
19160
  const pendingAnnotationsRef = useRef(/* @__PURE__ */ new Map());
18488
- const { skillsCatalog, mcpsCatalog, filesCatalog, ensureFiles: ensureFilesCatalog, ensureSkills: ensureSkillsCatalog, refreshSkills: onRefreshSkills, refreshMcps: onRefreshMcps } = useDiscovery();
19161
+ const { skillsCatalog, pluginsCatalog, mcpsCatalog, filesCatalog, ensureFiles: ensureFilesCatalog, ensureSkills: ensureSkillsCatalog, refreshSkills: onRefreshSkills, refreshMcps: onRefreshMcps, refreshPlugins: onRefreshPlugins } = useDiscovery();
18489
19162
  const mcpCredentialStore = useMemo(() => createFileMcpCredentialStore(dataDir), [dataDir]);
18490
19163
  const dispatchAuth = useMcpAuthDispatch();
18491
19164
  const dispatchAuthRef = useRef(dispatchAuth);
@@ -18494,8 +19167,38 @@ function AppShell() {
18494
19167
  const onLoginMcpRef = useRef(null);
18495
19168
  const skillsCatalogRef = useRef(skillsCatalog);
18496
19169
  skillsCatalogRef.current = skillsCatalog;
19170
+ const pluginsCatalogRef = useRef(pluginsCatalog);
19171
+ pluginsCatalogRef.current = pluginsCatalog;
18497
19172
  const enabledSkillsRef = useRef(settings.enabledSkills);
18498
19173
  enabledSkillsRef.current = settings.enabledSkills;
19174
+ const enabledPluginsRef = useRef(settings.enabledPlugins);
19175
+ enabledPluginsRef.current = settings.enabledPlugins;
19176
+ /**
19177
+ * Registry with the user's toggles applied, for surfaces that must reflect
19178
+ * them: disabling an extension or a plugin re-runs this memo and its
19179
+ * contributions disappear in the same frame the agent is rebuilt.
19180
+ *
19181
+ * Deliberately NOT used for the Extensions & Plugins tab's own rows — those
19182
+ * come from the RAW registry, because a disabled extension or plugin must
19183
+ * stay listed or its toggle becomes unreachable.
19184
+ */
19185
+ const liveRegistryForSettings = useMemo(() => filterRegistryPlugins(filterExtensionRegistry(extensionRegistry, settings.enabledExtensions), settings.enabledPlugins, new Set(pluginsCatalog.map((p) => p.name))), [
19186
+ extensionRegistry,
19187
+ settings.enabledExtensions,
19188
+ settings.enabledPlugins,
19189
+ pluginsCatalog
19190
+ ]);
19191
+ /**
19192
+ * Ref mirror of the above, read by `getExtensionsSnapshot`.
19193
+ *
19194
+ * The settings modal is built once inside a key handler
19195
+ * (`modal.open(<SettingsModal …/>)`), so its props are frozen at open time.
19196
+ * A getter closing over a REF is how the modal pulls current state anyway —
19197
+ * this puts the filtered registry on that same rail, so a plugin toggled off
19198
+ * while settings is open drops out of the Skills tab.
19199
+ */
19200
+ const liveRegistryForSettingsRef = useRef(liveRegistryForSettings);
19201
+ liveRegistryForSettingsRef.current = liveRegistryForSettings;
18499
19202
  const importForeignConfigsRef = useRef(settings.importForeignConfigs);
18500
19203
  importForeignConfigsRef.current = settings.importForeignConfigs;
18501
19204
  const importRulerConfigsRef = useRef(settings.importRulerConfigs);
@@ -18506,14 +19209,36 @@ function AppShell() {
18506
19209
  enabledMcpsRef.current = settings.enabledMcps;
18507
19210
  const disabledMcpToolsRef = useRef(settings.disabledMcpTools);
18508
19211
  disabledMcpToolsRef.current = settings.disabledMcpTools;
19212
+ /**
19213
+ * Fingerprint of everything that feeds the agent's MCP + plugin runtime.
19214
+ * A change schedules a rebuild (see the effect keyed on this below).
19215
+ *
19216
+ * Plugin state belongs here even though the name says "mcp": plugin
19217
+ * contributions are SNAPSHOTTED into the agent at build time (unlike
19218
+ * skills, which pass `scan` paths the agent resolves itself). The plugin
19219
+ * walk is async and nothing orders it before the first build, so without
19220
+ * the catalog in this key whether a cold boot picked up plugins would be
19221
+ * timing-dependent — and a late-landing catalog would have no recovery
19222
+ * path. `enabledPlugins` is here for the same reason a toggle of
19223
+ * `enabledMcps` is: it changes which servers should be connected.
19224
+ */
18509
19225
  const mcpRuntimeKey = useMemo(() => JSON.stringify({
18510
19226
  catalog: mcpsCatalog.map(({ config }) => config),
18511
19227
  enabled: settings.enabledMcps ?? null,
18512
- disabledTools: settings.disabledMcpTools ?? null
19228
+ disabledTools: settings.disabledMcpTools ?? null,
19229
+ plugins: pluginsCatalog.map(({ name, plugin }) => ({
19230
+ name,
19231
+ ok: plugin.ok,
19232
+ skills: plugin.skills,
19233
+ servers: plugin.mcpServers
19234
+ })),
19235
+ enabledPlugins: settings.enabledPlugins ?? null
18513
19236
  }), [
18514
19237
  mcpsCatalog,
18515
19238
  settings.enabledMcps,
18516
- settings.disabledMcpTools
19239
+ settings.disabledMcpTools,
19240
+ pluginsCatalog,
19241
+ settings.enabledPlugins
18517
19242
  ]);
18518
19243
  const filesCatalogRef = useRef(filesCatalog);
18519
19244
  filesCatalogRef.current = filesCatalog;
@@ -19430,18 +20155,26 @@ function AppShell() {
19430
20155
  foreignAgents: importForeignConfigsRef.current === true,
19431
20156
  ruler: importRulerConfigsRef.current !== false
19432
20157
  });
19433
- const liveExtensionRegistry = filterExtensionRegistry(extensionRegistry, enabledExtensionsRef.current);
20158
+ const liveExtensionRegistry = filterRegistryPlugins(filterExtensionRegistry(extensionRegistry, enabledExtensionsRef.current), enabledPluginsRef.current, new Set(pluginsCatalogRef.current.map((p) => p.name)));
20159
+ const pluginContributions = buildPluginContributions({
20160
+ discovered: pluginsCatalogRef.current,
20161
+ ...enabledPluginsRef.current !== void 0 ? { enabled: enabledPluginsRef.current } : {}
20162
+ });
19434
20163
  const extensionSkillNames = liveExtensionRegistry.skills.map((s) => s.name);
19435
20164
  const userEnabledSkills = enabledSkillsRef.current;
19436
20165
  const skillsConfig = buildSkillsConfig({
19437
20166
  scan: skillsScan,
19438
- enabled: userEnabledSkills === void 0 ? void 0 : Array.from(/* @__PURE__ */ new Set([...userEnabledSkills, ...extensionSkillNames]))
20167
+ enabled: userEnabledSkills === void 0 ? void 0 : Array.from(/* @__PURE__ */ new Set([
20168
+ ...userEnabledSkills,
20169
+ ...extensionSkillNames,
20170
+ ...pluginContributions.skills.map((s) => s.name)
20171
+ ]))
19439
20172
  });
19440
- const projectMcps = buildMcpServers({
20173
+ const projectMcps = [...buildMcpServers({
19441
20174
  discovered: mcpsCatalogRef.current,
19442
20175
  enabled: enabledMcpsRef.current,
19443
20176
  ...disabledMcpToolsRef.current ? { disabledTools: disabledMcpToolsRef.current } : {}
19444
- });
20177
+ }), ...pluginContributions.mcpServers];
19445
20178
  const allowInteraction = allowInteractionRef.current !== false;
19446
20179
  const interactionTools = allowInteraction ? createInteractionTools({ requestInteraction: makeRequestInteraction(interactions) }) : {};
19447
20180
  const actualCwd = cwdRef.current;
@@ -19493,14 +20226,18 @@ function AppShell() {
19493
20226
  ...skillsConfig,
19494
20227
  ...composedPreset.skills ?? {}
19495
20228
  };
19496
- const withExtensionSkill = !!session.metadata["zidane.extensionEdit"] || extensionDirForCwd(actualCwd) !== null ? withZidaneExtensionSkill(mergedSkills, { cwd: actualCwd }) : mergedSkills;
20229
+ const withPluginSkills = pluginContributions.skills.length > 0 ? {
20230
+ ...mergedSkills,
20231
+ catalog: [...pluginContributions.skills, ...mergedSkills.catalog ?? []]
20232
+ } : mergedSkills;
20233
+ const withExtensionSkill = !!session.metadata["zidane.extensionEdit"] || extensionDirForCwd(actualCwd) !== null ? withZidaneExtensionSkill(withPluginSkills, { cwd: actualCwd }) : withPluginSkills;
19497
20234
  const skillEditMarker = readSkillEditMarker(session.metadata[SKILL_EDIT_METADATA_KEY]);
19498
20235
  const skills = skillEditMarker ? withZidaneSkillEditSkill(withExtensionSkill, skillEditMarker, { cwd: actualCwd }) : withExtensionSkill;
19499
- const mergedMcpServers = dedupMcpServers([
20236
+ const mergedMcpServers = dedupMcpServersByName([
19500
20237
  ...projectMcps,
19501
20238
  ...launch?.mcp ?? [],
19502
20239
  ...composedPreset.mcpServers ?? []
19503
- ]);
20240
+ ], (names) => debugLog("mcp name collision — later configs dropped (first wins)", names));
19504
20241
  const mergedTools = {
19505
20242
  ...interactionTools,
19506
20243
  chatTurnsRead: chatTurnsReadTool,
@@ -22939,6 +23676,9 @@ function AppShell() {
22939
23676
  } } : {},
22940
23677
  extensionsCatalog: extensionRegistry.loaded,
22941
23678
  extensionsInactive: extensionRegistry.inactive,
23679
+ pluginsCatalog,
23680
+ pluginsFromExtensions: extensionRegistry.plugins,
23681
+ extensionContributedSkills: liveRegistryForSettings.skills,
22942
23682
  authentication: {
22943
23683
  currentProviderLabel: picked?.provider.label,
22944
23684
  currentProviderKey: picked?.provider.key,
@@ -22953,6 +23693,7 @@ function AppShell() {
22953
23693
  onCancelLoginMcp,
22954
23694
  onRefreshSkills,
22955
23695
  onRefreshMcps,
23696
+ onRefreshPlugins,
22956
23697
  onRefreshMcpTools,
22957
23698
  onEditExtension,
22958
23699
  onEditSkill,
@@ -22964,7 +23705,9 @@ function AppShell() {
22964
23705
  catalog: extensionRegistryLiveRef.current.loaded,
22965
23706
  inactive: extensionRegistryLiveRef.current.inactive,
22966
23707
  configCategories: extensionRegistryLiveRef.current.configCategories,
22967
- configItems: extensionRegistryLiveRef.current.configItems
23708
+ configItems: extensionRegistryLiveRef.current.configItems,
23709
+ plugins: extensionRegistryLiveRef.current.plugins,
23710
+ skills: liveRegistryForSettingsRef.current.skills
22968
23711
  }),
22969
23712
  onInstallSkill,
22970
23713
  onAddMcp,
@@ -24877,6 +25620,7 @@ async function runTui(options = {}) {
24877
25620
  });
24878
25621
  const prefix = options.prefix ?? process.env.ZIDANE_PREFIX ?? ".zidane";
24879
25622
  const extensionScanPaths = options.extensionScanPaths ?? parseExtensionScanPathEnv();
25623
+ const pluginScanPaths = options.pluginScanPaths ?? parsePluginScanPathEnv();
24880
25624
  const explicitStorageDir = options.storageDir ?? process.env.ZIDANE_STORAGE_DIR;
24881
25625
  const paths = resolveStoragePaths({
24882
25626
  prefix,
@@ -24912,6 +25656,7 @@ async function runTui(options = {}) {
24912
25656
  const config = resolveConfig({
24913
25657
  ...options,
24914
25658
  extensionScanPaths,
25659
+ pluginScanPaths,
24915
25660
  extensionRegistry,
24916
25661
  trustProjectExtensions,
24917
25662
  ...options.launch && !options.preset && !options.agents ? { preset: basic_default } : {},