zidane 6.4.4 → 6.5.1

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 (138) hide show
  1. package/README.md +1 -0
  2. package/dist/acp-cli.js +7 -6
  3. package/dist/acp-cli.js.map +1 -1
  4. package/dist/{acp-BLO58K3F.js → acp-o-LQAjFw.js} +4 -5
  5. package/dist/{acp-BLO58K3F.js.map → acp-o-LQAjFw.js.map} +1 -1
  6. package/dist/acp.d.ts +1 -1
  7. package/dist/acp.js +1 -1
  8. package/dist/{agent-8sUadp5M.js → agent-BVNkC1h4.js} +886 -113
  9. package/dist/agent-BVNkC1h4.js.map +1 -0
  10. package/dist/agent.d.ts +1 -1
  11. package/dist/agent.js +1 -2
  12. package/dist/{anthropic-D3lkw5mv.js → anthropic-Dov2mC5e.js} +3 -3
  13. package/dist/{anthropic-D3lkw5mv.js.map → anthropic-Dov2mC5e.js.map} +1 -1
  14. package/dist/{auth-BzYbbdN_.js → auth-B8ZuBcKC.js} +5 -5
  15. package/dist/{auth-BzYbbdN_.js.map → auth-B8ZuBcKC.js.map} +1 -1
  16. package/dist/chat/pure.d.ts +3 -3
  17. package/dist/chat/pure.js +2 -2
  18. package/dist/chat.d.ts +13 -44
  19. package/dist/chat.d.ts.map +1 -1
  20. package/dist/chat.js +8 -7
  21. package/dist/chat.js.map +1 -1
  22. package/dist/{edit-utils-EGosADZq.js → context-breakdown-BLF10tOO.js} +154 -2
  23. package/dist/context-breakdown-BLF10tOO.js.map +1 -0
  24. package/dist/{context-transport-D29WvHcm.js → context-transport-D-daWOmp.js} +3 -3
  25. package/dist/{context-transport-D29WvHcm.js.map → context-transport-D-daWOmp.js.map} +1 -1
  26. package/dist/contexts/daytona.d.ts +1 -1
  27. package/dist/contexts/e2b.d.ts +1 -1
  28. package/dist/eval.d.ts +1 -1
  29. package/dist/eval.js +1 -1
  30. package/dist/extensions.d.ts +2 -2
  31. package/dist/extensions.js +2 -2
  32. package/dist/headless.d.ts +1 -1
  33. package/dist/headless.js +3 -3
  34. package/dist/{index-C-Nd2ZbV.d.ts → index-CWBSrqI6.d.ts} +676 -3
  35. package/dist/index-CWBSrqI6.d.ts.map +1 -0
  36. package/dist/index.d.ts +2 -2
  37. package/dist/index.js +13 -13
  38. package/dist/{login-CMVVnAHS.js → login-CR3fDHXh.js} +3 -3
  39. package/dist/{login-CMVVnAHS.js.map → login-CR3fDHXh.js.map} +1 -1
  40. package/dist/{mcp-3GXUBwur.js → mcp-C6IkXNOS.js} +87 -6
  41. package/dist/mcp-C6IkXNOS.js.map +1 -0
  42. package/dist/mcp.d.ts +2 -2
  43. package/dist/mcp.js +3 -2
  44. package/dist/{messages-C01kKGj0.js → messages-C_H9soi5.js} +2 -3
  45. package/dist/{messages-C01kKGj0.js.map → messages-C_H9soi5.js.map} +1 -1
  46. package/dist/{openai-compat-B3c_hzwf.js → openai-compat-CnHeo4Lz.js} +216 -3
  47. package/dist/openai-compat-CnHeo4Lz.js.map +1 -0
  48. package/dist/origin-pinned-fetch-CqG8VZIq.js +159 -0
  49. package/dist/origin-pinned-fetch-CqG8VZIq.js.map +1 -0
  50. package/dist/output/stream-json.d.ts +1 -1
  51. package/dist/output/terminal.d.ts +1 -1
  52. package/dist/plugins-2PMTVcp-.js +1174 -0
  53. package/dist/plugins-2PMTVcp-.js.map +1 -0
  54. package/dist/plugins.d.ts +2 -0
  55. package/dist/plugins.js +2 -0
  56. package/dist/presets.d.ts +1 -1
  57. package/dist/presets.js +1 -1
  58. package/dist/providers/anthropic.d.ts +1 -1
  59. package/dist/providers/anthropic.js +1 -1
  60. package/dist/providers/arcee.d.ts +1 -1
  61. package/dist/providers/arcee.js +1 -1
  62. package/dist/providers/baseten.d.ts +1 -1
  63. package/dist/providers/baseten.js +1 -1
  64. package/dist/providers/cerebras.d.ts +1 -1
  65. package/dist/providers/cerebras.js +1 -1
  66. package/dist/providers/local.d.ts +1 -1
  67. package/dist/providers/local.js +1 -1
  68. package/dist/providers/openai-compat.d.ts +1 -1
  69. package/dist/providers/openai-compat.js +1 -1
  70. package/dist/providers/openai.d.ts +1 -1
  71. package/dist/providers/openai.js +2 -2
  72. package/dist/providers/openrouter.d.ts +1 -1
  73. package/dist/providers/openrouter.js +1 -1
  74. package/dist/providers/xai.d.ts +1 -1
  75. package/dist/providers/xai.js +1 -1
  76. package/dist/{providers-z1vU0b2Q.js → providers-B6mFe7OD.js} +3 -3
  77. package/dist/{providers-z1vU0b2Q.js.map → providers-B6mFe7OD.js.map} +1 -1
  78. package/dist/providers.d.ts +1 -1
  79. package/dist/providers.js +4 -4
  80. package/dist/restate.d.ts +178 -2
  81. package/dist/restate.d.ts.map +1 -1
  82. package/dist/restate.js +463 -7
  83. package/dist/restate.js.map +1 -1
  84. package/dist/session/sqlite.d.ts +1 -1
  85. package/dist/{session-cr9VdoEO.js → session-VpLrNaFQ.js} +2 -2
  86. package/dist/{session-cr9VdoEO.js.map → session-VpLrNaFQ.js.map} +1 -1
  87. package/dist/session.d.ts +1 -1
  88. package/dist/session.js +2 -2
  89. package/dist/{resolve-CZJxds85.js → skills-Cp9vnBwd.js} +1430 -1006
  90. package/dist/skills-Cp9vnBwd.js.map +1 -0
  91. package/dist/skills.d.ts +1 -1
  92. package/dist/skills.js +1 -2
  93. package/dist/{stdio-BvL8HGYW.js → stdio-DzUUkkhS.js} +2 -2
  94. package/dist/{stdio-BvL8HGYW.js.map → stdio-DzUUkkhS.js.map} +1 -1
  95. package/dist/{stdio-loader-CjfoqBle.js → stdio-loader-BILyUGmi.js} +2 -2
  96. package/dist/{stdio-loader-CjfoqBle.js.map → stdio-loader-BILyUGmi.js.map} +1 -1
  97. package/dist/{tool-formatters-DPQjso_C.d.ts → tool-formatters-gFqQbzPY.d.ts} +2 -2
  98. package/dist/{tool-formatters-DPQjso_C.d.ts.map → tool-formatters-gFqQbzPY.d.ts.map} +1 -1
  99. package/dist/tools/fetch-url.d.ts +1 -1
  100. package/dist/tools/web-search.d.ts +1 -1
  101. package/dist/tools.d.ts +1 -1
  102. package/dist/tools.js +1 -2
  103. package/dist/{transcript-anchors-CgDzYin1.js → transcript-anchors-DEIr0tJC.js} +44 -7
  104. package/dist/transcript-anchors-DEIr0tJC.js.map +1 -0
  105. package/dist/{transcript-anchors-BCUpbNxc.d.ts → transcript-anchors-qCs3wZpu.d.ts} +98 -92
  106. package/dist/transcript-anchors-qCs3wZpu.d.ts.map +1 -0
  107. package/dist/tui.d.ts +38 -4
  108. package/dist/tui.d.ts.map +1 -1
  109. package/dist/tui.js +843 -109
  110. package/dist/tui.js.map +1 -1
  111. package/dist/{turn-operations-CAvi8NYt.js → turn-operations-C3IijXMW.js} +2 -2
  112. package/dist/{turn-operations-CAvi8NYt.js.map → turn-operations-C3IijXMW.js.map} +1 -1
  113. package/dist/{turn-operations-Bu2Hkinw.d.ts → turn-operations-M9-jUyDd.d.ts} +3 -3
  114. package/dist/{turn-operations-Bu2Hkinw.d.ts.map → turn-operations-M9-jUyDd.d.ts.map} +1 -1
  115. package/dist/types-BIarq1qE.js.map +1 -1
  116. package/dist/types.d.ts +1 -1
  117. package/dist/{xai-BPwMQSuF.js → xai-Du6Wfnlb.js} +2 -2
  118. package/dist/{xai-BPwMQSuF.js.map → xai-Du6Wfnlb.js.map} +1 -1
  119. package/docs/PLUGINS.md +340 -0
  120. package/docs/RESTATE.md +168 -3
  121. package/docs/SKILL.md +24 -8
  122. package/package.json +6 -1
  123. package/dist/agent-8sUadp5M.js.map +0 -1
  124. package/dist/aliasing-lVFbm81n.js +0 -217
  125. package/dist/aliasing-lVFbm81n.js.map +0 -1
  126. package/dist/context-breakdown-Dzo25N45.js +0 -155
  127. package/dist/context-breakdown-Dzo25N45.js.map +0 -1
  128. package/dist/edit-utils-EGosADZq.js.map +0 -1
  129. package/dist/index-C-Nd2ZbV.d.ts.map +0 -1
  130. package/dist/mcp-3GXUBwur.js.map +0 -1
  131. package/dist/openai-compat-B3c_hzwf.js.map +0 -1
  132. package/dist/resolve-CZJxds85.js.map +0 -1
  133. package/dist/skills-Dd6XCi4s.js +0 -420
  134. package/dist/skills-Dd6XCi4s.js.map +0 -1
  135. package/dist/tool-state-DTs-3ZHH.js +0 -653
  136. package/dist/tool-state-DTs-3ZHH.js.map +0 -1
  137. package/dist/transcript-anchors-BCUpbNxc.d.ts.map +0 -1
  138. package/dist/transcript-anchors-CgDzYin1.js.map +0 -1
