zidane 6.2.20 → 6.2.22

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 (72) hide show
  1. package/dist/{acp-BoQdQUwo.js → acp-Cwp_Y78H.js} +2 -2
  2. package/dist/{acp-BoQdQUwo.js.map → acp-Cwp_Y78H.js.map} +1 -1
  3. package/dist/acp-cli.js +2 -2
  4. package/dist/acp.d.ts +1 -1
  5. package/dist/acp.js +1 -1
  6. package/dist/{agent-CBK6ZUXs.js → agent-IPshmLIF.js} +15 -2
  7. package/dist/agent-IPshmLIF.js.map +1 -0
  8. package/dist/agent.d.ts +1 -1
  9. package/dist/agent.js +1 -1
  10. package/dist/chat/pure.d.ts +3 -3
  11. package/dist/chat.d.ts +21 -5
  12. package/dist/chat.d.ts.map +1 -1
  13. package/dist/chat.js +3 -3
  14. package/dist/contexts/daytona.d.ts +1 -1
  15. package/dist/contexts/e2b.d.ts +1 -1
  16. package/dist/eval.d.ts +1 -1
  17. package/dist/extensions.d.ts +1 -1
  18. package/dist/extensions.js +1 -1
  19. package/dist/headless.d.ts +1 -1
  20. package/dist/headless.js +1 -1
  21. package/dist/{index-BRxJUFyo.d.ts → index-BDtxt4FY.d.ts} +26 -1
  22. package/dist/index-BDtxt4FY.d.ts.map +1 -0
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.js +4 -4
  25. package/dist/{login-DreurRAq.js → login-BkZ_d5sT.js} +2 -2
  26. package/dist/{login-DreurRAq.js.map → login-BkZ_d5sT.js.map} +1 -1
  27. package/dist/mcp.d.ts +1 -1
  28. package/dist/output/stream-json.d.ts +1 -1
  29. package/dist/output/terminal.d.ts +1 -1
  30. package/dist/presets.d.ts +1 -1
  31. package/dist/presets.js +1 -1
  32. package/dist/providers/anthropic.d.ts +1 -1
  33. package/dist/providers/arcee.d.ts +1 -1
  34. package/dist/providers/baseten.d.ts +1 -1
  35. package/dist/providers/cerebras.d.ts +1 -1
  36. package/dist/providers/local.d.ts +1 -1
  37. package/dist/providers/openai-compat.d.ts +1 -1
  38. package/dist/providers/openai.d.ts +1 -1
  39. package/dist/providers/openrouter.d.ts +1 -1
  40. package/dist/providers/xai.d.ts +1 -1
  41. package/dist/providers.d.ts +1 -1
  42. package/dist/{resolve-HtxmBWs9.js → resolve-x5mlFtLX.js} +28 -13
  43. package/dist/resolve-x5mlFtLX.js.map +1 -0
  44. package/dist/restate.d.ts +1 -1
  45. package/dist/session/sqlite.d.ts +1 -1
  46. package/dist/session.d.ts +1 -1
  47. package/dist/{skills-DKWqoaYH.js → skills-C1vGKbqv.js} +2 -2
  48. package/dist/{skills-DKWqoaYH.js.map → skills-C1vGKbqv.js.map} +1 -1
  49. package/dist/skills.d.ts +1 -1
  50. package/dist/skills.js +2 -2
  51. package/dist/{tool-formatters-Dokpl_jU.d.ts → tool-formatters-CMrkqO4q.d.ts} +2 -2
  52. package/dist/{tool-formatters-Dokpl_jU.d.ts.map → tool-formatters-CMrkqO4q.d.ts.map} +1 -1
  53. package/dist/tools/fetch-url.d.ts +1 -1
  54. package/dist/tools/web-search.d.ts +1 -1
  55. package/dist/tools.d.ts +1 -1
  56. package/dist/tools.js +1 -1
  57. package/dist/{transcript-anchors-5wRP7hln.d.ts → transcript-anchors-Bvb8iMiY.d.ts} +8 -2
  58. package/dist/{transcript-anchors-5wRP7hln.d.ts.map → transcript-anchors-Bvb8iMiY.d.ts.map} +1 -1
  59. package/dist/{transcript-anchors-Bm3bYmOD.js → transcript-anchors-DvLIRYLz.js} +215 -21
  60. package/dist/transcript-anchors-DvLIRYLz.js.map +1 -0
  61. package/dist/tui.d.ts +11 -3
  62. package/dist/tui.d.ts.map +1 -1
  63. package/dist/tui.js +876 -95
  64. package/dist/tui.js.map +1 -1
  65. package/dist/{turn-operations-D96guhSq.d.ts → turn-operations-BIw6n_H2.d.ts} +3 -3
  66. package/dist/{turn-operations-D96guhSq.d.ts.map → turn-operations-BIw6n_H2.d.ts.map} +1 -1
  67. package/dist/types.d.ts +1 -1
  68. package/package.json +1 -1
  69. package/dist/agent-CBK6ZUXs.js.map +0 -1
  70. package/dist/index-BRxJUFyo.d.ts.map +0 -1
  71. package/dist/resolve-HtxmBWs9.js.map +0 -1
  72. package/dist/transcript-anchors-Bm3bYmOD.js.map +0 -1
