chamba 0.6.1 → 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.
Files changed (118) hide show
  1. package/LICENSE +53 -14
  2. package/README.md +38 -11
  3. package/dist/commands/advanced.js +7 -278
  4. package/dist/commands/dev.js +11 -612
  5. package/dist/commands/doctor.js +1 -29
  6. package/dist/commands/menu.js +1 -80
  7. package/dist/commands/onboard.js +6 -230
  8. package/dist/commands/settings.js +15 -349
  9. package/dist/lib/agent-commands.js +2 -0
  10. package/dist/lib/agent-context.js +6 -184
  11. package/dist/lib/browser.js +1 -40
  12. package/dist/lib/chamba-yaml.js +10 -191
  13. package/dist/lib/constants.js +1 -125
  14. package/dist/lib/dockerfile-builder.js +41 -260
  15. package/dist/lib/env.js +2 -78
  16. package/dist/lib/git-env.js +1 -21
  17. package/dist/lib/global-config.js +4 -66
  18. package/dist/lib/pnpm-store.js +1 -19
  19. package/dist/lib/ports.js +1 -210
  20. package/dist/lib/safe-rm.js +1 -26
  21. package/dist/lib/sessions.js +2 -34
  22. package/dist/lib/shadows.js +1 -176
  23. package/dist/lib/skills.js +2 -0
  24. package/dist/lib/templates.js +1 -0
  25. package/dist/lib/webterm.js +1 -305
  26. package/dist/lib/workspace-identity.js +4 -260
  27. package/package.json +6 -6
  28. package/templates/Dockerfile +24 -12
  29. package/templates/claude-statusline.sh +5 -7
  30. package/templates/context/baseline.md +2 -0
  31. package/templates/pane-apps/client/assets/c-BNa-DeUl.json +3552 -0
  32. package/templates/pane-apps/client/assets/cpp-B-T4NRoH.json +20239 -0
  33. package/templates/pane-apps/client/assets/csharp-BbxHChhA.json +6010 -0
  34. package/templates/pane-apps/client/assets/css-CWCxuwmz.json +1860 -0
  35. package/templates/pane-apps/client/assets/diff-B-2N89g8.json +154 -0
  36. package/templates/pane-apps/client/assets/docker-DWTvYW26.json +103 -0
  37. package/templates/pane-apps/client/assets/files-D0nJgFAA.js +12 -0
  38. package/templates/pane-apps/client/assets/files-h0K1vZyQ.css +1 -0
  39. package/templates/pane-apps/client/assets/go-B2hyYeNg.json +3156 -0
  40. package/templates/pane-apps/client/assets/graphql-Z6Id2aNO.json +1292 -0
  41. package/templates/pane-apps/client/assets/html-BtzvlIfu.json +2627 -0
  42. package/templates/pane-apps/client/assets/ini-Bn3Twap1.json +108 -0
  43. package/templates/pane-apps/client/assets/java-C7t2dwLO.json +1885 -0
  44. package/templates/pane-apps/client/assets/javascript-DuC2v0C0.json +5996 -0
  45. package/templates/pane-apps/client/assets/json-CwdkWeV5.json +206 -0
  46. package/templates/pane-apps/client/assets/jsonc-MYWDbTU3.json +206 -0
  47. package/templates/pane-apps/client/assets/jsx-Cn8EZuIC.json +5996 -0
  48. package/templates/pane-apps/client/assets/kotlin-CFNOz1gw.json +580 -0
  49. package/templates/pane-apps/client/assets/make-BVnlFCcl.json +629 -0
  50. package/templates/pane-apps/client/assets/markdown-B4Xk2WM_.json +3263 -0
  51. package/templates/pane-apps/client/assets/php-CAQzQmmI.json +4188 -0
  52. package/templates/pane-apps/client/assets/python-CYQee8m4.json +4174 -0
  53. package/templates/pane-apps/client/assets/reviews-BsoGZ35r.css +1 -0
  54. package/templates/pane-apps/client/assets/reviews-iW0Mbn4e.js +153 -0
  55. package/templates/pane-apps/client/assets/ruby-D7R58tus.json +3114 -0
  56. package/templates/pane-apps/client/assets/rust-BFBbFiei.json +1103 -0
  57. package/templates/pane-apps/client/assets/scss-TQRhBkPG.json +1874 -0
  58. package/templates/pane-apps/client/assets/shellscript-DieAkC4W.json +2375 -0
  59. package/templates/pane-apps/client/assets/specs-D9z3neR-.js +13 -0
  60. package/templates/pane-apps/client/assets/sql-Bxd030LV.json +634 -0
  61. package/templates/pane-apps/client/assets/toml-ChrKGzsV.json +430 -0
  62. package/templates/pane-apps/client/assets/tsx-D2e90MFw.json +5996 -0
  63. package/templates/pane-apps/client/assets/typescript-DonXQ_n1.json +5747 -0
  64. package/templates/pane-apps/client/assets/xml-B17r2RXF.json +382 -0
  65. package/templates/pane-apps/client/assets/yaml-CKn4h1zy.json +627 -0
  66. package/templates/pane-apps/client/reviews/index.html +15 -0
  67. package/templates/pane-apps/client/specs/index.html +14 -0
  68. package/templates/pane-apps/server/reviews.mjs +20 -0
  69. package/templates/pane-apps/server/specs.mjs +5 -0
  70. package/templates/skills/chamba-statusline/SKILL.md +7 -19
  71. package/templates/skills/dx-review/SKILL.md +123 -0
  72. package/templates/skills/dx-review/references/acts.md +162 -0
  73. package/templates/skills/dx-spec/SKILL.md +561 -0
  74. package/templates/skills/dx-spec/references/imagination-guide.md +140 -0
  75. package/templates/skills/dx-spec/references/principles-template.md +2 -0
  76. package/templates/skills/dx-spec/references/review-guide.md +127 -0
  77. package/templates/skills/dx-spec/references/spec-guide.md +145 -0
  78. package/templates/skills/dx-spec/references/stages.md +484 -0
  79. package/templates/skills/dx-spec-execute/SKILL.md +437 -0
  80. package/templates/startup.mjs +10 -4
  81. package/templates/tool-helper.sh +166 -0
  82. package/templates/webterm/README.md +79 -9
  83. package/templates/webterm/color.js +61 -0
  84. package/templates/webterm/config.js +74 -0
  85. package/templates/webterm/context/claude.md +0 -1
  86. package/templates/webterm/public/app/alerts.js +4 -4
  87. package/templates/webterm/public/app/anchored.js +81 -0
  88. package/templates/webterm/public/app/composer.js +4 -1
  89. package/templates/webterm/public/app/connection.js +9 -0
  90. package/templates/webterm/public/app/dom.js +15 -5
  91. package/templates/webterm/public/app/frames.js +51 -10
  92. package/templates/webterm/public/app/main.js +26 -6
  93. package/templates/webterm/public/app/new-session.js +13 -41
  94. package/templates/webterm/public/app/palette.js +58 -0
  95. package/templates/webterm/public/app/pane-shape.js +167 -0
  96. package/templates/webterm/public/app/pane-shell.js +380 -0
  97. package/templates/webterm/public/app/pane.js +77 -193
  98. package/templates/webterm/public/app/reviews-host.js +15 -0
  99. package/templates/webterm/public/app/specs-host.js +16 -0
  100. package/templates/webterm/public/app/state.js +5 -0
  101. package/templates/webterm/public/app/tabs.js +6 -2
  102. package/templates/webterm/public/app/terminal.js +8 -0
  103. package/templates/webterm/public/app/theme.js +75 -27
  104. package/templates/webterm/public/app/tool-host.js +302 -0
  105. package/templates/webterm/public/app/workspace-color.js +192 -0
  106. package/templates/webterm/public/index.html +68 -30
  107. package/templates/webterm/public/styles.css +270 -34
  108. package/templates/webterm/server.js +321 -0
  109. package/templates/webterm/tool-document.js +67 -0
  110. package/templates/webterm/tools/commands.js +61 -0
  111. package/templates/webterm/tools/index.js +25 -0
  112. package/templates/webterm/tools/mount.js +337 -0
  113. package/templates/webterm/tools/paths.js +34 -0
  114. package/templates/webterm/tools/reviews.js +133 -0
  115. package/templates/webterm/tools/specs.js +154 -0
  116. package/templates/webterm/typed-line.js +148 -0
  117. package/templates/context/context-usage.md +0 -1
  118. package/templates/context-usage.sh +0 -266
