zidane 6.2.31 → 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.
- package/dist/{acp-C04z4yA7.js → acp-BOXBhzMv.js} +3 -3
- package/dist/{acp-C04z4yA7.js.map → acp-BOXBhzMv.js.map} +1 -1
- package/dist/acp-cli.js +4 -4
- package/dist/acp.d.ts +1 -1
- package/dist/acp.js +1 -1
- package/dist/{agent-CbFirigA.js → agent-CHtfwiSc.js} +75 -6
- package/dist/agent-CHtfwiSc.js.map +1 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +1 -1
- package/dist/{anthropic-fUETpZNm.js → anthropic--S5Wg4Oy.js} +2 -2
- package/dist/{anthropic-fUETpZNm.js.map → anthropic--S5Wg4Oy.js.map} +1 -1
- package/dist/{auth-qXqkD--3.js → auth-up7hgE9H.js} +2 -2
- package/dist/{auth-qXqkD--3.js.map → auth-up7hgE9H.js.map} +1 -1
- package/dist/chat/pure.d.ts +4 -4
- package/dist/chat/pure.js +3 -3
- package/dist/chat.d.ts +112 -5
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +6 -6
- package/dist/contexts/daytona.d.ts +1 -1
- package/dist/contexts/e2b.d.ts +1 -1
- package/dist/eval.d.ts +1 -1
- package/dist/eval.js +1 -1
- package/dist/extensions.d.ts +1 -1
- package/dist/extensions.js +1 -1
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +3 -3
- package/dist/{index-5AFYbHjc.d.ts → index-DZxVqh6L.d.ts} +248 -3
- package/dist/index-DZxVqh6L.d.ts.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8 -8
- package/dist/{login-CEOlp1r6.js → login-CWwu9Sex.js} +114 -5
- package/dist/login-CWwu9Sex.js.map +1 -0
- package/dist/mcp.d.ts +1 -1
- package/dist/{messages-Dy9t7DbQ.js → messages-t5Sv-vyv.js} +161 -2
- package/dist/{messages-Dy9t7DbQ.js.map → messages-t5Sv-vyv.js.map} +1 -1
- package/dist/output/stream-json.d.ts +1 -1
- package/dist/output/terminal.d.ts +1 -1
- package/dist/presets.d.ts +1 -1
- package/dist/presets.js +1 -1
- package/dist/providers/anthropic.d.ts +1 -1
- package/dist/providers/anthropic.js +1 -1
- package/dist/providers/arcee.d.ts +1 -1
- package/dist/providers/baseten.d.ts +1 -1
- package/dist/providers/cerebras.d.ts +1 -1
- package/dist/providers/local.d.ts +1 -1
- package/dist/providers/openai-compat.d.ts +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.js +1 -1
- package/dist/providers/openrouter.d.ts +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +1 -1
- package/dist/read-state-rDmfeLmz.js.map +1 -1
- package/dist/restate.d.ts +1 -1
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/{session-DkVnYGm9.js → session-DtRtNDBz.js} +61 -5
- package/dist/session-DtRtNDBz.js.map +1 -0
- package/dist/session.d.ts +2 -2
- package/dist/session.js +3 -3
- package/dist/skills.d.ts +1 -1
- package/dist/{tool-formatters-Dft7YVHI.d.ts → tool-formatters-BCA9SPQy.d.ts} +14 -3
- package/dist/tool-formatters-BCA9SPQy.d.ts.map +1 -0
- package/dist/tools/fetch-url.d.ts +1 -1
- package/dist/tools/web-search.d.ts +1 -1
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +1 -1
- package/dist/{transcript-anchors-DcsjjcXp.d.ts → transcript-anchors-BaU0gkGA.d.ts} +12 -3
- package/dist/{transcript-anchors-DcsjjcXp.d.ts.map → transcript-anchors-BaU0gkGA.d.ts.map} +1 -1
- package/dist/{transcript-anchors-TsFlSKWp.js → transcript-anchors-vVG3xyD0.js} +3330 -2639
- package/dist/transcript-anchors-vVG3xyD0.js.map +1 -0
- package/dist/tui.d.ts +11 -3
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +241 -805
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-ScYurEvl.d.ts → turn-operations-BofuxNtk.d.ts} +16 -4
- package/dist/turn-operations-BofuxNtk.d.ts.map +1 -0
- package/dist/{turn-operations-4tnvKwfJ.js → turn-operations-CtEpeB_s.js} +99 -3
- package/dist/turn-operations-CtEpeB_s.js.map +1 -0
- package/dist/types-BIarq1qE.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
- package/dist/agent-CbFirigA.js.map +0 -1
- package/dist/index-5AFYbHjc.d.ts.map +0 -1
- package/dist/login-CEOlp1r6.js.map +0 -1
- package/dist/session-DkVnYGm9.js.map +0 -1
- package/dist/tool-formatters-Dft7YVHI.d.ts.map +0 -1
- package/dist/transcript-anchors-TsFlSKWp.js.map +0 -1
- package/dist/turn-operations-4tnvKwfJ.js.map +0 -1
- package/dist/turn-operations-ScYurEvl.d.ts.map +0 -1
package/dist/tui.js
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import { $ as
|
|
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-
|
|
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
|
|
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-
|
|
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 {
|
|
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-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
3471
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
20739
|
-
const [
|
|
20740
|
-
const
|
|
20152
|
+
const pendingEditSessionRef = useRef(null);
|
|
20153
|
+
const [editSessionNonce, setEditSessionNonce] = useState(0);
|
|
20154
|
+
const openEditSession = useCallback((dir, metadata) => {
|
|
20741
20155
|
modal.close();
|
|
20742
|
-
|
|
20743
|
-
|
|
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
|
-
|
|
20748
|
-
debugLog("edit
|
|
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 =
|
|
20190
|
+
const pending = pendingEditSessionRef.current;
|
|
20753
20191
|
if (!pending || cwd !== pending.dir || !picked || busy) return;
|
|
20754
|
-
|
|
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
|
-
|
|
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,
|