@edspencer/paddock 0.56.0 → 0.57.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 +26 -0
- package/packages/server/dist/config.js +32 -0
- package/packages/server/dist/environment-prompt.d.ts +52 -0
- package/packages/server/dist/environment-prompt.js +61 -0
- package/packages/server/dist/herdctl.d.ts +51 -0
- package/packages/server/dist/herdctl.js +61 -0
- package/packages/server/dist/instance-config.d.ts +13 -5
- package/packages/server/dist/instance-config.js +40 -0
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/InstanceConfigPage-Bypd_quL.js +2 -0
- package/packages/web/dist/assets/{MarkdownRenderer-CJZFLj_8.js → MarkdownRenderer-Co8w9iuX.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-BHwVbbqB.js → ProjectRedirect-DXrA24Cy.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-BOvuAITg.js → ProjectView-B9opP7Sv.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-BCojZOGY.js → ProjectsGrid-D2cwb2jY.js} +1 -1
- package/packages/web/dist/assets/{arc-BVyxvnkl.js → arc-B--FIRB8.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-Cd9Puqg1.js → architectureDiagram-3BPJPVTR-B6dRWZZ6.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-B_AQt_Rx.js → blockDiagram-GPEHLZMM-DLxFAvvf.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-7e0TQtXB.js → c4Diagram-AAUBKEIU-CNee5ks4.js} +1 -1
- package/packages/web/dist/assets/channel-Bw0A52CB.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-Dz4WZmUZ.js → chunk-2J33WTMH-DObw03Xl.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-DVWYldo6.js → chunk-4BX2VUAB-CH2SR6nN.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-BIMrizQd.js → chunk-55IACEB6-WlVpegT3.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-CL2q9fF1.js → chunk-727SXJPM-g8GT9Rgd.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-BSq3HlQ9.js → chunk-AQP2D5EJ-DbN4PsDF.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-Bgt64pLE.js → chunk-FMBD7UC4-DSN3BjFG.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-C8Pr2A38.js → chunk-ND2GUHAM-D9R4le0z.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-BAOqh-MN.js → chunk-QZHKN3VN-D4YgRaKH.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-itf_kt-q.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-itf_kt-q.js +2 -0
- package/packages/web/dist/assets/{core-lJeGACQg.js → core-CysJ2gLj.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BF4tameA.js → cose-bilkent-S5V4N54A-D1PB0KNu.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-DdsLM6ql.js → dagre-BM42HDAG-C6Uuq-dq.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-BQ78S_RM.js → diagram-2AECGRRQ-C9JvQ425.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-CAKE6nHE.js → diagram-5GNKFQAL-Cu6pdh2d.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-fEvHRXgr.js → diagram-KO2AKTUF-PmXcUQhY.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-CtWsgeSm.js → diagram-LMA3HP47-9ww6_9Iq.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-Buog8k7_.js → diagram-OG6HWLK6-DWQVjv_n.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-CWvVVPZS.js → erDiagram-TEJ5UH35-6F7xTuff.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DsCGXtp4.js → flowDiagram-I6XJVG4X-SCiGu7m6.js} +1 -1
- package/packages/web/dist/assets/{format-DamJQ9LO.js → format-yf7Vf9kR.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-CsdwIvvC.js → ganttDiagram-6RSMTGT7-BNpPwxkL.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-hZvrX1sD.js → gitGraphDiagram-PVQCEYII-C0gyAJjW.js} +1 -1
- package/packages/web/dist/assets/{index-CGbZdrm8.js → index-B-YuzkF6.js} +2 -2
- package/packages/web/dist/assets/index-CvGiQddO.css +1 -0
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-nE_Vtz_l.js → infoDiagram-5YYISTIA-CJ2CFPfy.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-2Nlqs0oH.js → ishikawaDiagram-YF4QCWOH-Cs0D32Ry.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-mjU1fMc4.js → journeyDiagram-JHISSGLW-Bc9fuI2i.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-C_9xNaa3.js → kanban-definition-UN3LZRKU-D_tC7QfC.js} +1 -1
- package/packages/web/dist/assets/{linear-BxKd91ho.js → linear-BzGHkF9K.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-C-CuhD8I.js → mermaid.core-DlbsYzE-.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-Bs6yfcZv.js → mindmap-definition-RKZ34NQL-Bxeh-ToD.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-DNdqX7O5.js → pieDiagram-4H26LBE5-CDmilo8S.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-ZMJl9OdZ.js → quadrantDiagram-W4KKPZXB-D14-JyE9.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-RmxMdm2q.js → requirementDiagram-4Y6WPE33-CJ1PUGu-.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-OAAhWcfK.js → sankeyDiagram-5OEKKPKP-BkUK73W0.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-BWF7rCyN.js → sequenceDiagram-3UESZ5HK-sbcvsuLp.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-M3b_zqqo.js → stateDiagram-AJRCARHV-DV-7BdHK.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BlkGdXKW.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-BpOCh63N.js → timeline-definition-PNZ67QCA-C5su21G_.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-D_iPH45Q.js → vennDiagram-CIIHVFJN-JNkIJAaQ.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-DVD9I2Om.js → wardley-L42UT6IY-qcz6t2vR.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-Bn1IKwjn.js → wardleyDiagram-YWT4CUSO-CMrOIZ-c.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-DwtI0bkR.js → xychartDiagram-2RQKCTM6-C9q6Oy9u.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/sw.js +1 -1
- package/packages/web/dist/assets/InstanceConfigPage-4cQGTj9Y.js +0 -2
- package/packages/web/dist/assets/channel-C_1aISNn.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-B8JE7cHJ.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-B8JE7cHJ.js +0 -2
- package/packages/web/dist/assets/index-DJDQ210-.css +0 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CI7saFDa.js +0 -2
package/package.json
CHANGED
|
@@ -168,6 +168,25 @@ export interface PaddockConfig {
|
|
|
168
168
|
* instance CLAUDE.md up by walk-up. That closes #512.
|
|
169
169
|
*/
|
|
170
170
|
nativeSystemPrompt: boolean;
|
|
171
|
+
/**
|
|
172
|
+
* The **environment** prompt appended to every keeper turn's system prompt
|
|
173
|
+
* (issue #635) — the short note telling the agent it renders into a browser as
|
|
174
|
+
* GFM, not a terminal. Resolved from `PADDOCK_ENVIRONMENT_PROMPT` over the
|
|
175
|
+
* `environmentPrompt:` file key over
|
|
176
|
+
* {@link import("./environment-prompt.js").DEFAULT_ENVIRONMENT_PROMPT}.
|
|
177
|
+
*
|
|
178
|
+
* This is the RESOLVED, effective text, so it has only two meaningful states
|
|
179
|
+
* at the point of use: non-empty (append it) and `""` (append nothing — the
|
|
180
|
+
* instance opted out). The three *authoring* states — unset, a string, an
|
|
181
|
+
* empty string — collapse here, which is deliberate: every consumer just asks
|
|
182
|
+
* "is there anything to append?" and the Settings screen shows the text that
|
|
183
|
+
* is actually in force rather than a tri-state it would have to explain.
|
|
184
|
+
*
|
|
185
|
+
* Orthogonal to {@link nativeSystemPrompt}: that switch chooses the agent's
|
|
186
|
+
* *role* prompt (native preset vs Paddock's terse replace text); this states
|
|
187
|
+
* environmental fact about the deployment and applies on top of either.
|
|
188
|
+
*/
|
|
189
|
+
environmentPrompt: string;
|
|
171
190
|
/**
|
|
172
191
|
* Whether keeper turns are handed the read-only self-management MCP server
|
|
173
192
|
* (issue #214 Phase 1) — the `mcp__paddock_manage__*` tools that let a keeper
|
|
@@ -378,6 +397,13 @@ export interface PaddockConfigFile {
|
|
|
378
397
|
models?: string[] | string;
|
|
379
398
|
driveMode?: string;
|
|
380
399
|
nativeSystemPrompt?: boolean | string;
|
|
400
|
+
/**
|
|
401
|
+
* Environment system prompt override (issue #635). Absent ⇒ Paddock's built-in
|
|
402
|
+
* text; a non-empty string ⇒ that text instead; an EMPTY string ⇒ append
|
|
403
|
+
* nothing. Unlike almost every other key here, blank is meaningful and is NOT
|
|
404
|
+
* folded to the default — see {@link loadEnvironmentPrompt}.
|
|
405
|
+
*/
|
|
406
|
+
environmentPrompt?: string;
|
|
381
407
|
selfMcpEnabled?: boolean | string;
|
|
382
408
|
selfMcpWriteEnabled?: boolean | string;
|
|
383
409
|
selfMcpProjectsEnabled?: boolean | string;
|
|
@@ -24,6 +24,7 @@ import { fileURLToPath } from "node:url";
|
|
|
24
24
|
import YAML from "yaml";
|
|
25
25
|
import { DEFAULT_DRIVE_MODE, isKnownDriveMode, isKnownModel, } from "./models.js";
|
|
26
26
|
import { DEFAULT_MAX_SPAWN_DEPTH, isValidMaxSpawnDepth } from "./spawn-capability.js";
|
|
27
|
+
import { DEFAULT_ENVIRONMENT_PROMPT } from "./environment-prompt.js";
|
|
27
28
|
import { DEFAULT_RECOVERY } from "./recovery-config.js";
|
|
28
29
|
import { DEFAULT_CURATION } from "./curation-config.js";
|
|
29
30
|
import { resolveManagementApiConfig, } from "./management-config.js";
|
|
@@ -290,6 +291,7 @@ export function loadPaddockConfig() {
|
|
|
290
291
|
models: loadModels(file.models),
|
|
291
292
|
driveMode: loadDriveMode(file.driveMode),
|
|
292
293
|
nativeSystemPrompt: loadNativeSystemPrompt(file.nativeSystemPrompt),
|
|
294
|
+
environmentPrompt: loadEnvironmentPrompt(file.environmentPrompt),
|
|
293
295
|
selfMcpEnabled: loadSelfMcpEnabled(file.selfMcpEnabled),
|
|
294
296
|
selfMcpWriteEnabled: loadSelfMcpEnabled(file.selfMcpEnabled) && loadSelfMcpWriteEnabled(file.selfMcpWriteEnabled),
|
|
295
297
|
// Projects tool implies write implies read — the tool is appended in the write
|
|
@@ -534,6 +536,36 @@ function loadNativeSystemPrompt(file) {
|
|
|
534
536
|
const raw = envOr("PADDOCK_NATIVE_PROMPT", fileOr(file, "true")).toLowerCase();
|
|
535
537
|
return !(raw === "0" || raw === "false" || raw === "no");
|
|
536
538
|
}
|
|
539
|
+
/**
|
|
540
|
+
* Resolve the environment system prompt (issue #635): env
|
|
541
|
+
* `PADDOCK_ENVIRONMENT_PROMPT` over the `environmentPrompt:` file key over the
|
|
542
|
+
* built-in {@link DEFAULT_ENVIRONMENT_PROMPT}.
|
|
543
|
+
*
|
|
544
|
+
* Deliberately does NOT use `envOr`/`fileOr`. Those fold a blank value to the
|
|
545
|
+
* fallback, which would make opting out impossible — blank is the opt-out here,
|
|
546
|
+
* so precedence is decided on *definedness*, not on emptiness:
|
|
547
|
+
*
|
|
548
|
+
* - `PADDOCK_ENVIRONMENT_PROMPT` **defined at all** (even empty) wins outright.
|
|
549
|
+
* That is why the field is declared `envShadowWhenDefined` in
|
|
550
|
+
* instance-config.ts — same rule as `PADDOCK_BROWSER_MCP` — so the Settings
|
|
551
|
+
* screen renders it read-only in exactly the cases where it really is.
|
|
552
|
+
* - Otherwise a `string` file value wins, empty included. A non-string (a YAML
|
|
553
|
+
* number, a mapping, `null` from a bare `environmentPrompt:`) is ignored
|
|
554
|
+
* rather than stringified — `"null"` is not a prompt anyone meant to write.
|
|
555
|
+
* - Otherwise the built-in default.
|
|
556
|
+
*
|
|
557
|
+
* The value is used verbatim: no trimming, no normalization. An operator's
|
|
558
|
+
* trailing newline or leading indentation survives the round-trip, because the
|
|
559
|
+
* text is going into a prompt where whitespace is content.
|
|
560
|
+
*/
|
|
561
|
+
function loadEnvironmentPrompt(file) {
|
|
562
|
+
const env = process.env.PADDOCK_ENVIRONMENT_PROMPT;
|
|
563
|
+
if (env !== undefined)
|
|
564
|
+
return env;
|
|
565
|
+
if (typeof file === "string")
|
|
566
|
+
return file;
|
|
567
|
+
return DEFAULT_ENVIRONMENT_PROMPT;
|
|
568
|
+
}
|
|
537
569
|
/**
|
|
538
570
|
* Resolve the instance model allow-list (issue #457 Step 2), precedence
|
|
539
571
|
* env `PADDOCK_MODELS` (comma-separated ids) over YAML `models:` (a string array,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paddock's built-in **environment** system prompt (issue #635).
|
|
3
|
+
*
|
|
4
|
+
* Paddock injects no system prompt of its own: on a default instance the keeper
|
|
5
|
+
* runs on Claude Code's stock `claude_code` preset, which is written for a
|
|
6
|
+
* terminal. Nothing anywhere tells the model that its replies are rendered as
|
|
7
|
+
* GitHub-Flavored Markdown in a browser (`MarkdownRenderer.tsx` = react-markdown
|
|
8
|
+
* + remark-gfm), or that `mcp__paddock__send_file` renders a file inline.
|
|
9
|
+
*
|
|
10
|
+
* This text is a small **append** to whatever system prompt the agent already
|
|
11
|
+
* has — it states environmental fact about the deployment, and deliberately does
|
|
12
|
+
* not instruct on role, tone or workflow (that is the `CLAUDE.md` hierarchy's
|
|
13
|
+
* layer, and it stays the user's).
|
|
14
|
+
*
|
|
15
|
+
* ## Why exactly two rules
|
|
16
|
+
*
|
|
17
|
+
* The text is not written from first principles — an earlier draft was, and the
|
|
18
|
+
* audit in issue #635 refuted most of it. It comes from auditing the 100 most
|
|
19
|
+
* recent chats on the dogfood instance (3,944 assistant text blocks, 2.1 MB of
|
|
20
|
+
* user-facing prose). Two findings carried ~90% of the measured value:
|
|
21
|
+
*
|
|
22
|
+
* - **`#123` is dead text.** 4,440 bare refs vs 155 markdown links. Bare URLs
|
|
23
|
+
* autolink under GFM and came out fine (only 4 inert, all backticked), so the
|
|
24
|
+
* defect is specifically issue/PR refs — and it is not inability: single
|
|
25
|
+
* messages link some refs and leave others bare.
|
|
26
|
+
* - **Visual work is described, never shown.** 194 image reads, 138
|
|
27
|
+
* screenshots, and **zero** images ever sent to a user. `send_file` was used
|
|
28
|
+
* 10 times, none of them an image. In one chat this cost a full re-work round
|
|
29
|
+
* — the agent read 17 QC frames, showed none, misread one, and shipped a
|
|
30
|
+
* regression the user then had to screenshot themselves.
|
|
31
|
+
*
|
|
32
|
+
* Things measured and deliberately **cut**, so they don't get re-added: "no ANSI
|
|
33
|
+
* colour" (0 escapes in 2.1 MB), "don't paste long content" (0 fences ≥40 lines;
|
|
34
|
+
* median fence is 3), "use markdown structure" (580 headings, 892 bullets, 101
|
|
35
|
+
* well-formed tables — already excellent), `file.ts:123` guidance (real but
|
|
36
|
+
* low-stakes; 72 of 75 sampled sit in backticks beside the symbol name), and
|
|
37
|
+
* mermaid (0 genuine missed opportunities; an instruction would over-trigger).
|
|
38
|
+
*
|
|
39
|
+
* ## Keep in sync
|
|
40
|
+
*
|
|
41
|
+
* The `send_file` tool description (`send-file-mcp.ts`) is the only other
|
|
42
|
+
* Paddock-aware signal in a normal turn. These two are the same message told
|
|
43
|
+
* twice; change one and re-read the other.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* The default environment prompt appended to every keeper turn unless the
|
|
47
|
+
* instance overrides or opts out (see `environmentPrompt` in config.ts).
|
|
48
|
+
*
|
|
49
|
+
* Kept verbatim from issue #635's evidence pass. Resist adding a third rule
|
|
50
|
+
* without measurement — several plausible candidates were cut above.
|
|
51
|
+
*/
|
|
52
|
+
export declare const DEFAULT_ENVIRONMENT_PROMPT = "You are running in Paddock, a web app \u2014 your replies render as GitHub-Flavored\nMarkdown in a browser, not as terminal output.\n\n- Show, don't describe. A screenshot, rendering, chart or file the user is meant\n to look at should be sent with `mcp__paddock__send_file`, which renders it\n inline. A filesystem path, or \"it's up on port 5026\", leaves them unable to see\n your evidence \u2014 or to catch you misreading it.\n- Make clickable things clickable. Bare URLs autolink; `#123` does not \u2014 write\n `[#123](https://github.com/owner/repo/issues/123)`. Prefer a full URL to a bare\n port number, and link a chat or project rather than quoting its id.";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paddock's built-in **environment** system prompt (issue #635).
|
|
3
|
+
*
|
|
4
|
+
* Paddock injects no system prompt of its own: on a default instance the keeper
|
|
5
|
+
* runs on Claude Code's stock `claude_code` preset, which is written for a
|
|
6
|
+
* terminal. Nothing anywhere tells the model that its replies are rendered as
|
|
7
|
+
* GitHub-Flavored Markdown in a browser (`MarkdownRenderer.tsx` = react-markdown
|
|
8
|
+
* + remark-gfm), or that `mcp__paddock__send_file` renders a file inline.
|
|
9
|
+
*
|
|
10
|
+
* This text is a small **append** to whatever system prompt the agent already
|
|
11
|
+
* has — it states environmental fact about the deployment, and deliberately does
|
|
12
|
+
* not instruct on role, tone or workflow (that is the `CLAUDE.md` hierarchy's
|
|
13
|
+
* layer, and it stays the user's).
|
|
14
|
+
*
|
|
15
|
+
* ## Why exactly two rules
|
|
16
|
+
*
|
|
17
|
+
* The text is not written from first principles — an earlier draft was, and the
|
|
18
|
+
* audit in issue #635 refuted most of it. It comes from auditing the 100 most
|
|
19
|
+
* recent chats on the dogfood instance (3,944 assistant text blocks, 2.1 MB of
|
|
20
|
+
* user-facing prose). Two findings carried ~90% of the measured value:
|
|
21
|
+
*
|
|
22
|
+
* - **`#123` is dead text.** 4,440 bare refs vs 155 markdown links. Bare URLs
|
|
23
|
+
* autolink under GFM and came out fine (only 4 inert, all backticked), so the
|
|
24
|
+
* defect is specifically issue/PR refs — and it is not inability: single
|
|
25
|
+
* messages link some refs and leave others bare.
|
|
26
|
+
* - **Visual work is described, never shown.** 194 image reads, 138
|
|
27
|
+
* screenshots, and **zero** images ever sent to a user. `send_file` was used
|
|
28
|
+
* 10 times, none of them an image. In one chat this cost a full re-work round
|
|
29
|
+
* — the agent read 17 QC frames, showed none, misread one, and shipped a
|
|
30
|
+
* regression the user then had to screenshot themselves.
|
|
31
|
+
*
|
|
32
|
+
* Things measured and deliberately **cut**, so they don't get re-added: "no ANSI
|
|
33
|
+
* colour" (0 escapes in 2.1 MB), "don't paste long content" (0 fences ≥40 lines;
|
|
34
|
+
* median fence is 3), "use markdown structure" (580 headings, 892 bullets, 101
|
|
35
|
+
* well-formed tables — already excellent), `file.ts:123` guidance (real but
|
|
36
|
+
* low-stakes; 72 of 75 sampled sit in backticks beside the symbol name), and
|
|
37
|
+
* mermaid (0 genuine missed opportunities; an instruction would over-trigger).
|
|
38
|
+
*
|
|
39
|
+
* ## Keep in sync
|
|
40
|
+
*
|
|
41
|
+
* The `send_file` tool description (`send-file-mcp.ts`) is the only other
|
|
42
|
+
* Paddock-aware signal in a normal turn. These two are the same message told
|
|
43
|
+
* twice; change one and re-read the other.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* The default environment prompt appended to every keeper turn unless the
|
|
47
|
+
* instance overrides or opts out (see `environmentPrompt` in config.ts).
|
|
48
|
+
*
|
|
49
|
+
* Kept verbatim from issue #635's evidence pass. Resist adding a third rule
|
|
50
|
+
* without measurement — several plausible candidates were cut above.
|
|
51
|
+
*/
|
|
52
|
+
export const DEFAULT_ENVIRONMENT_PROMPT = `You are running in Paddock, a web app — your replies render as GitHub-Flavored
|
|
53
|
+
Markdown in a browser, not as terminal output.
|
|
54
|
+
|
|
55
|
+
- Show, don't describe. A screenshot, rendering, chart or file the user is meant
|
|
56
|
+
to look at should be sent with \`mcp__paddock__send_file\`, which renders it
|
|
57
|
+
inline. A filesystem path, or "it's up on port 5026", leaves them unable to see
|
|
58
|
+
your evidence — or to catch you misreading it.
|
|
59
|
+
- Make clickable things clickable. Bare URLs autolink; \`#123\` does not — write
|
|
60
|
+
\`[#123](https://github.com/owner/repo/issues/123)\`. Prefer a full URL to a bare
|
|
61
|
+
port number, and link a chat or project rather than quoting its id.`;
|
|
@@ -147,6 +147,57 @@ export declare class HerdctlService {
|
|
|
147
147
|
private jobsIndexOrNull;
|
|
148
148
|
private get jobsIndex();
|
|
149
149
|
constructor(cfg: PaddockConfig);
|
|
150
|
+
/**
|
|
151
|
+
* The environment system prompt to append to a keeper turn (issue #635), or
|
|
152
|
+
* `undefined` when the instance opted out (`environmentPrompt: ""`) — herdctl
|
|
153
|
+
* treats a falsy `systemPromptAppend` as "don't touch the system prompt", so
|
|
154
|
+
* an opted-out instance is byte-identical to pre-#635 behaviour.
|
|
155
|
+
*
|
|
156
|
+
* Read from `this.cfg` here rather than taken as a per-turn option **on
|
|
157
|
+
* purpose**. Every user-visible turn shape — a human send, a slash command, a
|
|
158
|
+
* scheduled wake, a spawned sub-chat, a `driveMode: batch` trigger — funnels
|
|
159
|
+
* through {@link chat}, {@link chatSession} or {@link runCommand}, so applying
|
|
160
|
+
* it at that choke point makes coverage structural. Threading it through
|
|
161
|
+
* `ChatTurnOptions` instead would make it one more thing five call sites have
|
|
162
|
+
* to remember, which is exactly how `isSidechainMessage` ended up wired into
|
|
163
|
+
* 1 of 5 turn paths.
|
|
164
|
+
*
|
|
165
|
+
* It is orthogonal to `nativeSystemPrompt`: herdctl appends to whatever prompt
|
|
166
|
+
* the agent has, so this rides on top of the native preset AND of Paddock's
|
|
167
|
+
* terse replace prompt (see `buildSystemPrompt` in core's sdk-runtime, which
|
|
168
|
+
* folds an append into a preset's `append` field or concatenates onto a
|
|
169
|
+
* literal string).
|
|
170
|
+
*/
|
|
171
|
+
private get environmentPromptAppend();
|
|
172
|
+
/**
|
|
173
|
+
* The same value for the **CLI** runtime (`driveMode: batch`), where it is NOT
|
|
174
|
+
* always safe to send.
|
|
175
|
+
*
|
|
176
|
+
* herdctl's CLI runtime has no `--append-system-prompt`; it folds
|
|
177
|
+
* `systemPromptAppend` into the single `--system-prompt` flag
|
|
178
|
+
* (`cli-runtime.ts`), and `--system-prompt` REPLACES Claude Code's preset:
|
|
179
|
+
*
|
|
180
|
+
* agent.system_prompt && append → --system-prompt "<agent>\n\n<append>" ✅ appends
|
|
181
|
+
* append only → --system-prompt "<append>" ❌ REPLACES
|
|
182
|
+
*
|
|
183
|
+
* Paddock sets `agent.system_prompt` only when `nativeSystemPrompt` is false
|
|
184
|
+
* (herdctl-agent-config.ts), so on a DEFAULT batch instance the second branch
|
|
185
|
+
* fires and a two-rule environment note would silently become the agent's
|
|
186
|
+
* entire system prompt — the whole coding preset gone. That is the same
|
|
187
|
+
* replace-vs-append trap #635 warns about, one layer down.
|
|
188
|
+
*
|
|
189
|
+
* So: on the CLI path we send the append only when it will genuinely be
|
|
190
|
+
* appended. A default (native) batch instance is left byte-identical to
|
|
191
|
+
* pre-#635 and simply doesn't get the environment prompt — a missing hint is
|
|
192
|
+
* survivable; a missing coding prompt is not. The SDK runtime has no such
|
|
193
|
+
* problem (it folds the text into the preset's `append` field), and the SDK
|
|
194
|
+
* runtime is what every chat actually uses: `openChatSession` hard-codes it,
|
|
195
|
+
* and `session` is the default drive mode. Batch is the legacy path.
|
|
196
|
+
*
|
|
197
|
+
* Lifting this needs `--append-system-prompt` support in core's CLI runtime;
|
|
198
|
+
* tracked upstream in edspencer/herdctl.
|
|
199
|
+
*/
|
|
200
|
+
private get environmentPromptAppendForCli();
|
|
150
201
|
/**
|
|
151
202
|
* Construct + initialize the FleetManager against a minimal zero-agent
|
|
152
203
|
* config (fleet + defaults only). Agents are then registered programmatically
|
|
@@ -208,6 +208,61 @@ export class HerdctlService {
|
|
|
208
208
|
constructor(cfg) {
|
|
209
209
|
this.cfg = cfg;
|
|
210
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* The environment system prompt to append to a keeper turn (issue #635), or
|
|
213
|
+
* `undefined` when the instance opted out (`environmentPrompt: ""`) — herdctl
|
|
214
|
+
* treats a falsy `systemPromptAppend` as "don't touch the system prompt", so
|
|
215
|
+
* an opted-out instance is byte-identical to pre-#635 behaviour.
|
|
216
|
+
*
|
|
217
|
+
* Read from `this.cfg` here rather than taken as a per-turn option **on
|
|
218
|
+
* purpose**. Every user-visible turn shape — a human send, a slash command, a
|
|
219
|
+
* scheduled wake, a spawned sub-chat, a `driveMode: batch` trigger — funnels
|
|
220
|
+
* through {@link chat}, {@link chatSession} or {@link runCommand}, so applying
|
|
221
|
+
* it at that choke point makes coverage structural. Threading it through
|
|
222
|
+
* `ChatTurnOptions` instead would make it one more thing five call sites have
|
|
223
|
+
* to remember, which is exactly how `isSidechainMessage` ended up wired into
|
|
224
|
+
* 1 of 5 turn paths.
|
|
225
|
+
*
|
|
226
|
+
* It is orthogonal to `nativeSystemPrompt`: herdctl appends to whatever prompt
|
|
227
|
+
* the agent has, so this rides on top of the native preset AND of Paddock's
|
|
228
|
+
* terse replace prompt (see `buildSystemPrompt` in core's sdk-runtime, which
|
|
229
|
+
* folds an append into a preset's `append` field or concatenates onto a
|
|
230
|
+
* literal string).
|
|
231
|
+
*/
|
|
232
|
+
get environmentPromptAppend() {
|
|
233
|
+
return this.cfg.environmentPrompt || undefined;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The same value for the **CLI** runtime (`driveMode: batch`), where it is NOT
|
|
237
|
+
* always safe to send.
|
|
238
|
+
*
|
|
239
|
+
* herdctl's CLI runtime has no `--append-system-prompt`; it folds
|
|
240
|
+
* `systemPromptAppend` into the single `--system-prompt` flag
|
|
241
|
+
* (`cli-runtime.ts`), and `--system-prompt` REPLACES Claude Code's preset:
|
|
242
|
+
*
|
|
243
|
+
* agent.system_prompt && append → --system-prompt "<agent>\n\n<append>" ✅ appends
|
|
244
|
+
* append only → --system-prompt "<append>" ❌ REPLACES
|
|
245
|
+
*
|
|
246
|
+
* Paddock sets `agent.system_prompt` only when `nativeSystemPrompt` is false
|
|
247
|
+
* (herdctl-agent-config.ts), so on a DEFAULT batch instance the second branch
|
|
248
|
+
* fires and a two-rule environment note would silently become the agent's
|
|
249
|
+
* entire system prompt — the whole coding preset gone. That is the same
|
|
250
|
+
* replace-vs-append trap #635 warns about, one layer down.
|
|
251
|
+
*
|
|
252
|
+
* So: on the CLI path we send the append only when it will genuinely be
|
|
253
|
+
* appended. A default (native) batch instance is left byte-identical to
|
|
254
|
+
* pre-#635 and simply doesn't get the environment prompt — a missing hint is
|
|
255
|
+
* survivable; a missing coding prompt is not. The SDK runtime has no such
|
|
256
|
+
* problem (it folds the text into the preset's `append` field), and the SDK
|
|
257
|
+
* runtime is what every chat actually uses: `openChatSession` hard-codes it,
|
|
258
|
+
* and `session` is the default drive mode. Batch is the legacy path.
|
|
259
|
+
*
|
|
260
|
+
* Lifting this needs `--append-system-prompt` support in core's CLI runtime;
|
|
261
|
+
* tracked upstream in edspencer/herdctl.
|
|
262
|
+
*/
|
|
263
|
+
get environmentPromptAppendForCli() {
|
|
264
|
+
return this.cfg.nativeSystemPrompt ? undefined : this.environmentPromptAppend;
|
|
265
|
+
}
|
|
211
266
|
/**
|
|
212
267
|
* Construct + initialize the FleetManager against a minimal zero-agent
|
|
213
268
|
* config (fleet + defaults only). Agents are then registered programmatically
|
|
@@ -581,6 +636,8 @@ export class HerdctlService {
|
|
|
581
636
|
onMessage: opts.onMessage,
|
|
582
637
|
onJobCreated: opts.onJobCreated,
|
|
583
638
|
injectedMcpServers: opts.injectedMcpServers,
|
|
639
|
+
// CLI runtime — see the getter: only safe when it will append, not replace.
|
|
640
|
+
systemPromptAppend: this.environmentPromptAppendForCli,
|
|
584
641
|
});
|
|
585
642
|
}
|
|
586
643
|
/**
|
|
@@ -629,6 +686,7 @@ export class HerdctlService {
|
|
|
629
686
|
prompt: opts.prompt,
|
|
630
687
|
manageLifecycle: true,
|
|
631
688
|
injectedMcpServers: opts.injectedMcpServers,
|
|
689
|
+
systemPromptAppend: this.environmentPromptAppend,
|
|
632
690
|
// Stream assistant text token-by-token: the SDK emits `stream_event` /
|
|
633
691
|
// `text_delta` chunks that the translator surfaces as incremental
|
|
634
692
|
// `chat:response` frames (edspencer/herdctl#382, paddock#315).
|
|
@@ -781,6 +839,9 @@ export class HerdctlService {
|
|
|
781
839
|
resume: opts.resume,
|
|
782
840
|
// Stream the command's assistant text token-by-token (paddock#315).
|
|
783
841
|
includePartialMessages: true,
|
|
842
|
+
// A slash command's output renders in the same chat bubble as any other
|
|
843
|
+
// turn, so it gets the same environment context (#635).
|
|
844
|
+
systemPromptAppend: this.environmentPromptAppend,
|
|
784
845
|
});
|
|
785
846
|
let sessionId = isResume ? opts.resume : null;
|
|
786
847
|
try {
|
|
@@ -5,7 +5,12 @@ export declare const GROUPS: {
|
|
|
5
5
|
label: string;
|
|
6
6
|
description?: string;
|
|
7
7
|
}[];
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* How the Settings screen renders a field. `text` is `string` with a multi-line
|
|
10
|
+
* control (a `<textarea>`) — same wire shape, same coercion contract; the only
|
|
11
|
+
* difference is that the UI stops squashing paragraphs into one line.
|
|
12
|
+
*/
|
|
13
|
+
type FieldType = "number" | "boolean" | "string" | "text" | "enum" | "string-list";
|
|
9
14
|
/** Outcome of coercing a raw patch value into a persistable one. */
|
|
10
15
|
type Coerced = {
|
|
11
16
|
ok: true;
|
|
@@ -32,10 +37,13 @@ interface FieldSpec {
|
|
|
32
37
|
envVars: readonly string[];
|
|
33
38
|
/**
|
|
34
39
|
* Env-shadow semantics. Default (`false`) = the var shadows only when set to a
|
|
35
|
-
* non-blank value, matching `envOr`/`envOpt` in config.ts. Set `true` for
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
40
|
+
* non-blank value, matching `envOr`/`envOpt` in config.ts. Set `true` for the
|
|
41
|
+
* fields whose loaders key on `env !== undefined` instead, so a defined-but-
|
|
42
|
+
* blank var is still authoritative and must render read-only here too:
|
|
43
|
+
* - `browserMcp` (`loadBrowserMcp`) — a blank `PADDOCK_BROWSER_MCP` forces
|
|
44
|
+
* `false`;
|
|
45
|
+
* - `environmentPrompt` (`loadEnvironmentPrompt`, #635) — a blank
|
|
46
|
+
* `PADDOCK_ENVIRONMENT_PROMPT` IS the opt-out.
|
|
39
47
|
*/
|
|
40
48
|
envShadowWhenDefined?: boolean;
|
|
41
49
|
/** Built-in default (what you get with neither env nor file). `null` ⇒ unset. */
|
|
@@ -35,6 +35,7 @@ import { DEFAULT_MAX_SPAWN_DEPTH, isValidMaxSpawnDepth } from "./spawn-capabilit
|
|
|
35
35
|
import { DEFAULT_RECOVERY } from "./recovery-config.js";
|
|
36
36
|
import { DEFAULT_ATTACHMENTS, sanitizeAllowedTypes } from "./attachments-config.js";
|
|
37
37
|
import { DEFAULT_CURATION } from "./curation-config.js";
|
|
38
|
+
import { DEFAULT_ENVIRONMENT_PROMPT } from "./environment-prompt.js";
|
|
38
39
|
/** Groups the Settings screen renders, in display order. */
|
|
39
40
|
export const GROUPS = [
|
|
40
41
|
{ id: "curation", label: "Curation", description: "Per-file token budgets the post-turn sweeper keeps its curated files under." },
|
|
@@ -76,6 +77,39 @@ const nonEmptyString = (raw) => typeof raw === "string" && raw.trim().length > 0
|
|
|
76
77
|
: { ok: false, error: "must be a non-empty string" };
|
|
77
78
|
/** A string that may be blank (blank clears the override → falls back to default). */
|
|
78
79
|
const optString = (raw) => typeof raw === "string" ? { ok: true, value: raw.trim() } : { ok: false, error: "must be a string" };
|
|
80
|
+
/**
|
|
81
|
+
* Upper bound on a prompt-shaped field, in characters. Nothing enforces a limit
|
|
82
|
+
* downstream — the text is concatenated into the system prompt of every single
|
|
83
|
+
* turn — so an accidental paste of a whole file would quietly tax every request
|
|
84
|
+
* on the instance forever. 32 KiB is ~50× the built-in default and far more than
|
|
85
|
+
* any deliberate environment note; past that, a 400 is kinder than silence.
|
|
86
|
+
*/
|
|
87
|
+
const MAX_PROMPT_CHARS = 32 * 1024;
|
|
88
|
+
/**
|
|
89
|
+
* A free-text prompt body. Unlike {@link optString} this is **verbatim**: no
|
|
90
|
+
* trimming, because leading/trailing whitespace is content in a prompt and the
|
|
91
|
+
* operator's text must round-trip through YAML byte-for-byte.
|
|
92
|
+
*
|
|
93
|
+
* The three authoring states map onto the wire like this:
|
|
94
|
+
* - `null` → delete the key → fall back to Paddock's built-in default,
|
|
95
|
+
* - `""` → written as an empty string → append nothing (opt out),
|
|
96
|
+
* - any other string → written verbatim → appended instead of the default.
|
|
97
|
+
*
|
|
98
|
+
* Rejects a NUL byte: it survives neither the repo's no-NUL invariant
|
|
99
|
+
* (`scripts/check-no-nul-bytes.mjs`) nor any useful reading of "a prompt".
|
|
100
|
+
*/
|
|
101
|
+
const promptText = (raw) => {
|
|
102
|
+
if (raw === null || raw === undefined)
|
|
103
|
+
return { ok: true, value: null };
|
|
104
|
+
if (typeof raw !== "string")
|
|
105
|
+
return { ok: false, error: "must be a string (or null to restore the default)" };
|
|
106
|
+
if (raw.length > MAX_PROMPT_CHARS) {
|
|
107
|
+
return { ok: false, error: `must be at most ${MAX_PROMPT_CHARS} characters (got ${raw.length})` };
|
|
108
|
+
}
|
|
109
|
+
if (raw.includes("\0"))
|
|
110
|
+
return { ok: false, error: "must not contain NUL bytes" };
|
|
111
|
+
return { ok: true, value: raw };
|
|
112
|
+
};
|
|
79
113
|
const oneOf = (values) => (raw) => typeof raw === "string" && values.includes(raw)
|
|
80
114
|
? { ok: true, value: raw }
|
|
81
115
|
: { ok: false, error: `must be one of: ${values.join(", ")}` };
|
|
@@ -139,6 +173,12 @@ export const FIELDS = [
|
|
|
139
173
|
{ 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) },
|
|
140
174
|
{ 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 },
|
|
141
175
|
{ 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 },
|
|
176
|
+
// Issue #635. `envShadowWhenDefined` because loadEnvironmentPrompt keys on
|
|
177
|
+
// definedness, not emptiness — a defined-but-empty PADDOCK_ENVIRONMENT_PROMPT
|
|
178
|
+
// IS the env-level opt-out, so it genuinely shadows the file and must render
|
|
179
|
+
// read-only. `default` carries the built-in text so the UI can offer a
|
|
180
|
+
// one-click restore (a `null` PUT deletes the key) without duplicating it.
|
|
181
|
+
{ key: "environmentPrompt", group: "capabilities", label: "Environment prompt", help: "Appended to every keeper turn's system prompt: what the agent should know about rendering into Paddock rather than a terminal. Clear it to append nothing.", type: "text", envVars: ["PADDOCK_ENVIRONMENT_PROMPT"], envShadowWhenDefined: true, default: DEFAULT_ENVIRONMENT_PROMPT, editable: true, coerce: promptText },
|
|
142
182
|
{ key: "selfMcpEnabled", group: "capabilities", label: "Self-management MCP (read)", help: "Let keepers list/read projects and other chats.", type: "boolean", envVars: ["PADDOCK_SELF_MCP"], default: false, editable: true, coerce: asBool },
|
|
143
183
|
{ key: "selfMcpWriteEnabled", group: "capabilities", label: "Self-management MCP (write)", help: "Let keepers create/fork/message chats (needs read enabled too).", type: "boolean", envVars: ["PADDOCK_SELF_MCP_WRITE"], default: false, editable: true, coerce: asBool },
|
|
144
184
|
{ key: "selfMcpProjectsEnabled", group: "capabilities", label: "Self-management MCP (projects)", help: "Let keepers create whole new projects, cloning a repo when repo-backed (needs self-MCP write).", type: "boolean", envVars: ["PADDOCK_SELF_MCP_PROJECTS"], default: false, editable: true, coerce: asBool },
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{r as o,b as f,j as e,A as b,k as E,l as A,m as O,M as F,n as I}from"./index-B-YuzkF6.js";function $(){const[t,s]=o.useState(null),[l,i]=o.useState(!0),[r,c]=o.useState(null),[d,m]=o.useState({}),[h,k]=o.useState(!1),[j,p]=o.useState(null),[N,u]=o.useState(!1);o.useEffect(()=>{let a=!0;return i(!0),f.getInstanceConfig().then(n=>{a&&(s(n),c(null))}).catch(n=>a&&c(n instanceof Error?n.message:String(n))).finally(()=>a&&i(!1)),()=>{a=!1}},[]);const v=(a,n)=>{u(!1),p(null),m(g=>({...g,[a]:n}))},S=a=>Object.prototype.hasOwnProperty.call(d,a.key)?d[a.key]:a.value,y=o.useMemo(()=>t?t.groups.flatMap(a=>a.fields):[],[t]),x=o.useMemo(()=>y.filter(a=>a.editable&&!a.envOverridden).filter(a=>Object.prototype.hasOwnProperty.call(d,a.key)).filter(a=>!B(d[a.key],a.value)).map(a=>a.key),[y,d]),w=async()=>{if(x.length===0)return;const a={};for(const n of x)a[n]=d[n];k(!0),p(null);try{await f.updateInstanceConfig(a);const n=await f.getInstanceConfig();s(n),m({}),u(!0)}catch(n){p(n instanceof A||n instanceof Error?n.message:String(n))}finally{k(!1)}},C=()=>{m({}),p(null),u(!1)};return e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"min-h-0 flex-1 overflow-y-auto px-4 py-5 sm:px-6",children:e.jsxs("div",{className:"mx-auto max-w-2xl",children:[e.jsx(L,{saved:N,configPath:t==null?void 0:t.configPath}),l&&e.jsx("p",{className:"text-sm text-paddock-500",children:"Loading…"}),r&&e.jsxs("p",{className:"text-sm text-red-600 dark:text-red-400",role:"alert",children:["Failed to load settings: ",r]}),t&&t.groups.map(a=>e.jsx(M,{title:a.label,description:a.description,children:e.jsx("div",{className:"grid grid-cols-1 gap-4",children:a.fields.map(n=>e.jsx(P,{field:n,value:S(n),onChange:g=>v(n.key,g)},n.key))})},a.id))]})}),t&&e.jsxs("footer",{className:"flex items-center gap-3 border-t border-paddock-200 px-4 py-3 dark:border-paddock-800 sm:px-6",children:[j&&e.jsxs("span",{className:"flex items-center gap-1.5 text-[13px] text-red-600 dark:text-red-400",role:"alert",children:[e.jsx(b,{width:14,height:14,className:"shrink-0"}),j]}),e.jsx("span",{className:"ml-auto text-[12px] text-paddock-400",children:x.length>0?`${x.length} unsaved change${x.length===1?"":"s"}`:"No changes"}),e.jsx("button",{type:"button",className:"btn-subtle",onClick:C,disabled:x.length===0||h,children:"Reset"}),e.jsx("button",{type:"button",className:"btn-primary",onClick:w,disabled:x.length===0||h,children:h?"Saving…":"Save changes"})]})]})}function L({saved:t,configPath:s}){return e.jsxs("div",{role:"status",className:`mb-6 flex items-start gap-2 rounded-lg border px-3 py-2.5 text-[13px] leading-snug ${t?"border-emerald-300 bg-emerald-50 text-emerald-800 dark:border-emerald-700/60 dark:bg-emerald-900/20 dark:text-emerald-300":"border-amber-300 bg-amber-50 text-amber-800 dark:border-amber-700/60 dark:bg-amber-900/20 dark:text-amber-300"}`,children:[t?e.jsx(E,{width:15,height:15,className:"mt-0.5 shrink-0"}):e.jsx(b,{width:15,height:15,className:"mt-0.5 shrink-0"}),e.jsx("span",{children:t?e.jsxs(e.Fragment,{children:[e.jsx("strong",{children:"Saved to disk."})," These changes take effect only after the server restarts."]}):e.jsxs(e.Fragment,{children:["Changes here are written to ",e.jsx("code",{className:"font-mono text-[12px]",children:T(s)})," ","and take effect only after the server restarts — the running instance keeps its current config until then."]})})]})}function M({title:t,description:s,children:l}){return e.jsxs("section",{className:"mb-6",children:[e.jsx("h2",{className:"text-sm font-semibold uppercase tracking-wide text-paddock-500",children:t}),s&&e.jsx("p",{className:"mb-3 mt-0.5 text-[13px] text-paddock-500",children:s}),e.jsx("div",{className:`card ${s?"":"mt-2"}`,children:l})]})}function P({field:t,value:s,onChange:l}){const i=!t.editable||t.envOverridden,r=`cfg-${t.key}`;return e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center justify-between gap-3",children:[e.jsxs("label",{htmlFor:i?void 0:r,className:"text-sm font-medium",children:[t.label,t.sensitive&&e.jsx("span",{className:"ml-1.5 align-middle text-[10px] font-semibold uppercase tracking-wide text-amber-600 dark:text-amber-400",children:"sensitive"})]}),t.type==="boolean"&&!i&&e.jsx("input",{id:r,type:"checkbox",className:"h-4 w-4 accent-accent",checked:!!s,onChange:c=>l(c.target.checked)})]}),t.help&&e.jsx("p",{className:"mt-0.5 text-[12px] leading-snug text-paddock-500",children:t.help}),i?e.jsx(V,{field:t,value:s}):t.type!=="boolean"&&e.jsx(R,{field:t,value:s,onChange:l,inputId:r}),t.envOverridden&&e.jsxs("p",{className:"mt-1 flex items-start gap-1.5 text-[12px] leading-snug text-amber-600 dark:text-amber-400",children:[e.jsx(b,{width:13,height:13,className:"mt-0.5 shrink-0"}),e.jsxs("span",{children:["Overridden by environment variable ",e.jsx("code",{className:"font-mono",children:t.envVar})," — edit that env var (and restart) to change it."]})]})]})}function R({field:t,value:s,onChange:l,inputId:i}){if(t.type==="enum")return e.jsx("select",{id:i,className:"input mt-1.5",value:String(s??""),onChange:r=>l(r.target.value),children:(t.enumValues??[]).map(r=>e.jsx("option",{value:r,children:r},r))});if(t.type==="number")return e.jsx("input",{id:i,type:"number",className:"input mt-1.5",value:s==null?"":String(s),placeholder:t.default===null?"default":String(t.default),onChange:r=>l(r.target.value===""?null:Number(r.target.value))});if(t.type==="text"){const r=s==null||s===t.default,c=s==null?String(t.default??""):String(s),d=typeof t.default=="string"&&!r;return e.jsxs("div",{className:"mt-1.5",children:[e.jsx("textarea",{id:i,className:"input min-h-[9rem] resize-y font-mono text-[12px] leading-relaxed",rows:10,spellCheck:!1,value:c,onChange:m=>l(m.target.value)}),e.jsxs("div",{className:"mt-1 flex items-center justify-between gap-3 text-[12px] text-paddock-500",children:[e.jsx("span",{children:r?"Using the built-in default.":s===""?"Empty — nothing will be appended.":`${c.length.toLocaleString()} characters.`}),d&&e.jsx("button",{type:"button",className:"shrink-0 underline underline-offset-2 hover:text-paddock-700 dark:hover:text-paddock-300",onClick:()=>l(null),children:"Restore default"})]})]})}if(t.type==="string-list"){const r=Array.isArray(s)?s.join(", "):String(s??"");return e.jsx("input",{id:i,type:"text",className:"input mt-1.5",value:r,onChange:c=>l(c.target.value.split(",").map(d=>d.trim()).filter(Boolean))})}return e.jsx("input",{id:i,type:"text",className:"input mt-1.5",value:s==null?"":String(s),placeholder:t.default===null?"":String(t.default),onChange:r=>l(r.target.value)})}function V({field:t,value:s}){return e.jsx("div",{className:"mt-1.5 rounded-md border border-dashed border-paddock-300 bg-paddock-50 px-2.5 py-1.5 text-[13px] text-paddock-600 dark:border-paddock-700 dark:bg-paddock-900/40 dark:text-paddock-300",children:t.type==="boolean"?e.jsx("span",{className:"font-mono",children:s?"true":"false"}):s==null||s===""?e.jsx("span",{className:"italic text-paddock-400",children:t.type==="text"&&s===""?"(empty — nothing appended)":"(not set)"}):t.type==="text"?e.jsx("span",{className:"block max-h-64 overflow-y-auto whitespace-pre-wrap font-mono text-[12px] leading-relaxed",children:String(s)}):e.jsx("span",{className:"break-all font-mono",children:Array.isArray(s)?s.join(", "):String(s)})})}function B(t,s){return Array.isArray(t)&&Array.isArray(s)?t.length===s.length&&t.every((l,i)=>l===s[i]):t===s}function T(t){if(!t)return"paddock.config.yaml";const s=t.split("/");return s[s.length-1]||t}function z(){const t=O(),s=(t==null?void 0:t.openNav)??(()=>{});return e.jsxs("div",{className:"flex h-full min-h-0 flex-col",children:[e.jsxs("header",{className:"pt-safe flex items-center gap-2 border-b border-paddock-200 px-3 pb-2.5 dark:border-paddock-800 sm:px-6 lg:py-4",children:[e.jsx("button",{type:"button",onClick:s,"aria-label":"Open menu",className:"btn-subtle -ml-1 shrink-0 px-2 py-1.5 lg:hidden",children:e.jsx(F,{width:20,height:20})}),e.jsx(I,{width:18,height:18,className:"shrink-0 text-paddock-400"}),e.jsx("h1",{className:"text-[15px] font-semibold tracking-tight",children:"Config"})]}),e.jsx($,{})]})}export{z as InstanceConfigPage};
|
|
2
|
+
|