zidane 6.2.30 → 6.2.32

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 (93) hide show
  1. package/dist/{acp-D3manAhU.js → acp-BOXBhzMv.js} +3 -3
  2. package/dist/{acp-D3manAhU.js.map → acp-BOXBhzMv.js.map} +1 -1
  3. package/dist/acp-cli.js +4 -4
  4. package/dist/acp.d.ts +1 -1
  5. package/dist/acp.js +1 -1
  6. package/dist/{agent-C--jBbuk.js → agent-CHtfwiSc.js} +79 -7
  7. package/dist/agent-CHtfwiSc.js.map +1 -0
  8. package/dist/agent.d.ts +1 -1
  9. package/dist/agent.js +2 -2
  10. package/dist/{anthropic-BEYY0hVK.js → anthropic--S5Wg4Oy.js} +10 -6
  11. package/dist/anthropic--S5Wg4Oy.js.map +1 -0
  12. package/dist/{auth-C4YPRDN4.js → auth-up7hgE9H.js} +2 -2
  13. package/dist/{auth-C4YPRDN4.js.map → auth-up7hgE9H.js.map} +1 -1
  14. package/dist/{cache-telemetry-KJ7B-_Gh.js → cache-telemetry-BhTryxFT.js} +7 -2
  15. package/dist/cache-telemetry-BhTryxFT.js.map +1 -0
  16. package/dist/chat/pure.d.ts +4 -4
  17. package/dist/chat/pure.js +3 -3
  18. package/dist/chat.d.ts +112 -5
  19. package/dist/chat.d.ts.map +1 -1
  20. package/dist/chat.js +6 -6
  21. package/dist/contexts/daytona.d.ts +1 -1
  22. package/dist/contexts/e2b.d.ts +1 -1
  23. package/dist/eval.d.ts +1 -1
  24. package/dist/eval.js +1 -1
  25. package/dist/extensions.d.ts +1 -1
  26. package/dist/extensions.js +1 -1
  27. package/dist/headless.d.ts +1 -1
  28. package/dist/headless.js +3 -3
  29. package/dist/{index-DzQyRZ8Q.d.ts → index-DZxVqh6L.d.ts} +278 -5
  30. package/dist/index-DZxVqh6L.d.ts.map +1 -0
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +9 -9
  33. package/dist/{login-CEOlp1r6.js → login-CWwu9Sex.js} +114 -5
  34. package/dist/login-CWwu9Sex.js.map +1 -0
  35. package/dist/mcp.d.ts +1 -1
  36. package/dist/{messages-Dy9t7DbQ.js → messages-t5Sv-vyv.js} +161 -2
  37. package/dist/{messages-Dy9t7DbQ.js.map → messages-t5Sv-vyv.js.map} +1 -1
  38. package/dist/output/stream-json.d.ts +1 -1
  39. package/dist/output/terminal.d.ts +1 -1
  40. package/dist/presets.d.ts +1 -1
  41. package/dist/presets.js +1 -1
  42. package/dist/providers/anthropic.d.ts +1 -1
  43. package/dist/providers/anthropic.js +1 -1
  44. package/dist/providers/arcee.d.ts +1 -1
  45. package/dist/providers/baseten.d.ts +1 -1
  46. package/dist/providers/cerebras.d.ts +1 -1
  47. package/dist/providers/local.d.ts +1 -1
  48. package/dist/providers/openai-compat.d.ts +1 -1
  49. package/dist/providers/openai.d.ts +1 -1
  50. package/dist/providers/openai.js +1 -1
  51. package/dist/providers/openrouter.d.ts +1 -1
  52. package/dist/providers/xai.d.ts +1 -1
  53. package/dist/providers.d.ts +1 -1
  54. package/dist/providers.js +1 -1
  55. package/dist/read-state-rDmfeLmz.js.map +1 -1
  56. package/dist/restate.d.ts +1 -1
  57. package/dist/session/sqlite.d.ts +1 -1
  58. package/dist/{session-DkVnYGm9.js → session-DtRtNDBz.js} +61 -5
  59. package/dist/session-DtRtNDBz.js.map +1 -0
  60. package/dist/session.d.ts +2 -2
  61. package/dist/session.js +3 -3
  62. package/dist/skills.d.ts +1 -1
  63. package/dist/{tool-formatters-BI_Z-0eY.d.ts → tool-formatters-BCA9SPQy.d.ts} +14 -3
  64. package/dist/tool-formatters-BCA9SPQy.d.ts.map +1 -0
  65. package/dist/tools/fetch-url.d.ts +1 -1
  66. package/dist/tools/web-search.d.ts +1 -1
  67. package/dist/tools.d.ts +1 -1
  68. package/dist/tools.js +1 -1
  69. package/dist/{transcript-anchors-CI398UQZ.d.ts → transcript-anchors-BaU0gkGA.d.ts} +12 -3
  70. package/dist/{transcript-anchors-CI398UQZ.d.ts.map → transcript-anchors-BaU0gkGA.d.ts.map} +1 -1
  71. package/dist/{transcript-anchors-0PpFmWrD.js → transcript-anchors-vVG3xyD0.js} +3330 -2639
  72. package/dist/transcript-anchors-vVG3xyD0.js.map +1 -0
  73. package/dist/tui.d.ts +11 -3
  74. package/dist/tui.d.ts.map +1 -1
  75. package/dist/tui.js +241 -805
  76. package/dist/tui.js.map +1 -1
  77. package/dist/{turn-operations-Z3R0sjye.d.ts → turn-operations-BofuxNtk.d.ts} +16 -4
  78. package/dist/turn-operations-BofuxNtk.d.ts.map +1 -0
  79. package/dist/{turn-operations-4tnvKwfJ.js → turn-operations-CtEpeB_s.js} +99 -3
  80. package/dist/turn-operations-CtEpeB_s.js.map +1 -0
  81. package/dist/types-BIarq1qE.js.map +1 -1
  82. package/dist/types.d.ts +1 -1
  83. package/package.json +1 -1
  84. package/dist/agent-C--jBbuk.js.map +0 -1
  85. package/dist/anthropic-BEYY0hVK.js.map +0 -1
  86. package/dist/cache-telemetry-KJ7B-_Gh.js.map +0 -1
  87. package/dist/index-DzQyRZ8Q.d.ts.map +0 -1
  88. package/dist/login-CEOlp1r6.js.map +0 -1
  89. package/dist/session-DkVnYGm9.js.map +0 -1
  90. package/dist/tool-formatters-BI_Z-0eY.d.ts.map +0 -1
  91. package/dist/transcript-anchors-0PpFmWrD.js.map +0 -1
  92. package/dist/turn-operations-4tnvKwfJ.js.map +0 -1
  93. package/dist/turn-operations-Z3R0sjye.d.ts.map +0 -1
