@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.
- package/package.json +1 -1
- package/packages/server/dist/config.d.ts +15 -0
- package/packages/server/dist/config.js +19 -0
- package/packages/server/dist/instance-config.js +4 -0
- package/packages/server/dist/models.d.ts +11 -2
- package/packages/server/dist/models.js +15 -4
- package/packages/server/dist/project-files.d.ts +5 -4
- package/packages/server/dist/project-files.js +6 -5
- package/packages/server/dist/project-mime.d.ts +5 -5
- package/packages/server/dist/project-mime.js +7 -5
- package/packages/server/dist/project-types.d.ts +1 -1
- package/packages/server/dist/routes/chats.js +24 -2
- package/packages/server/dist/routes/git.js +4 -1
- package/packages/server/dist/routes/meta.js +5 -1
- package/packages/server/dist/routes/projects.js +5 -2
- package/packages/server/dist/ui-config.d.ts +43 -0
- package/packages/server/dist/ui-config.js +28 -0
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/DiscoverPage-DSrVUGFF.js +2 -0
- package/packages/web/dist/assets/{DiscoverView-yPLCp_YE.js → DiscoverView-CKdc3D8Z.js} +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-aOgiBFLq.js → InstanceConfigPage-MM0nmNEs.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-BjrB0TDO.js → MarkdownRenderer-CVnBje44.js} +1 -1
- package/packages/web/dist/assets/{ProjectMenu-PhwXTmgn.js → ProjectMenu-zQFtluP9.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-N4g7WrZm.js → ProjectRedirect-BX4SfdzU.js} +1 -1
- package/packages/web/dist/assets/ProjectView-7roC9iqL.js +13 -0
- package/packages/web/dist/assets/{ProjectsGrid-DZF5VHJF.js → ProjectsGrid-oOF-G_s-.js} +1 -1
- package/packages/web/dist/assets/{RootHome-C9wEtWHu.js → RootHome-BtK12VMo.js} +1 -1
- package/packages/web/dist/assets/{Surfaces-BidCkENw.js → Surfaces-DDZFCO3e.js} +1 -1
- package/packages/web/dist/assets/{arc-BctfAqLY.js → arc-DYQC_1vS.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-BJgwSqOO.js → architectureDiagram-3BPJPVTR-BgynjaBF.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-BD2skmST.js → blockDiagram-GPEHLZMM-BTRHXLmn.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-DckMrhMv.js → c4Diagram-AAUBKEIU-C95tVk0O.js} +1 -1
- package/packages/web/dist/assets/channel-Chb_iJuO.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-CQDUH7rp.js → chunk-2J33WTMH-BgdqqzF3.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-Bae1J961.js → chunk-4BX2VUAB-j38TWclZ.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-CXxS5wOa.js → chunk-55IACEB6-CdlQIrbg.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-DSNFkPB3.js → chunk-727SXJPM-By46mvzL.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-B2mqFyB7.js → chunk-AQP2D5EJ-BiUnwI4Y.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-BfqEy3lu.js → chunk-FMBD7UC4-B1Be_jYk.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-Bm6CBVK7.js → chunk-ND2GUHAM-BXZhk15D.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-NmnLtgYc.js → chunk-QZHKN3VN-exq9p2G5.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DZrRewE_.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DZrRewE_.js +2 -0
- package/packages/web/dist/assets/{core-YKeFSK1q.js → core-BuPSasb-.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-qf_mXpa9.js → cose-bilkent-S5V4N54A-8TGjBYXU.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-C31v-j3s.js → dagre-BM42HDAG-C2T-q_1P.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-C_TiPHdS.js → diagram-2AECGRRQ-CQ3m9f9I.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-BC8gUFK-.js → diagram-5GNKFQAL-6tjwrrdY.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-vO_QYWxi.js → diagram-KO2AKTUF-ISmRY1Gx.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-vxJ-YZOg.js → diagram-LMA3HP47-Ce3F9VCL.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-BNIfJG6l.js → diagram-OG6HWLK6-DEPzzAyN.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-B37HsOFM.js → erDiagram-TEJ5UH35-TNzCrnEn.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DqA5X81H.js → flowDiagram-I6XJVG4X-C9H2ECB3.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-BMP0zpIg.js → ganttDiagram-6RSMTGT7-DR6hv-f9.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-P0nXaCzx.js → gitGraphDiagram-PVQCEYII-DF9JRnhR.js} +1 -1
- package/packages/web/dist/assets/index-Bmrlxsb0.css +1 -0
- package/packages/web/dist/assets/{index-C3MdluUb.js → index-f1oSUgoy.js} +14 -14
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-DpKYQ7VK.js → infoDiagram-5YYISTIA-jOFEju4y.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-BZeRMg3C.js → ishikawaDiagram-YF4QCWOH-CMyzDmHh.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-Cg2O0qzv.js → journeyDiagram-JHISSGLW-CUBQWPmB.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-DBwVxUvI.js → kanban-definition-UN3LZRKU-BqAbUFEs.js} +1 -1
- package/packages/web/dist/assets/{linear-R8qRwYqx.js → linear-DjyRs_00.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-jctTs2en.js → mermaid.core-CG28lhsQ.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-CtmsNfpb.js → mindmap-definition-RKZ34NQL-BOs-2qdc.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-C-UiLFDv.js → pieDiagram-4H26LBE5-Cyt8Zkv5.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-BS4SWa01.js → quadrantDiagram-W4KKPZXB-DmrG0QDS.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-LPPAC6Q1.js → requirementDiagram-4Y6WPE33-CAHUwDDn.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-tjF8KyS3.js → sankeyDiagram-5OEKKPKP-nuZY9MCl.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-kmddkdk2.js → sequenceDiagram-3UESZ5HK-DabNFvfl.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-B07mykTY.js → stateDiagram-AJRCARHV-B1s5Mh8t.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BDXALvLb.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-DrUgi0cT.js → timeline-definition-PNZ67QCA-DIOaXnfq.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-Dz6p0m7l.js → vennDiagram-CIIHVFJN-DMMvA259.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-DM16lA5x.js → wardley-L42UT6IY-D98OIGXX.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-CCFQkFGB.js → wardleyDiagram-YWT4CUSO-BbGNGShP.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-DiUI5Dos.js → xychartDiagram-2RQKCTM6-DDrQ6NSD.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/sw.js +1 -1
- package/packages/web/dist/assets/DiscoverPage-DhrXvgkl.js +0 -2
- package/packages/web/dist/assets/ProjectView-B-7AIt--.js +0 -13
- package/packages/web/dist/assets/channel-CsZOz8jm.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DdALkhE9.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DdALkhE9.js +0 -2
- package/packages/web/dist/assets/index-BkY_Nc_i.css +0 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-r6WdX0dw.js +0 -2
package/package.json
CHANGED
|
@@ -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,
|
|
13
|
-
* and Sonnet 5 all have a 1,000,000-token context window; Haiku 4.5 is
|
|
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,
|
|
13
|
-
* and Sonnet 5 all have a 1,000,000-token context window; Haiku 4.5 is
|
|
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
|
-
/**
|
|
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 *
|
|
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
|
|
78
|
-
* would mangle it
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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
|
|
160
|
-
* would mangle it
|
|
161
|
-
*
|
|
162
|
-
*
|
|
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`)
|
|
26
|
-
* `IMAGE_MIME` entry
|
|
27
|
-
*
|
|
28
|
-
* `application/pdf`, not the octet-stream the
|
|
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`)
|
|
32
|
-
* `IMAGE_MIME` entry
|
|
33
|
-
*
|
|
34
|
-
* `application/pdf`, not the octet-stream the
|
|
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
|
-
|
|
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
|
-
|
|
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 `
|
|
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
|
|
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
|
-
|
|
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,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
|
|