@edspencer/paddock 0.72.1 → 0.74.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 +5 -1
- package/packages/server/dist/models.d.ts +26 -6
- package/packages/server/dist/models.js +45 -10
- 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-paths.d.ts +15 -0
- package/packages/server/dist/project-paths.js +15 -0
- package/packages/server/dist/project-types.d.ts +1 -1
- package/packages/server/dist/projects.d.ts +2 -2
- package/packages/server/dist/projects.js +43 -7
- 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-BX2Q0ycN.js +2 -0
- package/packages/web/dist/assets/DiscoverView-BPH1QcJ9.js +2 -0
- package/packages/web/dist/assets/{InstanceConfigPage-aOgiBFLq.js → InstanceConfigPage-C0p7EP0D.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-BjrB0TDO.js → MarkdownRenderer-DpATMqaW.js} +1 -1
- package/packages/web/dist/assets/ProjectMenu-COBWHez-.js +2 -0
- package/packages/web/dist/assets/{ProjectRedirect-N4g7WrZm.js → ProjectRedirect-DsDlOmgR.js} +1 -1
- package/packages/web/dist/assets/ProjectView-Da-HMPaX.js +13 -0
- package/packages/web/dist/assets/ProjectsGrid-B7Tu8bJa.js +2 -0
- package/packages/web/dist/assets/{RootHome-C9wEtWHu.js → RootHome-BR2M_FNm.js} +1 -1
- package/packages/web/dist/assets/{Surfaces-BidCkENw.js → Surfaces-DKrmOzkk.js} +1 -1
- package/packages/web/dist/assets/{arc-BctfAqLY.js → arc-BUDm_v94.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-BJgwSqOO.js → architectureDiagram-3BPJPVTR-D4ZBf5MA.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-BD2skmST.js → blockDiagram-GPEHLZMM-VTYIZW4q.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-DckMrhMv.js → c4Diagram-AAUBKEIU-CyUzrDQ0.js} +1 -1
- package/packages/web/dist/assets/channel-BEbx-lLm.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-CQDUH7rp.js → chunk-2J33WTMH-BtndMYUq.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-Bae1J961.js → chunk-4BX2VUAB-sN2R3uRX.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-CXxS5wOa.js → chunk-55IACEB6-Dft9UrXF.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-DSNFkPB3.js → chunk-727SXJPM-B9VXcd9S.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-B2mqFyB7.js → chunk-AQP2D5EJ-D3HMB7td.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-BfqEy3lu.js → chunk-FMBD7UC4-B50IDAXn.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-Bm6CBVK7.js → chunk-ND2GUHAM-BthbFTUl.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-NmnLtgYc.js → chunk-QZHKN3VN-ZArPIBZ4.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DuHRhAai.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DuHRhAai.js +2 -0
- package/packages/web/dist/assets/{core-YKeFSK1q.js → core-DfGQq4F4.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-qf_mXpa9.js → cose-bilkent-S5V4N54A-o6695fVh.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-C31v-j3s.js → dagre-BM42HDAG-BbJtmARn.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-C_TiPHdS.js → diagram-2AECGRRQ-gMpt4UAN.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-BC8gUFK-.js → diagram-5GNKFQAL-6Yf0abcV.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-vO_QYWxi.js → diagram-KO2AKTUF-Cbux4Jaa.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-vxJ-YZOg.js → diagram-LMA3HP47-B4Pz9qdB.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-BNIfJG6l.js → diagram-OG6HWLK6-DUwGWY8v.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-B37HsOFM.js → erDiagram-TEJ5UH35-BYe6FsbI.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DqA5X81H.js → flowDiagram-I6XJVG4X-CBjmjKdl.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-BMP0zpIg.js → ganttDiagram-6RSMTGT7-a_XquFE3.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-P0nXaCzx.js → gitGraphDiagram-PVQCEYII-eTvZb_j2.js} +1 -1
- package/packages/web/dist/assets/index-Bmrlxsb0.css +1 -0
- package/packages/web/dist/assets/index-CIGlBQAs.js +71 -0
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-DpKYQ7VK.js → infoDiagram-5YYISTIA-D9KnLZ9C.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-BZeRMg3C.js → ishikawaDiagram-YF4QCWOH-CYfdAUXb.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-Cg2O0qzv.js → journeyDiagram-JHISSGLW-BS5b0yBH.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-DBwVxUvI.js → kanban-definition-UN3LZRKU-DNGUO5mT.js} +1 -1
- package/packages/web/dist/assets/{linear-R8qRwYqx.js → linear-DxEiALSJ.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-jctTs2en.js → mermaid.core-CrttYAaz.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-CtmsNfpb.js → mindmap-definition-RKZ34NQL-B5pnbSAD.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-C-UiLFDv.js → pieDiagram-4H26LBE5-GGhWYy-7.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-BS4SWa01.js → quadrantDiagram-W4KKPZXB-SErMkbVi.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-LPPAC6Q1.js → requirementDiagram-4Y6WPE33-JKwTIXzH.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-tjF8KyS3.js → sankeyDiagram-5OEKKPKP-DOAQnux6.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-kmddkdk2.js → sequenceDiagram-3UESZ5HK-m6O3XSti.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-B07mykTY.js → stateDiagram-AJRCARHV-kuTP2Tq-.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CQizvmc0.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-DrUgi0cT.js → timeline-definition-PNZ67QCA-BsUvko-L.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-Dz6p0m7l.js → vennDiagram-CIIHVFJN-BbzLQnXq.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-DM16lA5x.js → wardley-L42UT6IY-DZ9zhTE4.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-CCFQkFGB.js → wardleyDiagram-YWT4CUSO-DEtCkWvy.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-DiUI5Dos.js → xychartDiagram-2RQKCTM6-CPZHKB8u.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/DiscoverView-yPLCp_YE.js +0 -2
- package/packages/web/dist/assets/ProjectMenu-PhwXTmgn.js +0 -2
- package/packages/web/dist/assets/ProjectView-B-7AIt--.js +0 -13
- package/packages/web/dist/assets/ProjectsGrid-DZF5VHJF.js +0 -2
- 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/index-C3MdluUb.js +0 -71
- 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" },
|
|
@@ -245,7 +247,7 @@ export const FIELDS = [
|
|
|
245
247
|
{ key: "sweepMinIntervalMs", group: "sweeper", label: "Min sweep interval (ms)", help: "Minimum ms between post-turn sweeps for one project. Blank = default (5 min).", type: "number", envVars: ["PADDOCK_SWEEP_MIN_INTERVAL_MS"], default: null, editable: true, coerce: optNonNegNumber },
|
|
246
248
|
// Capabilities.
|
|
247
249
|
{ key: "driveMode", group: "capabilities", label: "Keeper drive mode", help: "session = persistent (streaming + cross-turn autonomy); batch = legacy one-shot.", type: "enum", enumValues: DRIVE_MODES, envVars: ["PADDOCK_DRIVE_MODE"], default: DEFAULT_DRIVE_MODE, editable: true, coerce: oneOf(DRIVE_MODES) },
|
|
248
|
-
{ key: "models", group: "capabilities", label: "Offered models", help: "Which built-in catalog models the picker offers, by id (e.g. claude-opus-5, claude-sonnet-5). Blank = offer all catalog models.", type: "string-list", envVars: ["PADDOCK_MODELS"], default: MODELS.map((m) => m.id), editable: true, coerce: modelList },
|
|
250
|
+
{ key: "models", group: "capabilities", label: "Offered models", help: "Which built-in catalog models the picker offers, by id (e.g. claude-opus-5-5, claude-sonnet-5). Blank = offer all catalog models.", type: "string-list", envVars: ["PADDOCK_MODELS"], default: MODELS.map((m) => m.id), editable: true, coerce: modelList },
|
|
249
251
|
{ key: "nativeSystemPrompt", group: "capabilities", label: "Native system prompt", help: "Use Claude Code's native prompt + CLAUDE.md hierarchy (recommended).", type: "boolean", envVars: ["PADDOCK_NATIVE_PROMPT"], default: true, editable: true, coerce: asBool },
|
|
250
252
|
// Issue #635. `envShadowWhenDefined` because loadEnvironmentPrompt keys on
|
|
251
253
|
// definedness, not emptiness — a defined-but-empty PADDOCK_ENVIRONMENT_PROMPT
|
|
@@ -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,10 +9,11 @@
|
|
|
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,
|
|
13
|
-
* and Sonnet 5 all have a 1,000,000-token context window;
|
|
14
|
-
*
|
|
15
|
-
* the
|
|
12
|
+
* Context limits (verified against the Models API): Opus 5.5, Opus 5, Fable 5.1,
|
|
13
|
+
* Fable 5, Opus 4.8 and Sonnet 5 all have a 1,000,000-token context window;
|
|
14
|
+
* Haiku 4.5 is 200,000.
|
|
15
|
+
* On the Max plan the keeper agents run Opus at its full 1M window, so the
|
|
16
|
+
* context meter must use 1M for it — otherwise a long chat reads >100%.
|
|
16
17
|
*/
|
|
17
18
|
/**
|
|
18
19
|
* Public first-party API list price per 1M tokens, used only to put a
|
|
@@ -28,6 +29,16 @@ 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}). Two models do:
|
|
35
|
+
* Fable 5.1 (and Mythos 5.1) read cached tokens at **0.025×** ($0.25/MTok
|
|
36
|
+
* against a $10 base), and Opus 5.5 at **0.05×** ($0.20/MTok against a $4
|
|
37
|
+
* base). Every other model is the standard tenth. Cache reads dominate a long
|
|
38
|
+
* chat's token counts, so getting this wrong overstates a Fable 5.1 chat's
|
|
39
|
+
* cost ~4× and an Opus 5.5 chat's ~2×.
|
|
40
|
+
*/
|
|
41
|
+
cacheReadMultiplier?: number;
|
|
31
42
|
}
|
|
32
43
|
/** A single selectable model: its id, a human label, and its context window. */
|
|
33
44
|
export interface ModelInfo {
|
|
@@ -46,8 +57,17 @@ export interface ModelInfo {
|
|
|
46
57
|
* renders.
|
|
47
58
|
*/
|
|
48
59
|
export declare const MODELS: ModelInfo[];
|
|
49
|
-
/**
|
|
50
|
-
|
|
60
|
+
/**
|
|
61
|
+
* The model a project runs on unless the project overrides it. Moved from
|
|
62
|
+
* Opus 5 to Opus 5.5 when 5.5 shipped: it is both newer and cheaper ($4/$20 vs
|
|
63
|
+
* $5/$25, and half-price cache reads), so unpinned projects get a better model
|
|
64
|
+
* for less. A project that pins `model` in its `project.yaml` is untouched.
|
|
65
|
+
*
|
|
66
|
+
* Keep this equal to `MODELS[0].id` — the picker renders catalog order, so the
|
|
67
|
+
* default must be the first entry or the UI implies a different default than
|
|
68
|
+
* the server applies. `models.test.ts` asserts the two agree.
|
|
69
|
+
*/
|
|
70
|
+
export declare const DEFAULT_MODEL = "claude-opus-5-5";
|
|
51
71
|
/**
|
|
52
72
|
* Resolve the models OFFERED (the picker/allow-list) from an optional list of
|
|
53
73
|
* allowed ids (issue #457 Step 2). {@link MODELS} stays the authoritative CATALOG
|
|
@@ -9,10 +9,11 @@
|
|
|
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,
|
|
13
|
-
* and Sonnet 5 all have a 1,000,000-token context window;
|
|
14
|
-
*
|
|
15
|
-
* the
|
|
12
|
+
* Context limits (verified against the Models API): Opus 5.5, Opus 5, Fable 5.1,
|
|
13
|
+
* Fable 5, Opus 4.8 and Sonnet 5 all have a 1,000,000-token context window;
|
|
14
|
+
* Haiku 4.5 is 200,000.
|
|
15
|
+
* On the Max plan the keeper agents run Opus at its full 1M window, so the
|
|
16
|
+
* context meter must use 1M for it — otherwise a long chat reads >100%.
|
|
16
17
|
*/
|
|
17
18
|
/**
|
|
18
19
|
* The selectable models, in picker order. The first entry is the keeper default
|
|
@@ -20,6 +21,12 @@
|
|
|
20
21
|
* renders.
|
|
21
22
|
*/
|
|
22
23
|
export const MODELS = [
|
|
24
|
+
{
|
|
25
|
+
id: "claude-opus-5-5",
|
|
26
|
+
label: "Opus 5.5",
|
|
27
|
+
contextLimit: 1_000_000,
|
|
28
|
+
pricing: { inputPer1M: 4, outputPer1M: 20, cacheReadMultiplier: 0.05 },
|
|
29
|
+
},
|
|
23
30
|
{
|
|
24
31
|
id: "claude-opus-5",
|
|
25
32
|
label: "Opus 5",
|
|
@@ -32,6 +39,12 @@ export const MODELS = [
|
|
|
32
39
|
contextLimit: 1_000_000,
|
|
33
40
|
pricing: { inputPer1M: 5, outputPer1M: 25 },
|
|
34
41
|
},
|
|
42
|
+
{
|
|
43
|
+
id: "claude-fable-5-1",
|
|
44
|
+
label: "Fable 5.1",
|
|
45
|
+
contextLimit: 1_000_000,
|
|
46
|
+
pricing: { inputPer1M: 10, outputPer1M: 50, cacheReadMultiplier: 0.025 },
|
|
47
|
+
},
|
|
35
48
|
{
|
|
36
49
|
id: "claude-fable-5",
|
|
37
50
|
label: "Fable 5",
|
|
@@ -42,7 +55,11 @@ export const MODELS = [
|
|
|
42
55
|
id: "claude-sonnet-5",
|
|
43
56
|
label: "Sonnet 5",
|
|
44
57
|
contextLimit: 1_000_000,
|
|
45
|
-
|
|
58
|
+
// $2/$10, not $3/$15: the $2/$10 launch price was announced as introductory
|
|
59
|
+
// through 2026-08-31, but the scheduled 2026-09-01 rise to $3/$15 was
|
|
60
|
+
// cancelled and $2/$10 is now standard. We shipped the increase that never
|
|
61
|
+
// happened, overstating every Sonnet 5 cost estimate by 50%.
|
|
62
|
+
pricing: { inputPer1M: 2, outputPer1M: 10 },
|
|
46
63
|
},
|
|
47
64
|
{
|
|
48
65
|
id: "claude-haiku-4-5-20251001",
|
|
@@ -51,8 +68,17 @@ export const MODELS = [
|
|
|
51
68
|
pricing: { inputPer1M: 1, outputPer1M: 5 },
|
|
52
69
|
},
|
|
53
70
|
];
|
|
54
|
-
/**
|
|
55
|
-
|
|
71
|
+
/**
|
|
72
|
+
* The model a project runs on unless the project overrides it. Moved from
|
|
73
|
+
* Opus 5 to Opus 5.5 when 5.5 shipped: it is both newer and cheaper ($4/$20 vs
|
|
74
|
+
* $5/$25, and half-price cache reads), so unpinned projects get a better model
|
|
75
|
+
* for less. A project that pins `model` in its `project.yaml` is untouched.
|
|
76
|
+
*
|
|
77
|
+
* Keep this equal to `MODELS[0].id` — the picker renders catalog order, so the
|
|
78
|
+
* default must be the first entry or the UI implies a different default than
|
|
79
|
+
* the server applies. `models.test.ts` asserts the two agree.
|
|
80
|
+
*/
|
|
81
|
+
export const DEFAULT_MODEL = "claude-opus-5-5";
|
|
56
82
|
/**
|
|
57
83
|
* Resolve the models OFFERED (the picker/allow-list) from an optional list of
|
|
58
84
|
* allowed ids (issue #457 Step 2). {@link MODELS} stays the authoritative CATALOG
|
|
@@ -142,7 +168,12 @@ export function isValidMaxTurns(n) {
|
|
|
142
168
|
}
|
|
143
169
|
/** The cheap model the post-turn sweeper (curator) always uses. */
|
|
144
170
|
export const SWEEPER_DEFAULT_MODEL = "claude-haiku-4-5-20251001";
|
|
145
|
-
/**
|
|
171
|
+
/**
|
|
172
|
+
* Default context limit when a model id is unknown. 200k is the conservative
|
|
173
|
+
* floor, not the common case — every catalog model except Haiku 4.5 is 1M. An
|
|
174
|
+
* unknown id reading 200k makes the context meter over-report rather than
|
|
175
|
+
* silently divide by undefined.
|
|
176
|
+
*/
|
|
146
177
|
const DEFAULT_CONTEXT_LIMIT = 200000;
|
|
147
178
|
/** Whether `id` is one of the known selectable models. */
|
|
148
179
|
export function isKnownModel(id) {
|
|
@@ -159,7 +190,10 @@ export function getContextLimit(id) {
|
|
|
159
190
|
export function getModelInfo(id) {
|
|
160
191
|
return MODELS.find((m) => m.id === id);
|
|
161
192
|
}
|
|
162
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* Cache-write (5-minute ephemeral) is 1.25× the input rate; cache-read is 0.1×
|
|
195
|
+
* — except on models that override it via {@link ModelPricing.cacheReadMultiplier}.
|
|
196
|
+
*/
|
|
163
197
|
const CACHE_WRITE_MULTIPLIER = 1.25;
|
|
164
198
|
const CACHE_READ_MULTIPLIER = 0.1;
|
|
165
199
|
/**
|
|
@@ -176,9 +210,10 @@ export function estimateCostUsd(modelId, t) {
|
|
|
176
210
|
if (!pricing)
|
|
177
211
|
return null;
|
|
178
212
|
const { inputPer1M, outputPer1M } = pricing;
|
|
213
|
+
const cacheRead = pricing.cacheReadMultiplier ?? CACHE_READ_MULTIPLIER;
|
|
179
214
|
const usd = (t.inputTokens * inputPer1M +
|
|
180
215
|
t.cacheCreationTokens * inputPer1M * CACHE_WRITE_MULTIPLIER +
|
|
181
|
-
t.cacheReadTokens * inputPer1M *
|
|
216
|
+
t.cacheReadTokens * inputPer1M * cacheRead +
|
|
182
217
|
t.outputTokens * outputPer1M) /
|
|
183
218
|
1_000_000;
|
|
184
219
|
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. */
|
|
@@ -23,6 +23,21 @@ export type WorkspaceKey = string;
|
|
|
23
23
|
export declare const ROOT_KEY: WorkspaceKey;
|
|
24
24
|
/** Whether a key addresses the root workspace (i.e. `projectsRoot` itself). */
|
|
25
25
|
export declare function isRootKey(key: WorkspaceKey): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* The root workspace's title when nothing has been set (#921).
|
|
28
|
+
*
|
|
29
|
+
* A project falls back to its slug; the root has none, so it used to read as
|
|
30
|
+
* `basename(projectsRoot)` — which surfaced a filesystem detail as a title:
|
|
31
|
+
* lowercase `projects` by default, and whatever `PADDOCK_PROJECTS_DIR` happens
|
|
32
|
+
* to be called otherwise. "Home" is what the rest of the app already calls this
|
|
33
|
+
* place (the side-nav row and the workspace's own tab label), so the title bar
|
|
34
|
+
* now agrees with them.
|
|
35
|
+
*
|
|
36
|
+
* It is a DEFAULT, never a stored value: `normalize` applies it on read, and
|
|
37
|
+
* `writeYaml` keeps it off disk (see there), so a later instance-name config
|
|
38
|
+
* field has one obvious place to feed and nothing baked in to fight.
|
|
39
|
+
*/
|
|
40
|
+
export declare const ROOT_DEFAULT_NAME = "Home";
|
|
26
41
|
/**
|
|
27
42
|
* The root workspace's name in the **herdctl agent namespace**.
|
|
28
43
|
*
|
|
@@ -16,6 +16,21 @@ export const ROOT_KEY = "";
|
|
|
16
16
|
export function isRootKey(key) {
|
|
17
17
|
return key === ROOT_KEY;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* The root workspace's title when nothing has been set (#921).
|
|
21
|
+
*
|
|
22
|
+
* A project falls back to its slug; the root has none, so it used to read as
|
|
23
|
+
* `basename(projectsRoot)` — which surfaced a filesystem detail as a title:
|
|
24
|
+
* lowercase `projects` by default, and whatever `PADDOCK_PROJECTS_DIR` happens
|
|
25
|
+
* to be called otherwise. "Home" is what the rest of the app already calls this
|
|
26
|
+
* place (the side-nav row and the workspace's own tab label), so the title bar
|
|
27
|
+
* now agrees with them.
|
|
28
|
+
*
|
|
29
|
+
* It is a DEFAULT, never a stored value: `normalize` applies it on read, and
|
|
30
|
+
* `writeYaml` keeps it off disk (see there), so a later instance-name config
|
|
31
|
+
* field has one obvious place to feed and nothing baked in to fight.
|
|
32
|
+
*/
|
|
33
|
+
export const ROOT_DEFAULT_NAME = "Home";
|
|
19
34
|
/**
|
|
20
35
|
* The root workspace's name in the **herdctl agent namespace**.
|
|
21
36
|
*
|
|
@@ -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
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { IMAGE_MIME, VIDEO_MIME, DOCUMENT_MIME, fileKind, contentTypeFor } from "./project-mime.js";
|
|
2
2
|
export { IMAGE_MIME, VIDEO_MIME, DOCUMENT_MIME, fileKind, contentTypeFor };
|
|
3
|
-
import { ROOT_KEY, isRootKey, isPathInside, isSystemPath, isManaged, contentDirFor, isValidRepoUrl, repoCheckoutName, workingDirFor, slugify, ProjectError } from "./project-paths.js";
|
|
4
|
-
export { ROOT_KEY, isRootKey, isPathInside, isSystemPath, isManaged, contentDirFor, isValidRepoUrl, repoCheckoutName, workingDirFor, slugify, ProjectError, };
|
|
3
|
+
import { ROOT_KEY, ROOT_DEFAULT_NAME, isRootKey, isPathInside, isSystemPath, isManaged, contentDirFor, isValidRepoUrl, repoCheckoutName, workingDirFor, slugify, ProjectError } from "./project-paths.js";
|
|
4
|
+
export { ROOT_KEY, ROOT_DEFAULT_NAME, isRootKey, isPathInside, isSystemPath, isManaged, contentDirFor, isValidRepoUrl, repoCheckoutName, workingDirFor, slugify, ProjectError, };
|
|
5
5
|
import { type Project, type FileKind, type FileEntry, type CreateProjectInput, type UpdateProjectInput } from "./project-types.js";
|
|
6
6
|
export * from "./project-types.js";
|
|
7
7
|
export declare class ProjectStore {
|
|
@@ -36,8 +36,8 @@ import { PROJECT_SCHEMA_VERSION, SCHEMA_VERSION_KEY, projectSchemaSkip, } from "
|
|
|
36
36
|
// them at `./projects.js` — the same one-place-to-import discipline as before.
|
|
37
37
|
import { IMAGE_MIME, VIDEO_MIME, DOCUMENT_MIME, fileKind, contentTypeFor, } from "./project-mime.js";
|
|
38
38
|
export { IMAGE_MIME, VIDEO_MIME, DOCUMENT_MIME, fileKind, contentTypeFor };
|
|
39
|
-
import { SLUG_RE, ROOT_KEY, isRootKey, isPathInside, isSystemPath, isManaged, contentDirFor, isValidRepoUrl, repoCheckoutName, workingDirFor, slugify, today, claudeTemplate, ProjectError, } from "./project-paths.js";
|
|
40
|
-
export { ROOT_KEY, isRootKey, isPathInside, isSystemPath, isManaged, contentDirFor, isValidRepoUrl, repoCheckoutName, workingDirFor, slugify, ProjectError, };
|
|
39
|
+
import { SLUG_RE, ROOT_KEY, ROOT_DEFAULT_NAME, isRootKey, isPathInside, isSystemPath, isManaged, contentDirFor, isValidRepoUrl, repoCheckoutName, workingDirFor, slugify, today, claudeTemplate, ProjectError, } from "./project-paths.js";
|
|
40
|
+
export { ROOT_KEY, ROOT_DEFAULT_NAME, isRootKey, isPathInside, isSystemPath, isManaged, contentDirFor, isValidRepoUrl, repoCheckoutName, workingDirFor, slugify, ProjectError, };
|
|
41
41
|
import { gitRemotes, normalizeRemote } from "./adoptable.js";
|
|
42
42
|
import { normalizeLinks, PATCHABLE_KEYS, } from "./project-types.js";
|
|
43
43
|
import * as projectFiles from "./project-files.js";
|
|
@@ -78,6 +78,36 @@ function sanitizeModelsOverride(raw) {
|
|
|
78
78
|
}
|
|
79
79
|
return ids.length > 0 ? ids : undefined;
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* A workspace's displayed name, defaulted (#921).
|
|
83
|
+
*
|
|
84
|
+
* A project falls back to its slug. The ROOT has no slug to fall back to, so it
|
|
85
|
+
* used to read as `basename(projectsRoot)` — a filesystem detail in the title
|
|
86
|
+
* bar: lowercase `projects` by default, and whatever `PADDOCK_PROJECTS_DIR`
|
|
87
|
+
* happened to be called otherwise. It now reads as {@link ROOT_DEFAULT_NAME},
|
|
88
|
+
* the word the side-nav row and the workspace's own tab label already use. A
|
|
89
|
+
* name the user actually set still wins. (No instance-name config field exists
|
|
90
|
+
* yet; when one lands, this is the single place it should feed.)
|
|
91
|
+
*
|
|
92
|
+
* The root's default also survives a BLANK on-disk name, not just an absent one.
|
|
93
|
+
* The UI cannot save one — its Save button disables on an empty Name — but the
|
|
94
|
+
* fix ships with "delete the `name:` line by hand" as the upgrade path for an
|
|
95
|
+
* instance that already persisted a derived value, and a half-done edit
|
|
96
|
+
* (`name:` with nothing after it, `name: ""`) is the likely slip. Unset, null and
|
|
97
|
+
* blank all mean the same thing here, so they all read as Home rather than as an
|
|
98
|
+
* empty title bar.
|
|
99
|
+
*/
|
|
100
|
+
function nameFor(name, key) {
|
|
101
|
+
if (isRootKey(key))
|
|
102
|
+
return name?.trim() ? name : ROOT_DEFAULT_NAME;
|
|
103
|
+
return name ?? key;
|
|
104
|
+
}
|
|
105
|
+
/** The on-disk record minus `name` — the root's un-persisted default (#921). */
|
|
106
|
+
function omitName(yaml) {
|
|
107
|
+
const { name: _name, ...rest } = yaml;
|
|
108
|
+
void _name;
|
|
109
|
+
return rest;
|
|
110
|
+
}
|
|
81
111
|
export class ProjectStore {
|
|
82
112
|
root;
|
|
83
113
|
dataDir;
|
|
@@ -1158,10 +1188,7 @@ export class ProjectStore {
|
|
|
1158
1188
|
normalize(p, key) {
|
|
1159
1189
|
const started = p.started ?? today();
|
|
1160
1190
|
return {
|
|
1161
|
-
|
|
1162
|
-
// directory's basename. (No instance-name config field exists yet; when
|
|
1163
|
-
// one lands, this is the single place it should feed.)
|
|
1164
|
-
name: p.name ?? (isRootKey(key) ? path.basename(path.resolve(this.root)) : key),
|
|
1191
|
+
name: nameFor(p.name, key),
|
|
1165
1192
|
slug: p.slug ?? key,
|
|
1166
1193
|
status: p.status ?? "active",
|
|
1167
1194
|
domain: Array.isArray(p.domain) ? p.domain : [],
|
|
@@ -1320,7 +1347,16 @@ export class ProjectStore {
|
|
|
1320
1347
|
// construction this build's shape whatever the file said before. A legacy
|
|
1321
1348
|
// file therefore gains the key the next time it is saved for some other
|
|
1322
1349
|
// reason — there is no backfill pass, and merely reading one writes nothing.
|
|
1323
|
-
|
|
1350
|
+
// A DERIVED root name never reaches the disk (#921). `normalize` runs on
|
|
1351
|
+
// every read and the mutators write the normalized record straight back, so
|
|
1352
|
+
// one unrelated settings save used to bake the fallback into project.yaml
|
|
1353
|
+
// permanently — where it would then outlive any later change to the default,
|
|
1354
|
+
// and would mask an instance-name config field if one ever lands. Filtered
|
|
1355
|
+
// for the ROOT only, and only when the value IS the default: an intentional
|
|
1356
|
+
// title is written like any other (and a user who types "Home" gets exactly
|
|
1357
|
+
// "Home" back from the fallback, so nothing is lost by not storing it).
|
|
1358
|
+
const record = isRootKey(slug) && yaml.name === ROOT_DEFAULT_NAME ? omitName(yaml) : yaml;
|
|
1359
|
+
const body = YAML.stringify({ [SCHEMA_VERSION_KEY]: PROJECT_SCHEMA_VERSION, ...record });
|
|
1324
1360
|
await fs.writeFile(path.join(this.dirFor(slug), PROJECT_FILE), header + body, "utf8");
|
|
1325
1361
|
}
|
|
1326
1362
|
/**
|
|
@@ -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", {
|