package/dist/tui.js CHANGED
@@ -1,30 +1,30 @@
1
- import { $ as McpAuthProvider, $t as filterChainFallbacks, A as runOAuthLogin, At as useSettings, B as mcpToolsCachePath, Bn as pruneTodosByRun, Ct as DEFAULT_SETTINGS, D as formatPathForCwd, Dn as accentColor, Dt as SETTINGS_TOGGLES, Et as SETTINGS_INPUTS, F as parseJsonTolerant, G as useMcpToolToggleMap, Gt as useDiscoveryOptional, Ht as createDiscoverySlot, I as parseMcpsFile, J as indexOfServerRow, Jt as resolveConfig, Kt as ConfigProvider, L as parseClaudeState, M as buildMcpServers, N as defaultMcpsConfigPaths, Nn as TODO_STATUS_GLYPHS, Nt as resolveChipColor, O as fetchOAuthRedirect, Ot as SettingsProvider, P as discoverProjectMcps, Pt as resolveTheme, Qt as chainsEqual, Sr as usageByModel, St as useEnabledToggleSet, T as suggestSafelistEntries, Tt as SETTINGS_CHOICES, U as subscribeMcpToolsCache, Un as useActiveTodos, Ut as DiscoveryProvider, V as refreshMcpToolsCatalog, Wt as useDiscovery, X as createFileMcpCredentialStore, Xt as buildRunModel, Y as parentServerName, Yt as resolveStoragePaths, Zt as chainPositionOf, _ as useSafeModeQueue, _r as sumRunCosts, _t as hintsLength, a as useSurfaces, an as tryOpenBrowser, at as InteractionsProvider, b as getSafelist, br as toolResultText, c as useStreamBuffer, cn as buildUpdateHint, cr as deriveSessionTitle, ct as createInteractionTools, d as discoverProjectSkills, dr as listSessionMeta, dt as pendingInteractionsFromTurns, en as providerKeySetChanged, er as buildBuildSystem, et as useMcpAuthDispatch, fr as loadState, g as useSafeModeActions, gr as stripSpawnTokensLine, gt as clipHintsToWidth, h as SafeModeProvider, hr as serverToolResultSummary, ht as EMPTY_HINTS, i as useSelectStyle, in as useCompletion, ir as createChatTurnsReadTool, it as ASK_USER_TOOL, j as supportsOAuth, k as oauthUsesManualCodePaste, kt as clampFps, l as buildSkillsConfig, ln as useUpdateCheck, lr as eventsFromTurns, m as writeSessionExport, mt as useInteractionsQueue, n as ThemeProvider, nr as envSection, nt as getMcpAuthStatus, or as compactSummaryEvent, ot as PRESENT_PLAN_TOOL, pr as marginTopFor, pt as useInteractionsActions, q as buildVisibleMcpRows, qt as useConfig, r as useColors, rn as toggleChainEntry, rr as chatReferenceTokensForPrompt, s as useTheme, sn as bootTick, st as buildResumedToolResultsTurn, t as computeTurnAnchors, tn as restoreChainFallbacks, tr as buildPlanSystem, tt as useMcpAuthState, u as defaultSkillScanPaths, ur as lastContextSizeFromTurns, ut as makeRequestInteraction, vn as chatAutoCompactBehavior, vt as truncateTrailing, wt as SETTINGS_CATEGORIES, x as isOnSafelist, xr as updateToolEventOutcomes, xt as listProjectFiles, y as addToSafelist, yn as discoverAgentsMd, yr as toolCallPreview, yt as buildHints, z as loadMcpToolsCache } from "./transcript-anchors-0PpFmWrD.js";
2
- import { B as createExtensionPromptBridge, Bt as KEYBINDING_KEY_COL_WIDTH, Ct as tailTruncate, Dt as createModelSearchTool, F as summaryToTurn, Gt as matchesBinding, Ht as formatBindingForDisplay, J as updateInstalledExtension, S as compactConversation, Tt as looksBinary, U as installExtensionFromSpec, Ut as groupBindings, V as ExtensionInstallError, Vt as ensureKeybindingsFile, Wt as keybindingsPath, X as registerExtensionHostModules, Xt as resolveExtensionKeybindings, Y as createExtensionHud, Z as discoverAndBuildExtensionRegistry, _ as resolveTasksDir, at as createExtensionHostRuntimeBridge, b as selectFilesFromSession, c as applyCompactSummaryCutoff, dn as resolveExtensionConfigValue, et as composeExtensionPreset, f as cleanupPersistedSession, fn as sanitizeExtensionConfigActionPatch, g as resolvePersistDir, gn as effectiveContextWindow, gt as isSpawnTool, h as resolveMcpWarningsDir, hn as OUTPUT_RESERVE_TOKENS, in as scanExtensionDirectories, it as partitionExtensionCatalog, m as rehomePersistedStubs, nt as filterExtensionRegistry, q as removeInstalledExtension, rn as parseExtensionScanPathEnv, sn as coerceExtensionConfigInput, t as createAgent, tn as inlineLoadedExtension, tt as disposeExtensionRegistry, ut as basic_default, v as buildPostCompactAttachments } from "./agent-C--jBbuk.js";
1
+ import { $ as InteractionsProvider, $t as countClaudeSessionFiles, A as runOAuthLogin, Bt as ConfigProvider, Cn as discoverAgentsMd, Cr as toolCallPreview, Ct as clampFps, D as formatPathForCwd, Dt as resolveChipColor, Er as usageByModel, F as refreshMcpToolsCatalog, Gt as chainPositionOf, H as indexOfServerRow, Ht as resolveConfig, It as createDiscoverySlot, J as useMcpAuthDispatch, Jt as providerKeySetChanged, Kt as chainsEqual, L as subscribeMcpToolsCache, Ln as TODO_STATUS_GLYPHS, Lt as DiscoveryProvider, N as loadMcpToolsCache, O as fetchOAuthRedirect, Ot as resolveTheme, P as mcpToolsCachePath, Q as ASK_USER_TOOL, Qt as useCompletion, Rt as useDiscovery, Sn as chatAutoCompactBehavior, St as SettingsProvider, T as suggestSafelistEntries, Tr as updateToolEventOutcomes, U as parentServerName, Ut as resolveStoragePaths, V as buildVisibleMcpRows, Vt as useConfig, W as createFileMcpCredentialStore, Wn as pruneTodosByRun, Wt as buildRunModel, X as getMcpAuthStatus, Y as useMcpAuthState, Yt as restoreChainFallbacks, Zt as toggleChainEntry, _ as useSafeModeQueue, _r as marginTopFor, _t as DEFAULT_SETTINGS, a as useSurfaces, an as buildMcpServers, ar as buildPlanSystem, at as pendingInteractionsFromTurns, b as getSafelist, br as stripSpawnTokensLine, bt as SETTINGS_INPUTS, c as useStreamBuffer, cr as createChatTurnsReadTool, ct as useInteractionsQueue, d as discoverProjectSkills, dn as bootTick, dt as hintsLength, en as hasClaudeCodeSetup, et as PRESENT_PLAN_TOOL, fn as buildUpdateHint, fr as deriveSessionTitle, ft as truncateTrailing, g as useSafeModeActions, gr as loadState, gt as useEnabledToggleSet, h as SafeModeProvider, hr as listSessionMeta, ht as listProjectFiles, i as useSelectStyle, in as upsertMcpServer, ir as buildBuildSystem, it as makeRequestInteraction, j as supportsOAuth, jn as accentColor, k as oauthUsesManualCodePaste, l as buildSkillsConfig, ln as tryOpenBrowser, lt as EMPTY_HINTS, m as writeSessionExport, mr as lastContextSizeFromTurns, n as ThemeProvider, nn as mcpsWritePath, nt as createInteractionTools, or as envSection, pn as useUpdateCheck, pr as eventsFromTurns, pt as buildHints, q as McpAuthProvider, qn as useActiveTodos, qt as filterChainFallbacks, r as useColors, rn as removeMcpServer, s as useTheme, sn as discoverProjectMcps, sr as chatReferenceTokensForPrompt, st as useInteractionsActions, t as computeTurnAnchors, tn as importFromClaudeCode, tt as buildResumedToolResultsTurn, u as defaultSkillScanPaths, ur as compactSummaryEvent, ut as clipHintsToWidth, vt as SETTINGS_CATEGORIES, wr as toolResultText, wt as useSettings, x as isOnSafelist, xr as sumRunCosts, xt as SETTINGS_TOGGLES, y as addToSafelist, yr as serverToolResultSummary, yt as SETTINGS_CHOICES, z as useMcpToolToggleMap, zt as useDiscoveryOptional } from "./transcript-anchors-vVG3xyD0.js";
2
+ import { B as createExtensionPromptBridge, Bt as KEYBINDING_KEY_COL_WIDTH, Ct as tailTruncate, Dt as createModelSearchTool, F as summaryToTurn, Gt as matchesBinding, Ht as formatBindingForDisplay, J as updateInstalledExtension, S as compactConversation, Tt as looksBinary, U as installExtensionFromSpec, Ut as groupBindings, V as ExtensionInstallError, Vt as ensureKeybindingsFile, Wt as keybindingsPath, X as registerExtensionHostModules, Xt as resolveExtensionKeybindings, Y as createExtensionHud, Z as discoverAndBuildExtensionRegistry, _ as resolveTasksDir, at as createExtensionHostRuntimeBridge, b as selectFilesFromSession, c as applyCompactSummaryCutoff, dn as resolveExtensionConfigValue, et as composeExtensionPreset, f as cleanupPersistedSession, fn as sanitizeExtensionConfigActionPatch, g as resolvePersistDir, gn as effectiveContextWindow, gt as isSpawnTool, h as resolveMcpWarningsDir, hn as OUTPUT_RESERVE_TOKENS, in as scanExtensionDirectories, it as partitionExtensionCatalog, m as rehomePersistedStubs, nt as filterExtensionRegistry, q as removeInstalledExtension, rn as parseExtensionScanPathEnv, sn as coerceExtensionConfigInput, t as createAgent, tn as inlineLoadedExtension, tt as disposeExtensionRegistry, ut as basic_default, v as buildPostCompactAttachments } from "./agent-CHtfwiSc.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-o8y5B3Tm.js";
6
6
  import { l as errorMessage } from "./errors-DJUxZg9b.js";
7
- import { M as findGitRoot, j as projectUserPaths } from "./resolve-BPTA1RpX.js";
7
+ import { M as findGitRoot } from "./resolve-BPTA1RpX.js";
8
8
  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
9
  import { i as sniffImageMediaType } from "./media-sniff-Bn76JxAu.js";
10
10
  import { h as hasAuthorizationHeader, n as connectMcpServers, o as nonOwningMcpView, p as McpOAuthProvider } from "./mcp-Ct5tF4oj.js";
11
11
  import { replaceDynamicSection } from "./prompt.js";
12
- import { a as getExtensionSessionState, c as setExtensionSessionState, i as demoteStaleRunningStatus, o as loadSession, t as createSession } from "./session-DkVnYGm9.js";
12
+ import { a as getExtensionSessionState, c as setExtensionSessionState, i as demoteStaleRunningStatus, o as loadSession, t as createSession } from "./session-DtRtNDBz.js";
13
13
  import { n as flattenTurns, r as formatTokenUsage, t as effectiveInputFromTurn } from "./stats-DAKBEKjc.js";
14
14
  import { o as withTimeout } from "./timeout-BDetbuN6.js";
15
15
  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-CbcdZpfC.js";
16
16
  import { s as resolveCompletionSubmit } from "./completion-core-BRo86R_L.js";
17
17
  import { i as installSkillFromSpec, r as SkillInstallError } from "./skills-C7eIjWYm.js";
18
- import { i as ZIDANE_EXTENSION_SKILL_NAME, l as withZidaneExtensionSkill, o as extensionDirForCwd, r as EXTENSION_EDIT_METADATA_KEY, s as extensionScaffold, t as loginMcpServer } from "./login-CEOlp1r6.js";
19
- 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-C4YPRDN4.js";
18
+ 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-CWwu9Sex.js";
19
+ 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-up7hgE9H.js";
20
20
  import { createDockerContext } from "./contexts/docker.js";
21
21
  import { createTuiStore } from "./session/sqlite.js";
