chamba 0.8.0 → 0.10.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/README.md +14 -3
- package/dist/commands/dev.js +11 -11
- package/dist/commands/menu.js +1 -1
- package/dist/commands/settings.js +9 -6
- package/dist/lib/awake-holder.js +3 -0
- package/dist/lib/awake-marker.js +4 -0
- package/dist/lib/awake.js +1 -0
- package/dist/lib/chamba-yaml.js +4 -4
- package/dist/lib/constants.js +1 -1
- package/dist/lib/global-config.js +3 -3
- package/dist/lib/skills.js +2 -2
- package/package.json +1 -1
- package/templates/Dockerfile +3 -3
- package/templates/context/baseline.md +2 -2
- package/templates/context/web-pane-craft.md +1 -1
- package/templates/pane-apps/client/assets/api-CguEsvNp.js +12 -0
- package/templates/pane-apps/client/assets/api-Cyoc3E6K.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/home-CLFnmg7u.css +1 -0
- package/templates/pane-apps/client/assets/home-CrhBkqDQ.js +1 -0
- package/templates/pane-apps/client/assets/pop-D5ZANBC3.mp3 +0 -0
- package/templates/pane-apps/client/assets/reviews-Bq8EwmG-.js +153 -0
- package/templates/pane-apps/client/assets/reviews-DzHtA35Q.css +1 -0
- package/templates/pane-apps/client/assets/search-DgFnnu-J.js +1 -0
- package/templates/pane-apps/client/assets/specs-JvdNFtXR.js +13 -0
- package/templates/pane-apps/client/assets/useNow-Ww-Gffnk.js +1 -0
- package/templates/pane-apps/client/home/index.html +16 -0
- package/templates/pane-apps/client/reviews/index.html +6 -4
- package/templates/pane-apps/client/specs/index.html +4 -3
- package/templates/pane-apps/server/home.mjs +11 -0
- package/templates/pane-apps/server/reviews.mjs +19 -18
- package/templates/pane-apps/server/specs.mjs +4 -4
- package/templates/skills/dx-review/SKILL.md +16 -3
- package/templates/skills/dx-review/references/acts.md +86 -15
- package/templates/skills/dx-spec/SKILL.md +11 -19
- package/templates/skills/dx-spec/references/review-guide.md +15 -9
- package/templates/skills/dx-spec/references/spec-guide.md +12 -5
- package/templates/skills/dx-spec/references/stages.md +44 -24
- package/templates/skills/dx-spec-execute/SKILL.md +78 -28
- package/templates/startup.mjs +13 -13
- package/templates/tool-helper.sh +4 -2
- package/templates/webterm/README.md +41 -17
- package/templates/webterm/awake.js +27 -0
- package/templates/webterm/config.js +30 -0
- package/templates/webterm/public/app/alerts.js +92 -11
- package/templates/webterm/public/app/composer.js +37 -3
- package/templates/webterm/public/app/connection.js +14 -0
- package/templates/webterm/public/app/dom.js +1 -0
- package/templates/webterm/public/app/frames.js +1 -1
- package/templates/webterm/public/app/home-host.js +16 -0
- package/templates/webterm/public/app/main.js +4 -2
- package/templates/webterm/public/app/pane-shape.js +5 -3
- package/templates/webterm/public/app/pane-shell.js +45 -5
- package/templates/webterm/public/app/pane.js +8 -2
- package/templates/webterm/public/app/reviews-host.js +5 -1
- package/templates/webterm/public/app/state.js +4 -0
- package/templates/webterm/public/app/tabs.js +37 -3
- package/templates/webterm/public/app/tool-host.js +19 -3
- package/templates/webterm/public/app/workspace-color.js +1 -1
- package/templates/webterm/public/index.html +5 -1
- package/templates/webterm/public/styles.css +65 -0
- package/templates/webterm/server.js +24 -1
- package/templates/webterm/tool-document.js +3 -2
- package/templates/webterm/tools/home.js +80 -0
- package/templates/webterm/tools/index.js +5 -4
- package/templates/webterm/tools/reviews.js +7 -3
- package/templates/pane-apps/client/assets/files-D0nJgFAA.js +0 -12
- package/templates/pane-apps/client/assets/files-h0K1vZyQ.css +0 -1
- package/templates/pane-apps/client/assets/reviews-BsoGZ35r.css +0 -1
- package/templates/pane-apps/client/assets/reviews-iW0Mbn4e.js +0 -153
- package/templates/pane-apps/client/assets/specs-D9z3neR-.js +0 -13
- package/templates/skills/dx-spec/references/principles-template.md +0 -2
|
@@ -35,8 +35,9 @@
|
|
|
35
35
|
// pane-shape.js the shape a pane has beside one session, and the store that keeps one for each
|
|
36
36
|
// pane.js the Pages tool: the chips bar and the page
|
|
37
37
|
// tool-host.js a tool's frame, and the credentials the shell hands into it
|
|
38
|
-
//
|
|
39
|
-
//
|
|
38
|
+
// home-host.js the Home tool: which tool it is, and its panel
|
|
39
|
+
// specs-host.js the Specs tool, the same way
|
|
40
|
+
// reviews-host.js the Code Reviews tool, the same way again
|
|
40
41
|
// pane-frame.js a published page in a sandboxed frame, and the answer that comes back out of it
|
|
41
42
|
// pane-arrival.js which page an incoming list opens by itself, and when none of them does
|
|
42
43
|
// status-strip.js the attached session's model, context, quota meters and version, above the composer
|
|
@@ -72,6 +73,7 @@ import "./dom.js";
|
|
|
72
73
|
import "./palette.js";
|
|
73
74
|
import "./drafts.js";
|
|
74
75
|
import "./frames.js";
|
|
76
|
+
import "./home-host.js";
|
|
75
77
|
import "./new-session.js";
|
|
76
78
|
import "./note.js";
|
|
77
79
|
import "./pane.js";
|
|
@@ -15,12 +15,14 @@ export const PANE_KEY = "webterm-pane";
|
|
|
15
15
|
|
|
16
16
|
// The tools there are to open, which a shape holds one of. Their ids and nothing else about them: what a tool
|
|
17
17
|
// is made of is the pane's business, and all this module asks is whether a stored id is one of them.
|
|
18
|
+
export const HOME = "home";
|
|
18
19
|
export const PAGES = "pages";
|
|
19
20
|
export const SPECS = "specs";
|
|
20
21
|
export const REVIEWS = "reviews";
|
|
21
|
-
const SHAPE_TOOLS = [PAGES, SPECS, REVIEWS];
|
|
22
|
-
// The tool a session opens on.
|
|
23
|
-
|
|
22
|
+
const SHAPE_TOOLS = [HOME, PAGES, SPECS, REVIEWS];
|
|
23
|
+
// The tool a session opens on. Home, because it is the reading of the repository a session starts in, and
|
|
24
|
+
// because a stored shape naming another tool keeps that tool: this is what a session with no shape gets.
|
|
25
|
+
const DEFAULT_TOOL = HOME;
|
|
24
26
|
|
|
25
27
|
// Narrower than this and the pane is not a pane any more, so it becomes the spine instead. The mock's
|
|
26
28
|
// threshold, and the width the spine itself takes.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// pane-shell.js - the pane itself: which tool is open in it, and how wide it is.
|
|
2
2
|
//
|
|
3
|
-
// The pane is furniture. What sits in it are tools -
|
|
4
|
-
// (reviews-host.js) - and the line between them is the whole point of this module: a tool draws inside its
|
|
3
|
+
// The pane is furniture. What sits in it are tools - Home (home-host.js), Specs (specs-host.js), Code Reviews
|
|
4
|
+
// (reviews-host.js) and Pages (pane.js) - and the line between them is the whole point of this module: a tool draws inside its
|
|
5
5
|
// own panel and nothing else, while the tab bar that says which tool is open, and the controls that say how
|
|
6
6
|
// wide the pane is, are drawn out here. A tool can therefore never move the pane, and never lie about which
|
|
7
7
|
// tool the user is looking at.
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
paneHalf,
|
|
32
32
|
spine,
|
|
33
33
|
spineBadge,
|
|
34
|
+
toolHome,
|
|
34
35
|
toolPages,
|
|
35
36
|
toolReviews,
|
|
36
37
|
toolSpecs,
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
DEFAULT_FRACTION,
|
|
41
42
|
defaultShape,
|
|
42
43
|
draggedTo,
|
|
44
|
+
HOME,
|
|
43
45
|
isDefaultWidth,
|
|
44
46
|
mergeShape,
|
|
45
47
|
PAGES,
|
|
@@ -52,14 +54,17 @@ import {
|
|
|
52
54
|
|
|
53
55
|
// The tools' ids come from the shape, which holds one of them. Re-exported here because this module is the
|
|
54
56
|
// pane, and a tool that asks whether it is open is asking the pane rather than the store.
|
|
55
|
-
export { PAGES, REVIEWS, SPECS };
|
|
57
|
+
export { HOME, PAGES, REVIEWS, SPECS };
|
|
56
58
|
|
|
57
59
|
// The tools, in the order their tabs are drawn. A tool is a tab, a panel, and a render function it hands over
|
|
58
60
|
// when it loads; the pane knows nothing else about any of them.
|
|
59
61
|
const TOOLS = [
|
|
60
|
-
{ id:
|
|
62
|
+
{ id: HOME, label: "Home", panel: toolHome },
|
|
61
63
|
{ id: SPECS, label: "Specs", panel: toolSpecs },
|
|
62
|
-
|
|
64
|
+
// "Reviews" on the tab and Code Reviews everywhere else: four tabs have to be readable at the width the
|
|
65
|
+
// pane reopens at, and the tab is the one place the tool's whole name does not fit.
|
|
66
|
+
{ id: REVIEWS, label: "Reviews", panel: toolReviews },
|
|
67
|
+
{ id: PAGES, label: "Pages", panel: toolPages },
|
|
63
68
|
];
|
|
64
69
|
|
|
65
70
|
// The one shape this window is drawing, and the session it belongs to. One and not a map of them: the store
|
|
@@ -213,6 +218,14 @@ function drawTabs() {
|
|
|
213
218
|
badge.className = mark.counted ? "tool-mark" : "tool-mark dot";
|
|
214
219
|
if (mark.counted) badge.textContent = String(mark.unread);
|
|
215
220
|
else badge.setAttribute("aria-label", "something new");
|
|
221
|
+
// The ring, for a tool something landed in a moment ago. This element is new on every redraw, so
|
|
222
|
+
// the ring would start from the beginning each time; a negative delay of what has already run
|
|
223
|
+
// puts it back where it was.
|
|
224
|
+
const ring = pulsing.get(tool.id);
|
|
225
|
+
if (ring) {
|
|
226
|
+
badge.classList.add("pulse");
|
|
227
|
+
badge.style.setProperty("--ring-delay", `-${Date.now() - ring.at}ms`);
|
|
228
|
+
}
|
|
216
229
|
button.append(badge);
|
|
217
230
|
}
|
|
218
231
|
button.addEventListener("click", () => showTool(tool.id));
|
|
@@ -337,6 +350,33 @@ export function flashPane() {
|
|
|
337
350
|
arrivalTimer = setTimeout(() => pane.classList.remove("arriving"), ARRIVAL_MS);
|
|
338
351
|
}
|
|
339
352
|
|
|
353
|
+
// Three turns of the ring in styles.css, which runs for 1.6s each. The two numbers have to agree: the
|
|
354
|
+
// keyframes are there and what takes the ring off again is here.
|
|
355
|
+
const TAB_PULSE_MS = 4_800;
|
|
356
|
+
|
|
357
|
+
// id -> when that tool's ring started, and the timer that ends it. A tab is rebuilt on every redraw, so the
|
|
358
|
+
// ring cannot be left on the element the way the pane's own flash is - what holds it is this map.
|
|
359
|
+
const pulsing = new Map();
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Something landed in a tool the user is not looking at, so its tab draws a ring around its mark for three
|
|
363
|
+
* turns and then rests.
|
|
364
|
+
*
|
|
365
|
+
* A second arrival during those three turns starts them again, which is what the mark means: the tab says
|
|
366
|
+
* that something arrived just now, and the number beside it says how much is waiting.
|
|
367
|
+
*/
|
|
368
|
+
export function pulseTool(id) {
|
|
369
|
+
clearTimeout(pulsing.get(id)?.timer);
|
|
370
|
+
pulsing.set(id, {
|
|
371
|
+
at: Date.now(),
|
|
372
|
+
timer: setTimeout(() => {
|
|
373
|
+
pulsing.delete(id);
|
|
374
|
+
renderShell();
|
|
375
|
+
}, TAB_PULSE_MS),
|
|
376
|
+
});
|
|
377
|
+
renderShell();
|
|
378
|
+
}
|
|
379
|
+
|
|
340
380
|
// --- The divider -------------------------------------------------------------------------------------------------------------------------
|
|
341
381
|
|
|
342
382
|
// Dragging sets a pixel width until it stops making sense, and below the threshold the pane becomes the
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
isToolOpen,
|
|
35
35
|
noteTool,
|
|
36
36
|
PAGES,
|
|
37
|
+
pulseTool,
|
|
37
38
|
registerTool,
|
|
38
39
|
rememberPageSize,
|
|
39
40
|
setPaneCollapsed,
|
|
@@ -91,7 +92,10 @@ export function applyPages(msg) {
|
|
|
91
92
|
// saying-so is held back while another tool has the pane.
|
|
92
93
|
if (arrived !== null) {
|
|
93
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.
|
|
94
97
|
if (isToolOpen(PAGES)) flashPane();
|
|
98
|
+
else pulseTool(PAGES);
|
|
95
99
|
}
|
|
96
100
|
// A pane someone put away comes back for a page, in the session the page was published in - which is
|
|
97
101
|
// the session this frame is about, since the list only reaches the tool for the one it is driving.
|
|
@@ -264,8 +268,10 @@ function render() {
|
|
|
264
268
|
// because a pane that only appears once an agent has used it is one nobody knows to ask for. What the
|
|
265
269
|
// empty pane says is the tool's own intro; the tab's mark is the shell's, from what the tool says here.
|
|
266
270
|
const bare = pages.length === 0 && !entry?.notice;
|
|
267
|
-
//
|
|
268
|
-
|
|
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 });
|
|
269
275
|
|
|
270
276
|
chips.replaceChildren(...pages.map((page) => chipFor(sid, page, page.id === entry?.selected)));
|
|
271
277
|
const at = pages.findIndex((page) => page.id === entry?.selected);
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
//
|
|
3
3
|
// As the Specs host beside it: the tool's own name, its panel and its opening sentence, and the frame around
|
|
4
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.
|
|
5
9
|
|
|
6
10
|
import { toolReviews } from "./dom.js";
|
|
7
11
|
import { REVIEWS } from "./pane-shell.js";
|
|
@@ -9,7 +13,7 @@ import { createToolHost } from "./tool-host.js";
|
|
|
9
13
|
|
|
10
14
|
createToolHost({
|
|
11
15
|
id: REVIEWS,
|
|
12
|
-
label: "
|
|
16
|
+
label: "Reviews",
|
|
13
17
|
panel: toolReviews,
|
|
14
18
|
opening: "Opening the reviews in this repository.",
|
|
15
19
|
});
|
|
@@ -17,6 +17,9 @@ export let workspaceName = "";
|
|
|
17
17
|
// the screen: the panel marks this, so "nothing picked" stays tellable from "picked the colour the port
|
|
18
18
|
// would have given anyway".
|
|
19
19
|
export let workspaceColorId = "";
|
|
20
|
+
// Whether the host is holding itself awake for this container. A fact about the machine the container runs
|
|
21
|
+
// on, which the server reads off a file the host writes; nothing in here can see it any other way.
|
|
22
|
+
export let hostAwake = false;
|
|
20
23
|
// Where "+ New session" starts an agent, relative to the workspace root ("" is the root itself). The server
|
|
21
24
|
// owns it - it follows the directory chamba was last run in - and the picker opens on it.
|
|
22
25
|
export let defaultCwd = "";
|
|
@@ -31,6 +34,7 @@ export function adoptSessionsFrame(msg) {
|
|
|
31
34
|
if (Array.isArray(msg.agents)) agents = msg.agents;
|
|
32
35
|
workspaceName = msg.workspace || "";
|
|
33
36
|
workspaceColorId = typeof msg.color === "string" ? msg.color : "";
|
|
37
|
+
hostAwake = msg.awake === true;
|
|
34
38
|
defaultCwd = msg.defaultCwd ?? "";
|
|
35
39
|
}
|
|
36
40
|
|
|
@@ -15,7 +15,7 @@ import { closeCard, stopCard } from "./cards.js";
|
|
|
15
15
|
import { sendFrame } from "./connection.js";
|
|
16
16
|
import { SVG_NS, tabbar } from "./dom.js";
|
|
17
17
|
import { repositionNewPop, toggleNewPop } from "./new-session.js";
|
|
18
|
-
import { agents, attachedSid, defaultAgent, defaultCwd, maxSessions, sessions, workspaceName } from "./state.js";
|
|
18
|
+
import { agents, attachedSid, defaultAgent, defaultCwd, hostAwake, maxSessions, sessions, workspaceName } from "./state.js";
|
|
19
19
|
import { focusTerminal, term } from "./terminal.js";
|
|
20
20
|
import { colorFor } from "./theme.js";
|
|
21
21
|
import { repositionColorPop, workspaceColorButton } from "./workspace-color.js";
|
|
@@ -444,6 +444,37 @@ function stopButton() {
|
|
|
444
444
|
return button;
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
+
// The host is holding itself awake for this container. A sign and not a button: there is nothing here for a
|
|
448
|
+
// window to change - the setting is the machine's, and it is changed in the chamba menu on the host.
|
|
449
|
+
function awakeIcon() {
|
|
450
|
+
const sign = document.createElement("span");
|
|
451
|
+
sign.id = "awakeicon";
|
|
452
|
+
sign.title = "The host stays awake while this container is up";
|
|
453
|
+
sign.setAttribute("role", "img");
|
|
454
|
+
sign.setAttribute("aria-label", "The host stays awake while this container is up");
|
|
455
|
+
const svg = document.createElementNS(SVG_NS, "svg");
|
|
456
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
457
|
+
svg.setAttribute("width", "14");
|
|
458
|
+
svg.setAttribute("height", "14");
|
|
459
|
+
svg.setAttribute("fill", "none");
|
|
460
|
+
svg.setAttribute("stroke", "currentColor");
|
|
461
|
+
svg.setAttribute("stroke-width", "2");
|
|
462
|
+
svg.setAttribute("stroke-linecap", "round");
|
|
463
|
+
// A sun: a small circle with eight rays, drawn as one dashed ring outside it so the rays cost one node.
|
|
464
|
+
const core = document.createElementNS(SVG_NS, "circle");
|
|
465
|
+
core.setAttribute("cx", "12");
|
|
466
|
+
core.setAttribute("cy", "12");
|
|
467
|
+
core.setAttribute("r", "4");
|
|
468
|
+
const rays = document.createElementNS(SVG_NS, "circle");
|
|
469
|
+
rays.setAttribute("cx", "12");
|
|
470
|
+
rays.setAttribute("cy", "12");
|
|
471
|
+
rays.setAttribute("r", "9");
|
|
472
|
+
rays.setAttribute("stroke-dasharray", "0.1 6.97");
|
|
473
|
+
svg.append(core, rays);
|
|
474
|
+
sign.append(svg);
|
|
475
|
+
return sign;
|
|
476
|
+
}
|
|
477
|
+
|
|
447
478
|
function chevronIcon() {
|
|
448
479
|
const svg = document.createElementNS(SVG_NS, "svg");
|
|
449
480
|
svg.setAttribute("viewBox", "0 0 24 24");
|
|
@@ -514,8 +545,11 @@ export function renderBar() {
|
|
|
514
545
|
ws.textContent = workspaceName;
|
|
515
546
|
right.append(ws);
|
|
516
547
|
}
|
|
517
|
-
// The
|
|
518
|
-
//
|
|
548
|
+
// The awake sign first, before the buttons: it is the one thing here nobody presses, and it says
|
|
549
|
+
// something about the machine this container runs on rather than about this window or this session.
|
|
550
|
+
if (hostAwake) right.append(awakeIcon());
|
|
551
|
+
// The colour next: it is about this window's identity rather than about what the window does, so it sits
|
|
552
|
+
// at the near end of the buttons, on the far side of the bell from the one that ends everything.
|
|
519
553
|
right.append(workspaceColorButton(), bellButton(), stopButton());
|
|
520
554
|
tabbar.append(right);
|
|
521
555
|
|
|
@@ -47,6 +47,17 @@ export function toolsMovedToSession() {
|
|
|
47
47
|
for (const host of hosts.values()) host.session();
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// Whether the bell in the bar is on. It rides in the session message, so a tool that makes a sound obeys the one
|
|
51
|
+
// control this window has rather than a mute of its own. Held here rather than read out of `alerts.js`, so the flow
|
|
52
|
+
// is one way: the bell says it moved, and every frame hears it in the message it already reads.
|
|
53
|
+
let toolSound = true;
|
|
54
|
+
|
|
55
|
+
/** The bell moved. Every frame hears the new preference in its next session message, which is sent now. */
|
|
56
|
+
export function toolsHearSound(on) {
|
|
57
|
+
toolSound = on;
|
|
58
|
+
toolsMovedToSession();
|
|
59
|
+
}
|
|
60
|
+
|
|
50
61
|
/**
|
|
51
62
|
* Put one tool on the page.
|
|
52
63
|
*
|
|
@@ -174,17 +185,22 @@ export function createToolHost({ id, label, panel, opening }) {
|
|
|
174
185
|
}
|
|
175
186
|
|
|
176
187
|
/**
|
|
177
|
-
* Tell one frame which session this window is driving,
|
|
188
|
+
* Tell one frame which session this window is driving, how that session reads, and whether this window
|
|
189
|
+
* makes a sound. `null` says there is no session.
|
|
178
190
|
*
|
|
179
191
|
* The theme rides with the session because it belongs to it: the frame has an opaque origin and no storage
|
|
180
|
-
* of its own, so what it was set to last is the pane's to keep and the pane's to hand back.
|
|
192
|
+
* of its own, so what it was set to last is the pane's to keep and the pane's to hand back. The sound
|
|
193
|
+
* preference rides in the same message because it reaches a frame the same way and moves the same way.
|
|
181
194
|
*/
|
|
182
195
|
function sendSession(target) {
|
|
183
196
|
const sid = attachedSid ?? null;
|
|
184
197
|
// Both fields say the same session or neither does: the pane is drawn in the session it opened with
|
|
185
198
|
// before the first frame names one, and handing the frame that session's palette beside "there is no
|
|
186
199
|
// session" would have it read in a theme it was never told the reason for.
|
|
187
|
-
target?.contentWindow?.postMessage(
|
|
200
|
+
target?.contentWindow?.postMessage(
|
|
201
|
+
{ t: named(SESSION), sid, theme: sid === null ? null : storedPaneTheme(), sound: toolSound },
|
|
202
|
+
"*",
|
|
203
|
+
);
|
|
188
204
|
}
|
|
189
205
|
|
|
190
206
|
/**
|
|
@@ -75,7 +75,7 @@ function cell(id, hex, label, stored) {
|
|
|
75
75
|
button.addEventListener("click", () => {
|
|
76
76
|
panel.close();
|
|
77
77
|
// Sent, never held: the server decides, and every window is told on the frame that comes back. A pick
|
|
78
|
-
// kept to send later would be undone by the next sessions frame anyway
|
|
78
|
+
// kept to send later would be undone by the next sessions frame anyway.
|
|
79
79
|
sendFrame({ t: "color", color: id });
|
|
80
80
|
});
|
|
81
81
|
return button;
|
|
@@ -65,9 +65,13 @@
|
|
|
65
65
|
</span>
|
|
66
66
|
</div>
|
|
67
67
|
|
|
68
|
+
<!-- Home: the pulse of the repository this workspace holds, in a frame of its own. The tool a
|
|
69
|
+
session with no stored shape opens on, so it is the one panel without `hidden`. -->
|
|
70
|
+
<div id="tool-home" class="tool-panel"></div>
|
|
71
|
+
|
|
68
72
|
<!-- Pages: the pages this session's agent published, with the chips bar and the counter drawn out
|
|
69
73
|
here by the interface, and the page itself inside a sandboxed frame. -->
|
|
70
|
-
<div id="tool-pages" class="tool-panel">
|
|
74
|
+
<div id="tool-pages" class="tool-panel" hidden>
|
|
71
75
|
<div id="pagebar">
|
|
72
76
|
<button type="button" id="pane-prev" class="pane-nav" title="Older pages" aria-label="Older pages" hidden>‹</button>
|
|
73
77
|
<div id="chips"></div>
|
|
@@ -760,6 +760,44 @@ body.dragging {
|
|
|
760
760
|
font-weight: 700;
|
|
761
761
|
}
|
|
762
762
|
|
|
763
|
+
/* Something landed in a tool the user is not looking at: a ring around its mark, three times, then rest.
|
|
764
|
+
TAB_PULSE_MS in app/pane-shell.js must match three turns of this - it is what takes the class off again,
|
|
765
|
+
since a tab, unlike the pane, is rebuilt from one frame to the next. The delay is how far a rebuilt tab is
|
|
766
|
+
already into the ring, so a redraw in the middle of a pulse resumes it. */
|
|
767
|
+
.tool-mark.pulse {
|
|
768
|
+
position: relative;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.tool-mark.pulse::after {
|
|
772
|
+
content: "";
|
|
773
|
+
position: absolute;
|
|
774
|
+
inset: -4px;
|
|
775
|
+
border: 2px solid var(--accent);
|
|
776
|
+
border-radius: 12px;
|
|
777
|
+
animation: toolMarkRing 1.6s ease-out 3;
|
|
778
|
+
animation-delay: var(--ring-delay, 0s);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
@keyframes toolMarkRing {
|
|
782
|
+
from {
|
|
783
|
+
opacity: 0.9;
|
|
784
|
+
transform: scale(0.8);
|
|
785
|
+
}
|
|
786
|
+
to {
|
|
787
|
+
opacity: 0;
|
|
788
|
+
transform: scale(1.5);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/* The same moment, held instead of pulsed, the way the pane's own arrival highlight is. Without this the
|
|
793
|
+
ring would never appear at all for someone who asked for less movement. */
|
|
794
|
+
@media (prefers-reduced-motion: reduce) {
|
|
795
|
+
.tool-mark.pulse::after {
|
|
796
|
+
opacity: 0.9;
|
|
797
|
+
animation: none;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
763
801
|
/* A tool that counts nothing says only that something arrived, so its mark carries no number and is drawn
|
|
764
802
|
as the dot it means. */
|
|
765
803
|
.tool-mark.dot {
|
|
@@ -1329,6 +1367,19 @@ body.offline #wscolorpop {
|
|
|
1329
1367
|
pointer-events: none;
|
|
1330
1368
|
}
|
|
1331
1369
|
|
|
1370
|
+
/* ---- The host is being kept awake ---- */
|
|
1371
|
+
|
|
1372
|
+
/* A sign rather than a control: no border, no hover, no pointer. It sits among buttons, so it has to read as
|
|
1373
|
+
the one thing here that nothing happens to when it is pressed. */
|
|
1374
|
+
#awakeicon {
|
|
1375
|
+
display: flex;
|
|
1376
|
+
align-items: center;
|
|
1377
|
+
justify-content: center;
|
|
1378
|
+
padding: 4px;
|
|
1379
|
+
color: var(--muted);
|
|
1380
|
+
opacity: 0.75;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1332
1383
|
/* ---- Sound, and stopping the container ---- */
|
|
1333
1384
|
|
|
1334
1385
|
/* Both sit at the far right of the bar, away from the tabs: neither one is about a single session. */
|
|
@@ -1493,6 +1544,20 @@ body.offline #wscolorpop {
|
|
|
1493
1544
|
/* Border carries the state (no separate status dot): lit in the workspace's colour when you can type into the
|
|
1494
1545
|
attached session, plain grey while this window is between sessions or reconnecting. A dropped connection is
|
|
1495
1546
|
not a failure here - the session on the other side kept running - so it goes quiet rather than warning. */
|
|
1547
|
+
/* The ruler autoGrow() measures the text in: out of the flow, out of sight, and of no height of its own, so
|
|
1548
|
+
its scroll height is the height of the text and nothing about it moves the composer. */
|
|
1549
|
+
#input-mirror {
|
|
1550
|
+
position: absolute;
|
|
1551
|
+
top: 0;
|
|
1552
|
+
left: 0;
|
|
1553
|
+
height: 0;
|
|
1554
|
+
min-height: 0;
|
|
1555
|
+
overflow: hidden;
|
|
1556
|
+
visibility: hidden;
|
|
1557
|
+
pointer-events: none;
|
|
1558
|
+
resize: none;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1496
1561
|
#input.online {
|
|
1497
1562
|
border-color: var(--ws);
|
|
1498
1563
|
box-shadow: 0 0 0 1px color-mix(in srgb, var(--ws) 25%, transparent);
|
|
@@ -18,11 +18,14 @@ import { join, resolve, sep } from "node:path";
|
|
|
18
18
|
import express from "express";
|
|
19
19
|
import { WebSocketServer } from "ws";
|
|
20
20
|
import { cleanLine, cleanSubmission, createPaneStore, isPageId } from "./artifacts.js";
|
|
21
|
+
import { isHostAwake } from "./awake.js";
|
|
21
22
|
import { readColor, storeColor } from "./color.js";
|
|
22
23
|
import {
|
|
23
24
|
AGENT_ARGS,
|
|
24
25
|
AGENT_STORES,
|
|
25
26
|
AGENTS,
|
|
27
|
+
AWAKE_FILE,
|
|
28
|
+
AWAKE_STALE_MS,
|
|
26
29
|
agentSpawnArgv,
|
|
27
30
|
CHECK_INTERVAL_MS,
|
|
28
31
|
CLIENT_PING_INTERVAL_MS,
|
|
@@ -171,6 +174,10 @@ function sessionAgent(raw) {
|
|
|
171
174
|
// nothing was picked, and the browser works one out from the published port.
|
|
172
175
|
let chosenColor = readColor(COLOR_FILE);
|
|
173
176
|
|
|
177
|
+
// Whether the host is being kept awake for this container. A fact about the machine out there rather than
|
|
178
|
+
// anything this server does, read off the marker on the status timer below and carried on the sessions frame.
|
|
179
|
+
let hostAwake = isHostAwake(AWAKE_FILE, AWAKE_STALE_MS);
|
|
180
|
+
|
|
174
181
|
// The one-line file that names the default agent, for the `webterm` launcher to read back. Written when the
|
|
175
182
|
// port is bound and again whenever the default moves, so a second `webterm <agent>` can say what is live.
|
|
176
183
|
function publishAgent() {
|
|
@@ -501,6 +508,9 @@ function broadcastSessions() {
|
|
|
501
508
|
// The accent colour, on the one frame that already reaches every window: a change has to travel
|
|
502
509
|
// to all of them either way, so this closes the door rather than adding a second one.
|
|
503
510
|
color: chosenColor,
|
|
511
|
+
// The same reasoning as the colour: window-wide, true for every session in here at once, and
|
|
512
|
+
// already travelling to all of them, so it rides this frame rather than opening a second door.
|
|
513
|
+
awake: hostAwake,
|
|
504
514
|
max: MAX_SESSIONS,
|
|
505
515
|
defaultCwd: workspaceLabel(defaultCwd),
|
|
506
516
|
list,
|
|
@@ -691,6 +701,16 @@ function sweepStatus() {
|
|
|
691
701
|
}
|
|
692
702
|
}
|
|
693
703
|
|
|
704
|
+
// The marker read, on the same pass. Nothing in the container hears a write the host made to the mount, so
|
|
705
|
+
// looking again is the whole mechanism - and a frame goes out only when the answer changed, because every
|
|
706
|
+
// window already has the last one.
|
|
707
|
+
function sweepAwake() {
|
|
708
|
+
const awake = isHostAwake(AWAKE_FILE, AWAKE_STALE_MS);
|
|
709
|
+
if (awake === hostAwake) return;
|
|
710
|
+
hostAwake = awake;
|
|
711
|
+
broadcastSessions();
|
|
712
|
+
}
|
|
713
|
+
|
|
694
714
|
// What the host asks before it offers to stop the container. `sessions` is how many conversations are
|
|
695
715
|
// alive (they all die with the container), `attached` how many a window is watching right now, `agent` the
|
|
696
716
|
// one a new session gets and `agents` what is actually running, per agent. Counts and names only - nothing
|
|
@@ -1485,7 +1505,10 @@ setInterval(() => pane.sweep(), PANE_SCAN_MS).unref();
|
|
|
1485
1505
|
|
|
1486
1506
|
// The strip's scan, for the same reason: a snapshot file is written by a shell script into a bind mount, so
|
|
1487
1507
|
// the only way to know it moved is to look.
|
|
1488
|
-
setInterval(() =>
|
|
1508
|
+
setInterval(() => {
|
|
1509
|
+
sweepStatus();
|
|
1510
|
+
sweepAwake();
|
|
1511
|
+
}, STATUS_SCAN_MS).unref();
|
|
1489
1512
|
|
|
1490
1513
|
// Keepalive pass. A window that stops answering is terminated, which releases the session it was
|
|
1491
1514
|
// driving so the window that comes back can pick it up without a takeover prompt. Sessions themselves
|
|
@@ -46,8 +46,8 @@ export function withNonce(html, nonce) {
|
|
|
46
46
|
*
|
|
47
47
|
* `default-src 'none'` is the floor, so anything not named below is refused. What runs is named by the nonce
|
|
48
48
|
* alone. What is fetched is named by `origin` - the scheme and host the document was loaded from - because
|
|
49
|
-
* the client's own assets, the tool's data routes and the jailed workspace files
|
|
50
|
-
* and an opaque origin has no `'self'` to say that with. `base-uri` and `form-action` are named on their own,
|
|
49
|
+
* the client's own assets, its images and its clips, the tool's data routes and the jailed workspace files
|
|
50
|
+
* are all served from here, and an opaque origin has no `'self'` to say that with. `base-uri` and `form-action` are named on their own,
|
|
51
51
|
* since neither one falls back to `default-src`.
|
|
52
52
|
*/
|
|
53
53
|
export function documentPolicy(nonce, origin) {
|
|
@@ -56,6 +56,7 @@ export function documentPolicy(nonce, origin) {
|
|
|
56
56
|
`script-src 'nonce-${nonce}'`,
|
|
57
57
|
`style-src-elem 'nonce-${nonce}'`,
|
|
58
58
|
`img-src ${origin}`,
|
|
59
|
+
`media-src ${origin}`,
|
|
59
60
|
`connect-src ${origin}`,
|
|
60
61
|
`frame-src ${origin}`,
|
|
61
62
|
"base-uri 'none'",
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// =========================================================================================================================================
|
|
2
|
+
// tools/home.js - What the Home tool is, as one table.
|
|
3
|
+
//
|
|
4
|
+
// The smallest table of the three, and the shape of a tool that only reads. There are no verbs, because Home has no helper and nothing
|
|
5
|
+
// to say to an agent; no events, because it delivers nothing into a session; and no raw side, because it serves no file of the
|
|
6
|
+
// workspace at all. What is left is three reads of the repository the workspace holds.
|
|
7
|
+
//
|
|
8
|
+
// The module is handed two directories rather than one: the workspace, which is the repository it draws, and the state directory
|
|
9
|
+
// outside it, which is where its cache goes. The cache is derived from the repository and is nobody's record, so a file of it in the
|
|
10
|
+
// workspace would show up in the `git status` of every repository Home opens. The module refuses a cache directory inside the
|
|
11
|
+
// workspace, so the two can never be the same place.
|
|
12
|
+
// =========================================================================================================================================
|
|
13
|
+
|
|
14
|
+
import { createHomeModule, refuseRaw, statusFor } from "../../pane-apps/server/home.mjs";
|
|
15
|
+
import {
|
|
16
|
+
HOME_CACHE_DIR,
|
|
17
|
+
HOME_KEY,
|
|
18
|
+
HOME_RAW_TOKEN,
|
|
19
|
+
MAX_HOME_BYTES,
|
|
20
|
+
REVIEWS_ROOT,
|
|
21
|
+
SPECS_ROOT,
|
|
22
|
+
WORKSPACE,
|
|
23
|
+
WORKSPACE_ROOT,
|
|
24
|
+
} from "../config.js";
|
|
25
|
+
|
|
26
|
+
// The module maker, re-exported so the path to the built dist is written once, as it is for every tool.
|
|
27
|
+
export { createHomeModule };
|
|
28
|
+
|
|
29
|
+
export const HOME_TABLE = {
|
|
30
|
+
id: "home",
|
|
31
|
+
/** What a refusal calls the tool. The word on the tab is the client's own, in `tool-host.js`. */
|
|
32
|
+
label: "Home",
|
|
33
|
+
key: HOME_KEY,
|
|
34
|
+
rawToken: HOME_RAW_TOKEN,
|
|
35
|
+
statusFor,
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The repository this draws, the directory outside it that the cache goes in, the name to call it, and the two directories the
|
|
39
|
+
* other tools keep their work in.
|
|
40
|
+
*
|
|
41
|
+
* The name is chamba's own for this workspace, which is what the bar wears, so the header row of the page and the corner of the
|
|
42
|
+
* window say the same word. Without it the header would show the mount point, which is "workspace" in every container there is.
|
|
43
|
+
*
|
|
44
|
+
* The two roots are the ones the Specs tool and the Code Reviews tool are mounted with, and Home leaves what is inside them out of
|
|
45
|
+
* every count of lines and of files, with a switch on the page to put them back. Both tools write what they serve - a spec's forms
|
|
46
|
+
* and answers, a review's grouping and its baseline blobs - and counted as code they say a workspace is mostly JSON. They come from
|
|
47
|
+
* here rather than from inside Home for the same reason each tool takes its own root here: a repository that keeps either one
|
|
48
|
+
* elsewhere is served the same way, and Home holds no word about what they are called.
|
|
49
|
+
*/
|
|
50
|
+
create: () =>
|
|
51
|
+
createHomeModule({
|
|
52
|
+
root: WORKSPACE_ROOT,
|
|
53
|
+
cacheDir: HOME_CACHE_DIR,
|
|
54
|
+
name: WORKSPACE,
|
|
55
|
+
toolPaths: [SPECS_ROOT, REVIEWS_ROOT],
|
|
56
|
+
}),
|
|
57
|
+
|
|
58
|
+
/** Home delivers nothing into a session, so there is no sentence for one to arrive as. */
|
|
59
|
+
events: {},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* How much a request may carry. Every route is a GET with no body, so nothing reaches this; it is here because the door that
|
|
63
|
+
* gates a body reads it for every tool, and a tool with no number is a tool with no gate.
|
|
64
|
+
*/
|
|
65
|
+
limits: { bytes: MAX_HOME_BYTES },
|
|
66
|
+
|
|
67
|
+
/** Home serves no workspace bytes at all, so every path is refused rather than resolved. */
|
|
68
|
+
raw: (_module, path) => refuseRaw(path),
|
|
69
|
+
|
|
70
|
+
// The three reads, each one a section of the page. Three and not one: each costs what its own question costs, and the page draws
|
|
71
|
+
// each section the moment that section's answer lands.
|
|
72
|
+
routes: (module) => ({
|
|
73
|
+
history: { GET: () => module.history() },
|
|
74
|
+
branches: { GET: () => module.branches() },
|
|
75
|
+
tree: { GET: () => module.tree() },
|
|
76
|
+
}),
|
|
77
|
+
|
|
78
|
+
/** No verbs. The helper door is registered for every tool and answers 400 for every name it is given. */
|
|
79
|
+
verbs: () => ({}),
|
|
80
|
+
};
|
|
@@ -5,15 +5,16 @@
|
|
|
5
5
|
// types and how much of the typing channel one session gets. server.js reads the list and never a tool's name, so a third tool is
|
|
6
6
|
// a row here and a table beside it.
|
|
7
7
|
//
|
|
8
|
-
// The order is the order the tabs are drawn in, which is the pane's own list in `public/app/pane-shell.js
|
|
9
|
-
//
|
|
10
|
-
// browser and this file is read in the server.
|
|
8
|
+
// The order is the order the tabs are drawn in, which is the pane's own list in `public/app/pane-shell.js`, less Pages: the
|
|
9
|
+
// interface draws that one itself and it has no table here. The two lists are checked against each other by the interface's tests
|
|
10
|
+
// rather than derived from one another, because the pane is drawn in the browser and this file is read in the server.
|
|
11
11
|
// =========================================================================================================================================
|
|
12
12
|
|
|
13
|
+
import { HOME_TABLE } from "./home.js";
|
|
13
14
|
import { REVIEWS_TABLE } from "./reviews.js";
|
|
14
15
|
import { SPECS_TABLE } from "./specs.js";
|
|
15
16
|
|
|
16
|
-
export const TOOLS = [SPECS_TABLE, REVIEWS_TABLE];
|
|
17
|
+
export const TOOLS = [HOME_TABLE, SPECS_TABLE, REVIEWS_TABLE];
|
|
17
18
|
|
|
18
19
|
/** The table of one tool, or undefined. Asked with `find`, so a name that is not a tool's is not one. */
|
|
19
20
|
export function toolTable(id) {
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// The same shape as the Specs table beside it, and the differences are the tool's own. A review is a directory with a slug, so its
|
|
5
5
|
// routes name their subject in the path rather than in a query, and the acts of the screen are one route with the act as a segment.
|
|
6
6
|
// Its raw files are two named locations rather than any workspace file, so a browser reaches a stored blob and a signature and
|
|
7
|
-
// nothing else. And it is the
|
|
8
|
-
// well as its own root: every git call it makes runs with the repository as its working directory.
|
|
7
|
+
// nothing else. And it is one of the two modules in this container that run a child process, Home being the other, which is why it
|
|
8
|
+
// is handed the workspace as well as its own root: every git call it makes runs with the repository as its working directory. The
|
|
9
|
+
// runner itself is shared, in the pane-apps server library, and this module hands it a closed table of read-only operations of its own.
|
|
9
10
|
//
|
|
10
11
|
// The framed sentences are the three acts that keep the review with the reviewer. Sending feedback, syncing and filing a review all
|
|
11
12
|
// hand it over, and each of those types the review's command instead: the act starts the agent, and the skill reads the review's
|
|
@@ -111,7 +112,7 @@ export const REVIEWS_TABLE = {
|
|
|
111
112
|
"review/:slug/blob": {
|
|
112
113
|
GET: ({ params, query }) => module.blobLines(params.slug, field(query.name), count(query.from), count(query.count)),
|
|
113
114
|
},
|
|
114
|
-
// One route for every act of
|
|
115
|
+
// One route for every act of a review. The module holds the list of them and refuses a name that is not one, so what
|
|
115
116
|
// an act is stays in one place rather than being a row here as well.
|
|
116
117
|
"review/:slug/:act": { POST: ({ params, body }) => module.act(params.slug, params.act, body) },
|
|
117
118
|
}),
|
|
@@ -125,6 +126,9 @@ export const REVIEWS_TABLE = {
|
|
|
125
126
|
// A review filed from the terminal, which the tab shows with a note saying so.
|
|
126
127
|
start: (_entry, payload) => module.start(payload, "terminal"),
|
|
127
128
|
metadata: (entry, payload) => module.metadata(entry, payload),
|
|
129
|
+
// One group at a time, which several workers may post at once without treading on one another.
|
|
130
|
+
group: (entry, payload) => module.group(entry, payload),
|
|
131
|
+
ungroup: (entry, payload) => module.ungroup(entry, payload),
|
|
128
132
|
reply: (entry, payload) => module.reply(entry, payload),
|
|
129
133
|
activity: (entry, payload) => module.activity(entry, payload),
|
|
130
134
|
done: (entry) => module.done(entry),
|