@edspencer/paddock 0.71.0 → 0.71.1

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 (91) hide show
  1. package/package.json +1 -1
  2. package/packages/server/dist/chat-dto.d.ts +3 -2
  3. package/packages/server/dist/chat-dto.js +9 -1
  4. package/packages/server/dist/cli/paddock.js +3 -3
  5. package/packages/server/dist/cli/service/index.d.ts +10 -1
  6. package/packages/server/dist/cli/service/index.js +43 -5
  7. package/packages/server/dist/cli/service/ready.d.ts +61 -0
  8. package/packages/server/dist/cli/service/ready.js +71 -0
  9. package/packages/server/dist/discover.d.ts +37 -0
  10. package/packages/server/dist/discover.js +51 -0
  11. package/packages/server/dist/herdctl.d.ts +2 -1
  12. package/packages/server/dist/herdctl.js +11 -6
  13. package/packages/server/dist/last-activity.d.ts +34 -0
  14. package/packages/server/dist/last-activity.js +178 -0
  15. package/packages/server/dist/management-ops.js +5 -1
  16. package/packages/server/dist/routes/discover.js +10 -3
  17. package/packages/server/dist/self-mcp-types.d.ts +4 -1
  18. package/packages/server/dist/transcripts.js +5 -3
  19. package/packages/server/package.json +1 -1
  20. package/packages/web/dist/assets/Chip-Db--PJFH.js +2 -0
  21. package/packages/web/dist/assets/DiscoverPage-BFo4FTzZ.js +2 -0
  22. package/packages/web/dist/assets/DiscoverView-C4hOB3nr.js +2 -0
  23. package/packages/web/dist/assets/InstanceConfigPage-U78M95nW.js +2 -0
  24. package/packages/web/dist/assets/{MarkdownRenderer-BOrDawoY.js → MarkdownRenderer-DdoNl0Ao.js} +2 -2
  25. package/packages/web/dist/assets/{ProjectMenu-B_8NYWZr.js → ProjectMenu-BxQu14ze.js} +1 -1
  26. package/packages/web/dist/assets/{ProjectRedirect-xFWTlADS.js → ProjectRedirect-QuGeN-qx.js} +1 -1
  27. package/packages/web/dist/assets/ProjectView-FUcyJMUf.js +13 -0
  28. package/packages/web/dist/assets/{ProjectsGrid-BF65wTqN.js → ProjectsGrid-CUFNVTif.js} +1 -1
  29. package/packages/web/dist/assets/RootHome-SIxo-xX9.js +2 -0
  30. package/packages/web/dist/assets/{arc-7GXE70CZ.js → arc-C0DRwCCT.js} +1 -1
  31. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-CAEj_lAw.js → architectureDiagram-3BPJPVTR-Wwpf4cvW.js} +1 -1
  32. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-wNLONvdQ.js → blockDiagram-GPEHLZMM-DKmCbgWk.js} +1 -1
  33. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-Cf7MUqJz.js → c4Diagram-AAUBKEIU-CJ8LiLsj.js} +1 -1
  34. package/packages/web/dist/assets/channel-DEmtNKeB.js +2 -0
  35. package/packages/web/dist/assets/{chunk-2J33WTMH-W18BVHtE.js → chunk-2J33WTMH-C3P-O4C3.js} +1 -1
  36. package/packages/web/dist/assets/{chunk-4BX2VUAB-DDJQi9KC.js → chunk-4BX2VUAB-Cs9u7x61.js} +1 -1
  37. package/packages/web/dist/assets/{chunk-55IACEB6-L5afa49k.js → chunk-55IACEB6-C1st284U.js} +1 -1
  38. package/packages/web/dist/assets/{chunk-727SXJPM-Bb0Sw8oK.js → chunk-727SXJPM-BjCAWo7B.js} +1 -1
  39. package/packages/web/dist/assets/{chunk-AQP2D5EJ-DRrKTv1K.js → chunk-AQP2D5EJ-BP6S9w2Q.js} +1 -1
  40. package/packages/web/dist/assets/{chunk-FMBD7UC4-CIsdDBaT.js → chunk-FMBD7UC4-C3ySTaPs.js} +1 -1
  41. package/packages/web/dist/assets/{chunk-ND2GUHAM-DN5ua8KT.js → chunk-ND2GUHAM-eNPD4vzc.js} +1 -1
  42. package/packages/web/dist/assets/{chunk-QZHKN3VN-DE1WNShj.js → chunk-QZHKN3VN-BbBpgHkO.js} +1 -1
  43. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-dAGAuccL.js +2 -0
  44. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-dAGAuccL.js +2 -0
  45. package/packages/web/dist/assets/{core-DadwNpE4.js → core-ZwDDec0y.js} +1 -1
  46. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-2FTPTDA6.js → cose-bilkent-S5V4N54A-DiM4eeF5.js} +1 -1
  47. package/packages/web/dist/assets/{dagre-BM42HDAG-eJ7MdaEX.js → dagre-BM42HDAG-DKIEUM4M.js} +1 -1
  48. package/packages/web/dist/assets/{diagram-2AECGRRQ-D8MZTSkB.js → diagram-2AECGRRQ-CqlX7j8t.js} +1 -1
  49. package/packages/web/dist/assets/{diagram-5GNKFQAL-DVXF3Z-H.js → diagram-5GNKFQAL-Dph5apqo.js} +1 -1
  50. package/packages/web/dist/assets/{diagram-KO2AKTUF-DoXfbQ0i.js → diagram-KO2AKTUF-TW6LyHGt.js} +1 -1
  51. package/packages/web/dist/assets/{diagram-LMA3HP47-CRW0fiqv.js → diagram-LMA3HP47-BxxIIv5a.js} +1 -1
  52. package/packages/web/dist/assets/{diagram-OG6HWLK6-DfhYXDqr.js → diagram-OG6HWLK6-B8M0DeV7.js} +1 -1
  53. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-HG0QTHgA.js → erDiagram-TEJ5UH35-Bfpy6UQa.js} +1 -1
  54. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-ChVekwUy.js → flowDiagram-I6XJVG4X-DstRzxek.js} +1 -1
  55. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-CeA-6Z-T.js → ganttDiagram-6RSMTGT7-Bg0oFbAl.js} +1 -1
  56. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-ew2oouNN.js → gitGraphDiagram-PVQCEYII-CX4IozE-.js} +1 -1
  57. package/packages/web/dist/assets/{index-CbqaEg2j.js → index-ASW1Jl0Y.js} +2 -2
  58. package/packages/web/dist/assets/index-bC5uCujx.css +1 -0
  59. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-BY9NYdld.js → infoDiagram-5YYISTIA-CG2qCk6u.js} +1 -1
  60. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-CgbdrGBm.js → ishikawaDiagram-YF4QCWOH-CPErvl3a.js} +1 -1
  61. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-BepLFtbU.js → journeyDiagram-JHISSGLW-Bm6krUsf.js} +1 -1
  62. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-JHMJYGJu.js → kanban-definition-UN3LZRKU-B4CC8CAx.js} +1 -1
  63. package/packages/web/dist/assets/{linear-Ccw8eVgC.js → linear-CJ1DbrOX.js} +1 -1
  64. package/packages/web/dist/assets/{mermaid.core-lHjASk7x.js → mermaid.core-Dk2kkhf9.js} +4 -4
  65. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-Bi5Iq2M4.js → mindmap-definition-RKZ34NQL-CThvf_YD.js} +1 -1
  66. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-rZuB_P1J.js → pieDiagram-4H26LBE5-DvkHcng6.js} +1 -1
  67. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-BuStI37i.js → quadrantDiagram-W4KKPZXB-CqhrTh9q.js} +1 -1
  68. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-BFElQKEX.js → requirementDiagram-4Y6WPE33-CZykiLym.js} +1 -1
  69. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-tzdQCr91.js → sankeyDiagram-5OEKKPKP-D-TzCKMx.js} +1 -1
  70. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-j1koqb6Q.js → sequenceDiagram-3UESZ5HK-C5pIItVu.js} +1 -1
  71. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-B-dUkvgq.js → stateDiagram-AJRCARHV-DE6G2JfB.js} +1 -1
  72. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BP6oowh1.js +2 -0
  73. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-BWyq6u64.js → timeline-definition-PNZ67QCA-C1VK2OfU.js} +1 -1
  74. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-B0CRl3xN.js → vennDiagram-CIIHVFJN-CtOozEwa.js} +1 -1
  75. package/packages/web/dist/assets/{wardley-L42UT6IY-C45bBEPS.js → wardley-L42UT6IY-DvHGK-lb.js} +1 -1
  76. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-DT50TWrW.js → wardleyDiagram-YWT4CUSO-DxKwZ3-G.js} +1 -1
  77. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-C8iBuFGD.js → xychartDiagram-2RQKCTM6-HM4j3l6y.js} +1 -1
  78. package/packages/web/dist/index.html +2 -2
  79. package/packages/web/dist/sw.js +1 -1
  80. package/packages/web/dist/assets/Chip-5S5MAayn.js +0 -2
  81. package/packages/web/dist/assets/DiscoverPage-CEzcRO7X.js +0 -2
  82. package/packages/web/dist/assets/DiscoverView-CArICIRO.js +0 -2
  83. package/packages/web/dist/assets/InstanceConfigPage-CKjJsaGZ.js +0 -2
  84. package/packages/web/dist/assets/ProjectView-polknqZw.js +0 -13
  85. package/packages/web/dist/assets/RootHome-32k1iDh4.js +0 -2
  86. package/packages/web/dist/assets/Surfaces-DCm19ZAD.js +0 -2
  87. package/packages/web/dist/assets/channel-U0NZSTs2.js +0 -2
  88. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-D1QjNKTc.js +0 -2
  89. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-D1QjNKTc.js +0 -2
  90. package/packages/web/dist/assets/index-tvJIh4A9.css +0 -1
  91. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-b1ksIBrs.js +0 -2
