@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.
- package/package.json +2 -2
- package/packages/server/dist/auto-memory.d.ts +22 -0
- package/packages/server/dist/auto-memory.js +165 -0
- package/packages/server/dist/herdctl-agent-config.d.ts +2 -2
- package/packages/server/dist/herdctl-agent-config.js +18 -2
- package/packages/server/dist/herdctl.d.ts +25 -0
- package/packages/server/dist/herdctl.js +44 -4
- package/packages/server/dist/transcripts.js +14 -8
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{DeleteProjectDialog-I2oqHBzS.js → DeleteProjectDialog-DX4WrELh.js} +1 -1
- package/packages/web/dist/assets/DiscoverPage-SmrSuKC8.js +2 -0
- package/packages/web/dist/assets/{DiscoverView-C21wIKtf.js → DiscoverView-DE65Ui9u.js} +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-EYk_wC3Y.js → InstanceConfigPage-B27CjE-g.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-BsTH5ue5.js → MarkdownRenderer-CHgKQK1c.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-ClRBMnRd.js → ProjectRedirect-CnUMNGmC.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-BvGNJ12B.js → ProjectView-BQJPlfPn.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-BR5dAAFl.js → ProjectsGrid-ByCT2JT8.js} +1 -1
- package/packages/web/dist/assets/{RootHome-AVtKnSgs.js → RootHome-Bd5UWE4_.js} +1 -1
- package/packages/web/dist/assets/{Surfaces-DPLI5OkT.js → Surfaces-CTGs5AAK.js} +1 -1
- package/packages/web/dist/assets/{arc-C5J-eVUW.js → arc-D-OBWbNz.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-CZCJKMwH.js → architectureDiagram-3BPJPVTR-uHjrv32s.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-WWCMaGwE.js → blockDiagram-GPEHLZMM-BfORDi4c.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-DSvcKk0w.js → c4Diagram-AAUBKEIU-CavMptIt.js} +1 -1
- package/packages/web/dist/assets/channel-DuOAMEMs.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-Bunmbxbf.js → chunk-2J33WTMH-DWbn9f15.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-BVlgK4fh.js → chunk-4BX2VUAB-B6n57Y4t.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-CGx4xEnG.js → chunk-55IACEB6-RPlieJ1B.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-B88C8y6G.js → chunk-727SXJPM-3fxjrr8e.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-BT2IBIj0.js → chunk-AQP2D5EJ-8773cg5n.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-kFOjbALN.js → chunk-FMBD7UC4-BaEdhz4G.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-BUeFecIE.js → chunk-ND2GUHAM-BGU6JXI8.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-DJyJVXra.js → chunk-QZHKN3VN-Bn7xewhK.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DJTQSdLF.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DJTQSdLF.js +2 -0
- package/packages/web/dist/assets/{core-C5DQrbiK.js → core-Dt-6hYRW.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-B4B7ZRSe.js → cose-bilkent-S5V4N54A-CY47vg-k.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-B2i2sau8.js → dagre-BM42HDAG-Bn5RQpQd.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-BbTlnpG6.js → diagram-2AECGRRQ-CRzZOKXG.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-B3Th9TJN.js → diagram-5GNKFQAL-DVBHmgdg.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-Bzrq4YSz.js → diagram-KO2AKTUF-CmrfF1N2.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-CwmWi6QE.js → diagram-LMA3HP47-CDT8x43G.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-Bb6iqeiS.js → diagram-OG6HWLK6-C71v9fTh.js} +1 -1
- package/packages/web/dist/assets/{documentTitle-BO1z6u5w.js → documentTitle-cxKpkGjk.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-CVQQunO1.js → erDiagram-TEJ5UH35-DutEYHIi.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-BEY3tz_1.js → flowDiagram-I6XJVG4X-MAg26MC3.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-BBdhMq6t.js → ganttDiagram-6RSMTGT7-CxjMzmWU.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-p26vw1Ba.js → gitGraphDiagram-PVQCEYII-DRdROoMO.js} +1 -1
- package/packages/web/dist/assets/{index-Ba5H3P_n.js → index-BYqBTUv_.js} +2 -2
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-C9Lm4jRf.js → infoDiagram-5YYISTIA-Yov5F3W5.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DvAVFsHW.js → ishikawaDiagram-YF4QCWOH-CaKXiwxT.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-BHAQBO0Z.js → journeyDiagram-JHISSGLW-DAis2VeB.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-c37yXqVr.js → kanban-definition-UN3LZRKU-C8gEFG37.js} +1 -1
- package/packages/web/dist/assets/{linear-CaLelNJV.js → linear-BGqCeIdL.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-BZ9rtnnW.js → mermaid.core-Byo4DAC8.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-DjANagUV.js → mindmap-definition-RKZ34NQL-DPc2OGmC.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-DLVKuVW7.js → pieDiagram-4H26LBE5-8LoTusWf.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-CzEweksI.js → quadrantDiagram-W4KKPZXB-PqmA7ZXs.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-Cmp8uj-X.js → requirementDiagram-4Y6WPE33-CTtvHeYi.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-DEr5os6k.js → sankeyDiagram-5OEKKPKP-DA2BOpvW.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-CUw8k4V7.js → sequenceDiagram-3UESZ5HK-BdksJUNO.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-CCPuhhD3.js → stateDiagram-AJRCARHV-BGK1rh1o.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-DTVNruiv.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-M4Oh-WQu.js → timeline-definition-PNZ67QCA-D62mOs5P.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-CaAp-n-E.js → vennDiagram-CIIHVFJN-DLfP6nl7.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-slzDPBwj.js → wardley-L42UT6IY-BFUgYulG.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-BaVLEx_-.js → wardleyDiagram-YWT4CUSO-BrEbgbXY.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-axwO9R4I.js → xychartDiagram-2RQKCTM6-ClURTg6_.js} +1 -1
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/sw.js +1 -1
- package/packages/web/dist/assets/DiscoverPage-BWJpx4AG.js +0 -2
- package/packages/web/dist/assets/channel-BwTacR4O.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DzYQps0D.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DzYQps0D.js +0 -2
- 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.
|
|
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.
|
|
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
|
-
|
|
437
|
-
|
|
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
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
package/packages/web/dist/assets/{DeleteProjectDialog-I2oqHBzS.js → DeleteProjectDialog-DX4WrELh.js}
RENAMED
|
@@ -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-
|
|
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
|
|
|
@@ -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
|
|