chamba 0.7.0 → 0.8.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 +28 -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 +12 -19
- package/templates/claude-statusline.sh +5 -7
- package/templates/context/baseline.md +2 -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-D0nJgFAA.js +12 -0
- package/templates/pane-apps/client/assets/files-h0K1vZyQ.css +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/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/python-CYQee8m4.json +4174 -0
- package/templates/pane-apps/client/assets/reviews-BsoGZ35r.css +1 -0
- package/templates/pane-apps/client/assets/reviews-iW0Mbn4e.js +153 -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-D9z3neR-.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/xml-B17r2RXF.json +382 -0
- package/templates/pane-apps/client/assets/yaml-CKn4h1zy.json +627 -0
- package/templates/pane-apps/client/reviews/index.html +15 -0
- package/templates/pane-apps/client/specs/index.html +3 -2
- package/templates/pane-apps/server/reviews.mjs +20 -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 +123 -0
- package/templates/skills/dx-review/references/acts.md +162 -0
- package/templates/skills/dx-spec/SKILL.md +223 -27
- package/templates/skills/dx-spec/references/principles-template.md +2 -0
- package/templates/skills/dx-spec/references/review-guide.md +4 -50
- package/templates/skills/dx-spec/references/spec-guide.md +24 -4
- package/templates/skills/dx-spec/references/stages.md +143 -58
- package/templates/skills/dx-spec-execute/SKILL.md +148 -35
- package/templates/startup.mjs +10 -4
- package/templates/tool-helper.sh +166 -0
- package/templates/webterm/README.md +60 -26
- package/templates/webterm/color.js +61 -0
- package/templates/webterm/config.js +35 -4
- package/templates/webterm/context/claude.md +0 -1
- package/templates/webterm/public/app/alerts.js +4 -4
- package/templates/webterm/public/app/anchored.js +81 -0
- package/templates/webterm/public/app/connection.js +9 -0
- package/templates/webterm/public/app/dom.js +2 -0
- package/templates/webterm/public/app/frames.js +51 -17
- package/templates/webterm/public/app/main.js +22 -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 +167 -0
- package/templates/webterm/public/app/pane-shell.js +199 -134
- package/templates/webterm/public/app/pane.js +33 -24
- package/templates/webterm/public/app/reviews-host.js +15 -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 +302 -0
- package/templates/webterm/public/app/workspace-color.js +192 -0
- package/templates/webterm/public/index.html +17 -3
- package/templates/webterm/public/styles.css +143 -21
- package/templates/webterm/server.js +198 -150
- package/templates/webterm/tools/commands.js +61 -0
- package/templates/webterm/tools/index.js +25 -0
- package/templates/webterm/tools/mount.js +337 -0
- package/templates/webterm/tools/paths.js +34 -0
- package/templates/webterm/tools/reviews.js +133 -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
|
@@ -1,9 +1,10 @@
|
|
|
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 - Pages (pane.js), Specs (specs-host.js)
|
|
4
|
-
// between them is the whole point of this module: a tool draws inside its
|
|
5
|
-
// the tab bar that says which tool is open, and the controls that say how
|
|
6
|
-
// here. A tool can therefore never move the pane, and never lie about which
|
|
3
|
+
// The pane is furniture. What sits in it are tools - Pages (pane.js), Specs (specs-host.js) and Code Reviews
|
|
4
|
+
// (reviews-host.js) - and the line between them is the whole point of this module: a tool draws inside its
|
|
5
|
+
// own panel and nothing else, while the tab bar that says which tool is open, and the controls that say how
|
|
6
|
+
// wide the pane is, are drawn out here. A tool can therefore never move the pane, and never lie about which
|
|
7
|
+
// tool the user is looking at.
|
|
7
8
|
//
|
|
8
9
|
// Width has three states, and only one of them is a width. Between the two ends the divider drags anything,
|
|
9
10
|
// clamped as it always was. Past the near end the pane is put away and becomes the spine. Past the far end it
|
|
@@ -11,90 +12,153 @@
|
|
|
11
12
|
// drag's own clamps stay where they are. The grip is reachable in every one of them, and dragging it is
|
|
12
13
|
// always the way back.
|
|
13
14
|
//
|
|
14
|
-
// What the pane is belongs to the
|
|
15
|
-
//
|
|
15
|
+
// What the pane is belongs to the session it is beside - how wide, which tool, how big a page is drawn, and
|
|
16
|
+
// the reading theme - so switching sessions gives each one the pane it was left with, and a reload gives them
|
|
17
|
+
// all back. What is in a tool belongs to the session too, and the tool keeps that itself.
|
|
16
18
|
//
|
|
17
|
-
// This module imports
|
|
18
|
-
// load, so it has to be finished before any of them runs.
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// This module imports dom.js and pane-shape.js and nothing else, deliberately: the tools import it and
|
|
20
|
+
// register themselves as they load, so it has to be finished before any of them runs. Both of those are
|
|
21
|
+
// leaves - pane-shape.js is the shape, its rules and its store, and it holds no element and reaches no
|
|
22
|
+
// other module. Every rule about a shape lives there rather than here, so it can be run in a test; what is
|
|
23
|
+
// left here is the pane as a thing on the screen.
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
grip,
|
|
27
|
+
pane,
|
|
28
|
+
panebar,
|
|
29
|
+
paneCollapse,
|
|
30
|
+
paneFull,
|
|
31
|
+
paneHalf,
|
|
32
|
+
spine,
|
|
33
|
+
spineBadge,
|
|
34
|
+
toolPages,
|
|
35
|
+
toolReviews,
|
|
36
|
+
toolSpecs,
|
|
37
|
+
toolTabs,
|
|
38
|
+
} from "./dom.js";
|
|
39
|
+
import {
|
|
40
|
+
DEFAULT_FRACTION,
|
|
41
|
+
defaultShape,
|
|
42
|
+
draggedTo,
|
|
43
|
+
isDefaultWidth,
|
|
44
|
+
mergeShape,
|
|
45
|
+
PAGES,
|
|
46
|
+
pruneShapes,
|
|
47
|
+
REVIEWS,
|
|
48
|
+
readShapes,
|
|
49
|
+
reopenedFrom,
|
|
50
|
+
SPECS,
|
|
51
|
+
} from "./pane-shape.js";
|
|
52
|
+
|
|
53
|
+
// The tools' ids come from the shape, which holds one of them. Re-exported here because this module is the
|
|
54
|
+
// pane, and a tool that asks whether it is open is asking the pane rather than the store.
|
|
55
|
+
export { PAGES, REVIEWS, SPECS };
|
|
21
56
|
|
|
22
57
|
// The tools, in the order their tabs are drawn. A tool is a tab, a panel, and a render function it hands over
|
|
23
58
|
// when it loads; the pane knows nothing else about any of them.
|
|
24
|
-
export const PAGES = "pages";
|
|
25
|
-
export const SPECS = "specs";
|
|
26
59
|
const TOOLS = [
|
|
27
60
|
{ id: PAGES, label: "Pages", panel: toolPages },
|
|
28
61
|
{ id: SPECS, label: "Specs", panel: toolSpecs },
|
|
62
|
+
{ id: REVIEWS, label: "Code Reviews", panel: toolReviews },
|
|
29
63
|
];
|
|
30
64
|
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
// The window's own pane preferences: how wide, whether it is collapsed, whether it has the whole row, which
|
|
41
|
-
// tool is open, and how big a page is drawn in it. Not the session's - a pane is furniture, and furniture
|
|
42
|
-
// does not move because you looked at another tab.
|
|
43
|
-
const GEOMETRY_KEY = "webterm-pane";
|
|
44
|
-
|
|
45
|
-
let width = null;
|
|
46
|
-
// null means nobody has said. An untouched pane keeps out of the way while the tool it is open on has nothing
|
|
47
|
-
// in it, and is open once that tool has something - which is the first page of a session opening on arrival,
|
|
48
|
-
// as it always did. A click on the spine or on the collapse button is an answer, and from then on it is the
|
|
49
|
-
// only one that counts.
|
|
50
|
-
let collapsed = null;
|
|
51
|
-
// The far end: the pane has the whole row and the terminal is squeezed out.
|
|
52
|
-
let full = false;
|
|
53
|
-
let openTool = PAGES;
|
|
54
|
-
// How big a page is drawn. The pane applies it, so it is stored here with the rest of the furniture, and the
|
|
55
|
-
// Pages tool owns the control and the steps.
|
|
56
|
-
let pageSize = null;
|
|
57
|
-
// What the last render decided, for the two things that only need to know whether the pane is showing.
|
|
58
|
-
let shut = true;
|
|
65
|
+
// The one shape this window is drawing, and the session it belongs to. One and not a map of them: the store
|
|
66
|
+
// is shared by every window of this origin, so a shape held here from a session this window left is a copy
|
|
67
|
+
// that has had time to go stale. It is read again on every move to another session instead.
|
|
68
|
+
let sid = null;
|
|
69
|
+
let current = defaultShape();
|
|
70
|
+
// Whether a frame from the server named that session. The id this window opens with comes from its own
|
|
71
|
+
// storage and is the session it is about to ask for, which is enough to draw in and not enough to write
|
|
72
|
+
// under: the server may hand this window a different one, or none.
|
|
73
|
+
let told = false;
|
|
59
74
|
|
|
60
75
|
// id -> what that tool has to say for itself on its tab: how many things are waiting in it, and whether it
|
|
61
76
|
// has anything at all. A tool nobody has heard from yet is empty and waiting on nothing.
|
|
62
|
-
const EMPTY_MARK = { unread: 0, empty: true };
|
|
77
|
+
const EMPTY_MARK = { unread: 0, empty: true, counted: true };
|
|
63
78
|
const marks = new Map();
|
|
64
79
|
// id -> the tool's own render function, handed over as it loads.
|
|
65
80
|
const renderers = new Map();
|
|
66
81
|
|
|
67
|
-
// --- What
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
const stored = JSON.parse(localStorage.getItem(GEOMETRY_KEY) ?? "{}");
|
|
71
|
-
if (typeof stored.width === "number" && stored.width >= COLLAPSE_AT) width = stored.width;
|
|
72
|
-
if (typeof stored.collapsed === "boolean") collapsed = stored.collapsed;
|
|
73
|
-
if (typeof stored.full === "boolean") full = stored.full;
|
|
74
|
-
if (TOOLS.some((tool) => tool.id === stored.tool)) openTool = stored.tool;
|
|
75
|
-
// The multiplier is stored rather than its position, so a window that was left at a size this version no
|
|
76
|
-
// longer offers falls back to the default instead of landing between two steps.
|
|
77
|
-
if (typeof stored.size === "number") pageSize = stored.size;
|
|
78
|
-
} catch {
|
|
79
|
-
// A disabled or full store only means the pane opens at its default width, on its default tool.
|
|
80
|
-
}
|
|
82
|
+
// --- What every session remembers --------------------------------------------------------------------------------------------------------
|
|
81
83
|
|
|
84
|
+
// Kept in the store, under the session this window is driving and no other. A window that has not been told
|
|
85
|
+
// its session yet writes nothing: the id it opened with may turn out to be another window's session, and the
|
|
86
|
+
// shape of that session is not this window's to set.
|
|
82
87
|
function remember() {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
if (!told) return;
|
|
89
|
+
mergeShape(localStorage, sid, current);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The shape of one session, off the store. A window with no session draws the defaults and stores nothing,
|
|
93
|
+
// because there is nobody to store it under.
|
|
94
|
+
function shapeOf(next) {
|
|
95
|
+
if (next === null) return defaultShape();
|
|
96
|
+
return readShapes(localStorage).get(next) ?? defaultShape();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Draw everything the shape decides: the pane's own chrome, and the tool open in it, which has a bar and a
|
|
100
|
+
// document to lay out at whatever width the pane now has.
|
|
101
|
+
function redraw() {
|
|
102
|
+
renderShell();
|
|
103
|
+
renderers.get(current.tool)?.();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The shape just changed, so keep it under this session and draw it. Every act on the pane ends here.
|
|
107
|
+
function reshape() {
|
|
108
|
+
remember();
|
|
109
|
+
redraw();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The window is beside that session now, so the pane is drawn in that session's own shape.
|
|
114
|
+
*
|
|
115
|
+
* The shape is read again here rather than kept from the load, so a window coming back to a session sees
|
|
116
|
+
* what it was left at, whichever window left it there. Called before the tools are told, because what they
|
|
117
|
+
* lay out depends on how wide the pane now is and on which of them is open in it.
|
|
118
|
+
*/
|
|
119
|
+
export function applySessionShape(next) {
|
|
120
|
+
const held = next ?? null;
|
|
121
|
+
// The first frame is worth acting on even where it names the session this window opened with: it is what
|
|
122
|
+
// makes the shape this window's to write, and the store may have moved since the page loaded.
|
|
123
|
+
if (held === sid && told) return;
|
|
124
|
+
// A drag in flight belongs to the session that is leaving. Ending it stores the width under that session
|
|
125
|
+
// rather than carrying the pointer into the next one's shape.
|
|
126
|
+
if (dragging) endDrag();
|
|
127
|
+
sid = held;
|
|
128
|
+
told = true;
|
|
129
|
+
current = shapeOf(held);
|
|
130
|
+
redraw();
|
|
88
131
|
}
|
|
89
132
|
|
|
90
|
-
/**
|
|
133
|
+
/** A session that left the bar takes its pane shape with it, the way its unsent message and its page list do. */
|
|
134
|
+
export function prunePaneShapes(live) {
|
|
135
|
+
pruneShapes(localStorage, live);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** How big a page was last drawn in this session, or null when nobody has said. */
|
|
91
139
|
export function storedPageSize() {
|
|
92
|
-
return
|
|
140
|
+
return current.size;
|
|
93
141
|
}
|
|
94
142
|
|
|
95
|
-
/** The Pages tool's zoom, kept with the rest of the
|
|
143
|
+
/** The Pages tool's zoom, kept with the rest of the session's shape so one key has one writer. */
|
|
96
144
|
export function rememberPageSize(size) {
|
|
97
|
-
|
|
145
|
+
current.size = size;
|
|
146
|
+
remember();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The reading theme this session was left on, or null for the system preference. */
|
|
150
|
+
export function storedPaneTheme() {
|
|
151
|
+
return current.theme;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The Specs tool's reading theme, kept with the rest of the session's shape.
|
|
156
|
+
*
|
|
157
|
+
* The tool's frame has an opaque origin and therefore no storage of its own, so the pane holds the choice
|
|
158
|
+
* and hands it back with the session. Anything but the two palettes means the system preference.
|
|
159
|
+
*/
|
|
160
|
+
export function rememberPaneTheme(theme) {
|
|
161
|
+
current.theme = theme === "dark" || theme === "light" ? theme : null;
|
|
98
162
|
remember();
|
|
99
163
|
}
|
|
100
164
|
|
|
@@ -110,136 +174,147 @@ export function registerTool(id, render) {
|
|
|
110
174
|
|
|
111
175
|
/** Whether the pane is open on that tool right now. */
|
|
112
176
|
export function isToolOpen(id) {
|
|
113
|
-
return
|
|
177
|
+
return current.tool === id;
|
|
114
178
|
}
|
|
115
179
|
|
|
116
180
|
/**
|
|
117
181
|
* What a tool has to say on its tab: `unread` is what is waiting for the user in it, and `empty` is whether
|
|
118
182
|
* it has anything to show at all - which is what an untouched pane reads to decide whether to stay out of
|
|
119
183
|
* the way.
|
|
184
|
+
*
|
|
185
|
+
* `counted` is whether that number is worth showing. A tool that asks the user things counts them, because
|
|
186
|
+
* two questions waiting is a different day from one. A tool that only receives counts nothing the user
|
|
187
|
+
* would act on differently, so its tab says that something arrived and leaves the number out of it.
|
|
120
188
|
*/
|
|
121
|
-
export function noteTool(id, { unread = 0, empty = false } = {}) {
|
|
122
|
-
marks.set(id, { unread, empty });
|
|
189
|
+
export function noteTool(id, { unread = 0, empty = false, counted = true } = {}) {
|
|
190
|
+
marks.set(id, { unread, empty, counted });
|
|
123
191
|
renderShell();
|
|
124
192
|
}
|
|
125
193
|
|
|
126
|
-
/** Open a tool, drawing its panel. A tool the user chose is the tool this
|
|
194
|
+
/** Open a tool, drawing its panel. A tool the user chose is the tool this session comes back on. */
|
|
127
195
|
export function showTool(id) {
|
|
128
196
|
if (!TOOLS.some((tool) => tool.id === id)) return;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
renderShell();
|
|
132
|
-
renderers.get(id)?.();
|
|
197
|
+
current.tool = id;
|
|
198
|
+
reshape();
|
|
133
199
|
}
|
|
134
200
|
|
|
135
201
|
function drawTabs() {
|
|
136
202
|
const tabs = TOOLS.map((tool) => {
|
|
137
203
|
const button = document.createElement("button");
|
|
138
204
|
button.type = "button";
|
|
139
|
-
button.className = tool.id ===
|
|
205
|
+
button.className = tool.id === current.tool ? "tool-tab on" : "tool-tab";
|
|
140
206
|
button.dataset.tool = tool.id;
|
|
141
207
|
button.append(tool.label);
|
|
142
208
|
// What is waiting in a tool nobody is looking at. The one thing a tab says beyond its name, so a tool
|
|
143
209
|
// never has to take the screen to be noticed.
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
210
|
+
const mark = marks.get(tool.id) ?? EMPTY_MARK;
|
|
211
|
+
if (mark.unread > 0) {
|
|
146
212
|
const badge = document.createElement("span");
|
|
147
|
-
badge.className = "tool-mark";
|
|
148
|
-
badge.textContent = String(
|
|
213
|
+
badge.className = mark.counted ? "tool-mark" : "tool-mark dot";
|
|
214
|
+
if (mark.counted) badge.textContent = String(mark.unread);
|
|
215
|
+
else badge.setAttribute("aria-label", "something new");
|
|
149
216
|
button.append(badge);
|
|
150
217
|
}
|
|
151
218
|
button.addEventListener("click", () => showTool(tool.id));
|
|
152
219
|
return button;
|
|
153
220
|
});
|
|
154
221
|
toolTabs.replaceChildren(...tabs);
|
|
155
|
-
for (const tool of TOOLS) tool.panel.hidden = tool.id !==
|
|
222
|
+
for (const tool of TOOLS) tool.panel.hidden = tool.id !== current.tool;
|
|
156
223
|
}
|
|
157
224
|
|
|
158
225
|
// --- How wide ----------------------------------------------------------------------------------------------------------------------------
|
|
159
226
|
|
|
160
227
|
// How wide, put away, or the whole row. Its own function because a drag is the one thing that changes the
|
|
161
228
|
// pane without changing anything in it: sixty of these a second are cheap, and sixty full redraws are not.
|
|
162
|
-
function applyGeometry(
|
|
163
|
-
|
|
229
|
+
function applyGeometry() {
|
|
230
|
+
const { width, full, collapsed } = current;
|
|
231
|
+
pane.classList.toggle("collapsed", collapsed);
|
|
164
232
|
// Only one of the two can be true: a pane that is put away has no width to be full of.
|
|
165
|
-
pane.classList.toggle("full", full && !
|
|
233
|
+
pane.classList.toggle("full", full && !collapsed);
|
|
166
234
|
// The width goes into a custom property rather than straight onto `width`, so the stylesheet keeps the
|
|
167
235
|
// last word: the collapsed spine, the full row and the narrow-screen layout are rules, and an inline
|
|
168
236
|
// width would beat all three.
|
|
169
237
|
pane.style.setProperty("--pane-width", width === null ? `${DEFAULT_FRACTION * 100}%` : `${width}px`);
|
|
170
|
-
paneFull.classList.toggle("on", full && !
|
|
171
|
-
|
|
238
|
+
paneFull.classList.toggle("on", full && !collapsed);
|
|
239
|
+
paneHalf.classList.toggle("on", isDefaultWidth(current));
|
|
240
|
+
const back = full && !collapsed;
|
|
172
241
|
paneFull.title = back ? "Give the terminal its room back" : "Full width";
|
|
173
242
|
paneFull.setAttribute("aria-label", paneFull.title);
|
|
174
243
|
}
|
|
175
244
|
|
|
176
245
|
/** Draw the pane's own chrome: the tabs, the width, and what the spine says while the pane is away. */
|
|
177
246
|
export function renderShell() {
|
|
178
|
-
const mark = marks.get(
|
|
179
|
-
shut = collapsed === null ? mark.empty : collapsed;
|
|
247
|
+
const mark = marks.get(current.tool) ?? EMPTY_MARK;
|
|
180
248
|
pane.hidden = false;
|
|
181
249
|
// There is no width worth choosing for a tool with nothing in it.
|
|
182
|
-
grip.hidden = mark.empty &&
|
|
250
|
+
grip.hidden = mark.empty && current.collapsed;
|
|
183
251
|
|
|
184
|
-
applyGeometry(
|
|
252
|
+
applyGeometry();
|
|
185
253
|
drawTabs();
|
|
186
254
|
|
|
187
255
|
// The spine stands for the whole pane, so it carries what every tool is waiting on, not just the open one.
|
|
256
|
+
// It counts what the tools count, and falls back to the bare dot when all that arrived was uncounted:
|
|
257
|
+
// a number here has to mean the same thing it means on a tab.
|
|
188
258
|
let waiting = 0;
|
|
189
|
-
|
|
259
|
+
let arrived = false;
|
|
260
|
+
for (const entry of marks.values()) {
|
|
261
|
+
if (entry.unread <= 0) continue;
|
|
262
|
+
if (entry.counted) waiting += entry.unread;
|
|
263
|
+
else arrived = true;
|
|
264
|
+
}
|
|
190
265
|
spineBadge.textContent = waiting > 0 ? String(waiting) : "";
|
|
191
|
-
spineBadge.
|
|
266
|
+
spineBadge.classList.toggle("dot", waiting === 0 && arrived);
|
|
267
|
+
spineBadge.hidden = waiting === 0 && !arrived;
|
|
192
268
|
}
|
|
193
269
|
|
|
194
270
|
/**
|
|
195
271
|
* Draw the pane for the first time, on the tool this window was left on. Called from the entry rather than
|
|
196
272
|
* here, because the tools register as they load and there is no pane to draw until they all have.
|
|
197
273
|
*/
|
|
198
|
-
export function mountPane() {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
/** Whether the pane is drawn as the spine rather than as a pane. */
|
|
204
|
-
export function isPaneShut() {
|
|
205
|
-
return shut;
|
|
274
|
+
export function mountPane(startSid = null) {
|
|
275
|
+
sid = startSid ?? null;
|
|
276
|
+
current = shapeOf(sid);
|
|
277
|
+
redraw();
|
|
206
278
|
}
|
|
207
279
|
|
|
208
|
-
/** Whether the
|
|
280
|
+
/** Whether the pane is put away in this session, which is whether it is drawn as the spine. */
|
|
209
281
|
export function isPaneCollapsed() {
|
|
210
|
-
return collapsed
|
|
282
|
+
return current.collapsed;
|
|
211
283
|
}
|
|
212
284
|
|
|
213
285
|
// Away, and back. Two ways in and one way out, because the pane is put away far more often than it is
|
|
214
286
|
// resized: the button in the bar is the obvious one, and dragging the grip past the threshold is the other.
|
|
215
287
|
export function setPaneCollapsed(next) {
|
|
216
|
-
collapsed = next;
|
|
217
|
-
|
|
218
|
-
if (collapsed) {
|
|
288
|
+
current.collapsed = next;
|
|
289
|
+
if (current.collapsed) {
|
|
219
290
|
// Coming back to a pane that fills the window, with the terminal nowhere, is not what putting one
|
|
220
291
|
// away asks for. The width it had is remembered; the far end is not.
|
|
221
|
-
full = false;
|
|
292
|
+
current.full = false;
|
|
222
293
|
} else {
|
|
223
|
-
|
|
224
|
-
// from a window remembered narrower than this one, has to open into something a page fits in.
|
|
225
|
-
const floor = Math.min(window.innerWidth * MIN_OPEN_FRACTION, window.innerWidth * MAX_FRACTION);
|
|
226
|
-
if (width !== null && width < floor) width = floor;
|
|
294
|
+
current.width = reopenedFrom(current, window.innerWidth);
|
|
227
295
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
296
|
+
reshape();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Back to the width the pane opens at. Not a width of its own: the pane is left with none, and the
|
|
300
|
+
// stylesheet draws the default fraction, so a window resized afterwards leaves the pane at half rather
|
|
301
|
+
// than at the pixels half happened to be when the button was pressed.
|
|
302
|
+
function setHalf() {
|
|
303
|
+
current.width = null;
|
|
304
|
+
current.full = false;
|
|
305
|
+
current.collapsed = false;
|
|
306
|
+
reshape();
|
|
231
307
|
}
|
|
232
308
|
|
|
233
309
|
// The far end, and back. A press either way, and a drag of the grip is the other way back.
|
|
234
310
|
function setFull(next) {
|
|
235
|
-
full = next;
|
|
236
|
-
if (full) collapsed = false;
|
|
237
|
-
|
|
238
|
-
renderShell();
|
|
239
|
-
renderers.get(openTool)?.();
|
|
311
|
+
current.full = next;
|
|
312
|
+
if (current.full) current.collapsed = false;
|
|
313
|
+
reshape();
|
|
240
314
|
}
|
|
241
315
|
|
|
242
|
-
paneFull.addEventListener("click", () => setFull(!full));
|
|
316
|
+
paneFull.addEventListener("click", () => setFull(!current.full));
|
|
317
|
+
paneHalf.addEventListener("click", setHalf);
|
|
243
318
|
paneCollapse.addEventListener("click", () => setPaneCollapsed(true));
|
|
244
319
|
spine.addEventListener("click", () => setPaneCollapsed(false));
|
|
245
320
|
|
|
@@ -275,37 +350,27 @@ let widthBeforeDrag = null;
|
|
|
275
350
|
|
|
276
351
|
grip.addEventListener("pointerdown", (event) => {
|
|
277
352
|
dragging = true;
|
|
278
|
-
widthBeforeDrag = width;
|
|
353
|
+
widthBeforeDrag = current.width;
|
|
279
354
|
document.body.classList.add("dragging");
|
|
280
355
|
grip.setPointerCapture(event.pointerId);
|
|
281
356
|
});
|
|
282
357
|
|
|
283
358
|
grip.addEventListener("pointermove", (event) => {
|
|
284
359
|
if (!dragging) return;
|
|
285
|
-
// A drag is a width, so it is the way out of the far end as well as the way into any other width.
|
|
286
|
-
full = false;
|
|
287
360
|
const wanted = Math.max(0, window.innerWidth - event.clientX - grip.offsetWidth / 2);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
collapsed = false;
|
|
293
|
-
width = Math.min(wanted, window.innerWidth * MAX_FRACTION);
|
|
294
|
-
}
|
|
295
|
-
// A drag is an answer too, so what it decides is what the next render starts from.
|
|
296
|
-
shut = collapsed;
|
|
297
|
-
applyGeometry(shut);
|
|
361
|
+
// A drag is a width, so it is the way into every state the pane has and the way out of each of them.
|
|
362
|
+
// Where each of those states begins is pane-shape.js's rule; what is left here is the pointer.
|
|
363
|
+
Object.assign(current, draggedTo(wanted, window.innerWidth, widthBeforeDrag));
|
|
364
|
+
applyGeometry();
|
|
298
365
|
});
|
|
299
366
|
|
|
300
367
|
function endDrag() {
|
|
301
368
|
if (!dragging) return;
|
|
302
369
|
dragging = false;
|
|
303
370
|
document.body.classList.remove("dragging");
|
|
304
|
-
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
renderShell();
|
|
308
|
-
renderers.get(openTool)?.();
|
|
371
|
+
// The shape changed under the pointer, and only the geometry followed it. A drag that crossed either
|
|
372
|
+
// threshold also changed which of the three states the pane is in, which the tool has to hear about.
|
|
373
|
+
reshape();
|
|
309
374
|
}
|
|
310
375
|
|
|
311
376
|
grip.addEventListener("pointerup", endDrag);
|
|
@@ -31,7 +31,6 @@ 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,
|
|
@@ -55,11 +54,14 @@ const AGE_TICK_MS = 30_000;
|
|
|
55
54
|
// heard about, so switching back to a tab lands on the page it was left on.
|
|
56
55
|
const panes = new Map();
|
|
57
56
|
|
|
58
|
-
// Which of TEXT_SIZES the shown page is drawn at.
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
// Which of TEXT_SIZES the shown page is drawn at. It belongs to the session, so it is asked of the pane each
|
|
58
|
+
// time rather than held here: a switch changes the answer. The multiplier is what the pane stores, so a
|
|
59
|
+
// session left at a size this version no longer offers falls back to the default instead of landing between
|
|
60
|
+
// two steps.
|
|
61
|
+
function sizeStep() {
|
|
62
|
+
const at = TEXT_SIZES.indexOf(storedPageSize());
|
|
63
|
+
return at === -1 ? DEFAULT_SIZE_STEP : at;
|
|
64
|
+
}
|
|
63
65
|
|
|
64
66
|
// --- The tool's own state ----------------------------------------------------------------------------------------------------------------
|
|
65
67
|
|
|
@@ -91,9 +93,8 @@ export function applyPages(msg) {
|
|
|
91
93
|
select(msg.sid, arrived.id, { user: false });
|
|
92
94
|
if (isToolOpen(PAGES)) flashPane();
|
|
93
95
|
}
|
|
94
|
-
// A pane someone put away comes back for a page
|
|
95
|
-
//
|
|
96
|
-
// the pane in every other empty session in this window too.
|
|
96
|
+
// A pane someone put away comes back for a page, in the session the page was published in - which is
|
|
97
|
+
// the session this frame is about, since the list only reaches the tool for the one it is driving.
|
|
97
98
|
if (arrived !== null && isToolOpen(PAGES) && isPaneCollapsed()) setPaneCollapsed(false);
|
|
98
99
|
else render();
|
|
99
100
|
}
|
|
@@ -172,7 +173,7 @@ function introState() {
|
|
|
172
173
|
const wrap = document.createElement("div");
|
|
173
174
|
wrap.className = "pane-intro";
|
|
174
175
|
const title = document.createElement("h2");
|
|
175
|
-
title.textContent = "
|
|
176
|
+
title.textContent = "Pages";
|
|
176
177
|
const lede = document.createElement("p");
|
|
177
178
|
lede.textContent = "Ask the agent for a page whenever the answer is longer or more structured than the terminal carries well:";
|
|
178
179
|
const asks = document.createElement("ul");
|
|
@@ -230,38 +231,41 @@ function chipFor(sid, page, selected) {
|
|
|
230
231
|
// would have to be put back on every one of those paths. Setting it out here costs nothing and cannot be
|
|
231
232
|
// missed.
|
|
232
233
|
function applyTextSize() {
|
|
233
|
-
const
|
|
234
|
+
const step = sizeStep();
|
|
235
|
+
const size = TEXT_SIZES[step];
|
|
234
236
|
toolPages.style.setProperty("--page-zoom", String(size));
|
|
235
237
|
pageSizeVal.textContent = `${Math.round(size * 100)}%`;
|
|
236
238
|
// A control that can do nothing says so, rather than swallowing the press.
|
|
237
|
-
pageSmaller.disabled =
|
|
238
|
-
pageBigger.disabled =
|
|
239
|
+
pageSmaller.disabled = step === 0;
|
|
240
|
+
pageBigger.disabled = step === TEXT_SIZES.length - 1;
|
|
239
241
|
}
|
|
240
242
|
|
|
241
243
|
function stepTextSize(by) {
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
244
|
+
const step = sizeStep();
|
|
245
|
+
const next = Math.min(Math.max(step + by, 0), TEXT_SIZES.length - 1);
|
|
246
|
+
if (next === step) return;
|
|
247
|
+
rememberPageSize(TEXT_SIZES[next]);
|
|
245
248
|
applyTextSize();
|
|
246
|
-
rememberPageSize(TEXT_SIZES[sizeStep]);
|
|
247
249
|
}
|
|
248
250
|
|
|
249
251
|
pageSmaller.addEventListener("click", () => stepTextSize(-1));
|
|
250
252
|
pageBigger.addEventListener("click", () => stepTextSize(1));
|
|
251
|
-
applyTextSize();
|
|
252
253
|
|
|
253
254
|
function render() {
|
|
255
|
+
// The zoom is the session's, so it is applied wherever this tool draws rather than once at load: the
|
|
256
|
+
// first draw is already in a session's shape, and every switch draws again.
|
|
257
|
+
applyTextSize();
|
|
254
258
|
const sid = attachedSid;
|
|
255
259
|
const entry = sid === null ? null : (panes.get(sid) ?? null);
|
|
256
260
|
const pages = entry?.pages ?? [];
|
|
257
261
|
const unread = pages.filter((page) => page.unread).length;
|
|
258
262
|
|
|
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.
|
|
263
|
+
// Nothing published in this session, and nothing to say about it. The tab stays and the pane stays open,
|
|
264
|
+
// because a pane that only appears once an agent has used it is one nobody knows to ask for. What the
|
|
265
|
+
// empty pane says is the tool's own intro; the tab's mark is the shell's, from what the tool says here.
|
|
263
266
|
const bare = pages.length === 0 && !entry?.notice;
|
|
264
|
-
|
|
267
|
+
// A page that arrived is a thing to look at, not a thing to answer, so the tab says so without a number.
|
|
268
|
+
noteTool(PAGES, { unread, empty: bare, counted: false });
|
|
265
269
|
|
|
266
270
|
chips.replaceChildren(...pages.map((page) => chipFor(sid, page, page.id === entry?.selected)));
|
|
267
271
|
const at = pages.findIndex((page) => page.id === entry?.selected);
|
|
@@ -291,6 +295,11 @@ function render() {
|
|
|
291
295
|
pageDoc.append(emptyState(notice));
|
|
292
296
|
return;
|
|
293
297
|
}
|
|
298
|
+
// Fetching a page is what marks it read - the server owns that, and it answers with a fresh list. So a
|
|
299
|
+
// tool nobody is looking at must not fetch: the tab's own mark, set a few lines up, would be cleared by
|
|
300
|
+
// the frame that fetch provokes, before anyone had seen it. The document is drawn when the user opens
|
|
301
|
+
// the tool, by this same function.
|
|
302
|
+
if (!isToolOpen(PAGES)) return;
|
|
294
303
|
show(sid, entry.selected);
|
|
295
304
|
// A chip that scrolled out of the bar while the user was reading something else.
|
|
296
305
|
chips.querySelector(".chip.sel")?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
@@ -300,7 +309,7 @@ function render() {
|
|
|
300
309
|
registerTool(PAGES, render);
|
|
301
310
|
|
|
302
311
|
setInterval(() => {
|
|
303
|
-
if (!
|
|
312
|
+
if (!isPaneCollapsed() && isToolOpen(PAGES)) render();
|
|
304
313
|
}, AGE_TICK_MS);
|
|
305
314
|
|
|
306
315
|
// A bar that fits at one width may not at another, and the arrows only exist when it does not.
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
import { toolReviews } from "./dom.js";
|
|
7
|
+
import { REVIEWS } from "./pane-shell.js";
|
|
8
|
+
import { createToolHost } from "./tool-host.js";
|
|
9
|
+
|
|
10
|
+
createToolHost({
|
|
11
|
+
id: REVIEWS,
|
|
12
|
+
label: "Code Reviews",
|
|
13
|
+
panel: toolReviews,
|
|
14
|
+
opening: "Opening the reviews in this repository.",
|
|
15
|
+
});
|