@edspencer/paddock 0.78.1 → 0.78.2

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 (74) hide show
  1. package/package.json +2 -2
  2. package/packages/server/dist/auto-memory.d.ts +22 -0
  3. package/packages/server/dist/auto-memory.js +165 -0
  4. package/packages/server/dist/herdctl-agent-config.d.ts +2 -2
  5. package/packages/server/dist/herdctl-agent-config.js +18 -2
  6. package/packages/server/dist/herdctl.d.ts +25 -0
  7. package/packages/server/dist/herdctl.js +44 -4
  8. package/packages/server/dist/transcripts.js +14 -8
  9. package/packages/server/package.json +1 -1
  10. package/packages/web/dist/assets/{DeleteProjectDialog-I2oqHBzS.js → DeleteProjectDialog-DX4WrELh.js} +1 -1
  11. package/packages/web/dist/assets/DiscoverPage-SmrSuKC8.js +2 -0
  12. package/packages/web/dist/assets/{DiscoverView-C21wIKtf.js → DiscoverView-DE65Ui9u.js} +1 -1
  13. package/packages/web/dist/assets/{InstanceConfigPage-EYk_wC3Y.js → InstanceConfigPage-B27CjE-g.js} +1 -1
  14. package/packages/web/dist/assets/{MarkdownRenderer-BsTH5ue5.js → MarkdownRenderer-CHgKQK1c.js} +1 -1
  15. package/packages/web/dist/assets/{ProjectRedirect-ClRBMnRd.js → ProjectRedirect-CnUMNGmC.js} +1 -1
  16. package/packages/web/dist/assets/{ProjectView-BvGNJ12B.js → ProjectView-BQJPlfPn.js} +3 -3
  17. package/packages/web/dist/assets/{ProjectsGrid-BR5dAAFl.js → ProjectsGrid-ByCT2JT8.js} +1 -1
  18. package/packages/web/dist/assets/{RootHome-AVtKnSgs.js → RootHome-Bd5UWE4_.js} +1 -1
  19. package/packages/web/dist/assets/{Surfaces-DPLI5OkT.js → Surfaces-CTGs5AAK.js} +1 -1
  20. package/packages/web/dist/assets/{arc-C5J-eVUW.js → arc-D-OBWbNz.js} +1 -1
  21. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-CZCJKMwH.js → architectureDiagram-3BPJPVTR-uHjrv32s.js} +1 -1
  22. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-WWCMaGwE.js → blockDiagram-GPEHLZMM-BfORDi4c.js} +1 -1
  23. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-DSvcKk0w.js → c4Diagram-AAUBKEIU-CavMptIt.js} +1 -1
  24. package/packages/web/dist/assets/channel-DuOAMEMs.js +2 -0
  25. package/packages/web/dist/assets/{chunk-2J33WTMH-Bunmbxbf.js → chunk-2J33WTMH-DWbn9f15.js} +1 -1
  26. package/packages/web/dist/assets/{chunk-4BX2VUAB-BVlgK4fh.js → chunk-4BX2VUAB-B6n57Y4t.js} +1 -1
  27. package/packages/web/dist/assets/{chunk-55IACEB6-CGx4xEnG.js → chunk-55IACEB6-RPlieJ1B.js} +1 -1
  28. package/packages/web/dist/assets/{chunk-727SXJPM-B88C8y6G.js → chunk-727SXJPM-3fxjrr8e.js} +1 -1
  29. package/packages/web/dist/assets/{chunk-AQP2D5EJ-BT2IBIj0.js → chunk-AQP2D5EJ-8773cg5n.js} +1 -1
  30. package/packages/web/dist/assets/{chunk-FMBD7UC4-kFOjbALN.js → chunk-FMBD7UC4-BaEdhz4G.js} +1 -1
  31. package/packages/web/dist/assets/{chunk-ND2GUHAM-BUeFecIE.js → chunk-ND2GUHAM-BGU6JXI8.js} +1 -1
  32. package/packages/web/dist/assets/{chunk-QZHKN3VN-DJyJVXra.js → chunk-QZHKN3VN-Bn7xewhK.js} +1 -1
  33. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DJTQSdLF.js +2 -0
  34. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DJTQSdLF.js +2 -0
  35. package/packages/web/dist/assets/{core-C5DQrbiK.js → core-Dt-6hYRW.js} +1 -1
  36. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-B4B7ZRSe.js → cose-bilkent-S5V4N54A-CY47vg-k.js} +1 -1
  37. package/packages/web/dist/assets/{dagre-BM42HDAG-B2i2sau8.js → dagre-BM42HDAG-Bn5RQpQd.js} +1 -1
  38. package/packages/web/dist/assets/{diagram-2AECGRRQ-BbTlnpG6.js → diagram-2AECGRRQ-CRzZOKXG.js} +1 -1
  39. package/packages/web/dist/assets/{diagram-5GNKFQAL-B3Th9TJN.js → diagram-5GNKFQAL-DVBHmgdg.js} +1 -1
  40. package/packages/web/dist/assets/{diagram-KO2AKTUF-Bzrq4YSz.js → diagram-KO2AKTUF-CmrfF1N2.js} +1 -1
  41. package/packages/web/dist/assets/{diagram-LMA3HP47-CwmWi6QE.js → diagram-LMA3HP47-CDT8x43G.js} +1 -1
  42. package/packages/web/dist/assets/{diagram-OG6HWLK6-Bb6iqeiS.js → diagram-OG6HWLK6-C71v9fTh.js} +1 -1
  43. package/packages/web/dist/assets/{documentTitle-BO1z6u5w.js → documentTitle-cxKpkGjk.js} +1 -1
  44. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-CVQQunO1.js → erDiagram-TEJ5UH35-DutEYHIi.js} +1 -1
  45. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-BEY3tz_1.js → flowDiagram-I6XJVG4X-MAg26MC3.js} +1 -1
  46. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-BBdhMq6t.js → ganttDiagram-6RSMTGT7-CxjMzmWU.js} +1 -1
  47. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-p26vw1Ba.js → gitGraphDiagram-PVQCEYII-DRdROoMO.js} +1 -1
  48. package/packages/web/dist/assets/{index-Ba5H3P_n.js → index-BYqBTUv_.js} +2 -2
  49. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-C9Lm4jRf.js → infoDiagram-5YYISTIA-Yov5F3W5.js} +1 -1
  50. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DvAVFsHW.js → ishikawaDiagram-YF4QCWOH-CaKXiwxT.js} +1 -1
  51. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-BHAQBO0Z.js → journeyDiagram-JHISSGLW-DAis2VeB.js} +1 -1
  52. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-c37yXqVr.js → kanban-definition-UN3LZRKU-C8gEFG37.js} +1 -1
  53. package/packages/web/dist/assets/{linear-CaLelNJV.js → linear-BGqCeIdL.js} +1 -1
  54. package/packages/web/dist/assets/{mermaid.core-BZ9rtnnW.js → mermaid.core-Byo4DAC8.js} +4 -4
  55. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-DjANagUV.js → mindmap-definition-RKZ34NQL-DPc2OGmC.js} +1 -1
  56. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-DLVKuVW7.js → pieDiagram-4H26LBE5-8LoTusWf.js} +1 -1
  57. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-CzEweksI.js → quadrantDiagram-W4KKPZXB-PqmA7ZXs.js} +1 -1
  58. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-Cmp8uj-X.js → requirementDiagram-4Y6WPE33-CTtvHeYi.js} +1 -1
  59. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-DEr5os6k.js → sankeyDiagram-5OEKKPKP-DA2BOpvW.js} +1 -1
  60. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-CUw8k4V7.js → sequenceDiagram-3UESZ5HK-BdksJUNO.js} +1 -1
  61. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-CCPuhhD3.js → stateDiagram-AJRCARHV-BGK1rh1o.js} +1 -1
  62. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-DTVNruiv.js +2 -0
  63. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-M4Oh-WQu.js → timeline-definition-PNZ67QCA-D62mOs5P.js} +1 -1
  64. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-CaAp-n-E.js → vennDiagram-CIIHVFJN-DLfP6nl7.js} +1 -1
  65. package/packages/web/dist/assets/{wardley-L42UT6IY-slzDPBwj.js → wardley-L42UT6IY-BFUgYulG.js} +1 -1
  66. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-BaVLEx_-.js → wardleyDiagram-YWT4CUSO-BrEbgbXY.js} +1 -1
  67. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-axwO9R4I.js → xychartDiagram-2RQKCTM6-ClURTg6_.js} +1 -1
  68. package/packages/web/dist/index.html +1 -1
  69. package/packages/web/dist/sw.js +1 -1
  70. package/packages/web/dist/assets/DiscoverPage-BWJpx4AG.js +0 -2
  71. package/packages/web/dist/assets/channel-BwTacR4O.js +0 -2
  72. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DzYQps0D.js +0 -2
  73. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DzYQps0D.js +0 -2
  74. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-DIMvU0dF.js +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edspencer/paddock",
3
- "version": "0.78.1",
3
+ "version": "0.78.2",
4
4
  "description": "Project-first launchpad for Claude Code: server-hosted, persistent, resumable sessions organized by project.",
