@edspencer/paddock 0.76.0 → 0.78.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/app.js +8 -1
- package/packages/server/dist/browser-gpu.d.ts +31 -0
- package/packages/server/dist/browser-gpu.js +63 -0
- package/packages/server/dist/herdctl-agent-config.d.ts +8 -2
- package/packages/server/dist/herdctl-agent-config.js +10 -4
- package/packages/server/dist/herdctl-agent-names.d.ts +8 -1
- package/packages/server/dist/herdctl-agent-names.js +12 -7
- package/packages/server/dist/herdctl.d.ts +6 -1
- package/packages/server/dist/herdctl.js +9 -3
- package/packages/server/dist/instance-config.js +2 -2
- package/packages/server/dist/routes/chats.d.ts +17 -0
- package/packages/server/dist/routes/chats.js +32 -4
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{DeleteProjectDialog-BVPw-9au.js → DeleteProjectDialog-Bt_5z1b7.js} +1 -1
- package/packages/web/dist/assets/DiscoverPage-LE2yFc0F.js +2 -0
- package/packages/web/dist/assets/DiscoverView-C_VQec3o.js +2 -0
- package/packages/web/dist/assets/InstanceConfigPage-bpZrpumd.js +2 -0
- package/packages/web/dist/assets/{MarkdownRenderer-DLN6sOj3.js → MarkdownRenderer-DmYpvXqr.js} +2 -2
- package/packages/web/dist/assets/{ProjectRedirect-L9_pdgQK.js → ProjectRedirect-CWAQx9Uc.js} +1 -1
- package/packages/web/dist/assets/ProjectView-JuOCBGKC.js +13 -0
- package/packages/web/dist/assets/ProjectsGrid-BihtN69c.js +2 -0
- package/packages/web/dist/assets/RootHome-Bw3ZuHAb.js +2 -0
- package/packages/web/dist/assets/{Surfaces-DMgKUcAq.js → Surfaces-iO74d5OF.js} +1 -1
- package/packages/web/dist/assets/{arc-CvfGJ7BR.js → arc-DTuVdQA0.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-B3VycBLn.js → architectureDiagram-3BPJPVTR-O9NSGqRe.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-CQA6m2h5.js → blockDiagram-GPEHLZMM-CDvwKA_y.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-A4xRu0rw.js → c4Diagram-AAUBKEIU-pNQuZhoW.js} +1 -1
- package/packages/web/dist/assets/channel-DF_bH_eP.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-D6GNeEqF.js → chunk-2J33WTMH-C11djcYQ.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-uRF_4e-n.js → chunk-4BX2VUAB-Dw2AiJPG.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-CPe6LZ2z.js → chunk-55IACEB6-DNkrrGDk.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-D1aJrcJM.js → chunk-727SXJPM-Cfe5G8_L.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-CNTind3K.js → chunk-AQP2D5EJ-CygyZWVK.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-BN13fkKv.js → chunk-FMBD7UC4-DCqy7Cyl.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-5brLkGsu.js → chunk-ND2GUHAM-DY-Tt8Fl.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-DsBFoxGC.js → chunk-QZHKN3VN-Vfq6pHq6.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CUAj9ybC.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CUAj9ybC.js +2 -0
- package/packages/web/dist/assets/{core-B_2f7agy.js → core-EsYWo_BG.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-DHK0CBMf.js → cose-bilkent-S5V4N54A-Dp85AcO4.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-CPfCIbbW.js → dagre-BM42HDAG-CRckd_YA.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-CYiqpVRZ.js → diagram-2AECGRRQ-JoJSPnwR.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-omK-yMpB.js → diagram-5GNKFQAL-Bson74dI.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-rdAbHGGr.js → diagram-KO2AKTUF-BNliER8l.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-UxMDuTlp.js → diagram-LMA3HP47-CGoQ3_PH.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-CFgssa9a.js → diagram-OG6HWLK6-DCdEm5k7.js} +1 -1
- package/packages/web/dist/assets/documentTitle-CXH5hpbS.js +2 -0
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-CpukfD6X.js → erDiagram-TEJ5UH35-BdasaK8s.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-Cw-kjJRb.js → flowDiagram-I6XJVG4X-B4JuMupx.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-DEysqPzv.js → ganttDiagram-6RSMTGT7-hJbdicJt.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-BMMJY8cj.js → gitGraphDiagram-PVQCEYII-pXZI1Zkh.js} +1 -1
- package/packages/web/dist/assets/index-4_u0mfSb.js +71 -0
- package/packages/web/dist/assets/index-Uc957im9.css +1 -0
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-0k3t7rhe.js → infoDiagram-5YYISTIA-BvJMTOeK.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-Dytq2hXT.js → ishikawaDiagram-YF4QCWOH-B77zOzN-.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-ckRGj3z7.js → journeyDiagram-JHISSGLW-V313L7-8.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-CkwFA-tN.js → kanban-definition-UN3LZRKU-zu-Sih2h.js} +1 -1
- package/packages/web/dist/assets/{linear-BcGvIzWi.js → linear-vmWftBq6.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-C3epT6Oz.js → mermaid.core-Da_mwYBC.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-DQh781it.js → mindmap-definition-RKZ34NQL-6sExeRgQ.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-3YruKSZq.js → pieDiagram-4H26LBE5-LjXeW1Vb.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-VEVqozsY.js → quadrantDiagram-W4KKPZXB-mm7l4NBx.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-DschbYSq.js → requirementDiagram-4Y6WPE33-BnEmeDZs.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-D2cjF2Ky.js → sankeyDiagram-5OEKKPKP-APXzF57J.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-DoxOLCts.js → sequenceDiagram-3UESZ5HK-DpjqNsed.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-LYtkahgw.js → stateDiagram-AJRCARHV-DhUpQhRh.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-D6jKC77W.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-BMnATX0_.js → timeline-definition-PNZ67QCA-wXCQTCZf.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-BxzXI3ZB.js → vennDiagram-CIIHVFJN-B6nxo-jj.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-5iJWKb5t.js → wardley-L42UT6IY-BeRZDa-K.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-ChgsGddr.js → wardleyDiagram-YWT4CUSO-BFBQl8LE.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-CDrM6f9L.js → xychartDiagram-2RQKCTM6-BwZGu1gQ.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-BWNFaeRY.js +0 -2
- package/packages/web/dist/assets/DiscoverView-85qMMRBH.js +0 -2
- package/packages/web/dist/assets/InstanceConfigPage-CuBTeXmA.js +0 -2
- package/packages/web/dist/assets/ProjectView-DytyYl6K.js +0 -13
- package/packages/web/dist/assets/ProjectsGrid-CjLJlqDF.js +0 -2
- package/packages/web/dist/assets/RootHome-Bo-ekc-k.js +0 -2
- package/packages/web/dist/assets/channel-DbxowUl8.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-aUuS6S9u.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-aUuS6S9u.js +0 -2
- package/packages/web/dist/assets/index-BBGMd6zo.js +0 -71
- package/packages/web/dist/assets/index-LM9Ga0Wn.css +0 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BH2W6rVN.js +0 -2
package/package.json
CHANGED
|
@@ -23,6 +23,7 @@ import { ensureClaudeHome, countLegacyTranscriptLinks, findPlantedChatsLinks, }
|
|
|
23
23
|
import { loadHostMcpSource } from "./claude-mcp.js";
|
|
24
24
|
import { loadHostPlugins } from "./claude-plugins.js";
|
|
25
25
|
import { declaredMcpNotices } from "./mcp-servers.js";
|
|
26
|
+
import { probeBrowserGpu } from "./browser-gpu.js";
|
|
26
27
|
import { installHerdctlLogBridge } from "./agent-errors.js";
|
|
27
28
|
import { ProjectStore, ROOT_KEY } from "./projects.js";
|
|
28
29
|
import { AttachmentStore } from "./attachments.js";
|
|
@@ -150,7 +151,13 @@ export async function buildApp(opts = {}) {
|
|
|
150
151
|
const hostPlugins = await loadHostPlugins(cfg);
|
|
151
152
|
for (const notice of hostPlugins.notices)
|
|
152
153
|
app.log[notice.level](notice.message);
|
|
153
|
-
|
|
154
|
+
// …and whether the browser MCP's Chromium can use a GPU (#964). Probed only
|
|
155
|
+
// when the browser is on; logged either way, because the fallback is silent —
|
|
156
|
+
// a missing device or package still renders, just on the CPU.
|
|
157
|
+
const browserGpu = cfg.browserMcp ? probeBrowserGpu() : undefined;
|
|
158
|
+
if (browserGpu)
|
|
159
|
+
app.log.info(browserGpu.reason);
|
|
160
|
+
const herdctl = new HerdctlService(cfg, { ...hostMcp.source, declared: cfg.mcpServers }, hostPlugins.source, browserGpu?.config);
|
|
154
161
|
const git = new GitService(cfg.projectsRoot, cfg.gitAuthor);
|
|
155
162
|
const githubAuth = new GithubAuth(path.join(cfg.dataDir, "github-auth.json"), cfg.githubClientId);
|
|
156
163
|
const archive = new ArchiveStore(cfg.dataDir);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** The DRM render node Mesa opens. The first GPU is always 128. */
|
|
2
|
+
export declare const BROWSER_GPU_DEVICE = "/dev/dri/renderD128";
|
|
3
|
+
/** The devbox image's `playwright-mcp --config` file carrying the ANGLE flags. */
|
|
4
|
+
export declare const BROWSER_GPU_CONFIG = "/etc/paddock/playwright-mcp-gpu.json";
|
|
5
|
+
export interface BrowserGpuProbe {
|
|
6
|
+
/** The `--config` path to hand `playwright-mcp`, or undefined for defaults. */
|
|
7
|
+
config?: string;
|
|
8
|
+
/** One line for the boot log saying which renderer agents will get, and why. */
|
|
9
|
+
reason: string;
|
|
10
|
+
}
|
|
11
|
+
/** The filesystem calls {@link probeBrowserGpu} makes — a seam for tests. */
|
|
12
|
+
export interface BrowserGpuFs {
|
|
13
|
+
openSync(path: string, flags: string): number;
|
|
14
|
+
closeSync(fd: number): void;
|
|
15
|
+
statSync(path: string): {
|
|
16
|
+
isFile(): boolean;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Decide whether the browser MCP should get hardware GL. Returns the config path
|
|
21
|
+
* only when BOTH hold:
|
|
22
|
+
*
|
|
23
|
+
* - the render node opens read-write. A real `open()`, not `access()` or
|
|
24
|
+
* `existsSync`: the server runs as root in the image, and root passes every
|
|
25
|
+
* permission-bit check, but a container's device allowlist can still refuse
|
|
26
|
+
* the open. Mesa does exactly this open, so this is the honest test;
|
|
27
|
+
* - the image's config file exists (and with it, the Mesa/EGL packages).
|
|
28
|
+
*
|
|
29
|
+
* Never throws. Any failure means "leave Playwright's defaults alone".
|
|
30
|
+
*/
|
|
31
|
+
export declare function probeBrowserGpu(device?: string, config?: string, io?: BrowserGpuFs): BrowserGpuProbe;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hardware GL for the browser MCP's Chromium (#964).
|
|
3
|
+
*
|
|
4
|
+
* Without help, the headless Chromium that `playwright-mcp` launches renders
|
|
5
|
+
* WebGL on SwiftShader — ANGLE's CPU rasterizer — even when a GPU is mapped into
|
|
6
|
+
* the container. Measured on an Intel UHD 630 that is 12.7× slower and 16× more
|
|
7
|
+
* CPU than the same GPU through Mesa; one WebGL page put a 12-core host at load
|
|
8
|
+
* 22. Chromium will not choose the GPU on its own: it needs
|
|
9
|
+
* `--use-gl=angle --use-angle=gl-egl` (NOT `--use-angle=gl`, which silently
|
|
10
|
+
* lands back on SwiftShader), plus system `libEGL.so.1` + Mesa, which the devbox
|
|
11
|
+
* image installs alongside {@link BROWSER_GPU_CONFIG}.
|
|
12
|
+
*
|
|
13
|
+
* `@playwright/mcp` has no CLI flag or env var for Chromium args — only a JSON
|
|
14
|
+
* `--config` file whose `browser.launchOptions.args` are appended after
|
|
15
|
+
* Playwright's own (so they win). That file is baked into the devbox image
|
|
16
|
+
* rather than written here, so its presence also proves the Mesa packages it
|
|
17
|
+
* depends on are installed: the two ship in the same image layer.
|
|
18
|
+
*
|
|
19
|
+
* The flags must be GATED, never unconditional. Measured with the flags set:
|
|
20
|
+
* - packages present, no device → on the built devbox image, NO WebGL at all:
|
|
21
|
+
* `getContext('webgl2')` and `getContext('webgl')` both return null. (A
|
|
22
|
+
* plainer Debian + Mesa image fell to llvmpipe instead: 51 fps on 6.35
|
|
23
|
+
* cores, more CPU than SwiftShader. Neither is acceptable.)
|
|
24
|
+
* - device present, no packages → silently SwiftShader, i.e. nothing gained.
|
|
25
|
+
* Most hosts have no GPU, so the default path has to stay exactly as it is.
|
|
26
|
+
*/
|
|
27
|
+
import fs from "node:fs";
|
|
28
|
+
/** The DRM render node Mesa opens. The first GPU is always 128. */
|
|
29
|
+
export const BROWSER_GPU_DEVICE = "/dev/dri/renderD128";
|
|
30
|
+
/** The devbox image's `playwright-mcp --config` file carrying the ANGLE flags. */
|
|
31
|
+
export const BROWSER_GPU_CONFIG = "/etc/paddock/playwright-mcp-gpu.json";
|
|
32
|
+
/**
|
|
33
|
+
* Decide whether the browser MCP should get hardware GL. Returns the config path
|
|
34
|
+
* only when BOTH hold:
|
|
35
|
+
*
|
|
36
|
+
* - the render node opens read-write. A real `open()`, not `access()` or
|
|
37
|
+
* `existsSync`: the server runs as root in the image, and root passes every
|
|
38
|
+
* permission-bit check, but a container's device allowlist can still refuse
|
|
39
|
+
* the open. Mesa does exactly this open, so this is the honest test;
|
|
40
|
+
* - the image's config file exists (and with it, the Mesa/EGL packages).
|
|
41
|
+
*
|
|
42
|
+
* Never throws. Any failure means "leave Playwright's defaults alone".
|
|
43
|
+
*/
|
|
44
|
+
export function probeBrowserGpu(device = BROWSER_GPU_DEVICE, config = BROWSER_GPU_CONFIG, io = fs) {
|
|
45
|
+
try {
|
|
46
|
+
io.closeSync(io.openSync(device, "r+"));
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
const code = err.code ?? String(err);
|
|
50
|
+
return { reason: `browser MCP: software WebGL (SwiftShader) — ${device} not openable (${code})` };
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
if (!io.statSync(config).isFile())
|
|
54
|
+
throw new Error("not a file");
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return {
|
|
58
|
+
reason: `browser MCP: software WebGL (SwiftShader) — ${device} is present but ${config} is not; ` +
|
|
59
|
+
`hardware GL needs the devbox image's Mesa/EGL packages`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return { config, reason: `browser MCP: hardware WebGL via ${device} (${config})` };
|
|
63
|
+
}
|
|
@@ -40,8 +40,13 @@ import { type PaddockTrigger } from "./trigger-config.js";
|
|
|
40
40
|
* resolved once at boot by `claude-plugins.ts` and gated by `claude.instructions`
|
|
41
41
|
* rather than `claude.mcpServers` — see that module for why. Keeper-only for the
|
|
42
42
|
* same reasons, and empty unless the lever is on.
|
|
43
|
+
*
|
|
44
|
+
* `browserGpuConfig` is the hardware-GL `--config` for the browser MCP (#964),
|
|
45
|
+
* probed once at boot by `browser-gpu.ts` — undefined unless this host has a
|
|
46
|
+
* usable GPU and the devbox image's Mesa packages. Ignored for a `docker: true`
|
|
47
|
+
* project (see {@link browserMcpServers}).
|
|
43
48
|
*/
|
|
44
|
-
export declare function buildAgentConfig(cfg: PaddockConfig, project: Project, modelOverride?: string, mcpSources?: McpSources, hostPlugins?: HostPluginSource): Record<string, unknown> & {
|
|
49
|
+
export declare function buildAgentConfig(cfg: PaddockConfig, project: Project, modelOverride?: string, mcpSources?: McpSources, hostPlugins?: HostPluginSource, browserGpuConfig?: string): Record<string, unknown> & {
|
|
45
50
|
name: string;
|
|
46
51
|
};
|
|
47
52
|
/**
|
|
@@ -102,8 +107,9 @@ export declare function buildSweeperConfig(cfg: PaddockConfig, project: Project)
|
|
|
102
107
|
* Runs in the project's WORKING dir (so a trigger's Bash/Write act on the same tree
|
|
103
108
|
* the keeper does). A trigger that declares no tools gets `allowed_tools: []` — which
|
|
104
109
|
* herdctl does not enforce as a deny-all; see {@link triggerToAgentToolConfig} (#647).
|
|
110
|
+
* `browserGpuConfig` is as for {@link buildAgentConfig}.
|
|
105
111
|
*/
|
|
106
|
-
export declare function buildTriggerConfig(cfg: PaddockConfig, project: Project, triggerName: string, trigger: PaddockTrigger): Record<string, unknown> & {
|
|
112
|
+
export declare function buildTriggerConfig(cfg: PaddockConfig, project: Project, triggerName: string, trigger: PaddockTrigger, browserGpuConfig?: string): Record<string, unknown> & {
|
|
107
113
|
name: string;
|
|
108
114
|
};
|
|
109
115
|
/**
|
|
@@ -55,8 +55,13 @@ import { keeperAgentName, sweeperAgentName, triggerAgentName, browserMcpServers,
|
|
|
55
55
|
* resolved once at boot by `claude-plugins.ts` and gated by `claude.instructions`
|
|
56
56
|
* rather than `claude.mcpServers` — see that module for why. Keeper-only for the
|
|
57
57
|
* same reasons, and empty unless the lever is on.
|
|
58
|
+
*
|
|
59
|
+
* `browserGpuConfig` is the hardware-GL `--config` for the browser MCP (#964),
|
|
60
|
+
* probed once at boot by `browser-gpu.ts` — undefined unless this host has a
|
|
61
|
+
* usable GPU and the devbox image's Mesa packages. Ignored for a `docker: true`
|
|
62
|
+
* project (see {@link browserMcpServers}).
|
|
58
63
|
*/
|
|
59
|
-
export function buildAgentConfig(cfg, project, modelOverride, mcpSources = EMPTY_MCP_SOURCES, hostPlugins = EMPTY_HOST_PLUGINS) {
|
|
64
|
+
export function buildAgentConfig(cfg, project, modelOverride, mcpSources = EMPTY_MCP_SOURCES, hostPlugins = EMPTY_HOST_PLUGINS, browserGpuConfig) {
|
|
60
65
|
const config = {
|
|
61
66
|
name: keeperAgentName(project.slug),
|
|
62
67
|
description: project.summary || `Claude Code agent for project ${project.name}.`,
|
|
@@ -133,7 +138,7 @@ export function buildAgentConfig(cfg, project, modelOverride, mcpSources = EMPTY
|
|
|
133
138
|
// unprivileged LXC) and someone else's `playwright` would almost certainly not
|
|
134
139
|
// start here. A declared collision is warned about by name at boot
|
|
135
140
|
// (`declaredMcpNotices`) so it is not silent.
|
|
136
|
-
const browser = browserMcpServers(cfg.browserMcp);
|
|
141
|
+
const browser = browserMcpServers(cfg.browserMcp, project.docker ? undefined : browserGpuConfig);
|
|
137
142
|
const external = mcpServersFor(mcpSources, project.workingDir);
|
|
138
143
|
const servers = { ...external, ...(browser ?? {}) };
|
|
139
144
|
if (Object.keys(servers).length > 0)
|
|
@@ -296,8 +301,9 @@ export function buildSweeperConfig(cfg, project) {
|
|
|
296
301
|
* Runs in the project's WORKING dir (so a trigger's Bash/Write act on the same tree
|
|
297
302
|
* the keeper does). A trigger that declares no tools gets `allowed_tools: []` — which
|
|
298
303
|
* herdctl does not enforce as a deny-all; see {@link triggerToAgentToolConfig} (#647).
|
|
304
|
+
* `browserGpuConfig` is as for {@link buildAgentConfig}.
|
|
299
305
|
*/
|
|
300
|
-
export function buildTriggerConfig(cfg, project, triggerName, trigger) {
|
|
306
|
+
export function buildTriggerConfig(cfg, project, triggerName, trigger, browserGpuConfig) {
|
|
301
307
|
const config = {
|
|
302
308
|
name: triggerAgentName(project.slug, triggerName),
|
|
303
309
|
description: `Trigger "${triggerName}" (${trigger.trigger.type}) for project ${project.name}.`,
|
|
@@ -317,7 +323,7 @@ export function buildTriggerConfig(cfg, project, triggerName, trigger) {
|
|
|
317
323
|
};
|
|
318
324
|
if (project.docker)
|
|
319
325
|
config.docker = { enabled: true };
|
|
320
|
-
const browser = browserMcpServers(cfg.browserMcp);
|
|
326
|
+
const browser = browserMcpServers(cfg.browserMcp, project.docker ? undefined : browserGpuConfig);
|
|
321
327
|
if (browser)
|
|
322
328
|
config.mcp_servers = browser;
|
|
323
329
|
return config;
|
|
@@ -162,8 +162,15 @@ export declare const DENIED_TOOLS: readonly string[];
|
|
|
162
162
|
* flag the server tries to `playwright install chrome` at first use and stalls.
|
|
163
163
|
* The role installs the open-source `chromium` engine, so we select it here.
|
|
164
164
|
* The tool-less sweeper deliberately never receives this server.
|
|
165
|
+
*
|
|
166
|
+
* `gpuConfig` is the `--config` file that switches Chromium to hardware GL
|
|
167
|
+
* (#964), as found by `probeBrowserGpu` at boot — undefined on any host without
|
|
168
|
+
* a usable GPU, which keeps the args byte-identical to before. Callers pass it
|
|
169
|
+
* only for agents that run on THIS host: a `docker: true` project's browser runs
|
|
170
|
+
* in another container, where neither the device nor the file is known to exist,
|
|
171
|
+
* and a `--config` naming a missing file stops the MCP server starting at all.
|
|
165
172
|
*/
|
|
166
|
-
export declare function browserMcpServers(enabled: boolean): Record<string, unknown> | undefined;
|
|
173
|
+
export declare function browserMcpServers(enabled: boolean, gpuConfig?: string): Record<string, unknown> | undefined;
|
|
167
174
|
/**
|
|
168
175
|
* How many chat turns a project's keeper may run at once. herdctl defaults an
|
|
169
176
|
* agent to `max_concurrent: 1`, which would serialize a project's chats and make
|
|
@@ -229,16 +229,21 @@ export const DENIED_TOOLS = [
|
|
|
229
229
|
* flag the server tries to `playwright install chrome` at first use and stalls.
|
|
230
230
|
* The role installs the open-source `chromium` engine, so we select it here.
|
|
231
231
|
* The tool-less sweeper deliberately never receives this server.
|
|
232
|
+
*
|
|
233
|
+
* `gpuConfig` is the `--config` file that switches Chromium to hardware GL
|
|
234
|
+
* (#964), as found by `probeBrowserGpu` at boot — undefined on any host without
|
|
235
|
+
* a usable GPU, which keeps the args byte-identical to before. Callers pass it
|
|
236
|
+
* only for agents that run on THIS host: a `docker: true` project's browser runs
|
|
237
|
+
* in another container, where neither the device nor the file is known to exist,
|
|
238
|
+
* and a `--config` naming a missing file stops the MCP server starting at all.
|
|
232
239
|
*/
|
|
233
|
-
export function browserMcpServers(enabled) {
|
|
240
|
+
export function browserMcpServers(enabled, gpuConfig) {
|
|
234
241
|
if (!enabled)
|
|
235
242
|
return undefined;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
},
|
|
241
|
-
};
|
|
243
|
+
const args = ["--headless", "--no-sandbox", "--isolated", "--browser", "chromium"];
|
|
244
|
+
if (gpuConfig)
|
|
245
|
+
args.push("--config", gpuConfig);
|
|
246
|
+
return { playwright: { command: "playwright-mcp", args } };
|
|
242
247
|
}
|
|
243
248
|
/**
|
|
244
249
|
* How many chat turns a project's keeper may run at once. herdctl defaults an
|
|
@@ -170,6 +170,7 @@ export declare class HerdctlService {
|
|
|
170
170
|
private readonly cfg;
|
|
171
171
|
private readonly mcpSources;
|
|
172
172
|
private readonly hostPlugins;
|
|
173
|
+
private readonly browserGpuConfig?;
|
|
173
174
|
private fleet;
|
|
174
175
|
private started;
|
|
175
176
|
/**
|
|
@@ -262,8 +263,12 @@ export declare class HerdctlService {
|
|
|
262
263
|
* `hostPlugins` is the host's Claude Code plugins (#700), read at boot by
|
|
263
264
|
* `loadHostPlugins` under `claude.instructions: host`, and handed in for
|
|
264
265
|
* exactly the same reasons.
|
|
266
|
+
*
|
|
267
|
+
* `browserGpuConfig` is the browser MCP's hardware-GL `--config` (#964), from
|
|
268
|
+
* `probeBrowserGpu` in `buildApp` — a device probe, logged at boot, so it is
|
|
269
|
+
* handed in on the same terms. Undefined (the default) means software GL.
|
|
265
270
|
*/
|
|
266
|
-
constructor(cfg: PaddockConfig, mcpSources?: McpSources, hostPlugins?: HostPluginSource);
|
|
271
|
+
constructor(cfg: PaddockConfig, mcpSources?: McpSources, hostPlugins?: HostPluginSource, browserGpuConfig?: string | undefined);
|
|
267
272
|
/**
|
|
268
273
|
* The Claude home every transcript symlink is planted in, plus where those
|
|
269
274
|
* symlinks point (#620, #691). One accessor, so no call site can quietly take
|
|
@@ -233,6 +233,7 @@ export class HerdctlService {
|
|
|
233
233
|
cfg;
|
|
234
234
|
mcpSources;
|
|
235
235
|
hostPlugins;
|
|
236
|
+
browserGpuConfig;
|
|
236
237
|
fleet = null;
|
|
237
238
|
started = false;
|
|
238
239
|
/**
|
|
@@ -327,11 +328,16 @@ export class HerdctlService {
|
|
|
327
328
|
* `hostPlugins` is the host's Claude Code plugins (#700), read at boot by
|
|
328
329
|
* `loadHostPlugins` under `claude.instructions: host`, and handed in for
|
|
329
330
|
* exactly the same reasons.
|
|
331
|
+
*
|
|
332
|
+
* `browserGpuConfig` is the browser MCP's hardware-GL `--config` (#964), from
|
|
333
|
+
* `probeBrowserGpu` in `buildApp` — a device probe, logged at boot, so it is
|
|
334
|
+
* handed in on the same terms. Undefined (the default) means software GL.
|
|
330
335
|
*/
|
|
331
|
-
constructor(cfg, mcpSources = EMPTY_MCP_SOURCES, hostPlugins = EMPTY_HOST_PLUGINS) {
|
|
336
|
+
constructor(cfg, mcpSources = EMPTY_MCP_SOURCES, hostPlugins = EMPTY_HOST_PLUGINS, browserGpuConfig) {
|
|
332
337
|
this.cfg = cfg;
|
|
333
338
|
this.mcpSources = mcpSources;
|
|
334
339
|
this.hostPlugins = hostPlugins;
|
|
340
|
+
this.browserGpuConfig = browserGpuConfig;
|
|
335
341
|
}
|
|
336
342
|
/**
|
|
337
343
|
* The Claude home every transcript symlink is planted in, plus where those
|
|
@@ -1858,7 +1864,7 @@ export class HerdctlService {
|
|
|
1858
1864
|
// existing unit-test seams (which reach `sweeperAgentConfig`/`ensureConfigFile`
|
|
1859
1865
|
// via a cast) and internal callers keep working.
|
|
1860
1866
|
keeperAgentConfig(project, modelOverride) {
|
|
1861
|
-
return buildAgentConfig(this.cfg, project, modelOverride, this.mcpSources, this.hostPlugins);
|
|
1867
|
+
return buildAgentConfig(this.cfg, project, modelOverride, this.mcpSources, this.hostPlugins, this.browserGpuConfig);
|
|
1862
1868
|
}
|
|
1863
1869
|
sweeperAgentConfig(project) {
|
|
1864
1870
|
return buildSweeperConfig(this.cfg, project);
|
|
@@ -1888,7 +1894,7 @@ export class HerdctlService {
|
|
|
1888
1894
|
this.unplantedChatsLinks.push(unplanted);
|
|
1889
1895
|
}
|
|
1890
1896
|
triggerAgentConfig(project, triggerName, trigger) {
|
|
1891
|
-
return buildTriggerConfig(this.cfg, project, triggerName, trigger);
|
|
1897
|
+
return buildTriggerConfig(this.cfg, project, triggerName, trigger, this.browserGpuConfig);
|
|
1892
1898
|
}
|
|
1893
1899
|
async ensureConfigFile() {
|
|
1894
1900
|
await writeBootConfigFile(this.cfg);
|
|
@@ -276,8 +276,8 @@ export const FIELDS = [
|
|
|
276
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 },
|
|
277
277
|
// Branding (issue #34).
|
|
278
278
|
{ key: "brand.name", group: "branding", label: "Name", type: "string", envVars: ["PADDOCK_BRAND_NAME"], default: "Paddock", editable: true, coerce: nonEmptyString },
|
|
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 },
|
|
280
|
-
{ key: "brand.accent", group: "branding", label: "Accent color", type: "string", envVars: ["PADDOCK_BRAND_ACCENT"], default: "#c2603c", editable: true, coerce: hexColor },
|
|
279
|
+
{ key: "brand.logo", group: "branding", label: "Logo", help: "An emoji/glyph, or a URL/path to an image. With the accent colour, it draws this instance's browser-tab icon — what tells its tabs apart from other instances'.", type: "string", envVars: ["PADDOCK_BRAND_LOGO"], default: "🐎", editable: true, coerce: nonEmptyString },
|
|
280
|
+
{ key: "brand.accent", group: "branding", label: "Accent color", help: "Left at the default, a renamed instance's tab icon and logo chip get a colour derived from its name.", type: "string", envVars: ["PADDOCK_BRAND_ACCENT"], default: "#c2603c", editable: true, coerce: hexColor },
|
|
281
281
|
// Transcription (voice dictation). endpoint is semi-sensitive; apiKey is a
|
|
282
282
|
// secret and deliberately NOT surfaced here.
|
|
283
283
|
{ key: "transcription.mode", group: "transcription", label: "Mode", type: "enum", enumValues: ["off", "local", "remote"], envVars: ["PADDOCK_WHISPER_MODE"], default: "off", editable: true, coerce: oneOf(["off", "local", "remote"]) },
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import type { FastifyInstance } from "fastify";
|
|
2
2
|
import type { RouteCtx } from "../route-context.js";
|
|
3
|
+
/**
|
|
4
|
+
* How many rows the attention feed's `recent` list carries. Home renders a
|
|
5
|
+
* screenful and offers the rest behind "Show more"; past this the list is the
|
|
6
|
+
* sidebar's job, not the front door's.
|
|
7
|
+
*/
|
|
8
|
+
export declare const RECENT_LIMIT = 50;
|
|
9
|
+
/** The two timestamps a chat DTO carries that say something happened in it. */
|
|
10
|
+
interface ChatTimes {
|
|
11
|
+
updatedAt?: string;
|
|
12
|
+
lastTurnCompletedAt?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* When a chat last DID something — the later of its last message and its last
|
|
16
|
+
* completed turn, as epoch ms (0 when neither parses). Exported for the tests.
|
|
17
|
+
*/
|
|
18
|
+
export declare function activityAt(c: ChatTimes): number;
|
|
3
19
|
/**
|
|
4
20
|
* Workspace-scoped chat routes: paths are declared RELATIVE to the workspace
|
|
5
21
|
* (e.g. `/chats/:sessionId`), and this plugin is mounted TWICE — once at
|
|
@@ -10,3 +26,4 @@ import type { RouteCtx } from "../route-context.js";
|
|
|
10
26
|
* ["slug"]` schemas below keep validating unchanged on both mounts.
|
|
11
27
|
*/
|
|
12
28
|
export declare function registerChatWorkspaceRoutes(app: FastifyInstance, ctx: RouteCtx): void;
|
|
29
|
+
export {};
|
|
@@ -26,6 +26,21 @@ import { sendProjectError } from "../route-errors.js";
|
|
|
26
26
|
import { mapWithConcurrency, FILE_FANOUT_CONCURRENCY } from "../concurrency.js";
|
|
27
27
|
import { CHAT_USAGE_SCOPES, SAFE_SESSION_ID, RUNS_SEEN_SESSION, clampRunsLimit, toChatUsage, buildProjectChats, makeTriggerResolver, makeParentResolver, normalizeBatchSessionIds, BATCH_SESSIONS_MAX, } from "../chat-dto.js";
|
|
28
28
|
import { quiesceSession, turnRunningError } from "../turn-interlock.js";
|
|
29
|
+
/**
|
|
30
|
+
* How many rows the attention feed's `recent` list carries. Home renders a
|
|
31
|
+
* screenful and offers the rest behind "Show more"; past this the list is the
|
|
32
|
+
* sidebar's job, not the front door's.
|
|
33
|
+
*/
|
|
34
|
+
export const RECENT_LIMIT = 50;
|
|
35
|
+
/**
|
|
36
|
+
* When a chat last DID something — the later of its last message and its last
|
|
37
|
+
* completed turn, as epoch ms (0 when neither parses). Exported for the tests.
|
|
38
|
+
*/
|
|
39
|
+
export function activityAt(c) {
|
|
40
|
+
const a = c.updatedAt ? Date.parse(c.updatedAt) : NaN;
|
|
41
|
+
const b = c.lastTurnCompletedAt ? Date.parse(c.lastTurnCompletedAt) : NaN;
|
|
42
|
+
return Math.max(Number.isFinite(a) ? a : 0, Number.isFinite(b) ? b : 0);
|
|
43
|
+
}
|
|
29
44
|
/**
|
|
30
45
|
* Workspace-scoped chat routes: paths are declared RELATIVE to the workspace
|
|
31
46
|
* (e.g. `/chats/:sessionId`), and this plugin is mounted TWICE — once at
|
|
@@ -150,8 +165,8 @@ export function registerChatWorkspaceRoutes(app, ctx) {
|
|
|
150
165
|
app.get("/chats/attention", {
|
|
151
166
|
schema: {
|
|
152
167
|
tags: ["Chats"],
|
|
153
|
-
summary: "List the chats in this workspace's subtree that are running or
|
|
154
|
-
description: "Returns `{ running, unread }` — the chats in this workspace AND its descendants that currently have a live turn (`running`)
|
|
168
|
+
summary: "List the chats in this workspace's subtree that are running, unread, or recently active",
|
|
169
|
+
description: "Returns `{ running, unread, recent }` — the chats in this workspace AND its descendants that currently have a live turn (`running`), hold an unread reply (`unread`), or are simply the most recently active (`recent`: every non-running, non-archived chat, read or not, newest first, capped at 50; a superset of the newest `unread` rows). All three lists are ordered newest activity first (the later of `updatedAt` and `lastTurnCompletedAt`). Each row is a chat DTO plus the `projectSlug`/`projectName` it belongs to, so a fleet-wide list stays attributable. On the root mount (`/api/root/chats/attention`) the subtree is the whole instance, because the root workspace's key (`\"\"`) prefixes every workspace key; on a project mount it is that project alone. A chat is never in both lists: a live turn hasn't landed a reply yet, so running wins. `running` rows additionally carry resolved `contextTokens`/`contextLimit` so a caller can draw a context gauge for live work; `unread` rows do not, because resolving usage streams a transcript per chat and only the running set is bounded by how many turns can be in flight at once.",
|
|
155
170
|
params: {
|
|
156
171
|
type: "object",
|
|
157
172
|
properties: {
|
|
@@ -161,7 +176,7 @@ export function registerChatWorkspaceRoutes(app, ctx) {
|
|
|
161
176
|
},
|
|
162
177
|
response: {
|
|
163
178
|
200: {
|
|
164
|
-
description: "Object `{ running, unread }`, each an array of chat DTOs enriched with `projectSlug` and `projectName`.",
|
|
179
|
+
description: "Object `{ running, unread, recent }`, each an array of chat DTOs enriched with `projectSlug` and `projectName`.",
|
|
165
180
|
type: "object",
|
|
166
181
|
additionalProperties: true,
|
|
167
182
|
},
|
|
@@ -186,6 +201,10 @@ export function registerChatWorkspaceRoutes(app, ctx) {
|
|
|
186
201
|
const lastTurnAt = await herdctl.lastTurnCompletedAt().catch(() => new Map());
|
|
187
202
|
const running = [];
|
|
188
203
|
const unreadRows = [];
|
|
204
|
+
// Every non-running, non-archived chat, read or not — the "Recent" half
|
|
205
|
+
// of Home's Running & Recent feed. Capped after sorting, so it is the
|
|
206
|
+
// newest RECENT_LIMIT across the whole subtree, not per workspace.
|
|
207
|
+
const recentRows = [];
|
|
189
208
|
for (const p of subtree) {
|
|
190
209
|
const sessions = await herdctl.listSessions(p).catch(() => []);
|
|
191
210
|
if (sessions.length === 0)
|
|
@@ -226,9 +245,18 @@ export function registerChatWorkspaceRoutes(app, ctx) {
|
|
|
226
245
|
const seen = c.lastSeen ?? 0;
|
|
227
246
|
if (c.unread || (Number.isFinite(completed) && completed > seen))
|
|
228
247
|
unreadRows.push(row);
|
|
248
|
+
recentRows.push(row);
|
|
229
249
|
}
|
|
230
250
|
}
|
|
231
|
-
|
|
251
|
+
// Newest activity first, in every list. These used to come back in
|
|
252
|
+
// workspace-then-transcript order, which on the root put a project's
|
|
253
|
+
// weeks-old unread above a reply that landed a minute ago — an order
|
|
254
|
+
// the reader could not see any reason for.
|
|
255
|
+
const byActivity = (a, b) => activityAt(b) - activityAt(a);
|
|
256
|
+
running.sort(byActivity);
|
|
257
|
+
unreadRows.sort(byActivity);
|
|
258
|
+
recentRows.sort(byActivity);
|
|
259
|
+
return { running, unread: unreadRows, recent: recentRows.slice(0, RECENT_LIMIT) };
|
|
232
260
|
}
|
|
233
261
|
catch (err) {
|
|
234
262
|
return sendProjectError(reply, err);
|
package/packages/web/dist/assets/{DeleteProjectDialog-BVPw-9au.js → DeleteProjectDialog-Bt_5z1b7.js}
RENAMED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{r as l,
|
|
1
|
+
import{r as l,e as k,j as s,A as y,d as p,a as w}from"./index-4_u0mfSb.js";import{c as N}from"./lastTab-CU1zsF0j.js";function v({open:e,title:t,message:c,confirmLabel:n="Delete",cancelLabel:o="Cancel",danger:a=!0,wide:h=!1,dismissOnBackdrop:b=!0,onConfirm:g,onClose:d}){const[r,u]=l.useState(!1),[f,m]=l.useState(null);if(l.useEffect(()=>{e&&m(null)},[e]),k(e&&!r,d),!e)return null;const j=async()=>{u(!0),m(null);try{await g()}catch(i){m(i instanceof Error?i.message:"Action failed"),u(!1)}};return s.jsx("div",{className:"fixed inset-0 z-[60] flex items-center justify-center bg-overlay p-4 backdrop-blur-sm",onClick:()=>b&&!r&&d(),children:s.jsxs("div",{className:`w-full ${h?"max-w-md":"max-w-sm"} animate-scale-in rounded-2xl border border-edge bg-surface-raised p-6 shadow-2xl`,onClick:i=>i.stopPropagation(),role:"alertdialog","aria-modal":"true",children:[s.jsxs("div",{className:"flex items-start gap-3",children:[a&&s.jsx("span",{className:"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-danger-soft text-danger",children:s.jsx(y,{width:18,height:18})}),s.jsxs("div",{className:"min-w-0",children:[s.jsx("h2",{className:"text-base font-semibold",children:t}),s.jsx("div",{className:"mt-1.5 text-sm text-fg-muted",children:c})]})]}),f&&s.jsx("p",{className:"mt-4 rounded-lg bg-danger-soft px-3 py-2 text-sm text-danger",children:f}),s.jsxs("div",{className:"mt-5 flex justify-end gap-2",children:[s.jsx("button",{type:"button",className:"btn-ghost",onClick:d,disabled:r,children:o}),s.jsx("button",{type:"button",className:a?"btn btn-danger":"btn-primary",onClick:j,disabled:r,children:r?"Working…":n})]})]})})}function P(e,t){return e===t?!0:e.startsWith(t.endsWith("/")?t:`${t}/`)}function x(e){return!P(e.workingDir,e.dir)}function C({project:e}){const t=s.jsx("span",{className:"font-medium text-fg",children:e.name});return x(e)?s.jsxs(s.Fragment,{children:[t," will be unlinked. Paddock’s notes, settings and chats for it are permanently removed, and this cannot be undone — but the directory it works in is left alone:",s.jsx("span",{className:"mt-1.5 block break-all font-mono text-xs text-fg",children:e.workingDir}),s.jsx("span",{className:"mt-1.5 block",children:"Nothing in there is deleted."})]}):s.jsxs(s.Fragment,{children:[t," and all its chats and files will be permanently removed. This cannot be undone."]})}function D(){const{remove:e}=p();return l.useCallback(async t=>{await w.deleteProject(t),e(t),N(t)},[e])}function W({project:e,open:t,onClose:c,onDeleted:n}){const o=D(),a=x(e);return s.jsx(v,{open:t,title:"Delete project?",message:s.jsx(C,{project:e}),confirmLabel:"Delete project",wide:a,dismissOnBackdrop:!a,onConfirm:async()=>{await o(e.slug),n==null||n()},onClose:c})}export{v as C,W as D,x as d};
|
|
2
2
|
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{h as oe,d as ie,a0 as de,r as u,a as A,j as s,V as W,A as X,a4 as Z,I as $,a1 as ce,Q as le,i as he,a5 as ue,a6 as J,J as T,c as K,a7 as pe,t as xe,Y as Q}from"./index-4_u0mfSb.js";import{E as R,C as me}from"./Surfaces-iO74d5OF.js";function ge(e){const t={};for(const n of e)t[n.path]={checked:!0,expanded:!1,loading:!1,error:null};return t}function fe(e){if(!e.sessions||!e.selected)return e.checked?"on":"off";const t=e.sessions.sessions.length,n=e.selected.size;return t===0||n===0?"off":n===t?"on":"mixed"}function C(e,t){return e.sessions&&e.selected?e.selected.size:e.checked?t.sessionCount:0}function je(e,t){return t.reduce((n,r)=>n+C(e[r.path]??O(),r),0)}function be(e,t){return t.filter(n=>C(e[n.path]??O(),n)>0)}function we(e,t,n){const r=e[t];if(!r)return e;const a={...r,checked:n};return r.sessions&&(a.selected=n?new Set(r.sessions.sessions.map(c=>c.sessionId)):new Set),{...e,[t]:a}}function ye(e,t,n){const r=e[t];if(!r||!r.sessions)return e;const a=new Set(r.selected??[]);return a.delete(n)||a.add(n),{...e,[t]:{...r,selected:a,checked:a.size>0}}}function ve(e,t,n){const r=e[t];if(!r)return e;const a=r.checked?new Set(n.sessions.map(c=>c.sessionId)):new Set;return{...e,[t]:{...r,sessions:n,selected:a,loading:!1,error:null}}}function D(e,t,n){const r=e[t];return r?{...e,[t]:{...r,...n}}:e}function Ne(e,t){const n=e??O();return C(n,t)===0?null:n.sessions&&n.selected?{sessionIds:[...n.selected]}:{}}function O(){return{checked:!1,expanded:!1,loading:!1,error:null}}function ke(e){return e.error!==void 0?e.stage==="create"?"danger":"warn":e.adopted>0?"success":"warn"}async function Se(e,t,n){let r;try{r=await e.createProject({name:t.name,slug:t.suggestedSlug,path:t.path,managed:!1})}catch(a){return{stage:"create",adopted:0,skipped:[],error:_(a)}}try{const a=await e.adoptChats(r.slug,{sourceCwd:r.workingDir,...n.sessionIds?{sessionIds:n.sessionIds}:{}});return{project:r,adopted:a.adopted.length,skipped:a.skipped}}catch(a){return{stage:"import",project:r,adopted:0,skipped:[],error:_(a)}}}async function $e(e,t,n){const r=[];for(const a of t){n(a.candidate.path,"running");const c=await Se(e,a.candidate,a.plan);r.push(c),n(a.candidate.path,"done",c)}return r}const Ce={sidechain:"sub-agent transcript","already-adopted":"already adopted","destination-exists":"a chat with that id already exists here","attributed-to-run":"belongs to a Paddock run",unreadable:"transcript unreadable","placement-failed":"copy failed","record-failed":"could not be recorded"};function Y(e){if(e.length===0)return"";const t=[...new Set(e.map(n=>Ce[n.reason]??n.reason))];return`${e.length} skipped (${t.join(", ")})`}function Ie(e,t){var l;if(e.stage==="create")return`Couldn't create the project: ${e.error}`;const n=((l=e.project)==null?void 0:l.slug)??t.suggestedSlug;if(e.stage==="import")return`Project “${n}” was created, but importing its chats failed: ${e.error}. It is there and empty — you can import them from the project itself.`;if(e.adopted===0){const m=Y(e.skipped);return t.recordedPath!==void 0?`Project “${n}” was created but no chats came with it. Its transcripts record ${t.recordedPath}, a different spelling of ${t.path}, and an import matches on the exact path.`:m?`Project “${n}” was created but no chats came with it — ${m}.`:`Project “${n}” was created but no chats came with it.`}const r=e.adopted,a=Y(e.skipped),c=`Adopted ${r} chat${r===1?"":"s"} into “${n}”`;return a?`${c} — ${a}.`:`${c}.`}function _(e){return e instanceof Error?e.message:String(e)}function Ge({firstRun:e=!1,embedded:t=!1,onLeave:n,onStartChat:r}){const a=oe(),{refresh:c}=ie(),l=de(),[m,d]=u.useState(!1),[j,I]=u.useState(!1),[h,x]=u.useState(null),[y,z]=u.useState(null),[F,P]=u.useState(!0),[f,v]=u.useState({}),[k,G]=u.useState(!1),[L,H]=u.useState({}),[E,B]=u.useState({}),[S,ee]=u.useState(!1);u.useEffect(()=>{let o=!0;return P(!0),z(null),A.discover({includeNonGit:m,includeOutsideHome:j}).then(i=>{o&&(x(i),v(ge(i.candidates)),P(!1))}).catch(i=>{o&&(z(i instanceof Error?i.message:"Discovery failed"),P(!1))}),()=>{o=!1}},[m,j]);const w=(h==null?void 0:h.candidates)??[],M=u.useMemo(()=>je(f,w),[f,w]),b=u.useMemo(()=>be(f,w),[f,w]),se=u.useCallback(o=>{v(p=>{const g=p[o.path];if(!g)return p;const N=D(p,o.path,{expanded:!g.expanded});return g.expanded||g.sessions||g.loading?N:D(N,o.path,{loading:!0,error:null})});const i=f[o.path];!i||i.expanded||i.sessions||i.loading||A.discoverSessions(o.path).then(p=>v(g=>ve(g,o.path,p))).catch(p=>v(g=>D(g,o.path,{loading:!1,error:p instanceof Error?p.message:"Could not read this directory's sessions"})))},[f]),te=u.useCallback(async()=>{if(k)return;G(!0),H(Object.fromEntries(b.map(i=>[i.path,"pending"]))),B({});const o=b.map(i=>({candidate:i,plan:Ne(f[i.path],i)??{}}));await $e(A,o,(i,p,g)=>{H(N=>({...N,[i]:p})),g&&B(N=>({...N,[i]:g}))}),G(!1),ee(!0),await c()},[b,f,k,c]),ne=u.useCallback(async()=>{await c(),n==null||n(),a("/")},[a,c,n]),ae=Object.values(E).reduce((o,i)=>o+i.adopted,0),re=Object.values(E).filter(o=>o.project).length,V=s.jsxs(s.Fragment,{children:[s.jsx("p",{className:"mb-5 text-sm text-fg-muted",children:e?"If you have used Claude Code in a terminal on this machine, Paddock can adopt that history: each directory becomes a project with its conversations already in it.":"Directories on this machine with existing Claude Code history. Adopting one links it as a project and brings its conversations in; your own history is never moved or deleted."}),S&&s.jsx(Te,{imported:ae,projects:re,onGetStarted:ne}),y!==null&&s.jsx(W,{tone:"danger",icon:s.jsx(X,{width:16,height:16}),children:y}),F&&s.jsx(De,{}),!F&&y===null&&h!==null&&s.jsxs(s.Fragment,{children:[h.scanned===0?s.jsx(Ae,{claudeHome:h.claudeHome,onStartChat:r}):w.length===0?s.jsx(R,{variant:"panel",icon:s.jsx(Z,{width:22,height:22}),title:"Nothing to adopt",body:"Paddock found Claude Code history on this machine, but none of it is available to adopt right now. The tally below says what was skipped, and the two toggles relax the rules that are safe to relax."}):s.jsx("ul",{className:"space-y-2",children:w.map(o=>s.jsx(Pe,{candidate:o,row:f[o.path],status:L[o.path],outcome:E[o.path],locked:k||S,onToggleRow:i=>v(p=>we(p,o.path,i)),onToggleSession:i=>v(p=>ye(p,o.path,i)),onToggleExpanded:()=>se(o)},o.path))}),!S&&h.scanned>0&&s.jsx(Ee,{result:h,includeNonGit:m,includeOutsideHome:j,disabled:k,onIncludeNonGit:d,onIncludeOutsideHome:I}),w.length>0&&!S&&s.jsxs("div",{className:"mt-5 flex items-center justify-between gap-3 border-t border-edge pt-4",children:[s.jsxs("span",{className:"text-sm text-fg-muted",children:[M," conversation",M===1?"":"s"," from ",b.length," director",b.length===1?"y":"ies"]}),s.jsxs($,{variant:"primary",onClick:()=>void te(),disabled:b.length===0,loading:k,loadingLabel:"Adopting…",children:["Adopt ",b.length," project",b.length===1?"":"s"]})]})]})]});return t?V:s.jsxs("div",{className:"flex h-full min-h-0 flex-col",children:[s.jsxs("header",{className:"pt-safe flex items-center gap-2 border-b border-edge px-3 pb-2.5 sm:px-6 lg:py-4",children:[s.jsx("button",{type:"button",onClick:()=>l==null?void 0:l.openNav(),"aria-label":"Open menu",className:"btn-subtle -ml-1 shrink-0 px-2 py-1.5 lg:hidden",children:s.jsx(ce,{width:20,height:20})}),s.jsx(le,{width:18,height:18,className:"shrink-0 text-fg-subtle"}),s.jsx("h1",{className:"text-md font-semibold tracking-tight",children:"Discover"})]}),s.jsx("div",{className:"flex-1 overflow-y-auto overscroll-contain",children:s.jsx("div",{className:"mx-auto max-w-3xl px-6 py-6",children:V})})]})}function Pe({candidate:e,row:t,status:n,outcome:r,locked:a,onToggleRow:c,onToggleSession:l,onToggleExpanded:m}){const d=t??{checked:!1,expanded:!1,loading:!1,error:null},j=fe(d),I=C(d,e),h=r?ke(r):null;return s.jsx("li",{children:s.jsxs(me,{flush:!0,className:K("overflow-hidden",h==="success"&&"border-success-edge bg-success-soft",h==="warn"&&"border-warn-edge bg-warn-soft",h==="danger"&&"border-danger-edge bg-danger-soft",n==="running"&&"border-accent-edge"),"data-testid":`discover-row-${e.suggestedSlug}`,"data-tone":h??void 0,children:[s.jsxs("div",{className:"flex items-start gap-3 px-3 py-2.5",children:[s.jsx(J,{className:"mt-1",checked:j==="on",indeterminate:j==="mixed",disabled:a,"aria-label":`Adopt ${e.path}`,onChange:x=>c(x.target.checked)}),s.jsxs("div",{className:"min-w-0 flex-1",children:[s.jsx("code",{className:"block break-all text-sm text-fg",children:e.path}),s.jsxs("div",{className:"mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-fg-muted",children:[s.jsxs("span",{className:"tabular",children:[e.sessionCount," conversation",e.sessionCount===1?"":"s"]}),e.filteredCount>0&&s.jsxs("span",{className:"tabular",title:"Empty, slash-command-only or sweeper transcripts, withheld as noise",children:["· ",e.filteredCount," filtered out"]}),e.lastSessionAt!==void 0&&s.jsxs("span",{className:"tabular",children:["· last ",U(e.lastSessionAt)]}),e.gitRemote!==void 0&&s.jsx(T,{tone:"lineage",size:"sm",children:e.gitRemote}),!e.hasGit&&s.jsx(T,{tone:"neutral",size:"sm",children:"no git"}),!e.insideHome&&s.jsx(T,{tone:"neutral",size:"sm",children:"outside home"})]}),e.recordedPath!==void 0&&s.jsx(W,{tone:"warn",className:"mt-2",icon:s.jsx(X,{width:14,height:14}),children:s.jsxs("span",{className:"text-xs",children:["These transcripts record"," ",s.jsx("code",{className:"break-all",children:e.recordedPath})," — a different spelling of this path. The import matches on the exact path, so it may bring nothing in."]})}),r&&s.jsx("p",{className:K("mt-2 text-xs",h==="success"&&"text-success",h==="warn"&&"text-warn",h==="danger"&&"text-danger"),children:Ie(r,e)}),n==="running"&&!r&&s.jsx("p",{className:"mt-2 text-xs text-fg-muted",children:"Adopting…"})]}),s.jsxs("span",{className:"flex shrink-0 items-center gap-1",children:[s.jsx("span",{className:"tabular text-xs text-fg-subtle",children:I}),s.jsx($,{size:"icon-sm",variant:"subtle","aria-label":d.expanded?"Hide conversations":"Show conversations","aria-expanded":d.expanded,onClick:m,children:d.expanded?s.jsx(pe,{width:14,height:14}):s.jsx(xe,{width:14,height:14})})]})]}),d.expanded&&s.jsxs("div",{className:"border-t border-edge px-3 py-2",children:[d.loading&&s.jsx("p",{className:"py-2 text-xs text-fg-muted",children:"Reading transcripts…"}),d.error!==null&&s.jsx("p",{className:"py-2 text-xs text-danger",children:d.error}),d.sessions&&d.sessions.sessions.length===0&&s.jsx("p",{className:"py-2 text-xs text-fg-muted",children:"Nothing to adopt here any more — it may have been adopted since the scan."}),d.sessions&&d.sessions.sessions.length>0&&s.jsx("ul",{className:"space-y-1",children:d.sessions.sessions.map(x=>{var y;return s.jsx("li",{children:s.jsxs("label",{className:"flex cursor-pointer items-start gap-2 rounded-lg px-2 py-1.5 transition-colors hover:bg-surface-hover",children:[s.jsx(J,{checked:((y=d.selected)==null?void 0:y.has(x.sessionId))??!1,disabled:a,"aria-label":q(x),onChange:()=>l(x.sessionId)}),s.jsxs("span",{className:"min-w-0 flex-1",children:[s.jsx("span",{className:"block truncate text-xs text-fg",children:q(x)}),s.jsxs("span",{className:"tabular mt-0.5 block text-3xs text-fg-muted",children:[U(x.mtime)," · ",Oe(x.sizeBytes)]})]})]})},x.sessionId)})}),d.sessions&&d.sessions.filtered.length>0&&s.jsxs("p",{className:"mt-2 px-2 text-3xs text-fg-subtle",children:[d.sessions.filtered.length," more withheld as noise (",[...new Set(d.sessions.filtered.map(x=>x.reason))].join(", "),")."]})]})]})})}function Ee({result:e,includeNonGit:t,includeOutsideHome:n,disabled:r,onIncludeNonGit:a,onIncludeOutsideHome:c}){const l=e.excluded["no-git"]??0,m=e.excluded["outside-home"]??0,d=l>0||t,j=m>0||n;return e.scanned===0?null:s.jsxs("div",{className:"mt-5 space-y-2 border-t border-edge pt-4",children:[s.jsx("p",{className:"text-xs text-fg-muted",children:Re(e)}),d&&s.jsxs("div",{className:"flex items-center gap-2 text-xs text-fg-muted",children:[s.jsx(Q,{checked:t,disabled:r,onChange:a,label:"Also offer directories without a git repository"}),s.jsxs("span",{children:["Also offer directories without a git repository",l>0&&` (${l} hidden)`]})]}),j&&s.jsxs("div",{className:"flex items-center gap-2 text-xs text-fg-muted",children:[s.jsx(Q,{checked:n,disabled:r,onChange:c,label:"Also offer directories outside your home directory"}),s.jsxs("span",{children:["Also offer directories outside ",s.jsx("code",{children:e.homeDir}),m>0&&` (${m} hidden)`]})]})]})}function Ae({claudeHome:e,onStartChat:t}){return s.jsx(R,{variant:"panel",icon:s.jsx(ue,{width:22,height:22}),title:"Nothing to adopt yet",body:s.jsxs(s.Fragment,{children:["No Claude Code history has been recorded under"," ",s.jsx("code",{className:"break-all",children:e}),". That is normal on a new install, and on a container whose Claude home is its own rather than yours — nothing is wrong. Start a chat here and Paddock writes its own history; or bind-mount an existing one there and re-open this page, or import it headlessly with ",s.jsx("code",{children:"npm run import-chats"}),"."]}),action:t&&s.jsx($,{variant:"primary",icon:s.jsx(he,{width:14,height:14}),onClick:t,children:"Start a chat"})})}function Te({imported:e,projects:t,onGetStarted:n}){return s.jsx("div",{className:"mb-6",children:s.jsx(R,{variant:"panel",icon:s.jsx(Z,{width:22,height:22}),title:t===0?"Nothing was adopted":`${t} project${t===1?"":"s"}, ${e} conversation${e===1?"":"s"}`,body:t===0?"Every row failed. Each one says why below.":"They are in the sidebar now. Anything that did not land says why below.",action:s.jsx($,{variant:"primary",onClick:()=>void n(),children:"Get started"})})})}function De(){return s.jsx("div",{className:"space-y-2","aria-label":"Scanning",role:"status",children:[0,1,2].map(e=>s.jsx("div",{className:"h-16 animate-pulse rounded-2xl bg-surface-active"},e))})}function Re(e){const t=[],n=[["temp-root",a=>`${a} in temp directories`],["no-git",a=>`${a} without a git repository`],["outside-home",a=>`${a} outside your home directory`],["already-managed",a=>`${a} already a project`],["no-sessions",a=>`${a} with nothing to adopt`],["no-recorded-cwd",a=>`${a} with no recorded directory`],["missing",a=>`${a} no longer on disk`],["home-root",a=>`${a} your home directory itself`],["system-path",a=>`${a} system directories`],["paddock-internal",a=>`${a} inside Paddock's own data`]];for(const[a,c]of n){const l=e.excluded[a];l!==void 0&&l>0&&t.push(c(l))}const r=`${e.scanned} transcript folder${e.scanned===1?"":"s"} scanned`;return t.length===0?`${r}.`:`${r}; skipped ${t.join(", ")}.`}function q(e){var r,a;const t=(r=e.autoName)==null?void 0:r.trim();if(t!==void 0&&t!=="")return t;const n=(a=e.preview)==null?void 0:a.trim();return n!==void 0&&n!==""?n:e.sessionId}function U(e){const t=new Date(e);return Number.isNaN(t.getTime())?"unknown date":t.toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"})}function Oe(e){return e<1024?`${e} B`:e<1024*1024?`${Math.round(e/1024)} kB`:`${(e/(1024*1024)).toFixed(1)} MB`}export{Ge as D};
|
|
2
|
+
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{r as d,v as Z,w as ee,x as te,y as je,j as e,z as ie,c as b,D as le,E as ve,F as ye,G as oe,H as we,I as E,J as ce,K as L,O as Ne,a as D,A as de,Q as ke,U as _,X as Ce,V as Se,W as Ae,Y as Ee,Z as Re,_ as Pe,$ as $e,a0 as Me,a1 as Fe,a2 as Oe,a3 as Ie}from"./index-4_u0mfSb.js";import{S as ne}from"./Surfaces-iO74d5OF.js";import{u as Te}from"./documentTitle-CXH5hpbS.js";const Le=Array.from({length:72},(t,n)=>n*5),A=[...le].sort((t,n)=>t.hue-n.hue),z=(t,n)=>Math.min(Math.abs(t-n),360-Math.abs(t-n));function se(t){const n=A.reduce((i,s)=>z(s.hue,t)<z(i.hue,t)?s:i);if(z(n.hue,t)<=8)return n.name;const a=A.find(i=>i.hue>t)??A[0];return`between ${A[(A.indexOf(a)-1+A.length)%A.length].name} and ${a.name}`}function De({theme:t,dark:n,hue:a,className:u}){const i=d.useRef(null);return d.useLayoutEffect(()=>{const s=i.current;s&&(a===null?ye(s):oe(a,s))},[t,n,a]),e.jsx("div",{ref:i,"data-theme":t,className:b("overflow-hidden rounded-lg border border-edge",n&&"dark",u),children:e.jsxs("div",{className:"flex h-full items-center gap-2 bg-surface p-2",children:[e.jsxs("div",{className:"flex-1 rounded-md border border-edge-subtle bg-surface-raised p-1.5",children:[e.jsx("div",{className:"h-1 w-4/5 rounded-full bg-fg"}),e.jsx("div",{className:"mt-1 h-1 w-3/5 rounded-full bg-fg-subtle"})]}),e.jsx("div",{className:"grid h-6 w-6 shrink-0 place-items-center rounded-md bg-accent-solid",children:e.jsx("div",{className:"h-1 w-3 rounded-full bg-accent-fg"})})]})})}function Ve({hexes:t,hue:n,active:a,onCommit:u}){const i=d.useRef(null),s=d.useRef(null),o=d.useRef(null),x=d.useRef(0),h=d.useRef(null),p=d.useRef(!1),g=d.useRef(null),c=d.useMemo(()=>{if(t.length===0)return;const m=t.map((j,w)=>`${j} ${(w/t.length*100).toFixed(2)}%`);return m.push(`${t[0]} 100%`),`linear-gradient(to right, ${m.join(", ")})`},[t]),f=d.useCallback(m=>{s.current&&(s.current.style.left=`${m/360*100}%`),o.current&&(o.current.textContent=se(m))},[]);d.useEffect(()=>{p.current||f(n)},[n,f]);const N=m=>{var P;const j=(P=i.current)==null?void 0:P.getBoundingClientRect();if(!j||j.width===0)return 0;const w=Math.min(1,Math.max(0,(m-j.left)/j.width));return Math.round(w*360)%360},y=d.useCallback(m=>{h.current=m,!x.current&&(x.current=requestAnimationFrame(()=>{x.current=0;const j=h.current;h.current=null,j!==null&&(f(j),oe(j,document.documentElement,{context:g.current??void 0,live:!0}))}))},[f]);d.useEffect(()=>()=>cancelAnimationFrame(x.current),[]);const I=m=>{p.current=!1,g.current=null,cancelAnimationFrame(x.current),x.current=0,h.current=null,u(m)};return e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"mt-2 flex items-baseline justify-between gap-2",children:[e.jsx("span",{className:"text-3xs text-fg-subtle",children:"Drag along the strip"}),e.jsx("span",{ref:o,className:"text-3xs font-medium text-fg-muted","aria-hidden":!0})]}),e.jsx("div",{ref:i,role:"slider",tabIndex:0,"aria-label":"Accent colour","aria-valuemin":0,"aria-valuemax":359,"aria-valuenow":n,"aria-valuetext":a?se(n):"the theme’s own colour",onPointerDown:m=>{m.currentTarget.setPointerCapture(m.pointerId),p.current=!0,g.current=we(),y(N(m.clientX))},onPointerMove:m=>{p.current&&y(N(m.clientX))},onPointerUp:m=>{p.current&&I(N(m.clientX))},onPointerCancel:()=>{p.current&&I(n)},onKeyDown:m=>{const j=m.key==="PageUp"||m.key==="PageDown"?15:m.shiftKey?10:2;let w=null;m.key==="ArrowLeft"||m.key==="ArrowDown"||m.key==="PageDown"?w=(n-j+360)%360:m.key==="ArrowRight"||m.key==="ArrowUp"||m.key==="PageUp"?w=(n+j)%360:m.key==="Home"?w=0:m.key==="End"&&(w=359),w!==null&&(m.preventDefault(),u(w))},style:{backgroundImage:c,touchAction:"none"},className:b("relative mt-1 h-8 cursor-pointer rounded-lg border border-edge shadow-xs select-none","focus-visible:focus-ring"),children:e.jsx("div",{ref:s,"aria-hidden":!0,style:{left:`${n/360*100}%`},className:b("pointer-events-none absolute top-1/2 h-[calc(100%+6px)] w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full","border border-fg bg-surface-raised shadow-sm",!a&&"opacity-0")})})]})}function Ue({className:t}){const[n,a]=d.useState(Z),[u,i]=d.useState(null),s=typeof document<"u"&&document.documentElement.classList.contains("dark"),o=d.useRef(n),x=d.useCallback(c=>{const f={...o.current,...c};o.current=f,a(f),i(ee(f))},[]);d.useEffect(()=>{const c=f=>i(f.detail);return window.addEventListener(te,c),i(ee(Z())),()=>window.removeEventListener(te,c)},[]);const h=d.useMemo(()=>je(Le),[n.theme,s,u==null?void 0:u.mode]),p=(u==null?void 0:u.hue)??0,g=n.hue!==null;return e.jsxs("div",{className:b("space-y-5",t),children:[e.jsxs("fieldset",{children:[e.jsx("legend",{className:"text-2xs font-semibold uppercase tracking-wide text-fg-muted",children:"Theme"}),e.jsx("div",{className:b("mt-2 grid gap-2","grid-cols-2 sm:grid-cols-4"),children:ie.map(c=>{const f=n.theme===c.id;return e.jsxs("button",{type:"button","aria-pressed":f,title:`${c.label} — ${c.blurb}`,onClick:()=>x({theme:c.id}),className:b("motion-fast rounded-xl border text-left transition-[border-color,box-shadow]","focus-visible:focus-ring","p-1.5",f?"border-accent-edge bg-accent-soft shadow-xs":"border-edge bg-surface-raised hover:border-edge-strong"),children:[e.jsx(De,{theme:c.id,dark:s,hue:n.hue,className:"h-10"}),e.jsxs("div",{className:"mt-1.5 min-w-0 px-0.5 pb-0.5",children:[e.jsx("span",{className:"block truncate text-xs font-semibold text-fg",children:c.label}),e.jsx("span",{className:"mt-0.5 line-clamp-2 text-3xs leading-snug text-fg-muted",children:c.blurb})]})]},c.id)})})]}),e.jsxs("div",{className:b("grid gap-5 lg:grid-cols-[minmax(0,1fr)_15rem] lg:items-start"),children:[e.jsxs("fieldset",{children:[e.jsx("legend",{className:"text-2xs font-semibold uppercase tracking-wide text-fg-muted",children:"Accent colour"}),e.jsx("p",{className:"mt-1 text-3xs leading-snug text-fg-subtle",children:"Pick any colour. The theme adjusts it so buttons and labels stay readable in both light and dark."}),e.jsx(Ve,{hexes:h,hue:p,active:g,onCommit:c=>x({hue:c})}),e.jsxs("div",{className:"mt-2 flex flex-wrap gap-1",children:[e.jsx("button",{type:"button",onClick:()=>x({hue:null}),"aria-pressed":!g,className:b("motion-fast rounded-md border px-2 py-1 text-3xs transition-colors","focus-visible:focus-ring",g?"border-edge text-fg-muted hover:border-edge-strong hover:text-fg":"border-accent-edge bg-accent-soft text-accent"),children:"Theme’s own"}),le.map(c=>{const f=g&&Math.abs(p-c.hue)<2.5;return e.jsxs("button",{type:"button","aria-pressed":f,onClick:()=>x({hue:c.hue}),className:b("motion-fast inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-3xs transition-colors","focus-visible:focus-ring",f?"border-edge-strong bg-surface-active text-fg":"border-edge text-fg-muted hover:border-edge-strong hover:text-fg"),children:[e.jsx("span",{"aria-hidden":!0,className:"h-2.5 w-2.5 rounded-full",style:{backgroundColor:h[Math.round(c.hue/5)%h.length]}}),c.name]},c.name)})]})]}),e.jsxs("fieldset",{children:[e.jsx("legend",{className:"text-2xs font-semibold uppercase tracking-wide text-fg-muted",children:"Tint the background"}),e.jsx("p",{className:"mt-1 text-3xs leading-snug text-fg-subtle",children:"Colours the whole page a little, not just the buttons. Useful for telling two Paddock instances apart at a glance."}),e.jsx("div",{className:"mt-2 inline-flex rounded-lg border border-edge p-0.5",children:ve.map(c=>e.jsx("button",{type:"button","aria-pressed":n.tint===c.level,onClick:()=>x({tint:c.level}),className:b("motion-fast rounded-md px-2.5 py-1 text-3xs transition-colors","focus-visible:focus-ring",n.tint===c.level?"bg-surface-active font-medium text-fg":"text-fg-muted hover:text-fg"),children:c.label},c.level))})]})]}),e.jsx(_e,{})]})}function _e(){return e.jsxs("div",{children:[e.jsx("p",{className:"text-2xs font-semibold uppercase tracking-wide text-fg-muted",children:"Preview"}),e.jsxs("div",{className:"mt-2 flex flex-wrap items-center gap-2 rounded-xl border border-edge bg-surface p-3",children:[e.jsx(E,{variant:"primary",size:"sm",children:"Send"}),e.jsx(E,{variant:"ghost",size:"sm",children:"Cancel"}),e.jsx(E,{variant:"link",size:"sm",children:"A link"}),e.jsx("span",{className:"inline-flex items-center gap-1 rounded-md border border-accent-edge bg-accent-soft px-1.5 py-0.5 text-2xs font-medium text-accent",children:"accent chip"}),e.jsx(ce,{tone:"accent",shape:"pill",dot:!0,children:"running"}),e.jsxs("span",{className:"inline-flex items-center gap-1.5 text-2xs text-fg-subtle",children:[e.jsx("span",{className:"h-4 w-4 rounded-md border border-edge bg-accent-solid-hover","aria-hidden":!0}),"hovered"]}),e.jsxs("span",{className:"ml-auto inline-flex items-center gap-1.5",children:[e.jsx(L,{tone:"success"}),e.jsx(L,{tone:"warn"}),e.jsx(L,{tone:"danger"}),e.jsx(L,{tone:"info"}),e.jsx("span",{className:"text-2xs text-fg-subtle",children:"status hues (theme’s, not yours)"})]})]})]})}const G="mx-auto w-full max-w-4xl",Be="sm:w-64";function He(){const[t,n]=d.useState(null),[a,u]=d.useState(!0),[i,s]=d.useState(null),[o,x]=d.useState({}),[h,p]=d.useState(!1),[g,c]=d.useState(null),[f,N]=d.useState(!1),[y,I]=d.useState(""),[m,j]=d.useState(!1),[w,P]=d.useState(null),q=d.useRef(null),ue=Ne("(min-width: 1024px)");d.useEffect(()=>{let r=!0;return u(!0),D.getInstanceConfig().then(l=>{r&&(n(l),s(null))}).catch(l=>r&&s(l instanceof Error?l.message:String(l))).finally(()=>r&&u(!1)),()=>{r=!1}},[]);const me=(r,l)=>{N(!1),c(null),x(v=>({...v,[r]:l}))},xe=r=>re(r,o),$=d.useMemo(()=>t?t.groups.flatMap(r=>r.fields):[],[t]),k=d.useMemo(()=>$.filter(r=>r.editable&&!r.envOverridden).filter(r=>Object.prototype.hasOwnProperty.call(o,r.key)).filter(r=>!ae(o[r.key],X(r))).map(r=>r.key),[$,o]),J=d.useMemo(()=>new Set(k),[k]),C=d.useMemo(()=>{if(!t)return[];const r=y.trim().toLowerCase(),l=[];for(const v of t.groups){const F=r!==""&&v.label.toLowerCase().includes(r),O=v.fields.filter(R=>{const T=re(R,o);return m&&ae(T,R.default)?!1:r===""||F?!0:nt(R).includes(r)});O.length>0&&l.push({...v,fields:O})}return l},[t,y,m,o]),W=C.reduce((r,l)=>r+l.fields.length,0),he=y.trim()!==""||m,B=!m&&(y.trim()===""||rt.includes(y.trim().toLowerCase())),M=d.useMemo(()=>B?[U,...C]:C,[B,C]),fe=C.some(r=>r.fields.some(l=>l.envOverridden)),H=d.useMemo(()=>{const r=$.find(l=>l.key==="profile");return typeof(r==null?void 0:r.value)=="string"?r.value:null},[$]),ge=C.some(r=>r.fields.some(l=>l.fromProfile)),Y=st(C,J);d.useEffect(()=>{const r=q.current;if(!r||M.length===0)return;const l=M.map(F=>F.id),v=()=>{const F=r.getBoundingClientRect().top;let O=l[0];for(const R of l){const T=document.getElementById(V(R));if(T)if(T.getBoundingClientRect().top-F<=80)O=R;else break}P(O)};return v(),r.addEventListener("scroll",v,{passive:!0}),()=>r.removeEventListener("scroll",v)},[M]);const Q=r=>{var v;const l=document.getElementById(V(r));(v=l==null?void 0:l.scrollIntoView)==null||v.call(l,{behavior:"smooth",block:"start"}),P(r)},pe=async()=>{if(k.length===0||!t)return;const r={};for(const l of k)r[l]=o[l];p(!0),c(null);try{await D.updateInstanceConfig(r,t.configVersion);const l=await D.getInstanceConfig();n(l),x({}),N(!0)}catch(l){if(l instanceof Re&&l.status===409)try{n(await D.getInstanceConfig())}catch{}c(l instanceof Error?l.message:String(l))}finally{p(!1)}},be=()=>{x({}),c(null),N(!1)};return e.jsxs("div",{className:"flex min-h-0 flex-1",children:[t&&e.jsx(ze,{groups:M,active:w,onJump:Q,dirtyCount:Y}),e.jsxs("div",{className:"flex min-h-0 min-w-0 flex-1 flex-col",children:[t&&e.jsx("div",{className:"border-b border-edge px-4 py-2.5 sm:px-6",children:e.jsxs("div",{className:G,children:[e.jsx(Ke,{query:y,onQuery:I,modifiedOnly:m,onModifiedOnly:j,visibleCount:W,totalCount:$.length,filtering:he,autoFocus:ue}),e.jsx(Ge,{groups:M,active:w,onJump:Q,dirtyCount:Y})]})}),e.jsx("div",{ref:q,className:"min-h-0 flex-1 overflow-y-auto px-4 py-5 sm:px-6",children:e.jsxs("div",{className:G,children:[B&&e.jsx("div",{className:"mb-8 border-b border-edge pb-7",children:e.jsx(ne,{id:V(U.id),title:U.label,description:U.description,variant:"rule",first:!0,children:e.jsx(Ue,{})})}),e.jsx(Xe,{saved:f,configPath:t==null?void 0:t.configPath,restartRequired:(t==null?void 0:t.restartRequired)??!1,fileError:t==null?void 0:t.configFileError}),a&&e.jsx("p",{className:"text-sm text-fg-muted",children:"Loading…"}),i&&e.jsxs("p",{className:"text-sm text-danger",role:"alert",children:["Failed to load settings: ",i]}),t&&ge&&H&&e.jsx(qe,{name:H}),t&&fe&&e.jsx(We,{}),t&&W===0&&e.jsxs("p",{className:"py-8 text-center text-sm text-fg-muted",children:["No settings match"," ",y.trim()?e.jsxs(e.Fragment,{children:["“",e.jsx("span",{className:"font-medium",children:y.trim()}),"”"]}):"this filter","."]}),C.map(r=>e.jsx(ne,{id:V(r.id),title:r.label,description:r.description,variant:"card",flush:!0,children:e.jsx("div",{className:"divide-y divide-edge-subtle",children:r.fields.map(l=>e.jsx(Ye,{field:l,value:xe(l),dirty:J.has(l.key),profileName:H,onChange:v=>me(l.key,v)},l.key))})},r.id))]})}),t&&e.jsx("footer",{className:"border-t border-edge px-4 py-3 sm:px-6",children:e.jsxs("div",{className:`${G} flex items-center gap-3`,children:[g&&e.jsxs("span",{className:"flex min-w-0 items-center gap-1.5 text-sm text-danger",role:"alert",children:[e.jsx(de,{width:14,height:14,className:"shrink-0"}),e.jsx("span",{className:"truncate",children:g})]}),e.jsx("span",{className:"ml-auto shrink-0 text-xs tabular text-fg-subtle",children:k.length>0?`${k.length} unsaved change${k.length===1?"":"s"}`:"No changes"}),e.jsx(E,{variant:"subtle",onClick:be,disabled:k.length===0||h,children:"Reset"}),e.jsx(E,{variant:"primary",onClick:pe,disabled:k.length===0,loading:h,loadingLabel:"Saving…",children:"Save changes"})]})})]})]})}function ze({groups:t,active:n,onJump:a,dirtyCount:u}){return e.jsx("nav",{"aria-label":"Config sections",className:"hidden w-52 shrink-0 overflow-y-auto border-r border-edge px-2 py-4 lg:block",children:e.jsx("ul",{className:"space-y-0.5",children:t.map(i=>{const s=i.id===n,o=u[i.id]??0;return e.jsx("li",{children:e.jsxs("button",{type:"button",onClick:()=>a(i.id),"aria-current":s?"true":void 0,className:b("motion-fast flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-sm transition-colors",s?"bg-surface-selected font-medium text-fg":"text-fg-muted hover:bg-surface-hover hover:text-fg"),children:[e.jsx("span",{className:"min-w-0 flex-1 truncate",children:i.label}),o>0&&e.jsx("span",{className:"h-1.5 w-1.5 shrink-0 rounded-full bg-accent-solid","aria-label":`${o} unsaved`}),i.fields.length>0&&e.jsx("span",{className:"shrink-0 text-2xs tabular text-fg-subtle",children:i.fields.length})]})},i.id)})})})}function Ge({groups:t,active:n,onJump:a,dirtyCount:u}){return t.length===0?null:e.jsx("nav",{"aria-label":"Config sections",className:"-mx-1 mt-2 flex gap-1 overflow-x-auto px-1 pb-0.5 lg:hidden",children:t.map(i=>{const s=i.id===n,o=u[i.id]??0;return e.jsxs("button",{type:"button",onClick:()=>a(i.id),"aria-current":s?"true":void 0,className:b("motion-fast flex shrink-0 items-center gap-1.5 rounded-md px-2.5 py-1 text-xs transition-colors",s?"bg-surface-selected font-medium text-fg":"text-fg-muted hover:bg-surface-hover hover:text-fg"),children:[i.label,o>0&&e.jsx("span",{className:"h-1.5 w-1.5 rounded-full bg-accent-solid","aria-label":`${o} unsaved`})]},i.id)})})}function Ke({query:t,onQuery:n,modifiedOnly:a,onModifiedOnly:u,visibleCount:i,totalCount:s,filtering:o,autoFocus:x}){return e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsxs("div",{className:"relative min-w-0 flex-1 sm:max-w-sm",children:[e.jsx(ke,{width:14,height:14,className:"pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-fg-subtle"}),e.jsx(_,{type:"search","aria-label":"Search settings",placeholder:"Search settings, keys, env vars…",className:"h-8 py-0 pl-8 pr-7 [&::-webkit-search-cancel-button]:appearance-none",value:t,autoFocus:x,onChange:h=>n(h.target.value),onKeyDown:h=>{h.key==="Escape"&&t!==""&&(h.preventDefault(),n(""))}}),t!==""&&e.jsx("button",{type:"button","aria-label":"Clear search",onClick:()=>n(""),className:"absolute right-2 top-1/2 -translate-y-1/2 text-fg-subtle hover:text-fg",children:e.jsx(Ce,{width:13,height:13})})]}),e.jsx("button",{type:"button","aria-pressed":a,onClick:()=>u(!a),className:b("motion-fast shrink-0 rounded-md border px-2.5 py-1 text-xs transition-colors",a?"border-accent-edge bg-accent-soft text-accent":"border-edge text-fg-muted hover:text-fg"),children:"Modified only"}),e.jsx("span",{className:"shrink-0 text-xs tabular text-fg-subtle",children:o?`${i} of ${s}`:`${s} settings`})]})}function Xe({saved:t,configPath:n,restartRequired:a,fileError:u}){const i=u?"danger":t?"success":"warn";return e.jsx(Se,{tone:i,className:"mb-5 leading-snug",icon:i==="success"?e.jsx(Ae,{width:15,height:15}):e.jsx(de,{width:15,height:15}),children:e.jsx("span",{role:"status",children:u?e.jsxs(e.Fragment,{children:[e.jsxs("strong",{children:["Could not read ",K(n),"."]})," ",u," — the values below are the running instance's; what a restart would load is unknown until the file parses."]}):t?e.jsxs(e.Fragment,{children:[e.jsx("strong",{children:"Saved to disk."})," These changes take effect only after the server restarts."]}):a?e.jsxs(e.Fragment,{children:[e.jsx("strong",{children:"Restart pending."})," ",e.jsx("code",{className:"font-mono text-xs",children:K(n)})," holds changes the running instance has not picked up — the fields below marked"," ",e.jsx(S,{tone:"warn",children:"restart"})," differ from what is in force right now."]}):e.jsxs(e.Fragment,{children:["Changes here are written to"," ",e.jsx("code",{className:"font-mono text-xs",children:K(n)})," and take effect only after the server restarts — the running instance keeps its current config until then."]})})})}function qe({name:t}){return e.jsxs("p",{className:"mb-3 text-xs leading-snug text-fg-muted",children:["This instance's posture profile is ",e.jsx("span",{className:"font-medium text-fg",children:t}),". Settings marked ",e.jsx(S,{tone:"neutral",children:"profile"})," are the ones it chose; anything you set yourself, here or in the environment, wins over it."," ",e.jsx("a",{className:"underline decoration-edge underline-offset-2 hover:text-fg",href:Je,target:"_blank",rel:"noreferrer",children:"What the profiles do"})]})}const Je="https://paddock.edspencer.net/configuration/profiles/";function We(){return e.jsxs("p",{className:"mb-5 text-xs leading-snug text-fg-muted",children:["Settings marked ",e.jsx(S,{tone:"neutral",children:"env"})," are overridden by an environment variable, which wins over this file — edit that variable (and restart) to change them."]})}function Ye({field:t,value:n,dirty:a,profileName:u,onChange:i}){const s=!t.editable||t.envOverridden,o=`cfg-${t.key}`,x=t.help?`${o}-help`:void 0,h=t.type==="boolean",p=d.useMemo(()=>tt(t,X(t)),[t]),g=h&&!s,c=!t.help&&!t.envOverridden&&!t.pendingRestart,f=c?"items-center":"items-start",N=c?"sm:items-center":"sm:items-start",y=s?e.jsx(et,{field:t,value:n}):h?e.jsx(Ee,{id:o,checked:!!n,onChange:i,label:t.label}):e.jsx(Ze,{field:t,value:n,onChange:i,inputId:o,describedBy:x});return e.jsxs("div",{className:"relative px-4 py-3",children:[a&&e.jsx("span",{"aria-hidden":!0,className:"absolute inset-y-2 left-0 w-0.5 rounded-full bg-accent-solid"}),e.jsxs("div",{className:b(p?"":g?`flex gap-4 ${f}`:`sm:flex sm:gap-6 ${N}`),children:[e.jsxs("div",{className:"min-w-0 flex-1",children:[e.jsxs("div",{className:"flex flex-wrap items-center gap-1.5",children:[e.jsx("label",{htmlFor:s?void 0:o,className:"text-sm font-medium text-fg",children:t.label}),t.sensitive&&e.jsx(S,{tone:"warn",children:"sensitive"}),t.envOverridden&&e.jsx(S,{tone:"neutral",title:`Overridden by environment variable ${t.envVar} — edit that env var (and restart) to change it.`,children:"env"}),t.fromProfile&&e.jsx(S,{tone:"neutral",title:u?`Set by the "${u}" posture profile — neither this config file nor an environment variable overrides it.`:"Set by this instance's posture profile — neither this config file nor an environment variable overrides it.",children:"profile"}),!t.editable&&!t.envOverridden&&e.jsx(S,{tone:"neutral",children:"read-only"}),t.pendingRestart&&e.jsx(S,{tone:"warn",title:"The config file and the running instance disagree on this field — restart to apply.",children:"restart"})]}),t.help&&e.jsx("p",{id:x,className:"mt-0.5 text-xs leading-snug text-fg-muted",children:t.help}),t.envOverridden&&e.jsx("p",{className:"mt-1 truncate font-mono text-2xs text-fg-subtle",children:t.envVar}),t.pendingRestart&&e.jsxs("p",{className:"mt-1 text-2xs leading-snug text-warn",children:["In force now: ",e.jsx("span",{className:"font-mono",children:Qe(t.value)})]})]}),e.jsx("div",{className:b(p?"mt-2":g?"flex shrink-0 justify-end":`mt-2 sm:mt-0 sm:shrink-0 ${Be}`),children:y})]})]})}function Qe(t){if(t==null||t==="")return"(not set)";const a=(Array.isArray(t)?t.join(", "):String(t)).replace(/\s+/g," ").trim();return a.length>60?`${a.slice(0,60)}…`:a}function S({children:t,tone:n,title:a}){return e.jsx(ce,{tone:n,size:"sm",title:a,className:"uppercase tracking-wide",children:t})}function Ze({field:t,value:n,onChange:a,inputId:u,describedBy:i}){if(t.type==="enum")return e.jsx(Pe,{id:u,"aria-describedby":i,value:String(n??""),onChange:s=>a(s.target.value),children:(t.enumValues??[]).map(s=>e.jsx("option",{value:s,children:s},s))});if(t.type==="number")return e.jsx(_,{id:u,type:"number","aria-describedby":i,className:"tabular",value:n==null?"":String(n),placeholder:t.default===null?"default":String(t.default),onChange:s=>a(s.target.value===""?null:Number(s.target.value))});if(t.type==="text"){const s=n==null||n===t.default,o=n==null?String(t.default??""):String(n),x=typeof t.default=="string"&&!s;return e.jsxs("div",{children:[e.jsx($e,{id:u,"aria-describedby":i,className:"min-h-[7rem] font-mono text-xs leading-relaxed",rows:6,spellCheck:!1,value:o,onChange:h=>a(h.target.value)}),e.jsxs("div",{className:"mt-1 flex items-center justify-between gap-3 text-xs text-fg-muted",children:[e.jsx("span",{children:s?"Using the built-in default.":n===""?"Empty — nothing will be appended.":`${o.length.toLocaleString()} characters.`}),x&&e.jsx(E,{variant:"link",size:"sm",className:"shrink-0",onClick:()=>a(null),children:"Restore default"})]})]})}if(t.type==="string-list"){const s=Array.isArray(t.default)?t.default.map(String):null,o=Array.isArray(n)?n.map(String):null,x=o===null||s!==null&&o.length===s.length&&o.every((g,c)=>g===s[c]),h=o!==null?o.join(", "):(s==null?void 0:s.join(", "))??"",p=s!==null&&!x;return e.jsxs("div",{children:[e.jsx(_,{id:u,type:"text","aria-describedby":i,value:h,placeholder:(s==null?void 0:s.join(", "))??"",onChange:g=>a(g.target.value.split(",").map(c=>c.trim()).filter(Boolean))}),e.jsxs("div",{className:"mt-1 flex items-center justify-between gap-3 text-xs text-fg-muted",children:[e.jsx("span",{children:x?"Using the built-in default.":o!==null&&o.length===0?"Empty — none offered.":`${(o==null?void 0:o.length)??0} in the list.`}),p&&e.jsx(E,{variant:"link",size:"sm",className:"shrink-0",onClick:()=>a(null),children:"Restore default"})]})]})}return e.jsx(_,{id:u,type:"text","aria-describedby":i,value:n==null?"":String(n),placeholder:t.default===null?"":String(t.default),onChange:s=>a(s.target.value)})}function et({field:t,value:n}){const a="rounded-lg bg-surface-sunken px-3 py-2 font-mono text-xs text-fg-muted";return t.type==="boolean"?e.jsx("div",{className:a,children:n?"true":"false"}):n==null||n===""?e.jsx("div",{className:a,children:e.jsx("span",{className:"italic text-fg-subtle",children:t.type==="text"&&n===""?"(empty — nothing appended)":"(not set)"})}):t.type==="text"?e.jsx("div",{className:b(a,"max-h-40 overflow-y-auto whitespace-pre-wrap leading-relaxed"),children:String(n)}):e.jsx("div",{className:b(a,"break-all"),children:Array.isArray(n)?n.join(", "):String(n)})}function tt(t,n){return t.type==="text"||t.type==="string-list"?!0:(Array.isArray(n)?n.join(", "):n==null?"":String(n)).length>38}function nt(t){return[t.label,t.key,t.help,t.envVar,...t.enumValues??[]].filter(Boolean).join(" ").toLowerCase()}function st(t,n){const a={};for(const u of t)a[u.id]=u.fields.filter(i=>n.has(i.key)).length;return a}const V=t=>`cfg-section-${t}`,U={id:"appearance",label:"Appearance",description:"Theme and accent colour for this browser. Applies immediately — no save, no restart.",fields:[]},rt=["appearance theme themes colour color accent tint dark light font",...ie.map(t=>t.id)].join(" ");function X(t){return t.pendingValue===void 0?t.value:t.pendingValue}function re(t,n){return Object.prototype.hasOwnProperty.call(n,t.key)?n[t.key]:X(t)}function ae(t,n){return Array.isArray(t)&&Array.isArray(n)?t.length===n.length&&t.every((a,u)=>a===n[u]):t===n}function K(t){if(!t)return"paddock.config.yaml";const n=t.split("/");return n[n.length-1]||t}function ot(){const t=Me(),n=(t==null?void 0:t.openNav)??(()=>{});return Te(["Config"]),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-edge px-3 pb-2.5 sm:px-6 lg:py-4",children:[e.jsx("button",{type:"button",onClick:n,"aria-label":"Open menu",className:"btn-subtle -ml-1 shrink-0 px-2 py-1.5 lg:hidden",children:e.jsx(Fe,{width:20,height:20})}),e.jsx(Oe,{width:18,height:18,className:"shrink-0 text-fg-subtle"}),e.jsx("h1",{className:"text-md font-semibold tracking-tight",children:"Config"})]}),e.jsx(Ie,{}),e.jsx(He,{})]})}export{ot as InstanceConfigPage};
|
|
2
|
+
|