chamba 0.7.0 → 0.9.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/LICENSE +53 -14
- package/README.md +34 -8
- package/dist/commands/advanced.js +7 -278
- package/dist/commands/dev.js +11 -612
- package/dist/commands/doctor.js +1 -29
- package/dist/commands/menu.js +1 -80
- package/dist/commands/onboard.js +6 -230
- package/dist/commands/settings.js +15 -349
- package/dist/lib/agent-commands.js +2 -0
- package/dist/lib/agent-context.js +6 -210
- package/dist/lib/browser.js +1 -40
- package/dist/lib/chamba-yaml.js +10 -191
- package/dist/lib/constants.js +1 -125
- package/dist/lib/dockerfile-builder.js +41 -261
- package/dist/lib/env.js +2 -78
- package/dist/lib/git-env.js +1 -21
- package/dist/lib/global-config.js +4 -66
- package/dist/lib/pnpm-store.js +1 -19
- package/dist/lib/ports.js +1 -210
- package/dist/lib/safe-rm.js +1 -36
- package/dist/lib/sessions.js +2 -34
- package/dist/lib/shadows.js +1 -176
- package/dist/lib/skills.js +2 -0
- package/dist/lib/templates.js +1 -0
- package/dist/lib/webterm.js +1 -305
- package/dist/lib/workspace-identity.js +4 -260
- package/package.json +4 -4
- package/templates/Dockerfile +15 -22
- package/templates/claude-statusline.sh +5 -7
- package/templates/context/baseline.md +4 -2
- package/templates/context/web-pane-craft.md +1 -1
- package/templates/pane-apps/client/assets/api-BBDf_rzz.js +12 -0
- package/templates/pane-apps/client/assets/api-DNLbSBpK.css +1 -0
- package/templates/pane-apps/client/assets/bee-B7t97l5B.webp +0 -0
- package/templates/pane-apps/client/assets/buzz-CUUsbnXH.mp3 +0 -0
- package/templates/pane-apps/client/assets/c-BNa-DeUl.json +3552 -0
- package/templates/pane-apps/client/assets/cpp-B-T4NRoH.json +20239 -0
- package/templates/pane-apps/client/assets/csharp-BbxHChhA.json +6010 -0
- package/templates/pane-apps/client/assets/css-CWCxuwmz.json +1860 -0
- package/templates/pane-apps/client/assets/diff-B-2N89g8.json +154 -0
- package/templates/pane-apps/client/assets/docker-DWTvYW26.json +103 -0
- package/templates/pane-apps/client/assets/files-BkmyervY.js +1 -0
- package/templates/pane-apps/client/assets/go-B2hyYeNg.json +3156 -0
- package/templates/pane-apps/client/assets/graphql-Z6Id2aNO.json +1292 -0
- package/templates/pane-apps/client/assets/home-3SeGzySM.css +1 -0
- package/templates/pane-apps/client/assets/home-Cu9xOd2i.js +1 -0
- package/templates/pane-apps/client/assets/html-BtzvlIfu.json +2627 -0
- package/templates/pane-apps/client/assets/ini-Bn3Twap1.json +108 -0
- package/templates/pane-apps/client/assets/java-C7t2dwLO.json +1885 -0
- package/templates/pane-apps/client/assets/javascript-DuC2v0C0.json +5996 -0
- package/templates/pane-apps/client/assets/json-CwdkWeV5.json +206 -0
- package/templates/pane-apps/client/assets/jsonc-MYWDbTU3.json +206 -0
- package/templates/pane-apps/client/assets/jsx-Cn8EZuIC.json +5996 -0
- package/templates/pane-apps/client/assets/kotlin-CFNOz1gw.json +580 -0
- package/templates/pane-apps/client/assets/make-BVnlFCcl.json +629 -0
- package/templates/pane-apps/client/assets/markdown-B4Xk2WM_.json +3263 -0
- package/templates/pane-apps/client/assets/php-CAQzQmmI.json +4188 -0
- package/templates/pane-apps/client/assets/pop-D5ZANBC3.mp3 +0 -0
- package/templates/pane-apps/client/assets/python-CYQee8m4.json +4174 -0
- package/templates/pane-apps/client/assets/reviews-DMrGB-kG.js +153 -0
- package/templates/pane-apps/client/assets/reviews-nK_yRav9.css +1 -0
- package/templates/pane-apps/client/assets/ruby-D7R58tus.json +3114 -0
- package/templates/pane-apps/client/assets/rust-BFBbFiei.json +1103 -0
- package/templates/pane-apps/client/assets/scss-TQRhBkPG.json +1874 -0
- package/templates/pane-apps/client/assets/shellscript-DieAkC4W.json +2375 -0
- package/templates/pane-apps/client/assets/specs-Cabb15F0.js +13 -0
- package/templates/pane-apps/client/assets/sql-Bxd030LV.json +634 -0
- package/templates/pane-apps/client/assets/toml-ChrKGzsV.json +430 -0
- package/templates/pane-apps/client/assets/tsx-D2e90MFw.json +5996 -0
- package/templates/pane-apps/client/assets/typescript-DonXQ_n1.json +5747 -0
- package/templates/pane-apps/client/assets/useNow-BHFKFraS.js +1 -0
- package/templates/pane-apps/client/assets/xml-B17r2RXF.json +382 -0
- package/templates/pane-apps/client/assets/yaml-CKn4h1zy.json +627 -0
- package/templates/pane-apps/client/home/index.html +16 -0
- package/templates/pane-apps/client/reviews/index.html +17 -0
- package/templates/pane-apps/client/specs/index.html +4 -2
- package/templates/pane-apps/server/home.mjs +12 -0
- package/templates/pane-apps/server/reviews.mjs +21 -0
- package/templates/pane-apps/server/specs.mjs +5 -1588
- package/templates/skills/chamba-statusline/SKILL.md +7 -19
- package/templates/skills/dx-review/SKILL.md +136 -0
- package/templates/skills/dx-review/references/acts.md +233 -0
- package/templates/skills/dx-spec/SKILL.md +230 -30
- package/templates/skills/dx-spec/references/principles-template.md +2 -0
- package/templates/skills/dx-spec/references/review-guide.md +6 -52
- package/templates/skills/dx-spec/references/spec-guide.md +25 -5
- package/templates/skills/dx-spec/references/stages.md +149 -60
- package/templates/skills/dx-spec-execute/SKILL.md +217 -38
- package/templates/startup.mjs +22 -16
- package/templates/tool-helper.sh +168 -0
- package/templates/webterm/README.md +82 -27
- package/templates/webterm/color.js +61 -0
- package/templates/webterm/config.js +55 -4
- package/templates/webterm/context/claude.md +0 -1
- package/templates/webterm/public/app/alerts.js +95 -14
- package/templates/webterm/public/app/anchored.js +81 -0
- package/templates/webterm/public/app/composer.js +37 -3
- package/templates/webterm/public/app/connection.js +23 -0
- package/templates/webterm/public/app/dom.js +3 -0
- package/templates/webterm/public/app/frames.js +52 -18
- package/templates/webterm/public/app/home-host.js +16 -0
- package/templates/webterm/public/app/main.js +24 -8
- package/templates/webterm/public/app/new-session.js +13 -41
- package/templates/webterm/public/app/palette.js +58 -0
- package/templates/webterm/public/app/pane-shape.js +169 -0
- package/templates/webterm/public/app/pane-shell.js +240 -135
- package/templates/webterm/public/app/pane.js +39 -24
- package/templates/webterm/public/app/reviews-host.js +19 -0
- package/templates/webterm/public/app/specs-host.js +12 -218
- package/templates/webterm/public/app/state.js +5 -0
- package/templates/webterm/public/app/tabs.js +6 -2
- package/templates/webterm/public/app/theme.js +75 -27
- package/templates/webterm/public/app/tool-host.js +318 -0
- package/templates/webterm/public/app/workspace-color.js +192 -0
- package/templates/webterm/public/index.html +22 -4
- package/templates/webterm/public/styles.css +195 -21
- package/templates/webterm/server.js +198 -150
- package/templates/webterm/tool-document.js +3 -2
- package/templates/webterm/tools/commands.js +61 -0
- package/templates/webterm/tools/home.js +58 -0
- package/templates/webterm/tools/index.js +26 -0
- package/templates/webterm/tools/mount.js +337 -0
- package/templates/webterm/tools/paths.js +34 -0
- package/templates/webterm/tools/reviews.js +137 -0
- package/templates/webterm/tools/specs.js +154 -0
- package/templates/webterm/typed-line.js +70 -7
- package/templates/context/context-usage.md +0 -1
- package/templates/context-usage.sh +0 -266
- package/templates/pane-apps/client/assets/specs-B1970L17.css +0 -1
- package/templates/pane-apps/client/assets/specs-cEee_SPn.js +0 -23
- package/templates/skills/dx-spec-config/SKILL.md +0 -313
- package/templates/skills/dx-spec-config/references/principles-template.md +0 -12
- package/templates/specs.sh +0 -106
- package/templates/webterm/specs.js +0 -358
|
@@ -31,10 +31,10 @@ import { dropPage, fetchPage, renderPage, submitFeedback } from "./pane-frame.js
|
|
|
31
31
|
import {
|
|
32
32
|
flashPane,
|
|
33
33
|
isPaneCollapsed,
|
|
34
|
-
isPaneShut,
|
|
35
34
|
isToolOpen,
|
|
36
35
|
noteTool,
|
|
37
36
|
PAGES,
|
|
37
|
+
pulseTool,
|
|
38
38
|
registerTool,
|
|
39
39
|
rememberPageSize,
|
|
40
40
|
setPaneCollapsed,
|
|
@@ -55,11 +55,14 @@ const AGE_TICK_MS = 30_000;
|
|
|
55
55
|
// heard about, so switching back to a tab lands on the page it was left on.
|
|
56
56
|
const panes = new Map();
|
|
57
57
|
|
|
58
|
-
// Which of TEXT_SIZES the shown page is drawn at.
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
// Which of TEXT_SIZES the shown page is drawn at. It belongs to the session, so it is asked of the pane each
|
|
59
|
+
// time rather than held here: a switch changes the answer. The multiplier is what the pane stores, so a
|
|
60
|
+
// session left at a size this version no longer offers falls back to the default instead of landing between
|
|
61
|
+
// two steps.
|
|
62
|
+
function sizeStep() {
|
|
63
|
+
const at = TEXT_SIZES.indexOf(storedPageSize());
|
|
64
|
+
return at === -1 ? DEFAULT_SIZE_STEP : at;
|
|
65
|
+
}
|
|
63
66
|
|
|
64
67
|
// --- The tool's own state ----------------------------------------------------------------------------------------------------------------
|
|
65
68
|
|
|
@@ -89,11 +92,13 @@ export function applyPages(msg) {
|
|
|
89
92
|
// saying-so is held back while another tool has the pane.
|
|
90
93
|
if (arrived !== null) {
|
|
91
94
|
select(msg.sid, arrived.id, { user: false });
|
|
95
|
+
// The pane switches tools for nobody. With Pages open the pane's own edge says the page is here;
|
|
96
|
+
// with another tool open the Pages tab says it, and the tool the user is working in is left alone.
|
|
92
97
|
if (isToolOpen(PAGES)) flashPane();
|
|
98
|
+
else pulseTool(PAGES);
|
|
93
99
|
}
|
|
94
|
-
// A pane someone put away comes back for a page
|
|
95
|
-
//
|
|
96
|
-
// the pane in every other empty session in this window too.
|
|
100
|
+
// A pane someone put away comes back for a page, in the session the page was published in - which is
|
|
101
|
+
// the session this frame is about, since the list only reaches the tool for the one it is driving.
|
|
97
102
|
if (arrived !== null && isToolOpen(PAGES) && isPaneCollapsed()) setPaneCollapsed(false);
|
|
98
103
|
else render();
|
|
99
104
|
}
|
|
@@ -172,7 +177,7 @@ function introState() {
|
|
|
172
177
|
const wrap = document.createElement("div");
|
|
173
178
|
wrap.className = "pane-intro";
|
|
174
179
|
const title = document.createElement("h2");
|
|
175
|
-
title.textContent = "
|
|
180
|
+
title.textContent = "Pages";
|
|
176
181
|
const lede = document.createElement("p");
|
|
177
182
|
lede.textContent = "Ask the agent for a page whenever the answer is longer or more structured than the terminal carries well:";
|
|
178
183
|
const asks = document.createElement("ul");
|
|
@@ -230,38 +235,43 @@ function chipFor(sid, page, selected) {
|
|
|
230
235
|
// would have to be put back on every one of those paths. Setting it out here costs nothing and cannot be
|
|
231
236
|
// missed.
|
|
232
237
|
function applyTextSize() {
|
|
233
|
-
const
|
|
238
|
+
const step = sizeStep();
|
|
239
|
+
const size = TEXT_SIZES[step];
|
|
234
240
|
toolPages.style.setProperty("--page-zoom", String(size));
|
|
235
241
|
pageSizeVal.textContent = `${Math.round(size * 100)}%`;
|
|
236
242
|
// A control that can do nothing says so, rather than swallowing the press.
|
|
237
|
-
pageSmaller.disabled =
|
|
238
|
-
pageBigger.disabled =
|
|
243
|
+
pageSmaller.disabled = step === 0;
|
|
244
|
+
pageBigger.disabled = step === TEXT_SIZES.length - 1;
|
|
239
245
|
}
|
|
240
246
|
|
|
241
247
|
function stepTextSize(by) {
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
248
|
+
const step = sizeStep();
|
|
249
|
+
const next = Math.min(Math.max(step + by, 0), TEXT_SIZES.length - 1);
|
|
250
|
+
if (next === step) return;
|
|
251
|
+
rememberPageSize(TEXT_SIZES[next]);
|
|
245
252
|
applyTextSize();
|
|
246
|
-
rememberPageSize(TEXT_SIZES[sizeStep]);
|
|
247
253
|
}
|
|
248
254
|
|
|
249
255
|
pageSmaller.addEventListener("click", () => stepTextSize(-1));
|
|
250
256
|
pageBigger.addEventListener("click", () => stepTextSize(1));
|
|
251
|
-
applyTextSize();
|
|
252
257
|
|
|
253
258
|
function render() {
|
|
259
|
+
// The zoom is the session's, so it is applied wherever this tool draws rather than once at load: the
|
|
260
|
+
// first draw is already in a session's shape, and every switch draws again.
|
|
261
|
+
applyTextSize();
|
|
254
262
|
const sid = attachedSid;
|
|
255
263
|
const entry = sid === null ? null : (panes.get(sid) ?? null);
|
|
256
264
|
const pages = entry?.pages ?? [];
|
|
257
265
|
const unread = pages.filter((page) => page.unread).length;
|
|
258
266
|
|
|
259
|
-
// Nothing published in this session, and nothing to say about it. The tab stays
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
// pane keeps out of the way, are the pane's decisions to take from what its open tool has.
|
|
267
|
+
// Nothing published in this session, and nothing to say about it. The tab stays and the pane stays open,
|
|
268
|
+
// because a pane that only appears once an agent has used it is one nobody knows to ask for. What the
|
|
269
|
+
// empty pane says is the tool's own intro; the tab's mark is the shell's, from what the tool says here.
|
|
263
270
|
const bare = pages.length === 0 && !entry?.notice;
|
|
264
|
-
|
|
271
|
+
// How many pages nobody has opened, the way Specs counts its open asks. Three waiting is a different day
|
|
272
|
+
// from one, and the pages already read are not a number anyone acts on, so the tab counts the unopened
|
|
273
|
+
// ones alone and never the whole list.
|
|
274
|
+
noteTool(PAGES, { unread, empty: bare, counted: true });
|
|
265
275
|
|
|
266
276
|
chips.replaceChildren(...pages.map((page) => chipFor(sid, page, page.id === entry?.selected)));
|
|
267
277
|
const at = pages.findIndex((page) => page.id === entry?.selected);
|
|
@@ -291,6 +301,11 @@ function render() {
|
|
|
291
301
|
pageDoc.append(emptyState(notice));
|
|
292
302
|
return;
|
|
293
303
|
}
|
|
304
|
+
// Fetching a page is what marks it read - the server owns that, and it answers with a fresh list. So a
|
|
305
|
+
// tool nobody is looking at must not fetch: the tab's own mark, set a few lines up, would be cleared by
|
|
306
|
+
// the frame that fetch provokes, before anyone had seen it. The document is drawn when the user opens
|
|
307
|
+
// the tool, by this same function.
|
|
308
|
+
if (!isToolOpen(PAGES)) return;
|
|
294
309
|
show(sid, entry.selected);
|
|
295
310
|
// A chip that scrolled out of the bar while the user was reading something else.
|
|
296
311
|
chips.querySelector(".chip.sel")?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
@@ -300,7 +315,7 @@ function render() {
|
|
|
300
315
|
registerTool(PAGES, render);
|
|
301
316
|
|
|
302
317
|
setInterval(() => {
|
|
303
|
-
if (!
|
|
318
|
+
if (!isPaneCollapsed() && isToolOpen(PAGES)) render();
|
|
304
319
|
}, AGE_TICK_MS);
|
|
305
320
|
|
|
306
321
|
// A bar that fits at one width may not at another, and the arrows only exist when it does not.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// reviews-host.js - the Code Reviews tool on the page.
|
|
2
|
+
//
|
|
3
|
+
// As the Specs host beside it: the tool's own name, its panel and its opening sentence, and the frame around
|
|
4
|
+
// them is `tool-host.js`.
|
|
5
|
+
//
|
|
6
|
+
// "Reviews" and not "Code Reviews", because this word is what the user reads on the tab and in the panel
|
|
7
|
+
// while the tool opens, and four tabs have to fit at the width the pane reopens at. The tool's own name is
|
|
8
|
+
// Code Reviews everywhere else, the server table included, so every refusal still names it in full.
|
|
9
|
+
|
|
10
|
+
import { toolReviews } from "./dom.js";
|
|
11
|
+
import { REVIEWS } from "./pane-shell.js";
|
|
12
|
+
import { createToolHost } from "./tool-host.js";
|
|
13
|
+
|
|
14
|
+
createToolHost({
|
|
15
|
+
id: REVIEWS,
|
|
16
|
+
label: "Reviews",
|
|
17
|
+
panel: toolReviews,
|
|
18
|
+
opening: "Opening the reviews in this repository.",
|
|
19
|
+
});
|
|
@@ -1,222 +1,16 @@
|
|
|
1
|
-
// specs-host.js - the Specs tool
|
|
1
|
+
// specs-host.js - the Specs tool on the page.
|
|
2
2
|
//
|
|
3
|
-
// The tool's
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// and the one message that gives the frame what it needs to ask the server anything.
|
|
7
|
-
//
|
|
8
|
-
// The handover is the whole point of this file. The frame gets a Specs-scoped key that opens the tool's data
|
|
9
|
-
// routes and a path prefix that opens workspace files, and it gets them once, unprompted, on the load of the
|
|
10
|
-
// document this shell put there. It is never handed out on request: a window object survives a navigation, so
|
|
11
|
-
// a document that navigated itself into this frame would ask with the same identity as the one that was
|
|
12
|
-
// loaded. A second load is therefore read as a navigation, and the frame is thrown away and built again.
|
|
3
|
+
// The host around a tool's frame is `tool-host.js`, where every tool's is. What is here is which tool this
|
|
4
|
+
// is: its name, which is what its routes and its messages are named after, the panel the pane gave it, and
|
|
5
|
+
// the sentence its tab shows while it is opening.
|
|
13
6
|
|
|
14
|
-
import { sendFrame } from "./connection.js";
|
|
15
7
|
import { toolSpecs } from "./dom.js";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// What the shell and the frame call each other's messages. Namespaced because a frame may be talking to
|
|
25
|
-
// something else of its own, and every message is checked against the frame it must come from.
|
|
26
|
-
const HANDOVER = "specs:credentials";
|
|
27
|
-
const CHANGE = "specs:change";
|
|
28
|
-
const OPEN = "specs:open";
|
|
29
|
-
const DELIVER = "specs:deliver";
|
|
30
|
-
const WAITING = "specs:waiting";
|
|
31
|
-
const DELIVERED = "specs:delivered";
|
|
32
|
-
|
|
33
|
-
// How many times a frame that navigates itself is rebuilt before the tab says so instead. A client that keeps
|
|
34
|
-
// leaving the document it was given is broken or is being driven, and either way rebuilding it for ever is a
|
|
35
|
-
// spinning window rather than a recovery.
|
|
36
|
-
const MAX_REBUILDS = 3;
|
|
37
|
-
|
|
38
|
-
let frame = null;
|
|
39
|
-
let handed = false;
|
|
40
|
-
let rebuilds = 0;
|
|
41
|
-
let credentials = null;
|
|
42
|
-
let asking = false;
|
|
43
|
-
let failure = null;
|
|
44
|
-
// How many things are waiting for the user inside the tool. Kept here rather than in the frame, because the
|
|
45
|
-
// mark has to hold while another tool is open and the frame is not being drawn.
|
|
46
|
-
let waiting = 0;
|
|
47
|
-
|
|
48
|
-
/** Fetch the frame's credentials. The shell holds the master key; this is the only route it opens for Specs. */
|
|
49
|
-
async function fetchCredentials() {
|
|
50
|
-
if (asking) return;
|
|
51
|
-
asking = true;
|
|
52
|
-
try {
|
|
53
|
-
const response = await fetch(KEYS_URL, { headers: { [KEY_HEADER]: WEB_KEY }, cache: "no-store" });
|
|
54
|
-
if (!response.ok) throw new Error(String(response.status));
|
|
55
|
-
const answer = await response.json();
|
|
56
|
-
if (typeof answer?.key !== "string" || typeof answer?.raw !== "string") throw new Error("no credentials");
|
|
57
|
-
credentials = answer;
|
|
58
|
-
failure = null;
|
|
59
|
-
} catch {
|
|
60
|
-
failure = "The Specs tool could not be reached. The next time this tab is opened it tries again.";
|
|
61
|
-
} finally {
|
|
62
|
-
asking = false;
|
|
63
|
-
}
|
|
64
|
-
// Drawn whether the tab is open or not. The panel is off the screen when it is not, so nothing is shown
|
|
65
|
-
// either way, but this is the step that puts the frame up - and the tool has to be running to say what
|
|
66
|
-
// is waiting for the user, wherever in the pane that user happens to be.
|
|
67
|
-
render();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Put a fresh frame in the panel, replacing whatever was there. */
|
|
71
|
-
function build() {
|
|
72
|
-
const next = document.createElement("iframe");
|
|
73
|
-
next.className = "tool-frame";
|
|
74
|
-
// The whole boundary in one attribute: scripts and forms, and no same-origin, so the document has an
|
|
75
|
-
// opaque origin and nothing of this window is reachable from inside it - not the DOM, not storage, and
|
|
76
|
-
// not the URL that carries the master key. Set before the src, since the sandbox of a frame that already
|
|
77
|
-
// has a document is not what a later attribute says it is.
|
|
78
|
-
next.setAttribute("sandbox", "allow-scripts allow-forms");
|
|
79
|
-
// A frame inherits its embedder's referrer, and this window's URL carries the master key - both as
|
|
80
|
-
// `document.referrer` inside the frame and as the `Referer` header on everything it loads.
|
|
81
|
-
next.setAttribute("referrerpolicy", "no-referrer");
|
|
82
|
-
next.setAttribute("title", "Specs");
|
|
83
|
-
next.addEventListener("load", () => onLoad(next));
|
|
84
|
-
next.src = FRAME_SRC;
|
|
85
|
-
frame = next;
|
|
86
|
-
handed = false;
|
|
87
|
-
toolSpecs.replaceChildren(next);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** The frame finished loading a document. The first one is the client; a second one is a navigation. */
|
|
91
|
-
function onLoad(loaded) {
|
|
92
|
-
if (loaded !== frame) return;
|
|
93
|
-
if (!handed) {
|
|
94
|
-
handed = true;
|
|
95
|
-
loaded.contentWindow?.postMessage({ t: HANDOVER, key: credentials.key, raw: credentials.raw }, "*");
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
if (rebuilds >= MAX_REBUILDS) {
|
|
99
|
-
frame = null;
|
|
100
|
-
failure = "The Specs tool kept leaving the page it was given, so it was stopped.";
|
|
101
|
-
render();
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
rebuilds += 1;
|
|
105
|
-
build();
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** One sentence in the middle of the panel: what the tab is doing, or why it is doing nothing. */
|
|
109
|
-
function say(line) {
|
|
110
|
-
const wrap = document.createElement("div");
|
|
111
|
-
wrap.className = "pane-intro";
|
|
112
|
-
const title = document.createElement("h2");
|
|
113
|
-
title.textContent = "Specs";
|
|
114
|
-
const lede = document.createElement("p");
|
|
115
|
-
lede.textContent = line;
|
|
116
|
-
wrap.append(title, lede);
|
|
117
|
-
toolSpecs.replaceChildren(wrap);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function render() {
|
|
121
|
-
// The tab always has something to show, so it never asks the pane to keep out of the way. What is on it
|
|
122
|
-
// is what the tool last said is waiting for the user, which holds while another tool is open.
|
|
123
|
-
noteTool(SPECS, { unread: waiting, empty: false });
|
|
124
|
-
if (failure !== null) {
|
|
125
|
-
// Said once and then forgotten, so coming back to the tab is what tries again.
|
|
126
|
-
const line = failure;
|
|
127
|
-
failure = null;
|
|
128
|
-
frame = null;
|
|
129
|
-
say(line);
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
// A frame that is up is left alone: it holds the tool's whole state, and rebuilding it would be a reload
|
|
133
|
-
// every time the user came back to the tab.
|
|
134
|
-
if (frame !== null) return;
|
|
135
|
-
if (credentials === null) {
|
|
136
|
-
say("Opening the specs in this repository.");
|
|
137
|
-
fetchCredentials();
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
build();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** The disk moved under a spec. Passed straight in: the frame decides what is worth re-reading. */
|
|
144
|
-
export function applySpecs(msg) {
|
|
145
|
-
if (frame === null || !handed) return;
|
|
146
|
-
frame.contentWindow?.postMessage({ t: CHANGE, change: msg.change }, "*");
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Whether a message is the tool asking for something, rather than anything else on this window.
|
|
151
|
-
*
|
|
152
|
-
* Two checks, and each one holds without the other. The sender must be the frame's own window, which refuses
|
|
153
|
-
* a workspace document nested inside the tool - it is a frame of its own and can post here. And the message
|
|
154
|
-
* must carry the Specs key, which only the document this shell loaded ever received, so a document that
|
|
155
|
-
* navigated the frame passes the window check and fails this one.
|
|
156
|
-
*/
|
|
157
|
-
function fromTool(event, message) {
|
|
158
|
-
if (frame === null || !handed || credentials === null) return false;
|
|
159
|
-
if (event.source !== frame.contentWindow) return false;
|
|
160
|
-
return typeof message.key === "string" && message.key === credentials.key;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// A link the tool will not follow itself. The frame has to stay the document this shell put in it, since a
|
|
164
|
-
// navigation would keep the window identity every check here rests on, so an outside link is opened as a
|
|
165
|
-
// window of this interface instead - the web alone, and with nothing of this window carried into it.
|
|
166
|
-
window.addEventListener("message", (event) => {
|
|
167
|
-
const message = event.data;
|
|
168
|
-
if (!message || typeof message !== "object" || message.t !== OPEN) return;
|
|
169
|
-
if (!fromTool(event, message)) return;
|
|
170
|
-
let url;
|
|
171
|
-
try {
|
|
172
|
-
url = new URL(String(message.url));
|
|
173
|
-
} catch {
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") return;
|
|
177
|
-
window.open(url.href, "_blank", "noopener,noreferrer");
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// The tool saying what it is waiting on. The count is the tool's own, and the mark it puts on the tab is the
|
|
181
|
-
// shell's - which is what makes something awaiting an answer visible from any other tool in the pane.
|
|
182
|
-
window.addEventListener("message", (event) => {
|
|
183
|
-
const message = event.data;
|
|
184
|
-
if (!message || typeof message !== "object" || message.t !== WAITING) return;
|
|
185
|
-
if (!fromTool(event, message)) return;
|
|
186
|
-
const count = Number(message.count);
|
|
187
|
-
waiting = Number.isFinite(count) && count > 0 ? Math.min(Math.floor(count), 99) : 0;
|
|
188
|
-
noteTool(SPECS, { unread: waiting, empty: false });
|
|
8
|
+
import { SPECS } from "./pane-shell.js";
|
|
9
|
+
import { createToolHost } from "./tool-host.js";
|
|
10
|
+
|
|
11
|
+
createToolHost({
|
|
12
|
+
id: SPECS,
|
|
13
|
+
label: "Specs",
|
|
14
|
+
panel: toolSpecs,
|
|
15
|
+
opening: "Opening the specs in this repository.",
|
|
189
16
|
});
|
|
190
|
-
|
|
191
|
-
// A delivery: the tool telling the agent beside it that something arrived. It rides this window's own socket,
|
|
192
|
-
// so the server resolves the session from the socket the way every other pane frame does, and nothing here
|
|
193
|
-
// or in the frame names one. What travels is the kind of event, a line of detail, the file the tool wrote,
|
|
194
|
-
// and whether the user agreed to start a session for it.
|
|
195
|
-
window.addEventListener("message", (event) => {
|
|
196
|
-
const message = event.data;
|
|
197
|
-
if (!message || typeof message !== "object" || message.t !== DELIVER) return;
|
|
198
|
-
if (!fromTool(event, message)) return;
|
|
199
|
-
sendFrame({
|
|
200
|
-
t: DELIVER,
|
|
201
|
-
event: String(message.event ?? ""),
|
|
202
|
-
detail: String(message.detail ?? ""),
|
|
203
|
-
path: String(message.path ?? ""),
|
|
204
|
-
create: message.create === true,
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
/** What the server made of the last delivery, passed back to the frame that asked for it. */
|
|
209
|
-
export function applySpecsDelivery(msg) {
|
|
210
|
-
if (frame === null || !handed) return;
|
|
211
|
-
frame.contentWindow?.postMessage({ t: DELIVERED, ok: msg.ok === true, error: msg.error ?? "" }, "*");
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
registerTool(SPECS, render);
|
|
215
|
-
|
|
216
|
-
// The frame is built at startup rather than the first time the tab is opened, because the mark on the tab is
|
|
217
|
-
// the tool's to put there and the tool cannot say anything until it is running. Waiting for the first open
|
|
218
|
-
// would make "something is waiting for you" visible only to a user who already went and looked, which is the
|
|
219
|
-
// one reader who did not need telling - and a window reopened on another tool would show nothing at all.
|
|
220
|
-
// Nothing is drawn by this: the panel is off the screen until its tab is picked, and the frame holds the
|
|
221
|
-
// tool's state from here on, so opening the tab shows what is already there instead of loading it then.
|
|
222
|
-
render();
|
|
@@ -13,6 +13,10 @@ export let maxSessions = 8;
|
|
|
13
13
|
export let defaultAgent = "the agent";
|
|
14
14
|
export let agents = [];
|
|
15
15
|
export let workspaceName = "";
|
|
16
|
+
// The accent colour id the user picked for this workspace, or "" for none. What is stored, never what is on
|
|
17
|
+
// the screen: the panel marks this, so "nothing picked" stays tellable from "picked the colour the port
|
|
18
|
+
// would have given anyway".
|
|
19
|
+
export let workspaceColorId = "";
|
|
16
20
|
// Where "+ New session" starts an agent, relative to the workspace root ("" is the root itself). The server
|
|
17
21
|
// owns it - it follows the directory chamba was last run in - and the picker opens on it.
|
|
18
22
|
export let defaultCwd = "";
|
|
@@ -26,6 +30,7 @@ export function adoptSessionsFrame(msg) {
|
|
|
26
30
|
defaultAgent = msg.agent || defaultAgent;
|
|
27
31
|
if (Array.isArray(msg.agents)) agents = msg.agents;
|
|
28
32
|
workspaceName = msg.workspace || "";
|
|
33
|
+
workspaceColorId = typeof msg.color === "string" ? msg.color : "";
|
|
29
34
|
defaultCwd = msg.defaultCwd ?? "";
|
|
30
35
|
}
|
|
31
36
|
|
|
@@ -18,6 +18,7 @@ import { repositionNewPop, toggleNewPop } from "./new-session.js";
|
|
|
18
18
|
import { agents, attachedSid, defaultAgent, defaultCwd, maxSessions, sessions, workspaceName } from "./state.js";
|
|
19
19
|
import { focusTerminal, term } from "./terminal.js";
|
|
20
20
|
import { colorFor } from "./theme.js";
|
|
21
|
+
import { repositionColorPop, workspaceColorButton } from "./workspace-color.js";
|
|
21
22
|
|
|
22
23
|
// The session tab being renamed right now (its id), and the text in its editor. Kept in module state so a
|
|
23
24
|
// bar re-render (an incoming server frame, or the 60s age tick) rebuilds the editor without losing what
|
|
@@ -513,11 +514,14 @@ export function renderBar() {
|
|
|
513
514
|
ws.textContent = workspaceName;
|
|
514
515
|
right.append(ws);
|
|
515
516
|
}
|
|
516
|
-
|
|
517
|
+
// The colour first: it is about this window's identity rather than about what the window does, so it sits
|
|
518
|
+
// next to the name it belongs with, on the far side of the bell from the button that ends everything.
|
|
519
|
+
right.append(workspaceColorButton(), bellButton(), stopButton());
|
|
517
520
|
tabbar.append(right);
|
|
518
521
|
|
|
519
|
-
//
|
|
522
|
+
// Both panels hang off the body, so they survive this rebuild - but the buttons under them just moved.
|
|
520
523
|
repositionNewPop();
|
|
524
|
+
repositionColorPop();
|
|
521
525
|
|
|
522
526
|
// Keep focus in the rename editor across re-renders. The first render after a double-click selects the
|
|
523
527
|
// whole label so it can be typed over; a later render (an incoming frame) only restores the caret.
|
|
@@ -1,25 +1,23 @@
|
|
|
1
|
-
// theme.js -
|
|
1
|
+
// theme.js - what colour this workspace is wearing right now, and what colour its sessions get.
|
|
2
2
|
//
|
|
3
|
-
// The workspace
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// so two containers open side by side are almost never
|
|
3
|
+
// The workspace wears one colour: the composer border, the active tab's edge, the buttons and the frame around
|
|
4
|
+
// the tab icon are all that one colour, so a window is recognisable across a screen full of them before a
|
|
5
|
+
// single word is read. The user picks it from the bar, and until someone does it comes from the published
|
|
6
|
+
// port, which chamba assigns per workspace, so two containers open side by side are almost never alike.
|
|
7
7
|
//
|
|
8
8
|
// The sessions inside that window then rotate through the slots the workspace did not take. That is what
|
|
9
9
|
// makes a tab's own dot mean "this session" rather than "this workspace", and it is why a dot can never come
|
|
10
10
|
// out the same colour as the window it lives in.
|
|
11
|
+
//
|
|
12
|
+
// The colour moves while the page is open, so nothing here is a constant for anyone else to import: it is read
|
|
13
|
+
// through workspaceColor() and set through applyWorkspaceColor(). A module that took a copy at load would keep
|
|
14
|
+
// drawing the colour the workspace had when it started. The palette and the rules over it are in palette.js,
|
|
15
|
+
// which touches no globals and is where the tests check them.
|
|
16
|
+
//
|
|
17
|
+
// The stored colour lives in the container and arrives over the socket, which connects after the first paint.
|
|
18
|
+
// So this module also keeps the browser's own copy of the last colour it was told, and paints that first.
|
|
11
19
|
|
|
12
|
-
|
|
13
|
-
// those read as a warning here, and the bar has enough to say without one. Keep the length one longer than
|
|
14
|
-
// PALETTE_SIZE in sessions.js (a test pins it): the workspace eats one slot, sessions rotate the rest.
|
|
15
|
-
const PALETTE = [
|
|
16
|
-
"#2fe58a", // spring green
|
|
17
|
-
"#1fe0cf", // turquoise
|
|
18
|
-
"#f24bff", // magenta
|
|
19
|
-
"#b45cff", // neon violet
|
|
20
|
-
"#29b6ff", // azure
|
|
21
|
-
"#ff2e88", // rose
|
|
22
|
-
];
|
|
20
|
+
import { automaticId, hexFor, sessionColors } from "./palette.js";
|
|
23
21
|
|
|
24
22
|
// A stable number for this window when there is no port to read (a proxy, or the default 80/443). Not a
|
|
25
23
|
// hash worth defending - it only has to be the same on every reload of the same address.
|
|
@@ -29,18 +27,68 @@ function hostSeed() {
|
|
|
29
27
|
return seed;
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
30
|
+
// What this workspace wears when nothing is stored for it.
|
|
31
|
+
const AUTOMATIC_ID = automaticId(Number(location.port) || hostSeed());
|
|
32
|
+
|
|
33
|
+
// The last colour this browser was told, kept so the next load paints it before the socket answers. One
|
|
34
|
+
// localStorage entry, named the way the interface names its own (webterm-sound, webterm-pane).
|
|
35
|
+
//
|
|
36
|
+
// A cache and never the truth. The file in the workspace's state directory decides, and the first sessions
|
|
37
|
+
// frame replaces whatever is held here. It exists for one thing: without it every load would open on the
|
|
38
|
+
// colour the port gives and then jump to the stored one, which is the flicker this work was asked to settle.
|
|
39
|
+
// localStorage is held per origin and the origin names the port, so a workspace whose published port moves
|
|
40
|
+
// starts again with nothing cached - it paints the new automatic colour once, then settles on the stored one.
|
|
41
|
+
const COLOR_KEY = "webterm-color";
|
|
42
|
+
|
|
43
|
+
function cachedColorId() {
|
|
44
|
+
try {
|
|
45
|
+
return localStorage.getItem(COLOR_KEY) || "";
|
|
46
|
+
} catch {
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Written whenever a frame carries a colour, and removed when one carries "". A browser that refuses storage
|
|
52
|
+
// still works: it just opens on the automatic colour every time and corrects itself when the socket answers.
|
|
53
|
+
//
|
|
54
|
+
// Only on a change. A sessions frame goes out on every working-or-idle transition the registry notices, which
|
|
55
|
+
// is many a minute under a busy agent, and this is a synchronous write of a value that almost never moves.
|
|
56
|
+
function rememberColorId(id) {
|
|
57
|
+
try {
|
|
58
|
+
if (id === cachedColorId()) return;
|
|
59
|
+
if (id === "") localStorage.removeItem(COLOR_KEY);
|
|
60
|
+
else localStorage.setItem(COLOR_KEY, id);
|
|
61
|
+
} catch {
|
|
62
|
+
// Nothing to do about it, and nothing worth saying to the user.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
39
65
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
66
|
+
let workspaceHex = "";
|
|
67
|
+
let sessionHexes = [];
|
|
68
|
+
|
|
69
|
+
// Handed to the stylesheet: everything that carries the window's identity reads --ws from there, so one line
|
|
70
|
+
// in the page decides what colour this workspace is. Called once below, before anything is drawn, and again
|
|
71
|
+
// whenever the stored colour changes.
|
|
72
|
+
export function applyWorkspaceColor(id) {
|
|
73
|
+
// Cached here rather than in the frame router, because the two callers are the frame that carries a
|
|
74
|
+
// colour and the load below that reads the cache back - so caching what is applied is caching what the
|
|
75
|
+
// server last said, and the load's own call rewrites the value it just read.
|
|
76
|
+
rememberColorId(id);
|
|
77
|
+
// The empty string asks for automatic, and an id this version does not know means the same thing.
|
|
78
|
+
const chosen = hexFor(id) ? id : AUTOMATIC_ID;
|
|
79
|
+
workspaceHex = hexFor(chosen);
|
|
80
|
+
sessionHexes = sessionColors(chosen, AUTOMATIC_ID);
|
|
81
|
+
document.documentElement.style.setProperty("--ws", workspaceHex);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The colour the workspace is wearing, read fresh by everything that draws it. */
|
|
85
|
+
export function workspaceColor() {
|
|
86
|
+
return workspaceHex;
|
|
87
|
+
}
|
|
43
88
|
|
|
44
89
|
export function colorFor(entry) {
|
|
45
|
-
return
|
|
90
|
+
return sessionHexes[(entry.colorIndex ?? 0) % sessionHexes.length];
|
|
46
91
|
}
|
|
92
|
+
|
|
93
|
+
// Before anything is drawn, and before the socket has answered: what this browser was told last time.
|
|
94
|
+
applyWorkspaceColor(cachedColorId());
|