chamba 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -6
- package/dist/lib/agent-context.js +33 -7
- package/dist/lib/dockerfile-builder.js +2 -1
- package/dist/lib/safe-rm.js +13 -3
- package/package.json +3 -3
- package/templates/Dockerfile +20 -1
- package/templates/pane-apps/client/assets/specs-B1970L17.css +1 -0
- package/templates/pane-apps/client/assets/specs-cEee_SPn.js +23 -0
- package/templates/pane-apps/client/specs/index.html +13 -0
- package/templates/pane-apps/server/specs.mjs +1588 -0
- package/templates/skills/dx-spec/SKILL.md +365 -0
- package/templates/skills/dx-spec/references/imagination-guide.md +140 -0
- package/templates/skills/dx-spec/references/review-guide.md +173 -0
- package/templates/skills/dx-spec/references/spec-guide.md +125 -0
- package/templates/skills/dx-spec/references/stages.md +399 -0
- package/templates/skills/dx-spec-config/SKILL.md +313 -0
- package/templates/skills/dx-spec-config/references/principles-template.md +12 -0
- package/templates/skills/dx-spec-execute/SKILL.md +324 -0
- package/templates/specs.sh +106 -0
- package/templates/webterm/README.md +42 -6
- package/templates/webterm/config.js +43 -0
- package/templates/webterm/public/app/composer.js +4 -1
- package/templates/webterm/public/app/dom.js +13 -5
- package/templates/webterm/public/app/frames.js +7 -0
- package/templates/webterm/public/app/main.js +7 -1
- package/templates/webterm/public/app/pane-shell.js +315 -0
- package/templates/webterm/public/app/pane.js +58 -183
- package/templates/webterm/public/app/specs-host.js +222 -0
- package/templates/webterm/public/app/terminal.js +8 -0
- package/templates/webterm/public/index.html +51 -27
- package/templates/webterm/public/styles.css +144 -30
- package/templates/webterm/server.js +273 -0
- package/templates/webterm/specs.js +358 -0
- package/templates/webterm/tool-document.js +67 -0
- package/templates/webterm/typed-line.js +85 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// specs-host.js - the Specs tool: the spec workspace, in a frame of its own.
|
|
2
|
+
//
|
|
3
|
+
// The tool's client is not part of this interface. It is built elsewhere and rendered here in a frame with an
|
|
4
|
+
// opaque origin, the same way a published page is, so the shell's key never enters it and one rendering bug
|
|
5
|
+
// can leak spec reading at worst. This module is the host around that frame: it owns the panel, the frame,
|
|
6
|
+
// and the one message that gives the frame what it needs to ask the server anything.
|
|
7
|
+
//
|
|
8
|
+
// The handover is the whole point of this file. The frame gets a Specs-scoped key that opens the tool's data
|
|
9
|
+
// routes and a path prefix that opens workspace files, and it gets them once, unprompted, on the load of the
|
|
10
|
+
// document this shell put there. It is never handed out on request: a window object survives a navigation, so
|
|
11
|
+
// a document that navigated itself into this frame would ask with the same identity as the one that was
|
|
12
|
+
// loaded. A second load is therefore read as a navigation, and the frame is thrown away and built again.
|
|
13
|
+
|
|
14
|
+
import { sendFrame } from "./connection.js";
|
|
15
|
+
import { toolSpecs } from "./dom.js";
|
|
16
|
+
import { noteTool, registerTool, SPECS } from "./pane-shell.js";
|
|
17
|
+
import { KEY_HEADER, WEB_KEY } from "./state.js";
|
|
18
|
+
|
|
19
|
+
// Where the built client is served from, and where the shell asks for the frame's credentials. The second one
|
|
20
|
+
// is behind the master key in a header, which is the shell's own gate and never the frame's.
|
|
21
|
+
const FRAME_SRC = "/pane-apps/specs/index.html";
|
|
22
|
+
const KEYS_URL = "/specs/keys";
|
|
23
|
+
|
|
24
|
+
// What the shell and the frame call each other's messages. Namespaced because a frame may be talking to
|
|
25
|
+
// something else of its own, and every message is checked against the frame it must come from.
|
|
26
|
+
const HANDOVER = "specs:credentials";
|
|
27
|
+
const CHANGE = "specs:change";
|
|
28
|
+
const OPEN = "specs:open";
|
|
29
|
+
const DELIVER = "specs:deliver";
|
|
30
|
+
const WAITING = "specs:waiting";
|
|
31
|
+
const DELIVERED = "specs:delivered";
|
|
32
|
+
|
|
33
|
+
// How many times a frame that navigates itself is rebuilt before the tab says so instead. A client that keeps
|
|
34
|
+
// leaving the document it was given is broken or is being driven, and either way rebuilding it for ever is a
|
|
35
|
+
// spinning window rather than a recovery.
|
|
36
|
+
const MAX_REBUILDS = 3;
|
|
37
|
+
|
|
38
|
+
let frame = null;
|
|
39
|
+
let handed = false;
|
|
40
|
+
let rebuilds = 0;
|
|
41
|
+
let credentials = null;
|
|
42
|
+
let asking = false;
|
|
43
|
+
let failure = null;
|
|
44
|
+
// How many things are waiting for the user inside the tool. Kept here rather than in the frame, because the
|
|
45
|
+
// mark has to hold while another tool is open and the frame is not being drawn.
|
|
46
|
+
let waiting = 0;
|
|
47
|
+
|
|
48
|
+
/** Fetch the frame's credentials. The shell holds the master key; this is the only route it opens for Specs. */
|
|
49
|
+
async function fetchCredentials() {
|
|
50
|
+
if (asking) return;
|
|
51
|
+
asking = true;
|
|
52
|
+
try {
|
|
53
|
+
const response = await fetch(KEYS_URL, { headers: { [KEY_HEADER]: WEB_KEY }, cache: "no-store" });
|
|
54
|
+
if (!response.ok) throw new Error(String(response.status));
|
|
55
|
+
const answer = await response.json();
|
|
56
|
+
if (typeof answer?.key !== "string" || typeof answer?.raw !== "string") throw new Error("no credentials");
|
|
57
|
+
credentials = answer;
|
|
58
|
+
failure = null;
|
|
59
|
+
} catch {
|
|
60
|
+
failure = "The Specs tool could not be reached. The next time this tab is opened it tries again.";
|
|
61
|
+
} finally {
|
|
62
|
+
asking = false;
|
|
63
|
+
}
|
|
64
|
+
// Drawn whether the tab is open or not. The panel is off the screen when it is not, so nothing is shown
|
|
65
|
+
// either way, but this is the step that puts the frame up - and the tool has to be running to say what
|
|
66
|
+
// is waiting for the user, wherever in the pane that user happens to be.
|
|
67
|
+
render();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Put a fresh frame in the panel, replacing whatever was there. */
|
|
71
|
+
function build() {
|
|
72
|
+
const next = document.createElement("iframe");
|
|
73
|
+
next.className = "tool-frame";
|
|
74
|
+
// The whole boundary in one attribute: scripts and forms, and no same-origin, so the document has an
|
|
75
|
+
// opaque origin and nothing of this window is reachable from inside it - not the DOM, not storage, and
|
|
76
|
+
// not the URL that carries the master key. Set before the src, since the sandbox of a frame that already
|
|
77
|
+
// has a document is not what a later attribute says it is.
|
|
78
|
+
next.setAttribute("sandbox", "allow-scripts allow-forms");
|
|
79
|
+
// A frame inherits its embedder's referrer, and this window's URL carries the master key - both as
|
|
80
|
+
// `document.referrer` inside the frame and as the `Referer` header on everything it loads.
|
|
81
|
+
next.setAttribute("referrerpolicy", "no-referrer");
|
|
82
|
+
next.setAttribute("title", "Specs");
|
|
83
|
+
next.addEventListener("load", () => onLoad(next));
|
|
84
|
+
next.src = FRAME_SRC;
|
|
85
|
+
frame = next;
|
|
86
|
+
handed = false;
|
|
87
|
+
toolSpecs.replaceChildren(next);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The frame finished loading a document. The first one is the client; a second one is a navigation. */
|
|
91
|
+
function onLoad(loaded) {
|
|
92
|
+
if (loaded !== frame) return;
|
|
93
|
+
if (!handed) {
|
|
94
|
+
handed = true;
|
|
95
|
+
loaded.contentWindow?.postMessage({ t: HANDOVER, key: credentials.key, raw: credentials.raw }, "*");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (rebuilds >= MAX_REBUILDS) {
|
|
99
|
+
frame = null;
|
|
100
|
+
failure = "The Specs tool kept leaving the page it was given, so it was stopped.";
|
|
101
|
+
render();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
rebuilds += 1;
|
|
105
|
+
build();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** One sentence in the middle of the panel: what the tab is doing, or why it is doing nothing. */
|
|
109
|
+
function say(line) {
|
|
110
|
+
const wrap = document.createElement("div");
|
|
111
|
+
wrap.className = "pane-intro";
|
|
112
|
+
const title = document.createElement("h2");
|
|
113
|
+
title.textContent = "Specs";
|
|
114
|
+
const lede = document.createElement("p");
|
|
115
|
+
lede.textContent = line;
|
|
116
|
+
wrap.append(title, lede);
|
|
117
|
+
toolSpecs.replaceChildren(wrap);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function render() {
|
|
121
|
+
// The tab always has something to show, so it never asks the pane to keep out of the way. What is on it
|
|
122
|
+
// is what the tool last said is waiting for the user, which holds while another tool is open.
|
|
123
|
+
noteTool(SPECS, { unread: waiting, empty: false });
|
|
124
|
+
if (failure !== null) {
|
|
125
|
+
// Said once and then forgotten, so coming back to the tab is what tries again.
|
|
126
|
+
const line = failure;
|
|
127
|
+
failure = null;
|
|
128
|
+
frame = null;
|
|
129
|
+
say(line);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// A frame that is up is left alone: it holds the tool's whole state, and rebuilding it would be a reload
|
|
133
|
+
// every time the user came back to the tab.
|
|
134
|
+
if (frame !== null) return;
|
|
135
|
+
if (credentials === null) {
|
|
136
|
+
say("Opening the specs in this repository.");
|
|
137
|
+
fetchCredentials();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
build();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The disk moved under a spec. Passed straight in: the frame decides what is worth re-reading. */
|
|
144
|
+
export function applySpecs(msg) {
|
|
145
|
+
if (frame === null || !handed) return;
|
|
146
|
+
frame.contentWindow?.postMessage({ t: CHANGE, change: msg.change }, "*");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Whether a message is the tool asking for something, rather than anything else on this window.
|
|
151
|
+
*
|
|
152
|
+
* Two checks, and each one holds without the other. The sender must be the frame's own window, which refuses
|
|
153
|
+
* a workspace document nested inside the tool - it is a frame of its own and can post here. And the message
|
|
154
|
+
* must carry the Specs key, which only the document this shell loaded ever received, so a document that
|
|
155
|
+
* navigated the frame passes the window check and fails this one.
|
|
156
|
+
*/
|
|
157
|
+
function fromTool(event, message) {
|
|
158
|
+
if (frame === null || !handed || credentials === null) return false;
|
|
159
|
+
if (event.source !== frame.contentWindow) return false;
|
|
160
|
+
return typeof message.key === "string" && message.key === credentials.key;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// A link the tool will not follow itself. The frame has to stay the document this shell put in it, since a
|
|
164
|
+
// navigation would keep the window identity every check here rests on, so an outside link is opened as a
|
|
165
|
+
// window of this interface instead - the web alone, and with nothing of this window carried into it.
|
|
166
|
+
window.addEventListener("message", (event) => {
|
|
167
|
+
const message = event.data;
|
|
168
|
+
if (!message || typeof message !== "object" || message.t !== OPEN) return;
|
|
169
|
+
if (!fromTool(event, message)) return;
|
|
170
|
+
let url;
|
|
171
|
+
try {
|
|
172
|
+
url = new URL(String(message.url));
|
|
173
|
+
} catch {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return;
|
|
177
|
+
window.open(url.href, "_blank", "noopener,noreferrer");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// The tool saying what it is waiting on. The count is the tool's own, and the mark it puts on the tab is the
|
|
181
|
+
// shell's - which is what makes something awaiting an answer visible from any other tool in the pane.
|
|
182
|
+
window.addEventListener("message", (event) => {
|
|
183
|
+
const message = event.data;
|
|
184
|
+
if (!message || typeof message !== "object" || message.t !== WAITING) return;
|
|
185
|
+
if (!fromTool(event, message)) return;
|
|
186
|
+
const count = Number(message.count);
|
|
187
|
+
waiting = Number.isFinite(count) && count > 0 ? Math.min(Math.floor(count), 99) : 0;
|
|
188
|
+
noteTool(SPECS, { unread: waiting, empty: false });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// A delivery: the tool telling the agent beside it that something arrived. It rides this window's own socket,
|
|
192
|
+
// so the server resolves the session from the socket the way every other pane frame does, and nothing here
|
|
193
|
+
// or in the frame names one. What travels is the kind of event, a line of detail, the file the tool wrote,
|
|
194
|
+
// and whether the user agreed to start a session for it.
|
|
195
|
+
window.addEventListener("message", (event) => {
|
|
196
|
+
const message = event.data;
|
|
197
|
+
if (!message || typeof message !== "object" || message.t !== DELIVER) return;
|
|
198
|
+
if (!fromTool(event, message)) return;
|
|
199
|
+
sendFrame({
|
|
200
|
+
t: DELIVER,
|
|
201
|
+
event: String(message.event ?? ""),
|
|
202
|
+
detail: String(message.detail ?? ""),
|
|
203
|
+
path: String(message.path ?? ""),
|
|
204
|
+
create: message.create === true,
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
/** What the server made of the last delivery, passed back to the frame that asked for it. */
|
|
209
|
+
export function applySpecsDelivery(msg) {
|
|
210
|
+
if (frame === null || !handed) return;
|
|
211
|
+
frame.contentWindow?.postMessage({ t: DELIVERED, ok: msg.ok === true, error: msg.error ?? "" }, "*");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
registerTool(SPECS, render);
|
|
215
|
+
|
|
216
|
+
// The frame is built at startup rather than the first time the tab is opened, because the mark on the tab is
|
|
217
|
+
// the tool's to put there and the tool cannot say anything until it is running. Waiting for the first open
|
|
218
|
+
// would make "something is waiting for you" visible only to a user who already went and looked, which is the
|
|
219
|
+
// one reader who did not need telling - and a window reopened on another tool would show nothing at all.
|
|
220
|
+
// Nothing is drawn by this: the panel is off the screen until its tab is picked, and the frame holds the
|
|
221
|
+
// tool's state from here on, so opening the tab shows what is already there instead of loading it then.
|
|
222
|
+
render();
|
|
@@ -87,10 +87,18 @@ term.onData((data) => {
|
|
|
87
87
|
// Keep the PTY's window size in step with the rendered terminal. Debounced: a resize storm
|
|
88
88
|
// (layout settling, pane drag) collapses to one fit + one resize, which cuts down the redraw
|
|
89
89
|
// artifacts the TUI shows when width changes mid-render.
|
|
90
|
+
//
|
|
91
|
+
// A terminal with no width on the screen is not a narrow terminal, and the PTY is not told about one.
|
|
92
|
+
// The pane at its full width leaves the row nothing, and the fit would answer with its own floor of two
|
|
93
|
+
// columns: the agent's TUI would take that as a real window, reflow its frame and its scrollback into
|
|
94
|
+
// two columns, and keep the mangled scrollback after the pane came back. Holding the last real size
|
|
95
|
+
// instead is what makes "squeezed out rather than hidden" true for the process as well as the layout -
|
|
96
|
+
// nothing is sent while it is out of the row, and the next pass sends the width it returns at.
|
|
90
97
|
let resizeTimer = null;
|
|
91
98
|
const resizeObserver = new ResizeObserver(() => {
|
|
92
99
|
if (resizeTimer) clearTimeout(resizeTimer);
|
|
93
100
|
resizeTimer = setTimeout(() => {
|
|
101
|
+
if (termEl.clientWidth === 0 || termEl.clientHeight === 0) return;
|
|
94
102
|
try {
|
|
95
103
|
fit.fit();
|
|
96
104
|
sendResize();
|
|
@@ -27,34 +27,58 @@
|
|
|
27
27
|
<span id="note"></span>
|
|
28
28
|
</div>
|
|
29
29
|
|
|
30
|
-
<!-- The web pane, rendered by app/pane.js: the
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
<!-- The web pane, rendered by app/pane-shell.js: the tool tabs and the two width controls are the
|
|
31
|
+
pane's own chrome, and one panel per tool sits under them - Pages (app/pane.js) and Specs
|
|
32
|
+
(app/specs-host.js). Both the pane and its drag handle are hidden until there is something in it. -->
|
|
33
33
|
<div id="grip" hidden></div>
|
|
34
34
|
<aside id="pane" hidden>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<
|
|
39
|
-
<span id="
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
35
|
+
<!-- The pane's own bar: which tool is open, and how wide the pane is. Nothing in it belongs to a
|
|
36
|
+
tool, so a tool can never move the pane or say which tool the user is looking at. -->
|
|
37
|
+
<div id="panebar">
|
|
38
|
+
<div id="tool-tabs"></div>
|
|
39
|
+
<span id="pane-ctl">
|
|
40
|
+
<!-- The pane's edge to the far wall, and back again. -->
|
|
41
|
+
<button type="button" id="pane-full" class="pane-nav" title="Full width" aria-label="Full width">
|
|
42
|
+
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
43
|
+
<path d="M4 5v14" />
|
|
44
|
+
<path d="M20 12H9" />
|
|
45
|
+
<path d="M13 8l-4 4 4 4" />
|
|
46
|
+
</svg>
|
|
47
|
+
</button>
|
|
48
|
+
<!-- An arrow into the pane's own edge: put it away, that way. Inline because this bar is
|
|
49
|
+
static chrome, unlike the session bar's icons, which are drawn per frame. -->
|
|
50
|
+
<button type="button" id="pane-collapse" class="pane-nav" title="Collapse the pane" aria-label="Collapse the pane">
|
|
51
|
+
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
52
|
+
<path d="M4 12h11" />
|
|
53
|
+
<path d="M11 8l4 4-4 4" />
|
|
54
|
+
<path d="M20 5v14" />
|
|
55
|
+
</svg>
|
|
56
|
+
</button>
|
|
46
57
|
</span>
|
|
47
|
-
<!-- An arrow into the pane's own edge: put it away, that way. Inline because this bar is static
|
|
48
|
-
chrome, unlike the session bar's icons, which are drawn per frame. -->
|
|
49
|
-
<button type="button" id="pane-collapse" class="pane-nav" title="Collapse the web pane" aria-label="Collapse the web pane">
|
|
50
|
-
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
51
|
-
<path d="M4 12h11" />
|
|
52
|
-
<path d="M11 8l4 4-4 4" />
|
|
53
|
-
<path d="M20 5v14" />
|
|
54
|
-
</svg>
|
|
55
|
-
</button>
|
|
56
58
|
</div>
|
|
57
|
-
|
|
59
|
+
|
|
60
|
+
<!-- Pages: the pages this session's agent published, with the chips bar and the counter drawn out
|
|
61
|
+
here by the interface, and the page itself inside a sandboxed frame. -->
|
|
62
|
+
<div id="tool-pages" class="tool-panel">
|
|
63
|
+
<div id="pagebar">
|
|
64
|
+
<button type="button" id="pane-prev" class="pane-nav" title="Older pages" aria-label="Older pages" hidden>‹</button>
|
|
65
|
+
<div id="chips"></div>
|
|
66
|
+
<button type="button" id="pane-next" class="pane-nav" title="Newer pages" aria-label="Newer pages" hidden>›</button>
|
|
67
|
+
<span id="page-count"></span>
|
|
68
|
+
<!-- How big the shown page is drawn, for this browser. Interface furniture: no agent knows
|
|
69
|
+
it exists, and a page written before it scales like any other. -->
|
|
70
|
+
<span id="page-size">
|
|
71
|
+
<button type="button" id="page-smaller" title="Smaller text" aria-label="Smaller text">−</button>
|
|
72
|
+
<span id="page-size-val"></span>
|
|
73
|
+
<button type="button" id="page-bigger" title="Larger text" aria-label="Larger text">+</button>
|
|
74
|
+
</span>
|
|
75
|
+
</div>
|
|
76
|
+
<div id="page-doc"></div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<!-- Specs: the spec workspace, in a frame of its own. -->
|
|
80
|
+
<div id="tool-specs" class="tool-panel" hidden></div>
|
|
81
|
+
|
|
58
82
|
<button type="button" id="spine" title="Open the web pane" aria-label="Open the web pane">
|
|
59
83
|
<span class="label"><span id="spine-badge" hidden></span> Web pane</span>
|
|
60
84
|
</button>
|
|
@@ -80,10 +104,10 @@
|
|
|
80
104
|
<!-- The one button that sends without being written: it types a fixed sentence into the
|
|
81
105
|
session, through the same paste path as anything else, and leaves the box alone. Its
|
|
82
106
|
place in the markup is what puts it above Send: the block fills row by row. -->
|
|
83
|
-
<button type="button" id="ask-page" class="icon-btn" title="Put the last answer
|
|
84
|
-
aria-label="Put the last answer
|
|
107
|
+
<button type="button" id="ask-page" class="icon-btn" title="Put the last answer on a page"
|
|
108
|
+
aria-label="Put the last answer on a page">
|
|
85
109
|
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
|
|
86
|
-
<title>Put the last answer
|
|
110
|
+
<title>Put the last answer on a page</title>
|
|
87
111
|
<rect x="3" y="4" width="18" height="16" rx="2" />
|
|
88
112
|
<path d="M14 4v16" />
|
|
89
113
|
<path d="M17 9h1" />
|
|
@@ -527,6 +527,13 @@ body {
|
|
|
527
527
|
display: flex;
|
|
528
528
|
flex: 1;
|
|
529
529
|
min-height: 0;
|
|
530
|
+
/* Stated once because two rules need it: the grip is this wide, and a full-width pane is the row less
|
|
531
|
+
the grip - which is what keeps the grip on the screen with the terminal squeezed out behind it. */
|
|
532
|
+
--grip-width: 12px;
|
|
533
|
+
/* The terminal's own inset, on both sides. A margin does not shrink with the box it is on, so a
|
|
534
|
+
full-width pane has to leave it as well or the row is wider than the window and the pane's own
|
|
535
|
+
controls, which sit at its far end, go off the screen. */
|
|
536
|
+
--term-gutter: 24px;
|
|
530
537
|
}
|
|
531
538
|
|
|
532
539
|
#termwrap {
|
|
@@ -537,7 +544,7 @@ body {
|
|
|
537
544
|
/* Spacing via margin, not padding: FitAddon measures the wrapper height but does not subtract
|
|
538
545
|
its padding, so wrapper padding makes it propose one row too many and the last line clips.
|
|
539
546
|
Margin gives the same visual inset (page and terminal share a background) without breaking fit. */
|
|
540
|
-
margin: 8px
|
|
547
|
+
margin: 8px calc(var(--term-gutter) / 2);
|
|
541
548
|
}
|
|
542
549
|
|
|
543
550
|
#term {
|
|
@@ -595,7 +602,7 @@ body {
|
|
|
595
602
|
#grip {
|
|
596
603
|
position: relative;
|
|
597
604
|
flex: none;
|
|
598
|
-
width:
|
|
605
|
+
width: var(--grip-width);
|
|
599
606
|
background: var(--panel);
|
|
600
607
|
border-left: 1px solid var(--border);
|
|
601
608
|
border-right: 1px solid var(--border);
|
|
@@ -626,8 +633,9 @@ body.dragging {
|
|
|
626
633
|
user-select: none;
|
|
627
634
|
}
|
|
628
635
|
|
|
629
|
-
/* The width is app/pane.js's, in a custom property so the
|
|
630
|
-
the narrow-screen layout - still do. The value here is what shows before the first
|
|
636
|
+
/* The width is app/pane-shell.js's, in a custom property so the three rules that must win - the collapsed
|
|
637
|
+
spine, the full row and the narrow-screen layout - still do. The value here is what shows before the first
|
|
638
|
+
drag. */
|
|
631
639
|
#pane {
|
|
632
640
|
display: flex;
|
|
633
641
|
flex: none;
|
|
@@ -637,6 +645,16 @@ body.dragging {
|
|
|
637
645
|
background: var(--bg);
|
|
638
646
|
}
|
|
639
647
|
|
|
648
|
+
/* The far end of the range: the pane takes the row and the terminal is squeezed out of it rather than hidden,
|
|
649
|
+
so the terminal keeps its place in the layout and comes back at whatever the next width is. The grip is
|
|
650
|
+
what the row leaves over, which is what keeps the way back on the screen, and the terminal's gutter is left
|
|
651
|
+
as well - it is a margin, so it stays whatever the box it is on shrinks to, and taking the row without it
|
|
652
|
+
would push the pane's own controls past the right edge of the window. Both are the same background, so what
|
|
653
|
+
the terminal leaves behind reads as part of the pane. */
|
|
654
|
+
#pane.full {
|
|
655
|
+
width: calc(100% - var(--grip-width) - var(--term-gutter));
|
|
656
|
+
}
|
|
657
|
+
|
|
640
658
|
/* A page arrived and opened itself: one pulse of the workspace colour along the pane's own edge, and nothing
|
|
641
659
|
moves. Inset, so it is drawn over the pane rather than pushing the layout about, and over the frame's edge
|
|
642
660
|
rather than under it. ARRIVAL_MS in app/pane.js must match the duration - it is what takes the class off
|
|
@@ -664,20 +682,8 @@ body.dragging {
|
|
|
664
682
|
}
|
|
665
683
|
}
|
|
666
684
|
|
|
667
|
-
/* The
|
|
668
|
-
|
|
669
|
-
truth about which page is open. */
|
|
670
|
-
#pagebar {
|
|
671
|
-
display: flex;
|
|
672
|
-
flex: none;
|
|
673
|
-
align-items: center;
|
|
674
|
-
gap: 6px;
|
|
675
|
-
padding: 7px 10px;
|
|
676
|
-
background: var(--panel);
|
|
677
|
-
border-bottom: 1px solid var(--border);
|
|
678
|
-
overflow: hidden;
|
|
679
|
-
}
|
|
680
|
-
|
|
685
|
+
/* The small square buttons of the pane's two bars: the width controls up in the pane's own chrome, and the
|
|
686
|
+
chip arrows down in the Pages bar. */
|
|
681
687
|
.pane-nav {
|
|
682
688
|
flex: none;
|
|
683
689
|
padding: 1px 6px;
|
|
@@ -695,6 +701,112 @@ body.dragging {
|
|
|
695
701
|
color: var(--text);
|
|
696
702
|
}
|
|
697
703
|
|
|
704
|
+
/* The pane's own bar: which tool is open, and how wide the pane is. Nothing in it belongs to a tool, which is
|
|
705
|
+
what stops a tool from moving the pane or from saying which tool the user is looking at. */
|
|
706
|
+
#panebar {
|
|
707
|
+
display: flex;
|
|
708
|
+
flex: none;
|
|
709
|
+
align-items: flex-end;
|
|
710
|
+
gap: 2px;
|
|
711
|
+
padding: 7px 10px 0;
|
|
712
|
+
background: var(--bg);
|
|
713
|
+
border-bottom: 1px solid var(--border);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
#tool-tabs {
|
|
717
|
+
display: flex;
|
|
718
|
+
gap: 2px;
|
|
719
|
+
min-width: 0;
|
|
720
|
+
overflow: hidden;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/* A tab is a folder mouth: the open one carries the panel's own background up into the bar, so the two read
|
|
724
|
+
as one surface and the others read as behind it. */
|
|
725
|
+
.tool-tab {
|
|
726
|
+
display: flex;
|
|
727
|
+
flex: none;
|
|
728
|
+
align-items: center;
|
|
729
|
+
gap: 6px;
|
|
730
|
+
padding: 4px 14px;
|
|
731
|
+
border: 1px solid transparent;
|
|
732
|
+
border-bottom: none;
|
|
733
|
+
border-radius: 7px 7px 0 0;
|
|
734
|
+
background: none;
|
|
735
|
+
color: var(--muted);
|
|
736
|
+
font: inherit;
|
|
737
|
+
font-size: 12.5px;
|
|
738
|
+
cursor: pointer;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
.tool-tab:hover {
|
|
742
|
+
color: var(--text);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
.tool-tab.on {
|
|
746
|
+
background: var(--panel);
|
|
747
|
+
border-color: var(--border);
|
|
748
|
+
color: var(--text);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/* What is waiting in a tool nobody is looking at. The same green as everything else that means "something
|
|
752
|
+
happened here". */
|
|
753
|
+
.tool-mark {
|
|
754
|
+
padding: 0 5px;
|
|
755
|
+
border-radius: 8px;
|
|
756
|
+
background: var(--accent);
|
|
757
|
+
color: #04140b;
|
|
758
|
+
font-size: 9.5px;
|
|
759
|
+
font-weight: 700;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
#pane-ctl {
|
|
763
|
+
display: flex;
|
|
764
|
+
flex: none;
|
|
765
|
+
margin-left: auto;
|
|
766
|
+
gap: 4px;
|
|
767
|
+
padding-bottom: 5px;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
#pane-ctl .pane-nav {
|
|
771
|
+
display: flex;
|
|
772
|
+
align-items: center;
|
|
773
|
+
justify-content: center;
|
|
774
|
+
padding: 2px 5px;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/* The pane is already at the far end, and the button is the way back from it. */
|
|
778
|
+
#pane-ctl .pane-nav.on {
|
|
779
|
+
background: #10151c;
|
|
780
|
+
color: var(--accent);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/* One tool's panel. Only the open one is on the screen, and each fills whatever the pane's bar leaves. */
|
|
784
|
+
.tool-panel {
|
|
785
|
+
display: flex;
|
|
786
|
+
flex: 1;
|
|
787
|
+
flex-direction: column;
|
|
788
|
+
min-width: 0;
|
|
789
|
+
min-height: 0;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
.tool-panel[hidden] {
|
|
793
|
+
display: none;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/* The pagination bar: this session's pages as chips, the arrows for a bar too narrow to hold them, and where
|
|
797
|
+
in the history the open page sits. Drawn by the interface, outside the page's frame, so it always says the
|
|
798
|
+
truth about which page is open. */
|
|
799
|
+
#pagebar {
|
|
800
|
+
display: flex;
|
|
801
|
+
flex: none;
|
|
802
|
+
align-items: center;
|
|
803
|
+
gap: 6px;
|
|
804
|
+
padding: 7px 10px;
|
|
805
|
+
background: var(--panel);
|
|
806
|
+
border-bottom: 1px solid var(--border);
|
|
807
|
+
overflow: hidden;
|
|
808
|
+
}
|
|
809
|
+
|
|
698
810
|
#chips {
|
|
699
811
|
display: flex;
|
|
700
812
|
gap: 6px;
|
|
@@ -810,15 +922,6 @@ body.dragging {
|
|
|
810
922
|
letter-spacing: 0.02em;
|
|
811
923
|
}
|
|
812
924
|
|
|
813
|
-
/* Out of the way, at the end of the bar: the pane is put away far more often than the chips are paged. */
|
|
814
|
-
#pane-collapse {
|
|
815
|
-
display: flex;
|
|
816
|
-
align-items: center;
|
|
817
|
-
justify-content: center;
|
|
818
|
-
margin-left: 4px;
|
|
819
|
-
padding: 2px 5px;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
925
|
#page-doc {
|
|
823
926
|
flex: 1;
|
|
824
927
|
min-width: 0;
|
|
@@ -841,10 +944,21 @@ body.dragging {
|
|
|
841
944
|
zoom: var(--page-zoom, 1);
|
|
842
945
|
}
|
|
843
946
|
|
|
947
|
+
/* A tool's own client, in a frame of its own. It fills the panel and this side styles nothing inside it: the
|
|
948
|
+
tool ships its own document, its own stylesheet and its own theme, and the shell cannot reach in anyway. */
|
|
949
|
+
.tool-frame {
|
|
950
|
+
display: block;
|
|
951
|
+
width: 100%;
|
|
952
|
+
height: 100%;
|
|
953
|
+
border: none;
|
|
954
|
+
background: var(--bg);
|
|
955
|
+
}
|
|
956
|
+
|
|
844
957
|
/* A page is a document with an event loop of its own, and a pointer that crosses into it during a drag is a
|
|
845
958
|
pointer the grip has to keep. Capture does that in every engine that has it; taking the frame out of the
|
|
846
959
|
hit test for the length of the drag is what makes it true everywhere. */
|
|
847
|
-
body.dragging .page-frame
|
|
960
|
+
body.dragging .page-frame,
|
|
961
|
+
body.dragging .tool-frame {
|
|
848
962
|
pointer-events: none;
|
|
849
963
|
}
|
|
850
964
|
|
|
@@ -893,8 +1007,8 @@ body.dragging .page-frame {
|
|
|
893
1007
|
width: 30px;
|
|
894
1008
|
}
|
|
895
1009
|
|
|
896
|
-
#pane.collapsed #
|
|
897
|
-
#pane.collapsed
|
|
1010
|
+
#pane.collapsed #panebar,
|
|
1011
|
+
#pane.collapsed .tool-panel {
|
|
898
1012
|
display: none;
|
|
899
1013
|
}
|
|
900
1014
|
|