@lchase/kiln 0.15.0 → 0.16.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/assets/serve.css +913 -310
- package/assets/serve.js +414 -41
- package/dist/config.js +3 -1
- package/dist/schemas.js +19 -0
- package/dist/serve-page.js +97 -29
- package/dist/serve.js +19 -18
- package/dist/settings.js +7 -0
- package/dist/theme.js +35 -0
- package/package.json +1 -1
package/dist/schemas.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { BOARD_THEME_IDS } from "./theme.js";
|
|
2
3
|
/**
|
|
3
4
|
* Kiln config file. `repos` is an open map: any name, not a fixed set.
|
|
4
5
|
* `path` is absolute, `~`, or (in older files) relative to the config directory.
|
|
@@ -179,6 +180,21 @@ export const TriggerToggleSchema = z
|
|
|
179
180
|
enabled: z.boolean(),
|
|
180
181
|
})
|
|
181
182
|
.strict();
|
|
183
|
+
/** Known board theme ids. An unknown id in a write is rejected. */
|
|
184
|
+
export const BoardThemeSchema = z.enum(BOARD_THEME_IDS);
|
|
185
|
+
/**
|
|
186
|
+
* `board` on disk. `theme` is free text so a typo does not make the whole
|
|
187
|
+
* file invalid; readers fall back to `kiln`.
|
|
188
|
+
*/
|
|
189
|
+
export const BoardFileSchema = z.object({
|
|
190
|
+
theme: z.string().optional(),
|
|
191
|
+
});
|
|
192
|
+
/** `board` on PUT /api/settings. Only a known theme id is written. */
|
|
193
|
+
export const BoardWriteSchema = z
|
|
194
|
+
.object({
|
|
195
|
+
theme: BoardThemeSchema,
|
|
196
|
+
})
|
|
197
|
+
.strict();
|
|
182
198
|
export const EditableKilnSettingsSchema = z.object({
|
|
183
199
|
repos: KilnReposSchema,
|
|
184
200
|
stallTimeoutMs,
|
|
@@ -188,6 +204,7 @@ export const EditableKilnSettingsSchema = z.object({
|
|
|
188
204
|
dailyDollarCap,
|
|
189
205
|
sandbox: EditableSandboxSchema.optional(),
|
|
190
206
|
triggers: z.array(TriggerToggleSchema).optional(),
|
|
207
|
+
board: BoardWriteSchema.optional(),
|
|
191
208
|
});
|
|
192
209
|
/** PUT /api/settings body. `revision` is the sha256 of the file the client loaded. */
|
|
193
210
|
export const SettingsWriteSchema = EditableKilnSettingsSchema.extend({
|
|
@@ -216,6 +233,8 @@ export const KilnConfigFileSchema = z.object({
|
|
|
216
233
|
* Omitted uses the packaged `prompts/kiln/review.md`.
|
|
217
234
|
*/
|
|
218
235
|
reviewPrompt: z.string().min(1).optional(),
|
|
236
|
+
/** Board appearance. Omitted theme means Kiln. */
|
|
237
|
+
board: BoardFileSchema.optional(),
|
|
219
238
|
});
|
|
220
239
|
/**
|
|
221
240
|
* Where a job came from. Adapters set this; tick and reconcile do not branch on it.
|
package/dist/serve-page.js
CHANGED
|
@@ -6,6 +6,7 @@ import { COLUMN_LABEL, PIPELINE_COLUMNS, formatCost, formatDuration, formatLocal
|
|
|
6
6
|
import { renderMarkdown } from "./markdown.js";
|
|
7
7
|
import { kilnVersion } from "./project.js";
|
|
8
8
|
import { formatSpend } from "./rails.js";
|
|
9
|
+
import { BOARD_THEME_CHOICES, DEFAULT_BOARD_THEME } from "./theme.js";
|
|
9
10
|
import { listWorkflowNames } from "./workflow.js";
|
|
10
11
|
const LIVE_SCRIPT = readFileSync(join(packageRoot(), "assets", "serve.js"), "utf8");
|
|
11
12
|
export function renderConsole(css, attention, page) {
|
|
@@ -23,7 +24,7 @@ export function renderApp(model) {
|
|
|
23
24
|
const capNote = page.spend?.blocked ? `<span class="cap" id="cap-note">cap hit</span>` : `<span class="cap" id="cap-note" hidden>cap hit</span>`;
|
|
24
25
|
const body = `<div class="shell">
|
|
25
26
|
<aside class="nav" aria-label="Kiln">
|
|
26
|
-
<div class="brand"><span class="logo" aria-hidden="true"><svg
|
|
27
|
+
<div class="brand"><span class="logo" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12V6l5-3 5 3v6l-5 3-5-3z"/><path d="M3 6l5 3 5-3M8 9v6"/></svg></span><div><h1>Kiln</h1>${version}</div></div>
|
|
27
28
|
<div class="runstate" id="runstate">
|
|
28
29
|
<span class="live-dot${paused ? " paused" : ""}" id="run-dot" aria-hidden="true"></span>
|
|
29
30
|
<span id="run-label">${paused ? "Paused" : "Running"}</span>
|
|
@@ -50,31 +51,34 @@ ${intakeSheet(page, model.attention)}
|
|
|
50
51
|
revision: model.attention.revision,
|
|
51
52
|
csrf: page.csrf,
|
|
52
53
|
section: model.section,
|
|
54
|
+
theme: page.theme ?? DEFAULT_BOARD_THEME,
|
|
53
55
|
...(job ? { jobId: job.jobId, latest: job.latestEventAt } : {}),
|
|
54
56
|
live: true,
|
|
55
57
|
body,
|
|
56
58
|
});
|
|
57
59
|
}
|
|
58
|
-
export function renderTextPage(css, title, text) {
|
|
60
|
+
export function renderTextPage(css, title, text, theme = DEFAULT_BOARD_THEME) {
|
|
59
61
|
return document({
|
|
60
62
|
css,
|
|
61
63
|
title,
|
|
62
64
|
needsYou: 0,
|
|
63
65
|
revision: "",
|
|
64
66
|
section: "job",
|
|
67
|
+
theme,
|
|
65
68
|
live: false,
|
|
66
69
|
body: `<a class="back" href="/">Pipeline</a>
|
|
67
70
|
<div class="page"><h1>${escapeHtml(title)}</h1>
|
|
68
71
|
<pre class="text">${escapeHtml(text)}</pre></div>`,
|
|
69
72
|
});
|
|
70
73
|
}
|
|
71
|
-
export function renderMessagePage(css, title, message) {
|
|
74
|
+
export function renderMessagePage(css, title, message, theme = DEFAULT_BOARD_THEME) {
|
|
72
75
|
return document({
|
|
73
76
|
css,
|
|
74
77
|
title,
|
|
75
78
|
needsYou: 0,
|
|
76
79
|
revision: "",
|
|
77
80
|
section: "job",
|
|
81
|
+
theme,
|
|
78
82
|
live: false,
|
|
79
83
|
body: `<a class="back" href="/">Pipeline</a>
|
|
80
84
|
<div class="page"><h1>${escapeHtml(title)}</h1>
|
|
@@ -112,7 +116,7 @@ ${item("/settings", "open-settings", "Settings", "settings")}
|
|
|
112
116
|
function mainHtml(model, page) {
|
|
113
117
|
const error = `<p class="meta config-error" id="config-error" role="status"${page.configError ? "" : " hidden"}>${escapeHtml(page.configError ?? "")}</p>`;
|
|
114
118
|
if (model.section === "settings") {
|
|
115
|
-
return `${error}<main class="main"><h2>Settings</h2><section id="settings" aria-label="Settings">${settingsForm()}</section></main>`;
|
|
119
|
+
return `${error}<main class="main"><h2>Settings</h2><section id="settings" aria-label="Settings">${settingsForm(page.theme ?? DEFAULT_BOARD_THEME)}</section></main>`;
|
|
116
120
|
}
|
|
117
121
|
if (model.section === "stats" && model.stats) {
|
|
118
122
|
return `${error}<main class="main">${statsHtml(model.stats)}</main>`;
|
|
@@ -263,7 +267,7 @@ function jobRow(job, section, csrf) {
|
|
|
263
267
|
classes.push("retrying");
|
|
264
268
|
if (section === "inbox" || (section === "hand" && !job.scheduledRetry))
|
|
265
269
|
classes.push("human");
|
|
266
|
-
const flag = classes.includes("human") ? `<span class="flag">
|
|
270
|
+
const flag = classes.includes("human") ? `<span class="flag">Needs a decision</span>` : "";
|
|
267
271
|
const repo = section === "repos" ? "" : `<span class="meta">${escapeHtml(job.repo)}</span>`;
|
|
268
272
|
const sandbox = job.sandboxed ? `<span class="meta">sandbox · docker</span>` : "";
|
|
269
273
|
const pr = section === "repos" && job.state === "in-review" ? `<span class="meta">PR review</span>` : "";
|
|
@@ -651,6 +655,8 @@ function reportHtml(report) {
|
|
|
651
655
|
</div>
|
|
652
656
|
<section class="stat"><h3>What ran</h3>${runs}</section>`;
|
|
653
657
|
}
|
|
658
|
+
/** Chips wrap up to this many choices. Past it, use a select or combobox. */
|
|
659
|
+
const PICK_CHIP_MAX = 5;
|
|
654
660
|
function tableRows(rows, empty) {
|
|
655
661
|
if (rows.length === 0)
|
|
656
662
|
return `<p class="empty">${escapeHtml(empty)}</p>`;
|
|
@@ -668,31 +674,50 @@ function intakeSheet(intake, attention) {
|
|
|
668
674
|
<button type="button" class="close" data-close aria-label="Close">×</button>
|
|
669
675
|
</div>
|
|
670
676
|
<form id="add-form" class="intake-form">
|
|
671
|
-
<div class="
|
|
672
|
-
<
|
|
673
|
-
<
|
|
677
|
+
<div class="intake-body">
|
|
678
|
+
<div class="field" id="repo-field"><span class="k" id="repo-label">Repo</span>${repoPicker(intake.repos)}
|
|
679
|
+
<p class="field-error" id="repo-error" role="alert" hidden></p></div>
|
|
680
|
+
<div class="field" id="workflow-field"><span class="k" id="workflow-label">Workflow</span>${workflowPicker(intake.workflows)}
|
|
681
|
+
<p class="field-error" id="workflow-error" role="alert" hidden></p></div>
|
|
682
|
+
<div class="field"><span class="k" id="source-label">Source</span>
|
|
683
|
+
<div class="seg" role="group" aria-labelledby="source-label">
|
|
674
684
|
<button type="button" data-source="prompt" aria-pressed="true">Prompt</button>
|
|
675
685
|
<button type="button" data-source="spec" aria-pressed="false">Spec file</button>
|
|
676
686
|
<button type="button" data-source="tickets" aria-pressed="false">Tickets folder</button>
|
|
677
687
|
<button type="button" data-source="jira" aria-pressed="false">Jira</button>
|
|
678
|
-
</div>
|
|
679
|
-
<div data-pane="prompt"><label class="field"><span class="k">Prompt</span><textarea id="prompt-text" rows="4" placeholder="What should this job do?"></textarea
|
|
680
|
-
<
|
|
688
|
+
</div></div>
|
|
689
|
+
<div data-pane="prompt"><label class="field"><span class="k">Prompt</span><textarea id="prompt-text" rows="4" placeholder="What should this job do?" aria-describedby="prompt-error"></textarea>
|
|
690
|
+
<p class="field-error" id="prompt-error" role="alert" hidden></p></label></div>
|
|
691
|
+
<div class="field" id="browser-field" hidden>
|
|
681
692
|
<div class="k" id="browser-label">Spec file</div>
|
|
682
693
|
<div class="fs" id="browser"></div>
|
|
683
694
|
<p class="meta" id="picked"></p>
|
|
695
|
+
<p class="field-error" id="source-error" role="alert" hidden></p>
|
|
684
696
|
</div>
|
|
685
|
-
<div data-pane="jira" hidden><label class="field"><span class="k">Jira key</span><input id="jira-key" type="text" placeholder="PROJ-123" autocomplete="off" spellcheck="false"
|
|
697
|
+
<div data-pane="jira" hidden><label class="field"><span class="k">Jira key</span><input id="jira-key" type="text" placeholder="PROJ-123" autocomplete="off" spellcheck="false" aria-describedby="jira-error">
|
|
698
|
+
<p class="field-error" id="jira-error" role="alert" hidden></p></label></div>
|
|
686
699
|
<label class="field" id="issue-field"><span class="k">Issue</span><input id="job-issue" type="text" placeholder="ABC-123" autocomplete="off" spellcheck="false"></label>
|
|
687
700
|
<label class="field" id="title-field"><span class="k">Title</span><input id="job-title" type="text" autocomplete="off"></label>
|
|
688
701
|
<fieldset class="field" id="after-field"><legend class="k">After</legend><div id="after-list">${afterOptions(items.map((item) => item.job))}</div></fieldset>
|
|
689
702
|
<pre class="text preview" id="intake-preview" hidden></pre>
|
|
690
703
|
<p class="form-error" id="intake-error" role="alert" hidden></p>
|
|
691
|
-
|
|
704
|
+
</div>
|
|
705
|
+
<div class="form-footer">
|
|
706
|
+
<button type="button" class="btn" id="preview-job" hidden>Preview</button>
|
|
707
|
+
<button type="button" class="btn" data-close>Cancel</button>
|
|
708
|
+
<button type="submit" class="btn primary" id="add-submit" disabled>Add</button>
|
|
709
|
+
</div>
|
|
692
710
|
</form>
|
|
693
711
|
</section>`;
|
|
694
712
|
}
|
|
695
|
-
function
|
|
713
|
+
function themePicker(selected) {
|
|
714
|
+
const buttons = BOARD_THEME_CHOICES.map((theme) => {
|
|
715
|
+
const on = theme.id === selected;
|
|
716
|
+
return `<button type="button" class="theme" data-theme-choice="${theme.id}" aria-pressed="${on ? "true" : "false"}"><span class="theme-swatch" data-theme="${theme.id}" aria-hidden="true"><span class="swatch bg"></span><span class="swatch panel"></span><span class="swatch card"></span><span class="swatch accent"></span></span><span class="theme-label">${escapeHtml(theme.label)}</span></button>`;
|
|
717
|
+
}).join("");
|
|
718
|
+
return `<div class="themes" id="theme-pick" role="group" aria-label="Theme">${buttons}</div>`;
|
|
719
|
+
}
|
|
720
|
+
function settingsForm(theme) {
|
|
696
721
|
return `<form id="settings-form" class="intake-form">
|
|
697
722
|
<p class="meta" id="settings-path"></p>
|
|
698
723
|
<p class="saved" id="settings-saved" role="status" hidden>Saved</p>
|
|
@@ -700,6 +725,7 @@ function settingsForm() {
|
|
|
700
725
|
<div class="seg" role="tablist" aria-label="Settings sections">
|
|
701
726
|
<button type="button" data-settings-tab="repos" aria-pressed="true">Repos</button>
|
|
702
727
|
<button type="button" data-settings-tab="global" aria-pressed="false">Global</button>
|
|
728
|
+
<button type="button" data-settings-tab="appearance" aria-pressed="false">Appearance</button>
|
|
703
729
|
</div>
|
|
704
730
|
<div id="settings-repos">
|
|
705
731
|
<div id="repo-editor" hidden>
|
|
@@ -760,29 +786,70 @@ function settingsForm() {
|
|
|
760
786
|
<p class="hint" id="hint-cap"></p>
|
|
761
787
|
<p class="field-error" data-error="dailyDollarCap" hidden></p>
|
|
762
788
|
</div>
|
|
763
|
-
<div
|
|
789
|
+
<div id="settings-appearance" hidden>
|
|
790
|
+
<p class="hint">Color palette for this board. The choice applies immediately and is saved here.</p>
|
|
791
|
+
${themePicker(theme)}
|
|
792
|
+
<p class="field-error" data-error="board.theme" hidden></p>
|
|
793
|
+
</div>
|
|
794
|
+
<div class="form-footer"><button type="submit" class="btn primary">Save</button></div>
|
|
764
795
|
</form>`;
|
|
765
796
|
}
|
|
766
|
-
function
|
|
767
|
-
|
|
768
|
-
return `<p class="empty">No repos are registered.</p>`;
|
|
769
|
-
return repos
|
|
770
|
-
.map((repo, index) => `<button type="button" class="pick" data-repo="${escapeHtml(repo.name)}" data-path="${escapeHtml(repo.path)}" aria-pressed="${index === 0 ? "true" : "false"}">${escapeHtml(repo.name)}</button>`)
|
|
771
|
-
.join("");
|
|
797
|
+
function repoListKey(repos) {
|
|
798
|
+
return repos.map((repo) => `${repo.name}\t${repo.path}`).join("\n");
|
|
772
799
|
}
|
|
773
|
-
function
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
800
|
+
function repoPicker(repos) {
|
|
801
|
+
const list = escapeHtml(repoListKey(repos));
|
|
802
|
+
if (repos.length === 0) {
|
|
803
|
+
return `<div class="picks" id="repo-pick" data-mode="empty" data-value="" data-path="" data-list="${list}" role="group" aria-labelledby="repo-label"><p class="empty">No repos are registered.</p></div>`;
|
|
804
|
+
}
|
|
805
|
+
const selected = repos[0];
|
|
806
|
+
if (!selected) {
|
|
807
|
+
return `<div class="picks" id="repo-pick" data-mode="empty" data-value="" data-path="" data-list="" role="group" aria-labelledby="repo-label"><p class="empty">No repos are registered.</p></div>`;
|
|
808
|
+
}
|
|
809
|
+
if (repos.length > PICK_CHIP_MAX) {
|
|
810
|
+
const options = repos
|
|
811
|
+
.map((repo, index) => {
|
|
812
|
+
const on = repo.name === selected.name;
|
|
813
|
+
return `<li role="option" id="repo-opt-${index}" data-value="${escapeHtml(repo.name)}" data-path="${escapeHtml(repo.path)}" aria-selected="${on ? "true" : "false"}">${escapeHtml(repo.name)}</li>`;
|
|
814
|
+
})
|
|
815
|
+
.join("");
|
|
816
|
+
return `<div class="combo" id="repo-pick" data-mode="select" data-value="${escapeHtml(selected.name)}" data-path="${escapeHtml(selected.path)}" data-list="${list}"><input id="repo-pick-query" class="combo-input" type="text" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="repo-pick-list" aria-labelledby="repo-label" placeholder="Search repos" value="${escapeHtml(selected.name)}" autocomplete="off" spellcheck="false"><ul class="combo-list" id="repo-pick-list" role="listbox" hidden>${options}<li class="empty" role="presentation" hidden>No matching repos.</li></ul></div>`;
|
|
817
|
+
}
|
|
818
|
+
const buttons = repos
|
|
819
|
+
.map((repo, index) => {
|
|
820
|
+
const on = index === 0;
|
|
821
|
+
return `<button type="button" class="pick" data-repo="${escapeHtml(repo.name)}" data-path="${escapeHtml(repo.path)}" aria-pressed="${on ? "true" : "false"}">${escapeHtml(repo.name)}</button>`;
|
|
822
|
+
})
|
|
823
|
+
.join("");
|
|
824
|
+
return `<div class="picks" id="repo-pick" data-mode="chips" data-value="${escapeHtml(selected.name)}" data-path="${escapeHtml(selected.path)}" data-list="${list}" role="group" aria-labelledby="repo-label">${buttons}</div>`;
|
|
825
|
+
}
|
|
826
|
+
function workflowPicker(names) {
|
|
827
|
+
const selected = names.includes("feature") ? "feature" : (names[0] ?? "");
|
|
828
|
+
if (names.length === 0 || names.length > PICK_CHIP_MAX) {
|
|
829
|
+
const options = names.length === 0
|
|
830
|
+
? `<option value="">none</option>`
|
|
831
|
+
: names
|
|
832
|
+
.map((name) => `<option value="${escapeHtml(name)}"${name === selected ? " selected" : ""}>${escapeHtml(name)}</option>`)
|
|
833
|
+
.join("");
|
|
834
|
+
return `<select id="workflow" name="workflow" aria-labelledby="workflow-label">${options}</select>`;
|
|
835
|
+
}
|
|
836
|
+
const buttons = names
|
|
837
|
+
.map((name) => `<button type="button" data-workflow="${escapeHtml(name)}" aria-pressed="${name === selected ? "true" : "false"}">${escapeHtml(name)}</button>`)
|
|
778
838
|
.join("");
|
|
839
|
+
return `<div class="seg" id="workflow-pick" data-mode="chips" data-workflow="${escapeHtml(selected)}" role="group" aria-labelledby="workflow-label">${buttons}</div>`;
|
|
840
|
+
}
|
|
841
|
+
function afterLabel(job) {
|
|
842
|
+
const title = job.title.trim();
|
|
843
|
+
if (!title || title === job.jobId)
|
|
844
|
+
return job.jobId;
|
|
845
|
+
return `${job.jobId} — ${title}`;
|
|
779
846
|
}
|
|
780
847
|
function afterOptions(jobs) {
|
|
781
848
|
const open = jobs.filter((job) => job.state !== "done");
|
|
782
849
|
if (open.length === 0)
|
|
783
850
|
return `<p class="empty">No open jobs.</p>`;
|
|
784
851
|
return open
|
|
785
|
-
.map((job) => `<label class="after"><input type="checkbox" name="after" value="${escapeHtml(job.jobId)}"><span
|
|
852
|
+
.map((job) => `<label class="after"><input type="checkbox" name="after" value="${escapeHtml(job.jobId)}"><span>${escapeHtml(afterLabel(job))}</span></label>`)
|
|
786
853
|
.join("");
|
|
787
854
|
}
|
|
788
855
|
function safeUrl(url) {
|
|
@@ -801,8 +868,9 @@ function safeUrl(url) {
|
|
|
801
868
|
function document(options) {
|
|
802
869
|
const job = options.jobId ? ` data-job="${escapeHtml(options.jobId)}"` : "";
|
|
803
870
|
const latest = options.latest ? ` data-latest="${escapeHtml(options.latest)}"` : "";
|
|
871
|
+
const theme = options.theme ?? DEFAULT_BOARD_THEME;
|
|
804
872
|
return `<!DOCTYPE html>
|
|
805
|
-
<html lang="en">
|
|
873
|
+
<html lang="en" data-theme="${theme}">
|
|
806
874
|
<head>
|
|
807
875
|
<meta charset="utf-8">
|
|
808
876
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -812,7 +880,7 @@ function document(options) {
|
|
|
812
880
|
${options.css}
|
|
813
881
|
</style>
|
|
814
882
|
</head>
|
|
815
|
-
<body data-section="${escapeHtml(options.section)}" data-needs-you="${options.needsYou}" data-revision="${escapeHtml(options.revision)}" data-csrf="${escapeHtml(options.csrf ?? "")}"${job}${latest}>
|
|
883
|
+
<body data-section="${escapeHtml(options.section)}" data-needs-you="${options.needsYou}" data-revision="${escapeHtml(options.revision)}" data-csrf="${escapeHtml(options.csrf ?? "")}" data-pick-limit="${PICK_CHIP_MAX}"${job}${latest}>
|
|
816
884
|
${options.body}
|
|
817
885
|
${options.live ? `<script>\n${LIVE_SCRIPT}\n</script>` : ""}
|
|
818
886
|
</body>
|
package/dist/serve.js
CHANGED
|
@@ -271,7 +271,7 @@ async function handle(req, res, state, boundPort, css, csrf) {
|
|
|
271
271
|
if (listed === "stats") {
|
|
272
272
|
const window = parseStatsWindow(url.searchParams.get("window"));
|
|
273
273
|
if (!window) {
|
|
274
|
-
sendHtml(res, 400, renderMessagePage(css, "Stats", "window must be today, 7d, or 30d."), secrets);
|
|
274
|
+
sendHtml(res, 400, renderMessagePage(css, "Stats", "window must be today, 7d, or 30d.", config.boardTheme), secrets);
|
|
275
275
|
return;
|
|
276
276
|
}
|
|
277
277
|
sendHtml(res, 200, renderShell(state, css, csrf, "stats", { window }), secrets);
|
|
@@ -287,11 +287,11 @@ async function handle(req, res, state, boundPort, css, csrf) {
|
|
|
287
287
|
name = decodeURIComponent(repoPage[1]);
|
|
288
288
|
}
|
|
289
289
|
catch {
|
|
290
|
-
sendHtml(res, 404, renderMessagePage(css, "Not found", "Not found."), secrets);
|
|
290
|
+
sendHtml(res, 404, renderMessagePage(css, "Not found", "Not found.", config.boardTheme), secrets);
|
|
291
291
|
return;
|
|
292
292
|
}
|
|
293
293
|
if (!state.config.repos[name]) {
|
|
294
|
-
sendHtml(res, 404, renderMessagePage(css, "Not found", `No repo ${name}
|
|
294
|
+
sendHtml(res, 404, renderMessagePage(css, "Not found", `No repo ${name}.`, config.boardTheme), secrets);
|
|
295
295
|
return;
|
|
296
296
|
}
|
|
297
297
|
sendHtml(res, 200, renderShell(state, css, csrf, "repos", { repo: name }), secrets);
|
|
@@ -302,12 +302,12 @@ async function handle(req, res, state, boundPort, css, csrf) {
|
|
|
302
302
|
const jobId = parseJobId(jobPage[1]);
|
|
303
303
|
const job = jobId ? findJob(config, jobId) : undefined;
|
|
304
304
|
if (!job) {
|
|
305
|
-
sendHtml(res, 404, renderMessagePage(css, "Not found", jobId ? `No job ${jobId}.` : "Not found."), secrets);
|
|
305
|
+
sendHtml(res, 404, renderMessagePage(css, "Not found", jobId ? `No job ${jobId}.` : "Not found.", config.boardTheme), secrets);
|
|
306
306
|
return;
|
|
307
307
|
}
|
|
308
308
|
const tabRaw = url.searchParams.get("tab");
|
|
309
309
|
if (tabRaw && tabRaw !== "overview" && tabRaw !== "changes" && tabRaw !== "timeline") {
|
|
310
|
-
sendHtml(res, 400, renderMessagePage(css, "Job", "tab must be overview, changes, or timeline."), secrets);
|
|
310
|
+
sendHtml(res, 400, renderMessagePage(css, "Job", "tab must be overview, changes, or timeline.", config.boardTheme), secrets);
|
|
311
311
|
return;
|
|
312
312
|
}
|
|
313
313
|
const tab = tabRaw === "changes" || tabRaw === "timeline" ? tabRaw : "overview";
|
|
@@ -319,7 +319,7 @@ async function handle(req, res, state, boundPort, css, csrf) {
|
|
|
319
319
|
const jobId = parseJobId(page[1]);
|
|
320
320
|
const job = jobId ? findJob(config, jobId) : undefined;
|
|
321
321
|
if (!job) {
|
|
322
|
-
sendHtml(res, 404, renderMessagePage(css, "Not found", jobId ? `No job ${jobId}.` : "Not found."), secrets);
|
|
322
|
+
sendHtml(res, 404, renderMessagePage(css, "Not found", jobId ? `No job ${jobId}.` : "Not found.", config.boardTheme), secrets);
|
|
323
323
|
return;
|
|
324
324
|
}
|
|
325
325
|
if (page[2] === "findings") {
|
|
@@ -328,22 +328,22 @@ async function handle(req, res, state, boundPort, css, csrf) {
|
|
|
328
328
|
}
|
|
329
329
|
const target = page[2] === "spec" ? job.specPath : job.logPath;
|
|
330
330
|
if (!target) {
|
|
331
|
-
sendHtml(res, 404, renderMessagePage(css, "Not found", page[2] === "log" ? "No log yet." : "No spec."), secrets);
|
|
331
|
+
sendHtml(res, 404, renderMessagePage(css, "Not found", page[2] === "log" ? "No log yet." : "No spec.", config.boardTheme), secrets);
|
|
332
332
|
return;
|
|
333
333
|
}
|
|
334
334
|
const text = readInside(config.kilnDir, target);
|
|
335
335
|
if (text === undefined) {
|
|
336
|
-
sendHtml(res, 404, renderMessagePage(css, "Not found", "That file is not in the kiln directory."), secrets);
|
|
336
|
+
sendHtml(res, 404, renderMessagePage(css, "Not found", "That file is not in the kiln directory.", config.boardTheme), secrets);
|
|
337
337
|
return;
|
|
338
338
|
}
|
|
339
339
|
const title = page[2] === "spec" ? `${job.title} spec` : `${job.title} log`;
|
|
340
|
-
sendHtml(res, 200, renderTextPage(css, title, text), secrets);
|
|
340
|
+
sendHtml(res, 200, renderTextPage(css, title, text, config.boardTheme), secrets);
|
|
341
341
|
return;
|
|
342
342
|
}
|
|
343
343
|
if (path.startsWith("/api/"))
|
|
344
344
|
sendJson(res, 404, { error: "Not found." }, secrets);
|
|
345
345
|
else
|
|
346
|
-
sendHtml(res, 404, renderMessagePage(css, "Not found", "Not found."), secrets);
|
|
346
|
+
sendHtml(res, 404, renderMessagePage(css, "Not found", "Not found.", config.boardTheme), secrets);
|
|
347
347
|
}
|
|
348
348
|
async function cancelQueued(res, config, secrets, raw) {
|
|
349
349
|
let parsed;
|
|
@@ -393,7 +393,7 @@ async function answer(req, res, config, css, secrets, jobId, choice) {
|
|
|
393
393
|
if (wantsJson(req))
|
|
394
394
|
sendJson(res, status, { error: message }, secrets);
|
|
395
395
|
else
|
|
396
|
-
sendHtml(res, status, renderMessagePage(css, "Could not answer", message), secrets);
|
|
396
|
+
sendHtml(res, status, renderMessagePage(css, "Could not answer", message, config.boardTheme), secrets);
|
|
397
397
|
return;
|
|
398
398
|
}
|
|
399
399
|
const job = findJob(config, jobId);
|
|
@@ -410,7 +410,7 @@ async function revise(req, res, config, css, secrets, jobId, raw) {
|
|
|
410
410
|
if (wantsJson(req))
|
|
411
411
|
sendJson(res, 400, { error: message }, secrets);
|
|
412
412
|
else
|
|
413
|
-
sendHtml(res, 400, renderMessagePage(css, "Could not revise", message), secrets);
|
|
413
|
+
sendHtml(res, 400, renderMessagePage(css, "Could not revise", message, config.boardTheme), secrets);
|
|
414
414
|
return;
|
|
415
415
|
}
|
|
416
416
|
try {
|
|
@@ -422,7 +422,7 @@ async function revise(req, res, config, css, secrets, jobId, raw) {
|
|
|
422
422
|
if (wantsJson(req))
|
|
423
423
|
sendJson(res, status, { error: message }, secrets);
|
|
424
424
|
else
|
|
425
|
-
sendHtml(res, status, renderMessagePage(css, "Could not revise", message), secrets);
|
|
425
|
+
sendHtml(res, status, renderMessagePage(css, "Could not revise", message, config.boardTheme), secrets);
|
|
426
426
|
return;
|
|
427
427
|
}
|
|
428
428
|
const job = findJob(config, jobId);
|
|
@@ -455,7 +455,7 @@ async function retry(req, res, config, css, secrets, jobId) {
|
|
|
455
455
|
if (wantsJson(req))
|
|
456
456
|
sendJson(res, status, { error: message }, secrets);
|
|
457
457
|
else
|
|
458
|
-
sendHtml(res, status, renderMessagePage(css, "Could not retry", message), secrets);
|
|
458
|
+
sendHtml(res, status, renderMessagePage(css, "Could not retry", message, config.boardTheme), secrets);
|
|
459
459
|
return;
|
|
460
460
|
}
|
|
461
461
|
const job = findJob(config, jobId);
|
|
@@ -467,19 +467,19 @@ async function retry(req, res, config, css, secrets, jobId) {
|
|
|
467
467
|
}
|
|
468
468
|
function sendFindings(res, config, css, secrets, job) {
|
|
469
469
|
if (job.workflow !== INVESTIGATION_WORKFLOW) {
|
|
470
|
-
sendHtml(res, 404, renderMessagePage(css, "Not found", "Not found."), secrets);
|
|
470
|
+
sendHtml(res, 404, renderMessagePage(css, "Not found", "Not found.", config.boardTheme), secrets);
|
|
471
471
|
return;
|
|
472
472
|
}
|
|
473
473
|
if (!job.findingsPath) {
|
|
474
|
-
sendHtml(res, 404, renderMessagePage(css, "Not found", "No findings yet."), secrets);
|
|
474
|
+
sendHtml(res, 404, renderMessagePage(css, "Not found", "No findings yet.", config.boardTheme), secrets);
|
|
475
475
|
return;
|
|
476
476
|
}
|
|
477
477
|
const text = readInside(config.kilnDir, job.findingsPath);
|
|
478
478
|
if (text === undefined) {
|
|
479
|
-
sendHtml(res, 404, renderMessagePage(css, "Not found", "That file is not in the kiln directory."), secrets);
|
|
479
|
+
sendHtml(res, 404, renderMessagePage(css, "Not found", "That file is not in the kiln directory.", config.boardTheme), secrets);
|
|
480
480
|
return;
|
|
481
481
|
}
|
|
482
|
-
sendHtml(res, 200, renderTextPage(css, `${job.title} findings`, text), secrets);
|
|
482
|
+
sendHtml(res, 200, renderTextPage(css, `${job.title} findings`, text, config.boardTheme), secrets);
|
|
483
483
|
}
|
|
484
484
|
function findJob(config, jobId) {
|
|
485
485
|
try {
|
|
@@ -753,6 +753,7 @@ function pageOptions(state, csrf) {
|
|
|
753
753
|
paused: isPaused(state.config.kilnDir),
|
|
754
754
|
spend,
|
|
755
755
|
...(state.configError ? { configError: state.configError } : {}),
|
|
756
|
+
theme: state.config.boardTheme,
|
|
756
757
|
};
|
|
757
758
|
}
|
|
758
759
|
function statusDocument(state) {
|
package/dist/settings.js
CHANGED
|
@@ -5,6 +5,7 @@ import { basename, dirname, join } from "node:path";
|
|
|
5
5
|
import { assertBranchName, assertCheckoutDirectory } from "./config.js";
|
|
6
6
|
import { DEFAULT_BRANCH_TEMPLATE, DEFAULT_PR_TITLE } from "./pr-names.js";
|
|
7
7
|
import { DEFAULT_MAX_ATTEMPTS, DEFAULT_MAX_FIX_ROUNDS, DEFAULT_MAX_SAME_FINGERPRINT, DEFAULT_STALL_TIMEOUT_MS, KilnConfigFileSchema, SettingsWriteSchema, TriggerConfigSchema, } from "./schemas.js";
|
|
8
|
+
import { resolveBoardTheme } from "./theme.js";
|
|
8
9
|
import { applyTriggerToggles, effectiveTriggers, knownTrigger, REVIEW_REQUESTS_TRIGGER, reviewMaxNew, reviewMaxQueued, reviewRequestLabel, } from "./triggers.js";
|
|
9
10
|
const DEFAULTS = {
|
|
10
11
|
deliver: "local",
|
|
@@ -78,6 +79,7 @@ function project(path, revision, file) {
|
|
|
78
79
|
...(file.dailyDollarCap !== undefined ? { dailyDollarCap: file.dailyDollarCap } : {}),
|
|
79
80
|
...(sandbox ? { sandbox } : {}),
|
|
80
81
|
triggers: effectiveTriggers(file.triggers).map(triggerView),
|
|
82
|
+
board: { theme: resolveBoardTheme(file.board?.theme) },
|
|
81
83
|
defaults: DEFAULTS,
|
|
82
84
|
};
|
|
83
85
|
}
|
|
@@ -126,6 +128,11 @@ function mergeSettings(raw, settings) {
|
|
|
126
128
|
else
|
|
127
129
|
next.sandbox = prior;
|
|
128
130
|
}
|
|
131
|
+
if (settings.board) {
|
|
132
|
+
const prior = isRecord(raw.board) ? { ...raw.board } : {};
|
|
133
|
+
prior.theme = settings.board.theme;
|
|
134
|
+
next.board = prior;
|
|
135
|
+
}
|
|
129
136
|
return next;
|
|
130
137
|
}
|
|
131
138
|
function triggerView(trigger) {
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Board color themes. Each id matches a `theme:<id>` block in `assets/serve.css`.
|
|
3
|
+
* Missing or unknown values resolve to `kiln`.
|
|
4
|
+
*/
|
|
5
|
+
export const BOARD_THEME_IDS = [
|
|
6
|
+
"kiln",
|
|
7
|
+
"nord",
|
|
8
|
+
"gruvbox-dark",
|
|
9
|
+
"gruvbox-light",
|
|
10
|
+
"catppuccin-mocha",
|
|
11
|
+
"catppuccin-macchiato",
|
|
12
|
+
"catppuccin-frappe",
|
|
13
|
+
"catppuccin-latte",
|
|
14
|
+
"monospace-dark",
|
|
15
|
+
"monospace-light",
|
|
16
|
+
];
|
|
17
|
+
export const DEFAULT_BOARD_THEME = "kiln";
|
|
18
|
+
export const BOARD_THEME_CHOICES = [
|
|
19
|
+
{ id: "kiln", label: "Kiln" },
|
|
20
|
+
{ id: "nord", label: "Nord" },
|
|
21
|
+
{ id: "gruvbox-dark", label: "Gruvbox Dark" },
|
|
22
|
+
{ id: "gruvbox-light", label: "Gruvbox Light" },
|
|
23
|
+
{ id: "catppuccin-mocha", label: "Catppuccin Mocha" },
|
|
24
|
+
{ id: "catppuccin-macchiato", label: "Catppuccin Macchiato" },
|
|
25
|
+
{ id: "catppuccin-frappe", label: "Catppuccin Frappé" },
|
|
26
|
+
{ id: "catppuccin-latte", label: "Catppuccin Latte" },
|
|
27
|
+
{ id: "monospace-dark", label: "Monospace Dark" },
|
|
28
|
+
{ id: "monospace-light", label: "Monospace Light" },
|
|
29
|
+
];
|
|
30
|
+
export function resolveBoardTheme(value) {
|
|
31
|
+
if (typeof value === "string" && BOARD_THEME_IDS.includes(value)) {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
return DEFAULT_BOARD_THEME;
|
|
35
|
+
}
|