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
|
@@ -42,6 +42,12 @@ export const RESUME_STAMP = process.env.WEBTERM_RESUME_STAMP || "/home/devuser/.
|
|
|
42
42
|
// chamba reads the same file when it starts the interface at the next container start.
|
|
43
43
|
export const STATE_FILE = process.env.WEBTERM_STATE_FILE || "/home/devuser/.chamba-webterm/agent";
|
|
44
44
|
|
|
45
|
+
// One-line file holding the id of the accent colour the user picked for this workspace, written when they
|
|
46
|
+
// pick one and deleted when they ask for automatic. Empty means nothing was picked, and the browser works the
|
|
47
|
+
// colour out from the published port instead. Sits beside STATE_FILE in the same mounted directory, so the
|
|
48
|
+
// choice outlives the container: a rebuild replaces everything except what is kept out here.
|
|
49
|
+
export const COLOR_FILE = process.env.WEBTERM_COLOR_FILE || "/home/devuser/.chamba-webterm/color";
|
|
50
|
+
|
|
45
51
|
// The mounted workspace, and the only tree a session may be started in. Every directory the interface
|
|
46
52
|
// deals with is this one or something under it.
|
|
47
53
|
export const WORKSPACE_ROOT = "/workspace";
|
|
@@ -75,11 +81,12 @@ export const MAX_PANE_FILES = Number(process.env.WEBTERM_MAX_PANE_FILES) || 5_00
|
|
|
75
81
|
// every host.
|
|
76
82
|
export const PANE_SCAN_MS = Number(process.env.WEBTERM_PANE_SCAN_MS) || 1_500;
|
|
77
83
|
|
|
78
|
-
// --- The
|
|
84
|
+
// --- The pane's tools --------------------------------------------------------------------------------------------------------------------
|
|
79
85
|
//
|
|
80
|
-
// The
|
|
81
|
-
// baked beside webterm
|
|
82
|
-
// credentials that open them.
|
|
86
|
+
// The tools beside the terminal, whose clients and server modules are built from the pane-apps package and
|
|
87
|
+
// baked beside webterm. This side names where they sit, what each one is pointed at, and the two scoped
|
|
88
|
+
// credentials that open each of them. What a tool is otherwise - its routes, its verbs, its sentences - is a
|
|
89
|
+
// table of its own in `tools/`.
|
|
83
90
|
|
|
84
91
|
// Where the built tool clients and server modules sit: a sibling of this directory, which is true both of the
|
|
85
92
|
// image's bake and of a checkout, where chamba's build stages the same dist.
|
|
@@ -118,6 +125,50 @@ export const MAX_SPECS_LINES_PER_SESSION = Number(process.env.WEBTERM_MAX_SPECS_
|
|
|
118
125
|
// its prompt, and a paste that arrives before it does is read by the terminal rather than by the agent.
|
|
119
126
|
export const SPECS_NEW_SESSION_DELAY_MS = Number(process.env.WEBTERM_SPECS_NEW_SESSION_DELAY_MS) || 3_000;
|
|
120
127
|
|
|
128
|
+
// The directory of reviews the second tool serves, beside the specs of the first. A mount parameter for the
|
|
129
|
+
// same reason: a repository keeps its reviews where it likes, and the tool is pointed at that directory.
|
|
130
|
+
export const REVIEWS_ROOT = process.env.WEBTERM_REVIEWS_ROOT || join(WORKSPACE_ROOT, "reviews");
|
|
131
|
+
|
|
132
|
+
// This tool's own two credentials, made the same way and opening nothing of the other's. One key per tool
|
|
133
|
+
// rather than one for the pane: a rendering bug inside one frame then leaks that tool's reading and no more.
|
|
134
|
+
export const REVIEWS_KEY = process.env.WEBTERM_REVIEWS_KEY || randomBytes(16).toString("hex");
|
|
135
|
+
export const REVIEWS_RAW_TOKEN = process.env.WEBTERM_REVIEWS_RAW_TOKEN || randomBytes(16).toString("hex");
|
|
136
|
+
|
|
137
|
+
// The largest body a Code Reviews data route accepts. Smaller than the Specs one by a lot: the biggest thing
|
|
138
|
+
// a review posts is an approval, which carries one signature drawn in a browser, and a page of hunks travels
|
|
139
|
+
// the other way. Nothing a reviewer sends is a file.
|
|
140
|
+
export const MAX_REVIEWS_BYTES = 4 * 1024 * 1024;
|
|
141
|
+
|
|
142
|
+
// What a Code Reviews delivery may be. The three bounds are the Specs ones, for the reasons written above
|
|
143
|
+
// them: a line is refused rather than trimmed, a gap is what a person looks like, and a session accepts only
|
|
144
|
+
// so many deliveries for as long as this container runs.
|
|
145
|
+
export const MAX_REVIEWS_LINE_LENGTH = Number(process.env.WEBTERM_MAX_REVIEWS_LINE_LENGTH) || 600;
|
|
146
|
+
export const REVIEWS_LINE_MIN_INTERVAL_MS = Number(process.env.WEBTERM_REVIEWS_LINE_MIN_INTERVAL_MS) || 1_000;
|
|
147
|
+
export const MAX_REVIEWS_LINES_PER_SESSION = Number(process.env.WEBTERM_MAX_REVIEWS_LINES_PER_SESSION) || 500;
|
|
148
|
+
|
|
149
|
+
// And the wait for a session this delivery had to start, which is about the agent CLI rather than the tool.
|
|
150
|
+
export const REVIEWS_NEW_SESSION_DELAY_MS = Number(process.env.WEBTERM_REVIEWS_NEW_SESSION_DELAY_MS) || 3_000;
|
|
151
|
+
|
|
152
|
+
// --- Home --------------------------------------------------------------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
// Where the Home tool keeps what it worked out about the repository. Its numbers are all derived and belong
|
|
155
|
+
// to nobody, so they are not the work: a file of them in the workspace would show up in the `git status` of
|
|
156
|
+
// every repository Home opens, and a file of them in the container would die with it. This directory is bind-
|
|
157
|
+
// mounted from the host workspace cache, beside STATE_FILE, so the cache outlives a rebuild. The module
|
|
158
|
+
// refuses a directory inside the workspace, so the two can never be the same place.
|
|
159
|
+
export const HOME_CACHE_DIR = process.env.WEBTERM_HOME_CACHE_DIR || "/home/devuser/.chamba-webterm";
|
|
160
|
+
|
|
161
|
+
// This tool's own two credentials, made the same way and opening nothing of the other tools'. Home serves no
|
|
162
|
+
// workspace file at all, so the raw token opens nothing; it is issued all the same, because the raw door is
|
|
163
|
+
// registered for every tool and a tool with no token would be a tool with no door to refuse at.
|
|
164
|
+
export const HOME_KEY = process.env.WEBTERM_HOME_KEY || randomBytes(16).toString("hex");
|
|
165
|
+
export const HOME_RAW_TOKEN = process.env.WEBTERM_HOME_RAW_TOKEN || randomBytes(16).toString("hex");
|
|
166
|
+
|
|
167
|
+
// The largest body a Home data route accepts. Every route is a GET with no body, so this is the size of the
|
|
168
|
+
// nothing they carry; the door that gates a body reads it for every tool, and a tool with no number is a tool
|
|
169
|
+
// with no gate.
|
|
170
|
+
export const MAX_HOME_BYTES = 64 * 1024;
|
|
171
|
+
|
|
121
172
|
// --- The status strip --------------------------------------------------------------------------------------------------------------------
|
|
122
173
|
|
|
123
174
|
// How often a session's snapshot file is looked at again for the strip above the composer. A poll for the
|
|
@@ -11,4 +11,3 @@ Use it for anything longer or more structured than a terminal reply carries well
|
|
|
11
11
|
The `web-pane` skill is how to write one well; read it before your first page.
|
|
12
12
|
|
|
13
13
|
**The status strip.** Above the composer the user can already see this session's model, the context it is holding, and what is left of the quota, so there is no need to report any of it unasked.
|
|
14
|
-
Run `context-usage` when you want the same numbers yourself.
|
|
@@ -8,7 +8,8 @@ import { SVG_NS } from "./dom.js";
|
|
|
8
8
|
import { sessions, workspaceName } from "./state.js";
|
|
9
9
|
import { arrivedAt, renderBar } from "./tabs.js";
|
|
10
10
|
import { focusTerminal } from "./terminal.js";
|
|
11
|
-
import {
|
|
11
|
+
import { workspaceColor } from "./theme.js";
|
|
12
|
+
import { toolsHearSound } from "./tool-host.js";
|
|
12
13
|
|
|
13
14
|
// --- The browser's own tab ---------------------------------------------------------------------------------------------------------------
|
|
14
15
|
//
|
|
@@ -20,6 +21,12 @@ import { WORKSPACE_COLOR } from "./theme.js";
|
|
|
20
21
|
// nothing - nothing is happening in here
|
|
21
22
|
// white bar across the bottom - an agent is working, with a lit segment moving along it
|
|
22
23
|
// green dot in the top corner - an agent finished and is waiting for you, pulsing until you go and look
|
|
24
|
+
// grey all through, and still - this window cannot reach its container, so it says nothing else at all
|
|
25
|
+
//
|
|
26
|
+
// The grey one is the odd member of the four, because it is about this window rather than about the container.
|
|
27
|
+
// Every other mark stands on the last sessions frame the server sent, and while the socket is down that frame
|
|
28
|
+
// only gets older - so a green dot calls you back to a session this window can no longer see. Draining the
|
|
29
|
+
// icon says the one thing that is still true, and the title and the chime below are held to the same rule.
|
|
23
30
|
//
|
|
24
31
|
// One mark at a time, and the two are told apart by where they sit before any colour is read: working owns the
|
|
25
32
|
// bottom edge, waiting owns the top-right corner. That was the lesson of the first version, where both were a
|
|
@@ -27,8 +34,8 @@ import { WORKSPACE_COLOR } from "./theme.js";
|
|
|
27
34
|
// are the same dot. Green outranks white, since something that wants you matters more than something still going.
|
|
28
35
|
//
|
|
29
36
|
// Working is white rather than a colour on purpose. Every hue in this interface belongs to a workspace or to a
|
|
30
|
-
// session, and the frame around this very icon is one of
|
|
31
|
-
//
|
|
37
|
+
// session, and the frame around this very icon is one of the eleven the user can choose from - so a blue mark
|
|
38
|
+
// sat inside a blue frame stopped reading as a mark at all. White belongs to nobody, and it leaves green as
|
|
32
39
|
// the only hue in the icon that means anything, which is what makes the pair read as a traffic light.
|
|
33
40
|
//
|
|
34
41
|
// Both marks move, and neither leans on a particular frame to be understood. A hidden tab is exactly the tab this
|
|
@@ -50,10 +57,27 @@ const PULSE_TICKS = 3;
|
|
|
50
57
|
const SWEEP_TICKS = 16;
|
|
51
58
|
const DONE_COLOR = "#2fe58a";
|
|
52
59
|
const BUSY_COLOR = "#e6edf3";
|
|
60
|
+
// The offline pair: the interface's own muted text grey for the spark, and its border grey for the frame. Flat,
|
|
61
|
+
// with no gradient - at 16px a gradient between two greys says nothing. Anything fainter than this reads as a
|
|
62
|
+
// broken tab rather than an offline one, which is a different thing to tell someone.
|
|
63
|
+
const OFFLINE_SPARK = "#8b949e";
|
|
64
|
+
const OFFLINE_FRAME = "#30363d";
|
|
65
|
+
// How long the socket has to stay down before the icon says so. Most disconnects are a sleeping laptop or a wifi
|
|
66
|
+
// blip and are over inside this, so the icon holds still rather than flickering grey and back. The curtain waits
|
|
67
|
+
// the same three seconds, and the two waits cannot be shared: the curtain's is measured from the end of a /status
|
|
68
|
+
// request that has three seconds of its own, and it is skipped entirely for a window that has stopped trying.
|
|
69
|
+
// It is not only the icon's: the chime below reads the same flag, so lengthening this leaves a window sounding
|
|
70
|
+
// for a container it cannot reach for that much longer.
|
|
71
|
+
const OFFLINE_GRACE_MS = 3_000;
|
|
53
72
|
|
|
54
73
|
let iconCanvas = null;
|
|
55
74
|
let iconTimer = null;
|
|
56
75
|
let iconFrame = 0;
|
|
76
|
+
// Whether this window has given up on reaching its container, and the grace that is counting towards it. A
|
|
77
|
+
// slower thing than the `offline` class on the body, which goes on at the first close: that one stops the page
|
|
78
|
+
// taking clicks it cannot answer, and this one waits to see whether there is anything worth saying.
|
|
79
|
+
let offline = false;
|
|
80
|
+
let offlineTimer = null;
|
|
57
81
|
|
|
58
82
|
// What the dot should say right now, or null for no dot. Read fresh on every paint rather than passed in, so a
|
|
59
83
|
// paint from anywhere - an incoming bar, or the pulse - draws what is true now.
|
|
@@ -85,7 +109,10 @@ function paintFavicon() {
|
|
|
85
109
|
iconCanvas.height = ICON_SIZE;
|
|
86
110
|
const ctx = iconCanvas.getContext("2d");
|
|
87
111
|
if (!ctx) return;
|
|
88
|
-
|
|
112
|
+
// No mark at all while the window cannot reach its container, whatever the last frame said was happening:
|
|
113
|
+
// both marks stand on state nothing is coming to correct, so the grey is the whole message and the two ways
|
|
114
|
+
// of arriving at it - going offline while an agent works, and going offline while one waits - are one icon.
|
|
115
|
+
const badgeColor = offline ? null : dotColor();
|
|
89
116
|
// Only one mark is ever drawn, so both phases can be handed over and the drawing picks the one it needs.
|
|
90
117
|
try {
|
|
91
118
|
drawIcon(ctx, badgeColor, badgeColor === DONE_COLOR && pulseDim(), sweepAt());
|
|
@@ -106,7 +133,9 @@ function drawIcon(ctx, badgeColor, dim, sweep) {
|
|
|
106
133
|
ctx.beginPath();
|
|
107
134
|
ctx.roundRect(0, 0, 32, 32, 7);
|
|
108
135
|
ctx.fill();
|
|
109
|
-
|
|
136
|
+
// Offline drains the frame with everything else: the workspace hue is the liveliest thing in the icon, and
|
|
137
|
+
// one grey shape says more than a grey spark inside a bright ring.
|
|
138
|
+
ctx.strokeStyle = offline ? OFFLINE_FRAME : workspaceColor();
|
|
110
139
|
ctx.lineWidth = 2;
|
|
111
140
|
ctx.beginPath();
|
|
112
141
|
ctx.roundRect(1, 1, 30, 30, 6);
|
|
@@ -115,9 +144,14 @@ function drawIcon(ctx, badgeColor, dim, sweep) {
|
|
|
115
144
|
// The spark: four points with a waist, stroked as well as filled so it nearly fills the icon and still
|
|
116
145
|
// reads at 16px. Same geometry and gradient as favicon.svg, which is what a browser without canvas still
|
|
117
146
|
// gets - the two are one drawing in two languages, so a change to either is a change to both.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
147
|
+
let spark;
|
|
148
|
+
if (offline) {
|
|
149
|
+
spark = OFFLINE_SPARK;
|
|
150
|
+
} else {
|
|
151
|
+
spark = ctx.createLinearGradient(0, 0, 0, 32);
|
|
152
|
+
spark.addColorStop(0, "#f0c078");
|
|
153
|
+
spark.addColorStop(1, "#d08a3c");
|
|
154
|
+
}
|
|
121
155
|
ctx.fillStyle = spark;
|
|
122
156
|
ctx.strokeStyle = spark;
|
|
123
157
|
ctx.lineWidth = 2;
|
|
@@ -194,7 +228,9 @@ function tickIcon() {
|
|
|
194
228
|
export function refreshBrowserTab() {
|
|
195
229
|
// Just the workspace, no product name: a tab strip gives you a few characters, and the icon already says
|
|
196
230
|
// this is chamba. What the title is for is which workspace, and how many sessions want you.
|
|
197
|
-
|
|
231
|
+
// The count goes with the icon's marks while the socket is down, and for the same reason. The title says the
|
|
232
|
+
// workspace and stops there: being offline is the icon's to say, and a tab strip cannot say it twice.
|
|
233
|
+
const waiting = offline ? [] : sessions.filter((entry) => entry.attention);
|
|
198
234
|
const name = workspaceName || "chamba";
|
|
199
235
|
document.title = waiting.length > 0 ? `(${waiting.length}) ${name}` : name;
|
|
200
236
|
|
|
@@ -202,7 +238,9 @@ export function refreshBrowserTab() {
|
|
|
202
238
|
// what the sessions are doing: an alert arriving next to one already up is the same state, and restarting the
|
|
203
239
|
// cycle on every frame the server sends would make the mark stutter.
|
|
204
240
|
paintFavicon();
|
|
205
|
-
|
|
241
|
+
// The grey icon is one still frame: nothing in it moves, so nothing has to be redrawn, and a window sitting
|
|
242
|
+
// offline for a day costs exactly one paint.
|
|
243
|
+
if (offline || !dotColor()) {
|
|
206
244
|
if (iconTimer) clearTimeout(iconTimer);
|
|
207
245
|
iconTimer = null;
|
|
208
246
|
iconFrame = 0;
|
|
@@ -211,6 +249,42 @@ export function refreshBrowserTab() {
|
|
|
211
249
|
if (!iconTimer) iconTimer = setTimeout(tickIcon, ICON_TICK_MS);
|
|
212
250
|
}
|
|
213
251
|
|
|
252
|
+
// The socket went. The only thing the connection has to say, and everything that follows from it - the grace, the
|
|
253
|
+
// flag, the title, the paint and the chime - is this module's, because all five are the same subject as the three
|
|
254
|
+
// states above.
|
|
255
|
+
//
|
|
256
|
+
// Counted from the first close and not restarted by the ones after it, which is the whole difference between
|
|
257
|
+
// three seconds and however long the reconnect loop takes to give up: every failed attempt closes a socket of its
|
|
258
|
+
// own, and each of those closes arrives here. The curtain's own wait is guarded the same way, for the same
|
|
259
|
+
// reason, and a tab that greys later than the curtain it sits above is a tab still showing a live green dot over
|
|
260
|
+
// a page that has already said the container cannot be reached.
|
|
261
|
+
export function socketWentDown() {
|
|
262
|
+
if (offline || offlineTimer) return;
|
|
263
|
+
offlineTimer = setTimeout(() => {
|
|
264
|
+
offlineTimer = null;
|
|
265
|
+
offline = true;
|
|
266
|
+
refreshBrowserTab();
|
|
267
|
+
}, OFFLINE_GRACE_MS);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// And came back.
|
|
271
|
+
export function socketCameBack() {
|
|
272
|
+
if (offlineTimer) {
|
|
273
|
+
clearTimeout(offlineTimer);
|
|
274
|
+
offlineTimer = null;
|
|
275
|
+
}
|
|
276
|
+
if (!offline) return;
|
|
277
|
+
// Spend whatever came due while the tab was grey, before the flag drops. An alert still standing and already
|
|
278
|
+
// due at this moment can only have come due during the disconnect, because nothing else was left running to
|
|
279
|
+
// catch it: the chime arms one timer for the soonest alert alone, and the icon's clock, which is what catches
|
|
280
|
+
// the rest, is stopped while the icon is grey. Without this, the first tick after a reconnect would find one
|
|
281
|
+
// of those alerts unspent and sound it - a chime for a finish that happened while this window was away.
|
|
282
|
+
maybeChime();
|
|
283
|
+
// Then straight back to whatever the sessions say, without waiting for the first frame of the new socket.
|
|
284
|
+
offline = false;
|
|
285
|
+
refreshBrowserTab();
|
|
286
|
+
}
|
|
287
|
+
|
|
214
288
|
// --- A sound, for the session you are not at ---------------------------------------------------------------------------------------------
|
|
215
289
|
//
|
|
216
290
|
// The fourth thing the "an agent finished" alert does, and the only one that reaches a window behind an editor.
|
|
@@ -262,6 +336,9 @@ const CHIME_VOLUME = 0.35;
|
|
|
262
336
|
// reload and covers every window of the same container without touching any other workspace. On by default: a
|
|
263
337
|
// notification nobody discovers is not a notification, and the bell is right there to turn off.
|
|
264
338
|
let soundOn = readSoundPref();
|
|
339
|
+
// Every pane tool that makes a sound of its own reads this window's one preference, so it is said once here at
|
|
340
|
+
// start and again on every toggle.
|
|
341
|
+
toolsHearSound(soundOn);
|
|
265
342
|
|
|
266
343
|
function readSoundPref() {
|
|
267
344
|
try {
|
|
@@ -389,7 +466,10 @@ function maybeChime() {
|
|
|
389
466
|
// Spent whether or not it is heard, so a muted window does not save up its chimes for whenever the bell is
|
|
390
467
|
// unmuted - nothing new would have happened by then.
|
|
391
468
|
for (const entry of due) chimed.add(entry.id);
|
|
392
|
-
|
|
469
|
+
// A window that cannot reach its container is the second case of exactly that: calling you back to a session
|
|
470
|
+
// it can no longer see is worse than saying nothing, and a sound held for the reconnect would be reporting a
|
|
471
|
+
// state that has had a whole disconnect to move on.
|
|
472
|
+
if (offline || !soundOn) return;
|
|
393
473
|
const now = Date.now();
|
|
394
474
|
if (now - lastChimeAt < CHIME_GAP_MS) return;
|
|
395
475
|
if (!claimChime()) return;
|
|
@@ -424,9 +504,9 @@ export function bellButton() {
|
|
|
424
504
|
button.type = "button";
|
|
425
505
|
button.classList.toggle("muted", !soundOn);
|
|
426
506
|
button.title = soundOn
|
|
427
|
-
? "Sound on - a chime when an agent finishes in a session you have not touched in a while. Click to mute."
|
|
428
|
-
: "Sound muted - click for a chime when an agent finishes in a session you have not touched in a while.";
|
|
429
|
-
button.setAttribute("aria-label", soundOn ? "Mute the
|
|
507
|
+
? "Sound on - a chime when an agent finishes in a session you have not touched in a while, and the bee on the Home tab. Click to mute."
|
|
508
|
+
: "Sound muted - click for a chime when an agent finishes in a session you have not touched in a while, and for the bee on the Home tab.";
|
|
509
|
+
button.setAttribute("aria-label", soundOn ? "Mute the interface" : "Unmute the interface");
|
|
430
510
|
button.setAttribute("aria-pressed", String(soundOn));
|
|
431
511
|
const svg = document.createElementNS(SVG_NS, "svg");
|
|
432
512
|
svg.setAttribute("viewBox", "0 0 24 24");
|
|
@@ -464,6 +544,7 @@ function toggleSound() {
|
|
|
464
544
|
// Not remembered past this page, but the toggle still works for as long as it is open.
|
|
465
545
|
}
|
|
466
546
|
renderBar();
|
|
547
|
+
toolsHearSound(soundOn);
|
|
467
548
|
// The click landed on a button, which took the keyboard off the terminal.
|
|
468
549
|
focusTerminal();
|
|
469
550
|
// Turning it on plays it once, so the first time it happens behind your editor it is a sound you have already
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// anchored.js - a panel that hangs under the button that opened it, and closes the way every panel here does.
|
|
2
|
+
//
|
|
3
|
+
// The bar is rebuilt from scratch on every sessions frame and on the age tick, so a panel nested inside it
|
|
4
|
+
// would close whenever any background session moved - and at the moment of use, since picking a workspace
|
|
5
|
+
// colour broadcasts a frame straight back to the window that picked. Every panel in the bar therefore hangs
|
|
6
|
+
// off the body, and is put back under its button whenever the bar is redrawn.
|
|
7
|
+
//
|
|
8
|
+
// Two panels work this way now, so the behaviour lives here rather than in either of them: where the panel
|
|
9
|
+
// goes, the clamp to the window, dismissal on a click anywhere else, and Escape. What a panel is made of, and
|
|
10
|
+
// what it does when something in it is clicked, stay its own business.
|
|
11
|
+
|
|
12
|
+
import { focusTerminal } from "./terminal.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A panel anchored under the element with `anchorId`. The id rather than the element itself, because the bar
|
|
16
|
+
* is rebuilt from scratch and the button under an open panel is a new element by the time it is looked at.
|
|
17
|
+
*
|
|
18
|
+
* Both callers run this at their own module load, which main.js otherwise forbids. It is allowed here because
|
|
19
|
+
* this reads nothing: it is a hoisted declaration that builds a closure and registers one listener, and the
|
|
20
|
+
* only module it touches is reached inside that listener, long after the page has finished loading.
|
|
21
|
+
*/
|
|
22
|
+
export function createAnchoredPanel(anchorId, onChange = () => {}) {
|
|
23
|
+
let panel = null;
|
|
24
|
+
|
|
25
|
+
function close() {
|
|
26
|
+
if (!panel) return;
|
|
27
|
+
panel.remove();
|
|
28
|
+
panel = null;
|
|
29
|
+
document.removeEventListener("pointerdown", onOutside, true);
|
|
30
|
+
// Every way out ends here - a click outside, Escape, the socket dropping, a choice made in the panel -
|
|
31
|
+
// so this is the one place that can tell the button the panel has gone.
|
|
32
|
+
onChange();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function onOutside(event) {
|
|
36
|
+
// The button itself is left alone: its own click handler toggles, and closing here first would reopen it.
|
|
37
|
+
if (panel?.contains(event.target) || event.target.closest?.(`#${anchorId}`)) return;
|
|
38
|
+
close();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Put an open panel back under its button after the bar was rebuilt. A frame arrives whenever anything in
|
|
42
|
+
// the container moves, and a panel that closed itself every time one did would be unusable; a button that
|
|
43
|
+
// is gone takes it with it. Clamped to the window, since the bar can be scrolled far to the right.
|
|
44
|
+
function reposition() {
|
|
45
|
+
if (!panel) return;
|
|
46
|
+
const anchor = document.getElementById(anchorId);
|
|
47
|
+
if (!anchor) {
|
|
48
|
+
close();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const box = anchor.getBoundingClientRect();
|
|
52
|
+
const width = panel.offsetWidth;
|
|
53
|
+
const left = Math.min(box.right - width, window.innerWidth - width - 8);
|
|
54
|
+
panel.style.top = `${Math.round(box.bottom + 6)}px`;
|
|
55
|
+
panel.style.left = `${Math.round(Math.max(8, left))}px`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function open(element) {
|
|
59
|
+
close();
|
|
60
|
+
// Off the body, and never into the bar: renderBar() empties the bar on every frame.
|
|
61
|
+
document.body.append(element);
|
|
62
|
+
panel = element;
|
|
63
|
+
reposition();
|
|
64
|
+
// Capture, so a click on a tab closes this before that tab switches session.
|
|
65
|
+
document.addEventListener("pointerdown", onOutside, true);
|
|
66
|
+
onChange();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
document.addEventListener("keydown", (event) => {
|
|
70
|
+
if (event.key !== "Escape" || !panel) return;
|
|
71
|
+
close();
|
|
72
|
+
focusTerminal();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
open,
|
|
77
|
+
close,
|
|
78
|
+
reposition,
|
|
79
|
+
isOpen: () => panel !== null,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -43,15 +43,49 @@ export function refreshComposer() {
|
|
|
43
43
|
else input.placeholder = "Pick a session above to type in it.";
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// The box the text is measured in: a second textarea, hidden and of no height, styled as the composer is.
|
|
47
|
+
// The composer sits under the pane in one column, so a height it takes for one layout pass is a height the
|
|
48
|
+
// tool frames lose for that pass, and a frame in its own process paints at that size before the pass is
|
|
49
|
+
// corrected. Measuring here leaves the composer's height alone until the text needs another line.
|
|
50
|
+
let mirror = null;
|
|
51
|
+
function mirrorOf() {
|
|
52
|
+
if (mirror !== null) return mirror;
|
|
53
|
+
mirror = document.createElement("textarea");
|
|
54
|
+
mirror.id = "input-mirror";
|
|
55
|
+
mirror.tabIndex = -1;
|
|
56
|
+
mirror.setAttribute("aria-hidden", "true");
|
|
57
|
+
mirror.readOnly = true;
|
|
58
|
+
input.parentElement.appendChild(mirror);
|
|
59
|
+
return mirror;
|
|
60
|
+
}
|
|
61
|
+
|
|
46
62
|
export function autoGrow() {
|
|
47
|
-
|
|
63
|
+
const box = getComputedStyle(input);
|
|
64
|
+
const ruler = mirrorOf();
|
|
65
|
+
// What decides where the text wraps and how tall a line is, copied from the composer as it stands now.
|
|
66
|
+
for (const name of [
|
|
67
|
+
"font",
|
|
68
|
+
"letterSpacing",
|
|
69
|
+
"lineHeight",
|
|
70
|
+
"padding",
|
|
71
|
+
"border",
|
|
72
|
+
"boxSizing",
|
|
73
|
+
"whiteSpace",
|
|
74
|
+
"wordBreak",
|
|
75
|
+
"overflowWrap",
|
|
76
|
+
"tabSize",
|
|
77
|
+
]) {
|
|
78
|
+
ruler.style[name] = box[name];
|
|
79
|
+
}
|
|
80
|
+
ruler.style.width = `${input.clientWidth + Number.parseFloat(box.borderLeftWidth) + Number.parseFloat(box.borderRightWidth)}px`;
|
|
81
|
+
ruler.value = input.value;
|
|
48
82
|
// The floor keeps the box off the button block behind it and the ceiling limits it to seven lines. Both are
|
|
49
83
|
// read from the box itself rather than written here as well: the same two numbers in two files is how the
|
|
50
84
|
// box comes to spring back to a height the stylesheet no longer says.
|
|
51
|
-
const box = getComputedStyle(input);
|
|
52
85
|
const floor = Number.parseFloat(box.minHeight) || 0;
|
|
53
86
|
const ceiling = Number.parseFloat(box.maxHeight) || Number.POSITIVE_INFINITY;
|
|
54
|
-
|
|
87
|
+
const wanted = `${Math.min(Math.max(ruler.scrollHeight, floor), ceiling)}px`;
|
|
88
|
+
if (input.style.height !== wanted) input.style.height = wanted;
|
|
55
89
|
}
|
|
56
90
|
input.addEventListener("input", () => {
|
|
57
91
|
autoGrow();
|
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
// the curtain is the one state that says "this window cannot do anything at all". They live together because
|
|
5
5
|
// they are the same subject read from two ends: what the socket is doing, and what the user is told about it.
|
|
6
6
|
|
|
7
|
+
import { socketCameBack, socketWentDown } from "./alerts.js";
|
|
7
8
|
import { refreshComposer } from "./composer.js";
|
|
8
9
|
import { curtain, curtainCard } from "./dom.js";
|
|
9
10
|
import { onFrame } from "./frames.js";
|
|
11
|
+
import { closeNewPop } from "./new-session.js";
|
|
10
12
|
import { attachedSid, KEY_QUERY, sessions } from "./state.js";
|
|
13
|
+
import { renderBar } from "./tabs.js";
|
|
11
14
|
import { term } from "./terminal.js";
|
|
15
|
+
import { closeColorPop } from "./workspace-color.js";
|
|
12
16
|
|
|
13
17
|
// Whether the socket is up right now, and whether it has ever been up on this page. The second one is what
|
|
14
18
|
// tells an opened page from a reconnected one, which the server treats differently.
|
|
@@ -194,6 +198,12 @@ export function noteSocketAlive() {
|
|
|
194
198
|
// being abandoned must not schedule a second reconnect or overwrite the new one's state.
|
|
195
199
|
function dropSocket() {
|
|
196
200
|
if (!ws) return;
|
|
201
|
+
// The one close nobody else hears about. A socket retired while the page still believes it is up is the wake
|
|
202
|
+
// case: after a sleep the socket reads OPEN for minutes with nothing on the other end, the ping gets no
|
|
203
|
+
// answer, and this replaces it - with the handlers nulled, so no onclose ever fires for it. The browser tab
|
|
204
|
+
// is told here instead, because a tab left wearing a green dot for a container it cannot reach is exactly
|
|
205
|
+
// what this state is. Its own grace covers the ordinary case, where the replacement opens a moment later.
|
|
206
|
+
if (connected) socketWentDown();
|
|
197
207
|
ws.onopen = null;
|
|
198
208
|
ws.onmessage = null;
|
|
199
209
|
ws.onclose = null;
|
|
@@ -222,6 +232,9 @@ export function connect() {
|
|
|
222
232
|
clearCurtainTimer();
|
|
223
233
|
hideCurtain();
|
|
224
234
|
document.body.classList.remove("offline");
|
|
235
|
+
// The browser tab reads the socket too, and it keeps a grace of its own. Told that the socket moved and
|
|
236
|
+
// nothing more: what the tab does about it belongs to the tab.
|
|
237
|
+
socketCameBack();
|
|
225
238
|
// Ask for the session this window was last looking at. The server decides whether it can have
|
|
226
239
|
// it back, and picks something sensible when it cannot.
|
|
227
240
|
// The last session this window drove: the server hands it back when no other window is on it.
|
|
@@ -246,6 +259,16 @@ export function connect() {
|
|
|
246
259
|
// Immediately, ahead of any curtain: every control on the page is answered over this socket, so
|
|
247
260
|
// while it is down they must stop taking clicks.
|
|
248
261
|
document.body.classList.add("offline");
|
|
262
|
+
// And the browser tab, which is the only thing a hidden window can say any of this with. Said on every
|
|
263
|
+
// close, whichever curtain the diagnosis ends at: one rule covers a container that is down, a spent key,
|
|
264
|
+
// a container being stopped and a container that has gone.
|
|
265
|
+
socketWentDown();
|
|
266
|
+
// An anchored panel hangs off the body, so the bar's own offline rules do not reach it - and no frame
|
|
267
|
+
// is coming to redraw the bar, which leaves the age tick up to a minute away. Both are shut here.
|
|
268
|
+
closeNewPop();
|
|
269
|
+
closeColorPop();
|
|
270
|
+
// The bar with them, so its buttons are rebuilt as unavailable rather than waiting for that tick.
|
|
271
|
+
renderBar();
|
|
249
272
|
refreshComposer();
|
|
250
273
|
// The container was on its way down and the relay has now gone with it. That is the confirmation the
|
|
251
274
|
// stopping curtain is waiting for, and there is nothing to diagnose: this close was the point.
|
|
@@ -24,11 +24,14 @@ export const pane = document.getElementById("pane");
|
|
|
24
24
|
export const panebar = document.getElementById("panebar");
|
|
25
25
|
export const toolTabs = document.getElementById("tool-tabs");
|
|
26
26
|
export const paneFull = document.getElementById("pane-full");
|
|
27
|
+
export const paneHalf = document.getElementById("pane-half");
|
|
27
28
|
export const paneCollapse = document.getElementById("pane-collapse");
|
|
28
29
|
export const spine = document.getElementById("spine");
|
|
29
30
|
export const spineBadge = document.getElementById("spine-badge");
|
|
31
|
+
export const toolHome = document.getElementById("tool-home");
|
|
30
32
|
export const toolPages = document.getElementById("tool-pages");
|
|
31
33
|
export const toolSpecs = document.getElementById("tool-specs");
|
|
34
|
+
export const toolReviews = document.getElementById("tool-reviews");
|
|
32
35
|
|
|
33
36
|
// The Pages tool. Every one of these is chrome the interface draws around a published page - the page itself
|
|
34
37
|
// lives in a frame built by app/pane-frame.js and is never queried back.
|
|
@@ -13,11 +13,13 @@ import { stopDictation } from "./dictation.js";
|
|
|
13
13
|
import { captureDraft, dropDraft, dropHistory, persistDrafts, pruneDrafts, pruneHistory, restoreDraft } from "./drafts.js";
|
|
14
14
|
import { noteMsg } from "./note.js";
|
|
15
15
|
import { applyPages, prunePanes, refreshPane } from "./pane.js";
|
|
16
|
-
import {
|
|
17
|
-
import { adoptSessionsFrame, attachedSid, defaultAgent, sessions, setAttachedSid } from "./state.js";
|
|
16
|
+
import { applySessionShape, prunePaneShapes } from "./pane-shell.js";
|
|
17
|
+
import { adoptSessionsFrame, attachedSid, defaultAgent, sessions, setAttachedSid, workspaceColorId } from "./state.js";
|
|
18
18
|
import { applySnapshot, pruneStatuses, refreshStrip } from "./status-strip.js";
|
|
19
19
|
import { noteArrivals, notePageArrivals, renderBar } from "./tabs.js";
|
|
20
20
|
import { focusTerminal, scheduleRefresh, sendResize, term } from "./terminal.js";
|
|
21
|
+
import { applyWorkspaceColor } from "./theme.js";
|
|
22
|
+
import { toolHost, toolsMovedToSession } from "./tool-host.js";
|
|
21
23
|
|
|
22
24
|
// What the terminal is holding: either a live session's screen (`shownSid`), or a message where a session used
|
|
23
25
|
// to be (`shownMessage`). At most one of them is set. They are tracked because a session can end while this
|
|
@@ -28,6 +30,35 @@ const EXITED_SCREEN = "exited";
|
|
|
28
30
|
let shownSid = null;
|
|
29
31
|
let shownMessage = null;
|
|
30
32
|
|
|
33
|
+
// Everything beside the terminal that belongs to the session rather than to the window: the pane, whichever
|
|
34
|
+
// tool is open in it, and the strip above the composer. The pane's own shape goes first, because what a tool
|
|
35
|
+
// lays out depends on how wide the pane now is and on which tool that is.
|
|
36
|
+
//
|
|
37
|
+
// The frame that follows the attached session hears about it here too, which matters most where this window
|
|
38
|
+
// loses its session without an exit message of its own - the last session closes, or another window takes it
|
|
39
|
+
// - because a tool still showing that session's work would offer to send where nobody is.
|
|
40
|
+
function movedToSession() {
|
|
41
|
+
applySessionShape(attachedSid);
|
|
42
|
+
refreshPane();
|
|
43
|
+
refreshStrip();
|
|
44
|
+
toolsMovedToSession();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The tool one frame is about, and which of its two kinds it is, or null.
|
|
49
|
+
*
|
|
50
|
+
* A tool's frames are named after the tool - `specs` for a change under it, `specs:delivered` for the answer
|
|
51
|
+
* to a delivery - so a tool nobody registered has no frames and a name that is not a tool's is not one.
|
|
52
|
+
*/
|
|
53
|
+
function toolFrame(name) {
|
|
54
|
+
if (typeof name !== "string") return null;
|
|
55
|
+
const answered = ":delivered";
|
|
56
|
+
const id = name.endsWith(answered) ? name.slice(0, -answered.length) : name;
|
|
57
|
+
const host = toolHost(id);
|
|
58
|
+
if (!host) return null;
|
|
59
|
+
return { host, kind: id === name ? "change" : "delivered" };
|
|
60
|
+
}
|
|
61
|
+
|
|
31
62
|
export function onFrame(msg) {
|
|
32
63
|
// Any frame at all proves the socket works, which is what a wake check is waiting to hear.
|
|
33
64
|
noteSocketAlive();
|
|
@@ -60,17 +91,17 @@ export function onFrame(msg) {
|
|
|
60
91
|
sendResize();
|
|
61
92
|
if (switching) restoreDraft(msg.sid);
|
|
62
93
|
refreshComposer();
|
|
63
|
-
|
|
64
|
-
refreshPane();
|
|
65
|
-
refreshStrip();
|
|
94
|
+
movedToSession();
|
|
66
95
|
} else if (msg.t === "pages") {
|
|
67
96
|
applyPages(msg);
|
|
68
|
-
} else if (msg.t
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
//
|
|
73
|
-
|
|
97
|
+
} else if (toolFrame(msg.t)) {
|
|
98
|
+
// A frame about one of the pane's tools, named after that tool. The disk moved under its work, which
|
|
99
|
+
// every window gets - what a tool serves belongs to the repository rather than to a session, and two
|
|
100
|
+
// windows reading the same work both need to know - or the server answered a delivery this window
|
|
101
|
+
// asked for, which goes back to the frame that asked.
|
|
102
|
+
const { host, kind } = toolFrame(msg.t);
|
|
103
|
+
if (kind === "change") host.change(msg);
|
|
104
|
+
else host.delivered(msg);
|
|
74
105
|
} else if (msg.t === "snapshot") {
|
|
75
106
|
applySnapshot(msg);
|
|
76
107
|
} else if (msg.t === "out") {
|
|
@@ -89,8 +120,7 @@ export function onFrame(msg) {
|
|
|
89
120
|
setAttachedSid(null);
|
|
90
121
|
takenCard(msg.sid);
|
|
91
122
|
refreshComposer();
|
|
92
|
-
|
|
93
|
-
refreshStrip();
|
|
123
|
+
movedToSession();
|
|
94
124
|
} else if (msg.t === "exit") {
|
|
95
125
|
if (msg.sid === attachedSid) {
|
|
96
126
|
setAttachedSid(null);
|
|
@@ -103,8 +133,7 @@ export function onFrame(msg) {
|
|
|
103
133
|
dropDraft(msg.sid);
|
|
104
134
|
dropHistory(msg.sid);
|
|
105
135
|
refreshComposer();
|
|
106
|
-
|
|
107
|
-
refreshStrip();
|
|
136
|
+
movedToSession();
|
|
108
137
|
} else if (msg.t === "stopping") {
|
|
109
138
|
// Sent to every window, not just the one that asked: the container is about to take them all.
|
|
110
139
|
stoppingCurtain();
|
|
@@ -123,6 +152,9 @@ export function onFrame(msg) {
|
|
|
123
152
|
|
|
124
153
|
function applySessions(msg) {
|
|
125
154
|
adoptSessionsFrame(msg);
|
|
155
|
+
// Before anything draws: the bar reads the session colours this works out, and the favicon below reads
|
|
156
|
+
// the workspace's own. An id this version does not know, and "" for nothing picked, both mean automatic.
|
|
157
|
+
applyWorkspaceColor(workspaceColorId);
|
|
126
158
|
// Before the bar draws: its flash reads these timestamps, so an alert plays once instead of on every frame.
|
|
127
159
|
noteArrivals();
|
|
128
160
|
// The same, for a page published in a session this window is not looking at.
|
|
@@ -137,13 +169,15 @@ function applySessions(msg) {
|
|
|
137
169
|
pruneHistory();
|
|
138
170
|
refreshComposer();
|
|
139
171
|
// Same for the pane: a session that left the bar takes this window's copy of its page list with it. The
|
|
140
|
-
// files themselves are untouched - a resumed conversation gets them back
|
|
172
|
+
// files themselves are untouched - a resumed conversation gets them back.
|
|
141
173
|
const live = new Set(sessions.map((entry) => entry.id));
|
|
142
174
|
prunePanes(live);
|
|
143
|
-
|
|
175
|
+
// And for the pane itself: the shape a session was left in ends with that session, the way its draft does.
|
|
176
|
+
prunePaneShapes(live);
|
|
144
177
|
// And for the strip: the numbers belong to a session, and that session is gone.
|
|
145
178
|
pruneStatuses(live);
|
|
146
|
-
|
|
179
|
+
// Everything that outlived a session is gone, so what is left is drawn for the session this window holds.
|
|
180
|
+
movedToSession();
|
|
147
181
|
// A session can vanish from under this window: closed here, closed from another window, or the agent exited.
|
|
148
182
|
// Where the server had a free session to move this window to, that session's replay has already painted over
|
|
149
183
|
// it. Where it did not - every remaining session is being driven elsewhere - this is the only thing that says
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// home-host.js - the Home tool on the page.
|
|
2
|
+
//
|
|
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.
|
|
6
|
+
|
|
7
|
+
import { toolHome } from "./dom.js";
|
|
8
|
+
import { HOME } from "./pane-shell.js";
|
|
9
|
+
import { createToolHost } from "./tool-host.js";
|
|
10
|
+
|
|
11
|
+
createToolHost({
|
|
12
|
+
id: HOME,
|
|
13
|
+
label: "Home",
|
|
14
|
+
panel: toolHome,
|
|
15
|
+
opening: "Reading the pulse of this repository.",
|
|
16
|
+
});
|