22
- import { $ as extractEditPayload, B as isVisible, C as chunkMarkdownForRender, E as stripHtmlCommentsOutsideCode, F as BASH_CALL_ID_PREFIX, G as streamEventSearchText, H as promptHistoryFromEvents, I as EDIT_TOOL_NAMES, J as buildContextualDiff, K as turnSelectionOwnership, L as isEditErrorResult, N as blendHsl, O as createSkillsCompletionProvider, P as buildLinearRamp, R as isTurnHighlighted, T as splitMarkdownCodeBlocks, U as selectableTurnIds, V as isVisibleDuringSearch, W as selectionAfterTurnRemoval, X as clipDiffForRender, Y as buildUnifiedDiff, _ as turnContextSize, a as TOOL_DISPLAY, at as buildEditOutcomesAnnotation, b as filterModelCatalog, ct as parseEditOutcomesFromResult, d as finalizeStreamingMarkdown, dt as stripEditOutcomesAnnotation, et as filetypeFromPath, f as finalizeStreamingMarkdownForOwner, ft as summarizeOutcomes, g as revealEarlierTranscriptEvents, h as replaceTranscriptTurnEvents, ht as createChatsCompletionProvider, i as turnAsText, j as createFilesCompletionProvider, k as uniqueSkillNamesFromReferences, lt as resolveApprovalForPayload, n as deleteTurnSafely, o as displayNameFor, p as hiddenEarlierTranscriptCount, r as truncateTurnsAt, rt as summarizeEditPayload, s as formatToolCall, st as mergeApprovalAndBodyOutcomes, tt as previewEditPayload, u as capTranscriptEvents, ut as rewriteMultiEditHeader, v as splitPromptSegments, w as normalizeMarkdownCodeFences, y as buildModelCatalog, z as isUserBashEvent } from "./turn-operations-4tnvKwfJ.js";
22
+ import { B as isTurnHighlighted, C as chunkMarkdownForRender, E as stripHtmlCommentsOutsideCode, F as blendHsl, G as selectableTurnIds, H as isVisible, I as buildLinearRamp, J as turnSelectionOwnership, K as selectionAfterTurnRemoval, L as BASH_CALL_ID_PREFIX, O as createSkillsCompletionProvider, P as createCommandsCompletionProvider, Q as clipDiffForRender, R as EDIT_TOOL_NAMES, T as splitMarkdownCodeBlocks, U as isVisibleDuringSearch, V as isUserBashEvent, W as promptHistoryFromEvents, X as buildContextualDiff, Z as buildUnifiedDiff, _ as turnContextSize, _t as createChatsCompletionProvider, a as TOOL_DISPLAY, at as summarizeEditPayload, b as filterModelCatalog, d as finalizeStreamingMarkdown, dt as resolveApprovalForPayload, f as finalizeStreamingMarkdownForOwner, ft as rewriteMultiEditHeader, g as revealEarlierTranscriptEvents, h as replaceTranscriptTurnEvents, i as turnAsText, j as createFilesCompletionProvider, k as uniqueSkillNamesFromReferences, lt as mergeApprovalAndBodyOutcomes, mt as summarizeOutcomes, n as deleteTurnSafely, nt as filetypeFromPath, o as displayNameFor, p as hiddenEarlierTranscriptCount, pt as stripEditOutcomesAnnotation, q as streamEventSearchText, r as truncateTurnsAt, rt as previewEditPayload, s as formatToolCall, st as buildEditOutcomesAnnotation, tt as extractEditPayload, u as capTranscriptEvents, ut as parseEditOutcomesFromResult, v as splitPromptSegments, w as normalizeMarkdownCodeFences, y as buildModelCatalog, z as isEditErrorResult } from "./turn-operations-CtEpeB_s.js";
23
23
  import { a as searchHighlightRanges, c as searchSessionHistoryIndexAsync, i as highlightSegments, n as buildSessionSearchIndexAsync, r as filterSessionSearchCatalog, u as searchSessionIndexAsync } from "./history-search-C4bk1Wz_.js";
24
24
  import { basename, dirname, join, relative, resolve } from "node:path";
25
25
  import { Buffer } from "node:buffer";
26
26
  import * as fs from "node:fs";