package/dist/tui.js CHANGED
@@ -1,10 +1,10 @@
1
- import { $ as useMcpAuthDispatch, $n as buildBuildSystem, $t as providerKeySetChanged, A as runOAuthLogin, B as refreshMcpToolsCatalog, Ct as SETTINGS_CATEGORIES, D as formatPathForCwd, Dt as SettingsProvider, En as accentColor, Et as SETTINGS_TOGGLES, F as parseJsonTolerant, Gt as ConfigProvider, H as subscribeMcpToolsCache, Hn as useActiveTodos, Ht as DiscoveryProvider, J as parentServerName, Jt as resolveStoragePaths, K as buildVisibleMcpRows, Kt as useConfig, M as buildMcpServers, Mn as TODO_STATUS_GLYPHS, Mt as resolveChipColor, Nt as resolveTheme, O as fetchOAuthRedirect, Ot as clampFps, P as discoverProjectMcps, Q as McpAuthProvider, Qt as filterChainFallbacks, R as loadMcpToolsCache, St as DEFAULT_SETTINGS, T as suggestSafelistEntries, Tt as SETTINGS_INPUTS, Ut as useDiscovery, Vt as createDiscoverySlot, W as useMcpToolToggleMap, Wt as useDiscoveryOptional, Xt as chainPositionOf, Y as createFileMcpCredentialStore, Yt as buildRunModel, Zt as chainsEqual, _ as useSafeModeQueue, _n as chatAutoCompactBehavior, _t as truncateTrailing, a as useSurfaces, ar as compactSummaryEvent, at as PRESENT_PLAN_TOOL, b as getSafelist, br as updateToolEventOutcomes, bt as listProjectFiles, c as useStreamBuffer, cn as useUpdateCheck, cr as eventsFromTurns, d as discoverProjectSkills, dr as loadState, en as restoreChainFallbacks, er as buildPlanSystem, et as useMcpAuthState, fr as marginTopFor, ft as useInteractionsActions, g as useSafeModeActions, gr as sumRunCosts, gt as hintsLength, h as SafeModeProvider, hr as stripSpawnTokensLine, ht as clipHintsToWidth, i as useSelectStyle, in as tryOpenBrowser, it as InteractionsProvider, j as supportsOAuth, k as oauthUsesManualCodePaste, kt as useSettings, l as buildSkillsConfig, lr as lastContextSizeFromTurns, lt as makeRequestInteraction, m as writeSessionExport, mr as serverToolResultSummary, mt as EMPTY_HINTS, n as ThemeProvider, nn as toggleChainEntry, nr as chatReferenceTokensForPrompt, on as bootTick, ot as buildResumedToolResultsTurn, pt as useInteractionsQueue, q as indexOfServerRow, qt as resolveConfig, r as useColors, rn as useCompletion, rr as createChatTurnsReadTool, rt as ASK_USER_TOOL, s as useTheme, sn as buildUpdateHint, sr as deriveSessionTitle, st as createInteractionTools, t as computeTurnAnchors, tr as envSection, tt as getMcpAuthStatus, u as defaultSkillScanPaths, ur as listSessionMeta, ut as pendingInteractionsFromTurns, vn as discoverAgentsMd, vr as toolCallPreview, vt as buildHints, wt as SETTINGS_CHOICES, x as isOnSafelist, xr as usageByModel, xt as useEnabledToggleSet, y as addToSafelist, yr as toolResultText, z as mcpToolsCachePath } from "./transcript-anchors-Bm3bYmOD.js";
2
- import { $ as composeExtensionPreset, B as ExtensionInstallError, Bt as ensureKeybindingsFile, Et as createModelSearchTool, H as installExtensionFromSpec, Ht as groupBindings, J as createExtensionHud, K as removeInstalledExtension, P as summaryToTurn, St as tailTruncate, Ut as keybindingsPath, Vt as formatBindingForDisplay, Wt as matchesBinding, X as discoverAndBuildExtensionRegistry, Y as registerExtensionHostModules, Yt as resolveExtensionKeybindings, _ as buildPostCompactAttachments, c as applyCompactSummaryCutoff, dn as sanitizeExtensionConfigActionPatch, en as inlineLoadedExtension, et as disposeExtensionRegistry, f as cleanupPersistedSession, g as resolveTasksDir, h as resolvePersistDir, hn as effectiveContextWindow, ht as isSpawnTool, it as createExtensionHostRuntimeBridge, lt as basic_default, m as resolveMcpWarningsDir, mn as OUTPUT_RESERVE_TOKENS, nn as parseExtensionScanPathEnv, on as coerceExtensionConfigInput, q as updateInstalledExtension, rn as scanExtensionDirectories, rt as partitionExtensionCatalog, t as createAgent, tt as filterExtensionRegistry, un as resolveExtensionConfigValue, wt as looksBinary, x as compactConversation, y as selectFilesFromSession, z as createExtensionPromptBridge, zt as KEYBINDING_KEY_COL_WIDTH } from "./agent-CBK6ZUXs.js";
1
+ import { $ as McpAuthProvider, $t as filterChainFallbacks, A as runOAuthLogin, At as useSettings, B as mcpToolsCachePath, 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-DvLIRYLz.js";
2
+ import { $ as composeExtensionPreset, B as ExtensionInstallError, Bt as ensureKeybindingsFile, Et as createModelSearchTool, H as installExtensionFromSpec, Ht as groupBindings, J as createExtensionHud, K as removeInstalledExtension, P as summaryToTurn, St as tailTruncate, Ut as keybindingsPath, Vt as formatBindingForDisplay, Wt as matchesBinding, X as discoverAndBuildExtensionRegistry, Y as registerExtensionHostModules, Yt as resolveExtensionKeybindings, _ as buildPostCompactAttachments, c as applyCompactSummaryCutoff, dn as sanitizeExtensionConfigActionPatch, en as inlineLoadedExtension, et as disposeExtensionRegistry, f as cleanupPersistedSession, g as resolveTasksDir, h as resolvePersistDir, hn as effectiveContextWindow, ht as isSpawnTool, it as createExtensionHostRuntimeBridge, lt as basic_default, m as resolveMcpWarningsDir, mn as OUTPUT_RESERVE_TOKENS, nn as parseExtensionScanPathEnv, on as coerceExtensionConfigInput, q as updateInstalledExtension, rn as scanExtensionDirectories, rt as partitionExtensionCatalog, t as createAgent, tt as filterExtensionRegistry, un as resolveExtensionConfigValue, wt as looksBinary, x as compactConversation, y as selectFilesFromSession, z as createExtensionPromptBridge, zt as KEYBINDING_KEY_COL_WIDTH } from "./agent-IPshmLIF.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-kBUHBlnq.js";
5
5
  import { i as killProcessGroup, n as SUPPORTS_PROCESS_GROUPS, r as createProcessContext } from "./contexts-A4ep-iJs.js";
6
6
  import { l as errorMessage } from "./errors-DJUxZg9b.js";
7
- import { M as findGitRoot, j as projectUserPaths } from "./resolve-HtxmBWs9.js";
7
+ import { M as findGitRoot, j as projectUserPaths } from "./resolve-x5mlFtLX.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 { d as McpOAuthProvider, i as nonOwningMcpView, n as connectMcpServers, p as hasAuthorizationHeader } from "./mcp-H6bqyM94.js";
@@ -14,8 +14,8 @@ import { a as getExtensionSessionState, i as demoteStaleRunningStatus, o as load
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-BAwBcZtD.js";
16
16
  import { s as resolveCompletionSubmit } from "./completion-core-BRo86R_L.js";
17
- import { i as installSkillFromSpec, r as SkillInstallError } from "./skills-DKWqoaYH.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-DreurRAq.js";
17
+ import { i as installSkillFromSpec, r as SkillInstallError } from "./skills-C1vGKbqv.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-BkZ_d5sT.js";
19
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 } from "./auth-CpdQebUy.js";
20
20
  import { createDockerContext } from "./contexts/docker.js";
21
21
  import { createTuiStore } from "./session/sqlite.js";
@@ -24,7 +24,7 @@ import { a as searchHighlightRanges, c as searchSessionHistoryIndexAsync, i as h
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 { createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
27
+ import { closeSync, cpSync, createWriteStream, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, 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,84 +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
- /** Filter + rank commands against a query (name-priority, then description, then fuzzy on name). */
45
- function scoreCommands(catalog, query) {
46
- const q = query.trim().toLowerCase();
47
- const fuzzyHits = q.length > 0 ? new Set(new Fzf([...catalog], {
48
- selector: (c) => c.name,
49
- casing: "case-insensitive"
50
- }).find(q).map((r) => r.item)) : null;
51
- const scored = [];
52
- for (const command of catalog) {
53
- const name = command.name.toLowerCase();
54
- const aliases = (command.aliases ?? []).map((a) => a.toLowerCase());
55
- const group = command.group?.toLowerCase();
56
- const desc = command.description.toLowerCase();
57
- if (q.length === 0) scored.push({
58
- command,
59
- rank: 4
60
- });
61
- else if (name.startsWith(q) || aliases.some((a) => a.startsWith(q))) scored.push({
62
- command,
63
- rank: 0
64
- });
65
- else if (name.includes(q) || aliases.some((a) => a.includes(q)) || group !== void 0 && q.length >= 2 && group.startsWith(q)) scored.push({
66
- command,
67
- rank: 1
68
- });
69
- else if (desc.includes(q)) scored.push({
70
- command,
71
- rank: 2
72
- });
73
- else if (fuzzyHits?.has(command)) scored.push({
74
- command,
75
- rank: 3
76
- });
77
- }
78
- scored.sort((a, b) => a.rank !== b.rank ? a.rank - b.rank : a.command.name.localeCompare(b.command.name));
79
- return scored.map(({ command }) => ({
80
- id: command.name,
81
- label: command.name,
82
- description: command.aliases?.length ? `${command.description} (alias: ${command.aliases.map((a) => `/${a}`).join(", ")})` : command.description,
83
- insertText: `/${command.name} `,
84
- data: command
85
- }));
86
- }
87
- /**
88
- * Build a commands completion provider against a *live* command list. The
89
- * getter is re-evaluated per `suggest` so registry toggles / cwd rebuilds
90
- * take effect without re-instantiating the provider.
91
- */
92
- function createCommandsCompletionProvider(opts) {
93
- return {
94
- id: "commands",
95
- trigger: "/",
96
- label: "Commands",
97
- allowSpacesInQuery: true,
98
- suggest(query, ctx) {
99
- const spaceIdx = query.search(/\s/);
100
- if (spaceIdx !== -1) {
101
- if (ctx.text.trimStart()[0] !== "/") return [];
102
- const typed = query.slice(0, spaceIdx).toLowerCase();
103
- const command = opts.getCommands().find((c) => c.name.toLowerCase() === typed || c.aliases?.some((a) => a.toLowerCase() === typed));
104
- if (!command?.completeArgs) return [];
105
- const argsPrefix = query.slice(spaceIdx + 1).replace(/^\s+/, "");
106
- return Promise.resolve(command.completeArgs(argsPrefix)).then((suggestions) => (suggestions ?? []).map((s) => ({
107
- id: `${command.name}:${s.value}`,
108
- label: s.label ?? s.value,
109
- ...s.description !== void 0 ? { description: s.description } : {},
110
- insertText: `/${command.name} ${s.value} `,
111
- data: command
112
- })));
113
- }
114
- return scoreCommands(opts.getCommands(), query);
115
- },
116
- parseReferences() {
117
- return [];
118
- }
119
- };
120
- }
121
- //#endregion
122
44
  //#region src/chat/mcps-write.ts
123
45
  /**
124
46
  * Resolve the CANONICAL write target for a scope — always the
@@ -240,6 +162,601 @@ function isRecord(value) {
240
162
  return typeof value === "object" && value !== null && !Array.isArray(value);
241
163
  }
242
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 { projectRoot, home } = roots(opts);
202
+ const projectsDir = resolve(home, ".claude/projects");
203
+ if (!existsSync(projectsDir)) return [];
204
+ const slugs = [claudeProjectSlug(projectRoot), claudeProjectSlug(realpathOr(projectRoot))];
205
+ const exact = new Set(slugs);
206
+ const prefixes = [...new Set(slugs.map((s) => `${s}-`))];
207
+ const wantedRoots = /* @__PURE__ */ new Set([projectRoot, realpathOr(projectRoot)]);
208
+ const out = [];
209
+ for (const entry of readdirSync(projectsDir, { withFileTypes: true })) {
210
+ if (!entry.isDirectory()) continue;
211
+ const exactMatch = exact.has(entry.name);
212
+ if (!opts.allProjects && !exactMatch && !prefixes.some((p) => entry.name.startsWith(p))) continue;
213
+ const projectDir = join(projectsDir, entry.name);
214
+ for (const file of readdirSync(projectDir)) {
215
+ if (!file.endsWith(".jsonl")) continue;
216
+ const path = join(projectDir, file);
217
+ if (!opts.allProjects) {
218
+ const root = transcriptProjectRoot(path);
219
+ if (root === null ? !exactMatch : !(wantedRoots.has(root) || wantedRoots.has(realpathOr(root)))) continue;
220
+ }
221
+ out.push({
222
+ path,
223
+ projectDir
224
+ });
225
+ }
226
+ }
227
+ return out;
228
+ }
229
+ /**
230
+ * Peek a transcript's project root: scan the first chunk for the first
231
+ * record carrying a `cwd` and resolve it the same way
232
+ * `convertClaudeSession` does. When the chunk was truncated (giant first
233
+ * records — e.g. pasted base64 images) and held no cwd, fall back to the
234
+ * full file: the import reads it in full anyway, so correctness beats the
235
+ * shortcut. Null when no cwd exists at all (empty/torn file) or the file
236
+ * is unreadable.
237
+ */
238
+ function transcriptProjectRoot(path) {
239
+ let text;
240
+ let truncated = false;
241
+ try {
242
+ const fd = openSync(path, "r");
243
+ try {
244
+ const buf = Buffer.alloc(256 * 1024);
245
+ const n = readSync(fd, buf, 0, buf.length, 0);
246
+ text = buf.subarray(0, n).toString("utf8");
247
+ truncated = n === buf.length;
248
+ } finally {
249
+ closeSync(fd);
250
+ }
251
+ } catch {
252
+ return null;
253
+ }
254
+ const fromChunk = scanForCwd(text);
255
+ if (fromChunk !== null || !truncated) return fromChunk;
256
+ try {
257
+ return scanForCwd(readFileSync(path, "utf8"));
258
+ } catch {
259
+ return null;
260
+ }
261
+ }
262
+ function scanForCwd(text) {
263
+ for (const line of text.split("\n")) {
264
+ if (!line.trim()) continue;
265
+ try {
266
+ const record = JSON.parse(line);
267
+ if (typeof record.cwd === "string") return findGitRoot(record.cwd) ?? record.cwd;
268
+ } catch {
269
+ continue;
270
+ }
271
+ }
272
+ return null;
273
+ }
274
+ function realpathOr(path) {
275
+ try {
276
+ return realpathSync(path);
277
+ } catch {
278
+ return path;
279
+ }
280
+ }
281
+ /**
282
+ * Convert one Claude Code session transcript (JSONL text) to a zidane
283
+ * `SessionData`. Returns null when no conversational turns survive the
284
+ * filter (hook noise, sidechains, queue bookkeeping).
285
+ *
286
+ * Kept-vs-dropped:
287
+ * - `user` / `assistant` records with a `message` → turns. Consecutive
288
+ * assistant records sharing a `message.id` merge into ONE turn (Claude
289
+ * writes text and tool_use as separate records of the same API message).
290
+ * - Consecutive tool_result-only user records merge into ONE user turn:
291
+ * Claude writes one record per result of a parallel tool batch, but
292
+ * zidane persists a batch as a single user turn and
293
+ * `ensureToolResultPairing` enforces strict adjacency — split turns
294
+ * would be "repaired" into orphan markers + error placeholders on resume.
295
+ * - `custom-title` / `summary` records → `metadata.title`.
296
+ * - Sidechains (subagent transcripts), `isMeta` records, `attachment`,
297
+ * `system`, `queue-operation`, `last-prompt` → dropped.
298
+ * - Thinking blocks → dropped (provider-bound signatures, not replayable).
299
+ */
300
+ function convertClaudeSession(jsonl, opts = {}) {
301
+ const turns = [];
302
+ let title;
303
+ let claudeSessionId;
304
+ let cwd;
305
+ let lastAssistantMessageId;
306
+ for (const line of jsonl.split("\n")) {
307
+ if (!line.trim()) continue;
308
+ let record;
309
+ try {
310
+ record = JSON.parse(line);
311
+ } catch {
312
+ continue;
313
+ }
314
+ if (record.type === "custom-title" && typeof record.customTitle === "string") {
315
+ title = record.customTitle;
316
+ continue;
317
+ }
318
+ if (record.type === "summary" && typeof record.summary === "string") {
319
+ title ??= record.summary;
320
+ continue;
321
+ }
322
+ if (record.type !== "user" && record.type !== "assistant") continue;
323
+ if (record.isSidechain || record.isMeta || !record.message) continue;
324
+ claudeSessionId ??= record.sessionId;
325
+ cwd ??= record.cwd;
326
+ const parsedTs = record.timestamp ? Date.parse(record.timestamp) : NaN;
327
+ const createdAt = Number.isNaN(parsedTs) ? Date.now() : parsedTs;
328
+ if (record.type === "assistant") {
329
+ const content = claudeBlocksToContent(record.message.content);
330
+ if (content.length === 0) continue;
331
+ const messageId = record.message.id;
332
+ const previous = turns[turns.length - 1];
333
+ if (messageId && messageId === lastAssistantMessageId && previous?.role === "assistant") {
334
+ previous.content.push(...content);
335
+ const usage = claudeUsage(record.message.usage, record.message.model);
336
+ if (usage) previous.usage = usage;
337
+ continue;
338
+ }
339
+ lastAssistantMessageId = messageId;
340
+ const usage = claudeUsage(record.message.usage, record.message.model);
341
+ turns.push({
342
+ id: record.uuid ?? crypto.randomUUID(),
343
+ role: "assistant",
344
+ content,
345
+ createdAt,
346
+ ...usage ? { usage } : {}
347
+ });
348
+ continue;
349
+ }
350
+ lastAssistantMessageId = void 0;
351
+ const content = claudeBlocksToContent(record.message.content);
352
+ if (content.length === 0) continue;
353
+ const previous = turns[turns.length - 1];
354
+ if (previous?.role === "user" && isToolResultOnly(content) && isToolResultOnly(previous.content)) {
355
+ previous.content.push(...content);
356
+ continue;
357
+ }
358
+ turns.push({
359
+ id: record.uuid ?? crypto.randomUUID(),
360
+ role: "user",
361
+ content,
362
+ createdAt
363
+ });
364
+ }
365
+ if (turns.length === 0) return null;
366
+ const id = claudeSessionId ?? opts.fallbackId ?? crypto.randomUUID();
367
+ const projectRoot = cwd ? findGitRoot(cwd) ?? cwd : void 0;
368
+ return {
369
+ session: {
370
+ id,
371
+ ...projectRoot ? { projectRoot } : {},
372
+ turns,
373
+ runs: [],
374
+ status: "idle",
375
+ metadata: {
376
+ ...title ? { title } : {},
377
+ importedFrom: "claude-code",
378
+ claudeSessionId: id
379
+ },
380
+ createdAt: turns[0].createdAt,
381
+ updatedAt: turns[turns.length - 1].createdAt
382
+ },
383
+ claudeSessionId: id
384
+ };
385
+ }
386
+ /** Map Claude/Anthropic content blocks to zidane `SessionContentBlock`s. */
387
+ function claudeBlocksToContent(content) {
388
+ if (typeof content === "string") return content.trim() ? [{
389
+ type: "text",
390
+ text: content
391
+ }] : [];
392
+ if (!Array.isArray(content)) return [];
393
+ const out = [];
394
+ for (const raw of content) {
395
+ if (!raw || typeof raw !== "object") continue;
396
+ const block = raw;
397
+ switch (block.type) {
398
+ case "text":
399
+ if (typeof block.text === "string" && block.text.trim()) out.push({
400
+ type: "text",
401
+ text: block.text
402
+ });
403
+ break;
404
+ case "tool_use":
405
+ if (typeof block.id === "string" && typeof block.name === "string") {
406
+ const input = block.input && typeof block.input === "object" && !Array.isArray(block.input) ? block.input : {};
407
+ out.push({
408
+ type: "tool_call",
409
+ id: block.id,
410
+ name: block.name,
411
+ input
412
+ });
413
+ }
414
+ break;
415
+ case "tool_result":
416
+ if (typeof block.tool_use_id === "string") out.push({
417
+ type: "tool_result",
418
+ callId: block.tool_use_id,
419
+ output: claudeToolResultOutput(block.content),
420
+ ...block.is_error === true ? { isError: true } : {}
421
+ });
422
+ break;
423
+ case "image": {
424
+ const image = claudeImage(block);
425
+ if (image) out.push(image);
426
+ break;
427
+ }
428
+ }
429
+ }
430
+ return out;
431
+ }
432
+ function claudeImage(block) {
433
+ const source = block.source;
434
+ if (!source || source.type !== "base64" || typeof source.media_type !== "string" || typeof source.data !== "string") return null;
435
+ return {
436
+ type: "image",
437
+ mediaType: source.media_type,
438
+ data: source.data
439
+ };
440
+ }
441
+ function claudeToolResultOutput(content) {
442
+ if (typeof content === "string") return content;
443
+ if (!Array.isArray(content)) return "";
444
+ const parts = [];
445
+ for (const raw of content) {
446
+ if (!raw || typeof raw !== "object") continue;
447
+ const block = raw;
448
+ if (block.type === "text" && typeof block.text === "string") parts.push({
449
+ type: "text",
450
+ text: block.text
451
+ });
452
+ else if (block.type === "image") {
453
+ const image = claudeImage(block);
454
+ if (image?.type === "image" && image.data) parts.push({
455
+ type: "image",
456
+ mediaType: image.mediaType,
457
+ data: image.data
458
+ });
459
+ }
460
+ }
461
+ if (parts.every((p) => p.type === "text")) return parts.map((p) => p.text).join("\n");
462
+ return parts;
463
+ }
464
+ function claudeUsage(usage, model) {
465
+ if (!usage || typeof usage !== "object") return void 0;
466
+ return {
467
+ input: usage.input_tokens ?? 0,
468
+ output: usage.output_tokens ?? 0,
469
+ ...usage.cache_creation_input_tokens ? { cacheCreation: usage.cache_creation_input_tokens } : {},
470
+ ...usage.cache_read_input_tokens ? { cacheRead: usage.cache_read_input_tokens } : {},
471
+ ...model ? { modelId: model } : {}
472
+ };
473
+ }
474
+ function isToolResultOnly(blocks) {
475
+ return blocks.length > 0 && blocks.every((b) => b.type === "tool_result");
476
+ }
477
+ /** Import Claude Code session transcripts into `store`. */
478
+ async function importClaudeSessions(store, opts = {}) {
479
+ const result = {
480
+ imported: 0,
481
+ skipped: 0,
482
+ empty: 0,
483
+ failed: []
484
+ };
485
+ for (const file of listClaudeSessionFiles(opts)) try {
486
+ const converted = convertClaudeSession(readFileSync(file.path, "utf8"), { fallbackId: basename(file.path, ".jsonl") });
487
+ if (!converted) {
488
+ result.empty++;
489
+ continue;
490
+ }
491
+ if (await store.load(converted.session.id)) {
492
+ result.skipped++;
493
+ continue;
494
+ }
495
+ await store.save(converted.session);
496
+ result.imported++;
497
+ } catch (err) {
498
+ result.failed.push({
499
+ path: file.path,
500
+ error: err instanceof Error ? err.message : String(err)
501
+ });
502
+ }
503
+ return result;
504
+ }
505
+ /**
506
+ * Copy Claude Code's MCP servers into zidane-owned `mcps.json` files.
507
+ * The repo-shared `.mcp.json` writes to `<root>/.{prefix}/mcps.json`;
508
+ * BOTH `~/.claude.json` scopes write to `~/.{prefix}/mcps.json` —
509
+ * Claude's "local" scope (the per-project map inside the user state
510
+ * file) is private-by-design and routinely carries tokens in
511
+ * `env`/`headers`, so materializing it into a committable project file
512
+ * would leak credentials. User scope loses the per-project activation
513
+ * (zidane has no user-private per-project config), which is the safer
514
+ * direction to err. Collisions with names in ANY native config path are
515
+ * skipped.
516
+ */
517
+ function importClaudeMcps(opts = {}) {
518
+ const { projectRoot, home } = roots(opts);
519
+ const result = {
520
+ copied: [],
521
+ skipped: [],
522
+ failed: []
523
+ };
524
+ const nativeNames = /* @__PURE__ */ new Set();
525
+ for (const candidate of defaultMcpsConfigPaths({
526
+ ...opts.cwd ? { cwd: opts.cwd } : {},
527
+ home,
528
+ ...opts.prefix ? { prefix: opts.prefix } : {}
529
+ })) {
530
+ if (!existsSync(candidate.path)) continue;
531
+ for (const name of rawMcpNames(readFileSync(candidate.path, "utf8"))) nativeNames.add(name);
532
+ }
533
+ const sources = [];
534
+ const mcpJson = resolve(projectRoot, ".mcp.json");
535
+ if (existsSync(mcpJson)) try {
536
+ sources.push({
537
+ servers: parseMcpsFile(readFileSync(mcpJson, "utf8")),
538
+ scope: "project"
539
+ });
540
+ } catch (err) {
541
+ result.failed.push({
542
+ name: mcpJson,
543
+ error: err instanceof Error ? err.message : String(err)
544
+ });
545
+ }
546
+ const claudeState = resolve(home, ".claude.json");
547
+ if (existsSync(claudeState)) try {
548
+ const state = JSON.parse(readFileSync(claudeState, "utf8"));
549
+ sources.push({
550
+ servers: parseClaudeState(state, "claude-project-state", projectRoot),
551
+ scope: "user"
552
+ });
553
+ sources.push({
554
+ servers: parseClaudeState(state, "claude-user-state"),
555
+ scope: "user"
556
+ });
557
+ } catch (err) {
558
+ result.failed.push({
559
+ name: claudeState,
560
+ error: err instanceof Error ? err.message : String(err)
561
+ });
562
+ }
563
+ const seen = /* @__PURE__ */ new Set();
564
+ for (const { servers, scope } of sources) for (const server of servers) {
565
+ if (seen.has(server.name)) continue;
566
+ seen.add(server.name);
567
+ if (nativeNames.has(server.name)) {
568
+ result.skipped.push(server.name);
569
+ continue;
570
+ }
571
+ try {
572
+ const target = mcpsWritePath(scope, {
573
+ ...opts.cwd ? { cwd: opts.cwd } : {},
574
+ home,
575
+ ...opts.prefix ? { prefix: opts.prefix } : {}
576
+ });
577
+ const { action } = upsertMcpServer(target, server);
578
+ if (action === "updated") {
579
+ result.failed.push({
580
+ name: server.name,
581
+ error: `overwrote an existing entry in ${target} — restore it from version control if unintended`
582
+ });
583
+ continue;
584
+ }
585
+ result.copied.push(server.name);
586
+ } catch (err) {
587
+ result.failed.push({
588
+ name: server.name,
589
+ error: err instanceof Error ? err.message : String(err)
590
+ });
591
+ }
592
+ }
593
+ return result;
594
+ }
595
+ /**
596
+ * Names physically present in an mcps config file, from the raw parsed
597
+ * shape — mirrors `upsertMcpServer`'s walk (flat array `name`s, wrapper
598
+ * `mcpServers` keys, bare-map keys) with none of discovery's filtering.
599
+ * Unreadable/malformed files yield no names; `upsertMcpServer` throws on
600
+ * those before writing, so they still can't be overwritten.
601
+ */
602
+ function rawMcpNames(text) {
603
+ let raw;
604
+ try {
605
+ raw = parseJsonTolerant(text);
606
+ } catch {
607
+ return [];
608
+ }
609
+ if (Array.isArray(raw)) return raw.flatMap((entry) => entry && typeof entry === "object" && typeof entry.name === "string" ? [entry.name] : []);
610
+ if (raw && typeof raw === "object") {
611
+ const record = raw;
612
+ const map = record.mcpServers && typeof record.mcpServers === "object" && !Array.isArray(record.mcpServers) ? record.mcpServers : record;
613
+ return Object.keys(map);
614
+ }
615
+ return [];
616
+ }
617
+ /**
618
+ * Copy `.claude/skills/<name>` directories (project + user) into the
619
+ * corresponding `.{prefix}/skills`. A skill "exists natively" when a
620
+ * directory of the same name sits in any native scan path — those are
621
+ * skipped, not overwritten.
622
+ */
623
+ function importClaudeSkills(opts = {}) {
624
+ const { projectRoot, home } = roots(opts);
625
+ const prefix = (opts.prefix ?? ".zidane").replace(/^\./, "");
626
+ const result = {
627
+ copied: [],
628
+ skipped: [],
629
+ failed: []
630
+ };
631
+ const nativeNames = /* @__PURE__ */ new Set();
632
+ for (const scan of projectUserPaths({
633
+ subPath: "skills",
634
+ ...opts.cwd ? { cwd: opts.cwd } : {},
635
+ home,
636
+ ...opts.prefix ? { prefix: opts.prefix } : {}
637
+ })) {
638
+ if (!existsSync(scan.path)) continue;
639
+ for (const entry of readdirSync(scan.path, { withFileTypes: true })) if (entry.isDirectory()) nativeNames.add(entry.name);
640
+ }
641
+ const pairs = [{
642
+ from: resolve(projectRoot, ".claude/skills"),
643
+ to: resolve(projectRoot, `.${prefix}/skills`)
644
+ }, {
645
+ from: resolve(home, ".claude/skills"),
646
+ to: resolve(home, `.${prefix}/skills`)
647
+ }];
648
+ for (const { from, to } of pairs) {
649
+ if (!existsSync(from)) continue;
650
+ for (const entry of readdirSync(from, { withFileTypes: true })) {
651
+ if (!entry.isDirectory()) continue;
652
+ if (!existsSync(join(from, entry.name, "SKILL.md"))) continue;
653
+ if (nativeNames.has(entry.name)) {
654
+ result.skipped.push(entry.name);
655
+ continue;
656
+ }
657
+ try {
658
+ cpSync(join(from, entry.name), join(to, entry.name), {
659
+ recursive: true,
660
+ errorOnExist: true,
661
+ force: false
662
+ });
663
+ nativeNames.add(entry.name);
664
+ result.copied.push(entry.name);
665
+ } catch (err) {
666
+ result.failed.push({
667
+ name: entry.name,
668
+ error: err instanceof Error ? err.message : String(err)
669
+ });
670
+ }
671
+ }
672
+ }
673
+ return result;
674
+ }
675
+ async function importFromClaudeCode(store, opts = {}) {
676
+ const summary = { notes: ["CLAUDE.md is already read natively — nothing to import.", "Claude permissions/hooks/plugins have no zidane equivalent — not imported."] };
677
+ if (opts.sessions !== false) summary.sessions = await importClaudeSessions(store, opts);
678
+ if (opts.mcps !== false) summary.mcps = importClaudeMcps(opts);
679
+ if (opts.skills !== false) summary.skills = importClaudeSkills(opts);
680
+ return summary;
681
+ }
682
+ /** Filter + rank commands against a query (name-priority, then description, then fuzzy on name). */
683
+ function scoreCommands(catalog, query) {
684
+ const q = query.trim().toLowerCase();
685
+ const fuzzyHits = q.length > 0 ? new Set(new Fzf([...catalog], {
686
+ selector: (c) => c.name,
687
+ casing: "case-insensitive"
688
+ }).find(q).map((r) => r.item)) : null;
689
+ const scored = [];
690
+ for (const command of catalog) {
691
+ const name = command.name.toLowerCase();
692
+ const aliases = (command.aliases ?? []).map((a) => a.toLowerCase());
693
+ const group = command.group?.toLowerCase();
694
+ const desc = command.description.toLowerCase();
695
+ if (q.length === 0) scored.push({
696
+ command,
697
+ rank: 4
698
+ });
699
+ else if (name.startsWith(q) || aliases.some((a) => a.startsWith(q))) scored.push({
700
+ command,
701
+ rank: 0
702
+ });
703
+ else if (name.includes(q) || aliases.some((a) => a.includes(q)) || group !== void 0 && q.length >= 2 && group.startsWith(q)) scored.push({
704
+ command,
705
+ rank: 1
706
+ });
707
+ else if (desc.includes(q)) scored.push({
708
+ command,
709
+ rank: 2
710
+ });
711
+ else if (fuzzyHits?.has(command)) scored.push({
712
+ command,
713
+ rank: 3
714
+ });
715
+ }
716
+ scored.sort((a, b) => a.rank !== b.rank ? a.rank - b.rank : a.command.name.localeCompare(b.command.name));
717
+ return scored.map(({ command }) => ({
718
+ id: command.name,
719
+ label: command.name,
720
+ description: command.aliases?.length ? `${command.description} (alias: ${command.aliases.map((a) => `/${a}`).join(", ")})` : command.description,
721
+ insertText: `/${command.name} `,
722
+ data: command
723
+ }));
724
+ }
725
+ /**
726
+ * Build a commands completion provider against a *live* command list. The
727
+ * getter is re-evaluated per `suggest` so registry toggles / cwd rebuilds
728
+ * take effect without re-instantiating the provider.
729
+ */
730
+ function createCommandsCompletionProvider(opts) {
731
+ return {
732
+ id: "commands",
733
+ trigger: "/",
734
+ label: "Commands",
735
+ allowSpacesInQuery: true,
736
+ suggest(query, ctx) {
737
+ const spaceIdx = query.search(/\s/);
738
+ if (spaceIdx !== -1) {
739
+ if (ctx.text.trimStart()[0] !== "/") return [];
740
+ const typed = query.slice(0, spaceIdx).toLowerCase();
741
+ const command = opts.getCommands().find((c) => c.name.toLowerCase() === typed || c.aliases?.some((a) => a.toLowerCase() === typed));
742
+ if (!command?.completeArgs) return [];
743
+ const argsPrefix = query.slice(spaceIdx + 1).replace(/^\s+/, "");
744
+ return Promise.resolve(command.completeArgs(argsPrefix)).then((suggestions) => (suggestions ?? []).map((s) => ({
745
+ id: `${command.name}:${s.value}`,
746
+ label: s.label ?? s.value,
747
+ ...s.description !== void 0 ? { description: s.description } : {},
748
+ insertText: `/${command.name} ${s.value} `,
749
+ data: command
750
+ })));
751
+ }
752
+ return scoreCommands(opts.getCommands(), query);
753
+ },
754
+ parseReferences() {
755
+ return [];
756
+ }
757
+ };
758
+ }
759
+ //#endregion
243
760
  //#region src/mcp/warn-log.ts
244
761
  /**
245
762
  * Subscribe to `mcp:warn`
@@ -806,6 +1323,217 @@ function truncate(s, max) {
806
1323
  return `${s.slice(0, max - 1)}…`;
807
1324
  }
808
1325
  //#endregion
1326
+ //#region src/tui/claude-import-modal.tsx
1327
+ /**
1328
+ * "Import from Claude Code" — launched from the settings modal's Agent
1329
+ * tab. Two phases:
1330
+ *
1331
+ * 1. Pick — toggle which pieces to import (sessions / MCP servers /
1332
+ * skills) and the session scope (this project vs every Claude
1333
+ * project), then ↵ on the Import row runs it.
1334
+ * 2. Done — the summary of what was imported / skipped / failed, plus
1335
+ * the standing notes about what is deliberately NOT imported
1336
+ * (CLAUDE.md is read natively; permissions/hooks have no zidane
1337
+ * equivalent).
1338
+ *
1339
+ * All writes go to zidane-owned locations (session store, `.{prefix}/`
1340
+ * files); the `.claude` setup itself is never touched. Idempotent — the
1341
+ * import skips whatever a previous run already brought over, so re-running
1342
+ * is always safe.
1343
+ */
1344
+ function ClaudeImportModal({ store, cwd, prefix, onDone }) {
1345
+ const COLOR = useColors();
1346
+ const [options, setOptions] = useState({
1347
+ sessions: true,
1348
+ mcps: true,
1349
+ skills: true,
1350
+ allProjects: false
1351
+ });
1352
+ const [cursor, setCursor] = useState(0);
1353
+ const [phase, setPhase] = useState("pick");
1354
+ const [summary, setSummary] = useState(null);
1355
+ const [error, setError] = useState(null);
1356
+ const runningRef = useRef(false);
1357
+ const counts = useMemo(() => ({
1358
+ project: listClaudeSessionFiles({ cwd }).length,
1359
+ all: listClaudeSessionFiles({
1360
+ cwd,
1361
+ allProjects: true
1362
+ }).length
1363
+ }), [cwd]);
1364
+ const rows = [
1365
+ {
1366
+ key: "sessions",
1367
+ label: "Conversations",
1368
+ detail: `${options.allProjects ? counts.all : counts.project} transcript file(s) found`
1369
+ },
1370
+ {
1371
+ key: "allProjects",
1372
+ label: "… from all projects",
1373
+ detail: `off = current project only (${counts.project} here, ${counts.all} total)`
1374
+ },
1375
+ {
1376
+ key: "mcps",
1377
+ label: "MCP servers",
1378
+ detail: "copy into zidane-owned mcps.json (native names never overwritten)"
1379
+ },
1380
+ {
1381
+ key: "skills",
1382
+ label: "Skills",
1383
+ detail: `copy .claude/skills into ${prefix ?? ".zidane"}/skills`
1384
+ },
1385
+ {
1386
+ key: "run",
1387
+ label: "Import",
1388
+ detail: "run with the selection above"
1389
+ }
1390
+ ];
1391
+ const runImport = async () => {
1392
+ if (runningRef.current) return;
1393
+ runningRef.current = true;
1394
+ setPhase("running");
1395
+ await new Promise((resolve) => setTimeout(resolve, 0));
1396
+ let result;
1397
+ try {
1398
+ result = await importFromClaudeCode(store, {
1399
+ cwd,
1400
+ ...prefix ? { prefix } : {},
1401
+ sessions: options.sessions,
1402
+ mcps: options.mcps,
1403
+ skills: options.skills,
1404
+ allProjects: options.allProjects
1405
+ });
1406
+ setSummary(result);
1407
+ setPhase("done");
1408
+ } catch (err) {
1409
+ setError(err instanceof Error ? err.message : String(err));
1410
+ setPhase("done");
1411
+ } finally {
1412
+ runningRef.current = false;
1413
+ }
1414
+ if (result) try {
1415
+ await onDone?.(result);
1416
+ } catch {}
1417
+ };
1418
+ useKeyboard((key) => {
1419
+ if (phase !== "pick") return;
1420
+ if (key.name === "up" || key.ctrl && key.name === "p") setCursor((c) => (c - 1 + rows.length) % rows.length);
1421
+ else if (key.name === "down" || key.ctrl && key.name === "n") setCursor((c) => (c + 1) % rows.length);
1422
+ else if (key.name === "return" || key.name === "space") {
1423
+ const row = rows[cursor];
1424
+ if (row.key === "run") runImport();
1425
+ else setOptions((prev) => ({
1426
+ ...prev,
1427
+ [row.key]: !prev[row.key]
1428
+ }));
1429
+ }
1430
+ });
1431
+ if (phase === "running") return /* @__PURE__ */ jsx$1(Modal, {
1432
+ title: "Import from Claude Code",
1433
+ disableEscape: true,
1434
+ children: /* @__PURE__ */ jsx$1("text", {
1435
+ fg: COLOR.mute,
1436
+ children: "importing…"
1437
+ })
1438
+ });
1439
+ if (phase === "done") return /* @__PURE__ */ jsxs(Modal, {
1440
+ title: "Import from Claude Code",
1441
+ children: [/* @__PURE__ */ jsxs("box", {
1442
+ style: { flexDirection: "column" },
1443
+ children: [
1444
+ error && /* @__PURE__ */ jsx$1("text", {
1445
+ fg: COLOR.error,
1446
+ children: `import failed: ${error}`
1447
+ }),
1448
+ summary?.sessions && /* @__PURE__ */ jsxs("text", {
1449
+ fg: COLOR.dim,
1450
+ children: [
1451
+ `conversations: ${summary.sessions.imported} imported, ${summary.sessions.skipped} already present`,
1452
+ summary.sessions.empty > 0 ? `, ${summary.sessions.empty} empty` : "",
1453
+ summary.sessions.failed.length > 0 ? `, ${summary.sessions.failed.length} failed` : ""
1454
+ ]
1455
+ }),
1456
+ summary?.mcps && /* @__PURE__ */ jsx$1("text", {
1457
+ fg: COLOR.dim,
1458
+ children: copyLine("mcp servers", summary.mcps)
1459
+ }),
1460
+ summary?.skills && /* @__PURE__ */ jsx$1("text", {
1461
+ fg: COLOR.dim,
1462
+ children: copyLine("skills", summary.skills)
1463
+ }),
1464
+ summary?.notes.map((note) => /* @__PURE__ */ jsx$1("text", {
1465
+ fg: COLOR.mute,
1466
+ children: `· ${note}`
1467
+ }, note))
1468
+ ]
1469
+ }), /* @__PURE__ */ jsxs("text", {
1470
+ fg: COLOR.mute,
1471
+ children: [/* @__PURE__ */ jsx$1("span", {
1472
+ fg: COLOR.warn,
1473
+ children: "esc"
1474
+ }), " close"]
1475
+ })]
1476
+ });
1477
+ const enabledFor = (key) => key === "run" ? void 0 : options[key];
1478
+ return /* @__PURE__ */ jsxs(Modal, {
1479
+ title: "Import from Claude Code",
1480
+ children: [/* @__PURE__ */ jsx$1("box", {
1481
+ style: { flexDirection: "column" },
1482
+ children: rows.map((row, i) => {
1483
+ const focused = i === cursor;
1484
+ const enabled = enabledFor(row.key);
1485
+ return /* @__PURE__ */ jsxs("text", {
1486
+ fg: focused ? COLOR.brand : COLOR.dim,
1487
+ children: [
1488
+ /* @__PURE__ */ jsx$1("span", {
1489
+ fg: focused ? COLOR.brand : COLOR.mute,
1490
+ children: focused ? "▶ " : " "
1491
+ }),
1492
+ enabled !== void 0 && /* @__PURE__ */ jsx$1("span", {
1493
+ fg: enabled ? COLOR.accent : COLOR.mute,
1494
+ children: enabled ? "[✓] " : "[ ] "
1495
+ }),
1496
+ /* @__PURE__ */ jsx$1("span", {
1497
+ fg: focused ? COLOR.brand : COLOR.dim,
1498
+ children: row.label
1499
+ }),
1500
+ /* @__PURE__ */ jsxs("span", {
1501
+ fg: COLOR.mute,
1502
+ children: [" ", row.detail]
1503
+ })
1504
+ ]
1505
+ }, row.key);
1506
+ })
1507
+ }), /* @__PURE__ */ jsxs("text", {
1508
+ fg: COLOR.mute,
1509
+ children: [
1510
+ /* @__PURE__ */ jsx$1("span", {
1511
+ fg: COLOR.warn,
1512
+ children: "↑↓"
1513
+ }),
1514
+ " navigate · ",
1515
+ /* @__PURE__ */ jsx$1("span", {
1516
+ fg: COLOR.warn,
1517
+ children: "↵"
1518
+ }),
1519
+ " toggle / run · ",
1520
+ /* @__PURE__ */ jsx$1("span", {
1521
+ fg: COLOR.warn,
1522
+ children: "esc"
1523
+ }),
1524
+ " close"
1525
+ ]
1526
+ })]
1527
+ });
1528
+ }
1529
+ function copyLine(label, result) {
1530
+ const parts = [];
1531
+ parts.push(result.copied.length > 0 ? `${result.copied.length} copied (${result.copied.join(", ")})` : "none copied");
1532
+ if (result.skipped.length > 0) parts.push(`${result.skipped.length} skipped as native (${result.skipped.join(", ")})`);
1533
+ if (result.failed.length > 0) parts.push(`${result.failed.length} failed`);
1534
+ return `${label}: ${parts.join(" · ")}`;
1535
+ }
1536
+ //#endregion
809
1537
  //#region src/tui/clipboard.ts
810
1538
  /**
811
1539
  * Two-pronged clipboard write.
@@ -4939,6 +5667,7 @@ function DiscoveryShell({ children }) {
4939
5667
  const config = useConfig();
4940
5668
  const { settings } = useSettings();
4941
5669
  const foreignAgents = settings.importForeignConfigs === true;
5670
+ const rulerConfigs = settings.importRulerConfigs !== false;
4942
5671
  const dispatchAuth = useMcpAuthDispatch();
4943
5672
  const dispatchAuthRef = useRef(dispatchAuth);
4944
5673
  dispatchAuthRef.current = dispatchAuth;
@@ -5010,7 +5739,8 @@ function DiscoveryShell({ children }) {
5010
5739
  walk: () => discoverProjectSkills({
5011
5740
  cwd: projectDir,
5012
5741
  prefix: config.prefix,
5013
- foreignAgents
5742
+ foreignAgents,
5743
+ ruler: rulerConfigs
5014
5744
  }),
5015
5745
  onLoad: (items) => {
5016
5746
  if (skillsSlotRef.current === skillsSlot) setSkillsCatalog(items);
@@ -5026,7 +5756,8 @@ function DiscoveryShell({ children }) {
5026
5756
  const { servers, errors } = discoverProjectMcps({
5027
5757
  cwd: projectDir,
5028
5758
  prefix: config.prefix,
5029
- foreignAgents
5759
+ foreignAgents,
5760
+ ruler: rulerConfigs
5030
5761
  });
5031
5762
  mcpsFingerprintRef.current = JSON.stringify({
5032
5763
  servers,
@@ -5047,7 +5778,8 @@ function DiscoveryShell({ children }) {
5047
5778
  projectDir,
5048
5779
  config.prefix,
5049
5780
  seedMcpAuthBadges,
5050
- foreignAgents
5781
+ foreignAgents,
5782
+ rulerConfigs
5051
5783
  ]);
5052
5784
  const ensureFiles = useCallback(() => filesSlotRef.current?.ensure() ?? Promise.resolve([]), []);
5053
5785
  const ensureSkills = useCallback(() => skillsSlotRef.current?.ensure() ?? Promise.resolve([]), []);
@@ -5058,7 +5790,8 @@ function DiscoveryShell({ children }) {
5058
5790
  const { servers, errors } = discoverProjectMcps({
5059
5791
  cwd: projectDir,
5060
5792
  prefix: config.prefix,
5061
- foreignAgents
5793
+ foreignAgents,
5794
+ ruler: rulerConfigs
5062
5795
  });
5063
5796
  const fingerprint = JSON.stringify({
5064
5797
  servers,
@@ -5078,7 +5811,8 @@ function DiscoveryShell({ children }) {
5078
5811
  projectDir,
5079
5812
  config.prefix,
5080
5813
  seedMcpAuthBadges,
5081
- foreignAgents
5814
+ foreignAgents,
5815
+ rulerConfigs
5082
5816
  ]);
5083
5817
  useEffect(() => {
5084
5818
  let lastSignature = cacheFileSignature(dataDir);
@@ -13606,6 +14340,14 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
13606
14340
  description: `current: ${settings.compactionModel || "main model"} — pick any provider's model for compaction summaries (pick the main model to clear)`,
13607
14341
  onPick: actions.onPickCompactionModel
13608
14342
  });
14343
+ if (actions?.onImportClaudeCode) acts.push({
14344
+ kind: "action",
14345
+ category: "agent",
14346
+ id: "import-claude-code",
14347
+ label: "Import from Claude Code",
14348
+ description: "copy conversations, MCP servers, and skills from your ~/.claude setup into zidane",
14349
+ onPick: actions.onImportClaudeCode
14350
+ });
13609
14351
  const inputs = SETTINGS_INPUTS.filter((inp) => inp.key !== "sessionIcon" && inp.key !== "attentionIcon" && inp.key !== "activeIcon" || settings.terminalTitle === "icon-session").map((inp) => ({
13610
14352
  kind: "input",
13611
14353
  ...inp
@@ -17238,6 +17980,8 @@ function AppShell() {
17238
17980
  enabledSkillsRef.current = settings.enabledSkills;
17239
17981
  const importForeignConfigsRef = useRef(settings.importForeignConfigs);
17240
17982
  importForeignConfigsRef.current = settings.importForeignConfigs;
17983
+ const importRulerConfigsRef = useRef(settings.importRulerConfigs);
17984
+ importRulerConfigsRef.current = settings.importRulerConfigs;
17241
17985
  const mcpsCatalogRef = useRef(mcpsCatalog);
17242
17986
  mcpsCatalogRef.current = mcpsCatalog;
17243
17987
  const enabledMcpsRef = useRef(settings.enabledMcps);
@@ -18127,7 +18871,8 @@ function AppShell() {
18127
18871
  const skillsScan = defaultSkillScanPaths({
18128
18872
  cwd: projectDir,
18129
18873
  prefix: config.prefix,
18130
- foreignAgents: importForeignConfigsRef.current === true
18874
+ foreignAgents: importForeignConfigsRef.current === true,
18875
+ ruler: importRulerConfigsRef.current !== false
18131
18876
  });
18132
18877
  const liveExtensionRegistry = filterExtensionRegistry(extensionRegistry, enabledExtensionsRef.current);
18133
18878
  const extensionSkillNames = liveExtensionRegistry.skills.map((s) => s.name);
@@ -18147,7 +18892,8 @@ function AppShell() {
18147
18892
  const agentsMd = discoverAgentsMd({
18148
18893
  cwd: actualCwd,
18149
18894
  prefix: config.prefix,
18150
- scope: userInstructionsScopeRef.current
18895
+ scope: userInstructionsScopeRef.current,
18896
+ ruler: importRulerConfigsRef.current !== false
18151
18897
  });
18152
18898
  const envOpts = {
18153
18899
  cwd: actualCwd,
@@ -18254,7 +19000,6 @@ function AppShell() {
18254
19000
  tools: mergedTools,
18255
19001
  ...composedPreset.toolAliases ? { toolAliases: composedPreset.toolAliases } : {},
18256
19002
  ...effectiveSystem ? { system: effectiveSystem } : {},
18257
- mcpServers: mergedMcpServers,
18258
19003
  skills,
18259
19004
  behavior: mergedBehavior,
18260
19005
  spawnTool: seamCatalog.length === 0 ? seamSpawnBase : seamSpawnBase.withModels(seamCatalog)
@@ -19702,6 +20447,30 @@ function AppShell() {
19702
20447
  modal,
19703
20448
  setSetting
19704
20449
  ]);
20450
+ /**
20451
+ * "Import from Claude Code" — launched from the settings modal's Agent
20452
+ * tab (row only wired when `~/.claude` exists). The import modal
20453
+ * replaces the settings modal in the single modal slot; a successful
20454
+ * session import refreshes the sessions list through the live ref.
20455
+ */
20456
+ const onImportClaudeCode = useCallback(() => {
20457
+ modal.open(/* @__PURE__ */ jsx$1(ClaudeImportModal, {
20458
+ store,
20459
+ cwd: cwdRef.current,
20460
+ ...config.prefix ? { prefix: config.prefix } : {},
20461
+ onDone: (summary) => {
20462
+ if ((summary.sessions?.imported ?? 0) > 0) refreshSessionsRef.current().catch((err) => debugLog("claude-import: refreshSessions failed", err));
20463
+ if ((summary.mcps?.copied.length ?? 0) > 0) Promise.resolve(onRefreshMcps()).catch((err) => debugLog("claude-import: refreshMcps failed", err));
20464
+ if ((summary.skills?.copied.length ?? 0) > 0) Promise.resolve(onRefreshSkills()).catch((err) => debugLog("claude-import: refreshSkills failed", err));
20465
+ }
20466
+ }));
20467
+ }, [
20468
+ modal,
20469
+ store,
20470
+ config.prefix,
20471
+ onRefreshMcps,
20472
+ onRefreshSkills
20473
+ ]);
19705
20474
  const onPickEffort = useCallback((effort) => {
19706
20475
  setPicked((prev) => {
19707
20476
  if (!prev) return prev;
@@ -20134,11 +20903,22 @@ function AppShell() {
20134
20903
  if (tasks.length === 0) return [];
20135
20904
  await flushCommandTurn();
20136
20905
  const handle = await agent.ensureExecution();
20906
+ let onAbort;
20137
20907
  try {
20138
- await agent.warmup();
20908
+ await Promise.race([agent.warmup(), new Promise((resolveAbort) => {
20909
+ if (controller.signal.aborted) {
20910
+ resolveAbort();
20911
+ return;
20912
+ }
20913
+ onAbort = () => resolveAbort();
20914
+ controller.signal.addEventListener("abort", onAbort, { once: true });
20915
+ })]);
20139
20916
  } catch (err) {
20140
20917
  debugLog("spawnAgents: warmup failed — children fall back to config bootstrap", err);
20918
+ } finally {
20919
+ if (onAbort) controller.signal.removeEventListener("abort", onAbort);
20141
20920
  }
20921
+ if (controller.signal.aborted) throw new Error("spawnAgents: command aborted");
20142
20922
  const parentMcp = agent.mcpConnection;
20143
20923
  const results = tasks.map((task, i) => ({
20144
20924
  label: task.label ?? `task-${i + 1}`,
@@ -20169,7 +20949,7 @@ function AppShell() {
20169
20949
  ...seam.system !== void 0 ? { system: seam.system } : {},
20170
20950
  tools: seam.tools,
20171
20951
  ...seam.toolAliases !== void 0 ? { toolAliases: seam.toolAliases } : {},
20172
- mcpServers: seam.mcpServers,
20952
+ mcpServers: [...agent.mcpServers],
20173
20953
  ...parentMcp ? { mcpConnection: nonOwningMcpView(parentMcp) } : {},
20174
20954
  skills: seam.skills,
20175
20955
  behavior: seam.behavior,
@@ -21436,7 +22216,8 @@ function AppShell() {
21436
22216
  onAddMcp,
21437
22217
  onRemoveSkill,
21438
22218
  onRemoveMcp,
21439
- onPickCompactionModel
22219
+ onPickCompactionModel,
22220
+ ...hasClaudeCodeSetup() ? { onImportClaudeCode } : {}
21440
22221
  }
21441
22222
  }));
21442
22223
  return;