package/dist/tui.js CHANGED
@@ -1,33 +1,33 @@
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-CgDzYin1.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-8sUadp5M.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-DEIr0tJC.js";
2
+ import { C as estimateRebilledCostUsd } from "./openai-compat-CnHeo4Lz.js";
3
+ import { $ as installExtensionFromSpec, $t as groupBindings, A as selectFilesFromSession, C as resolveLegacySessionDir, Cn as resolveExtensionConfigValue, D as resolveTasksDir, Dn as OUTPUT_RESERVE_TOKENS, E as resolveSessionDir, Et as isSpawnTool, G as summaryToTurn, It as looksBinary, M as compactConversation, O as buildPostCompactAttachments, On as effectiveContextWindow, Pt as tailTruncate, Qt as formatBindingForDisplay, Rt as createModelSearchTool, S as resolveLegacyPersistDir, T as resolvePersistDir, X as createExtensionPromptBridge, Xt as KEYBINDING_KEY_COL_WIDTH, Z as ExtensionInstallError, Zt as ensureKeybindingsFile, at as createExtensionHud, bt as basic_default, cn as registerExtensionHostModules, dt as filterExtensionRegistry, en as keybindingsPath, f as applyCompactSummaryCutoff, fn as inlineLoadedExtension, ft as filterRegistryPlugins, gn as createDebugLog, hn as scanExtensionDirectories, ht as createExtensionHostRuntimeBridge, it as updateInstalledExtension, lt as composeExtensionPreset, mn as parseExtensionScanPathEnv, mt as partitionExtensionCatalog, on as resolveExtensionKeybindings, ot as discoverAndBuildExtensionRegistry, rt as removeInstalledExtension, t as createAgent, tn as matchesBinding, ut as disposeExtensionRegistry, v as cleanupPersistedSession, w as resolveMcpWarningsDir, wn as sanitizeExtensionConfigActionPatch, x as rehomePersistedStubs, yn as coerceExtensionConfigInput } from "./agent-BVNkC1h4.js";
3
4
  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