27
- import { closeSync, cpSync, createWriteStream, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
27
+ import { createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
28
28
  import { readFile, unlink } from "node:fs/promises";
29
29
  import { spawn, spawnSync } from "node:child_process";
30
30
  import { homedir, tmpdir } from "node:os";
@@ -41,780 +41,6 @@ import * as OpenTuiReactJsxDevRuntime from "@opentui/react/jsx-dev-runtime";
41
41
  import * as OpenTuiReactJsxRuntime from "@opentui/react/jsx-runtime";
42
42
  import { Fragment, jsx as jsx$1, jsxs } from "@opentui/react/jsx-runtime";
43
43
  import * as ReactJsxDevRuntime from "react/jsx-dev-runtime";
44
- //#region src/chat/mcps-write.ts
45
- /**
46
- * Resolve the CANONICAL write target for a scope — always the
47
- * `.{prefix}/mcps.json` flavor, never `.agents` (we don't own that
48
- * convention) and never `mcp.json` (Cursor's spelling is read-compat
49
- * only).
50
- */
51
- function mcpsWritePath(scope, opts) {
52
- const prefix = (opts?.prefix ?? ".zidane").replace(/^\./, "");
53
- const target = projectUserPaths({
54
- subPath: "mcps.json",
55
- ...opts?.cwd ? { cwd: opts.cwd } : {},
56
- ...opts?.home ? { home: opts.home } : {},
57
- ...opts?.prefix ? { prefix: opts.prefix } : {}
58
- }).find((c) => c.source === scope && c.path.includes(`.${prefix}`));
59
- if (!target) throw new Error(`No ${scope} mcps.json path resolved.`);
60
- return target.path;
61
- }
62
- /**
63
- * Upsert `entry` (matched by `name`) into the mcps config at `filePath`.
64
- * Preserves the existing file's shape and every field the writer doesn't
65
- * own; creates a canonical flat-array file when none exists. Throws on
66
- * malformed JSON — silently rewriting a broken file the user is mid-edit
67
- * on would destroy their work.
68
- */
69
- function upsertMcpServer(filePath, entry) {
70
- if (!existsSync(filePath)) {
71
- mkdirSync(dirname(filePath), { recursive: true });
72
- writeFileSync(filePath, `${JSON.stringify([entry], null, 2)}\n`);
73
- return {
74
- path: filePath,
75
- action: "added"
76
- };
77
- }
78
- const raw = parseJsonTolerant(readFileSync(filePath, "utf8"));
79
- if (Array.isArray(raw)) {
80
- const idx = raw.findIndex((e) => isRecord(e) && e.name === entry.name);
81
- const next = [...raw];
82
- const action = idx >= 0 ? "updated" : "added";
83
- if (idx >= 0) next[idx] = entry;
84
- else next.push(entry);
85
- writeFileSync(filePath, `${JSON.stringify(next, null, 2)}\n`);
86
- return {
87
- path: filePath,
88
- action
89
- };
90
- }
91
- if (isRecord(raw)) {
92
- if (isRecord(raw.mcpServers)) {
93
- const map = raw.mcpServers;
94
- const action = entry.name in map ? "updated" : "added";
95
- const next = {
96
- ...raw,
97
- mcpServers: {
98
- ...map,
99
- [entry.name]: stripName(entry)
100
- }
101
- };
102
- writeFileSync(filePath, `${JSON.stringify(next, null, 2)}\n`);
103
- return {
104
- path: filePath,
105
- action
106
- };
107
- }
108
- const action = entry.name in raw ? "updated" : "added";
109
- const next = {
110
- ...raw,
111
- [entry.name]: stripName(entry)
112
- };
113
- writeFileSync(filePath, `${JSON.stringify(next, null, 2)}\n`);
114
- return {
115
- path: filePath,
116
- action
117
- };
118
- }
119
- throw new Error(`${filePath} is not a recognized mcps.json shape (array, map, or {"mcpServers": …}).`);
120
- }
121
- /**
122
- * Remove the entry named `name` from the mcps config at `filePath`,
123
- * preserving the file's shape and everything else in it — the removal
124
- * counterpart of {@link upsertMcpServer}. Throws when the file is
125
- * missing, malformed, an unrecognized shape, or has no entry of that
126
- * name (the caller shows the message on the settings op line; silently
127
- * "succeeding" on a no-op would lie about what happened).
128
- */
129
- function removeMcpServer(filePath, name) {
130
- if (!existsSync(filePath)) throw new Error(`${filePath} does not exist.`);
131
- const raw = parseJsonTolerant(readFileSync(filePath, "utf8"));
132
- if (Array.isArray(raw)) {
133
- const next = raw.filter((e) => !(isRecord(e) && e.name === name));
134
- if (next.length === raw.length) throw new Error(`no entry named "${name}" in ${filePath}.`);
135
- writeFileSync(filePath, `${JSON.stringify(next, null, 2)}\n`);
136
- return { path: filePath };
137
- }
138
- if (isRecord(raw)) {
139
- if (isRecord(raw.mcpServers)) {
140
- if (!(name in raw.mcpServers)) throw new Error(`no entry named "${name}" in ${filePath}.`);
141
- const { [name]: _dropped, ...rest } = raw.mcpServers;
142
- const next = {
143
- ...raw,
144
- mcpServers: rest
145
- };
146
- writeFileSync(filePath, `${JSON.stringify(next, null, 2)}\n`);
147
- return { path: filePath };
148
- }
149
- if (!(name in raw)) throw new Error(`no entry named "${name}" in ${filePath}.`);
150
- const { [name]: _dropped, ...next } = raw;
151
- writeFileSync(filePath, `${JSON.stringify(next, null, 2)}\n`);
152
- return { path: filePath };
153
- }
154
- throw new Error(`${filePath} is not a recognized mcps.json shape (array, map, or {"mcpServers": …}).`);
155
- }
156
- /** Map-shaped files key entries by name — the inner object must not repeat it. */
157
- function stripName(entry) {
158
- const { name: _name, ...rest } = entry;
159
- return rest;
160
- }
161
- function isRecord(value) {
162
- return typeof value === "object" && value !== null && !Array.isArray(value);
163
- }
164
- //#endregion
165
- //#region src/chat/claude-import.ts
166
- function roots(opts) {
167
- const cwd = opts.cwd ?? process.cwd();
168
- return {
169
- projectRoot: findGitRoot(cwd) ?? cwd,
170
- home: opts.home ?? homedir()
171
- };
172
- }
173
- /** `~/.claude` — presence gates the settings action row. */
174
- function hasClaudeCodeSetup(opts = {}) {
175
- return existsSync(resolve(roots(opts).home, ".claude"));
176
- }
177
- /**
178
- * Claude Code names each `~/.claude/projects/` subdirectory after the
179
- * project path with every non-alphanumeric character flattened to `-`
180
- * (`/Users/me/.claude/wt` → `-Users-me--claude-wt`). Lossy, so matching
181
- * goes slug → slug rather than trying to reverse it.
182
- */
183
- function claudeProjectSlug(path) {
184
- return path.replace(/[^a-z0-9]/gi, "-");
185
- }
186
- /**
187
- * Session transcripts for the current project, or every project when
188
- * `allProjects` is set.
189
- *
190
- * Claude Code slugs by LAUNCH directory, so a session started in a
191
- * monorepo subdir (`repo/packages/foo`) lands in a different dir than
192
- * one started at the repo root. Scoped listing therefore accepts both
193
- * the exact slug of the resolved git root (realpath variants included)
194
- * AND any dir whose slug extends it — but because slugs are lossy
195
- * (`repo/foo` and a sibling `repo-foo` collide), prefix-matched dirs
196
- * are confirmed per-file against the transcript's recorded `cwd`
197
- * (resolved to its git root), mirroring how the converted session's
198
- * `projectRoot` is derived.
199
- */
200
- function listClaudeSessionFiles(opts = {}) {
201
- const out = [];
202
- walkClaudeSessionFiles(opts, opts.allProjects ? (file) => out.push(file) : (file, scoped) => {
203
- if (scoped()) out.push(file);
204
- });
205
- return out;
206
- }
207
- /**
208
- * Detection-preview counts — scoped and all-projects figures from ONE
209
- * directory walk (the naive two-listing approach reads every project dir
210
- * twice). The scoped classification (with its per-file cwd peek for
211
- * candidate dirs) runs at most once per file via the lazy `scoped()`
212
- * thunk.
213
- */
214
- function countClaudeSessionFiles(opts = {}) {
215
- let project = 0;
216
- let all = 0;
217
- walkClaudeSessionFiles({
218
- ...opts,
219
- allProjects: false
220
- }, (_file, scoped) => {
221
- all++;
222
- if (scoped()) project++;
223
- }, true);
224
- return {
225
- project,
226
- all
227
- };
228
- }
229
- /**
230
- * Shared walk under `~/.claude/projects`. `visit` receives every
231
- * transcript of every CANDIDATE dir (scoped mode) or every dir
232
- * (`allProjects` / `visitAllDirs`), plus a lazy `scoped()` thunk that
233
- * answers "does this file belong to the current project?" — lazy because
234
- * the answer can cost a `transcriptProjectRoot` peek (up to the whole
235
- * file), which pure all-projects listings never need.
236
- */
237
- function walkClaudeSessionFiles(opts, visit, visitAllDirs = false) {
238
- const { projectRoot, home } = roots(opts);
239
- const projectsDir = resolve(home, ".claude/projects");
240
- if (!existsSync(projectsDir)) return;
241
- const slugs = [claudeProjectSlug(projectRoot), claudeProjectSlug(realpathOr(projectRoot))];
242
- const exact = new Set(slugs);
243
- const prefixes = [...new Set(slugs.map((s) => `${s}-`))];
244
- const wantedRoots = /* @__PURE__ */ new Set([projectRoot, realpathOr(projectRoot)]);
245
- for (const entry of readdirSync(projectsDir, { withFileTypes: true })) {
246
- if (!entry.isDirectory()) continue;
247
- const exactMatch = exact.has(entry.name);
248
- const candidate = exactMatch || prefixes.some((p) => entry.name.startsWith(p));
249
- if (!opts.allProjects && !visitAllDirs && !candidate) continue;
250
- const projectDir = join(projectsDir, entry.name);
251
- for (const file of readdirSync(projectDir)) {
252
- if (!file.endsWith(".jsonl")) continue;
253
- const path = join(projectDir, file);
254
- const scoped = () => {
255
- if (!candidate) return false;
256
- const root = transcriptProjectRoot(path);
257
- return root === null ? exactMatch : wantedRoots.has(root) || wantedRoots.has(realpathOr(root));
258
- };
259
- visit({
260
- path,
261
- projectDir
262
- }, scoped);
263
- }
264
- }
265
- }
266
- /**
267
- * Peek a transcript's project root: scan the first chunk for the first
268
- * record carrying a `cwd` and resolve it the same way
269
- * `convertClaudeSession` does. When the chunk was truncated (giant first
270
- * records — e.g. pasted base64 images) and held no cwd, fall back to the
271
- * full file: the import reads it in full anyway, so correctness beats the
272
- * shortcut. Null when no cwd exists at all (empty/torn file) or the file
273
- * is unreadable.
274
- */
275
- function transcriptProjectRoot(path) {
276
- let text;
277
- let truncated = false;
278
- try {
279
- const fd = openSync(path, "r");
280
- try {
281
- const buf = Buffer.alloc(256 * 1024);
282
- const n = readSync(fd, buf, 0, buf.length, 0);
283
- text = buf.subarray(0, n).toString("utf8");
284
- truncated = n === buf.length;
285
- } finally {
286
- closeSync(fd);
287
- }
288
- } catch {
289
- return null;
290
- }
291
- const fromChunk = scanForCwd(text);
292
- if (fromChunk !== null || !truncated) return fromChunk;
293
- try {
294
- return scanForCwd(readFileSync(path, "utf8"));
295
- } catch {
296
- return null;
297
- }
298
- }
299
- function scanForCwd(text) {
300
- for (const line of text.split("\n")) {
301
- if (!line.trim()) continue;
302
- try {
303
- const record = JSON.parse(line);
304
- if (typeof record.cwd === "string") return findGitRoot(record.cwd) ?? record.cwd;
305
- } catch {
306
- continue;
307
- }
308
- }
309
- return null;
310
- }
311
- function realpathOr(path) {
312
- try {
313
- return realpathSync(path);
314
- } catch {
315
- return path;
316
- }
317
- }
318
- /**
319
- * Convert one Claude Code session transcript (JSONL text) to a zidane
320
- * `SessionData`. Returns null when no conversational turns survive the
321
- * filter (hook noise, sidechains, queue bookkeeping).
322
- *
323
- * Kept-vs-dropped:
324
- * - `user` / `assistant` records with a `message` → turns. Consecutive
325
- * assistant records sharing a `message.id` merge into ONE turn (Claude
326
- * writes text and tool_use as separate records of the same API message).
327
- * - Consecutive tool_result-only user records merge into ONE user turn:
328
- * Claude writes one record per result of a parallel tool batch, but
329
- * zidane persists a batch as a single user turn and
330
- * `ensureToolResultPairing` enforces strict adjacency — split turns
331
- * would be "repaired" into orphan markers + error placeholders on resume.
332
- * - `custom-title` / `summary` records → `metadata.title`.
333
- * - Sidechains (subagent transcripts), `isMeta` records, `attachment`,
334
- * `system`, `queue-operation`, `last-prompt` → dropped.
335
- * - Thinking blocks → dropped (provider-bound signatures, not replayable).
336
- */
337
- function convertClaudeSession(jsonl, opts = {}) {
338
- const turns = [];
339
- let title;
340
- let claudeSessionId;
341
- let cwd;
342
- let lastAssistantMessageId;
343
- for (const line of jsonl.split("\n")) {
344
- if (!line.trim()) continue;
345
- let record;
346
- try {
347
- record = JSON.parse(line);
348
- } catch {
349
- continue;
350
- }
351
- if (record.type === "custom-title" && typeof record.customTitle === "string") {
352
- title = record.customTitle;
353
- continue;
354
- }
355
- if (record.type === "summary" && typeof record.summary === "string") {
356
- title ??= record.summary;
357
- continue;
358
- }
359
- if (record.type !== "user" && record.type !== "assistant") continue;
360
- if (record.isSidechain || record.isMeta || !record.message) continue;
361
- claudeSessionId ??= record.sessionId;
362
- cwd ??= record.cwd;
363
- const parsedTs = record.timestamp ? Date.parse(record.timestamp) : NaN;
364
- const createdAt = Number.isNaN(parsedTs) ? Date.now() : parsedTs;
365
- if (record.type === "assistant") {
366
- const content = claudeBlocksToContent(record.message.content);
367
- if (content.length === 0) continue;
368
- const messageId = record.message.id;
369
- const previous = turns[turns.length - 1];
370
- if (messageId && messageId === lastAssistantMessageId && previous?.role === "assistant") {
371
- previous.content.push(...content);
372
- const usage = claudeUsage(record.message.usage, record.message.model);
373
- if (usage) previous.usage = usage;
374
- continue;
375
- }
376
- lastAssistantMessageId = messageId;
377
- const usage = claudeUsage(record.message.usage, record.message.model);
378
- turns.push({
379
- id: record.uuid ?? crypto.randomUUID(),
380
- role: "assistant",
381
- content,
382
- createdAt,
383
- ...usage ? { usage } : {}
384
- });
385
- continue;
386
- }
387
- lastAssistantMessageId = void 0;
388
- const content = claudeBlocksToContent(record.message.content);
389
- if (content.length === 0) continue;
390
- const previous = turns[turns.length - 1];
391
- if (previous?.role === "user" && isToolResultOnly(content) && isToolResultOnly(previous.content)) {
392
- previous.content.push(...content);
393
- continue;
394
- }
395
- turns.push({
396
- id: record.uuid ?? crypto.randomUUID(),
397
- role: "user",
398
- content,
399
- createdAt
400
- });
401
- }
402
- if (turns.length === 0) return null;
403
- const id = opts.fallbackId ?? claudeSessionId ?? crypto.randomUUID();
404
- const projectRoot = cwd ? findGitRoot(cwd) ?? cwd : void 0;
405
- return {
406
- session: {
407
- id,
408
- ...projectRoot ? { projectRoot } : {},
409
- turns,
410
- runs: [],
411
- status: "idle",
412
- metadata: {
413
- ...title ? { title } : {},
414
- importedFrom: "claude-code",
415
- claudeSessionId: id,
416
- ...claudeSessionId && claudeSessionId !== id ? { claudeLineageId: claudeSessionId } : {}
417
- },
418
- createdAt: turns[0].createdAt,
419
- updatedAt: turns[turns.length - 1].createdAt
420
- },
421
- claudeSessionId: id
422
- };
423
- }
424
- /** Map Claude/Anthropic content blocks to zidane `SessionContentBlock`s. */
425
- function claudeBlocksToContent(content) {
426
- if (typeof content === "string") return content.trim() ? [{
427
- type: "text",
428
- text: content
429
- }] : [];
430
- if (!Array.isArray(content)) return [];
431
- const out = [];
432
- for (const raw of content) {
433
- if (!raw || typeof raw !== "object") continue;
434
- const block = raw;
435
- switch (block.type) {
436
- case "text":
437
- if (typeof block.text === "string" && block.text.trim()) out.push({
438
- type: "text",
439
- text: block.text
440
- });
441
- break;
442
- case "tool_use":
443
- if (typeof block.id === "string" && typeof block.name === "string") {
444
- const input = block.input && typeof block.input === "object" && !Array.isArray(block.input) ? block.input : {};
445
- out.push({
446
- type: "tool_call",
447
- id: block.id,
448
- name: block.name,
449
- input
450
- });
451
- }
452
- break;
453
- case "tool_result":
454
- if (typeof block.tool_use_id === "string") out.push({
455
- type: "tool_result",
456
- callId: block.tool_use_id,
457
- output: claudeToolResultOutput(block.content),
458
- ...block.is_error === true ? { isError: true } : {}
459
- });
460
- break;
461
- case "image": {
462
- const image = claudeImage(block);
463
- if (image) out.push(image);
464
- break;
465
- }
466
- }
467
- }
468
- return out;
469
- }
470
- function claudeImage(block) {
471
- const source = block.source;
472
- if (!source || source.type !== "base64" || typeof source.media_type !== "string" || typeof source.data !== "string") return null;
473
- return {
474
- type: "image",
475
- mediaType: source.media_type,
476
- data: source.data
477
- };
478
- }
479
- function claudeToolResultOutput(content) {
480
- if (typeof content === "string") return content;
481
- if (!Array.isArray(content)) return "";
482
- const parts = [];
483
- for (const raw of content) {
484
- if (!raw || typeof raw !== "object") continue;
485
- const block = raw;
486
- if (block.type === "text" && typeof block.text === "string") parts.push({
487
- type: "text",
488
- text: block.text
489
- });
490
- else if (block.type === "image") {
491
- const image = claudeImage(block);
492
- if (image?.type === "image" && image.data) parts.push({
493
- type: "image",
494
- mediaType: image.mediaType,
495
- data: image.data
496
- });
497
- }
498
- }
499
- if (parts.every((p) => p.type === "text")) return parts.map((p) => p.text).join("\n");
500
- return parts;
501
- }
502
- function claudeUsage(usage, model) {
503
- if (!usage || typeof usage !== "object") return void 0;
504
- return {
505
- input: usage.input_tokens ?? 0,
506
- output: usage.output_tokens ?? 0,
507
- ...usage.cache_creation_input_tokens ? { cacheCreation: usage.cache_creation_input_tokens } : {},
508
- ...usage.cache_read_input_tokens ? { cacheRead: usage.cache_read_input_tokens } : {},
509
- ...model ? { modelId: model } : {}
510
- };
511
- }
512
- function isToolResultOnly(blocks) {
513
- return blocks.length > 0 && blocks.every((b) => b.type === "tool_result");
514
- }
515
- /** Import Claude Code session transcripts into `store`. */
516
- async function importClaudeSessions(store, opts = {}) {
517
- const result = {
518
- imported: 0,
519
- skipped: 0,
520
- empty: 0,
521
- failed: []
522
- };
523
- for (const file of listClaudeSessionFiles(opts)) try {
524
- const fallbackId = basename(file.path, ".jsonl");
525
- if (await store.load(fallbackId)) {
526
- result.skipped++;
527
- continue;
528
- }
529
- const converted = convertClaudeSession(readFileSync(file.path, "utf8"), { fallbackId });
530
- if (!converted) {
531
- result.empty++;
532
- continue;
533
- }
534
- await store.save(converted.session);
535
- result.imported++;
536
- } catch (err) {
537
- result.failed.push({
538
- path: file.path,
539
- error: err instanceof Error ? err.message : String(err)
540
- });
541
- }
542
- return result;
543
- }
544
- /**
545
- * Copy Claude Code's MCP servers into zidane-owned `mcps.json` files.
546
- * The repo-shared `.mcp.json` writes to `<root>/.{prefix}/mcps.json`;
547
- * BOTH `~/.claude.json` scopes write to `~/.{prefix}/mcps.json` —
548
- * Claude's "local" scope (the per-project map inside the user state
549
- * file) is private-by-design and routinely carries tokens in
550
- * `env`/`headers`, so materializing it into a committable project file
551
- * would leak credentials. User scope loses the per-project activation
552
- * (zidane has no user-private per-project config), which is the safer
553
- * direction to err. Collisions with names in ANY native config path are
554
- * skipped.
555
- */
556
- function importClaudeMcps(opts = {}) {
557
- const { projectRoot, home } = roots(opts);
558
- const result = {
559
- copied: [],
560
- skipped: [],
561
- failed: [],
562
- duplicates: []
563
- };
564
- const nativeNames = /* @__PURE__ */ new Set();
565
- for (const candidate of defaultMcpsConfigPaths({
566
- ...opts.cwd ? { cwd: opts.cwd } : {},
567
- home,
568
- ...opts.prefix ? { prefix: opts.prefix } : {}
569
- })) {
570
- if (!existsSync(candidate.path)) continue;
571
- for (const name of rawMcpNames(readFileSync(candidate.path, "utf8"))) nativeNames.add(name);
572
- }
573
- const sources = [];
574
- const mcpJson = resolve(projectRoot, ".mcp.json");
575
- if (existsSync(mcpJson)) try {
576
- sources.push({
577
- servers: parseMcpsFile(readFileSync(mcpJson, "utf8")),
578
- scope: "project"
579
- });
580
- } catch (err) {
581
- result.failed.push({
582
- name: mcpJson,
583
- error: err instanceof Error ? err.message : String(err)
584
- });
585
- }
586
- const claudeState = resolve(home, ".claude.json");
587
- if (existsSync(claudeState)) try {
588
- const state = JSON.parse(readFileSync(claudeState, "utf8"));
589
- sources.push({
590
- servers: parseClaudeState(state, "claude-project-state", projectRoot),
591
- scope: "user"
592
- });
593
- sources.push({
594
- servers: parseClaudeState(state, "claude-user-state"),
595
- scope: "user"
596
- });
597
- } catch (err) {
598
- result.failed.push({
599
- name: claudeState,
600
- error: err instanceof Error ? err.message : String(err)
601
- });
602
- }
603
- const seen = /* @__PURE__ */ new Set();
604
- for (const { servers, scope } of sources) for (const server of servers) {
605
- if (seen.has(server.name)) {
606
- result.duplicates.push(server.name);
607
- continue;
608
- }
609
- seen.add(server.name);
610
- if (nativeNames.has(server.name)) {
611
- result.skipped.push(server.name);
612
- continue;
613
- }
614
- try {
615
- const target = mcpsWritePath(scope, {
616
- ...opts.cwd ? { cwd: opts.cwd } : {},
617
- home,
618
- ...opts.prefix ? { prefix: opts.prefix } : {}
619
- });
620
- const { action } = upsertMcpServer(target, server);
621
- if (action === "updated") {
622
- result.failed.push({
623
- name: server.name,
624
- error: `overwrote an existing entry in ${target} — restore it from version control if unintended`
625
- });
626
- continue;
627
- }
628
- result.copied.push(server.name);
629
- } catch (err) {
630
- result.failed.push({
631
- name: server.name,
632
- error: err instanceof Error ? err.message : String(err)
633
- });
634
- }
635
- }
636
- return result;
637
- }
638
- /**
639
- * Names physically present in an mcps config file, from the raw parsed
640
- * shape — mirrors `upsertMcpServer`'s walk (flat array `name`s, wrapper
641
- * `mcpServers` keys, bare-map keys) with none of discovery's filtering.
642
- * Unreadable/malformed files yield no names; `upsertMcpServer` throws on
643
- * those before writing, so they still can't be overwritten.
644
- */
645
- function rawMcpNames(text) {
646
- let raw;
647
- try {
648
- raw = parseJsonTolerant(text);
649
- } catch {
650
- return [];
651
- }
652
- if (Array.isArray(raw)) return raw.flatMap((entry) => entry && typeof entry === "object" && typeof entry.name === "string" ? [entry.name] : []);
653
- if (raw && typeof raw === "object") {
654
- const record = raw;
655
- const map = record.mcpServers && typeof record.mcpServers === "object" && !Array.isArray(record.mcpServers) ? record.mcpServers : record;
656
- return Object.keys(map);
657
- }
658
- return [];
659
- }
660
- /**
661
- * Copy `.claude/skills/<name>` directories (project + user) into the
662
- * corresponding `.{prefix}/skills`. A skill "exists natively" when a
663
- * directory of the same name sits in any native scan path — those are
664
- * skipped, not overwritten.
665
- */
666
- function importClaudeSkills(opts = {}) {
667
- const { projectRoot, home } = roots(opts);
668
- const prefix = (opts.prefix ?? ".zidane").replace(/^\./, "");
669
- const result = {
670
- copied: [],
671
- skipped: [],
672
- failed: []
673
- };
674
- const nativeNames = /* @__PURE__ */ new Set();
675
- for (const scan of projectUserPaths({
676
- subPath: "skills",
677
- ...opts.cwd ? { cwd: opts.cwd } : {},
678
- home,
679
- ...opts.prefix ? { prefix: opts.prefix } : {}
680
- })) {
681
- if (!existsSync(scan.path)) continue;
682
- for (const entry of readdirSync(scan.path, { withFileTypes: true })) if (entry.isDirectory()) nativeNames.add(entry.name);
683
- }
684
- const pairs = [{
685
- from: resolve(projectRoot, ".claude/skills"),
686
- to: resolve(projectRoot, `.${prefix}/skills`)
687
- }, {
688
- from: resolve(home, ".claude/skills"),
689
- to: resolve(home, `.${prefix}/skills`)
690
- }];
691
- for (const { from, to } of pairs) {
692
- if (!existsSync(from)) continue;
693
- for (const entry of readdirSync(from, { withFileTypes: true })) {
694
- if (!entry.isDirectory()) continue;
695
- if (!existsSync(join(from, entry.name, "SKILL.md"))) continue;
696
- if (nativeNames.has(entry.name)) {
697
- result.skipped.push(entry.name);
698
- continue;
699
- }
700
- try {
701
- cpSync(join(from, entry.name), join(to, entry.name), {
702
- recursive: true,
703
- errorOnExist: true,
704
- force: false
705
- });
706
- nativeNames.add(entry.name);
707
- result.copied.push(entry.name);
708
- } catch (err) {
709
- result.failed.push({
710
- name: entry.name,
711
- error: err instanceof Error ? err.message : String(err)
712
- });
713
- }
714
- }
715
- }
716
- return result;
717
- }
718
- async function importFromClaudeCode(store, opts = {}) {
719
- const summary = { notes: ["CLAUDE.md is already read natively — nothing to import.", "Claude permissions/hooks/plugins have no zidane equivalent — not imported."] };
720
- if (opts.sessions !== false) summary.sessions = await importClaudeSessions(store, opts);
721
- if (opts.mcps !== false) summary.mcps = importClaudeMcps(opts);
722
- if (opts.skills !== false) summary.skills = importClaudeSkills(opts);
723
- return summary;
724
- }
725
- /**
726
- * Per-catalog fzf instance, keyed on catalog reference identity —
727
- * `suggest()` runs per keystroke and `new Fzf(...)` walks the whole
728
- * command list to build its match structures. The getter returns a
729
- * stable array until a registry rebuild swaps it.
730
- */
731
- const fzfByCatalog = /* @__PURE__ */ new WeakMap();
732
- function fzfFor(catalog) {
733
- let fzf = fzfByCatalog.get(catalog);
734
- if (!fzf) {
735
- fzf = new Fzf(catalog, {
736
- selector: (c) => c.name,
737
- casing: "case-insensitive"
738
- });
739
- fzfByCatalog.set(catalog, fzf);
740
- }
741
- return fzf;
742
- }
743
- /** Filter + rank commands against a query (name-priority, then description, then fuzzy on name). */
744
- function scoreCommands(catalog, query) {
745
- const q = query.trim().toLowerCase();
746
- const fuzzyHits = q.length > 0 ? new Set(fzfFor(catalog).find(q).map((r) => r.item)) : null;
747
- const scored = [];
748
- for (const command of catalog) {
749
- const name = command.name.toLowerCase();
750
- const aliases = (command.aliases ?? []).map((a) => a.toLowerCase());
751
- const group = command.group?.toLowerCase();
752
- const desc = command.description.toLowerCase();
753
- if (q.length === 0) scored.push({
754
- command,
755
- rank: 4
756
- });
757
- else if (name.startsWith(q) || aliases.some((a) => a.startsWith(q))) scored.push({
758
- command,
759
- rank: 0
760
- });
761
- else if (name.includes(q) || aliases.some((a) => a.includes(q)) || group !== void 0 && q.length >= 2 && group.startsWith(q)) scored.push({
762
- command,
763
- rank: 1
764
- });
765
- else if (desc.includes(q)) scored.push({
766
- command,
767
- rank: 2
768
- });
769
- else if (fuzzyHits?.has(command)) scored.push({
770
- command,
771
- rank: 3
772
- });
773
- }
774
- scored.sort((a, b) => a.rank !== b.rank ? a.rank - b.rank : a.command.name.localeCompare(b.command.name));
775
- return scored.map(({ command }) => ({
776
- id: command.name,
777
- label: command.name,
778
- description: command.aliases?.length ? `${command.description} (alias: ${command.aliases.map((a) => `/${a}`).join(", ")})` : command.description,
779
- insertText: `/${command.name} `,
780
- data: command
781
- }));
782
- }
783
- /**
784
- * Build a commands completion provider against a *live* command list. The
785
- * getter is re-evaluated per `suggest` so registry toggles / cwd rebuilds
786
- * take effect without re-instantiating the provider.
787
- */
788
- function createCommandsCompletionProvider(opts) {
789
- return {
790
- id: "commands",
791
- trigger: "/",
792
- label: "Commands",
793
- allowSpacesInQuery: true,
794
- suggest(query, ctx) {
795
- const spaceIdx = query.search(/\s/);
796
- if (spaceIdx !== -1) {
797
- if (ctx.text.trimStart()[0] !== "/") return [];
798
- const typed = query.slice(0, spaceIdx).toLowerCase();
799
- const command = opts.getCommands().find((c) => c.name.toLowerCase() === typed || c.aliases?.some((a) => a.toLowerCase() === typed));
800
- if (!command?.completeArgs) return [];
801
- const argsPrefix = query.slice(spaceIdx + 1).replace(/^\s+/, "");
802
- return Promise.resolve(command.completeArgs(argsPrefix)).then((suggestions) => (suggestions ?? []).map((s) => ({
803
- id: `${command.name}:${s.value}`,
804
- label: s.label ?? s.value,
805
- ...s.description !== void 0 ? { description: s.description } : {},
806
- insertText: `/${command.name} ${s.value} `,
807
- data: command
808
- })));
809
- }
810
- return scoreCommands(opts.getCommands(), query);
811
- },
812
- parseReferences() {
813
- return [];
814
- }
815
- };
816
- }
817
- //#endregion
818
44
  //#region src/mcp/warn-log.ts
819
45
  /**
820
46
  * Subscribe to `mcp:warn`
@@ -3108,6 +2334,69 @@ function sameElements(a, b) {
3108
2334
  if (!a || !b || a.length !== b.length) return false;
3109
2335
  return a.every((value, i) => value === b[i]);
3110
2336
  }
2337
+ /**
2338
+ * Single collapsible row for ALL parent-owned tool calls of one exchange
2339
+ * (`Settings.groupToolCalls`). Collapsed, it shows the summary line
2340
+ * (`↳ ▸ 12 tool calls · 3 files edited · 2 commands`) with an `[expand]`
2341
+ * affordance; expanded, the individual tool rows render exactly as the
2342
+ * ungrouped transcript would, under a `[collapse]` header. Clicking the
2343
+ * header toggles — same mouse affordance as the fenced-code `[copy]`
2344
+ * button (keyboard users still reach the calls via the turn-details
2345
+ * modal, which never groups).
2346
+ *
2347
+ * Memoized with {@link SubagentBlock}'s element-wise comparator —
2348
+ * `partitionTranscript` rebuilds the `events` array per drain tick, but
2349
+ * the inner event references stay stable until their content changes.
2350
+ */
2351
+ const ToolGroupBlock = memo(({ groupKey, events, previous, expanded, onToggle, selectedTurnId = null, ownership, anchorIds }) => {
2352
+ const COLOR = useColors();
2353
+ const SURFACE = useSurfaces();
2354
+ const [hovered, setHovered] = useState(false);
2355
+ const summary = summarizeToolGroup(events);
2356
+ const gap = marginTopFor(events[0], previous);
2357
+ const isRowSelected = (evt) => selectedTurnId !== null && ownership !== void 0 && isTurnHighlighted(evt, selectedTurnId, ownership);
2358
+ const highlighted = events.some((evt) => isRowSelected(evt));
2359
+ const firstAnchorId = anchorIds?.find((id) => id !== void 0);
2360
+ return /* @__PURE__ */ jsxs("box", {
2361
+ id: expanded ? void 0 : firstAnchorId,
2362
+ style: {
2363
+ paddingTop: gap,
2364
+ backgroundColor: !expanded && highlighted ? SURFACE.selection : SURFACE.background,
2365
+ alignSelf: "stretch",
2366
+ flexShrink: 0,
2367
+ flexDirection: "column"
2368
+ },
2369
+ children: [/* @__PURE__ */ jsxs("text", {
2370
+ selectable: false,
2371
+ onMouseDown: (event) => {
2372
+ event.stopPropagation();
2373
+ event.preventDefault();
2374
+ onToggle(groupKey);
2375
+ },
2376
+ onMouseOver: () => setHovered(true),
2377
+ onMouseOut: () => setHovered(false),
2378
+ children: [
2379
+ /* @__PURE__ */ jsx$1("span", {
2380
+ fg: COLOR.mute,
2381
+ children: expanded ? "↳ ▾ " : "↳ ▸ "
2382
+ }),
2383
+ /* @__PURE__ */ jsx$1("span", {
2384
+ fg: hovered ? COLOR.accent : COLOR.mute,
2385
+ children: summary
2386
+ }),
2387
+ /* @__PURE__ */ jsx$1("span", {
2388
+ fg: hovered ? COLOR.accent : COLOR.mute,
2389
+ children: expanded ? " [collapse]" : " [expand]"
2390
+ })
2391
+ ]
2392
+ }), expanded && events.map((evt, i) => /* @__PURE__ */ jsx$1(EventLine, {
2393
+ event: evt,
2394
+ previous: i === 0 ? events[0] : events[i - 1],
2395
+ selected: isRowSelected(evt),
2396
+ anchorId: anchorIds?.[i]
2397
+ }, i))]
2398
+ });
2399
+ }, (prev, next) => sameElements(prev.events, next.events) && sameElements(prev.anchorIds, next.anchorIds) && prev.groupKey === next.groupKey && prev.previous === next.previous && prev.expanded === next.expanded && prev.onToggle === next.onToggle && prev.selectedTurnId === next.selectedTurnId && prev.ownership === next.ownership);
3111
2400
  const NO_SEARCH_MATCHES = {
3112
2401
  direct: /* @__PURE__ */ new Set(),
3113
2402
  callIds: /* @__PURE__ */ new Set(),
@@ -3169,6 +2458,15 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false, sea
3169
2458
  searchQuery,
3170
2459
  searchMatches
3171
2460
  ]);
2461
+ const [expandedGroups, setExpandedGroups] = useState(() => /* @__PURE__ */ new Set());
2462
+ const toggleGroup = useCallback((key) => {
2463
+ setExpandedGroups((prev) => {
2464
+ const next = new Set(prev);
2465
+ if (next.has(key)) next.delete(key);
2466
+ else next.add(key);
2467
+ return next;
2468
+ });
2469
+ }, []);
3172
2470
  const scrollboxRef = useRef(null);
3173
2471
  const mouseSelectionRef = useRef(false);
3174
2472
  const pressedTurnRef = useRef(null);
@@ -3242,7 +2540,7 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false, sea
3242
2540
  for (let i = items.length - 1; i >= 0 && open.size > 0; i--) {
3243
2541
  const item = items[i];
3244
2542
  const here = [];
3245
- for (const [id, child] of open) if (item.kind === "child-run" ? item.events.some((e) => e.childId === id) : item.event.kind === "spawn-start" && item.event.childId === id) {
2543
+ for (const [id, child] of open) if (item.kind === "child-run" ? item.events.some((e) => e.childId === id) : item.kind === "event" && item.event.kind === "spawn-start" && item.event.childId === id) {
3246
2544
  here.push({
3247
2545
  id,
3248
2546
  activity: spawnActivity(child.events),
@@ -3359,6 +2657,16 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false, sea
3359
2657
  children: line
3360
2658
  }, i);
3361
2659
  }
2660
+ if (item.kind === "tool-group") return /* @__PURE__ */ jsx$1(ToolGroupBlock, {
2661
+ groupKey: item.key,
2662
+ events: item.events,
2663
+ previous: item.previous,
2664
+ expanded: expandedGroups.has(item.key),
2665
+ onToggle: toggleGroup,
2666
+ selectedTurnId,
2667
+ ownership,
2668
+ anchorIds: anchors.ids[i]
2669
+ }, i);
3362
2670
  return /* @__PURE__ */ jsx$1(SubagentBlock, {
3363
2671
  events: item.events,
3364
2672
  previous: item.previous,
@@ -3464,14 +2772,85 @@ function collapseCompletedSpawnStarts(events) {
3464
2772
  }
3465
2773
  return events.filter((_, i) => keep[i]);
3466
2774
  }
2775
+ /** Row kinds that end an exchange-wide tool group (`Settings.groupToolCalls`). */
2776
+ const TOOL_GROUP_BOUNDARIES = /* @__PURE__ */ new Set([
2777
+ "user-prompt",
2778
+ "separator",
2779
+ "compact-summary"
2780
+ ]);
2781
+ /**
2782
+ * Is this a parent-owned tool row eligible for exchange-wide grouping?
2783
+ * Subagent-owned rows (childId / depth) stay inside their subagent
2784
+ * cluster, and user-initiated `!` bash rows stay standalone — the user
2785
+ * ran the command to SEE it; folding it into a group would hide it.
2786
+ */
2787
+ function isGroupableToolRow(event) {
2788
+ if ((event.depth ?? 0) > 0 || event.childId) return false;
2789
+ if (event.kind !== "tool" && event.kind !== "tool-result") return false;
2790
+ return !isUserBashEvent(event);
2791
+ }
2792
+ /**
2793
+ * Per-event group keys for `Settings.groupToolCalls`: walk `visible` once,
2794
+ * split it into exchanges at {@link TOOL_GROUP_BOUNDARIES} rows, and — for
2795
+ * every exchange with ≥2 tool CALLS (single calls pass through, mirroring
2796
+ * the GUI's adjacency batching) — tag each groupable tool/tool-result row
2797
+ * with a shared key. The key derives from the first grouped event so it is
2798
+ * stable while a live stream appends more calls to the same exchange
2799
+ * (expand state keys off it).
2800
+ */
2801
+ function computeToolGroupKeys(visible) {
2802
+ const keys = Array.from({ length: visible.length }).fill(null);
2803
+ let exchange = [];
2804
+ let ordinal = 0;
2805
+ const flush = () => {
2806
+ if (exchange.reduce((sum, i) => sum + (visible[i].kind === "tool" ? 1 : 0), 0) >= 2) {
2807
+ const first = visible[exchange[0]];
2808
+ const key = `tool-group-${first.callId ?? first.turnId ?? ordinal}`;
2809
+ for (const i of exchange) keys[i] = key;
2810
+ }
2811
+ exchange = [];
2812
+ ordinal += 1;
2813
+ };
2814
+ for (let i = 0; i < visible.length; i++) {
2815
+ const event = visible[i];
2816
+ if (TOOL_GROUP_BOUNDARIES.has(event.kind)) {
2817
+ flush();
2818
+ continue;
2819
+ }
2820
+ if (isGroupableToolRow(event)) exchange.push(i);
2821
+ }
2822
+ flush();
2823
+ return keys;
2824
+ }
2825
+ /**
2826
+ * Collapsed-group summary line — `12 tool calls · 3 files edited ·
2827
+ * 2 commands`. Files count distinct edit-tool paths; commands count
2828
+ * `shell` calls. Sections with a zero count are dropped (the leading
2829
+ * call count always shows).
2830
+ */
2831
+ function summarizeToolGroup(events) {
2832
+ let calls = 0;
2833
+ let commands = 0;
2834
+ const edited = /* @__PURE__ */ new Set();
2835
+ for (const event of events) {
2836
+ if (event.kind !== "tool") continue;
2837
+ calls += 1;
2838
+ if (event.tool === "shell") commands += 1;
2839
+ if (event.tool && EDIT_TOOL_NAMES.has(event.tool)) {
2840
+ const path = typeof event.input?.path === "string" ? event.input.path : event.edit?.path;
2841
+ edited.add(path ?? `call-${calls}`);
2842
+ }
2843
+ }
2844
+ const parts = [`${calls} tool call${calls === 1 ? "" : "s"}`];
2845
+ if (edited.size > 0) parts.push(`${edited.size} file${edited.size === 1 ? "" : "s"} edited`);
2846
+ if (commands > 0) parts.push(`${commands} command${commands === 1 ? "" : "s"}`);
2847
+ return parts.join(" · ");
2848
+ }
3467
2849
  function partitionTranscript(events, settings, searchReveal) {
3468
2850
  const visible = collapseCompletedSpawnStarts(events).filter((event) => isVisible(event, settings) || searchReveal !== void 0 && (event.depth ?? 0) === 0 && searchReveal(event));
3469
2851
  if (visible.length === 0) return [];
3470
- if (settings.hideSubagentOutput) return visible.map((event, i) => ({
3471
- kind: "event",
3472
- event,
3473
- previous: visible[i - 1]
3474
- }));
2852
+ const groupKeys = settings.groupToolCalls && searchReveal === void 0 ? computeToolGroupKeys(visible) : null;
2853
+ const groups = /* @__PURE__ */ new Map();
3475
2854
  const items = [];
3476
2855
  let run = [];
3477
2856
  let runPrevious;
@@ -3488,7 +2867,24 @@ function partitionTranscript(events, settings, searchReveal) {
3488
2867
  };
3489
2868
  for (let i = 0; i < visible.length; i++) {
3490
2869
  const event = visible[i];
3491
- if (isChild(event)) {
2870
+ const groupKey = groupKeys?.[i] ?? null;
2871
+ if (groupKey !== null) {
2872
+ flush();
2873
+ const group = groups.get(groupKey);
2874
+ if (group) group.events.push(event);
2875
+ else {
2876
+ const created = {
2877
+ kind: "tool-group",
2878
+ key: groupKey,
2879
+ events: [event],
2880
+ previous: visible[i - 1]
2881
+ };
2882
+ groups.set(groupKey, created);
2883
+ items.push(created);
2884
+ }
2885
+ continue;
2886
+ }
2887
+ if (!settings.hideSubagentOutput && isChild(event)) {
3492
2888
  if (run.length === 0) runPrevious = visible[i - 1];
3493
2889
  run.push(event);
3494
2890
  } else {
@@ -15456,6 +14852,17 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15456
14852
  setSkillInstallOpen(true);
15457
14853
  return;
15458
14854
  }
14855
+ if (key.ctrl && key.name === "e" && activeTab === "skills" && actions?.onEditSkill) {
14856
+ key.preventDefault();
14857
+ const skill = focusedSkill;
14858
+ if (!skill || skillOp?.kind === "busy") return;
14859
+ if (skill.baseDir) actions.onEditSkill(skill);
14860
+ else setSkillOp({
14861
+ kind: "notice",
14862
+ text: `"${skill.name}" is ${skill.source ?? "inline"} — no directory to edit.`
14863
+ });
14864
+ return;
14865
+ }
15459
14866
  if (key.ctrl && key.name === "x" && activeTab === "skills" && actions?.onRemoveSkill) {
15460
14867
  key.preventDefault();
15461
14868
  const skill = focusedSkill;
@@ -15794,6 +15201,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15794
15201
  canRemoveExtension: !!actions?.onRemoveExtension,
15795
15202
  canInstallExtension: !!actions?.onInstallExtension,
15796
15203
  canInstallSkill: !!actions?.onInstallSkill,
15204
+ canEditSkill: !!actions?.onEditSkill,
15797
15205
  canAddMcp: !!actions?.onAddMcp,
15798
15206
  canRemoveSkill: !!actions?.onRemoveSkill,
15799
15207
  canRemoveMcp: !!actions?.onRemoveMcp,
@@ -17223,7 +16631,7 @@ function renderMcpErrors(errors, home, warnColor, dimColor) {
17223
16631
  }, err.path))
17224
16632
  });
17225
16633
  }
17226
- function Hints({ activeTab, authorizing = false, focusedMcp, focusedMcpRow, focusedMcpStatus, canRefreshSkills, canRefreshMcps, canEditExtension, canCreateExtension, canUpdateExtension, canRemoveExtension, canInstallExtension, canInstallSkill, canAddMcp, canRemoveSkill, canRemoveMcp, focusedExtensionExpandable }) {
16634
+ function Hints({ activeTab, authorizing = false, focusedMcp, focusedMcpRow, focusedMcpStatus, canRefreshSkills, canRefreshMcps, canEditExtension, canCreateExtension, canUpdateExtension, canRemoveExtension, canInstallExtension, canInstallSkill, canEditSkill, canAddMcp, canRemoveSkill, canRemoveMcp, focusedExtensionExpandable }) {
17227
16635
  const COLOR = useColors();
17228
16636
  const onServerRow = focusedMcpRow?.kind === "server";
17229
16637
  const showLogin = activeTab === "mcps" && onServerRow && !!focusedMcp && !!focusedMcpStatus && canLogin$1(focusedMcp, focusedMcpStatus);
@@ -17277,6 +16685,10 @@ function Hints({ activeTab, authorizing = false, focusedMcp, focusedMcpRow, focu
17277
16685
  key: "ctrl+g",
17278
16686
  label: "install"
17279
16687
  }] : [],
16688
+ ...activeTab === "skills" && canEditSkill ? [{
16689
+ key: "ctrl+e",
16690
+ label: "edit"
16691
+ }] : [],
17280
16692
  ...activeTab === "skills" && canInstallSkill ? [{
17281
16693
  key: "ctrl+g",
17282
16694
  label: "install"
@@ -19634,7 +19046,9 @@ function AppShell() {
19634
19046
  ...skillsConfig,
19635
19047
  ...composedPreset.skills ?? {}
19636
19048
  };
19637
- const skills = !!session.metadata["zidane.extensionEdit"] || extensionDirForCwd(actualCwd) !== null ? withZidaneExtensionSkill(mergedSkills, { cwd: actualCwd }) : mergedSkills;
19049
+ const withExtensionSkill = !!session.metadata["zidane.extensionEdit"] || extensionDirForCwd(actualCwd) !== null ? withZidaneExtensionSkill(mergedSkills, { cwd: actualCwd }) : mergedSkills;
19050
+ const skillEditMarker = readSkillEditMarker(session.metadata[SKILL_EDIT_METADATA_KEY]);
19051
+ const skills = skillEditMarker ? withZidaneSkillEditSkill(withExtensionSkill, skillEditMarker, { cwd: actualCwd }) : withExtensionSkill;
19638
19052
  const mergedMcpServers = dedupMcpServers([
19639
19053
  ...projectMcps,
19640
19054
  ...launch?.mcp ?? [],
@@ -20735,32 +20149,53 @@ function AppShell() {
20735
20149
  await refreshSessions();
20736
20150
  setScreen("sessions");
20737
20151
  }, [refreshSessions]);
20738
- const pendingExtSessionRef = useRef(null);
20739
- const [extEditNonce, setExtEditNonce] = useState(0);
20740
- const onEditExtension = useCallback((dir) => {
20152
+ const pendingEditSessionRef = useRef(null);
20153
+ const [editSessionNonce, setEditSessionNonce] = useState(0);
20154
+ const openEditSession = useCallback((dir, metadata) => {
20741
20155
  modal.close();
20742
- pendingExtSessionRef.current = { dir };
20743
- setExtEditNonce((n) => n + 1);
20156
+ pendingEditSessionRef.current = {
20157
+ dir,
20158
+ metadata
20159
+ };
20160
+ setEditSessionNonce((n) => n + 1);
20744
20161
  try {
20745
20162
  setCwd(dir);
20746
20163
  } catch (err) {
20747
- pendingExtSessionRef.current = null;
20748
- debugLog("edit extension: chdir failed", err);
20164
+ pendingEditSessionRef.current = null;
20165
+ debugLog("edit session: chdir failed", err);
20749
20166
  }
20750
20167
  }, [modal, setCwd]);
20168
+ const onEditExtension = useCallback((dir) => {
20169
+ openEditSession(dir, {
20170
+ ...editSessionMetadata({
20171
+ kind: "extension",
20172
+ name: basename(dir),
20173
+ dir
20174
+ }),
20175
+ [ACTIVE_SKILLS_META_KEY]: [ZIDANE_EXTENSION_SKILL_NAME]
20176
+ });
20177
+ }, [openEditSession]);
20178
+ const onEditSkill = useCallback((skill) => {
20179
+ if (!skill.baseDir) return;
20180
+ openEditSession(skill.baseDir, {
20181
+ ...editSessionMetadata({
20182
+ kind: "skill",
20183
+ name: skill.name,
20184
+ dir: skill.baseDir
20185
+ }),
20186
+ [ACTIVE_SKILLS_META_KEY]: [ZIDANE_SKILL_EDIT_SKILL_NAME]
20187
+ });
20188
+ }, [openEditSession]);
20751
20189
  useEffect(() => {
20752
- const pending = pendingExtSessionRef.current;
20190
+ const pending = pendingEditSessionRef.current;
20753
20191
  if (!pending || cwd !== pending.dir || !picked || busy) return;
20754
- pendingExtSessionRef.current = null;
20755
- activateSession(null, picked.provider.key, { metadata: {
20756
- [EXTENSION_EDIT_METADATA_KEY]: { dir: pending.dir },
20757
- [ACTIVE_SKILLS_META_KEY]: [ZIDANE_EXTENSION_SKILL_NAME]
20758
- } }).catch((err) => debugLog("edit extension: activate session failed", err));
20192
+ pendingEditSessionRef.current = null;
20193
+ activateSession(null, picked.provider.key, { metadata: pending.metadata }).catch((err) => debugLog("edit session: activate session failed", err));
20759
20194
  }, [
20760
20195
  cwd,
20761
20196
  picked,
20762
20197
  busy,
20763
- extEditNonce,
20198
+ editSessionNonce,
20764
20199
  activateSession
20765
20200
  ]);
20766
20201
  const onCreateExtension = useCallback(() => {
@@ -23020,6 +22455,7 @@ function AppShell() {
23020
22455
  onRefreshMcps,
23021
22456
  onRefreshMcpTools,
23022
22457
  onEditExtension,
22458
+ onEditSkill,
23023
22459
  onCreateExtension,
23024
22460
  onUpdateExtension,
23025
22461
  onRemoveExtension,