@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.
Files changed (72) hide show
  1. package/package.json +1 -1
  2. package/packages/server/dist/config.d.ts +26 -0
  3. package/packages/server/dist/config.js +32 -0
  4. package/packages/server/dist/environment-prompt.d.ts +52 -0
  5. package/packages/server/dist/environment-prompt.js +61 -0
  6. package/packages/server/dist/herdctl.d.ts +51 -0
  7. package/packages/server/dist/herdctl.js +61 -0
  8. package/packages/server/dist/instance-config.d.ts +13 -5
  9. package/packages/server/dist/instance-config.js +40 -0
  10. package/packages/server/package.json +1 -1
  11. package/packages/web/dist/assets/InstanceConfigPage-Bypd_quL.js +2 -0
  12. package/packages/web/dist/assets/{MarkdownRenderer-CJZFLj_8.js → MarkdownRenderer-Co8w9iuX.js} +1 -1
  13. package/packages/web/dist/assets/{ProjectRedirect-BHwVbbqB.js → ProjectRedirect-DXrA24Cy.js} +1 -1
  14. package/packages/web/dist/assets/{ProjectView-BOvuAITg.js → ProjectView-B9opP7Sv.js} +3 -3
  15. package/packages/web/dist/assets/{ProjectsGrid-BCojZOGY.js → ProjectsGrid-D2cwb2jY.js} +1 -1
  16. package/packages/web/dist/assets/{arc-BVyxvnkl.js → arc-B--FIRB8.js} +1 -1
  17. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-Cd9Puqg1.js → architectureDiagram-3BPJPVTR-B6dRWZZ6.js} +1 -1
  18. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-B_AQt_Rx.js → blockDiagram-GPEHLZMM-DLxFAvvf.js} +1 -1
  19. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-7e0TQtXB.js → c4Diagram-AAUBKEIU-CNee5ks4.js} +1 -1
  20. package/packages/web/dist/assets/channel-Bw0A52CB.js +2 -0
  21. package/packages/web/dist/assets/{chunk-2J33WTMH-Dz4WZmUZ.js → chunk-2J33WTMH-DObw03Xl.js} +1 -1
  22. package/packages/web/dist/assets/{chunk-4BX2VUAB-DVWYldo6.js → chunk-4BX2VUAB-CH2SR6nN.js} +1 -1
  23. package/packages/web/dist/assets/{chunk-55IACEB6-BIMrizQd.js → chunk-55IACEB6-WlVpegT3.js} +1 -1
  24. package/packages/web/dist/assets/{chunk-727SXJPM-CL2q9fF1.js → chunk-727SXJPM-g8GT9Rgd.js} +1 -1
  25. package/packages/web/dist/assets/{chunk-AQP2D5EJ-BSq3HlQ9.js → chunk-AQP2D5EJ-DbN4PsDF.js} +1 -1
  26. package/packages/web/dist/assets/{chunk-FMBD7UC4-Bgt64pLE.js → chunk-FMBD7UC4-DSN3BjFG.js} +1 -1
  27. package/packages/web/dist/assets/{chunk-ND2GUHAM-C8Pr2A38.js → chunk-ND2GUHAM-D9R4le0z.js} +1 -1
  28. package/packages/web/dist/assets/{chunk-QZHKN3VN-BAOqh-MN.js → chunk-QZHKN3VN-D4YgRaKH.js} +1 -1
  29. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-itf_kt-q.js +2 -0
  30. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-itf_kt-q.js +2 -0
  31. package/packages/web/dist/assets/{core-lJeGACQg.js → core-CysJ2gLj.js} +1 -1
  32. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BF4tameA.js → cose-bilkent-S5V4N54A-D1PB0KNu.js} +1 -1
  33. package/packages/web/dist/assets/{dagre-BM42HDAG-DdsLM6ql.js → dagre-BM42HDAG-C6Uuq-dq.js} +1 -1
  34. package/packages/web/dist/assets/{diagram-2AECGRRQ-BQ78S_RM.js → diagram-2AECGRRQ-C9JvQ425.js} +1 -1
  35. package/packages/web/dist/assets/{diagram-5GNKFQAL-CAKE6nHE.js → diagram-5GNKFQAL-Cu6pdh2d.js} +1 -1
  36. package/packages/web/dist/assets/{diagram-KO2AKTUF-fEvHRXgr.js → diagram-KO2AKTUF-PmXcUQhY.js} +1 -1
  37. package/packages/web/dist/assets/{diagram-LMA3HP47-CtWsgeSm.js → diagram-LMA3HP47-9ww6_9Iq.js} +1 -1
  38. package/packages/web/dist/assets/{diagram-OG6HWLK6-Buog8k7_.js → diagram-OG6HWLK6-DWQVjv_n.js} +1 -1
  39. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-CWvVVPZS.js → erDiagram-TEJ5UH35-6F7xTuff.js} +1 -1
  40. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DsCGXtp4.js → flowDiagram-I6XJVG4X-SCiGu7m6.js} +1 -1
  41. package/packages/web/dist/assets/{format-DamJQ9LO.js → format-yf7Vf9kR.js} +1 -1
  42. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-CsdwIvvC.js → ganttDiagram-6RSMTGT7-BNpPwxkL.js} +1 -1
  43. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-hZvrX1sD.js → gitGraphDiagram-PVQCEYII-C0gyAJjW.js} +1 -1
  44. package/packages/web/dist/assets/{index-CGbZdrm8.js → index-B-YuzkF6.js} +2 -2
  45. package/packages/web/dist/assets/index-CvGiQddO.css +1 -0
  46. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-nE_Vtz_l.js → infoDiagram-5YYISTIA-CJ2CFPfy.js} +1 -1
  47. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-2Nlqs0oH.js → ishikawaDiagram-YF4QCWOH-Cs0D32Ry.js} +1 -1
  48. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-mjU1fMc4.js → journeyDiagram-JHISSGLW-Bc9fuI2i.js} +1 -1
  49. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-C_9xNaa3.js → kanban-definition-UN3LZRKU-D_tC7QfC.js} +1 -1
  50. package/packages/web/dist/assets/{linear-BxKd91ho.js → linear-BzGHkF9K.js} +1 -1
  51. package/packages/web/dist/assets/{mermaid.core-C-CuhD8I.js → mermaid.core-DlbsYzE-.js} +4 -4
  52. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-Bs6yfcZv.js → mindmap-definition-RKZ34NQL-Bxeh-ToD.js} +1 -1
  53. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-DNdqX7O5.js → pieDiagram-4H26LBE5-CDmilo8S.js} +1 -1
  54. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-ZMJl9OdZ.js → quadrantDiagram-W4KKPZXB-D14-JyE9.js} +1 -1
  55. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-RmxMdm2q.js → requirementDiagram-4Y6WPE33-CJ1PUGu-.js} +1 -1
  56. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-OAAhWcfK.js → sankeyDiagram-5OEKKPKP-BkUK73W0.js} +1 -1
  57. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-BWF7rCyN.js → sequenceDiagram-3UESZ5HK-sbcvsuLp.js} +1 -1
  58. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-M3b_zqqo.js → stateDiagram-AJRCARHV-DV-7BdHK.js} +1 -1
  59. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BlkGdXKW.js +2 -0
  60. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-BpOCh63N.js → timeline-definition-PNZ67QCA-C5su21G_.js} +1 -1
  61. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-D_iPH45Q.js → vennDiagram-CIIHVFJN-JNkIJAaQ.js} +1 -1
  62. package/packages/web/dist/assets/{wardley-L42UT6IY-DVD9I2Om.js → wardley-L42UT6IY-qcz6t2vR.js} +1 -1
  63. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-Bn1IKwjn.js → wardleyDiagram-YWT4CUSO-CMrOIZ-c.js} +1 -1
  64. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-DwtI0bkR.js → xychartDiagram-2RQKCTM6-C9q6Oy9u.js} +1 -1
  65. package/packages/web/dist/index.html +2 -2
  66. package/packages/web/dist/sw.js +1 -1
  67. package/packages/web/dist/assets/InstanceConfigPage-4cQGTj9Y.js +0 -2
  68. package/packages/web/dist/assets/channel-C_1aISNn.js +0 -2
  69. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-B8JE7cHJ.js +0 -2
  70. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-B8JE7cHJ.js +0 -2
  71. package/packages/web/dist/assets/index-DJDQ210-.css +0 -1
  72. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CI7saFDa.js +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edspencer/paddock",
3
- "version": "0.56.0",
3
+ "version": "0.57.0",
4
4
  "description": "Project-first launchpad for Claude Code: server-hosted, persistent, resumable sessions organized by project.",
5
5
  "keywords": [
6
6
  "claude",
@@ -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
- type FieldType = "number" | "boolean" | "string" | "enum" | "string-list";
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
- * `browserMcp`, whose loader (`loadBrowserMcp`) treats ANY defined value —
37
- * including blank — as authoritative (`env !== undefined`), so a defined-but-
38
- * blank `PADDOCK_BROWSER_MCP` forces `false` and must render read-only here too.
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 },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paddock/server",
3
- "version": "0.56.0",
3
+ "version": "0.57.0",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -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
+