5
  import { n as appendCapped } from "./output-cap-C9zukcCj.js";
5
6
  import { i as killProcessGroup, n as SUPPORTS_PROCESS_GROUPS, r as createProcessContext } from "./contexts-DWVt7MvF.js";
6
7
  import { u as errorMessage } from "./errors-BwBxu3zD.js";
7
- import { M as findGitRoot } from "./resolve-CZJxds85.js";
8
+ import { R as projectUserPaths, c as SkillInstallError, l as installSkillFromSpec, z as findGitRoot } from "./skills-Cp9vnBwd.js";
9
+ import { n as defaultPluginDataRoot, r as loadAgentPlugin } from "./plugins-2PMTVcp-.js";
8
10
  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
11
  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-3GXUBwur.js";
11
- import { C as estimateRebilledCostUsd } from "./openai-compat-B3c_hzwf.js";
12
+ import { _ as hasAuthorizationHeader, h as McpOAuthProvider, n as connectMcpServers, s as nonOwningMcpView } from "./mcp-C6IkXNOS.js";
12
13
  import { replaceDynamicSection } from "./prompt.js";
13
- import { a as getExtensionSessionState, c as setExtensionSessionState, i as demoteStaleRunningStatus, o as loadSession, t as createSession } from "./session-cr9VdoEO.js";
14
+ import { a as getExtensionSessionState, c as setExtensionSessionState, i as demoteStaleRunningStatus, o as loadSession, t as createSession } from "./session-VpLrNaFQ.js";
14
15
  import { n as flattenTurns, r as formatTokenUsage, t as effectiveInputFromTurn } from "./stats-DAKBEKjc.js";
15
16
  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-CMVVnAHS.js";
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";
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-CR3fDHXh.js";
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-B8ZuBcKC.js";
22
22
  import { createDockerContext } from "./contexts/docker.js";
23
23
  import { createTuiStore } from "./session/sqlite.js";
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";
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-C3IijXMW.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,
@@ -13340,9 +13507,230 @@ function statusColor(status, COLOR) {
13340
13507
  }
13341
13508
  }
13342
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
13343
13729
  //#region src/tui/extension-rows.ts
13344
13730
  function buildVisibleExtensionRows(opts) {
13345
13731
  const { errored, toggleable, configItemsByExt, expanded, queryActive, matchExtension, matchConfigItem } = opts;
13732
+ const plugins = opts.plugins ?? [];
13733
+ const matchPlugin = opts.matchPlugin ?? (() => true);
13346
13734
  const out = [];
13347
13735
  for (const ext of errored) {
13348
13736
  if (queryActive && !matchExtension(ext)) continue;
@@ -13351,10 +13739,39 @@ function buildVisibleExtensionRows(opts) {
13351
13739
  ext
13352
13740
  });
13353
13741
  }
13354
- 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;
13355
13772
  const items = Object.values(configItemsByExt[ext.name] ?? {});
13356
13773
  const headerMatches = !queryActive || matchExtension(ext);
13357
- const matchingItems = queryActive ? items.filter((item) => matchConfigItem(ext.name, item)) : items;
13774
+ const matchingItems = queryActive ? items.filter((configItem) => matchConfigItem(ext.name, configItem)) : items;
13358
13775
  if (queryActive && !headerMatches && matchingItems.length === 0) continue;
13359
13776
  const hasConfig = items.length > 0;
13360
13777
  const isExpanded = hasConfig && (queryActive ? true : expanded.has(ext.name));