@@ -0,0 +1,380 @@
1
+ // pane-shell.js - the pane itself: which tool is open in it, and how wide it is.
2
+ //
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.
8
+ //
9
+ // Width has three states, and only one of them is a width. Between the two ends the divider drags anything,
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
+ // takes the whole row and the terminal is squeezed out - which is a state rather than a wider drag, so the
12
+ // drag's own clamps stay where they are. The grip is reachable in every one of them, and dragging it is
13
+ // always the way back.
14
+ //
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.
18
+ //
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 };
56
+
57
+ // The tools, in the order their tabs are drawn. A tool is a tab, a panel, and a render function it hands over
58
+ // when it loads; the pane knows nothing else about any of them.
59
+ const TOOLS = [
60
+ { id: PAGES, label: "Pages", panel: toolPages },
61
+ { id: SPECS, label: "Specs", panel: toolSpecs },
62
+ { id: REVIEWS, label: "Code Reviews", panel: toolReviews },
63
+ ];
64
+
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;
74
+
75
+ // id -> what that tool has to say for itself on its tab: how many things are waiting in it, and whether it
76
+ // has anything at all. A tool nobody has heard from yet is empty and waiting on nothing.
77
+ const EMPTY_MARK = { unread: 0, empty: true, counted: true };
78
+ const marks = new Map();
79
+ // id -> the tool's own render function, handed over as it loads.
80
+ const renderers = new Map();
81
+
82
+ // --- What every session remembers --------------------------------------------------------------------------------------------------------
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.
87
+ function remember() {
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();
131
+ }
132
+
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. */
139
+ export function storedPageSize() {
140
+ return current.size;
141
+ }
142
+
143
+ /** The Pages tool's zoom, kept with the rest of the session's shape so one key has one writer. */
144
+ export function rememberPageSize(size) {
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;
162
+ remember();
163
+ }
164
+
165
+ // --- The tools ---------------------------------------------------------------------------------------------------------------------------
166
+
167
+ /**
168
+ * A tool says how to draw its panel. Called as the tool's module loads, which is why this module imports
169
+ * nothing that could still be half-built at that point.
170
+ */
171
+ export function registerTool(id, render) {
172
+ renderers.set(id, render);
173
+ }
174
+
175
+ /** Whether the pane is open on that tool right now. */
176
+ export function isToolOpen(id) {
177
+ return current.tool === id;
178
+ }
179
+
180
+ /**
181
+ * What a tool has to say on its tab: `unread` is what is waiting for the user in it, and `empty` is whether
182
+ * it has anything to show at all - which is what an untouched pane reads to decide whether to stay out of
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.
188
+ */
189
+ export function noteTool(id, { unread = 0, empty = false, counted = true } = {}) {
190
+ marks.set(id, { unread, empty, counted });
191
+ renderShell();
192
+ }
193
+
194
+ /** Open a tool, drawing its panel. A tool the user chose is the tool this session comes back on. */
195
+ export function showTool(id) {
196
+ if (!TOOLS.some((tool) => tool.id === id)) return;
197
+ current.tool = id;
198
+ reshape();
199
+ }
200
+
201
+ function drawTabs() {
202
+ const tabs = TOOLS.map((tool) => {
203
+ const button = document.createElement("button");
204
+ button.type = "button";
205
+ button.className = tool.id === current.tool ? "tool-tab on" : "tool-tab";
206
+ button.dataset.tool = tool.id;
207
+ button.append(tool.label);
208
+ // What is waiting in a tool nobody is looking at. The one thing a tab says beyond its name, so a tool
209
+ // never has to take the screen to be noticed.
210
+ const mark = marks.get(tool.id) ?? EMPTY_MARK;
211
+ if (mark.unread > 0) {
212
+ const badge = document.createElement("span");
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");
216
+ button.append(badge);
217
+ }
218
+ button.addEventListener("click", () => showTool(tool.id));
219
+ return button;
220
+ });
221
+ toolTabs.replaceChildren(...tabs);
222
+ for (const tool of TOOLS) tool.panel.hidden = tool.id !== current.tool;
223
+ }
224
+
225
+ // --- How wide ----------------------------------------------------------------------------------------------------------------------------
226
+
227
+ // How wide, put away, or the whole row. Its own function because a drag is the one thing that changes the
228
+ // pane without changing anything in it: sixty of these a second are cheap, and sixty full redraws are not.
229
+ function applyGeometry() {
230
+ const { width, full, collapsed } = current;
231
+ pane.classList.toggle("collapsed", collapsed);
232
+ // Only one of the two can be true: a pane that is put away has no width to be full of.
233
+ pane.classList.toggle("full", full && !collapsed);
234
+ // The width goes into a custom property rather than straight onto `width`, so the stylesheet keeps the
235
+ // last word: the collapsed spine, the full row and the narrow-screen layout are rules, and an inline
236
+ // width would beat all three.
237
+ pane.style.setProperty("--pane-width", width === null ? `${DEFAULT_FRACTION * 100}%` : `${width}px`);
238
+ paneFull.classList.toggle("on", full && !collapsed);
239
+ paneHalf.classList.toggle("on", isDefaultWidth(current));
240
+ const back = full && !collapsed;
241
+ paneFull.title = back ? "Give the terminal its room back" : "Full width";
242
+ paneFull.setAttribute("aria-label", paneFull.title);
243
+ }
244
+
245
+ /** Draw the pane's own chrome: the tabs, the width, and what the spine says while the pane is away. */
246
+ export function renderShell() {
247
+ const mark = marks.get(current.tool) ?? EMPTY_MARK;
248
+ pane.hidden = false;
249
+ // There is no width worth choosing for a tool with nothing in it.
250
+ grip.hidden = mark.empty && current.collapsed;
251
+
252
+ applyGeometry();
253
+ drawTabs();
254
+
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.
258
+ let waiting = 0;
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
+ }
265
+ spineBadge.textContent = waiting > 0 ? String(waiting) : "";
266
+ spineBadge.classList.toggle("dot", waiting === 0 && arrived);
267
+ spineBadge.hidden = waiting === 0 && !arrived;
268
+ }
269
+
270
+ /**
271
+ * Draw the pane for the first time, on the tool this window was left on. Called from the entry rather than
272
+ * here, because the tools register as they load and there is no pane to draw until they all have.
273
+ */
274
+ export function mountPane(startSid = null) {
275
+ sid = startSid ?? null;
276
+ current = shapeOf(sid);
277
+ redraw();
278
+ }
279
+
280
+ /** Whether the pane is put away in this session, which is whether it is drawn as the spine. */
281
+ export function isPaneCollapsed() {
282
+ return current.collapsed;
283
+ }
284
+
285
+ // Away, and back. Two ways in and one way out, because the pane is put away far more often than it is
286
+ // resized: the button in the bar is the obvious one, and dragging the grip past the threshold is the other.
287
+ export function setPaneCollapsed(next) {
288
+ current.collapsed = next;
289
+ if (current.collapsed) {
290
+ // Coming back to a pane that fills the window, with the terminal nowhere, is not what putting one
291
+ // away asks for. The width it had is remembered; the far end is not.
292
+ current.full = false;
293
+ } else {
294
+ current.width = reopenedFrom(current, window.innerWidth);
295
+ }
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();
307
+ }
308
+
309
+ // The far end, and back. A press either way, and a drag of the grip is the other way back.
310
+ function setFull(next) {
311
+ current.full = next;
312
+ if (current.full) current.collapsed = false;
313
+ reshape();
314
+ }
315
+
316
+ paneFull.addEventListener("click", () => setFull(!current.full));
317
+ paneHalf.addEventListener("click", setHalf);
318
+ paneCollapse.addEventListener("click", () => setPaneCollapsed(true));
319
+ spine.addEventListener("click", () => setPaneCollapsed(false));
320
+
321
+ // --- Saying that something arrived -------------------------------------------------------------------------------------------------------
322
+
323
+ // One pulse of the pane's edge, in the workspace colour. ARRIVAL_MS must match the keyframes in styles.css.
324
+ const ARRIVAL_MS = 1_000;
325
+ let arrivalTimer = null;
326
+
327
+ // The class comes off on a timer rather than on animationend, because with a reduced-motion preference there
328
+ // is no animation to end - the stylesheet holds the highlight instead of pulsing it, and this is what makes
329
+ // the two last the same moment.
330
+ export function flashPane() {
331
+ clearTimeout(arrivalTimer);
332
+ // A pulse still running when the next page lands restarts rather than stacking: off, force the browser to
333
+ // notice, on again. Reading a layout property is what makes the restart real.
334
+ pane.classList.remove("arriving");
335
+ void pane.offsetWidth;
336
+ pane.classList.add("arriving");
337
+ arrivalTimer = setTimeout(() => pane.classList.remove("arriving"), ARRIVAL_MS);
338
+ }
339
+
340
+ // --- The divider -------------------------------------------------------------------------------------------------------------------------
341
+
342
+ // Dragging sets a pixel width until it stops making sense, and below the threshold the pane becomes the
343
+ // spine rather than a sliver. Pointer capture keeps the drag alive over the terminal and the frame alike -
344
+ // a page is a document with its own event loop, and without capture the pointer would be lost in it.
345
+ let dragging = false;
346
+ // The width the pane had when the drag started. Dragging it shut is a request to put it away, not a request
347
+ // for a 150px pane, so crossing the threshold hands its old width back rather than keeping the sliver the
348
+ // pointer passed through on the way there.
349
+ let widthBeforeDrag = null;
350
+
351
+ grip.addEventListener("pointerdown", (event) => {
352
+ dragging = true;
353
+ widthBeforeDrag = current.width;
354
+ document.body.classList.add("dragging");
355
+ grip.setPointerCapture(event.pointerId);
356
+ });
357
+
358
+ grip.addEventListener("pointermove", (event) => {
359
+ if (!dragging) return;
360
+ const wanted = Math.max(0, window.innerWidth - event.clientX - grip.offsetWidth / 2);
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();
365
+ });
366
+
367
+ function endDrag() {
368
+ if (!dragging) return;
369
+ dragging = false;
370
+ document.body.classList.remove("dragging");
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();
374
+ }
375
+
376
+ grip.addEventListener("pointerup", endDrag);
377
+ grip.addEventListener("pointercancel", endDrag);
378
+
379
+ // The bar is chrome: a click on it must not leave the terminal without focus for no reason.
380
+ panebar.addEventListener("mousedown", (event) => event.preventDefault());