@edspencer/paddock 0.72.1 → 0.73.0

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 (85) hide show
  1. package/package.json +1 -1
  2. package/packages/server/dist/config.d.ts +15 -0
  3. package/packages/server/dist/config.js +19 -0
  4. package/packages/server/dist/instance-config.js +4 -0
  5. package/packages/server/dist/models.d.ts +11 -2
  6. package/packages/server/dist/models.js +15 -4
  7. package/packages/server/dist/project-files.d.ts +5 -4
  8. package/packages/server/dist/project-files.js +6 -5
  9. package/packages/server/dist/project-mime.d.ts +5 -5
  10. package/packages/server/dist/project-mime.js +7 -5
  11. package/packages/server/dist/project-types.d.ts +1 -1
  12. package/packages/server/dist/routes/chats.js +24 -2
  13. package/packages/server/dist/routes/git.js +4 -1
  14. package/packages/server/dist/routes/meta.js +5 -1
  15. package/packages/server/dist/routes/projects.js +5 -2
  16. package/packages/server/dist/ui-config.d.ts +43 -0
  17. package/packages/server/dist/ui-config.js +28 -0
  18. package/packages/server/package.json +1 -1
  19. package/packages/web/dist/assets/DiscoverPage-DSrVUGFF.js +2 -0
  20. package/packages/web/dist/assets/{DiscoverView-yPLCp_YE.js → DiscoverView-CKdc3D8Z.js} +1 -1
  21. package/packages/web/dist/assets/{InstanceConfigPage-aOgiBFLq.js → InstanceConfigPage-MM0nmNEs.js} +1 -1
  22. package/packages/web/dist/assets/{MarkdownRenderer-BjrB0TDO.js → MarkdownRenderer-CVnBje44.js} +1 -1
  23. package/packages/web/dist/assets/{ProjectMenu-PhwXTmgn.js → ProjectMenu-zQFtluP9.js} +1 -1
  24. package/packages/web/dist/assets/{ProjectRedirect-N4g7WrZm.js → ProjectRedirect-BX4SfdzU.js} +1 -1
  25. package/packages/web/dist/assets/ProjectView-7roC9iqL.js +13 -0
  26. package/packages/web/dist/assets/{ProjectsGrid-DZF5VHJF.js → ProjectsGrid-oOF-G_s-.js} +1 -1
  27. package/packages/web/dist/assets/{RootHome-C9wEtWHu.js → RootHome-BtK12VMo.js} +1 -1
  28. package/packages/web/dist/assets/{Surfaces-BidCkENw.js → Surfaces-DDZFCO3e.js} +1 -1
  29. package/packages/web/dist/assets/{arc-BctfAqLY.js → arc-DYQC_1vS.js} +1 -1
  30. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-BJgwSqOO.js → architectureDiagram-3BPJPVTR-BgynjaBF.js} +1 -1
  31. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-BD2skmST.js → blockDiagram-GPEHLZMM-BTRHXLmn.js} +1 -1
  32. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-DckMrhMv.js → c4Diagram-AAUBKEIU-C95tVk0O.js} +1 -1
  33. package/packages/web/dist/assets/channel-Chb_iJuO.js +2 -0
  34. package/packages/web/dist/assets/{chunk-2J33WTMH-CQDUH7rp.js → chunk-2J33WTMH-BgdqqzF3.js} +1 -1
  35. package/packages/web/dist/assets/{chunk-4BX2VUAB-Bae1J961.js → chunk-4BX2VUAB-j38TWclZ.js} +1 -1
  36. package/packages/web/dist/assets/{chunk-55IACEB6-CXxS5wOa.js → chunk-55IACEB6-CdlQIrbg.js} +1 -1
  37. package/packages/web/dist/assets/{chunk-727SXJPM-DSNFkPB3.js → chunk-727SXJPM-By46mvzL.js} +1 -1
  38. package/packages/web/dist/assets/{chunk-AQP2D5EJ-B2mqFyB7.js → chunk-AQP2D5EJ-BiUnwI4Y.js} +1 -1
  39. package/packages/web/dist/assets/{chunk-FMBD7UC4-BfqEy3lu.js → chunk-FMBD7UC4-B1Be_jYk.js} +1 -1
  40. package/packages/web/dist/assets/{chunk-ND2GUHAM-Bm6CBVK7.js → chunk-ND2GUHAM-BXZhk15D.js} +1 -1
  41. package/packages/web/dist/assets/{chunk-QZHKN3VN-NmnLtgYc.js → chunk-QZHKN3VN-exq9p2G5.js} +1 -1
  42. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DZrRewE_.js +2 -0
  43. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DZrRewE_.js +2 -0
  44. package/packages/web/dist/assets/{core-YKeFSK1q.js → core-BuPSasb-.js} +1 -1
  45. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-qf_mXpa9.js → cose-bilkent-S5V4N54A-8TGjBYXU.js} +1 -1
  46. package/packages/web/dist/assets/{dagre-BM42HDAG-C31v-j3s.js → dagre-BM42HDAG-C2T-q_1P.js} +1 -1
  47. package/packages/web/dist/assets/{diagram-2AECGRRQ-C_TiPHdS.js → diagram-2AECGRRQ-CQ3m9f9I.js} +1 -1
  48. package/packages/web/dist/assets/{diagram-5GNKFQAL-BC8gUFK-.js → diagram-5GNKFQAL-6tjwrrdY.js} +1 -1
  49. package/packages/web/dist/assets/{diagram-KO2AKTUF-vO_QYWxi.js → diagram-KO2AKTUF-ISmRY1Gx.js} +1 -1
  50. package/packages/web/dist/assets/{diagram-LMA3HP47-vxJ-YZOg.js → diagram-LMA3HP47-Ce3F9VCL.js} +1 -1
  51. package/packages/web/dist/assets/{diagram-OG6HWLK6-BNIfJG6l.js → diagram-OG6HWLK6-DEPzzAyN.js} +1 -1
  52. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-B37HsOFM.js → erDiagram-TEJ5UH35-TNzCrnEn.js} +1 -1
  53. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DqA5X81H.js → flowDiagram-I6XJVG4X-C9H2ECB3.js} +1 -1
  54. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-BMP0zpIg.js → ganttDiagram-6RSMTGT7-DR6hv-f9.js} +1 -1
  55. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-P0nXaCzx.js → gitGraphDiagram-PVQCEYII-DF9JRnhR.js} +1 -1
  56. package/packages/web/dist/assets/index-Bmrlxsb0.css +1 -0
  57. package/packages/web/dist/assets/{index-C3MdluUb.js → index-f1oSUgoy.js} +14 -14
  58. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-DpKYQ7VK.js → infoDiagram-5YYISTIA-jOFEju4y.js} +1 -1
  59. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-BZeRMg3C.js → ishikawaDiagram-YF4QCWOH-CMyzDmHh.js} +1 -1
  60. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-Cg2O0qzv.js → journeyDiagram-JHISSGLW-CUBQWPmB.js} +1 -1
  61. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-DBwVxUvI.js → kanban-definition-UN3LZRKU-BqAbUFEs.js} +1 -1
  62. package/packages/web/dist/assets/{linear-R8qRwYqx.js → linear-DjyRs_00.js} +1 -1
  63. package/packages/web/dist/assets/{mermaid.core-jctTs2en.js → mermaid.core-CG28lhsQ.js} +4 -4
  64. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-CtmsNfpb.js → mindmap-definition-RKZ34NQL-BOs-2qdc.js} +1 -1
  65. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-C-UiLFDv.js → pieDiagram-4H26LBE5-Cyt8Zkv5.js} +1 -1
  66. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-BS4SWa01.js → quadrantDiagram-W4KKPZXB-DmrG0QDS.js} +1 -1
  67. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-LPPAC6Q1.js → requirementDiagram-4Y6WPE33-CAHUwDDn.js} +1 -1
  68. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-tjF8KyS3.js → sankeyDiagram-5OEKKPKP-nuZY9MCl.js} +1 -1
  69. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-kmddkdk2.js → sequenceDiagram-3UESZ5HK-DabNFvfl.js} +1 -1
  70. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-B07mykTY.js → stateDiagram-AJRCARHV-B1s5Mh8t.js} +1 -1
  71. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BDXALvLb.js +2 -0
  72. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-DrUgi0cT.js → timeline-definition-PNZ67QCA-DIOaXnfq.js} +1 -1
  73. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-Dz6p0m7l.js → vennDiagram-CIIHVFJN-DMMvA259.js} +1 -1
  74. package/packages/web/dist/assets/{wardley-L42UT6IY-DM16lA5x.js → wardley-L42UT6IY-D98OIGXX.js} +1 -1
  75. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-CCFQkFGB.js → wardleyDiagram-YWT4CUSO-BbGNGShP.js} +1 -1
  76. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-DiUI5Dos.js → xychartDiagram-2RQKCTM6-DDrQ6NSD.js} +1 -1
  77. package/packages/web/dist/index.html +2 -2
  78. package/packages/web/dist/sw.js +1 -1
  79. package/packages/web/dist/assets/DiscoverPage-DhrXvgkl.js +0 -2
  80. package/packages/web/dist/assets/ProjectView-B-7AIt--.js +0 -13
  81. package/packages/web/dist/assets/channel-CsZOz8jm.js +0 -2
  82. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DdALkhE9.js +0 -2
  83. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DdALkhE9.js +0 -2
  84. package/packages/web/dist/assets/index-BkY_Nc_i.css +0 -1
  85. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-r6WdX0dw.js +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edspencer/paddock",