@@ -13366,10 +13783,10 @@ function buildVisibleExtensionRows(opts) {
13366
13783
  });
13367
13784
  if (!isExpanded) continue;
13368
13785
  const rows = queryActive && !headerMatches ? matchingItems : items;
13369
- for (const item of rows) out.push({
13786
+ for (const configItem of rows) out.push({
13370
13787
  kind: "config",
13371
13788
  extName: ext.name,
13372
- item
13789
+ item: configItem
13373
13790
  });
13374
13791
  }
13375
13792
  return out;
@@ -13383,6 +13800,7 @@ function parentExtensionName(row) {
13383
13800
  if (!row) return void 0;
13384
13801
  if (row.kind === "extension") return row.ext.name;
13385
13802
  if (row.kind === "config") return row.extName;
13803
+ if (row.kind === "plugin") return row.hasContents ? row.plugin.name : void 0;
13386
13804
  }
13387
13805
  /**
13388
13806
  * Index of the `extension` header row for the given name. `-1` when absent.
@@ -14190,7 +14608,7 @@ const TAB_LABELS = {
14190
14608
  authentication: "Authentication",
14191
14609
  skills: "Skills",
14192
14610
  mcps: "MCP servers",
14193
- extensions: "Extensions",
14611
+ extensions: "Extensions & Plugins",
14194
14612
  logs: "Logs"
14195
14613
  };
14196
14614
  function isCategoryTab(id) {
@@ -14224,7 +14642,7 @@ function truncateToCols(s, maxCols) {
14224
14642
  if (maxCols === 1) return "…";
14225
14643
  return `${clean.slice(0, maxCols - 1)}…`;
14226
14644
  }
14227
- 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 } = {}) {
14228
14646
  const COLOR = useColors();
14229
14647
  const SURFACE = useSurfaces();
14230
14648
  const { settings, toggle: toggleBoolean, setSetting } = useSettings();
@@ -14236,12 +14654,45 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
14236
14654
  const { width: termWidth } = useTerminalDimensions();
14237
14655
  const contentCols = Math.max(40, termWidth - 2 - 4 - 2 - 1);
14238
14656
  const discovery = useDiscoveryOptional();
14239
- 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
+ ]);
14240
14691
  const mcpsCatalog = discovery?.mcpsCatalog ?? mcpsCatalogProp ?? [];
14241
14692
  const mcpsErrors = discovery?.mcpsErrors ?? mcpsErrorsProp;
14242
14693
  const mcpToolsByServer = discovery?.mcpToolsByServer ?? EMPTY_TOOLS;
14243
14694
  const skillsToggle = useEnabledToggleSet({
14244
- catalog: skillsCatalog,
14695
+ catalog: diskSkillsCatalog,
14245
14696
  keyOf: (s) => s.name,
14246
14697
  settingKey: "enabledSkills"
14247
14698
  });
@@ -14250,11 +14701,29 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
14250
14701
  keyOf: (m) => m.config.name,
14251
14702
  settingKey: "enabledMcps"
14252
14703
  });
14253
- const [extSnapshot, setExtSnapshot] = useState(null);
14254
14704
  const refreshExtensionRows = useCallback(() => {
14255
14705
  const snap = actions?.getExtensionsSnapshot?.();
14256
14706
  if (snap) setExtSnapshot(snap);
14257
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
+ ]);
14258
14727
  const extensionsInactiveAll = extSnapshot?.inactive ?? extensionsInactiveProp ?? [];
14259
14728
  const extensionsPartition = useMemo(() => partitionExtensionCatalog(extSnapshot?.catalog ?? extensionsCatalogProp ?? [], extensionsInactiveAll), [
14260
14729
  extSnapshot,
@@ -14269,6 +14738,20 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
14269
14738
  settingKey: "enabledExtensions",
14270
14739
  gcKeys: extensionsPartition.allNames
14271
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
+ });
14272
14755
  const extensionConfigCategories = extSnapshot?.configCategories ?? extensionConfigCategoriesProp ?? EMPTY_CONFIG_CATEGORIES;
14273
14756
  const extensionConfigItems = extSnapshot?.configItems ?? extensionConfigItemsProp ?? EMPTY_CONFIG_ITEMS;
14274
14757
  const configInputRef = useRef(null);
@@ -14716,14 +15199,17 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
14716
15199
  expanded: expandedExtensions,
14717
15200
  queryActive,
14718
15201
  matchExtension: (ext) => matchesTerms(extensionCorpus(ext), queryTerms),
14719
- 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)
14720
15205
  }), [
14721
15206
  extensionsErrored,
14722
15207
  extensionsCatalog,
14723
15208
  extensionConfigItems,
14724
15209
  expandedExtensions,
14725
15210
  queryActive,
14726
- queryTerms
15211
+ queryTerms,
15212
+ pluginList
14727
15213
  ]);
14728
15214
  const mcpVisibleRows = useMemo(() => buildVisibleMcpRows(filteredMcps, expandedMcps, mcpToolsByServer), [
14729
15215
  filteredMcps,
@@ -14898,6 +15384,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
14898
15384
  const row = extensionVisibleRows[cursor];
14899
15385
  if (!row) return void 0;
14900
15386
  if (row.kind === "extension" || row.kind === "errored") return row.ext;
15387
+ if (row.kind === "plugin") return void 0;
14901
15388
  return extensionsCatalog.find((e) => e.name === row.extName) ?? extensionsErrored.find((e) => e.name === row.extName);
14902
15389
  }, [
14903
15390
  activeTab,
@@ -14956,14 +15443,16 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
14956
15443
  }
14957
15444
  if (activeTab === "skills") {
14958
15445
  const s = filteredSkills[index];
14959
- if (s) skillsToggle.toggle(s.name);
15446
+ if (s && !skillOriginBadge(s)) skillsToggle.toggle(s.name);
14960
15447
  return;
14961
15448
  }
14962
15449
  if (activeTab === "extensions") {
14963
15450
  const row = extensionVisibleRows[index];
14964
15451
  if (!row) return;
14965
15452
  if (row.kind === "extension") extensionsToggle.toggle(row.ext.name);
14966
- 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);
14967
15456
  return;
14968
15457
  }
14969
15458
  if (activeTab === "mcps") {
@@ -14995,6 +15484,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
14995
15484
  skillsToggle,
14996
15485
  extensionVisibleRows,
14997
15486
  extensionsToggle,
15487
+ pluginsToggle,
14998
15488
  activateConfigRow,
14999
15489
  mcpVisibleRows,
15000
15490
  mcpsToggle,
@@ -15055,6 +15545,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15055
15545
  return;
15056
15546
  }
15057
15547
  if (row?.kind === "config" && row.item.type === "boolean") return;
15548
+ if (row?.kind === "plugin") return;
15058
15549
  activateRowAt(index);
15059
15550
  return;
15060
15551
  }
@@ -15341,6 +15832,10 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15341
15832
  actions.onRefreshSkills();
15342
15833
  return;
15343
15834
  }
15835
+ if (activeTab === "extensions" && actions?.onRefreshPlugins) {
15836
+ actions.onRefreshPlugins();
15837
+ return;
15838
+ }
15344
15839
  if (activeTab === "mcps") {
15345
15840
  if (actions?.onRefreshMcps) actions.onRefreshMcps();
15346
15841
  fireMcpToolsRefresh();
@@ -15366,8 +15861,8 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15366
15861
  total: mcpsCatalog.length
15367
15862
  },
15368
15863
  extensions: {
15369
- current: extensionVisibleRows.filter((r) => r.kind === "extension" || r.kind === "errored").length,
15370
- 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
15371
15866
  }
15372
15867
  };
15373
15868
  const editingInputItem = editingInputKey ? SETTINGS_INPUTS.find((inp) => inp.key === editingInputKey) ?? null : null;
@@ -15452,9 +15947,10 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15452
15947
  onRowClick: clickRowAt,
15453
15948
  rows: extensionVisibleRows,
15454
15949
  enabledSet: extensionsToggle.enabledSet,
15950
+ pluginEnabledSet: pluginsToggle.enabledSet,
15455
15951
  extensionConfig: settings.extensionConfig,
15456
15952
  categories: extensionConfigCategories,
15457
- totalCount: extensionsCatalog.length + extensionsErrored.length,
15953
+ totalCount: extensionsCatalog.length + extensionsErrored.length + pluginList.length,
15458
15954
  cursor,
15459
15955
  highlightBg: SURFACE.selection,
15460
15956
  query,
@@ -15609,6 +16105,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15609
16105
  focusedMcpStatus,
15610
16106
  canRefreshSkills: !!actions?.onRefreshSkills,
15611
16107
  canRefreshMcps: !!actions?.onRefreshMcps,
16108
+ canRefreshPlugins: !!actions?.onRefreshPlugins,
15612
16109
  canEditExtension: !!actions?.onEditExtension,
15613
16110
  canCreateExtension: !!actions?.onCreateExtension,
15614
16111
  canUpdateExtension: !!actions?.onUpdateExtension,
@@ -15782,57 +16279,89 @@ function GeneralList({ items, cursor, highlightBg, query, contentCols, editingKe
15782
16279
  });
15783
16280
  }
15784
16281
  const EXTENSION_CONFIG_INDENT = 3;
15785
- 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 }) {
15786
16283
  const COLOR = useColors();
16284
+ const SURFACE = useSurfaces();
16285
+ const extBadge = kindBadgeStyle("ext", COLOR, SURFACE);
16286
+ const pluginBadge = kindBadgeStyle("plugin", COLOR, SURFACE);
15787
16287
  const home = homedir();
15788
16288
  if (totalCount === 0) return /* @__PURE__ */ jsxs("box", {
15789
16289
  style: { flexDirection: "column" },
15790
- children: [/* @__PURE__ */ jsx$1("text", {
15791
- fg: COLOR.dim,
15792
- children: "No extensions discovered."
15793
- }), /* @__PURE__ */ jsxs("text", {
15794
- fg: COLOR.mute,
15795
- children: [
15796
- "Drop an",
15797
- /* @__PURE__ */ jsx$1("span", {
15798
- fg: COLOR.model,
15799
- children: " index.ts "
15800
- }),
15801
- "into",
15802
- /* @__PURE__ */ jsx$1("span", {
15803
- fg: COLOR.model,
15804
- children: " .zidane/extensions/<name>/ "
15805
- }),
15806
- "or",
15807
- /* @__PURE__ */ jsx$1("span", {
15808
- fg: COLOR.model,
15809
- children: " .agents/extensions/<name>/ "
15810
- }),
15811
- "(project or",
15812
- /* @__PURE__ */ jsx$1("span", {
15813
- fg: COLOR.model,
15814
- children: " ~/"
15815
- }),
15816
- "). Author with",
15817
- /* @__PURE__ */ jsx$1("span", {
15818
- fg: COLOR.model,
15819
- children: " defineExtension({ name, setup }) "
15820
- }),
15821
- "from",
15822
- /* @__PURE__ */ jsx$1("span", {
15823
- fg: COLOR.model,
15824
- children: " zidane "
15825
- }),
15826
- "— see",
15827
- /* @__PURE__ */ jsx$1("span", {
15828
- fg: COLOR.model,
15829
- children: " docs/EXTENSIONS.md "
15830
- }),
15831
- "."
15832
- ]
15833
- })]
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
+ ]
15834
16363
  });