5
5
  "keywords": [
6
6
  "claude",
@@ -37,7 +37,7 @@
37
37
  "@fastify/swagger-ui": "4.2.0",
38
38
  "@fastify/websocket": "10.0.1",
39
39
  "@herdctl/chat": "0.8.0",
40
- "@herdctl/core": "5.33.2",
40
+ "@herdctl/core": "5.34.0",
41
41
  "@modelcontextprotocol/sdk": "1.29.0",
42
42
  "fastify": "4.28.1",
43
43
  "jose": "6.2.3",
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The directory Claude Code keys auto-memory on for an agent started in
3
+ * `workingDir`: the canonical git root above it, else the cwd. Starts from the
4
+ * realpath, as Claude Code's own `process.cwd()` does. Filesystem only — no git
5
+ * subprocess, so nothing here can hang on, or be redirected by, git.
6
+ */
7
+ export declare function memoryRoot(workingDir: string): Promise<string>;
8
+ /**
9
+ * `fs.realpath` for a path that may not fully exist yet: resolve the deepest
10
+ * existing ancestor and re-append the rest. The memory dir itself is created
11
+ * lazily by Claude Code on first write, so it is usually missing at boot — but
12
+ * the symlink above it, which is the part that matters, is already planted.
13
+ */
14
+ export declare function realpathLenient(p: string): Promise<string>;
15
+ /**
16
+ * The symlink-free auto-memory dir for an agent whose Claude home is
17
+ * `claudeHome` and whose cwd is `workingDir`, or undefined when the `memory`
18
+ * entry is itself a symlink (see the module doc). Call it AFTER every project's
19
+ * transcript symlink is planted — a notebook project's memory root is usually
20
+ * ANOTHER workspace's link (the notes repo's), not its own.
21
+ */
22
+ export declare function resolveAutoMemoryDir(claudeHome: string, workingDir: string, userHome?: string): Promise<string | undefined>;
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The agent memory directory, handed to Claude Code as `autoMemoryDirectory` (#955).
3
+ *
4
+ * Claude Code always lets an agent write to its own auto-memory dir, which by
5
+ * default is `<claudeHome>/projects/<enc(root)>/memory/`. Paddock makes
6
+ * `<claudeHome>/projects/<enc>` a SYMLINK (to `.chats/` under `transcripts:
7
+ * own`, into `~/.claude` under `host` — see `transcripts.ts`), and the harness
8
+ * checks a write by resolving symlinks first. The resolved path no longer
9
+ * starts with the configured (unresolved) memory dir, so the exemption misses
10
+ * and a memory write is judged as an ordinary write to wherever it resolves.
11
+ *
12
+ * Under `own` that is `.chats/`, which the keeper's inherited `Write` allow rule
13
+ * happens to cover, so the miss is invisible. Under `host` it is the user's
14
+ * `~/.claude/projects/…`, a protected path that no allow rule can pre-approve:
15
+ * "…which is a sensitive file … resolves through a symlink…", denied, with
16
+ * nobody there to approve it. Reproduced on CLI 2.1.216 and 2.1.282 against a
17
+ * stub API, and on a running instance; see the PR. The fix is applied in both
18
+ * modes, because `own` only works by accident of the allowlist.
19
+ *
20
+ * The fix is to tell Claude Code the memory dir's REAL path, so the literal and
21
+ * resolved paths agree. It has to travel as a flag-tier setting (herdctl's agent
22
+ * `settings`): Claude Code ignores `autoMemoryDirectory` in a checked-in
23
+ * `.claude/settings.json`, paddock's SDK agents never load the user source, and
24
+ * the `CLAUDE_COWORK_MEMORY_PATH_OVERRIDE` env alternative switches the write
25
+ * exemption off altogether.
26
+ *
27
+ * ## Which directory — deliberately the one Claude Code already picks
28
+ *
29
+ * This changes how the memory dir is SPELLED, never which one it is, so no
30
+ * existing memory moves. Claude Code keys memory on the canonical git root of
31
+ * its (realpath'd) cwd: the nearest ancestor holding a `.git`, or for a linked
32
+ * worktree the MAIN checkout it belongs to, and the cwd itself outside git.
33
+ * {@link memoryRoot} is a port of that filesystem walk, NOT a `git rev-parse`:
34
+ * git disagrees with it whenever git refuses or is absent (another uid's repo —
35
+ * "dubious ownership" —, no git binary, inherited `GIT_DIR`, git < 2.31 without
36
+ * `--path-format`), and every disagreement silently re-keys memory. Notebook
37
+ * projects inside one notes repo therefore keep sharing that repo's memory,
38
+ * exactly as before. Re-keying memory per project is a separate decision.
39
+ *
40
+ * ## The setting is a write grant, so it is contained
41
+ *
42
+ * Claude Code lets an agent write `*.md` under its memory dir without approval,
43
+ * including into protected paths like `~/.claude/…`. So the realpath is only
44
+ * accepted where paddock itself points that link: a real `projects/<enc>` dir in
45
+ * paddock's own home, a `.chats` store, or the user's `~/.claude/projects/<enc>`
46
+ * under `host`. A symlink planted elsewhere — at `memory`, or over a `.chats`
47
+ * store, by an agent with Bash or committed in a notes repo — could otherwise aim
48
+ * the grant anywhere. {@link resolveAutoMemoryDir} refuses those layouts and
49
+ * returns undefined, leaving the agent on Claude Code's default.
50
+ */
51
+ import { promises as fs } from "node:fs";
52
+ import path from "node:path";
53
+ import { encodePathForCli } from "@herdctl/core";
54
+ /** Does `<dir>/.git` exist as a directory or file (following a symlink)? */
55
+ async function hasGitEntry(dir) {
56
+ const st = await fs.stat(path.join(dir, ".git")).catch(() => null);
57
+ return !!st && (st.isDirectory() || st.isFile());
58
+ }
59
+ /** Is `p` itself a symlink (not followed)? */
60
+ async function isSymlink(p) {
61
+ const st = await fs.lstat(p).catch(() => null);
62
+ return !!st?.isSymbolicLink();
63
+ }
64
+ /** Read a small text file, or undefined. */
65
+ async function readText(p) {
66
+ return fs.readFile(p, "utf8").then((s) => s.trim(), () => undefined);
67
+ }
68
+ /**
69
+ * Claude Code's canonical-root rule for a `.git` FILE (a linked worktree): follow
70
+ * `gitdir:` to `<main>/.git/worktrees/<name>`, its `commondir` back to the main
71
+ * `.git`, and require the worktree's `gitdir` back-pointer to name this
72
+ * checkout. Anything that does not check out — a submodule, whose gitdir has no
73
+ * `commondir`, or a hand-edited file — keys on the checkout itself.
74
+ */
75
+ async function canonicalRoot(root) {
76
+ const dotGit = await readText(path.join(root, ".git"));
77
+ if (!dotGit?.startsWith("gitdir:"))
78
+ return root;
79
+ const gitDir = path.resolve(root, dotGit.slice("gitdir:".length).trim());
80
+ const common = await readText(path.join(gitDir, "commondir"));
81
+ if (!common)
82
+ return root;
83
+ const commonDir = path.resolve(gitDir, common);
84
+ if (path.dirname(gitDir) !== path.join(commonDir, "worktrees"))
85
+ return root;
86
+ const back = await readText(path.join(gitDir, "gitdir"));
87
+ if (!back)
88
+ return root;
89
+ const [backReal, rootReal] = await Promise.all([
90
+ realpathLenient(path.resolve(gitDir, back)),
91
+ realpathLenient(root),
92
+ ]);
93
+ if (backReal !== path.join(rootReal, ".git"))
94
+ return root;
95
+ if (path.basename(commonDir) === ".git")
96
+ return path.dirname(commonDir);
97
+ // A bare main repo (`commondir` not named `.git`) keys on the common dir
98
+ // itself — unless it in turn holds a `.git`, which Claude Code declines.
99
+ return (await hasGitEntry(commonDir)) ? root : commonDir;
100
+ }
101
+ /**
102
+ * The directory Claude Code keys auto-memory on for an agent started in
103
+ * `workingDir`: the canonical git root above it, else the cwd. Starts from the
104
+ * realpath, as Claude Code's own `process.cwd()` does. Filesystem only — no git
105
+ * subprocess, so nothing here can hang on, or be redirected by, git.
106
+ */
107
+ export async function memoryRoot(workingDir) {
108
+ const start = await realpathLenient(workingDir);
109
+ for (let dir = start;;) {
110
+ if (await hasGitEntry(dir))
111
+ return canonicalRoot(dir);
112
+ const parent = path.dirname(dir);
113
+ if (parent === dir)
114
+ return start;
115
+ dir = parent;
116
+ }
117
+ }
118
+ /**
119
+ * `fs.realpath` for a path that may not fully exist yet: resolve the deepest
120
+ * existing ancestor and re-append the rest. The memory dir itself is created
121
+ * lazily by Claude Code on first write, so it is usually missing at boot — but
122
+ * the symlink above it, which is the part that matters, is already planted.
123
+ */
124
+ export async function realpathLenient(p) {
125
+ const rest = [];
126
+ let cur = path.resolve(p);
127
+ for (;;) {
128
+ try {
129
+ return path.join(await fs.realpath(cur), ...rest);
130
+ }
131
+ catch {
132
+ const parent = path.dirname(cur);
133
+ if (parent === cur)
134
+ return path.resolve(p);
135
+ rest.unshift(path.basename(cur));
136
+ cur = parent;
137
+ }
138
+ }
139
+ }
140
+ /**
141
+ * The symlink-free auto-memory dir for an agent whose Claude home is
142
+ * `claudeHome` and whose cwd is `workingDir`, or undefined when the `memory`
143
+ * entry is itself a symlink (see the module doc). Call it AFTER every project's
144
+ * transcript symlink is planted — a notebook project's memory root is usually
145
+ * ANOTHER workspace's link (the notes repo's), not its own.
146
+ */
147
+ export async function resolveAutoMemoryDir(claudeHome, workingDir, userHome) {
148
+ const root = await memoryRoot(workingDir);
149
+ const enc = encodePathForCli(root);
150
+ const literal = path.join(claudeHome, "projects", enc);
151
+ const base = await realpathLenient(literal);
152
+ const expected =
153
+ // a real dir in paddock's own home: no link was followed
154
+ !(await isSymlink(literal)) ||
155
+ // a `.chats` store (`own`), or the user's own folder for it (`host`)
156
+ path.basename(base) === ".chats" ||
157
+ (!!userHome && base === path.join(await realpathLenient(userHome), "projects", enc));
158
+ if (!expected)
159
+ return undefined;
160
+ const memory = path.join(base, "memory");
161
+ const st = await fs.lstat(memory).catch(() => null);
162
+ if (st && !st.isDirectory())
163
+ return undefined;
164
+ return memory;
165
+ }
@@ -46,7 +46,7 @@ import { type PaddockTrigger } from "./trigger-config.js";
46
46
  * usable GPU and the devbox image's Mesa packages. Ignored for a `docker: true`
47
47
  * project (see {@link browserMcpServers}).
48
48
  */
49
- export declare function buildAgentConfig(cfg: PaddockConfig, project: Project, modelOverride?: string, mcpSources?: McpSources, hostPlugins?: HostPluginSource, browserGpuConfig?: string): Record<string, unknown> & {
49
+ export declare function buildAgentConfig(cfg: PaddockConfig, project: Project, modelOverride?: string, mcpSources?: McpSources, hostPlugins?: HostPluginSource, browserGpuConfig?: string, autoMemoryDir?: string): Record<string, unknown> & {
50
50
  name: string;
51
51
  };
52
52
  /**
@@ -109,7 +109,7 @@ export declare function buildSweeperConfig(cfg: PaddockConfig, project: Project)
109
109
  * herdctl does not enforce as a deny-all; see {@link triggerToAgentToolConfig} (#647).
110
110
  * `browserGpuConfig` is as for {@link buildAgentConfig}.
111
111
  */
112
- export declare function buildTriggerConfig(cfg: PaddockConfig, project: Project, triggerName: string, trigger: PaddockTrigger, browserGpuConfig?: string): Record<string, unknown> & {
112
+ export declare function buildTriggerConfig(cfg: PaddockConfig, project: Project, triggerName: string, trigger: PaddockTrigger, browserGpuConfig?: string, autoMemoryDir?: string): Record<string, unknown> & {
113
113
  name: string;
114
114
  };
115
115
  /**
@@ -61,7 +61,7 @@ import { keeperAgentName, sweeperAgentName, triggerAgentName, browserMcpServers,
61
61
  * usable GPU and the devbox image's Mesa packages. Ignored for a `docker: true`
62
62
  * project (see {@link browserMcpServers}).
63
63
  */
64
- export function buildAgentConfig(cfg, project, modelOverride, mcpSources = EMPTY_MCP_SOURCES, hostPlugins = EMPTY_HOST_PLUGINS, browserGpuConfig) {
64
+ export function buildAgentConfig(cfg, project, modelOverride, mcpSources = EMPTY_MCP_SOURCES, hostPlugins = EMPTY_HOST_PLUGINS, browserGpuConfig, autoMemoryDir) {
65
65
  const config = {
66
66
  name: keeperAgentName(project.slug),
67
67
  description: project.summary || `Claude Code agent for project ${project.name}.`,
@@ -103,6 +103,7 @@ export function buildAgentConfig(cfg, project, modelOverride, mcpSources = EMPTY
103
103
  // leaves it off keeps inheriting the fleet default (no Docker) unchanged.
104
104
  if (project.docker)
105
105
  config.docker = { enabled: true };
106
+ applyAutoMemoryDir(config, project, autoMemoryDir);
106
107
  // Native by default: omit the replace prompt so the default coding prompt +
107
108
  // CLAUDE.md hierarchy apply (issue #176). Only a non-native instance
108
109
  // (PADDOCK_NATIVE_PROMPT=false) gets the terse replace prompt.
@@ -303,7 +304,7 @@ export function buildSweeperConfig(cfg, project) {
303
304
  * herdctl does not enforce as a deny-all; see {@link triggerToAgentToolConfig} (#647).
304
305
  * `browserGpuConfig` is as for {@link buildAgentConfig}.
305
306
  */
306
- export function buildTriggerConfig(cfg, project, triggerName, trigger, browserGpuConfig) {
307
+ export function buildTriggerConfig(cfg, project, triggerName, trigger, browserGpuConfig, autoMemoryDir) {
307
308
  const config = {
308
309
  name: triggerAgentName(project.slug, triggerName),
309
310
  description: `Trigger "${triggerName}" (${trigger.trigger.type}) for project ${project.name}.`,
@@ -323,11 +324,26 @@ export function buildTriggerConfig(cfg, project, triggerName, trigger, browserGp
323
324
  };
324
325
  if (project.docker)
325
326
  config.docker = { enabled: true };
327
+ applyAutoMemoryDir(config, project, autoMemoryDir);
326
328
  const browser = browserMcpServers(cfg.browserMcp, project.docker ? undefined : browserGpuConfig);
327
329
  if (browser)
328
330
  config.mcp_servers = browser;
329
331
  return config;
330
332
  }
333
+ /**
334
+ * Hand Claude Code the symlink-free memory dir as a flag-tier setting (#955) —
335
+ * see `auto-memory.ts` for why the default spelling, which runs through
336
+ * paddock's transcript symlink, gets every memory write denied.
337
+ *
338
+ * Skipped for a `docker: true` project: the path is resolved on the host, and
339
+ * inside the container it would name a directory that is not there. Without it
340
+ * the agent keeps Claude Code's default, which is today's behaviour.
341
+ */
342
+ function applyAutoMemoryDir(config, project, autoMemoryDir) {
343
+ if (!autoMemoryDir || project.docker)
344
+ return;
345
+ config.settings = { autoMemoryDirectory: autoMemoryDir };
346
+ }
331
347
  /**
332
348
  * Write the minimal herdctl.yaml the FleetManager boots from: a fleet block
333
349
  * plus the fleet-wide `defaults` (deep-merged into agents added at runtime),
@@ -192,6 +192,14 @@ export declare class HerdctlService {
192
192
  * fix's residue gate — see {@link residueDepthFor} + `./resume-drain.ts`).
193
193
  */
194
194
  private agentWorkingDirs;
195
+ /**
196
+ * The symlink-free auto-memory dir for each working directory (#955), keyed
197
+ * by `workingDir`. Resolved asynchronously in {@link refreshAutoMemoryDir}
198
+ * once the transcript symlink is planted, so the synchronous config builders
199
+ * can read it — including the re-registrations ({@link ensureAgentModel},
200
+ * {@link ensureTriggerAgent}) that do not re-plant anything.
201
+ */
202
+ private autoMemoryDirs;
195
203
  /**
196
204
  * Each agent's `.chats` HOST dir — `project.dir`, which for a repo-backed
197
205
  * project is NOT `workingDir` (#187). Recorded so {@link deleteSession} can ask
@@ -335,6 +343,16 @@ export declare class HerdctlService {
335
343
  * included. No per-agent yaml files; no `reload()`.
336
344
  */
337
345
  init(projects: Project[]): Promise<void>;
346
+ /**
347
+ * Boot-time registration, split out of {@link init} so it can be driven
348
+ * against a fake fleet. TWO passes, and the split is load-bearing (#955):
349
+ * every transcript symlink is planted before any memory dir is resolved,
350
+ * because a notebook project's memory root is usually ANOTHER workspace's
351
+ * link — the notes repo's, i.e. the root workspace's, which is registered
352
+ * last. Resolving in one pass cached the pre-symlink path on a first boot,
353
+ * and the previous mode's target on the boot after a `transcripts` flip.
354
+ */
355
+ registerProjects(projects: Project[]): Promise<void>;
338
356
  /** Start the scheduler (keeper agents currently have no schedules, but this
339
357
  * keeps the fleet "running" for status + future cron curators). */
340
358
  start(): Promise<void>;
@@ -1015,6 +1033,13 @@ export declare class HerdctlService {
1015
1033
  private commandsCache;
1016
1034
  listCommands(agentName: string): Promise<SlashCommand[]>;
1017
1035
  private keeperAgentConfig;
1036
+ /**
1037
+ * Resolve and record the project's auto-memory dir (#955). Must run AFTER
1038
+ * {@link ensureChats}: it follows the transcript symlink that call plants.
1039
+ * Re-run on every registration, so a working-directory change (#953) or a
1040
+ * `transcripts` mode switch picks up the new target.
1041
+ */
1042
+ private refreshAutoMemoryDir;
1018
1043
  private sweeperAgentConfig;
1019
1044
  /**
1020
1045
  * Create the sweeper's own working directory (+ symlink its encoded transcript
@@ -57,6 +57,7 @@ import { chatsDirIsPlanted, ensureProjectChats, projectChatsDir, } from "./trans
57
57
  import { triggerRunsOnOwnAgent, isCuratorTrigger, } from "./trigger-config.js";
58
58
  import { buildAgentConfig, buildSweeperConfig, sweeperWorkingDir, buildTriggerConfig, ensureConfigFile as writeBootConfigFile, } from "./herdctl-agent-config.js";
59
59
  import { EMPTY_MCP_SOURCES } from "./claude-mcp.js";
60
+ import { resolveAutoMemoryDir } from "./auto-memory.js";
60
61
  import { EMPTY_HOST_PLUGINS } from "./claude-plugins.js";
61
62
  import * as jobs from "./herdctl-jobs.js";
62
63
  import { JobsDirIndex } from "./herdctl-jobs-index.js";
@@ -255,6 +256,14 @@ export class HerdctlService {
255
256
  * fix's residue gate — see {@link residueDepthFor} + `./resume-drain.ts`).
256
257
  */
257
258
  agentWorkingDirs = new Map();
259
+ /**
260
+ * The symlink-free auto-memory dir for each working directory (#955), keyed
261
+ * by `workingDir`. Resolved asynchronously in {@link refreshAutoMemoryDir}
262
+ * once the transcript symlink is planted, so the synchronous config builders
263
+ * can read it — including the re-registrations ({@link ensureAgentModel},
264
+ * {@link ensureTriggerAgent}) that do not re-plant anything.
265
+ */
266
+ autoMemoryDirs = new Map();
258
267
  /**
259
268
  * Each agent's `.chats` HOST dir — `project.dir`, which for a repo-backed
260
269
  * project is NOT `workingDir` (#187). Recorded so {@link deleteSession} can ask
@@ -433,8 +442,20 @@ export class HerdctlService {
433
442
  claudeHomePath: this.cfg.claudeHome,
434
443
  });
435
444
  await this.fleet.initialize();
436
- // Register a keeper + sweeper for every existing project, recording each
437
- // keeper's resolved model so per-chat overrides can short-circuit later.
445
+ await this.registerProjects(projects);
446
+ }
447
+ /**
448
+ * Boot-time registration, split out of {@link init} so it can be driven
449
+ * against a fake fleet. TWO passes, and the split is load-bearing (#955):
450
+ * every transcript symlink is planted before any memory dir is resolved,
451
+ * because a notebook project's memory root is usually ANOTHER workspace's
452
+ * link — the notes repo's, i.e. the root workspace's, which is registered
453
+ * last. Resolving in one pass cached the pre-symlink path on a first boot,
454
+ * and the previous mode's target on the boot after a `transcripts` flip.
455
+ */
456
+ async registerProjects(projects) {
457
+ if (!this.fleet)
458
+ return;
438
459
  // ensureProjectChats relocates this project's transcripts into <dir>/.chats
439
460
  // (migrating any existing real transcript dir) so the project is portable.
440
461
  for (const project of projects) {
@@ -444,6 +465,11 @@ export class HerdctlService {
444
465
  // project workingDir === dir, so this is the classic behavior.
445
466
  await this.ensureChats(project.workingDir, project.dir);
446
467
  await this.ensureSweeperHome(project);
468
+ }
469
+ // Register a keeper + sweeper for every existing project, recording each
470
+ // keeper's resolved model so per-chat overrides can short-circuit later.
471
+ for (const project of projects) {
472
+ await this.refreshAutoMemoryDir(project);
447
473
  await this.fleet.addAgent(this.keeperAgentConfig(project), { replace: true });
448
474
  await this.fleet.addAgent(this.sweeperAgentConfig(project), { replace: true });
449
475
  // Register each EVENT trigger as its own agent `trigger-<slug>-<name>` (Epic T /
@@ -519,6 +545,7 @@ export class HerdctlService {
519
545
  return;
520
546
  await this.ensureChats(project.workingDir, project.dir);
521
547
  await this.ensureSweeperHome(project);
548
+ await this.refreshAutoMemoryDir(project);
522
549
  await this.fleet.addAgent(this.keeperAgentConfig(project), { replace: true });
523
550
  await this.fleet.addAgent(this.sweeperAgentConfig(project), { replace: true });
524
551
  // Re-register the project's EVENT-trigger agents (Epic T / T1) from the live
@@ -1864,7 +1891,20 @@ export class HerdctlService {
1864
1891
  // existing unit-test seams (which reach `sweeperAgentConfig`/`ensureConfigFile`
1865
1892
  // via a cast) and internal callers keep working.
1866
1893
  keeperAgentConfig(project, modelOverride) {
1867
- return buildAgentConfig(this.cfg, project, modelOverride, this.mcpSources, this.hostPlugins, this.browserGpuConfig);
1894
+ return buildAgentConfig(this.cfg, project, modelOverride, this.mcpSources, this.hostPlugins, this.browserGpuConfig, this.autoMemoryDirs.get(project.workingDir));
1895
+ }
1896
+ /**
1897
+ * Resolve and record the project's auto-memory dir (#955). Must run AFTER
1898
+ * {@link ensureChats}: it follows the transcript symlink that call plants.
1899
+ * Re-run on every registration, so a working-directory change (#953) or a
1900
+ * `transcripts` mode switch picks up the new target.
1901
+ */
1902
+ async refreshAutoMemoryDir(project) {
1903
+ const dir = await resolveAutoMemoryDir(this.cfg.claudeHome, project.workingDir, this.cfg.legacyClaudeHome);
1904
+ if (dir)
1905
+ this.autoMemoryDirs.set(project.workingDir, dir);
1906
+ else
1907
+ this.autoMemoryDirs.delete(project.workingDir);
1868
1908
  }
1869
1909
  sweeperAgentConfig(project) {
1870
1910
  return buildSweeperConfig(this.cfg, project);
@@ -1894,7 +1934,7 @@ export class HerdctlService {
1894
1934
  this.unplantedChatsLinks.push(unplanted);
1895
1935
  }
1896
1936
  triggerAgentConfig(project, triggerName, trigger) {
1897
- return buildTriggerConfig(this.cfg, project, triggerName, trigger, this.browserGpuConfig);
1937
+ return buildTriggerConfig(this.cfg, project, triggerName, trigger, this.browserGpuConfig, this.autoMemoryDirs.get(project.workingDir));
1898
1938
  }
1899
1939
  async ensureConfigFile() {
1900
1940
  await writeBootConfigFile(this.cfg);
@@ -22,14 +22,20 @@
22
22
  *
23
23
  * The obvious implementation of "share the user's transcripts" is to point
24
24
  * `CLAUDE_CONFIG_DIR` at `~/.claude`, which is what 0.61.1 did. It drags memory,
25
- * `.claude.json` and the credential-store service name along as collateral — and
26
- * it breaks agent memory (#690), because the agent harness refuses to write to any
27
- * path containing a `.claude` component and the memory dir is
28
- * `<claudeHome>/projects/<enc>/memory`. Keeping paddock's own home and pointing one
29
- * symlink outward fixes that by construction: the LITERAL path handed to the agent
30
- * is always under `<dataDir>/claude-home/…`, which has no `.claude` component,
31
- * while the files it resolves to are the user's. Pinned by
32
- * `test/unit/transcripts.test.ts` in BOTH modes — do not "simplify" this back.
25
+ * `.claude.json` and the credential-store service name along as collateral
26
+ * (#690). Keeping paddock's own home and pointing one symlink outward moves only
27
+ * the transcripts. Pinned by `test/unit/transcripts.test.ts` in BOTH modes — do
28
+ * not "simplify" this back.
29
+ *
30
+ * This comment used to claim the outward symlink also kept agent memory writable
31
+ * "by construction", because the harness refuses any path with a `.claude`
32
+ * component. Neither half holds (#955). A memory write is exempt from approval
33
+ * when its path, after resolving symlinks, is under the configured memory dir —
34
+ * `.claude` or not. ANY symlink in that path, this one in either mode, makes the
35
+ * two disagree, and under `host` the write then lands in the user's `~/.claude`
36
+ * and is denied. `auto-memory.ts` closes it by handing the agent the resolved
37
+ * path as `autoMemoryDirectory`; `test/integration/auto-memory-write.test.ts`
38
+ * proves the write with the real binary, into a `.claude/projects/…/memory`.
33
39
  *
34
40
  * `ensureProjectChats` is idempotent and self-healing: on first run for a project
35
41
  * whose encoded path is still a real directory (existing transcripts), it migrates
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paddock/server",
3
- "version": "0.78.1",
3
+ "version": "0.78.2",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -1,2 +1,2 @@
1
- import{r as l,e as k,j as s,A as y,d as p,a as w}from"./index-Ba5H3P_n.js";import{c as N}from"./lastTab-CU1zsF0j.js";function v({open:e,title:t,message:c,confirmLabel:n="Delete",cancelLabel:o="Cancel",danger:a=!0,wide:h=!1,dismissOnBackdrop:b=!0,onConfirm:g,onClose:d}){const[r,m]=l.useState(!1),[f,u]=l.useState(null);if(l.useEffect(()=>{e?u(null):m(!1)},[e]),k(e&&!r,d),!e)return null;const j=async()=>{m(!0),u(null);try{await g()}catch(i){u(i instanceof Error?i.message:"Action failed"),m(!1)}};return s.jsx("div",{className:"fixed inset-0 z-[60] flex items-center justify-center bg-overlay p-4 backdrop-blur-sm",onClick:()=>b&&!r&&d(),children:s.jsxs("div",{className:`w-full ${h?"max-w-md":"max-w-sm"} animate-scale-in rounded-2xl border border-edge bg-surface-raised p-6 shadow-2xl`,onClick:i=>i.stopPropagation(),role:"alertdialog","aria-modal":"true",children:[s.jsxs("div",{className:"flex items-start gap-3",children:[a&&s.jsx("span",{className:"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-danger-soft text-danger",children:s.jsx(y,{width:18,height:18})}),s.jsxs("div",{className:"min-w-0",children:[s.jsx("h2",{className:"text-base font-semibold",children:t}),s.jsx("div",{className:"mt-1.5 text-sm text-fg-muted",children:c})]})]}),f&&s.jsx("p",{className:"mt-4 rounded-lg bg-danger-soft px-3 py-2 text-sm text-danger",children:f}),s.jsxs("div",{className:"mt-5 flex justify-end gap-2",children:[s.jsx("button",{type:"button",className:"btn-ghost",onClick:d,disabled:r,children:o}),s.jsx("button",{type:"button",className:a?"btn btn-danger":"btn-primary",onClick:j,disabled:r,children:r?"Working…":n})]})]})})}function P(e,t){return e===t?!0:e.startsWith(t.endsWith("/")?t:`${t}/`)}function x(e){return!P(e.workingDir,e.dir)}function C({project:e}){const t=s.jsx("span",{className:"font-medium text-fg",children:e.name});return x(e)?s.jsxs(s.Fragment,{children:[t," will be unlinked. Paddock’s notes, settings and chats for it are permanently removed, and this cannot be undone — but the directory it works in is left alone:",s.jsx("span",{className:"mt-1.5 block break-all font-mono text-xs text-fg",children:e.workingDir}),s.jsx("span",{className:"mt-1.5 block",children:"Nothing in there is deleted."})]}):s.jsxs(s.Fragment,{children:[t," and all its chats and files will be permanently removed. This cannot be undone."]})}function D(){const{remove:e}=p();return l.useCallback(async t=>{await w.deleteProject(t),e(t),N(t)},[e])}function W({project:e,open:t,onClose:c,onDeleted:n}){const o=D(),a=x(e);return s.jsx(v,{open:t,title:"Delete project?",message:s.jsx(C,{project:e}),confirmLabel:"Delete project",wide:a,dismissOnBackdrop:!a,onConfirm:async()=>{await o(e.slug),n==null||n()},onClose:c})}export{v as C,W as D,x as d};
1
+ import{r as l,e as k,j as s,A as y,d as p,a as w}from"./index-BYqBTUv_.js";import{c as N}from"./lastTab-CU1zsF0j.js";function v({open:e,title:t,message:c,confirmLabel:n="Delete",cancelLabel:o="Cancel",danger:a=!0,wide:h=!1,dismissOnBackdrop:b=!0,onConfirm:g,onClose:d}){const[r,m]=l.useState(!1),[f,u]=l.useState(null);if(l.useEffect(()=>{e?u(null):m(!1)},[e]),k(e&&!r,d),!e)return null;const j=async()=>{m(!0),u(null);try{await g()}catch(i){u(i instanceof Error?i.message:"Action failed"),m(!1)}};return s.jsx("div",{className:"fixed inset-0 z-[60] flex items-center justify-center bg-overlay p-4 backdrop-blur-sm",onClick:()=>b&&!r&&d(),children:s.jsxs("div",{className:`w-full ${h?"max-w-md":"max-w-sm"} animate-scale-in rounded-2xl border border-edge bg-surface-raised p-6 shadow-2xl`,onClick:i=>i.stopPropagation(),role:"alertdialog","aria-modal":"true",children:[s.jsxs("div",{className:"flex items-start gap-3",children:[a&&s.jsx("span",{className:"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-danger-soft text-danger",children:s.jsx(y,{width:18,height:18})}),s.jsxs("div",{className:"min-w-0",children:[s.jsx("h2",{className:"text-base font-semibold",children:t}),s.jsx("div",{className:"mt-1.5 text-sm text-fg-muted",children:c})]})]}),f&&s.jsx("p",{className:"mt-4 rounded-lg bg-danger-soft px-3 py-2 text-sm text-danger",children:f}),s.jsxs("div",{className:"mt-5 flex justify-end gap-2",children:[s.jsx("button",{type:"button",className:"btn-ghost",onClick:d,disabled:r,children:o}),s.jsx("button",{type:"button",className:a?"btn btn-danger":"btn-primary",onClick:j,disabled:r,children:r?"Working…":n})]})]})})}function P(e,t){return e===t?!0:e.startsWith(t.endsWith("/")?t:`${t}/`)}function x(e){return!P(e.workingDir,e.dir)}function C({project:e}){const t=s.jsx("span",{className:"font-medium text-fg",children:e.name});return x(e)?s.jsxs(s.Fragment,{children:[t," will be unlinked. Paddock’s notes, settings and chats for it are permanently removed, and this cannot be undone — but the directory it works in is left alone:",s.jsx("span",{className:"mt-1.5 block break-all font-mono text-xs text-fg",children:e.workingDir}),s.jsx("span",{className:"mt-1.5 block",children:"Nothing in there is deleted."})]}):s.jsxs(s.Fragment,{children:[t," and all its chats and files will be permanently removed. This cannot be undone."]})}function D(){const{remove:e}=p();return l.useCallback(async t=>{await w.deleteProject(t),e(t),N(t)},[e])}function W({project:e,open:t,onClose:c,onDeleted:n}){const o=D(),a=x(e);return s.jsx(v,{open:t,title:"Delete project?",message:s.jsx(C,{project:e}),confirmLabel:"Delete project",wide:a,dismissOnBackdrop:!a,onConfirm:async()=>{await o(e.slug),n==null||n()},onClose:c})}export{v as C,W as D,x as d};
2
2
 
@@ -0,0 +1,2 @@
1
+ import{j as o}from"./index-BYqBTUv_.js";import{D as r}from"./DiscoverView-DE65Ui9u.js";import{u as e}from"./documentTitle-cxKpkGjk.js";import"./Surfaces-CTGs5AAK.js";function p(){return e(["Discover"]),o.jsx(r,{})}export{p as DiscoverPage};
2
+
@@ -1,2 +1,2 @@
1
- import{h as oe,d as ie,a0 as de,r as u,a as A,j as s,V as W,A as X,a4 as Z,I as $,a1 as ce,Q as le,i as he,a5 as ue,a6 as J,J as T,c as K,a7 as pe,t as xe,Y as Q}from"./index-Ba5H3P_n.js";import{E as R,C as me}from"./Surfaces-DPLI5OkT.js";function ge(e){const t={};for(const n of e)t[n.path]={checked:!0,expanded:!1,loading:!1,error:null};return t}function fe(e){if(!e.sessions||!e.selected)return e.checked?"on":"off";const t=e.sessions.sessions.length,n=e.selected.size;return t===0||n===0?"off":n===t?"on":"mixed"}function C(e,t){return e.sessions&&e.selected?e.selected.size:e.checked?t.sessionCount:0}function je(e,t){return t.reduce((n,r)=>n+C(e[r.path]??O(),r),0)}function be(e,t){return t.filter(n=>C(e[n.path]??O(),n)>0)}function we(e,t,n){const r=e[t];if(!r)return e;const a={...r,checked:n};return r.sessions&&(a.selected=n?new Set(r.sessions.sessions.map(c=>c.sessionId)):new Set),{...e,[t]:a}}function ye(e,t,n){const r=e[t];if(!r||!r.sessions)return e;const a=new Set(r.selected??[]);return a.delete(n)||a.add(n),{...e,[t]:{...r,selected:a,checked:a.size>0}}}function ve(e,t,n){const r=e[t];if(!r)return e;const a=r.checked?new Set(n.sessions.map(c=>c.sessionId)):new Set;return{...e,[t]:{...r,sessions:n,selected:a,loading:!1,error:null}}}function D(e,t,n){const r=e[t];return r?{...e,[t]:{...r,...n}}:e}function Ne(e,t){const n=e??O();return C(n,t)===0?null:n.sessions&&n.selected?{sessionIds:[...n.selected]}:{}}function O(){return{checked:!1,expanded:!1,loading:!1,error:null}}function ke(e){return e.error!==void 0?e.stage==="create"?"danger":"warn":e.adopted>0?"success":"warn"}async function Se(e,t,n){let r;try{r=await e.createProject({name:t.name,slug:t.suggestedSlug,path:t.path,managed:!1})}catch(a){return{stage:"create",adopted:0,skipped:[],error:_(a)}}try{const a=await e.adoptChats(r.slug,{sourceCwd:r.workingDir,...n.sessionIds?{sessionIds:n.sessionIds}:{}});return{project:r,adopted:a.adopted.length,skipped:a.skipped}}catch(a){return{stage:"import",project:r,adopted:0,skipped:[],error:_(a)}}}async function $e(e,t,n){const r=[];for(const a of t){n(a.candidate.path,"running");const c=await Se(e,a.candidate,a.plan);r.push(c),n(a.candidate.path,"done",c)}return r}const Ce={sidechain:"sub-agent transcript","already-adopted":"already adopted","destination-exists":"a chat with that id already exists here","attributed-to-run":"belongs to a Paddock run",unreadable:"transcript unreadable","placement-failed":"copy failed","record-failed":"could not be recorded"};function Y(e){if(e.length===0)return"";const t=[...new Set(e.map(n=>Ce[n.reason]??n.reason))];return`${e.length} skipped (${t.join(", ")})`}function Ie(e,t){var l;if(e.stage==="create")return`Couldn't create the project: ${e.error}`;const n=((l=e.project)==null?void 0:l.slug)??t.suggestedSlug;if(e.stage==="import")return`Project “${n}” was created, but importing its chats failed: ${e.error}. It is there and empty — you can import them from the project itself.`;if(e.adopted===0){const m=Y(e.skipped);return t.recordedPath!==void 0?`Project “${n}” was created but no chats came with it. Its transcripts record ${t.recordedPath}, a different spelling of ${t.path}, and an import matches on the exact path.`:m?`Project “${n}” was created but no chats came with it — ${m}.`:`Project “${n}” was created but no chats came with it.`}const r=e.adopted,a=Y(e.skipped),c=`Adopted ${r} chat${r===1?"":"s"} into “${n}”`;return a?`${c} — ${a}.`:`${c}.`}function _(e){return e instanceof Error?e.message:String(e)}function Ge({firstRun:e=!1,embedded:t=!1,onLeave:n,onStartChat:r}){const a=oe(),{refresh:c}=ie(),l=de(),[m,d]=u.useState(!1),[j,I]=u.useState(!1),[h,x]=u.useState(null),[y,z]=u.useState(null),[F,P]=u.useState(!0),[f,v]=u.useState({}),[k,G]=u.useState(!1),[L,H]=u.useState({}),[E,B]=u.useState({}),[S,ee]=u.useState(!1);u.useEffect(()=>{let o=!0;return P(!0),z(null),A.discover({includeNonGit:m,includeOutsideHome:j}).then(i=>{o&&(x(i),v(ge(i.candidates)),P(!1))}).catch(i=>{o&&(z(i instanceof Error?i.message:"Discovery failed"),P(!1))}),()=>{o=!1}},[m,j]);const w=(h==null?void 0:h.candidates)??[],M=u.useMemo(()=>je(f,w),[f,w]),b=u.useMemo(()=>be(f,w),[f,w]),se=u.useCallback(o=>{v(p=>{const g=p[o.path];if(!g)return p;const N=D(p,o.path,{expanded:!g.expanded});return g.expanded||g.sessions||g.loading?N:D(N,o.path,{loading:!0,error:null})});const i=f[o.path];!i||i.expanded||i.sessions||i.loading||A.discoverSessions(o.path).then(p=>v(g=>ve(g,o.path,p))).catch(p=>v(g=>D(g,o.path,{loading:!1,error:p instanceof Error?p.message:"Could not read this directory's sessions"})))},[f]),te=u.useCallback(async()=>{if(k)return;G(!0),H(Object.fromEntries(b.map(i=>[i.path,"pending"]))),B({});const o=b.map(i=>({candidate:i,plan:Ne(f[i.path],i)??{}}));await $e(A,o,(i,p,g)=>{H(N=>({...N,[i]:p})),g&&B(N=>({...N,[i]:g}))}),G(!1),ee(!0),await c()},[b,f,k,c]),ne=u.useCallback(async()=>{await c(),n==null||n(),a("/")},[a,c,n]),ae=Object.values(E).reduce((o,i)=>o+i.adopted,0),re=Object.values(E).filter(o=>o.project).length,V=s.jsxs(s.Fragment,{children:[s.jsx("p",{className:"mb-5 text-sm text-fg-muted",children:e?"If you have used Claude Code in a terminal on this machine, Paddock can adopt that history: each directory becomes a project with its conversations already in it.":"Directories on this machine with existing Claude Code history. Adopting one links it as a project and brings its conversations in; your own history is never moved or deleted."}),S&&s.jsx(Te,{imported:ae,projects:re,onGetStarted:ne}),y!==null&&s.jsx(W,{tone:"danger",icon:s.jsx(X,{width:16,height:16}),children:y}),F&&s.jsx(De,{}),!F&&y===null&&h!==null&&s.jsxs(s.Fragment,{children:[h.scanned===0?s.jsx(Ae,{claudeHome:h.claudeHome,onStartChat:r}):w.length===0?s.jsx(R,{variant:"panel",icon:s.jsx(Z,{width:22,height:22}),title:"Nothing to adopt",body:"Paddock found Claude Code history on this machine, but none of it is available to adopt right now. The tally below says what was skipped, and the two toggles relax the rules that are safe to relax."}):s.jsx("ul",{className:"space-y-2",children:w.map(o=>s.jsx(Pe,{candidate:o,row:f[o.path],status:L[o.path],outcome:E[o.path],locked:k||S,onToggleRow:i=>v(p=>we(p,o.path,i)),onToggleSession:i=>v(p=>ye(p,o.path,i)),onToggleExpanded:()=>se(o)},o.path))}),!S&&h.scanned>0&&s.jsx(Ee,{result:h,includeNonGit:m,includeOutsideHome:j,disabled:k,onIncludeNonGit:d,onIncludeOutsideHome:I}),w.length>0&&!S&&s.jsxs("div",{className:"mt-5 flex items-center justify-between gap-3 border-t border-edge pt-4",children:[s.jsxs("span",{className:"text-sm text-fg-muted",children:[M," conversation",M===1?"":"s"," from ",b.length," director",b.length===1?"y":"ies"]}),s.jsxs($,{variant:"primary",onClick:()=>void te(),disabled:b.length===0,loading:k,loadingLabel:"Adopting…",children:["Adopt ",b.length," project",b.length===1?"":"s"]})]})]})]});return t?V:s.jsxs("div",{className:"flex h-full min-h-0 flex-col",children:[s.jsxs("header",{className:"pt-safe flex items-center gap-2 border-b border-edge px-3 pb-2.5 sm:px-6 lg:py-4",children:[s.jsx("button",{type:"button",onClick:()=>l==null?void 0:l.openNav(),"aria-label":"Open menu",className:"btn-subtle -ml-1 shrink-0 px-2 py-1.5 lg:hidden",children:s.jsx(ce,{width:20,height:20})}),s.jsx(le,{width:18,height:18,className:"shrink-0 text-fg-subtle"}),s.jsx("h1",{className:"text-md font-semibold tracking-tight",children:"Discover"})]}),s.jsx("div",{className:"flex-1 overflow-y-auto overscroll-contain",children:s.jsx("div",{className:"mx-auto max-w-3xl px-6 py-6",children:V})})]})}function Pe({candidate:e,row:t,status:n,outcome:r,locked:a,onToggleRow:c,onToggleSession:l,onToggleExpanded:m}){const d=t??{checked:!1,expanded:!1,loading:!1,error:null},j=fe(d),I=C(d,e),h=r?ke(r):null;return s.jsx("li",{children:s.jsxs(me,{flush:!0,className:K("overflow-hidden",h==="success"&&"border-success-edge bg-success-soft",h==="warn"&&"border-warn-edge bg-warn-soft",h==="danger"&&"border-danger-edge bg-danger-soft",n==="running"&&"border-accent-edge"),"data-testid":`discover-row-${e.suggestedSlug}`,"data-tone":h??void 0,children:[s.jsxs("div",{className:"flex items-start gap-3 px-3 py-2.5",children:[s.jsx(J,{className:"mt-1",checked:j==="on",indeterminate:j==="mixed",disabled:a,"aria-label":`Adopt ${e.path}`,onChange:x=>c(x.target.checked)}),s.jsxs("div",{className:"min-w-0 flex-1",children:[s.jsx("code",{className:"block break-all text-sm text-fg",children:e.path}),s.jsxs("div",{className:"mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-fg-muted",children:[s.jsxs("span",{className:"tabular",children:[e.sessionCount," conversation",e.sessionCount===1?"":"s"]}),e.filteredCount>0&&s.jsxs("span",{className:"tabular",title:"Empty, slash-command-only or sweeper transcripts, withheld as noise",children:["· ",e.filteredCount," filtered out"]}),e.lastSessionAt!==void 0&&s.jsxs("span",{className:"tabular",children:["· last ",U(e.lastSessionAt)]}),e.gitRemote!==void 0&&s.jsx(T,{tone:"lineage",size:"sm",children:e.gitRemote}),!e.hasGit&&s.jsx(T,{tone:"neutral",size:"sm",children:"no git"}),!e.insideHome&&s.jsx(T,{tone:"neutral",size:"sm",children:"outside home"})]}),e.recordedPath!==void 0&&s.jsx(W,{tone:"warn",className:"mt-2",icon:s.jsx(X,{width:14,height:14}),children:s.jsxs("span",{className:"text-xs",children:["These transcripts record"," ",s.jsx("code",{className:"break-all",children:e.recordedPath})," — a different spelling of this path. The import matches on the exact path, so it may bring nothing in."]})}),r&&s.jsx("p",{className:K("mt-2 text-xs",h==="success"&&"text-success",h==="warn"&&"text-warn",h==="danger"&&"text-danger"),children:Ie(r,e)}),n==="running"&&!r&&s.jsx("p",{className:"mt-2 text-xs text-fg-muted",children:"Adopting…"})]}),s.jsxs("span",{className:"flex shrink-0 items-center gap-1",children:[s.jsx("span",{className:"tabular text-xs text-fg-subtle",children:I}),s.jsx($,{size:"icon-sm",variant:"subtle","aria-label":d.expanded?"Hide conversations":"Show conversations","aria-expanded":d.expanded,onClick:m,children:d.expanded?s.jsx(pe,{width:14,height:14}):s.jsx(xe,{width:14,height:14})})]})]}),d.expanded&&s.jsxs("div",{className:"border-t border-edge px-3 py-2",children:[d.loading&&s.jsx("p",{className:"py-2 text-xs text-fg-muted",children:"Reading transcripts…"}),d.error!==null&&s.jsx("p",{className:"py-2 text-xs text-danger",children:d.error}),d.sessions&&d.sessions.sessions.length===0&&s.jsx("p",{className:"py-2 text-xs text-fg-muted",children:"Nothing to adopt here any more — it may have been adopted since the scan."}),d.sessions&&d.sessions.sessions.length>0&&s.jsx("ul",{className:"space-y-1",children:d.sessions.sessions.map(x=>{var y;return s.jsx("li",{children:s.jsxs("label",{className:"flex cursor-pointer items-start gap-2 rounded-lg px-2 py-1.5 transition-colors hover:bg-surface-hover",children:[s.jsx(J,{checked:((y=d.selected)==null?void 0:y.has(x.sessionId))??!1,disabled:a,"aria-label":q(x),onChange:()=>l(x.sessionId)}),s.jsxs("span",{className:"min-w-0 flex-1",children:[s.jsx("span",{className:"block truncate text-xs text-fg",children:q(x)}),s.jsxs("span",{className:"tabular mt-0.5 block text-3xs text-fg-muted",children:[U(x.mtime)," · ",Oe(x.sizeBytes)]})]})]})},x.sessionId)})}),d.sessions&&d.sessions.filtered.length>0&&s.jsxs("p",{className:"mt-2 px-2 text-3xs text-fg-subtle",children:[d.sessions.filtered.length," more withheld as noise (",[...new Set(d.sessions.filtered.map(x=>x.reason))].join(", "),")."]})]})]})})}function Ee({result:e,includeNonGit:t,includeOutsideHome:n,disabled:r,onIncludeNonGit:a,onIncludeOutsideHome:c}){const l=e.excluded["no-git"]??0,m=e.excluded["outside-home"]??0,d=l>0||t,j=m>0||n;return e.scanned===0?null:s.jsxs("div",{className:"mt-5 space-y-2 border-t border-edge pt-4",children:[s.jsx("p",{className:"text-xs text-fg-muted",children:Re(e)}),d&&s.jsxs("div",{className:"flex items-center gap-2 text-xs text-fg-muted",children:[s.jsx(Q,{checked:t,disabled:r,onChange:a,label:"Also offer directories without a git repository"}),s.jsxs("span",{children:["Also offer directories without a git repository",l>0&&` (${l} hidden)`]})]}),j&&s.jsxs("div",{className:"flex items-center gap-2 text-xs text-fg-muted",children:[s.jsx(Q,{checked:n,disabled:r,onChange:c,label:"Also offer directories outside your home directory"}),s.jsxs("span",{children:["Also offer directories outside ",s.jsx("code",{children:e.homeDir}),m>0&&` (${m} hidden)`]})]})]})}function Ae({claudeHome:e,onStartChat:t}){return s.jsx(R,{variant:"panel",icon:s.jsx(ue,{width:22,height:22}),title:"Nothing to adopt yet",body:s.jsxs(s.Fragment,{children:["No Claude Code history has been recorded under"," ",s.jsx("code",{className:"break-all",children:e}),". That is normal on a new install, and on a container whose Claude home is its own rather than yours — nothing is wrong. Start a chat here and Paddock writes its own history; or bind-mount an existing one there and re-open this page, or import it headlessly with ",s.jsx("code",{children:"npm run import-chats"}),"."]}),action:t&&s.jsx($,{variant:"primary",icon:s.jsx(he,{width:14,height:14}),onClick:t,children:"Start a chat"})})}function Te({imported:e,projects:t,onGetStarted:n}){return s.jsx("div",{className:"mb-6",children:s.jsx(R,{variant:"panel",icon:s.jsx(Z,{width:22,height:22}),title:t===0?"Nothing was adopted":`${t} project${t===1?"":"s"}, ${e} conversation${e===1?"":"s"}`,body:t===0?"Every row failed. Each one says why below.":"They are in the sidebar now. Anything that did not land says why below.",action:s.jsx($,{variant:"primary",onClick:()=>void n(),children:"Get started"})})})}function De(){return s.jsx("div",{className:"space-y-2","aria-label":"Scanning",role:"status",children:[0,1,2].map(e=>s.jsx("div",{className:"h-16 animate-pulse rounded-2xl bg-surface-active"},e))})}function Re(e){const t=[],n=[["temp-root",a=>`${a} in temp directories`],["no-git",a=>`${a} without a git repository`],["outside-home",a=>`${a} outside your home directory`],["already-managed",a=>`${a} already a project`],["no-sessions",a=>`${a} with nothing to adopt`],["no-recorded-cwd",a=>`${a} with no recorded directory`],["missing",a=>`${a} no longer on disk`],["home-root",a=>`${a} your home directory itself`],["system-path",a=>`${a} system directories`],["paddock-internal",a=>`${a} inside Paddock's own data`]];for(const[a,c]of n){const l=e.excluded[a];l!==void 0&&l>0&&t.push(c(l))}const r=`${e.scanned} transcript folder${e.scanned===1?"":"s"} scanned`;return t.length===0?`${r}.`:`${r}; skipped ${t.join(", ")}.`}function q(e){var r,a;const t=(r=e.autoName)==null?void 0:r.trim();if(t!==void 0&&t!=="")return t;const n=(a=e.preview)==null?void 0:a.trim();return n!==void 0&&n!==""?n:e.sessionId}function U(e){const t=new Date(e);return Number.isNaN(t.getTime())?"unknown date":t.toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"})}function Oe(e){return e<1024?`${e} B`:e<1024*1024?`${Math.round(e/1024)} kB`:`${(e/(1024*1024)).toFixed(1)} MB`}export{Ge as D};
1
+ import{h as oe,d as ie,a0 as de,r as u,a as A,j as s,V as W,A as X,a4 as Z,I as $,a1 as ce,Q as le,i as he,a5 as ue,a6 as J,J as T,c as K,a7 as pe,t as xe,Y as Q}from"./index-BYqBTUv_.js";import{E as R,C as me}from"./Surfaces-CTGs5AAK.js";function ge(e){const t={};for(const n of e)t[n.path]={checked:!0,expanded:!1,loading:!1,error:null};return t}function fe(e){if(!e.sessions||!e.selected)return e.checked?"on":"off";const t=e.sessions.sessions.length,n=e.selected.size;return t===0||n===0?"off":n===t?"on":"mixed"}function C(e,t){return e.sessions&&e.selected?e.selected.size:e.checked?t.sessionCount:0}function je(e,t){return t.reduce((n,r)=>n+C(e[r.path]??O(),r),0)}function be(e,t){return t.filter(n=>C(e[n.path]??O(),n)>0)}function we(e,t,n){const r=e[t];if(!r)return e;const a={...r,checked:n};return r.sessions&&(a.selected=n?new Set(r.sessions.sessions.map(c=>c.sessionId)):new Set),{...e,[t]:a}}function ye(e,t,n){const r=e[t];if(!r||!r.sessions)return e;const a=new Set(r.selected??[]);return a.delete(n)||a.add(n),{...e,[t]:{...r,selected:a,checked:a.size>0}}}function ve(e,t,n){const r=e[t];if(!r)return e;const a=r.checked?new Set(n.sessions.map(c=>c.sessionId)):new Set;return{...e,[t]:{...r,sessions:n,selected:a,loading:!1,error:null}}}function D(e,t,n){const r=e[t];return r?{...e,[t]:{...r,...n}}:e}function Ne(e,t){const n=e??O();return C(n,t)===0?null:n.sessions&&n.selected?{sessionIds:[...n.selected]}:{}}function O(){return{checked:!1,expanded:!1,loading:!1,error:null}}function ke(e){return e.error!==void 0?e.stage==="create"?"danger":"warn":e.adopted>0?"success":"warn"}async function Se(e,t,n){let r;try{r=await e.createProject({name:t.name,slug:t.suggestedSlug,path:t.path,managed:!1})}catch(a){return{stage:"create",adopted:0,skipped:[],error:_(a)}}try{const a=await e.adoptChats(r.slug,{sourceCwd:r.workingDir,...n.sessionIds?{sessionIds:n.sessionIds}:{}});return{project:r,adopted:a.adopted.length,skipped:a.skipped}}catch(a){return{stage:"import",project:r,adopted:0,skipped:[],error:_(a)}}}async function $e(e,t,n){const r=[];for(const a of t){n(a.candidate.path,"running");const c=await Se(e,a.candidate,a.plan);r.push(c),n(a.candidate.path,"done",c)}return r}const Ce={sidechain:"sub-agent transcript","already-adopted":"already adopted","destination-exists":"a chat with that id already exists here","attributed-to-run":"belongs to a Paddock run",unreadable:"transcript unreadable","placement-failed":"copy failed","record-failed":"could not be recorded"};function Y(e){if(e.length===0)return"";const t=[...new Set(e.map(n=>Ce[n.reason]??n.reason))];return`${e.length} skipped (${t.join(", ")})`}function Ie(e,t){var l;if(e.stage==="create")return`Couldn't create the project: ${e.error}`;const n=((l=e.project)==null?void 0:l.slug)??t.suggestedSlug;if(e.stage==="import")return`Project “${n}” was created, but importing its chats failed: ${e.error}. It is there and empty — you can import them from the project itself.`;if(e.adopted===0){const m=Y(e.skipped);return t.recordedPath!==void 0?`Project “${n}” was created but no chats came with it. Its transcripts record ${t.recordedPath}, a different spelling of ${t.path}, and an import matches on the exact path.`:m?`Project “${n}” was created but no chats came with it — ${m}.`:`Project “${n}” was created but no chats came with it.`}const r=e.adopted,a=Y(e.skipped),c=`Adopted ${r} chat${r===1?"":"s"} into “${n}”`;return a?`${c} — ${a}.`:`${c}.`}function _(e){return e instanceof Error?e.message:String(e)}function Ge({firstRun:e=!1,embedded:t=!1,onLeave:n,onStartChat:r}){const a=oe(),{refresh:c}=ie(),l=de(),[m,d]=u.useState(!1),[j,I]=u.useState(!1),[h,x]=u.useState(null),[y,z]=u.useState(null),[F,P]=u.useState(!0),[f,v]=u.useState({}),[k,G]=u.useState(!1),[L,H]=u.useState({}),[E,B]=u.useState({}),[S,ee]=u.useState(!1);u.useEffect(()=>{let o=!0;return P(!0),z(null),A.discover({includeNonGit:m,includeOutsideHome:j}).then(i=>{o&&(x(i),v(ge(i.candidates)),P(!1))}).catch(i=>{o&&(z(i instanceof Error?i.message:"Discovery failed"),P(!1))}),()=>{o=!1}},[m,j]);const w=(h==null?void 0:h.candidates)??[],M=u.useMemo(()=>je(f,w),[f,w]),b=u.useMemo(()=>be(f,w),[f,w]),se=u.useCallback(o=>{v(p=>{const g=p[o.path];if(!g)return p;const N=D(p,o.path,{expanded:!g.expanded});return g.expanded||g.sessions||g.loading?N:D(N,o.path,{loading:!0,error:null})});const i=f[o.path];!i||i.expanded||i.sessions||i.loading||A.discoverSessions(o.path).then(p=>v(g=>ve(g,o.path,p))).catch(p=>v(g=>D(g,o.path,{loading:!1,error:p instanceof Error?p.message:"Could not read this directory's sessions"})))},[f]),te=u.useCallback(async()=>{if(k)return;G(!0),H(Object.fromEntries(b.map(i=>[i.path,"pending"]))),B({});const o=b.map(i=>({candidate:i,plan:Ne(f[i.path],i)??{}}));await $e(A,o,(i,p,g)=>{H(N=>({...N,[i]:p})),g&&B(N=>({...N,[i]:g}))}),G(!1),ee(!0),await c()},[b,f,k,c]),ne=u.useCallback(async()=>{await c(),n==null||n(),a("/")},[a,c,n]),ae=Object.values(E).reduce((o,i)=>o+i.adopted,0),re=Object.values(E).filter(o=>o.project).length,V=s.jsxs(s.Fragment,{children:[s.jsx("p",{className:"mb-5 text-sm text-fg-muted",children:e?"If you have used Claude Code in a terminal on this machine, Paddock can adopt that history: each directory becomes a project with its conversations already in it.":"Directories on this machine with existing Claude Code history. Adopting one links it as a project and brings its conversations in; your own history is never moved or deleted."}),S&&s.jsx(Te,{imported:ae,projects:re,onGetStarted:ne}),y!==null&&s.jsx(W,{tone:"danger",icon:s.jsx(X,{width:16,height:16}),children:y}),F&&s.jsx(De,{}),!F&&y===null&&h!==null&&s.jsxs(s.Fragment,{children:[h.scanned===0?s.jsx(Ae,{claudeHome:h.claudeHome,onStartChat:r}):w.length===0?s.jsx(R,{variant:"panel",icon:s.jsx(Z,{width:22,height:22}),title:"Nothing to adopt",body:"Paddock found Claude Code history on this machine, but none of it is available to adopt right now. The tally below says what was skipped, and the two toggles relax the rules that are safe to relax."}):s.jsx("ul",{className:"space-y-2",children:w.map(o=>s.jsx(Pe,{candidate:o,row:f[o.path],status:L[o.path],outcome:E[o.path],locked:k||S,onToggleRow:i=>v(p=>we(p,o.path,i)),onToggleSession:i=>v(p=>ye(p,o.path,i)),onToggleExpanded:()=>se(o)},o.path))}),!S&&h.scanned>0&&s.jsx(Ee,{result:h,includeNonGit:m,includeOutsideHome:j,disabled:k,onIncludeNonGit:d,onIncludeOutsideHome:I}),w.length>0&&!S&&s.jsxs("div",{className:"mt-5 flex items-center justify-between gap-3 border-t border-edge pt-4",children:[s.jsxs("span",{className:"text-sm text-fg-muted",children:[M," conversation",M===1?"":"s"," from ",b.length," director",b.length===1?"y":"ies"]}),s.jsxs($,{variant:"primary",onClick:()=>void te(),disabled:b.length===0,loading:k,loadingLabel:"Adopting…",children:["Adopt ",b.length," project",b.length===1?"":"s"]})]})]})]});return t?V:s.jsxs("div",{className:"flex h-full min-h-0 flex-col",children:[s.jsxs("header",{className:"pt-safe flex items-center gap-2 border-b border-edge px-3 pb-2.5 sm:px-6 lg:py-4",children:[s.jsx("button",{type:"button",onClick:()=>l==null?void 0:l.openNav(),"aria-label":"Open menu",className:"btn-subtle -ml-1 shrink-0 px-2 py-1.5 lg:hidden",children:s.jsx(ce,{width:20,height:20})}),s.jsx(le,{width:18,height:18,className:"shrink-0 text-fg-subtle"}),s.jsx("h1",{className:"text-md font-semibold tracking-tight",children:"Discover"})]}),s.jsx("div",{className:"flex-1 overflow-y-auto overscroll-contain",children:s.jsx("div",{className:"mx-auto max-w-3xl px-6 py-6",children:V})})]})}function Pe({candidate:e,row:t,status:n,outcome:r,locked:a,onToggleRow:c,onToggleSession:l,onToggleExpanded:m}){const d=t??{checked:!1,expanded:!1,loading:!1,error:null},j=fe(d),I=C(d,e),h=r?ke(r):null;return s.jsx("li",{children:s.jsxs(me,{flush:!0,className:K("overflow-hidden",h==="success"&&"border-success-edge bg-success-soft",h==="warn"&&"border-warn-edge bg-warn-soft",h==="danger"&&"border-danger-edge bg-danger-soft",n==="running"&&"border-accent-edge"),"data-testid":`discover-row-${e.suggestedSlug}`,"data-tone":h??void 0,children:[s.jsxs("div",{className:"flex items-start gap-3 px-3 py-2.5",children:[s.jsx(J,{className:"mt-1",checked:j==="on",indeterminate:j==="mixed",disabled:a,"aria-label":`Adopt ${e.path}`,onChange:x=>c(x.target.checked)}),s.jsxs("div",{className:"min-w-0 flex-1",children:[s.jsx("code",{className:"block break-all text-sm text-fg",children:e.path}),s.jsxs("div",{className:"mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-fg-muted",children:[s.jsxs("span",{className:"tabular",children:[e.sessionCount," conversation",e.sessionCount===1?"":"s"]}),e.filteredCount>0&&s.jsxs("span",{className:"tabular",title:"Empty, slash-command-only or sweeper transcripts, withheld as noise",children:["· ",e.filteredCount," filtered out"]}),e.lastSessionAt!==void 0&&s.jsxs("span",{className:"tabular",children:["· last ",U(e.lastSessionAt)]}),e.gitRemote!==void 0&&s.jsx(T,{tone:"lineage",size:"sm",children:e.gitRemote}),!e.hasGit&&s.jsx(T,{tone:"neutral",size:"sm",children:"no git"}),!e.insideHome&&s.jsx(T,{tone:"neutral",size:"sm",children:"outside home"})]}),e.recordedPath!==void 0&&s.jsx(W,{tone:"warn",className:"mt-2",icon:s.jsx(X,{width:14,height:14}),children:s.jsxs("span",{className:"text-xs",children:["These transcripts record"," ",s.jsx("code",{className:"break-all",children:e.recordedPath})," — a different spelling of this path. The import matches on the exact path, so it may bring nothing in."]})}),r&&s.jsx("p",{className:K("mt-2 text-xs",h==="success"&&"text-success",h==="warn"&&"text-warn",h==="danger"&&"text-danger"),children:Ie(r,e)}),n==="running"&&!r&&s.jsx("p",{className:"mt-2 text-xs text-fg-muted",children:"Adopting…"})]}),s.jsxs("span",{className:"flex shrink-0 items-center gap-1",children:[s.jsx("span",{className:"tabular text-xs text-fg-subtle",children:I}),s.jsx($,{size:"icon-sm",variant:"subtle","aria-label":d.expanded?"Hide conversations":"Show conversations","aria-expanded":d.expanded,onClick:m,children:d.expanded?s.jsx(pe,{width:14,height:14}):s.jsx(xe,{width:14,height:14})})]})]}),d.expanded&&s.jsxs("div",{className:"border-t border-edge px-3 py-2",children:[d.loading&&s.jsx("p",{className:"py-2 text-xs text-fg-muted",children:"Reading transcripts…"}),d.error!==null&&s.jsx("p",{className:"py-2 text-xs text-danger",children:d.error}),d.sessions&&d.sessions.sessions.length===0&&s.jsx("p",{className:"py-2 text-xs text-fg-muted",children:"Nothing to adopt here any more — it may have been adopted since the scan."}),d.sessions&&d.sessions.sessions.length>0&&s.jsx("ul",{className:"space-y-1",children:d.sessions.sessions.map(x=>{var y;return s.jsx("li",{children:s.jsxs("label",{className:"flex cursor-pointer items-start gap-2 rounded-lg px-2 py-1.5 transition-colors hover:bg-surface-hover",children:[s.jsx(J,{checked:((y=d.selected)==null?void 0:y.has(x.sessionId))??!1,disabled:a,"aria-label":q(x),onChange:()=>l(x.sessionId)}),s.jsxs("span",{className:"min-w-0 flex-1",children:[s.jsx("span",{className:"block truncate text-xs text-fg",children:q(x)}),s.jsxs("span",{className:"tabular mt-0.5 block text-3xs text-fg-muted",children:[U(x.mtime)," · ",Oe(x.sizeBytes)]})]})]})},x.sessionId)})}),d.sessions&&d.sessions.filtered.length>0&&s.jsxs("p",{className:"mt-2 px-2 text-3xs text-fg-subtle",children:[d.sessions.filtered.length," more withheld as noise (",[...new Set(d.sessions.filtered.map(x=>x.reason))].join(", "),")."]})]})]})})}function Ee({result:e,includeNonGit:t,includeOutsideHome:n,disabled:r,onIncludeNonGit:a,onIncludeOutsideHome:c}){const l=e.excluded["no-git"]??0,m=e.excluded["outside-home"]??0,d=l>0||t,j=m>0||n;return e.scanned===0?null:s.jsxs("div",{className:"mt-5 space-y-2 border-t border-edge pt-4",children:[s.jsx("p",{className:"text-xs text-fg-muted",children:Re(e)}),d&&s.jsxs("div",{className:"flex items-center gap-2 text-xs text-fg-muted",children:[s.jsx(Q,{checked:t,disabled:r,onChange:a,label:"Also offer directories without a git repository"}),s.jsxs("span",{children:["Also offer directories without a git repository",l>0&&` (${l} hidden)`]})]}),j&&s.jsxs("div",{className:"flex items-center gap-2 text-xs text-fg-muted",children:[s.jsx(Q,{checked:n,disabled:r,onChange:c,label:"Also offer directories outside your home directory"}),s.jsxs("span",{children:["Also offer directories outside ",s.jsx("code",{children:e.homeDir}),m>0&&` (${m} hidden)`]})]})]})}function Ae({claudeHome:e,onStartChat:t}){return s.jsx(R,{variant:"panel",icon:s.jsx(ue,{width:22,height:22}),title:"Nothing to adopt yet",body:s.jsxs(s.Fragment,{children:["No Claude Code history has been recorded under"," ",s.jsx("code",{className:"break-all",children:e}),". That is normal on a new install, and on a container whose Claude home is its own rather than yours — nothing is wrong. Start a chat here and Paddock writes its own history; or bind-mount an existing one there and re-open this page, or import it headlessly with ",s.jsx("code",{children:"npm run import-chats"}),"."]}),action:t&&s.jsx($,{variant:"primary",icon:s.jsx(he,{width:14,height:14}),onClick:t,children:"Start a chat"})})}function Te({imported:e,projects:t,onGetStarted:n}){return s.jsx("div",{className:"mb-6",children:s.jsx(R,{variant:"panel",icon:s.jsx(Z,{width:22,height:22}),title:t===0?"Nothing was adopted":`${t} project${t===1?"":"s"}, ${e} conversation${e===1?"":"s"}`,body:t===0?"Every row failed. Each one says why below.":"They are in the sidebar now. Anything that did not land says why below.",action:s.jsx($,{variant:"primary",onClick:()=>void n(),children:"Get started"})})})}function De(){return s.jsx("div",{className:"space-y-2","aria-label":"Scanning",role:"status",children:[0,1,2].map(e=>s.jsx("div",{className:"h-16 animate-pulse rounded-2xl bg-surface-active"},e))})}function Re(e){const t=[],n=[["temp-root",a=>`${a} in temp directories`],["no-git",a=>`${a} without a git repository`],["outside-home",a=>`${a} outside your home directory`],["already-managed",a=>`${a} already a project`],["no-sessions",a=>`${a} with nothing to adopt`],["no-recorded-cwd",a=>`${a} with no recorded directory`],["missing",a=>`${a} no longer on disk`],["home-root",a=>`${a} your home directory itself`],["system-path",a=>`${a} system directories`],["paddock-internal",a=>`${a} inside Paddock's own data`]];for(const[a,c]of n){const l=e.excluded[a];l!==void 0&&l>0&&t.push(c(l))}const r=`${e.scanned} transcript folder${e.scanned===1?"":"s"} scanned`;return t.length===0?`${r}.`:`${r}; skipped ${t.join(", ")}.`}function q(e){var r,a;const t=(r=e.autoName)==null?void 0:r.trim();if(t!==void 0&&t!=="")return t;const n=(a=e.preview)==null?void 0:a.trim();return n!==void 0&&n!==""?n:e.sessionId}function U(e){const t=new Date(e);return Number.isNaN(t.getTime())?"unknown date":t.toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"})}function Oe(e){return e<1024?`${e} B`:e<1024*1024?`${Math.round(e/1024)} kB`:`${(e/(1024*1024)).toFixed(1)} MB`}export{Ge as D};
2
2