3
- "version": "0.72.1",
3
+ "version": "0.73.0",
4
4
  "description": "Project-first launchpad for Claude Code: server-hosted, persistent, resumable sessions organized by project.",
5
5
  "keywords": [
6
6
  "claude",
@@ -10,6 +10,7 @@ import { type RecoveryConfig } from "./recovery-config.js";
10
10
  import { type CurationConfig } from "./curation-config.js";
11
11
  import { type ManagementApiConfig, type ManagementApiConfigFile } from "./management-config.js";
12
12
  import { type AttachmentsConfig } from "./attachments-config.js";
13
+ import { type UiConfig } from "./ui-config.js";
13
14
  /**
14
15
  * User-authentication strategy.
15
16
  *
@@ -433,6 +434,13 @@ export interface PaddockConfig {
433
434
  * allow-all by default.
434
435
  */
435
436
  attachments: AttachmentsConfig;
437
+ /**
438
+ * Instance-level UI knobs (issue #914) — currently just how many trailing
439
+ * messages a chat transcript renders on open. Instance-only by design: there is
440
+ * no per-project override, because this describes the operator's browser
441
+ * tolerance, not anything about a project.
442
+ */
443
+ ui: UiConfig;
436
444
  /**
437
445
  * Per-file token budgets the post-turn sweeper (curation) must keep its three
438
446
  * curated files under (issue #379). These bound the context every keeper chat
@@ -614,6 +622,13 @@ export interface PaddockConfigFile {
614
622
  maxFilesPerMessage?: number | string;
615
623
  allowedTypes?: string[] | string;
616
624
  };
625
+ /**
626
+ * Instance-level UI knobs (issue #914). Every field optional; a matching
627
+ * `PADDOCK_UI_*` env var still overrides it (file < env).
628
+ */
629
+ ui?: {
630
+ transcriptRenderLimit?: number | string;
631
+ };
617
632
  /**
618
633
  * Per-file curation token budgets (issue #379). Every field optional; a
619
634
  * matching `PADDOCK_CURATION_*` env var still overrides it (file < env).
@@ -37,6 +37,7 @@ import { DEFAULT_RECOVERY } from "./recovery-config.js";
37
37
  import { DEFAULT_CURATION } from "./curation-config.js";
38
38
  import { resolveManagementApiConfig, } from "./management-config.js";
39
39
  import { DEFAULT_ATTACHMENTS, sanitizeAllowedTypes, } from "./attachments-config.js";
40
+ import { DEFAULT_UI } from "./ui-config.js";
40
41
  function abs(p) {
41
42
  return path.isAbsolute(p) ? p : path.resolve(process.cwd(), p);
42
43
  }
@@ -348,6 +349,7 @@ export function loadPaddockConfig(opts = {}) {
348
349
  hooksMcpEnabled: loadHooksMcpEnabled(file.hooksMcpEnabled, p.hooksMcpEnabled),
349
350
  recovery: loadRecoveryConfig(file.recovery),
350
351
  attachments: loadAttachmentsConfig(file.attachments),
352
+ ui: loadUiConfig(file.ui),
351
353
  curation: loadCurationConfig(file.curation),
352
354
  logLevel: envOr("LOG_LEVEL", fileOr(file.logLevel, "info")),
353
355
  browserMcp: loadBrowserMcp(file.browserMcp, p.browserMcp),
@@ -514,6 +516,23 @@ function loadAttachmentsConfig(file) {
514
516
  allowedTypes: loadAllowedTypes(f.allowedTypes, DEFAULT_ATTACHMENTS.allowedTypes),
515
517
  };
516
518
  }
519
+ /**
520
+ * Resolve the instance-level UI config (issue #914).
521
+ *
522
+ * `transcriptRenderLimit` is a NON-NEGATIVE knob — `0` is the meaningful value
523
+ * "unlimited", so this cannot reuse {@link loadAttachmentsInt}, whose floor is 1.
524
+ * A missing/blank/non-integer/negative value falls back to the default rather
525
+ * than failing startup, like every other loader here.
526
+ */
527
+ function loadUiConfig(file) {
528
+ const raw = envOpt("PADDOCK_UI_TRANSCRIPT_RENDER_LIMIT") ?? fileOpt(file?.transcriptRenderLimit);
529
+ if (raw === undefined)
530
+ return { ...DEFAULT_UI };
531
+ const n = Number(raw);
532
+ return {
533
+ transcriptRenderLimit: Number.isInteger(n) && n >= 0 ? n : DEFAULT_UI.transcriptRenderLimit,
534
+ };
535
+ }
517
536
  /**
518
537
  * Resolve the per-deployment schedule-mutation gate (issue #265 / DD-7). Defaults
519
538
  * OFF so a plain instance can't have its schedules mutated programmatically; an
@@ -35,6 +35,7 @@ import { DRIVE_MODES, DEFAULT_DRIVE_MODE, MODELS, isKnownModel } from "./models.
35
35
  import { DEFAULT_MAX_SPAWN_DEPTH, isValidMaxSpawnDepth } from "./spawn-capability.js";
36
36
  import { DEFAULT_RECOVERY } from "./recovery-config.js";
37
37
  import { DEFAULT_ATTACHMENTS, sanitizeAllowedTypes } from "./attachments-config.js";
38
+ import { DEFAULT_UI } from "./ui-config.js";
38
39
  import { DEFAULT_CURATION } from "./curation-config.js";
39
40
  import { DEFAULT_ENVIRONMENT_PROMPT } from "./environment-prompt.js";
40
41
  import { CONFIG_SCHEMA_VERSION, SCHEMA_VERSION_KEY } from "./schema-version.js";
@@ -51,6 +52,7 @@ export const GROUPS = [
51
52
  { id: "capabilities", label: "Capabilities", description: "What keeper agents are allowed to do. Most default off." },
52
53
  { id: "recovery", label: "Recovery" },
53
54
  { id: "attachments", label: "Attachments" },
55
+ { id: "ui", label: "Interface", description: "How the web UI renders. These affect the browser only — nothing is deleted or changed on disk." },
54
56
  { id: "branding", label: "Branding" },
55
57
  { id: "transcription", label: "Transcription" },
56
58
  { id: "git", label: "Git identity" },
@@ -270,6 +272,8 @@ export const FIELDS = [
270
272
  { key: "attachments.maxFileSizeMb", group: "attachments", label: "Max file size (MB)", type: "number", envVars: ["PADDOCK_ATTACHMENTS_MAX_FILE_SIZE_MB"], default: DEFAULT_ATTACHMENTS.maxFileSizeMb, editable: true, coerce: posInt },
271
273
  { key: "attachments.maxFilesPerMessage", group: "attachments", label: "Max files / message", type: "number", envVars: ["PADDOCK_ATTACHMENTS_MAX_FILES_PER_MESSAGE"], default: DEFAULT_ATTACHMENTS.maxFilesPerMessage, editable: true, coerce: posInt },
272
274
  { key: "attachments.allowedTypes", group: "attachments", label: "Allowed types", help: "MIME types / extensions (e.g. image/*, .pdf). * = allow all.", type: "string-list", envVars: ["PADDOCK_ATTACHMENTS_ALLOWED_TYPES"], default: [...DEFAULT_ATTACHMENTS.allowedTypes], editable: true, coerce: stringList },
275
+ // Interface (issue #914). `nonNegInt`, not `posInt`: 0 means "render everything".
276
+ { key: "ui.transcriptRenderLimit", group: "ui", label: "Transcript render limit", help: "How many recent messages a chat renders on open. 0 = no limit. Older messages stay on disk and are not deleted.", type: "number", envVars: ["PADDOCK_UI_TRANSCRIPT_RENDER_LIMIT"], default: DEFAULT_UI.transcriptRenderLimit, editable: true, coerce: nonNegInt },
273
277
  // Branding (issue #34).
274
278
  { key: "brand.name", group: "branding", label: "Name", type: "string", envVars: ["PADDOCK_BRAND_NAME"], default: "Paddock", editable: true, coerce: nonEmptyString },
275
279
  { key: "brand.logo", group: "branding", label: "Logo", help: "An emoji/glyph, or a URL/path to an image.", type: "string", envVars: ["PADDOCK_BRAND_LOGO"], default: "🐎", editable: true, coerce: nonEmptyString },
@@ -9,8 +9,9 @@
9
9
  * constants — so changing the available models or a context limit is a one-file
10
10
  * edit here.
11
11
  *
12
- * Context limits (verified against the Models API): Opus 5, Fable 5, Opus 4.8
13
- * and Sonnet 5 all have a 1,000,000-token context window; Haiku 4.5 is 200,000.
12
+ * Context limits (verified against the Models API): Opus 5, Fable 5.1, Fable 5,
13
+ * Opus 4.8 and Sonnet 5 all have a 1,000,000-token context window; Haiku 4.5 is
14
+ * 200,000.
14
15
  * On the Max plan the keeper agents run Opus 5 at its full 1M window, so
15
16
  * the context meter must use 1M for it — otherwise a long chat reads >100%.
16
17
  */
@@ -28,6 +29,14 @@ export interface ModelPricing {
28
29
  inputPer1M: number;
29
30
  /** USD per 1M output tokens. */
30
31
  outputPer1M: number;
32
+ /**
33
+ * Cache-read price as a multiple of {@link inputPer1M}, when the model departs
34
+ * from the standard 0.1× ({@link CACHE_READ_MULTIPLIER}). Fable 5.1 and Mythos
35
+ * 5.1 read cached tokens at **0.025×** ($0.25/MTok against a $10 base); every
36
+ * other model is the standard tenth. Cache reads dominate a long chat's token
37
+ * counts, so getting this wrong overstates a Fable 5.1 chat's cost ~4×.
38
+ */
39
+ cacheReadMultiplier?: number;
31
40
  }
32
41
  /** A single selectable model: its id, a human label, and its context window. */
33
42
  export interface ModelInfo {
@@ -9,8 +9,9 @@
9
9
  * constants — so changing the available models or a context limit is a one-file
10
10
  * edit here.
11
11
  *
12
- * Context limits (verified against the Models API): Opus 5, Fable 5, Opus 4.8
13
- * and Sonnet 5 all have a 1,000,000-token context window; Haiku 4.5 is 200,000.
12
+ * Context limits (verified against the Models API): Opus 5, Fable 5.1, Fable 5,
13
+ * Opus 4.8 and Sonnet 5 all have a 1,000,000-token context window; Haiku 4.5 is
14
+ * 200,000.
14
15
  * On the Max plan the keeper agents run Opus 5 at its full 1M window, so
15
16
  * the context meter must use 1M for it — otherwise a long chat reads >100%.
16
17
  */
@@ -32,6 +33,12 @@ export const MODELS = [
32
33
  contextLimit: 1_000_000,
33
34
  pricing: { inputPer1M: 5, outputPer1M: 25 },
34
35
  },
36
+ {
37
+ id: "claude-fable-5-1",
38
+ label: "Fable 5.1",
39
+ contextLimit: 1_000_000,
40
+ pricing: { inputPer1M: 10, outputPer1M: 50, cacheReadMultiplier: 0.025 },
41
+ },
35
42
  {
36
43
  id: "claude-fable-5",
37
44
  label: "Fable 5",
@@ -159,7 +166,10 @@ export function getContextLimit(id) {
159
166
  export function getModelInfo(id) {
160
167
  return MODELS.find((m) => m.id === id);
161
168
  }
162
- /** Cache-write (5-minute ephemeral) is 1.25× the input rate; cache-read is 0.1×. */
169
+ /**
170
+ * Cache-write (5-minute ephemeral) is 1.25× the input rate; cache-read is 0.1×
171
+ * — except on models that override it via {@link ModelPricing.cacheReadMultiplier}.
172
+ */
163
173
  const CACHE_WRITE_MULTIPLIER = 1.25;
164
174
  const CACHE_READ_MULTIPLIER = 0.1;
165
175
  /**
@@ -176,9 +186,10 @@ export function estimateCostUsd(modelId, t) {
176
186
  if (!pricing)
177
187
  return null;
178
188
  const { inputPer1M, outputPer1M } = pricing;
189
+ const cacheRead = pricing.cacheReadMultiplier ?? CACHE_READ_MULTIPLIER;
179
190
  const usd = (t.inputTokens * inputPer1M +
180
191
  t.cacheCreationTokens * inputPer1M * CACHE_WRITE_MULTIPLIER +
181
- t.cacheReadTokens * inputPer1M * CACHE_READ_MULTIPLIER +
192
+ t.cacheReadTokens * inputPer1M * cacheRead +
182
193
  t.outputTokens * outputPer1M) /
183
194
  1_000_000;
184
195
  return usd;
@@ -74,10 +74,11 @@ export declare function readFileBytes(dir: string, name: string, hiddenSegments?
74
74
  * UI's markdown/Mermaid + sandboxed-iframe renderers (issue #3) and the image
75
75
  * viewer (issue #61).
76
76
  *
77
- * For an IMAGE the raw bytes are NOT returned here (decoding binary as UTF-8
78
- * would mangle it): `content` is empty and the client fetches the bytes from
79
- * the raw endpoint. We still stat the file so a missing image 404s. Path-
80
- * traversal guarded; throws ProjectError("not_found") when missing. See
77
+ * For a BINARY kind (image, pdf) the raw bytes are NOT returned here (decoding
78
+ * binary as UTF-8 would mangle it — a PDF read this way is the garbled text of
79
+ * issue #917): `content` is empty and the client fetches the bytes from the raw
80
+ * endpoint. We still stat the file so a missing one 404s. Path-traversal
81
+ * guarded; throws ProjectError("not_found") when missing. See
81
82
  * {@link resolveInProject} for `hiddenSegments`.
82
83
  */
83
84
  export declare function readFileWithKind(dir: string, name: string, hiddenSegments?: "refuse" | "allow"): Promise<{
@@ -156,15 +156,16 @@ export async function readFileBytes(dir, name, hiddenSegments = "refuse") {
156
156
  * UI's markdown/Mermaid + sandboxed-iframe renderers (issue #3) and the image
157
157
  * viewer (issue #61).
158
158
  *
159
- * For an IMAGE the raw bytes are NOT returned here (decoding binary as UTF-8
160
- * would mangle it): `content` is empty and the client fetches the bytes from
161
- * the raw endpoint. We still stat the file so a missing image 404s. Path-
162
- * traversal guarded; throws ProjectError("not_found") when missing. See
159
+ * For a BINARY kind (image, pdf) the raw bytes are NOT returned here (decoding
160
+ * binary as UTF-8 would mangle it — a PDF read this way is the garbled text of
161
+ * issue #917): `content` is empty and the client fetches the bytes from the raw
162
+ * endpoint. We still stat the file so a missing one 404s. Path-traversal
163
+ * guarded; throws ProjectError("not_found") when missing. See
163
164
  * {@link resolveInProject} for `hiddenSegments`.
164
165
  */
165
166
  export async function readFileWithKind(dir, name, hiddenSegments = "refuse") {
166
167
  const kind = fileKind(name);
167
- if (kind === "image") {
168
+ if (kind === "image" || kind === "pdf") {
168
169
  // Existence check only — the bytes go over the raw endpoint.
169
170
  try {
170
171
  await fs.stat(resolveInProject(dir, name, hiddenSegments));
@@ -22,11 +22,11 @@ export declare const IMAGE_MIME: Record<string, string>;
22
22
  export declare const VIDEO_MIME: Record<string, string>;
23
23
  /**
24
24
  * Non-image document extensions → their MIME type. Kept SEPARATE from
25
- * `IMAGE_MIME` on purpose: the file-kind classifier (`fileKind`) treats every
26
- * `IMAGE_MIME` entry as `kind: "image"`, so a `.pdf` must not live there. It's
27
- * used only for the byte endpoint's Content-Type (a PDF must serve as
28
- * `application/pdf`, not the octet-stream the attachment store rewrites to
29
- * `text/plain`).
25
+ * `IMAGE_MIME` on purpose: the file-kind classifier (`fileKind`) maps every
26
+ * `IMAGE_MIME` entry to `kind: "image"`, so a `.pdf` must not live there — it
27
+ * gets its own `kind: "pdf"` (issue #917). Also the byte endpoint's
28
+ * Content-Type (a PDF must serve as `application/pdf`, not the octet-stream the
29
+ * attachment store rewrites to `text/plain`).
30
30
  */
31
31
  export declare const DOCUMENT_MIME: Record<string, string>;
32
32
  /** Derive a render kind from a file name's extension. */
@@ -28,11 +28,11 @@ export const VIDEO_MIME = {
28
28
  };
29
29
  /**
30
30
  * Non-image document extensions → their MIME type. Kept SEPARATE from
31
- * `IMAGE_MIME` on purpose: the file-kind classifier (`fileKind`) treats every
32
- * `IMAGE_MIME` entry as `kind: "image"`, so a `.pdf` must not live there. It's
33
- * used only for the byte endpoint's Content-Type (a PDF must serve as
34
- * `application/pdf`, not the octet-stream the attachment store rewrites to
35
- * `text/plain`).
31
+ * `IMAGE_MIME` on purpose: the file-kind classifier (`fileKind`) maps every
32
+ * `IMAGE_MIME` entry to `kind: "image"`, so a `.pdf` must not live there — it
33
+ * gets its own `kind: "pdf"` (issue #917). Also the byte endpoint's
34
+ * Content-Type (a PDF must serve as `application/pdf`, not the octet-stream the
35
+ * attachment store rewrites to `text/plain`).
36
36
  */
37
37
  export const DOCUMENT_MIME = {
38
38
  ".pdf": "application/pdf",
@@ -46,6 +46,8 @@ export function fileKind(name) {
46
46
  return "html";
47
47
  if (ext in IMAGE_MIME)
48
48
  return "image";
49
+ if (ext in DOCUMENT_MIME)
50
+ return "pdf";
49
51
  return "text";
50
52
  }
51
53
  /** The MIME type for a file name's extension, defaulting to octet-stream. */
@@ -25,7 +25,7 @@ export declare const PROJECT_STATUSES: readonly ["idea", "active", "paused", "bl
25
25
  export type ProjectStatus = (typeof PROJECT_STATUSES)[number];
26
26
  export type ProjectVisibility = "public" | "private";
27
27
  /** Render-kind hint for a project file (drives the UI renderer choice). */
28
- export type FileKind = "markdown" | "html" | "text" | "image";
28
+ export type FileKind = "markdown" | "html" | "text" | "image" | "pdf";
29
29
  /**
30
30
  * One entry in a project directory listing (issue #259): a name plus whether
31
31
  * it's a file or a subdirectory, so the Files tab can distinguish the two and
@@ -15,6 +15,7 @@ import { ROOT_KEY } from "../projects.js";
15
15
  import { applyMessageProvenance } from "../message-provenance.js";
16
16
  import { buildProjectRuns } from "../runs.js";
17
17
  import { DEFAULT_MODEL } from "../models.js";
18
+ import { capMessages } from "../ui-config.js";
18
19
  import { projectChatsDir } from "../transcripts.js";
19
20
  import { readSubagentMessages, readSessionTokenUsageWithSubagents } from "../subagents.js";
20
21
  import { readContextSeries } from "../usage.js";
@@ -461,7 +462,7 @@ export function registerChatWorkspaceRoutes(app, ctx) {
461
462
  schema: {
462
463
  tags: ["Chats"],
463
464
  summary: "Read a project chat's messages",
464
- description: "READ-ONLY: returns the messages of a project chat, enriched with tool details and per-message provenance, plus a synthetic trailing notice if the chat dead-ended at a usage/subscription limit. Sending a message is WebSocket-only (a `chat:send` frame on GET /ws) — there is no HTTP send-message endpoint. Response: `{ messages: [...] }`.",
465
+ description: "READ-ONLY: returns the messages of a project chat, enriched with tool details and per-message provenance, plus a synthetic trailing notice if the chat dead-ended at a usage/subscription limit. Sending a message is WebSocket-only (a `chat:send` frame on GET /ws) — there is no HTTP send-message endpoint. Optional `?limit=N` returns only the trailing N messages (issue #914); omitted or `0` returns the whole transcript, which is the default and unchanged behaviour. Response: `{ messages: [...], total, truncated }`, where `total` is the full count before any cap.",
465
466
  params: {
466
467
  type: "object",
467
468
  properties: {
@@ -470,6 +471,16 @@ export function registerChatWorkspaceRoutes(app, ctx) {
470
471
  },
471
472
  required: ["slug", "sessionId"],
472
473
  },
474
+ querystring: {
475
+ type: "object",
476
+ properties: {
477
+ limit: {
478
+ type: "integer",
479
+ minimum: 0,
480
+ description: "Return only the trailing N messages. Omit (or pass 0) for the whole transcript. The SPA passes the instance's `ui.transcriptRenderLimit`; other clients are uncapped by default so the published contract is unchanged.",
481
+ },
482
+ },
483
+ },
473
484
  response: {
474
485
  200: {
475
486
  description: "Object `{ messages }` with the chat's message array.",
@@ -525,7 +536,18 @@ export function registerChatWorkspaceRoutes(app, ctx) {
525
536
  notice,
526
537
  });
527
538
  }
528
- return { messages: withProvenance };
539
+ // Transcript render cap (issue #914). The slice is deliberately the LAST
540
+ // step, after enrich/provenance/context-fill, even though slicing earlier
541
+ // would also save that work: `applyMessageProvenance` walks its markers
542
+ // with an ordered cursor from index 0 (`message-provenance.ts:274-288`),
543
+ // so handing it a head-sliced list desyncs the cursor and silently drops
544
+ // attribution for the whole window. Slicing here means every message the
545
+ // client receives is byte-identical to the uncapped response — the only
546
+ // difference is how many of them there are. Moving the cut earlier is a
547
+ // real optimisation, but it needs a marker offset plumbed through first.
548
+ const total = withProvenance.length;
549
+ const capped = capMessages(withProvenance, req.query.limit ?? 0);
550
+ return { messages: capped, total, truncated: capped.length < total };
529
551
  }
530
552
  catch (err) {
531
553
  return sendProjectError(reply, err);
@@ -1,6 +1,7 @@
1
1
  import { sendProjectError } from "../route-errors.js";
2
2
  import { ProjectError } from "../project-paths.js";
3
3
  import { readFileWithKind, readFileBytes } from "../project-files.js";
4
+ import { cspFor } from "../http-bytes.js";
4
5
  export function registerGitRoutes(app, ctx) {
5
6
  const { git, githubAuth } = ctx;
6
7
  // --- git (backing store): fleet-level remote + connection state --------
@@ -358,7 +359,9 @@ export function registerGitWorkspaceRoutes(app, ctx) {
358
359
  .header("content-type", mime)
359
360
  .header("content-disposition", "inline")
360
361
  .header("x-content-type-options", "nosniff")
361
- .header("content-security-policy", "sandbox; default-src 'none'")
362
+ // Per-MIME, not a hard-coded constant: a bare `sandbox` token stops
363
+ // the browser's native PDF viewer painting at all (issue #917).
364
+ .header("content-security-policy", cspFor(mime))
362
365
  .header("cache-control", "private, max-age=60")
363
366
  .send(bytes);
364
367
  }
@@ -230,7 +230,7 @@ export function registerMetaRoutes(app, ctx) {
230
230
  schema: {
231
231
  tags: ["System"],
232
232
  summary: "Selectable models and instance defaults",
233
- description: "Returns the models this instance offers (the built-in catalog filtered to the `PADDOCK_MODELS` / YAML `models:` allow-list; unset ⇒ the whole catalog) plus the default model and the box-wide defaults (drive mode, max spawn depth, recovery, attachments, curation) that projects fall back to when their own overrides are unset. `defaultModel` is the effective default for the offered list. Returns a JSON object with `models`, `defaultModel`, `driveModeDefault`, `maxSpawnDepthDefault`, `recoveryDefault`, `attachmentsDefault`, and `curationDefault`.",
233
+ description: "Returns the models this instance offers (the built-in catalog filtered to the `PADDOCK_MODELS` / YAML `models:` allow-list; unset ⇒ the whole catalog) plus the default model and the box-wide defaults (drive mode, max spawn depth, recovery, attachments, curation) that projects fall back to when their own overrides are unset. `defaultModel` is the effective default for the offered list. Returns a JSON object with `models`, `defaultModel`, `driveModeDefault`, `maxSpawnDepthDefault`, `recoveryDefault`, `attachmentsDefault`, `curationDefault`, and `uiDefault`.",
234
234
  response: {
235
235
  200: {
236
236
  description: "Model list and inherited defaults.",
@@ -263,6 +263,10 @@ export function registerMetaRoutes(app, ctx) {
263
263
  // when its own `curation` override fields are unset (issue #384). Settings
264
264
  // shows these as the "Instance default" for each per-file token budget.
265
265
  curationDefault: cfg.curation,
266
+ // Instance-level UI knobs (issue #914). Unlike its neighbours this has no
267
+ // per-project override — the transcript render cap is a property of the
268
+ // operator's browser tolerance, not of a project — so the SPA uses it as-is.
269
+ uiDefault: cfg.ui,
266
270
  };
267
271
  });
268
272
  app.get("/api/fleet", {
@@ -3,6 +3,7 @@ import { keeperAgentName } from "../herdctl.js";
3
3
  import { isKnownModel, isKnownPermissionMode, isValidMaxTurns, isKnownDriveMode, resolveModels, MAX_TURNS_LIMIT, } from "../models.js";
4
4
  import { isValidMaxSpawnDepth, MAX_SPAWN_DEPTH_LIMIT } from "../spawn-capability.js";
5
5
  import { sendProjectError } from "../route-errors.js";
6
+ import { cspFor } from "../http-bytes.js";
6
7
  import { buildProjectChats, makeTriggerResolver, makeParentResolver } from "../chat-dto.js";
7
8
  import { quiesceProject, turnRunningError } from "../turn-interlock.js";
8
9
  /**
@@ -722,7 +723,7 @@ export function registerProjectWorkspaceRoutes(app, ctx) {
722
723
  schema: {
723
724
  tags: ["Projects"],
724
725
  summary: "Get a single project file",
725
- description: "Dual-mode. By default returns JSON with the file content plus a render-kind hint (markdown | html | text | image). With `?raw=1` it instead STREAMS the file's raw bytes with the correct Content-Type (locked down: sandbox CSP + nosniff + inline disposition) — how the image viewer loads an `<img>`. No response schema is declared so the raw byte stream is never corrupted.",
726
+ description: "Dual-mode. By default returns JSON with the file content plus a render-kind hint (markdown | html | text | image | pdf). Binary kinds (image, pdf) carry an EMPTY `content` — their bytes come from the raw mode instead. With `?raw=1` it STREAMS the file's raw bytes with the correct Content-Type (locked down: nosniff + inline disposition + a per-MIME CSP — `sandbox` is dropped for PDF, which cannot render under it). No response schema is declared so the raw byte stream is never corrupted.",
726
727
  params: {
727
728
  type: "object",
728
729
  properties: {
@@ -752,7 +753,9 @@ export function registerProjectWorkspaceRoutes(app, ctx) {
752
753
  .header("content-type", mime)
753
754
  .header("content-disposition", "inline")
754
755
  .header("x-content-type-options", "nosniff")
755
- .header("content-security-policy", "sandbox; default-src 'none'")
756
+ // Per-MIME, not a hard-coded constant: a bare `sandbox` token stops
757
+ // the browser's native PDF viewer painting at all (issue #917).
758
+ .header("content-security-policy", cspFor(mime))
756
759
  .header("cache-control", "private, max-age=60")
757
760
  .send(bytes);
758
761
  }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Instance-level UI knobs (issue #914) — settings that shape how the SPA renders,
3
+ * as opposed to what the server will do. Instance-level only: there is deliberately
4
+ * no per-project override, because these describe the operator's browser/box
5
+ * tolerance rather than anything about a project's content.
6
+ *
7
+ * Kept as its own block module per the one-file-per-config-block convention (cf.
8
+ * `attachments-config.ts`, `recovery-config.ts`).
9
+ */
10
+ /**
11
+ * Resolved UI config — all fields concrete. Held on
12
+ * {@link import("./config.js").PaddockConfig.ui} and served to the SPA as
13
+ * `uiDefault` on `GET /api/models`.
14
+ */
15
+ export interface UiConfig {
16
+ /**
17
+ * How many trailing messages a chat's transcript renders on open.
18
+ *
19
+ * A long-running chat is ~1,500 turns, and mounting all of them is what makes
20
+ * switching into one slow (#914). The cap is applied at the `/messages` join,
21
+ * so it also bounds the server-side enrich/provenance/serialise work — not just
22
+ * the DOM.
23
+ *
24
+ * **`0` means unlimited** (render everything, the pre-#914 behaviour), so this
25
+ * is a NON-NEGATIVE integer knob — unlike the attachment counts, `1` is not the
26
+ * floor. Default 500. Env `PADDOCK_UI_TRANSCRIPT_RENDER_LIMIT`.
27
+ *
28
+ * This is a *render* cap, not a retention policy: nothing is deleted, and the
29
+ * full transcript is always one uncapped fetch away (#915).
30
+ */
31
+ transcriptRenderLimit: number;
32
+ }
33
+ /** The built-in UI defaults (beneath env + YAML): last 500 messages rendered. */
34
+ export declare const DEFAULT_UI: UiConfig;
35
+ /**
36
+ * Apply a transcript render cap to a message list: the trailing `limit`, or the
37
+ * whole list when `limit` is 0 (unlimited) or the list already fits.
38
+ *
39
+ * Split out as a pure function so the slice semantics are unit-testable without
40
+ * a transcript on disk, and so the route and any future caller cannot disagree
41
+ * about what "the last N" means.
42
+ */
43
+ export declare function capMessages<T>(messages: T[], limit: number): T[];
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Instance-level UI knobs (issue #914) — settings that shape how the SPA renders,
3
+ * as opposed to what the server will do. Instance-level only: there is deliberately
4
+ * no per-project override, because these describe the operator's browser/box
5
+ * tolerance rather than anything about a project's content.
6
+ *
7
+ * Kept as its own block module per the one-file-per-config-block convention (cf.
8
+ * `attachments-config.ts`, `recovery-config.ts`).
9
+ */
10
+ /** The built-in UI defaults (beneath env + YAML): last 500 messages rendered. */
11
+ export const DEFAULT_UI = Object.freeze({
12
+ transcriptRenderLimit: 500,
13
+ });
14
+ /**
15
+ * Apply a transcript render cap to a message list: the trailing `limit`, or the
16
+ * whole list when `limit` is 0 (unlimited) or the list already fits.
17
+ *
18
+ * Split out as a pure function so the slice semantics are unit-testable without
19
+ * a transcript on disk, and so the route and any future caller cannot disagree
20
+ * about what "the last N" means.
21
+ */
22
+ export function capMessages(messages, limit) {
23
+ if (!Number.isInteger(limit) || limit <= 0)
24
+ return messages;
25
+ if (messages.length <= limit)
26
+ return messages;
27
+ return messages.slice(messages.length - limit);
28
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paddock/server",
3
- "version": "0.72.1",
3
+ "version": "0.73.0",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -0,0 +1,2 @@
1
+ import{j as r}from"./index-f1oSUgoy.js";import{D as o}from"./DiscoverView-CKdc3D8Z.js";import"./Surfaces-DDZFCO3e.js";function s(){return r.jsx(o,{})}export{s as DiscoverPage};
2
+
@@ -1,2 +1,2 @@
1
- import{a as oe,u as ie,V as de,r as u,b as A,j as s,J as X,G as Y,a2 as Z,z as $,W as ce,H as le,P as he,a3 as ue,a4 as J,D,l as K,a5 as pe,h as xe,M as W}from"./index-C3MdluUb.js";import{E as R,C as me}from"./Surfaces-BidCkENw.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]??z(),r),0)}function be(e,t){return t.filter(n=>C(e[n.path]??z(),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 T(e,t,n){const r=e[t];return r?{...e,[t]:{...r,...n}}:e}function Ne(e,t){const n=e??z();return C(n,t)===0?null:n.sessions&&n.selected?{sessionIds:[...n.selected]}:{}}function z(){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 He({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,O]=u.useState(null),[G,P]=u.useState(!0),[f,v]=u.useState({}),[k,H]=u.useState(!1),[L,F]=u.useState({}),[E,B]=u.useState({}),[S,ee]=u.useState(!1);u.useEffect(()=>{let o=!0;return P(!0),O(null),A.discover({includeNonGit:m,includeOutsideHome:j}).then(i=>{o&&(x(i),v(ge(i.candidates)),P(!1))}).catch(i=>{o&&(O(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=T(p,o.path,{expanded:!g.expanded});return g.expanded||g.sessions||g.loading?N:T(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=>T(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;H(!0),F(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)=>{F(N=>({...N,[i]:p})),g&&B(N=>({...N,[i]:g}))}),H(!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(De,{imported:ae,projects:re,onGetStarted:ne}),y!==null&&s.jsx(X,{tone:"danger",icon:s.jsx(Y,{width:16,height:16}),children:y}),G&&s.jsx(Te,{}),!G&&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(D,{tone:"lineage",size:"sm",children:e.gitRemote}),!e.hasGit&&s.jsx(D,{tone:"neutral",size:"sm",children:"no git"}),!e.insideHome&&s.jsx(D,{tone:"neutral",size:"sm",children:"outside home"})]}),e.recordedPath!==void 0&&s.jsx(X,{tone:"warn",className:"mt-2",icon:s.jsx(Y,{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)," · ",ze(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(W,{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(W,{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 De({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 Te(){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 ze(e){return e<1024?`${e} B`:e<1024*1024?`${Math.round(e/1024)} kB`:`${(e/(1024*1024)).toFixed(1)} MB`}export{He as D};
1
+ import{a as oe,u as ie,V as de,r as u,b as A,j as s,J as X,G as Y,a2 as Z,z as $,W as ce,H as le,P as he,a3 as ue,a4 as J,D,l as K,a5 as pe,h as xe,M as W}from"./index-f1oSUgoy.js";import{E as R,C as me}from"./Surfaces-DDZFCO3e.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]??z(),r),0)}function be(e,t){return t.filter(n=>C(e[n.path]??z(),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 T(e,t,n){const r=e[t];return r?{...e,[t]:{...r,...n}}:e}function Ne(e,t){const n=e??z();return C(n,t)===0?null:n.sessions&&n.selected?{sessionIds:[...n.selected]}:{}}function z(){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 He({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,O]=u.useState(null),[G,P]=u.useState(!0),[f,v]=u.useState({}),[k,H]=u.useState(!1),[L,F]=u.useState({}),[E,B]=u.useState({}),[S,ee]=u.useState(!1);u.useEffect(()=>{let o=!0;return P(!0),O(null),A.discover({includeNonGit:m,includeOutsideHome:j}).then(i=>{o&&(x(i),v(ge(i.candidates)),P(!1))}).catch(i=>{o&&(O(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=T(p,o.path,{expanded:!g.expanded});return g.expanded||g.sessions||g.loading?N:T(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=>T(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;H(!0),F(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)=>{F(N=>({...N,[i]:p})),g&&B(N=>({...N,[i]:g}))}),H(!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(De,{imported:ae,projects:re,onGetStarted:ne}),y!==null&&s.jsx(X,{tone:"danger",icon:s.jsx(Y,{width:16,height:16}),children:y}),G&&s.jsx(Te,{}),!G&&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(D,{tone:"lineage",size:"sm",children:e.gitRemote}),!e.hasGit&&s.jsx(D,{tone:"neutral",size:"sm",children:"no git"}),!e.insideHome&&s.jsx(D,{tone:"neutral",size:"sm",children:"outside home"})]}),e.recordedPath!==void 0&&s.jsx(X,{tone:"warn",className:"mt-2",icon:s.jsx(Y,{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)," · ",ze(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(W,{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(W,{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 De({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 Te(){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 ze(e){return e<1024?`${e} B`:e<1024*1024?`${Math.round(e/1024)} kB`:`${(e/(1024*1024)).toFixed(1)} MB`}export{He as D};
2
2