15835
- 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}"` });
15836
16365
  const descCols = Math.max(0, contentCols - 1 - 1 - 6);
15837
16366
  const groups = [];
15838
16367
  for (let i = 0; i < rows.length; i++) {
@@ -15845,6 +16374,16 @@ function ExtensionsList({ rows, enabledSet, extensionConfig, categories, totalCo
15845
16374
  });
15846
16375
  continue;
15847
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
+ }
15848
16387
  if (row.kind === "extension") {
15849
16388
  groups.push({
15850
16389
  kind: "extension",
@@ -15893,6 +16432,12 @@ function ExtensionsList({ rows, enabledSet, extensionConfig, categories, totalCo
15893
16432
  fg: focused ? COLOR.brand : COLOR.dim,
15894
16433
  children: group.ext.name
15895
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
+ }),
15896
16441
  /* @__PURE__ */ jsx$1("span", {
15897
16442
  fg: COLOR.mute,
15898
16443
  children: ` ${where}`
@@ -15911,6 +16456,108 @@ function ExtensionsList({ rows, enabledSet, extensionConfig, categories, totalCo
15911
16456
  })]
15912
16457
  }, `err:${group.ext.name}`);
15913
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
+ }
15914
16561
  const focused = group.index === cursor;
15915
16562
  const bg = focused ? highlightBg : void 0;
15916
16563
  const enabled = enabledSet.has(group.ext.name);
@@ -16029,6 +16676,12 @@ function ExtensionsList({ rows, enabledSet, extensionConfig, categories, totalCo
16029
16676
  group.ext.definition?.version && /* @__PURE__ */ jsx$1("span", {
16030
16677
  fg: COLOR.mute,
16031
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
16032
16685
  })
16033
16686
  ]
16034
16687
  }),