@@ -0,0 +1,178 @@
1
+ /**
2
+ * When a chat was last *actually* used, as opposed to when its file was last
3
+ * touched (#863).
4
+ *
5
+ * ## The bug this exists to remove
6
+ *
7
+ * The chat list's "updated X ago" — and the order the list is in — came from
8
+ * the transcript `.jsonl`'s filesystem **mtime**. That is a property of the
9
+ * file, not of the conversation, and Paddock touches transcripts for reasons
10
+ * that have nothing to do with anyone talking: discovery re-stats, attribution,
11
+ * a resume appending a mode record, a relocation. Every one of those bumps the
12
+ * mtime with no new message in the file. The visible result was days-old chats
13
+ * announcing themselves as "updated a few minutes ago" — in *batches*,
14
+ * phase-locked to a periodic task, because that is what actually moved them —
15
+ * and jumping to the top of a list sorted by recency. Nothing was lost; the
16
+ * ordering was simply not about what it claimed to be about.
17
+ *
18
+ * So: read the timestamp of the last real message **inside** the transcript,
19
+ * and fall back to mtime only when there is no datable record to read.
20
+ *
21
+ * ## Why a tail read rather than a parse
22
+ *
23
+ * The value wanted is on the last qualifying line, so this seeks to the end and
24
+ * walks backwards, stopping at the first line that qualifies. A transcript is
25
+ * append-only, so that is a couple of pages of IO regardless of whether the
26
+ * chat holds ten messages or ten thousand. `@herdctl/core` does compute this
27
+ * during `extractSessionMetadata`, but as part of a whole-file parse the
28
+ * listing path deliberately does not do — reusing it would have traded a
29
+ * cosmetic bug for a real one.
30
+ *
31
+ * Results are memoised against mtime (the same LRU the subagent and usage
32
+ * readers use). Note the irony, and that it is fine: a spurious touch — the
33
+ * very thing this module exists to ignore — still invalidates the cache and
34
+ * costs one tail read. It buys correctness in the other direction, where a
35
+ * touch accompanies a genuine append, and a tail read is cheap enough that
36
+ * paying it on a false alarm is not worth a subtler key.
37
+ */
38
+ import fs from "node:fs/promises";
39
+ import path from "node:path";
40
+ import { projectChatsDir } from "./transcripts.js";
41
+ import { mtimeCacheGet, mtimeCacheSet, statMtimeMs } from "./subagents.js";
42
+ /** How much of the file's end to look at before widening the window. */
43
+ const TAIL_BYTES = 128 * 1024;
44
+ /** The second and last attempt — enough to clear a single enormous tool result. */
45
+ const WIDE_TAIL_BYTES = 4 * 1024 * 1024;
46
+ const cache = new Map();
47
+ /**
48
+ * Does this record represent a human or the agent actually saying something?
49
+ *
50
+ * The exclusions are the point of the whole issue, not defensive noise. A
51
+ * resume, a permission-mode change or a background agent finishing all append
52
+ * records to a transcript **without any turn having happened**, and counting
53
+ * those as activity reintroduces the same bug one layer up: a chat nobody has
54
+ * touched in a week, freshly stamped, back at the top of the list.
55
+ */
56
+ function isConversation(rec) {
57
+ // Drops `summary` title entries and every `type: "system"` control line
58
+ // (local-command records, permission-mode changes, hook output).
59
+ if (rec.type !== "user" && rec.type !== "assistant")
60
+ return false;
61
+ // Harness-injected synthetic user lines: skill bodies, slash-command
62
+ // expansions, hook output. Nobody typed them.
63
+ if (rec.isMeta === true)
64
+ return false;
65
+ // A spawned agent's stop/complete block. Real work, but not a turn in THIS
66
+ // chat, and it lands without the user or the agent here saying anything.
67
+ if (rec.origin?.kind === "task-notification")
68
+ return false;
69
+ return true;
70
+ }
71
+ /** The last `bytes` of `file`, plus whether that covered the whole thing. */
72
+ async function readTail(file, size, bytes) {
73
+ const take = Math.min(bytes, size);
74
+ const start = size - take;
75
+ const handle = await fs.open(file, "r");
76
+ try {
77
+ const buf = Buffer.alloc(take);
78
+ await handle.read(buf, 0, take, start);
79
+ // A byte offset can land mid-character; only the first line can be affected
80
+ // and the caller discards it whenever `whole` is false.
81
+ return { text: buf.toString("utf8"), whole: start === 0 };
82
+ }
83
+ finally {
84
+ await handle.close();
85
+ }
86
+ }
87
+ /** Walk a window backwards for the newest qualifying record's timestamp. */
88
+ function scanBackwards(text, whole) {
89
+ const lines = text.split("\n");
90
+ // Seeking to a byte offset almost certainly landed inside a line. Dropping it
91
+ // costs nothing (it is the OLDEST line in the window) and keeps a fragment
92
+ // from being mistaken for a malformed record.
93
+ if (!whole)
94
+ lines.shift();
95
+ for (let i = lines.length - 1; i >= 0; i--) {
96
+ const line = lines[i].trim();
97
+ if (!line)
98
+ continue;
99
+ let rec;
100
+ try {
101
+ rec = JSON.parse(line);
102
+ }
103
+ catch {
104
+ continue;
105
+ }
106
+ if (!isConversation(rec))
107
+ continue;
108
+ const ts = rec.timestamp;
109
+ if (typeof ts === "string" && !Number.isNaN(Date.parse(ts)))
110
+ return ts;
111
+ }
112
+ return undefined;
113
+ }
114
+ /**
115
+ * Timestamp of the last real message in a project chat's transcript.
116
+ *
117
+ * `undefined` — a missing file, an unreadable one, a transcript of nothing but
118
+ * control records — is a legitimate answer meaning "nothing better than the
119
+ * mtime", and every caller treats it that way.
120
+ */
121
+ export async function lastMessageAt(projectDir, sessionId) {
122
+ // Same guard as `readFirstUserText`: keep a session id from walking out of
123
+ // `.chats/`.
124
+ if (!/^[A-Za-z0-9._-]+$/.test(sessionId))
125
+ return undefined;
126
+ const file = path.join(projectChatsDir(projectDir), `${sessionId}.jsonl`);
127
+ const mtimeMs = await statMtimeMs(file);
128
+ if (mtimeMs === undefined)
129
+ return undefined;
130
+ const cached = mtimeCacheGet(cache, file, mtimeMs);
131
+ if (cached.hit)
132
+ return cached.value;
133
+ let found;
134
+ try {
135
+ const { size } = await fs.stat(file);
136
+ if (size > 0) {
137
+ const near = await readTail(file, size, TAIL_BYTES);
138
+ found = scanBackwards(near.text, near.whole);
139
+ // Nothing qualified, and there is more file to look at: one transcript
140
+ // record can be megabytes (a large tool result), so the window can hold a
141
+ // single unterminated line. One wider retry, then give up to the mtime.
142
+ if (found === undefined && !near.whole) {
143
+ const far = await readTail(file, size, WIDE_TAIL_BYTES);
144
+ found = scanBackwards(far.text, far.whole);
145
+ }
146
+ }
147
+ }
148
+ catch {
149
+ found = undefined;
150
+ }
151
+ mtimeCacheSet(cache, file, mtimeMs, found);
152
+ return found;
153
+ }
154
+ /**
155
+ * Attach `lastMessageAt` to each session and order them newest-first.
156
+ *
157
+ * The sort lives here rather than in the client because the client is not the
158
+ * only reader: the MCP `list_chats` tool and the project-detail payload both
159
+ * take the server's order as given. The comparison falls back to `mtime`
160
+ * per-session rather than for the list as a whole, so a chat with no datable
161
+ * record still sorts sensibly against ones that have.
162
+ */
163
+ export async function withLastActivity(projectDir, sessions) {
164
+ const enriched = await Promise.all(sessions.map(async (s) => {
165
+ const at = await lastMessageAt(projectDir, s.sessionId).catch(() => undefined);
166
+ return at === undefined ? { ...s } : { ...s, lastMessageAt: at };
167
+ }));
168
+ // ISO-8601 sorts lexicographically in chronological order, but only against
169
+ // another ISO-8601 string in the same shape — `Date.parse` rather than `<`,
170
+ // because `lastMessageAt` comes out of a transcript written by Claude Code
171
+ // and `mtime` out of `toISOString()`, and mixing offsets would otherwise
172
+ // compare as text.
173
+ return enriched.sort((a, b) => activityMs(b) - activityMs(a));
174
+ }
175
+ /** Epoch ms a session should be ordered by: its last message, else its mtime. */
176
+ export function activityMs(s) {
177
+ return Date.parse(s.lastMessageAt ?? s.mtime) || 0;
178
+ }
@@ -113,7 +113,11 @@ export function buildManagementOps(ctx, params) {
113
113
  project: p.slug,
114
114
  sessionId: s.sessionId,
115
115
  name: s.customName ?? s.autoName ?? s.sessionId.slice(0, 8),
116
- updatedAt: s.mtime,
116
+ // Last real message, else the file mtime (#863) — the same rule
117
+ // `chat-dto.ts` applies, because the MCP list and the web list
118
+ // disagreeing about when a chat was last used is how a background
119
+ // agent picks the wrong chat to follow up on.
120
+ updatedAt: s.lastMessageAt ?? s.mtime,
117
121
  running: hub.isRunning(s.sessionId),
118
122
  // #489: the archived flag the web UI has always had (`chat-dto.ts`) but
119
123
  // the MCP surface never reported — so the two disagreed about what "the
@@ -37,7 +37,7 @@
37
37
  * here; on a shared authenticated instance it is worth knowing about.
38
38
  */
39
39
  import path from "node:path";
40
- import { defaultTempRoots, discoverCandidates, discoverSessions, DiscoverPathError, } from "../discover.js";
40
+ import { defaultTempRoots, discoverCandidates, discoverSessions, DiscoverPathError, withoutOwnTranscriptFolders, } from "../discover.js";
41
41
  import { ROOT_KEY } from "../project-paths.js";
42
42
  /** A `?flag=` that is present and not explicitly falsy. */
43
43
  function flag(value) {
@@ -54,7 +54,14 @@ export function registerDiscoverRoutes(app, ctx) {
54
54
  async function context() {
55
55
  const existing = [...(await projects.list()), await projects.get(ROOT_KEY)];
56
56
  return {
57
- folders: await herdctl.transcriptFolders(),
57
+ // Paddock's own `.chats` bridges are removed BEFORE the scan sees them, so
58
+ // they are not counted in `scanned` either (#865). A pristine instance has
59
+ // two of them and nothing else, and counting those was what made
60
+ // `scanned === 0` — "no history on this machine" — unreachable.
61
+ folders: withoutOwnTranscriptFolders(await herdctl.transcriptFolders(), {
62
+ ownRoots: [cfg.projectsRoot, cfg.dataDir],
63
+ claudeHomes: [cfg.claudeHome, cfg.legacyClaudeHome],
64
+ }),
58
65
  claudeHome: cfg.claudeHome,
59
66
  // The user's own home, taken from the one place config already resolves it
60
67
  // (`legacyClaudeHome` is `os.homedir()/.claude`, with no override) so this
@@ -73,7 +80,7 @@ export function registerDiscoverRoutes(app, ctx) {
73
80
  schema: {
74
81
  tags: ["System"],
75
82
  summary: "Directories on this machine that could become projects",
76
- description: "Scans `<claudeHome>/projects/*` for directories with existing Claude Code history and returns the ones worth importing, ranked by how many non-noise sessions each holds. Never walks the filesystem: the only directories it can name are ones a transcript already records, and each is put through the same path floor a linked project must clear (no system directories, no temp roots, nothing inside Paddock's own data dir, nothing overlapping an existing project). Two rules are relaxable: `includeNonGit=1` offers directories that are not git checkouts, `includeOutsideHome=1` offers directories outside `$HOME`. Note the two counts are in DIFFERENT units and do not reconcile by subtraction: `scanned` counts transcript FOLDERS, while `excluded` reports how many DIRECTORIES each rule ate — one directory routinely has two transcript folders (the user's own and Paddock's copy), so a rule that ate one directory may account for two scanned folders. The single exception is `no-recorded-cwd`, which is necessarily per-folder: a folder with no recorded cwd has no directory to be counted under. A container whose Claude home is empty reports `scanned: 0`, which is what distinguishes 'no history' from 'all of it was filtered'. Response: `{ claudeHome, homeDir, scanned, candidates, excluded }`.",
83
+ description: "Scans `<claudeHome>/projects/*` for directories with existing Claude Code history and returns the ones worth importing, ranked by how many non-noise sessions each holds. Never walks the filesystem: the only directories it can name are ones a transcript already records, and each is put through the same path floor a linked project must clear (no system directories, no temp roots, nothing inside Paddock's own data dir, nothing overlapping an existing project). Two rules are relaxable: `includeNonGit=1` offers directories that are not git checkouts, `includeOutsideHome=1` offers directories outside `$HOME`. Note the two counts are in DIFFERENT units and do not reconcile by subtraction: `scanned` counts transcript FOLDERS, while `excluded` reports how many DIRECTORIES each rule ate — one directory routinely has two transcript folders (the user's own and Paddock's copy), so a rule that ate one directory may account for two scanned folders. The single exception is `no-recorded-cwd`, which is necessarily per-folder: a folder with no recorded cwd has no directory to be counted under. A machine with no Claude Code history reports `scanned: 0`, which is what distinguishes 'no history' from 'all of it was filtered'. Paddock's own `.chats` bridge folders are excluded before that count is taken — they exist on an instance nobody has used yet, and counting them made `scanned: 0` unreachable. Response: `{ claudeHome, homeDir, scanned, candidates, excluded }`.",
77
84
  querystring: {
78
85
  type: "object",
79
86
  properties: {
@@ -68,7 +68,10 @@ export interface SelfMcpChat {
68
68
  sessionId: string;
69
69
  /** Display name (custom name, else auto-name, else a short id). */
70
70
  name: string;
71
- /** ISO timestamp of the last transcript write (mtime). */
71
+ /**
72
+ * ISO timestamp of the last real message in the transcript (#863) — the file's
73
+ * mtime only when no message in it carries a usable date.
74
+ */
72
75
  updatedAt: string;
73
76
  /** Whether a turn is currently in flight for this chat. */
74
77
  running: boolean;
@@ -179,9 +179,11 @@ export async function ensureProjectChats(workingDir, chatsHostDir, home) {
179
179
  // cp+rm is robust across filesystems (rename would EXDEV across mounts).
180
180
  // `preserveTimestamps` is load-bearing, not cosmetic (#588): fs.cp
181
181
  // defaults to stamping the copy with NOW, and a transcript's mtime is
182
- // both the chat-list sort key and the cache key for auto-name / preview /
183
- // sidechain detection — so without it, relocating a months-old archive
184
- // collapses every one of those chats to "today".
182
+ // the cache key for auto-name / preview / sidechain detection — so
183
+ // without it, relocating a months-old archive re-derives every one of
184
+ // those. It is no longer the chat-list sort key (#863, that now comes
185
+ // from the last message INSIDE the transcript), which narrows the blast
186
+ // radius of getting this wrong but does not remove it.
185
187
  await fs.cp(from, to, { recursive: true, preserveTimestamps: true });
186
188
  await fs.rm(from, { recursive: true, force: true });
187
189
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paddock/server",
3
- "version": "0.71.0",
3
+ "version": "0.71.1",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -0,0 +1,2 @@
1
+ import{j as e,q as c,r as u}from"./index-ASW1Jl0Y.js";const h={primary:"bg-accent-solid text-accent-fg shadow-xs hover:bg-accent-solid-hover active:scale-[0.98]",ghost:"bg-surface-active text-fg hover:bg-surface-selected",subtle:"text-fg-muted hover:bg-surface-hover hover:text-fg",danger:"bg-danger-solid text-danger-fg shadow-xs hover:brightness-110 active:scale-[0.98]",link:"text-accent underline underline-offset-2 hover:no-underline"},p={sm:"gap-1.5 px-2 py-1 text-xs rounded-md",md:"gap-2 px-3.5 py-2 text-sm rounded-lg","icon-sm":"h-6 w-6 justify-center rounded-md","icon-md":"h-9 w-9 justify-center rounded-lg"};function w({variant:s="ghost",size:t="md",icon:r,loading:a=!1,loadingLabel:n,align:o="center",className:d,children:l,disabled:i,type:m="button",...b}){return e.jsxs("button",{type:m,disabled:i||a,"aria-busy":a||void 0,className:c("inline-flex select-none items-center font-medium","motion-fast transition-[color,background-color,border-color,box-shadow,transform]","disabled:cursor-not-allowed disabled:opacity-50",o==="start"?"justify-start text-left":"justify-center",p[t],h[s],d),...b,children:[r,a&&n?n:l]})}function j({interactive:s=!1,flush:t=!1,className:r,...a}){return e.jsx("div",{className:c("rounded-2xl border border-edge bg-surface-raised shadow-xs",t?"":"p-4",s&&"motion-fast transition-[border-color,box-shadow] hover:border-edge-strong hover:shadow-sm",r),...a})}function v({title:s,description:t,action:r,id:a,variant:n="card",first:o=!1,flush:d=!1,className:l,children:i}){return e.jsxs("section",{id:a,className:c("scroll-mt-4",n==="rule"&&!o&&"mt-9 border-t border-edge pt-7",n==="rule"?"":"mb-6",l),children:[e.jsxs("div",{className:"flex items-baseline justify-between gap-3",children:[e.jsx("h3",{className:c(n==="rule"?"text-base font-semibold tracking-tight text-fg":"text-sm font-semibold uppercase tracking-wide text-fg-muted"),children:s}),r]}),t&&e.jsx("p",{className:"mt-0.5 text-xs leading-snug text-fg-muted",children:t}),n==="card"?e.jsx(j,{className:"mt-2",flush:d,children:i}):e.jsx("div",{className:"mt-4",children:i})]})}function y({title:s,body:t,action:r,icon:a,variant:n="inline",className:o}){return n==="inline"?e.jsxs("div",{className:c("px-1 py-2",o),children:[e.jsx("p",{className:"text-sm text-fg-muted",children:s}),t&&e.jsx("p",{className:"mt-0.5 text-xs text-fg-subtle",children:t}),r&&e.jsx("div",{className:"mt-2",children:r})]}):e.jsxs("div",{className:c("mx-auto max-w-lg rounded-2xl border border-dashed border-edge px-8 py-12 text-center",o),children:[a&&e.jsx("div",{className:"mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-accent-soft text-accent",children:a}),e.jsx("h3",{className:"text-base font-semibold text-fg",children:s}),t&&e.jsx("p",{className:"mx-auto mt-1.5 max-w-sm text-sm text-fg-muted",children:t}),r&&e.jsx("div",{className:"mt-5 flex items-center justify-center gap-2",children:r})]})}const x=["w-full rounded-lg border border-edge-strong bg-surface-sunken px-3 py-2 text-sm text-fg","outline-none motion-fast transition-[border-color,box-shadow]","focus:border-accent focus:ring-2 focus:ring-accent/25","disabled:cursor-not-allowed disabled:opacity-60","aria-[invalid=true]:border-danger aria-[invalid=true]:focus:ring-danger/25"].join(" ");function k({className:s,...t}){return e.jsx("input",{className:c(x,s),...t})}function S({className:s,...t}){return e.jsx("textarea",{className:c(x,"resize-y",s),...t})}function E({className:s,...t}){return e.jsx("select",{className:c(x,s),...t})}function C({checked:s,onChange:t,label:r,disabled:a,id:n,className:o}){const d=u.useId(),l=n??d;return e.jsxs("label",{htmlFor:l,className:c("relative inline-block shrink-0 cursor-pointer",o),children:[e.jsx("input",{id:l,type:"checkbox","aria-label":r,className:"peer sr-only",checked:s,disabled:a,onChange:i=>t(i.target.checked)}),e.jsx("span",{className:c("block h-[18px] w-8 rounded-full bg-edge-strong","motion-fast transition-colors","after:absolute after:left-0.5 after:top-0.5 after:h-3.5 after:w-3.5 after:rounded-full","after:bg-surface-raised after:shadow-2xs after:transition-transform after:content-['']","peer-checked:bg-accent-solid peer-checked:after:translate-x-3.5","peer-focus-visible:ring-2 peer-focus-visible:ring-accent/50","peer-disabled:opacity-50")})]})}function T({label:s,description:t,indeterminate:r=!1,className:a,...n}){const o=u.useRef(null);u.useEffect(()=>{o.current&&(o.current.indeterminate=r)},[r]);const d=e.jsx("input",{ref:o,type:"checkbox",...r?{"aria-checked":"mixed"}:{},className:c("h-4 w-4 shrink-0 cursor-pointer rounded border-edge-strong accent-[var(--accent-solid)]","disabled:cursor-not-allowed disabled:opacity-50",s?"mt-0.5":"",a),...n});return s?e.jsxs("label",{className:"flex cursor-pointer items-start gap-2 text-sm text-fg",children:[d,e.jsxs("span",{className:"min-w-0",children:[s,t&&e.jsx("span",{className:"block text-xs leading-snug text-fg-muted",children:t})]})]}):d}const f={neutral:"bg-surface-active text-fg-muted",accent:"bg-accent-soft text-accent",success:"bg-success-soft text-success",warn:"bg-warn-soft text-warn",danger:"bg-danger-soft text-danger",info:"bg-info-soft text-info",lineage:"bg-lineage-soft text-lineage"},g={neutral:"bg-fg-subtle",accent:"bg-accent-solid",success:"bg-success-solid",warn:"bg-warn-solid",danger:"bg-danger-solid",info:"bg-info-solid",lineage:"bg-lineage-solid"};function I({tone:s="neutral",shape:t="tag",size:r="md",dot:a=!1,icon:n,title:o,className:d,children:l}){return e.jsxs("span",{title:o,className:c("inline-flex items-center gap-1 font-medium leading-none",t==="pill"?"rounded-full px-2 py-0.5":"rounded-md px-1.5 py-0.5",r==="sm"?"text-3xs":"text-2xs",f[s],d),children:[a&&e.jsx("span",{className:c("h-1.5 w-1.5 shrink-0 rounded-full",g[s])}),n,l]})}function O({tone:s="neutral",pulse:t=!1,className:r}){return e.jsx("span",{className:c("inline-block h-2 w-2 shrink-0 rounded-full",g[s],t&&"animate-pulse",r)})}function R({tone:s="info",icon:t,className:r,children:a}){const n={neutral:"border-edge",accent:"border-accent-edge",success:"border-success-edge",warn:"border-warn-edge",danger:"border-danger-edge",info:"border-info-edge",lineage:"border-lineage-edge"};return e.jsxs("div",{role:s==="danger"?"alert":void 0,className:c("flex items-start gap-2 rounded-lg border px-3 py-2 text-sm",f[s],n[s],r),children:[t&&e.jsx("span",{className:"mt-0.5 shrink-0",children:t}),e.jsx("div",{className:"min-w-0",children:a})]})}export{w as B,I as C,y as E,k as I,O as S,C as T,v as a,R as b,E as c,S as d,j as e,T as f};
2
+
@@ -0,0 +1,2 @@
1
+ import{j as r}from"./index-ASW1Jl0Y.js";import{D as o}from"./DiscoverView-C4hOB3nr.js";import"./Chip-Db--PJFH.js";function s(){return r.jsx(o,{})}export{s as DiscoverPage};
2
+
@@ -0,0 +1,2 @@
1
+ import{a as oe,u as ie,H as de,r as u,b as A,j as s,D as X,J as Y,M as ce,E as le,P as he,K as ue,q,O as pe,h as xe}from"./index-ASW1Jl0Y.js";import{b as Z,E as O,B as $,e as me,f as J,C as T,T as V}from"./Chip-Db--PJFH.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]??R(),r),0)}function be(e,t){return t.filter(n=>C(e[n.path]??R(),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??R();return C(n,t)===0?null:n.sessions&&n.selected?{sessionIds:[...n.selected]}:{}}function R(){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:Q(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:Q(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 _(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=_(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=_(e.skipped),c=`Adopted ${r} chat${r===1?"":"s"} into “${n}”`;return a?`${c} — ${a}.`:`${c}.`}function Q(e){return e instanceof Error?e.message:String(e)}function Be({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),[H,P]=u.useState(!0),[f,v]=u.useState({}),[k,B]=u.useState(!1),[L,F]=u.useState({}),[E,G]=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;B(!0),F(Object.fromEntries(b.map(i=>[i.path,"pending"]))),G({});const o=b.map(i=>({candidate:i,plan:Ne(f[i.path],i)??{}}));await $e(A,o,(i,p,g)=>{F(N=>({...N,[i]:p})),g&&G(N=>({...N,[i]:g}))}),B(!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,K=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(Z,{tone:"danger",icon:s.jsx(X,{width:16,height:16}),children:y}),H&&s.jsx(De,{}),!H&&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(O,{variant:"panel",icon:s.jsx(Y,{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?K: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:K})})]})}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:q("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 ",W(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(Z,{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:q("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":U(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:U(x)}),s.jsxs("span",{className:"tabular mt-0.5 block text-3xs text-fg-muted",children:[W(x.mtime)," · ",Re(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:Oe(e)}),d&&s.jsxs("div",{className:"flex items-center gap-2 text-xs text-fg-muted",children:[s.jsx(V,{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(V,{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(O,{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(O,{variant:"panel",icon:s.jsx(Y,{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 Oe(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 U(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 W(e){const t=new Date(e);return Number.isNaN(t.getTime())?"unknown date":t.toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"})}function Re(e){return e<1024?`${e} B`:e<1024*1024?`${Math.round(e/1024)} kB`:`${(e/(1024*1024)).toFixed(1)} MB`}export{Be as D};
2
+
@@ -0,0 +1,2 @@
1
+ import{r as u,m as W,s as Z,A as ee,n as pe,j as e,p as ae,q as b,t as ie,v as be,w as je,x as le,y as ve,z as ye,b as O,D as oe,E as we,X as Ne,F as ke,G as Ce,H as Se,M as Ae,I as Ee}from"./index-ASW1Jl0Y.js";import{B as A,C as ce,S as L,a as te,I as B,b as Re,T as $e,c as Me,d as Pe}from"./Chip-Db--PJFH.js";const Ie=Array.from({length:72},(t,n)=>n*5),S=[...ie].sort((t,n)=>t.hue-n.hue),z=(t,n)=>Math.min(Math.abs(t-n),360-Math.abs(t-n));function ne(t){const n=S.reduce((a,s)=>z(s.hue,t)<z(a.hue,t)?s:a);if(z(n.hue,t)<=8)return n.name;const r=S.find(a=>a.hue>t)??S[0];return`between ${S[(S.indexOf(r)-1+S.length)%S.length].name} and ${r.name}`}function Fe({theme:t,dark:n,hue:r,className:c}){const a=u.useRef(null);return u.useLayoutEffect(()=>{const s=a.current;s&&(r===null?je(s):le(r,s))},[t,n,r]),e.jsx("div",{ref:a,"data-theme":t,className:b("overflow-hidden rounded-lg border border-edge",n&&"dark",c),children:e.jsxs("div",{className:"flex h-full items-center gap-2 bg-surface p-2",children:[e.jsxs("div",{className:"flex-1 rounded-md border border-edge-subtle bg-surface-raised p-1.5",children:[e.jsx("div",{className:"h-1 w-4/5 rounded-full bg-fg"}),e.jsx("div",{className:"mt-1 h-1 w-3/5 rounded-full bg-fg-subtle"})]}),e.jsx("div",{className:"grid h-6 w-6 shrink-0 place-items-center rounded-md bg-accent-solid",children:e.jsx("div",{className:"h-1 w-3 rounded-full bg-accent-fg"})})]})})}function Te({hexes:t,hue:n,active:r,onCommit:c}){const a=u.useRef(null),s=u.useRef(null),d=u.useRef(null),x=u.useRef(0),h=u.useRef(null),p=u.useRef(!1),g=u.useRef(null),l=u.useMemo(()=>{if(t.length===0)return;const m=t.map((j,y)=>`${j} ${(y/t.length*100).toFixed(2)}%`);return m.push(`${t[0]} 100%`),`linear-gradient(to right, ${m.join(", ")})`},[t]),f=u.useCallback(m=>{s.current&&(s.current.style.left=`${m/360*100}%`),d.current&&(d.current.textContent=ne(m))},[]);u.useEffect(()=>{p.current||f(n)},[n,f]);const N=m=>{var $;const j=($=a.current)==null?void 0:$.getBoundingClientRect();if(!j||j.width===0)return 0;const y=Math.min(1,Math.max(0,(m-j.left)/j.width));return Math.round(y*360)%360},w=u.useCallback(m=>{h.current=m,!x.current&&(x.current=requestAnimationFrame(()=>{x.current=0;const j=h.current;h.current=null,j!==null&&(f(j),le(j,document.documentElement,{context:g.current??void 0,live:!0}))}))},[f]);u.useEffect(()=>()=>cancelAnimationFrame(x.current),[]);const F=m=>{p.current=!1,g.current=null,cancelAnimationFrame(x.current),x.current=0,h.current=null,c(m)};return e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"mt-2 flex items-baseline justify-between gap-2",children:[e.jsx("span",{className:"text-3xs text-fg-subtle",children:"Drag along the strip"}),e.jsx("span",{ref:d,className:"text-3xs font-medium text-fg-muted","aria-hidden":!0})]}),e.jsx("div",{ref:a,role:"slider",tabIndex:0,"aria-label":"Accent colour","aria-valuemin":0,"aria-valuemax":359,"aria-valuenow":n,"aria-valuetext":r?ne(n):"the theme’s own colour",onPointerDown:m=>{m.currentTarget.setPointerCapture(m.pointerId),p.current=!0,g.current=ve(),w(N(m.clientX))},onPointerMove:m=>{p.current&&w(N(m.clientX))},onPointerUp:m=>{p.current&&F(N(m.clientX))},onPointerCancel:()=>{p.current&&F(n)},onKeyDown:m=>{const j=m.key==="PageUp"||m.key==="PageDown"?15:m.shiftKey?10:2;let y=null;m.key==="ArrowLeft"||m.key==="ArrowDown"||m.key==="PageDown"?y=(n-j+360)%360:m.key==="ArrowRight"||m.key==="ArrowUp"||m.key==="PageUp"?y=(n+j)%360:m.key==="Home"?y=0:m.key==="End"&&(y=359),y!==null&&(m.preventDefault(),c(y))},style:{backgroundImage:l,touchAction:"none"},className:b("relative mt-1 h-8 cursor-pointer rounded-lg border border-edge shadow-xs select-none","focus-visible:focus-ring"),children:e.jsx("div",{ref:s,"aria-hidden":!0,style:{left:`${n/360*100}%`},className:b("pointer-events-none absolute top-1/2 h-[calc(100%+6px)] w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full","border border-fg bg-surface-raised shadow-sm",!r&&"opacity-0")})})]})}function Oe({className:t}){const[n,r]=u.useState(W),[c,a]=u.useState(null),s=typeof document<"u"&&document.documentElement.classList.contains("dark"),d=u.useRef(n),x=u.useCallback(l=>{const f={...d.current,...l};d.current=f,r(f),a(Z(f))},[]);u.useEffect(()=>{const l=f=>a(f.detail);return window.addEventListener(ee,l),a(Z(W())),()=>window.removeEventListener(ee,l)},[]);const h=u.useMemo(()=>pe(Ie),[n.theme,s,c==null?void 0:c.mode]),p=(c==null?void 0:c.hue)??0,g=n.hue!==null;return e.jsxs("div",{className:b("space-y-5",t),children:[e.jsxs("fieldset",{children:[e.jsx("legend",{className:"text-2xs font-semibold uppercase tracking-wide text-fg-muted",children:"Theme"}),e.jsx("div",{className:b("mt-2 grid gap-2","grid-cols-2 sm:grid-cols-4"),children:ae.map(l=>{const f=n.theme===l.id;return e.jsxs("button",{type:"button","aria-pressed":f,title:`${l.label} — ${l.blurb}`,onClick:()=>x({theme:l.id}),className:b("motion-fast rounded-xl border text-left transition-[border-color,box-shadow]","focus-visible:focus-ring","p-1.5",f?"border-accent-edge bg-accent-soft shadow-xs":"border-edge bg-surface-raised hover:border-edge-strong"),children:[e.jsx(Fe,{theme:l.id,dark:s,hue:n.hue,className:"h-10"}),e.jsxs("div",{className:"mt-1.5 min-w-0 px-0.5 pb-0.5",children:[e.jsx("span",{className:"block truncate text-xs font-semibold text-fg",children:l.label}),e.jsx("span",{className:"mt-0.5 line-clamp-2 text-3xs leading-snug text-fg-muted",children:l.blurb})]})]},l.id)})})]}),e.jsxs("div",{className:b("grid gap-5 lg:grid-cols-[minmax(0,1fr)_15rem] lg:items-start"),children:[e.jsxs("fieldset",{children:[e.jsx("legend",{className:"text-2xs font-semibold uppercase tracking-wide text-fg-muted",children:"Accent colour"}),e.jsx("p",{className:"mt-1 text-3xs leading-snug text-fg-subtle",children:"Pick any colour. The theme adjusts it so buttons and labels stay readable in both light and dark."}),e.jsx(Te,{hexes:h,hue:p,active:g,onCommit:l=>x({hue:l})}),e.jsxs("div",{className:"mt-2 flex flex-wrap gap-1",children:[e.jsx("button",{type:"button",onClick:()=>x({hue:null}),"aria-pressed":!g,className:b("motion-fast rounded-md border px-2 py-1 text-3xs transition-colors","focus-visible:focus-ring",g?"border-edge text-fg-muted hover:border-edge-strong hover:text-fg":"border-accent-edge bg-accent-soft text-accent"),children:"Theme’s own"}),ie.map(l=>{const f=g&&Math.abs(p-l.hue)<2.5;return e.jsxs("button",{type:"button","aria-pressed":f,onClick:()=>x({hue:l.hue}),className:b("motion-fast inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-3xs transition-colors","focus-visible:focus-ring",f?"border-edge-strong bg-surface-active text-fg":"border-edge text-fg-muted hover:border-edge-strong hover:text-fg"),children:[e.jsx("span",{"aria-hidden":!0,className:"h-2.5 w-2.5 rounded-full",style:{backgroundColor:h[Math.round(l.hue/5)%h.length]}}),l.name]},l.name)})]})]}),e.jsxs("fieldset",{children:[e.jsx("legend",{className:"text-2xs font-semibold uppercase tracking-wide text-fg-muted",children:"Tint the background"}),e.jsx("p",{className:"mt-1 text-3xs leading-snug text-fg-subtle",children:"Colours the whole page a little, not just the buttons. Useful for telling two Paddock instances apart at a glance."}),e.jsx("div",{className:"mt-2 inline-flex rounded-lg border border-edge p-0.5",children:be.map(l=>e.jsx("button",{type:"button","aria-pressed":n.tint===l.level,onClick:()=>x({tint:l.level}),className:b("motion-fast rounded-md px-2.5 py-1 text-3xs transition-colors","focus-visible:focus-ring",n.tint===l.level?"bg-surface-active font-medium text-fg":"text-fg-muted hover:text-fg"),children:l.label},l.level))})]})]}),e.jsx(Le,{})]})}function Le(){return e.jsxs("div",{children:[e.jsx("p",{className:"text-2xs font-semibold uppercase tracking-wide text-fg-muted",children:"Preview"}),e.jsxs("div",{className:"mt-2 flex flex-wrap items-center gap-2 rounded-xl border border-edge bg-surface p-3",children:[e.jsx(A,{variant:"primary",size:"sm",children:"Send"}),e.jsx(A,{variant:"ghost",size:"sm",children:"Cancel"}),e.jsx(A,{variant:"link",size:"sm",children:"A link"}),e.jsx("span",{className:"inline-flex items-center gap-1 rounded-md border border-accent-edge bg-accent-soft px-1.5 py-0.5 text-2xs font-medium text-accent",children:"accent chip"}),e.jsx(ce,{tone:"accent",shape:"pill",dot:!0,children:"running"}),e.jsxs("span",{className:"inline-flex items-center gap-1.5 text-2xs text-fg-subtle",children:[e.jsx("span",{className:"h-4 w-4 rounded-md border border-edge bg-accent-solid-hover","aria-hidden":!0}),"hovered"]}),e.jsxs("span",{className:"ml-auto inline-flex items-center gap-1.5",children:[e.jsx(L,{tone:"success"}),e.jsx(L,{tone:"warn"}),e.jsx(L,{tone:"danger"}),e.jsx(L,{tone:"info"}),e.jsx("span",{className:"text-2xs text-fg-subtle",children:"status hues (theme’s, not yours)"})]})]})]})}const _="mx-auto w-full max-w-4xl",De="sm:w-64";function Ve(){const[t,n]=u.useState(null),[r,c]=u.useState(!0),[a,s]=u.useState(null),[d,x]=u.useState({}),[h,p]=u.useState(!1),[g,l]=u.useState(null),[f,N]=u.useState(!1),[w,F]=u.useState(""),[m,j]=u.useState(!1),[y,$]=u.useState(null),K=u.useRef(null),de=ye("(min-width: 1024px)");u.useEffect(()=>{let i=!0;return c(!0),O.getInstanceConfig().then(o=>{i&&(n(o),s(null))}).catch(o=>i&&s(o instanceof Error?o.message:String(o))).finally(()=>i&&c(!1)),()=>{i=!1}},[]);const ue=(i,o)=>{N(!1),l(null),x(v=>({...v,[i]:o}))},me=i=>se(i,d),U=u.useMemo(()=>t?t.groups.flatMap(i=>i.fields):[],[t]),k=u.useMemo(()=>U.filter(i=>i.editable&&!i.envOverridden).filter(i=>Object.prototype.hasOwnProperty.call(d,i.key)).filter(i=>!re(d[i.key],q(i))).map(i=>i.key),[U,d]),X=u.useMemo(()=>new Set(k),[k]),C=u.useMemo(()=>{if(!t)return[];const i=w.trim().toLowerCase(),o=[];for(const v of t.groups){const P=i!==""&&v.label.toLowerCase().includes(i),I=v.fields.filter(E=>{const T=se(E,d);return m&&re(T,E.default)?!1:i===""||P?!0:Ye(E).includes(i)});I.length>0&&o.push({...v,fields:I})}return o},[t,w,m,d]),J=C.reduce((i,o)=>i+o.fields.length,0),xe=w.trim()!==""||m,H=!m&&(w.trim()===""||We.includes(w.trim().toLowerCase())),M=u.useMemo(()=>H?[V,...C]:C,[H,C]),he=C.some(i=>i.fields.some(o=>o.envOverridden)),Y=Qe(C,X);u.useEffect(()=>{const i=K.current;if(!i||M.length===0)return;const o=M.map(P=>P.id),v=()=>{const P=i.getBoundingClientRect().top;let I=o[0];for(const E of o){const T=document.getElementById(D(E));if(T)if(T.getBoundingClientRect().top-P<=80)I=E;else break}$(I)};return v(),i.addEventListener("scroll",v,{passive:!0}),()=>i.removeEventListener("scroll",v)},[M]);const Q=i=>{var v;const o=document.getElementById(D(i));(v=o==null?void 0:o.scrollIntoView)==null||v.call(o,{behavior:"smooth",block:"start"}),$(i)},fe=async()=>{if(k.length===0||!t)return;const i={};for(const o of k)i[o]=d[o];p(!0),l(null);try{await O.updateInstanceConfig(i,t.configVersion);const o=await O.getInstanceConfig();n(o),x({}),N(!0)}catch(o){if(o instanceof Ce&&o.status===409)try{n(await O.getInstanceConfig())}catch{}l(o instanceof Error?o.message:String(o))}finally{p(!1)}},ge=()=>{x({}),l(null),N(!1)};return e.jsxs("div",{className:"flex min-h-0 flex-1",children:[t&&e.jsx(Be,{groups:M,active:y,onJump:Q,dirtyCount:Y}),e.jsxs("div",{className:"flex min-h-0 min-w-0 flex-1 flex-col",children:[t&&e.jsx("div",{className:"border-b border-edge px-4 py-2.5 sm:px-6",children:e.jsxs("div",{className:_,children:[e.jsx(He,{query:w,onQuery:F,modifiedOnly:m,onModifiedOnly:j,visibleCount:J,totalCount:U.length,filtering:xe,autoFocus:de}),e.jsx(Ue,{groups:M,active:y,onJump:Q,dirtyCount:Y})]})}),e.jsx("div",{ref:K,className:"min-h-0 flex-1 overflow-y-auto px-4 py-5 sm:px-6",children:e.jsxs("div",{className:_,children:[H&&e.jsx("div",{className:"mb-8 border-b border-edge pb-7",children:e.jsx(te,{id:D(V.id),title:V.label,description:V.description,variant:"rule",first:!0,children:e.jsx(Oe,{})})}),e.jsx(ze,{saved:f,configPath:t==null?void 0:t.configPath,restartRequired:(t==null?void 0:t.restartRequired)??!1,fileError:t==null?void 0:t.configFileError}),r&&e.jsx("p",{className:"text-sm text-fg-muted",children:"Loading…"}),a&&e.jsxs("p",{className:"text-sm text-danger",role:"alert",children:["Failed to load settings: ",a]}),t&&he&&e.jsx(_e,{}),t&&J===0&&e.jsxs("p",{className:"py-8 text-center text-sm text-fg-muted",children:["No settings match"," ",w.trim()?e.jsxs(e.Fragment,{children:["“",e.jsx("span",{className:"font-medium",children:w.trim()}),"”"]}):"this filter","."]}),C.map(i=>e.jsx(te,{id:D(i.id),title:i.label,description:i.description,variant:"card",flush:!0,children:e.jsx("div",{className:"divide-y divide-edge-subtle",children:i.fields.map(o=>e.jsx(Ge,{field:o,value:me(o),dirty:X.has(o.key),onChange:v=>ue(o.key,v)},o.key))})},i.id))]})}),t&&e.jsx("footer",{className:"border-t border-edge px-4 py-3 sm:px-6",children:e.jsxs("div",{className:`${_} flex items-center gap-3`,children:[g&&e.jsxs("span",{className:"flex min-w-0 items-center gap-1.5 text-sm text-danger",role:"alert",children:[e.jsx(oe,{width:14,height:14,className:"shrink-0"}),e.jsx("span",{className:"truncate",children:g})]}),e.jsx("span",{className:"ml-auto shrink-0 text-xs tabular text-fg-subtle",children:k.length>0?`${k.length} unsaved change${k.length===1?"":"s"}`:"No changes"}),e.jsx(A,{variant:"subtle",onClick:ge,disabled:k.length===0||h,children:"Reset"}),e.jsx(A,{variant:"primary",onClick:fe,disabled:k.length===0,loading:h,loadingLabel:"Saving…",children:"Save changes"})]})})]})]})}function Be({groups:t,active:n,onJump:r,dirtyCount:c}){return e.jsx("nav",{"aria-label":"Config sections",className:"hidden w-52 shrink-0 overflow-y-auto border-r border-edge px-2 py-4 lg:block",children:e.jsx("ul",{className:"space-y-0.5",children:t.map(a=>{const s=a.id===n,d=c[a.id]??0;return e.jsx("li",{children:e.jsxs("button",{type:"button",onClick:()=>r(a.id),"aria-current":s?"true":void 0,className:b("motion-fast flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-sm transition-colors",s?"bg-surface-selected font-medium text-fg":"text-fg-muted hover:bg-surface-hover hover:text-fg"),children:[e.jsx("span",{className:"min-w-0 flex-1 truncate",children:a.label}),d>0&&e.jsx("span",{className:"h-1.5 w-1.5 shrink-0 rounded-full bg-accent-solid","aria-label":`${d} unsaved`}),a.fields.length>0&&e.jsx("span",{className:"shrink-0 text-2xs tabular text-fg-subtle",children:a.fields.length})]})},a.id)})})})}function Ue({groups:t,active:n,onJump:r,dirtyCount:c}){return t.length===0?null:e.jsx("nav",{"aria-label":"Config sections",className:"-mx-1 mt-2 flex gap-1 overflow-x-auto px-1 pb-0.5 lg:hidden",children:t.map(a=>{const s=a.id===n,d=c[a.id]??0;return e.jsxs("button",{type:"button",onClick:()=>r(a.id),"aria-current":s?"true":void 0,className:b("motion-fast flex shrink-0 items-center gap-1.5 rounded-md px-2.5 py-1 text-xs transition-colors",s?"bg-surface-selected font-medium text-fg":"text-fg-muted hover:bg-surface-hover hover:text-fg"),children:[a.label,d>0&&e.jsx("span",{className:"h-1.5 w-1.5 rounded-full bg-accent-solid","aria-label":`${d} unsaved`})]},a.id)})})}function He({query:t,onQuery:n,modifiedOnly:r,onModifiedOnly:c,visibleCount:a,totalCount:s,filtering:d,autoFocus:x}){return e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsxs("div",{className:"relative min-w-0 flex-1 sm:max-w-sm",children:[e.jsx(we,{width:14,height:14,className:"pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-fg-subtle"}),e.jsx(B,{type:"search","aria-label":"Search settings",placeholder:"Search settings, keys, env vars…",className:"h-8 py-0 pl-8 pr-7 [&::-webkit-search-cancel-button]:appearance-none",value:t,autoFocus:x,onChange:h=>n(h.target.value),onKeyDown:h=>{h.key==="Escape"&&t!==""&&(h.preventDefault(),n(""))}}),t!==""&&e.jsx("button",{type:"button","aria-label":"Clear search",onClick:()=>n(""),className:"absolute right-2 top-1/2 -translate-y-1/2 text-fg-subtle hover:text-fg",children:e.jsx(Ne,{width:13,height:13})})]}),e.jsx("button",{type:"button","aria-pressed":r,onClick:()=>c(!r),className:b("motion-fast shrink-0 rounded-md border px-2.5 py-1 text-xs transition-colors",r?"border-accent-edge bg-accent-soft text-accent":"border-edge text-fg-muted hover:text-fg"),children:"Modified only"}),e.jsx("span",{className:"shrink-0 text-xs tabular text-fg-subtle",children:d?`${a} of ${s}`:`${s} settings`})]})}function ze({saved:t,configPath:n,restartRequired:r,fileError:c}){const a=c?"danger":t?"success":"warn";return e.jsx(Re,{tone:a,className:"mb-5 leading-snug",icon:a==="success"?e.jsx(ke,{width:15,height:15}):e.jsx(oe,{width:15,height:15}),children:e.jsx("span",{role:"status",children:c?e.jsxs(e.Fragment,{children:[e.jsxs("strong",{children:["Could not read ",G(n),"."]})," ",c," — the values below are the running instance's; what a restart would load is unknown until the file parses."]}):t?e.jsxs(e.Fragment,{children:[e.jsx("strong",{children:"Saved to disk."})," These changes take effect only after the server restarts."]}):r?e.jsxs(e.Fragment,{children:[e.jsx("strong",{children:"Restart pending."})," ",e.jsx("code",{className:"font-mono text-xs",children:G(n)})," holds changes the running instance has not picked up — the fields below marked"," ",e.jsx(R,{tone:"warn",children:"restart"})," differ from what is in force right now."]}):e.jsxs(e.Fragment,{children:["Changes here are written to"," ",e.jsx("code",{className:"font-mono text-xs",children:G(n)})," and take effect only after the server restarts — the running instance keeps its current config until then."]})})})}function _e(){return e.jsxs("p",{className:"mb-5 text-xs leading-snug text-fg-muted",children:["Settings marked ",e.jsx(R,{tone:"neutral",children:"env"})," are overridden by an environment variable, which wins over this file — edit that variable (and restart) to change them."]})}function Ge({field:t,value:n,dirty:r,onChange:c}){const a=!t.editable||t.envOverridden,s=`cfg-${t.key}`,d=t.help?`${s}-help`:void 0,x=t.type==="boolean",h=u.useMemo(()=>Je(t,q(t)),[t]),p=x&&!a,g=!t.help&&!t.envOverridden&&!t.pendingRestart,l=g?"items-center":"items-start",f=g?"sm:items-center":"sm:items-start",N=a?e.jsx(Xe,{field:t,value:n}):x?e.jsx($e,{id:s,checked:!!n,onChange:c,label:t.label}):e.jsx(Ke,{field:t,value:n,onChange:c,inputId:s,describedBy:d});return e.jsxs("div",{className:"relative px-4 py-3",children:[r&&e.jsx("span",{"aria-hidden":!0,className:"absolute inset-y-2 left-0 w-0.5 rounded-full bg-accent-solid"}),e.jsxs("div",{className:b(h?"":p?`flex gap-4 ${l}`:`sm:flex sm:gap-6 ${f}`),children:[e.jsxs("div",{className:"min-w-0 flex-1",children:[e.jsxs("div",{className:"flex flex-wrap items-center gap-1.5",children:[e.jsx("label",{htmlFor:a?void 0:s,className:"text-sm font-medium text-fg",children:t.label}),t.sensitive&&e.jsx(R,{tone:"warn",children:"sensitive"}),t.envOverridden&&e.jsx(R,{tone:"neutral",title:`Overridden by environment variable ${t.envVar} — edit that env var (and restart) to change it.`,children:"env"}),!t.editable&&!t.envOverridden&&e.jsx(R,{tone:"neutral",children:"read-only"}),t.pendingRestart&&e.jsx(R,{tone:"warn",title:"The config file and the running instance disagree on this field — restart to apply.",children:"restart"})]}),t.help&&e.jsx("p",{id:d,className:"mt-0.5 text-xs leading-snug text-fg-muted",children:t.help}),t.envOverridden&&e.jsx("p",{className:"mt-1 truncate font-mono text-2xs text-fg-subtle",children:t.envVar}),t.pendingRestart&&e.jsxs("p",{className:"mt-1 text-2xs leading-snug text-warn",children:["In force now: ",e.jsx("span",{className:"font-mono",children:qe(t.value)})]})]}),e.jsx("div",{className:b(h?"mt-2":p?"flex shrink-0 justify-end":`mt-2 sm:mt-0 sm:shrink-0 ${De}`),children:N})]})]})}function qe(t){if(t==null||t==="")return"(not set)";const r=(Array.isArray(t)?t.join(", "):String(t)).replace(/\s+/g," ").trim();return r.length>60?`${r.slice(0,60)}…`:r}function R({children:t,tone:n,title:r}){return e.jsx(ce,{tone:n,size:"sm",title:r,className:"uppercase tracking-wide",children:t})}function Ke({field:t,value:n,onChange:r,inputId:c,describedBy:a}){if(t.type==="enum")return e.jsx(Me,{id:c,"aria-describedby":a,value:String(n??""),onChange:s=>r(s.target.value),children:(t.enumValues??[]).map(s=>e.jsx("option",{value:s,children:s},s))});if(t.type==="number")return e.jsx(B,{id:c,type:"number","aria-describedby":a,className:"tabular",value:n==null?"":String(n),placeholder:t.default===null?"default":String(t.default),onChange:s=>r(s.target.value===""?null:Number(s.target.value))});if(t.type==="text"){const s=n==null||n===t.default,d=n==null?String(t.default??""):String(n),x=typeof t.default=="string"&&!s;return e.jsxs("div",{children:[e.jsx(Pe,{id:c,"aria-describedby":a,className:"min-h-[7rem] font-mono text-xs leading-relaxed",rows:6,spellCheck:!1,value:d,onChange:h=>r(h.target.value)}),e.jsxs("div",{className:"mt-1 flex items-center justify-between gap-3 text-xs text-fg-muted",children:[e.jsx("span",{children:s?"Using the built-in default.":n===""?"Empty — nothing will be appended.":`${d.length.toLocaleString()} characters.`}),x&&e.jsx(A,{variant:"link",size:"sm",className:"shrink-0",onClick:()=>r(null),children:"Restore default"})]})]})}if(t.type==="string-list"){const s=Array.isArray(t.default)?t.default.map(String):null,d=Array.isArray(n)?n.map(String):null,x=d===null||s!==null&&d.length===s.length&&d.every((g,l)=>g===s[l]),h=d!==null?d.join(", "):(s==null?void 0:s.join(", "))??"",p=s!==null&&!x;return e.jsxs("div",{children:[e.jsx(B,{id:c,type:"text","aria-describedby":a,value:h,placeholder:(s==null?void 0:s.join(", "))??"",onChange:g=>r(g.target.value.split(",").map(l=>l.trim()).filter(Boolean))}),e.jsxs("div",{className:"mt-1 flex items-center justify-between gap-3 text-xs text-fg-muted",children:[e.jsx("span",{children:x?"Using the built-in default.":d!==null&&d.length===0?"Empty — none offered.":`${(d==null?void 0:d.length)??0} in the list.`}),p&&e.jsx(A,{variant:"link",size:"sm",className:"shrink-0",onClick:()=>r(null),children:"Restore default"})]})]})}return e.jsx(B,{id:c,type:"text","aria-describedby":a,value:n==null?"":String(n),placeholder:t.default===null?"":String(t.default),onChange:s=>r(s.target.value)})}function Xe({field:t,value:n}){const r="rounded-lg bg-surface-sunken px-3 py-2 font-mono text-xs text-fg-muted";return t.type==="boolean"?e.jsx("div",{className:r,children:n?"true":"false"}):n==null||n===""?e.jsx("div",{className:r,children:e.jsx("span",{className:"italic text-fg-subtle",children:t.type==="text"&&n===""?"(empty — nothing appended)":"(not set)"})}):t.type==="text"?e.jsx("div",{className:b(r,"max-h-40 overflow-y-auto whitespace-pre-wrap leading-relaxed"),children:String(n)}):e.jsx("div",{className:b(r,"break-all"),children:Array.isArray(n)?n.join(", "):String(n)})}function Je(t,n){return t.type==="text"||t.type==="string-list"?!0:(Array.isArray(n)?n.join(", "):n==null?"":String(n)).length>38}function Ye(t){return[t.label,t.key,t.help,t.envVar,...t.enumValues??[]].filter(Boolean).join(" ").toLowerCase()}function Qe(t,n){const r={};for(const c of t)r[c.id]=c.fields.filter(a=>n.has(a.key)).length;return r}const D=t=>`cfg-section-${t}`,V={id:"appearance",label:"Appearance",description:"Theme and accent colour for this browser. Applies immediately — no save, no restart.",fields:[]},We=["appearance theme themes colour color accent tint dark light font",...ae.map(t=>t.id)].join(" ");function q(t){return t.pendingValue===void 0?t.value:t.pendingValue}function se(t,n){return Object.prototype.hasOwnProperty.call(n,t.key)?n[t.key]:q(t)}function re(t,n){return Array.isArray(t)&&Array.isArray(n)?t.length===n.length&&t.every((r,c)=>r===n[c]):t===n}function G(t){if(!t)return"paddock.config.yaml";const n=t.split("/");return n[n.length-1]||t}function tt(){const t=Se(),n=(t==null?void 0:t.openNav)??(()=>{});return e.jsxs("div",{className:"flex h-full min-h-0 flex-col",children:[e.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:[e.jsx("button",{type:"button",onClick:n,"aria-label":"Open menu",className:"btn-subtle -ml-1 shrink-0 px-2 py-1.5 lg:hidden",children:e.jsx(Ae,{width:20,height:20})}),e.jsx(Ee,{width:18,height:18,className:"shrink-0 text-fg-subtle"}),e.jsx("h1",{className:"text-md font-semibold tracking-tight",children:"Config"})]}),e.jsx(Ve,{})]})}export{tt as InstanceConfigPage};
2
+