@@ -16272,6 +16925,7 @@ function ConfigModelPickerRow({ id, label, description, catalog, multi, initial,
16272
16925
  }
16273
16926
  function SkillsList({ items, enabledSet, totalCount, cursor, highlightBg, query, contentCols, onRowClick }) {
16274
16927
  const COLOR = useColors();
16928
+ const contributedBadge = kindBadgeStyle("plugin", COLOR, useSurfaces());
16275
16929
  if (totalCount === 0) return /* @__PURE__ */ jsxs("box", {
16276
16930
  style: { flexDirection: "column" },
16277
16931
  children: [/* @__PURE__ */ jsx$1("text", {
@@ -16314,6 +16968,7 @@ function SkillsList({ items, enabledSet, totalCount, cursor, highlightBg, query,
16314
16968
  const enabled = enabledSet.has(entry.name);
16315
16969
  const description = truncateToCols(entry.description ?? "", descCols);
16316
16970
  const origin = skillOriginLabel(entry);
16971
+ const badge = skillOriginBadge(entry);
16317
16972
  return /* @__PURE__ */ jsxs("box", {
16318
16973
  onMouseDown: onRowClick && ((event) => {
16319
16974
  event.stopPropagation();
@@ -16333,14 +16988,17 @@ function SkillsList({ items, enabledSet, totalCount, cursor, highlightBg, query,
16333
16988
  onMouseDown: onRowClick && ((event) => {
16334
16989
  event.stopPropagation();
16335
16990
  event.preventDefault();
16336
- onRowClick(i, inCheckboxZone(event, 2) ? "checkbox" : "row");
16991
+ onRowClick(i, !badge && inCheckboxZone(event, 2) ? "checkbox" : "row");
16337
16992
  }),
16338
16993
  children: [
16339
16994
  /* @__PURE__ */ jsx$1("span", {
16340
16995
  fg: focused ? COLOR.brand : COLOR.mute,
16341
16996
  children: focused ? "▶ " : " "
16342
16997
  }),
16343
- /* @__PURE__ */ jsx$1("span", {
16998
+ badge ? /* @__PURE__ */ jsx$1("span", {
16999
+ fg: COLOR.mute,
17000
+ children: " "
17001
+ }) : /* @__PURE__ */ jsx$1("span", {
16344
17002
  fg: enabled ? COLOR.accent : COLOR.mute,
16345
17003
  children: enabled ? "[✓] " : "[ ] "
16346
17004
  }),
@@ -16348,7 +17006,11 @@ function SkillsList({ items, enabledSet, totalCount, cursor, highlightBg, query,
16348
17006
  fg: focused ? COLOR.brand : COLOR.dim,
16349
17007
  children: entry.name
16350
17008
  }),
16351
- 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", {
16352
17014
  fg: COLOR.mute,
16353
17015
  children: [" · ", origin]
16354
17016
  })
@@ -17051,13 +17713,13 @@ function renderMcpErrors(errors, home, warnColor, dimColor) {
17051
17713
  }, err.path))
17052
17714
  });
17053
17715
  }
17054
- 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 }) {
17055
17717
  const COLOR = useColors();
17056
17718
  const onServerRow = focusedMcpRow?.kind === "server";
17057
17719
  const showLogin = activeTab === "mcps" && onServerRow && !!focusedMcp && !!focusedMcpStatus && canLogin$1(focusedMcp, focusedMcpStatus);
17058
17720
  const showLogout = activeTab === "mcps" && onServerRow && !!focusedMcpStatus && canLogout$1(focusedMcpStatus);
17059
17721
  const showCancel = activeTab === "mcps" && authorizing;
17060
- const showRefresh = activeTab === "skills" && canRefreshSkills || activeTab === "mcps" && canRefreshMcps;
17722
+ const showRefresh = activeTab === "skills" && canRefreshSkills || activeTab === "mcps" && canRefreshMcps || activeTab === "extensions" && canRefreshPlugins;
17061
17723
  return /* @__PURE__ */ jsx$1(HintRow, { hints: [
17062
17724
  {
17063
17725
  key: "←→",
@@ -17183,6 +17845,21 @@ function extensionCorpus(e) {
17183
17845
  ].join(" ").toLowerCase();
17184
17846
  }
17185
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
+ /**
17186
17863
  * What a config row shows at rest. Secrets are masked (last 4 kept so the user
17187
17864
  * can tell WHICH token is stored), unset model rows read `(unset)`.
17188
17865
  *
@@ -18134,7 +18811,7 @@ const SESSION_SEARCH_INDEX_CACHE_MAX_BYTES = 64 * 1024 * 1024;
18134
18811
  */
18135
18812
  /** How long the `⚡ cache break` footer toast stays visible after a break. */
18136
18813
  const CACHE_BREAK_CHIP_MS = 8e3;
18137
- const debugLog = process.env.ZIDANE_DEBUG ? (label, err) => console.error(`[zidane/tui] ${label}:`, err) : () => {};
18814
+ const debugLog = createDebugLog("tui");
18138
18815
  /**
18139
18816
  * Launch the user's preferred editor on `path`. Tries `$EDITOR` /
18140
18817
  * `$VISUAL` first (the standard *nix convention for "open this
@@ -18220,21 +18897,6 @@ function reduceEditsByOutcomes(edits, outcomes) {
18220
18897
  }
18221
18898
  return out;
18222
18899
  }
18223
- /**
18224
- * Dedup an MCP-server list by `name`. First entry wins — used at
18225
- * agent build to give project-level `mcps.json` precedence over any
18226
- * extension-contributed servers that happen to share a name.
18227
- */
18228
- function dedupMcpServers(servers) {
18229
- const seen = /* @__PURE__ */ new Set();
18230
- const out = [];
18231
- for (const server of servers) {
18232
- if (seen.has(server.name)) continue;
18233
- seen.add(server.name);
18234
- out.push(server);
18235
- }
18236
- return out;
18237
- }
18238
18900
  const ExtensionRegistryContext = createContext(null);
18239
18901
  function useExtensionRegistryState() {
18240
18902
  const ctx = useContext(ExtensionRegistryContext);
@@ -18496,7 +19158,7 @@ function AppShell() {
18496
19158
  const sessionSafelistRef = useRef(/* @__PURE__ */ new Set());
18497
19159
  const streamRef = useRef(null);
18498
19160
  const pendingAnnotationsRef = useRef(/* @__PURE__ */ new Map());
18499
- 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();
18500
19162
  const mcpCredentialStore = useMemo(() => createFileMcpCredentialStore(dataDir), [dataDir]);
18501
19163
  const dispatchAuth = useMcpAuthDispatch();
18502
19164
  const dispatchAuthRef = useRef(dispatchAuth);
@@ -18505,8 +19167,38 @@ function AppShell() {
18505
19167
  const onLoginMcpRef = useRef(null);
18506
19168
  const skillsCatalogRef = useRef(skillsCatalog);
18507
19169
  skillsCatalogRef.current = skillsCatalog;
19170
+ const pluginsCatalogRef = useRef(pluginsCatalog);
19171
+ pluginsCatalogRef.current = pluginsCatalog;
18508
19172
  const enabledSkillsRef = useRef(settings.enabledSkills);
18509
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;
18510
19202
  const importForeignConfigsRef = useRef(settings.importForeignConfigs);
18511
19203
  importForeignConfigsRef.current = settings.importForeignConfigs;
18512
19204
  const importRulerConfigsRef = useRef(settings.importRulerConfigs);
@@ -18517,14 +19209,36 @@ function AppShell() {
18517
19209
  enabledMcpsRef.current = settings.enabledMcps;
18518
19210
  const disabledMcpToolsRef = useRef(settings.disabledMcpTools);
18519
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
+ */
18520
19225
  const mcpRuntimeKey = useMemo(() => JSON.stringify({
18521
19226
  catalog: mcpsCatalog.map(({ config }) => config),
18522
19227
  enabled: settings.enabledMcps ?? null,
18523
- 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
18524
19236
  }), [
18525
19237
  mcpsCatalog,
18526
19238
  settings.enabledMcps,
18527
- settings.disabledMcpTools
19239
+ settings.disabledMcpTools,
19240
+ pluginsCatalog,
19241
+ settings.enabledPlugins
18528
19242
  ]);
18529
19243
  const filesCatalogRef = useRef(filesCatalog);
18530
19244
  filesCatalogRef.current = filesCatalog;
@@ -19441,18 +20155,26 @@ function AppShell() {
19441
20155
  foreignAgents: importForeignConfigsRef.current === true,
19442
20156
  ruler: importRulerConfigsRef.current !== false
19443
20157
  });
19444
- 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
+ });
19445
20163
  const extensionSkillNames = liveExtensionRegistry.skills.map((s) => s.name);
19446
20164
  const userEnabledSkills = enabledSkillsRef.current;
19447
20165
  const skillsConfig = buildSkillsConfig({
19448
20166
  scan: skillsScan,
19449
- 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
+ ]))
19450
20172
  });
19451
- const projectMcps = buildMcpServers({
20173
+ const projectMcps = [...buildMcpServers({
19452
20174
  discovered: mcpsCatalogRef.current,
19453
20175
  enabled: enabledMcpsRef.current,
19454
20176
  ...disabledMcpToolsRef.current ? { disabledTools: disabledMcpToolsRef.current } : {}
19455
- });
20177
+ }), ...pluginContributions.mcpServers];
19456
20178
  const allowInteraction = allowInteractionRef.current !== false;
19457
20179
  const interactionTools = allowInteraction ? createInteractionTools({ requestInteraction: makeRequestInteraction(interactions) }) : {};
19458
20180
  const actualCwd = cwdRef.current;
@@ -19504,14 +20226,18 @@ function AppShell() {
19504
20226
  ...skillsConfig,
19505
20227
  ...composedPreset.skills ?? {}
19506
20228
  };
19507
- 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;
19508
20234
  const skillEditMarker = readSkillEditMarker(session.metadata[SKILL_EDIT_METADATA_KEY]);
19509
20235
  const skills = skillEditMarker ? withZidaneSkillEditSkill(withExtensionSkill, skillEditMarker, { cwd: actualCwd }) : withExtensionSkill;
19510
- const mergedMcpServers = dedupMcpServers([
20236
+ const mergedMcpServers = dedupMcpServersByName([
19511
20237
  ...projectMcps,
19512
20238
  ...launch?.mcp ?? [],
19513
20239
  ...composedPreset.mcpServers ?? []
19514
- ]);
20240
+ ], (names) => debugLog("mcp name collision — later configs dropped (first wins)", names));
19515
20241
  const mergedTools = {
19516
20242
  ...interactionTools,
19517
20243
  chatTurnsRead: chatTurnsReadTool,
@@ -22950,6 +23676,9 @@ function AppShell() {
22950
23676
  } } : {},
22951
23677
  extensionsCatalog: extensionRegistry.loaded,
22952
23678
  extensionsInactive: extensionRegistry.inactive,
23679
+ pluginsCatalog,
23680
+ pluginsFromExtensions: extensionRegistry.plugins,
23681
+ extensionContributedSkills: liveRegistryForSettings.skills,
22953
23682
  authentication: {
22954
23683
  currentProviderLabel: picked?.provider.label,
22955
23684
  currentProviderKey: picked?.provider.key,
@@ -22964,6 +23693,7 @@ function AppShell() {
22964
23693
  onCancelLoginMcp,
22965
23694
  onRefreshSkills,
22966
23695
  onRefreshMcps,
23696
+ onRefreshPlugins,
22967
23697
  onRefreshMcpTools,
22968
23698
  onEditExtension,
22969
23699
  onEditSkill,
@@ -22975,7 +23705,9 @@ function AppShell() {
22975
23705
  catalog: extensionRegistryLiveRef.current.loaded,
22976
23706
  inactive: extensionRegistryLiveRef.current.inactive,
22977
23707
  configCategories: extensionRegistryLiveRef.current.configCategories,
22978
- configItems: extensionRegistryLiveRef.current.configItems
23708
+ configItems: extensionRegistryLiveRef.current.configItems,
23709
+ plugins: extensionRegistryLiveRef.current.plugins,
23710
+ skills: liveRegistryForSettingsRef.current.skills
22979
23711
  }),
22980
23712
  onInstallSkill,
22981
23713
  onAddMcp,
@@ -24888,6 +25620,7 @@ async function runTui(options = {}) {
24888
25620
  });
24889
25621
  const prefix = options.prefix ?? process.env.ZIDANE_PREFIX ?? ".zidane";
24890
25622
  const extensionScanPaths = options.extensionScanPaths ?? parseExtensionScanPathEnv();
25623
+ const pluginScanPaths = options.pluginScanPaths ?? parsePluginScanPathEnv();
24891
25624
  const explicitStorageDir = options.storageDir ?? process.env.ZIDANE_STORAGE_DIR;
24892
25625
  const paths = resolveStoragePaths({
24893
25626
  prefix,
@@ -24923,6 +25656,7 @@ async function runTui(options = {}) {
24923
25656
  const config = resolveConfig({
24924
25657
  ...options,
24925
25658
  extensionScanPaths,
25659
+ pluginScanPaths,
24926
25660
  extensionRegistry,
24927
25661
  trustProjectExtensions,
24928
25662
  ...options.launch && !options.preset && !options.agents ? { preset: basic_default } : {},