chamba 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +53 -14
- package/README.md +34 -8
- package/dist/commands/advanced.js +7 -278
- package/dist/commands/dev.js +11 -612
- package/dist/commands/doctor.js +1 -29
- package/dist/commands/menu.js +1 -80
- package/dist/commands/onboard.js +6 -230
- package/dist/commands/settings.js +15 -349
- package/dist/lib/agent-commands.js +2 -0
- package/dist/lib/agent-context.js +6 -210
- package/dist/lib/browser.js +1 -40
- package/dist/lib/chamba-yaml.js +10 -191
- package/dist/lib/constants.js +1 -125
- package/dist/lib/dockerfile-builder.js +41 -261
- package/dist/lib/env.js +2 -78
- package/dist/lib/git-env.js +1 -21
- package/dist/lib/global-config.js +4 -66
- package/dist/lib/pnpm-store.js +1 -19
- package/dist/lib/ports.js +1 -210
- package/dist/lib/safe-rm.js +1 -36
- package/dist/lib/sessions.js +2 -34
- package/dist/lib/shadows.js +1 -176
- package/dist/lib/skills.js +2 -0
- package/dist/lib/templates.js +1 -0
- package/dist/lib/webterm.js +1 -305
- package/dist/lib/workspace-identity.js +4 -260
- package/package.json +4 -4
- package/templates/Dockerfile +15 -22
- package/templates/claude-statusline.sh +5 -7
- package/templates/context/baseline.md +4 -2
- package/templates/context/web-pane-craft.md +1 -1
- package/templates/pane-apps/client/assets/api-BBDf_rzz.js +12 -0
- package/templates/pane-apps/client/assets/api-DNLbSBpK.css +1 -0
- package/templates/pane-apps/client/assets/bee-B7t97l5B.webp +0 -0
- package/templates/pane-apps/client/assets/buzz-CUUsbnXH.mp3 +0 -0
- package/templates/pane-apps/client/assets/c-BNa-DeUl.json +3552 -0
- package/templates/pane-apps/client/assets/cpp-B-T4NRoH.json +20239 -0
- package/templates/pane-apps/client/assets/csharp-BbxHChhA.json +6010 -0
- package/templates/pane-apps/client/assets/css-CWCxuwmz.json +1860 -0
- package/templates/pane-apps/client/assets/diff-B-2N89g8.json +154 -0
- package/templates/pane-apps/client/assets/docker-DWTvYW26.json +103 -0
- package/templates/pane-apps/client/assets/files-BkmyervY.js +1 -0
- package/templates/pane-apps/client/assets/go-B2hyYeNg.json +3156 -0
- package/templates/pane-apps/client/assets/graphql-Z6Id2aNO.json +1292 -0
- package/templates/pane-apps/client/assets/home-3SeGzySM.css +1 -0
- package/templates/pane-apps/client/assets/home-Cu9xOd2i.js +1 -0
- package/templates/pane-apps/client/assets/html-BtzvlIfu.json +2627 -0
- package/templates/pane-apps/client/assets/ini-Bn3Twap1.json +108 -0
- package/templates/pane-apps/client/assets/java-C7t2dwLO.json +1885 -0
- package/templates/pane-apps/client/assets/javascript-DuC2v0C0.json +5996 -0
- package/templates/pane-apps/client/assets/json-CwdkWeV5.json +206 -0
- package/templates/pane-apps/client/assets/jsonc-MYWDbTU3.json +206 -0
- package/templates/pane-apps/client/assets/jsx-Cn8EZuIC.json +5996 -0
- package/templates/pane-apps/client/assets/kotlin-CFNOz1gw.json +580 -0
- package/templates/pane-apps/client/assets/make-BVnlFCcl.json +629 -0
- package/templates/pane-apps/client/assets/markdown-B4Xk2WM_.json +3263 -0
- package/templates/pane-apps/client/assets/php-CAQzQmmI.json +4188 -0
- package/templates/pane-apps/client/assets/pop-D5ZANBC3.mp3 +0 -0
- package/templates/pane-apps/client/assets/python-CYQee8m4.json +4174 -0
- package/templates/pane-apps/client/assets/reviews-DMrGB-kG.js +153 -0
- package/templates/pane-apps/client/assets/reviews-nK_yRav9.css +1 -0
- package/templates/pane-apps/client/assets/ruby-D7R58tus.json +3114 -0
- package/templates/pane-apps/client/assets/rust-BFBbFiei.json +1103 -0
- package/templates/pane-apps/client/assets/scss-TQRhBkPG.json +1874 -0
- package/templates/pane-apps/client/assets/shellscript-DieAkC4W.json +2375 -0
- package/templates/pane-apps/client/assets/specs-Cabb15F0.js +13 -0
- package/templates/pane-apps/client/assets/sql-Bxd030LV.json +634 -0
- package/templates/pane-apps/client/assets/toml-ChrKGzsV.json +430 -0
- package/templates/pane-apps/client/assets/tsx-D2e90MFw.json +5996 -0
- package/templates/pane-apps/client/assets/typescript-DonXQ_n1.json +5747 -0
- package/templates/pane-apps/client/assets/useNow-BHFKFraS.js +1 -0
- package/templates/pane-apps/client/assets/xml-B17r2RXF.json +382 -0
- package/templates/pane-apps/client/assets/yaml-CKn4h1zy.json +627 -0
- package/templates/pane-apps/client/home/index.html +16 -0
- package/templates/pane-apps/client/reviews/index.html +17 -0
- package/templates/pane-apps/client/specs/index.html +4 -2
- package/templates/pane-apps/server/home.mjs +12 -0
- package/templates/pane-apps/server/reviews.mjs +21 -0
- package/templates/pane-apps/server/specs.mjs +5 -1588
- package/templates/skills/chamba-statusline/SKILL.md +7 -19
- package/templates/skills/dx-review/SKILL.md +136 -0
- package/templates/skills/dx-review/references/acts.md +233 -0
- package/templates/skills/dx-spec/SKILL.md +230 -30
- package/templates/skills/dx-spec/references/principles-template.md +2 -0
- package/templates/skills/dx-spec/references/review-guide.md +6 -52
- package/templates/skills/dx-spec/references/spec-guide.md +25 -5
- package/templates/skills/dx-spec/references/stages.md +149 -60
- package/templates/skills/dx-spec-execute/SKILL.md +217 -38
- package/templates/startup.mjs +22 -16
- package/templates/tool-helper.sh +168 -0
- package/templates/webterm/README.md +82 -27
- package/templates/webterm/color.js +61 -0
- package/templates/webterm/config.js +55 -4
- package/templates/webterm/context/claude.md +0 -1
- package/templates/webterm/public/app/alerts.js +95 -14
- package/templates/webterm/public/app/anchored.js +81 -0
- package/templates/webterm/public/app/composer.js +37 -3
- package/templates/webterm/public/app/connection.js +23 -0
- package/templates/webterm/public/app/dom.js +3 -0
- package/templates/webterm/public/app/frames.js +52 -18
- package/templates/webterm/public/app/home-host.js +16 -0
- package/templates/webterm/public/app/main.js +24 -8
- package/templates/webterm/public/app/new-session.js +13 -41
- package/templates/webterm/public/app/palette.js +58 -0
- package/templates/webterm/public/app/pane-shape.js +169 -0
- package/templates/webterm/public/app/pane-shell.js +240 -135
- package/templates/webterm/public/app/pane.js +39 -24
- package/templates/webterm/public/app/reviews-host.js +19 -0
- package/templates/webterm/public/app/specs-host.js +12 -218
- package/templates/webterm/public/app/state.js +5 -0
- package/templates/webterm/public/app/tabs.js +6 -2
- package/templates/webterm/public/app/theme.js +75 -27
- package/templates/webterm/public/app/tool-host.js +318 -0
- package/templates/webterm/public/app/workspace-color.js +192 -0
- package/templates/webterm/public/index.html +22 -4
- package/templates/webterm/public/styles.css +195 -21
- package/templates/webterm/server.js +198 -150
- package/templates/webterm/tool-document.js +3 -2
- package/templates/webterm/tools/commands.js +61 -0
- package/templates/webterm/tools/home.js +58 -0
- package/templates/webterm/tools/index.js +26 -0
- package/templates/webterm/tools/mount.js +337 -0
- package/templates/webterm/tools/paths.js +34 -0
- package/templates/webterm/tools/reviews.js +137 -0
- package/templates/webterm/tools/specs.js +154 -0
- package/templates/webterm/typed-line.js +70 -7
- package/templates/context/context-usage.md +0 -1
- package/templates/context-usage.sh +0 -266
- package/templates/pane-apps/client/assets/specs-B1970L17.css +0 -1
- package/templates/pane-apps/client/assets/specs-cEee_SPn.js +0 -23
- package/templates/skills/dx-spec-config/SKILL.md +0 -313
- package/templates/skills/dx-spec-config/references/principles-template.md +0 -12
- package/templates/specs.sh +0 -106
- package/templates/webterm/specs.js +0 -358
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# The agent's half of a pane tool's channel, in this session's web pane.
|
|
3
|
+
# Baked into the image at /usr/local/share/chamba/tool-helper.sh, on PATH as `specs` and as `reviews`.
|
|
4
|
+
#
|
|
5
|
+
# One script, and the tool it is for is the name it was called by. The two channels differ in a word, a door
|
|
6
|
+
# and a list of verbs, and in nothing else: the pane writes a file and types one line into this terminal, and
|
|
7
|
+
# these verbs are how the work moves the other way. Everything they change is a file in the tool's own
|
|
8
|
+
# directory, so every exchange is something git can see.
|
|
9
|
+
#
|
|
10
|
+
# The caller says who it is by its own pid and nothing more, exactly as `webpane` does. The server walks up
|
|
11
|
+
# the process tree from there to a session it started, and refuses a caller that is not in one.
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
CONTAINER_PORT="${WEBTERM_PORT:-3899}"
|
|
15
|
+
KEY_FILE="${WEBTERM_KEY_FILE:-/tmp/webterm.key}"
|
|
16
|
+
|
|
17
|
+
# Which tool this is. The link's own name, so `specs` and `reviews` are one script with one set of rules.
|
|
18
|
+
TOOL="$(basename "$0")"
|
|
19
|
+
|
|
20
|
+
case "$TOOL" in
|
|
21
|
+
specs | reviews) ;;
|
|
22
|
+
*)
|
|
23
|
+
echo "tool-helper: no pane tool is called ${TOOL}." >&2
|
|
24
|
+
exit 1
|
|
25
|
+
;;
|
|
26
|
+
esac
|
|
27
|
+
|
|
28
|
+
usage_specs() {
|
|
29
|
+
cat <<'TEXT'
|
|
30
|
+
Usage: specs <verb> [<spec>] < payload.json
|
|
31
|
+
|
|
32
|
+
Verbs:
|
|
33
|
+
stages Where the specs live, and every stage a protocol may hold.
|
|
34
|
+
state <spec> Where the work stands: protocol, activity, what awaits the user.
|
|
35
|
+
intake [<spec>] File a new piece of work. { name, text, attachments, wish }
|
|
36
|
+
propose <spec> Recommend a protocol. { stages: [{ id, recommended, why }], note }
|
|
37
|
+
recommend <spec> Recommend how the run is shaped. { commits, branch, branchName, pauses, pausePhases, pauseReasons, noPauseReason, checkpoints }
|
|
38
|
+
await <spec> Declare a gate, a round or a decision card. { id, kind, title, file, payload }
|
|
39
|
+
withdraw <spec> Take back an ask nobody needs to answer. { id, reason }
|
|
40
|
+
post <spec> Say what is happening. { kind: activity | stage | phase | plan | status | note, ... }
|
|
41
|
+
review <spec> Post a round of review. { verdict, findings, judgments }, and { round } to replace one already posted
|
|
42
|
+
answers <spec> Read back what the user answered. { id }
|
|
43
|
+
|
|
44
|
+
A payload is JSON on stdin, where the verb takes one. The answer is JSON on stdout.
|
|
45
|
+
TEXT
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
usage_reviews() {
|
|
49
|
+
cat <<'TEXT'
|
|
50
|
+
Usage: reviews <verb> [<review>] < payload.json
|
|
51
|
+
|
|
52
|
+
Verbs:
|
|
53
|
+
stages Where the reviews live, and what a review may hold.
|
|
54
|
+
state <review> The state, the baseline's summary, and what awaits whom.
|
|
55
|
+
start File a review from this terminal. { name, selection }
|
|
56
|
+
metadata <review> Post the whole grouping and its notes, replacing what is there. { fingerprint, groups, notes }
|
|
57
|
+
group <review> Add or replace one group, and the notes on its hunks. { fingerprint, group, notes }
|
|
58
|
+
ungroup <review> Remove one group; the notes on its hunks stay. { fingerprint, id }
|
|
59
|
+
reply <review> Append one reply to a thread. { thread, text }
|
|
60
|
+
activity <review> Say what you are doing. { said }
|
|
61
|
+
done <review> End your act; the tool recomputes the baseline where one is due.
|
|
62
|
+
log <review> One dated line for the activity log. { said }
|
|
63
|
+
|
|
64
|
+
A payload is JSON on stdin, where the verb takes one. The answer is JSON on stdout.
|
|
65
|
+
|
|
66
|
+
Nothing here marks a hunk seen, closes a thread, approves, or changes the baseline of its own accord.
|
|
67
|
+
Those are the reviewer's, through the tab.
|
|
68
|
+
TEXT
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
VERB="${1:-}"
|
|
72
|
+
ENTRY="${2:-}"
|
|
73
|
+
|
|
74
|
+
case "$VERB" in
|
|
75
|
+
"" | -h | --help)
|
|
76
|
+
"usage_${TOOL}"
|
|
77
|
+
exit 0
|
|
78
|
+
;;
|
|
79
|
+
esac
|
|
80
|
+
|
|
81
|
+
if [ ! -r "$KEY_FILE" ]; then
|
|
82
|
+
echo "${TOOL}: the web interface is not running in this container, so there is no pane to tell." >&2
|
|
83
|
+
exit 1
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
# A payload is read only for a verb that takes one. The verbs that carry none never touch stdin: an agent's
|
|
87
|
+
# shell often runs with a stdin that is neither a terminal nor closed - a socket, an open pipe - and a read on
|
|
88
|
+
# it waits for a close that never comes, which hangs the agent rather than answering it. So what is read is a
|
|
89
|
+
# pipe or a file, which is what a payload arrives on, and nothing else is read at all.
|
|
90
|
+
#
|
|
91
|
+
# The list is every such verb of either tool: `stages` and `state` read, and `reviews done` says its one thing
|
|
92
|
+
# by being called. A name no tool has is refused by the server whatever happens here.
|
|
93
|
+
BODY_FILE="$(mktemp)"
|
|
94
|
+
REQUEST_FILE="$(mktemp)"
|
|
95
|
+
PAYLOAD_FILE="$(mktemp)"
|
|
96
|
+
trap 'rm -f "$BODY_FILE" "$REQUEST_FILE" "$PAYLOAD_FILE"' EXIT
|
|
97
|
+
|
|
98
|
+
# The payload lands in a file rather than in a variable, because it is passed to jq and an argument has a
|
|
99
|
+
# size limit of its own: the grouping of a large review is hundreds of kilobytes, which is well under what
|
|
100
|
+
# the server accepts and over what one argument can carry.
|
|
101
|
+
echo -n "null" >"$PAYLOAD_FILE"
|
|
102
|
+
case "$VERB" in
|
|
103
|
+
stages | state | done) ;;
|
|
104
|
+
*)
|
|
105
|
+
if [ -p /dev/stdin ] || [ -f /dev/stdin ]; then
|
|
106
|
+
cat >"$PAYLOAD_FILE"
|
|
107
|
+
if [ ! -s "$PAYLOAD_FILE" ]; then
|
|
108
|
+
echo -n "null" >"$PAYLOAD_FILE"
|
|
109
|
+
fi
|
|
110
|
+
fi
|
|
111
|
+
;;
|
|
112
|
+
esac
|
|
113
|
+
|
|
114
|
+
# The request is composed with jq so the payload stays JSON rather than being spliced into a string. jq is in
|
|
115
|
+
# the image; without it there is no safe way to build this, so the helper says so instead of guessing.
|
|
116
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
117
|
+
echo "${TOOL}: jq is not in this container, and the request cannot be composed without it." >&2
|
|
118
|
+
exit 1
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# `--slurpfile` reads the payload as JSON from the file, and jq's own words are kept: a payload that is
|
|
122
|
+
# not JSON and a payload too large to read say different things, and an agent told the wrong one rewrites
|
|
123
|
+
# correct JSON for ever.
|
|
124
|
+
if ! SAID="$(jq -n --arg verb "$VERB" --arg entry "$ENTRY" --slurpfile payload "$PAYLOAD_FILE" \
|
|
125
|
+
'{ verb: $verb, entry: $entry, payload: $payload[0] }' 2>&1 >"$REQUEST_FILE")"; then
|
|
126
|
+
echo "${TOOL}: the payload on stdin is not JSON - ${SAID}" >&2
|
|
127
|
+
exit 1
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# How long an answer is waited for. Most verbs write a file and answer; `start` and `done` recompute a
|
|
131
|
+
# baseline, which reads the whole diff of a selection and every blob in it, and a wait that ran out there
|
|
132
|
+
# would tell the agent the act failed while the interface was still doing it.
|
|
133
|
+
case "$VERB" in
|
|
134
|
+
start | done) DEADLINE=300 ;;
|
|
135
|
+
*) DEADLINE=30 ;;
|
|
136
|
+
esac
|
|
137
|
+
|
|
138
|
+
# $$ is this script's own pid. The server climbs from it to whichever session started it.
|
|
139
|
+
STATUS="$(
|
|
140
|
+
curl -sS -o "$BODY_FILE" -w '%{http_code}' \
|
|
141
|
+
--connect-timeout 3 --max-time "$DEADLINE" \
|
|
142
|
+
-X POST \
|
|
143
|
+
-H "Content-Type: application/json" \
|
|
144
|
+
-H "X-Webterm-Key: $(head -n 1 "$KEY_FILE")" \
|
|
145
|
+
-H "X-${TOOL^}-Pid: $$" \
|
|
146
|
+
--data-binary "@${REQUEST_FILE}" \
|
|
147
|
+
"http://127.0.0.1:${CONTAINER_PORT}/${TOOL}/agent" 2>/dev/null
|
|
148
|
+
)" || {
|
|
149
|
+
echo "${TOOL}: could not reach the web interface on this container's port ${CONTAINER_PORT}." >&2
|
|
150
|
+
exit 1
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if [ "$STATUS" = "200" ]; then
|
|
154
|
+
jq . <"$BODY_FILE"
|
|
155
|
+
exit 0
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
REASON="$(jq -r '.error // empty' <"$BODY_FILE" 2>/dev/null)"
|
|
159
|
+
if [ -z "$REASON" ]; then
|
|
160
|
+
case "$STATUS" in
|
|
161
|
+
403) REASON="the key on disk is not the one the interface is using" ;;
|
|
162
|
+
409) REASON="this is not running inside a web session" ;;
|
|
163
|
+
413) REASON="the payload is too large" ;;
|
|
164
|
+
*) REASON="the interface answered ${STATUS}" ;;
|
|
165
|
+
esac
|
|
166
|
+
fi
|
|
167
|
+
echo "${TOOL}: nothing was done - ${REASON}." >&2
|
|
168
|
+
exit 1
|
|
@@ -61,7 +61,14 @@ That is why a session showing as working never also shows as waiting for you.
|
|
|
61
61
|
- **A tab flashes and then stays lit** when its agent finishes. Every ending, whoever is watching: it costs nothing to say a thing twice on screen, and one rule for every session is easier to trust than one for the tab you have open and another for the rest. Touching the session, or arriving at it, is what puts it out.
|
|
62
62
|
- **A tab takes its session's colour and grows a small page** beside its age when a page is published into that session's pane while you are looking at another one, and flashes once as it arrives. It holds until you get to that session, where the newest page opens itself; anything older that piled up behind it keeps its chip badge, so the tab marks itself again - without a second flash - once you leave. Its own state, not the one above: that one is an agent finishing, and this one is a page waiting to be read.
|
|
63
63
|
- **The browser tab speaks too**, because that is all a window behind something else can do. The title counts the sessions waiting for you, and the favicon carries one mark: a white bar across its bottom edge, with a lit segment moving along it, while an agent is working, and a green dot in its top corner - pulsing until you go and look - when one is waiting. Two shapes at opposite ends of the icon rather than two colours in one place, because at 16px a hue is the first thing to go. White for working because every hue here belongs to a workspace or a session, and the frame around the icon is one of them.
|
|
64
|
+
- **And it goes grey when the window cannot reach the container.** Three seconds after the socket drops - long enough that a wifi blip or a closed lid passes without it - the spark and the frame drain to one flat grey, both marks go, and nothing in the icon moves again until the socket is back.
|
|
65
|
+
The title drops its count with them and keeps the workspace name alone.
|
|
66
|
+
Everything else the tab says comes from the last frame the server sent, and while the socket is down that frame only gets older, so a green dot would call you back to a session this window can no longer see.
|
|
67
|
+
The icon is the same grey for every kind of disconnect: a container that is stopped, a key that is spent, and a container that has gone.
|
|
68
|
+
It says whether this window can reach the container, not why - the curtain on the page is where the reason is.
|
|
64
69
|
- **And ten seconds later it says so out loud** - one soft chime, once, for an ending nobody came back to. A sound cannot be taken back and it reaches you in the next room, so it is the one thing here that asks whether you were there. Several sessions finishing together are one chime, and so are two windows open on the same container. The bell beside the power button mutes it, and remembers.
|
|
70
|
+
A grey tab is a silent one: a finish that comes due while the icon is grey is spent without a sound, and the reconnect does not play it late.
|
|
71
|
+
By then the chime would be calling you to a session this window could not see when the finish came due, and a sound is the one thing here that cannot be taken back.
|
|
65
72
|
|
|
66
73
|
Whether you were there is answered by whether that session gets touched - a keystroke, a click, a scroll - in the ten seconds after it finishes, not by whether the browser had focus.
|
|
67
74
|
Focus was the first answer and the wrong one: it varies by browser and platform, it has to be re-reported on every reconnect, and every hole in it fails towards silence, which is the one direction a notification must not fail in.
|
|
@@ -82,6 +89,20 @@ Existing sessions are untouched - a session's directory never changes under it.
|
|
|
82
89
|
chamba asks it when the last terminal session closes, so `exit` in the terminal warns before it stops a container with live browser sessions in it.
|
|
83
90
|
A window whose socket dropped asks it too: an answer means the relay is fine, a `403` means this window's key is spent, and no answer at all means the container is gone - three different things to say, and this is what tells them apart.
|
|
84
91
|
|
|
92
|
+
## The workspace colour
|
|
93
|
+
|
|
94
|
+
One colour says which window this is from across a screen full of them: the composer border, the active tab's edge, the buttons, and the frame around the browser tab's icon are all of them that one hue.
|
|
95
|
+
The palette button in the bar, between the workspace name and the bell, is where it is set.
|
|
96
|
+
It opens a panel of eleven colours with a way back to automatic in the last cell, and one click applies the colour with nothing to confirm.
|
|
97
|
+
|
|
98
|
+
Until someone picks one, the colour comes from the published port, so two containers open side by side are almost never alike.
|
|
99
|
+
A colour that is picked is written to `~/.chamba-webterm/color` as its id, `azure` and not a hex, and every open window on the workspace follows within a second.
|
|
100
|
+
The panel marks what is stored rather than what is on the screen, so a workspace nobody has chosen for shows the mark on automatic.
|
|
101
|
+
|
|
102
|
+
Session dots keep the six colours they have always had, and rotate through five of them.
|
|
103
|
+
One slot is always held back: the workspace's own when it wears one of the six, and the slot the port would have chosen otherwise.
|
|
104
|
+
So a dot never carries the window's own colour, and picking a colour from outside the six leaves the dots where they were.
|
|
105
|
+
|
|
85
106
|
## Run
|
|
86
107
|
|
|
87
108
|
webterm is baked into the chamba image with its dependencies preinstalled, and there is nothing to turn on.
|
|
@@ -94,7 +115,10 @@ webterm is baked into the chamba image with its dependencies preinstalled, and t
|
|
|
94
115
|
The key on the end is what the relay checks, and it changes every time the server starts.
|
|
95
116
|
3. In the container, `webterm <agent>` (`claude`, `opencode`, or `codex`) moves which agent new browser sessions start with.
|
|
96
117
|
The agent is always explicit; `webterm` on its own prints usage and the URL, and never picks one for you.
|
|
97
|
-
The choice is written to `~/.chamba-webterm/agent`,
|
|
118
|
+
The choice is written to `~/.chamba-webterm/agent`, and the host reads it back at the next container start - so it holds across restarts.
|
|
119
|
+
chamba bind-mounts that directory from the workspace cache, and three files live in it: `agent`, the resume stamp `resumed-at`, and `color`, which holds the workspace colour.
|
|
120
|
+
Home's cache directory `.home/` sits beside them, for the same reason: what outlives the container goes here.
|
|
121
|
+
A rebuild replaces the container and leaves all of it where it is.
|
|
98
122
|
|
|
99
123
|
The first session after a container start reopens the most recent conversation; later sessions start fresh.
|
|
100
124
|
The server decides that itself, in `resume.js`, by reading the same agent stores the pane reads - the host is not involved and plants nothing.
|
|
@@ -107,47 +131,78 @@ Sessions already open keep running the agent they were started with.
|
|
|
107
131
|
|
|
108
132
|
## The web pane
|
|
109
133
|
|
|
110
|
-
Beside the terminal is a pane, and the pane holds tools. **
|
|
134
|
+
Beside the terminal is a pane, and the pane holds tools. **Home** is the first of them, and the tool a pane with no shape of its own opens on: the pulse of the workspace repository, read from git. **Pages** is the interface's own: the pages this session's agent published - a plan, a table, a diagram, a question with more structure than a terminal prompt can hold. **Specs** and **Code Reviews** sit beside them, and Home, Specs and Code Reviews each have a section of their own below.
|
|
111
135
|
|
|
112
|
-
- **The pane's own chrome.** One tab per tool, and the width controls beside them. Everything in that bar is the pane's, never a tool's, so no tool can resize the pane or say which tool you are looking at - the same reason the chips bar is drawn outside the page it lists. Width has three states and only one of them is a width: the divider drags anything between the two ends, full width takes the whole row and squeezes the terminal out, and collapse folds the pane into the vertical "Web pane" spine. The grip stays on the screen in every one of them, so dragging it is always the way back, and a second press of the full-width button is the other. The spine carries what every tool is waiting on, whichever tool is open.
|
|
136
|
+
- **The pane's own chrome.** One tab per tool, drawn as a flat label with a line under the open one in the workspace's own colour, and the width controls beside them. Everything in that bar is the pane's, never a tool's, so no tool can resize the pane or say which tool you are looking at - the same reason the chips bar is drawn outside the page it lists. Width has three states and only one of them is a width: the divider drags anything between the two ends, full width takes the whole row and squeezes the terminal out, and collapse folds the pane into the vertical "Web pane" spine. Between those two buttons is a third for half the window, which is the width the pane opens at - half itself rather than the pixels half came to, so the pane stays at half when the window is resized, and the button is lit exactly while it is there. The grip stays on the screen in every one of them, so dragging it is always the way back, and a second press of the full-width button is the other. The spine carries what every tool is waiting on, whichever tool is open. The pane's shape belongs to the session it is beside - which tool is open, how wide it is, which of the three states it is in, how big a page is drawn and which palette the reading is in - so switching sessions gives each one the pane it was left with, and a reload gives them all back. A session nobody has shaped opens at half the window, on Home. The browser keeps this, one shape per session, and a session that leaves the bar takes its shape with it.
|
|
113
137
|
- **Publishing.** `webpane <file.html> [--title "..."]` inside a session, from an agent or by hand. Anything in the session's process tree may publish: the helper sends only its own pid, and the server walks up the process tree to a session it started, so no caller ever names a directory.
|
|
114
138
|
- **The files.** `~/.webpane/<conversation-id>/<NN>-<slug>.html`, bind-mounted from the host workspace cache. A page is a plain standalone HTML file, and a file copied into the directory by hand shows up in the pane within a second or two, exactly like a published one.
|
|
115
139
|
- **Per conversation, not per session.** The directory is named after the agent's own conversation id, so resuming a conversation - after a container restart, or by hand with `claude --resume <id>` - opens with its pages again. A session whose agent has not written its id down yet publishes into a `pending-...` directory, which is renamed the moment the id appears. Two live sessions never share a directory: a conversation already open elsewhere is refused, and the second session gets its own empty pane with a notice.
|
|
116
140
|
- **Feedback.** A page may carry `<form data-feedback>`. Submitting it writes `~/.webpane/<conversation-id>/feedback/<page>-<epoch-ms>.json` - `{ page, submittedAt, fields, text }`, where a field named `text` becomes the free-text box - and types one line into the agent's terminal saying where to read it. An agent that has already exited still gets the file; only the line is skipped.
|
|
117
|
-
- **What you see.** The pane is beside the terminal from the start,
|
|
118
|
-
- **How an agent comes to use it.** Two layers, so it happens without being asked each time. Every agent's injected context carries the standing rule - prefer a page when the answer is longer or more structured than a terminal reply carries well, and when a question has more options or structure than the agent's own question tooling holds - plus the palette to match and the form contract. Every agent also gets the craft in full - a standalone dark page, inline everything, how to ask with a form, and what a submitted form does and does not prove -
|
|
141
|
+
- **What you see.** The pane is beside the terminal from the start, open at half the window in a session that has published nothing - a pane that only appears once an agent has used it is one nobody knows to ask for. The empty pane says what it is for and gives three things to say to get a page. Once there are pages, the history is the chips bar above them: one chip per page with its title and age, newest at the end. A page that arrives opens itself and pulses the pane's edge once, and takes nothing else: the caret stays where it was mid-sentence, so you keep typing and click into the page when you want it. "Arrives" is the server's own unread flag rather than "new to this window", so a reload opens nothing that was already read, and a page waiting in a session you have not visited still opens when you get there. A pane you put away comes back for it where Pages is the tool it was put away on, at no less than a readable width - the one place this interface overrides a choice you made. A pane put away on another tool stays away, and its Pages tab carries the page until you get there. A pane a session was never put away in is open already, so nothing has to arrive for it to be there. A page published in a session you are *not* looking at changes nothing where you are: its tab takes that session's colour and a small page beside its age, flashes once, and holds until you get there. A page that lands while the pane is open on another tool changes nothing on the screen either: it is selected, ready for when you come back, and the number on the Pages tab is what says so. The minus and plus beside the counter step the shown page through five text sizes, 80% to 150% with 100% the default, and nothing reloads on a press, so a scroll position and a half-filled form both survive it. That control is the browser's own furniture: the size is applied from outside the frame, no page has to account for it, and nothing an agent reads mentions it. The collapse button in the pane's own bar puts the pane away into the spine, which reopens on a click, and dragging the divider past the collapse threshold does the same. Reopening gives back the width the pane had, but never less than a third of the window - a pane that comes back as a sliver may as well have stayed shut. The width, the collapse, the text size and the open tool belong to the session, the way the pages, the selection and the badges do, so switching sessions gives each one the pane it was left with.
|
|
142
|
+
- **How an agent comes to use it.** Two layers, so it happens without being asked each time. Every agent's injected context carries the standing rule - prefer a page when the answer is longer or more structured than a terminal reply carries well, and when a question has more options or structure than the agent's own question tooling holds - plus the palette to match and the form contract. Every agent also gets the craft in full - a standalone dark page, inline everything, how to ask with a form, and what a submitted form does and does not prove - through one channel: the `web-pane` skill, whose body is `templates/context/web-pane-craft.md` and whose description is the phrases a user actually says ("show me", "as a page", "in the pane", "publish"). All three read it that way, from the skills home their own CLI reads. The craft is long and it is needed on the turns that write a page, so it arrives when the skill loader shows its description and the model opens it, rather than in context on every turn. The standing rule above is what makes that safe: reaching for a page does not wait on the skill loading. The session greeting names the pane too, so a user who has never heard of it learns it exists in the first sentence.
|
|
119
143
|
- **Nothing is deleted.** No removal by age, nothing removed when a session closes. Growth is bounded instead: a page over the per-page limit is refused, and once the whole directory reaches its total - in bytes or in number of files - new publishes are refused and what is there stays. Advanced > Clear agent memory in chamba is what clears it.
|
|
120
144
|
|
|
145
|
+
## The Home tool
|
|
146
|
+
|
|
147
|
+
**Home** is the pane's first tab, and the tool a session with no stored shape opens on.
|
|
148
|
+
It reads the workspace repository through git and draws the pulse of it, so the first thing the pane shows is the work itself rather than an empty panel.
|
|
149
|
+
|
|
150
|
+
- **What it serves.** The workspace as a repository, and nothing else. A directory that is no repository is a state Home draws - one plain line - rather than a failure, and so is a repository with no commit yet.
|
|
151
|
+
- **The child process.** git, through the same shared runner Code Reviews uses, as a closed list of read-only operations of Home's own. A branch name never reaches a `--format=` string as text: where a branch is the subject, Home passes its object id.
|
|
152
|
+
- **When it reads.** When its tab is first given a width, and when the user presses Refresh. Nothing watches the repository and nothing polls it, so a commit made in the terminal beside the tab shows on the next opening or the next refresh. The row says how long ago the page read the repository.
|
|
153
|
+
- **The cache.** Derived numbers alone - a walk of the history and a verdict per branch - kept in `.home/` under the bind-mounted state directory. The module refuses a cache directory inside the workspace, because a file there would show up in the `git status` of every repository Home opens, and a file in the container would die with it.
|
|
154
|
+
- **No door for the agent.** Three routes, each a GET the page calls: the history, the branches and the tree. No verb, no helper on PATH, no framed sentence and no workspace file served raw. Home is the one tool an agent cannot reach at all.
|
|
155
|
+
- **The bee.** A bee crosses the page about once a minute, and a click on it plays one of two short clips and shows a quote. It is one animated image and two sound files, built into the Home client. Its sound is the bell in the bar: the pane hands that preference to every tool frame in the session message, so Home has no mute of its own. Under reduced motion the bee parks beside the header row and stays there.
|
|
156
|
+
|
|
121
157
|
## The Specs tool
|
|
122
158
|
|
|
123
|
-
**Specs** is the pane's second tab
|
|
159
|
+
**Specs** is the pane's second tab: a separate application - its own client, its own server side - that webterm hosts rather than contains, built and mounted the way Home is.
|
|
124
160
|
It reads the specs the workspace holds, and it is where the spec workflow grows.
|
|
125
161
|
|
|
126
162
|
- **Where it comes from.** The private `pane-apps` package builds one dist - a client per tool, and one bundled server module per tool - which chamba's build stages into its templates and the image bakes at `/usr/local/share/chamba/pane-apps/`, beside this directory. Nothing is installed for it: the module imports node builtins and nothing else.
|
|
127
|
-
- **How it is mounted.** `
|
|
163
|
+
- **How it is mounted.** `tools/mount.js` is the mount, and it is the mount of every tool: what one tool is - its module, its routes, its helper's verbs, its raw files, its sentences and its limits - is the table in `tools/specs.js`, and `tools/index.js` names the tables. The mount hands the module the directory to serve and an `onChange`, registers its answers behind gates the module does not hold itself, and turns its changes into socket frames - the same shape the pane store is mounted with. Every path a tool answers on is derived from its id in `tools/paths.js`, so a prefix, a key header and a pid header cannot come to disagree. There is no second process and no second port. A failure inside the module is one request answered with a status: every terminal in the container carries on, and the tab recovers on the next thing you do.
|
|
128
164
|
- **What it serves.** One directory of the workspace, `WEBTERM_SPECS_ROOT`, which is `specs/` unless it is set. The directory is a mount parameter rather than a name inside the tool, so the tool serves whatever a repository calls its specs.
|
|
129
165
|
- **Live.** Every change under that root reaches every open window, not one: a spec belongs to the repository rather than to a session, so two windows reading the same spec both hear it.
|
|
130
166
|
- **In a frame of its own.** The client is not part of this page. It is loaded into an iframe with an opaque origin, and what it may ask the server for is decided by two scoped credentials the shell hands in - see Security below.
|
|
131
|
-
- **What it shows.**
|
|
167
|
+
- **What it shows.** The rail on the left, one artifact read as one continuous document in the middle, and, with a spec open, the state of that work on the right. The rail has two modes and never both at once - with nothing open it is what this root holds, the work in hand and the work signed off; with a spec open it is that spec's own artifacts and sections, with the way back at the top. The third pane follows the same rule from the other side: it says what is true of the open spec, so with nothing open there are two panes and the document takes the width. A section carries a read mark, and a section that changed since it was read says so. A page or a diagram beside a spec renders in a frame of the jailed route, an image renders as an image, and anything else is named rather than drawn.
|
|
132
168
|
- **Where a link goes.** A link inside a document is never followed by the frame, which has to stay the document the shell put in it. One inside the spec opens in place at the section it named; one out of the repository is asked of the shell, which opens it as a window of its own.
|
|
133
169
|
- **Where a piece of work stands.** One `state.json` inside each spec's `.specs/`, written by the tool alone: the confirmed protocol stage by stage, the mode flags, what the agent is doing right now, what is waiting for the user, and the dated log. `README.md` beside it is rendered from that file every time it moves, so the board in the pane and the file in git are the same facts rather than two copies somebody keeps in step.
|
|
134
|
-
- **Starting one.** `New`
|
|
135
|
-
- **Choosing the steps.** The agent reads the intake and posts a recommendation; the pane renders it as a form with every stage of the catalog in it, recommended or not, each with the agent's one line. Confirming writes the protocol into the state file in catalog order. Imagination mode is offered exactly when the run includes exploration or the mocks, and
|
|
136
|
-
- **
|
|
137
|
-
- **
|
|
138
|
-
- **
|
|
139
|
-
- **
|
|
170
|
+
- **Starting one.** `New spec` at the top of the rail is one form - a working name, one box for everything you have to say, and files by drop or paste. The name says what the work will be saved as as you type it, and one quiet line at the foot asks for imagination mode: a wish the agent shapes its recommendation around, and not a decision, because the steps are where the mode is confirmed. Sending it creates the directory, saves what you wrote as `intake.md`, starts the state file, and types the spec command with the fresh directory's name into the terminal - filing the form is the user's own act, so the line is the command the user would type. Each agent spells that command its own way, and the line is spelled for the session it goes to: `/dx-spec` for claude and for opencode, `$dx-spec` for codex. An agent that hears the idea in conversation files the same intake through the helper, and gets the same directory.
|
|
171
|
+
- **Choosing the steps.** The agent reads the intake and posts a recommendation; the pane renders it as a form with every stage of the catalog in it, recommended or not, each with the agent's one line. One stage brings a question of its own, asked beside that stage and only while it is ticked: how wide to cast under research. Confirming writes the protocol into the state file in catalog order. Imagination mode is offered exactly when the run includes exploration or the mocks, and opens ticked where the intake asked for it, and `Ask` sends the question to the terminal instead of answering it here.
|
|
172
|
+
- **Changing them later.** `change` beside the board's Protocol heading reopens the same form over what was confirmed. A stage that is done or running is locked, because a step already taken is not untaken by unticking a box; a stage still waiting can go. Confirming writes the amended protocol through the same act that wrote the first one - there is no second writer - and types one line into the terminal saying what moved, so the agent in the middle of the work hears about it. `Quality review` in the rail is that same act in one press, for the one change that is asked for most.
|
|
173
|
+
- **The helper.** `specs <verb>` on PATH inside any session, gated the way `webpane` is - the key from the key file, the caller as its own pid, refused for a process that is not in a session. The verbs are `stages` (the catalog, as data rather than something an agent remembers), `state`, `intake`, `propose`, `recommend`, `await`, `withdraw`, `post`, `review` and `answers`. A payload is JSON on stdin and the answer is JSON on stdout. The agent moves the work only through these: nothing hand-edits the state file.
|
|
174
|
+
- **Annotating.** Select text in a document, or point at an image or the artifact itself, and there are two actions and no others: say something about it, or ask for it to be removed. A removal is complete by pointing, so it joins the queue in the right pane at once. A comment is written where it is about, in a small box that opens at the point, and nothing joins the queue until it is submitted - an empty box leaves nothing behind. What is queued reads back as what was said, with the acts to change it, send it on its own or drop it, and nobody has been told until it is sent. The queue is the piece of work's own: it is kept in that work's `.specs/` directory, so it survives a page reload, a session that ends and a container that is rebuilt, and every window open on the same spec draws the same list. Nothing an agent reads has changed until a round is sent.
|
|
175
|
+
- **Telling the agent.** Sending writes one round into the spec's own `.specs/user-feedback.json` and then types one line into the terminal beside the pane, naming the file rather than carrying what is in it. The line goes to the session that window is driving - the tool names no session, and there is none for it to name. With no session running, the tool asks first, and starts one only if you say so; the line then waits a moment for the agent to be there to read it. `Continue` is the same channel with no file: it asks the session to pick the spec up. `Execute`, offered once the spec holds an execution plan, opens the run form below.
|
|
176
|
+
- **Setting the run up.** One form carries every decision that shapes a run: how the work is saved, which branch it lands on, where it stops for you, and who reviews it, with a panel of one focus per reviewer. A reviewer on that panel is a chip of plain text with a button to drop it, because a focus is a sentence and a field cannot show one whole. One box at the end of the panel is where a reviewer is written: Enter or its own `add` button puts what is in it on the panel and empties it, a box left with words in it becomes a reviewer at submit, and clicking a chip's words puts them back in that box, which is how a reviewer is changed. It opens on the agent's own recommendation, marked as such, and on the built-in defaults where there is none - and on the decisions the run started with, once it has started, because a second execute is the same run carrying on. Submitting records the decisions in the state file and only then types the command that starts the run, so the agent reads what you decided rather than what the last run did. A pause or a checkpoint naming a phase the plan no longer holds is marked, and the form waits for you to re-place it.
|
|
177
|
+
- **One view for each session.** The pane is one frame for the whole window, and the window drives one session at a time, so what is open follows the session rather than the window: two pieces of work in two sessions are two readers. Moving between sessions gives each one back the spec it was reading, where it was, the answer form it was in the middle of, and the New-spec form with what was typed into it. The queued annotations are not on that list: the queue belongs to the piece of work rather than to the reader of it, so it is the same list in every session and in every window open on that spec. A window with no session shows the overview, because a screen whose send actions have nowhere to go is not worth keeping. The one exception is the session that starts from the tool's own offer: it arrives at a window that was driving none, and takes over the spec the reader was looking at when they asked for it.
|
|
178
|
+
- **What the run asks you.** Three things, and one mechanism under all of them. A gate judges one artifact: it opens that artifact and stands under it, and it takes an approval or a change request with the words that say what to change. A round asks questions: each one carries its options, whether one answer or several are allowed, a marked recommendation, and a box of its own for anything the options missed. A decision card is one question of that same kind, plus a pointer to the passage it is about: it names the artifact and the section, the tab opens that artifact at that section and draws the card beneath it, and the ways out are the agent's own options rather than two fixed buttons - what was found and what each way costs are in the question's own words. Each of the three is one file the agent declared under the spec's `.specs/rounds/`, and answering writes the answer into that same file.
|
|
179
|
+
- **Not answering yet.** Every one of the three offers two ways out that settle nothing. `Ask` sends what you typed to the terminal as a question and answers nothing: the ask stays on the list, the form stays open, and the agent's answer comes back beside it. `Later` closes the form and leaves the ask waiting, with what you wrote still in it when you open it again. Neither one writes anything into the round file, and neither one is a decision.
|
|
140
180
|
- **Answering.** A round goes with whatever you answered: a question you left alone is written as unanswered rather than left out, so the agent can tell a "no" from a question you skipped. The first answer wins - two windows can be looking at one gate, and the second is told it was already answered instead of overwriting the first. The outcome becomes a dated line in the state file, the item stops waiting, and one framed line goes to the terminal naming the file the answer went into.
|
|
141
|
-
- **What is waiting for you.** Everything the run is waiting on is a card in the right pane, each naming the artifact it is about and each opening it. The count is on the Specs tab itself, so it is visible from Pages and from the collapsed spine, and it goes when the last item is answered.
|
|
142
|
-
- **The
|
|
143
|
-
- **How an agent comes to use it.** The `dx-spec` family of skills, shipped in the image and injected like every other skill: `/dx-spec` builds a spec
|
|
181
|
+
- **What is waiting for you.** Everything the run is waiting on is a card in the right pane, each naming the artifact it is about and each opening it. They sit at the top of the board, above the protocol, the run and the review rounds: all of those are a record of what has happened, and that record grows for as long as the work runs, so anything under it leaves the pane. The count is on the Specs tab itself, so it is visible from Pages and from the collapsed spine, and it goes when the last item is answered.
|
|
182
|
+
- **The board.** Under the protocol heading, one line per confirmed stage with a mark saying where it stands - green for done, the accent blue for running, muted for waiting. A stage's name is the way into what it produced: one artifact opens from the name itself, several fold out under it on a click and fold away on the next, and a stage that produced nothing is plain text. No list is drawn until it is asked for, because the same paths under stage after stage bury the shape of the work in its own filenames. What the agent is doing at this moment is under its own **Now** heading with a pulsing dot, which is the one line on the board that is not a record of what has happened. It is there while it is current, and the heading goes with it: an activity is over the moment anything else is posted, and one that nothing follows is held for half an hour, because a quiet step is still a step. So a run that ends mid-activity - a container rebuilt, a session closed - takes its line off the board by itself, and the board never says an agent is working when none is. Under **Execution**, once there is an execution plan, the phases and where each one stands; under **Review**, each round of quality review with its verdict and how many findings it made. A pause in the run is a gate like any other: the document explaining it opens, and the gate to carry on stands beside it.
|
|
183
|
+
- **How an agent comes to use it.** The `dx-spec` family of skills, shipped in the image and injected like every other skill: `/dx-spec` builds a spec and `/dx-spec-execute` runs its plan, both honoring the engineering principles the user keeps in `principles.md` beside the work. They carry the craft alone - how to interview, what goes in a spec, how to review it, how to run a plan - and they ask through the tab, because the forms are the product's half. None of them starts a flow in a terminal with no web session: they say how to open one and stop, since there is one flow and it needs the tab.
|
|
184
|
+
|
|
185
|
+
## The Code Reviews tool
|
|
186
|
+
|
|
187
|
+
**Code Reviews** is the pane's third tab, built and mounted the same way Specs is, and it is the tool that reads the repository as a change.
|
|
188
|
+
A review is a changeset the reviewer reads hunk by hunk, with the agent on the other side of it.
|
|
189
|
+
|
|
190
|
+
- **What it serves.** One directory of the workspace, `WEBTERM_REVIEWS_ROOT`, which is `reviews/` unless it is set, and the workspace itself as the repository the reviews are of. Every git call runs with the repository as its working directory, and every diff the tool computes excludes the reviews root by pathspec, so a review is never a review of itself.
|
|
191
|
+
- **The child process.** git, and nothing else, which is the one program Home runs too. It is run by an absolute path, with no shell, as a closed list of read-only operations with fixed arguments, and with limits on time and output. An argument outside the list is refused rather than passed on. The interface never writes to the workspace from a review: no checkout, no staging and no commit, so the commit at approval and a checkout from the "Different branch" banner are asked of the agent instead.
|
|
192
|
+
- **What it shows.** A rail of the agent's groups on the left, one row each with what is left in it, and All groups as the last row. The group in view is a section in the middle, under a header that sticks to the top of the feed and holds the group's name, its counts, the agent's narrative and facts, the acts on the whole group, and four controls that walk the group hunk by hunk - the next unseen hunk, the next issue, the next attention note and the next unread reply. A hunk is a card of the diff, coloured by its own language, and the agent's notes are rows inside that diff, each one directly above the lines it is about. Under both columns runs one status bar with what is left of the review, the activity log and the way to approve.
|
|
193
|
+
- **What a review holds.** One directory each, under the reviews root, with the review's own files in a `.reviews/` inside it: the state, the baseline of the changeset with its raw diff and the blobs it needs, the reader's marks, the threads the comments made, what the reader put aside, the pending feedback, where the reader has read to, the agent's metadata and the approval. The tool is the single writer, every write is atomic, and `README.md` beside the state is rendered from it, so what the pane draws and what git holds are the same facts.
|
|
194
|
+
- **Live.** Every change under the root reaches every open window, coalesced per review: a recompute writes hundreds of files and a window that re-reads the review has no use for hundreds of events.
|
|
195
|
+
- **In a frame of its own.** As Specs: an opaque origin, two scoped credentials of its own, and nothing of the shell reachable from inside - see Security below.
|
|
196
|
+
- **What a browser may load from a review.** Two locations and no others: a stored blob, addressed by its own object name, and the signature image of an approval. Both come back through the tool's own reader, jailed like every other workspace byte. Nothing else under a review, and nothing of the repository, is served that way.
|
|
197
|
+
- **The helper.** `reviews <verb>` on PATH inside any session, the same script as `specs` under its other name and gated the same way. The verbs are `stages`, `state`, `start`, `metadata`, `group`, `ungroup`, `reply`, `activity`, `done` and `log`. Nothing in that list marks a hunk seen, closes a thread, approves, or changes the baseline of its own accord: those are the reviewer's acts, through the tab.
|
|
198
|
+
- **How an agent comes to use it.** The `dx-review` skill, shipped in the image and injected like every other skill. Every act that hands a review to the agent types that skill's command into the reviewer's session - `/dx-review` on claude and opencode, `$dx-review` on codex - and the skill reads the review's state to know which of its acts is being asked for. The three events that keep the review with the reviewer type a framed sentence instead, tagged `[reviews]`.
|
|
144
199
|
|
|
145
200
|
## The status strip
|
|
146
201
|
|
|
147
202
|
Between the terminal and the composer, a claude session shows its own numbers, live: the model and its effort, the context it is holding against the window it fits in, what is left of each rate-limit window - the five-hour one and the seven-day one, tagged `5h` and `7d` - with the time until each recharges, and the installed Claude Code version.
|
|
148
203
|
A window the account does not have takes its whole meter off the strip rather than showing an empty one.
|
|
149
204
|
|
|
150
|
-
- **Where the numbers come from.** The status line script chamba installs (`claude-statusline.sh`, baked into the image) prints nothing at all - in a browser a status line would scroll away with the output and cost a terminal row every prompt. What it does instead is write `~/.claude/context-usage/<session-id>.json` on every prompt render, and the strip is drawn from that file.
|
|
205
|
+
- **Where the numbers come from.** The status line script chamba installs (`claude-statusline.sh`, baked into the image) prints nothing at all - in a browser a status line would scroll away with the output and cost a terminal row every prompt. What it does instead is write `~/.claude/context-usage/<session-id>.json` on every prompt render, and the strip is drawn from that file.
|
|
151
206
|
- **Whose numbers they are.** A snapshot records the pid of the claude process that wrote it, and the server matches it to a session by walking up the process tree from that pid to the session's PTY leader - not by assuming the two are equal, since a wrapper or a shell may sit between them. The start time recorded beside the pid is the tiebreak: the snapshot directory is a chamba mount, so it holds files written by containers that are gone, and their pids can be live again as something else.
|
|
152
207
|
- **Only claude has one.** The snapshot is a Claude Code side effect, so a codex or opencode tab has no strip - the server sends no frame for one rather than an empty one. A claude session that has not rendered a prompt yet has no strip either, and gets one within a second or two of its first.
|
|
153
208
|
- **Live, and only when it moved.** The file is polled (`WEBTERM_STATUS_SCAN_MS`, 2s) rather than watched - it is written by a shell script into a bind mount - and a frame goes out only when something the strip shows actually changed. It is pushed to the one window watching that session, never broadcast.
|
|
@@ -170,17 +225,17 @@ A window the account does not have takes its whole meter off the strip rather th
|
|
|
170
225
|
- **A page bigger than a page is never read whole.** Anything can write into the pane directory, so a file there is not held to the publish limit. Listing a page reads only the first few kilobytes, looking for its title, and serving one is refused outright past the per-page limit - one huge file cannot stall the interface for every session in the container. The conversation-id discovery reads its stores the same way.
|
|
171
226
|
- **Pages are read without leaving the pane directory.** Keys and filenames are checked against a narrow pattern before they become a path, symlinks and anything that is not a regular file are refused rather than followed, and the resolved path is checked to still be inside the root. A file over the per-page limit is refused rather than read into memory.
|
|
172
227
|
- **Feedback is bounded and bound.** The body has a size limit, a page accepts one submission per second and only so many in its life (past that the page is refused, and no line is typed into the agent's terminal - answers are small, so a byte limit alone would let a page in a loop drive an agent for days), the answers count against the directory's total and its file count like pages do (past either, they are refused, and nothing is removed), and the directory, the page and the filename all come from the artifact the pane is showing - the request body contributes nothing but the answers, which are themselves limited in count and length. The line typed into the agent's terminal is built only from text the server controls: a title reduced to a single clean line, and a path the server generated.
|
|
173
|
-
- **
|
|
228
|
+
- **A pane tool never holds this interface's key.** Every tool's client renders in a frame with `sandbox="allow-scripts allow-forms"` and no `allow-same-origin`, exactly like a published page, so it has an opaque origin and nothing of the shell - the DOM, storage, the URL the key is in - is reachable from inside it. Each one gets two credentials of its own instead, both made fresh at server start and neither one the master key: a key that opens that tool's data routes, presented as a header, and a raw-route token that opens the files it serves, carried as a path segment because a frame and an image send no headers. Neither opens any other route, neither one is the other tool's, and presenting either one to anything else is refused. So one bug in spec rendering leaks spec reading at worst - never a review, and never the key that types into terminals.
|
|
174
229
|
- **The key is handed over once, and never on request.** The shell posts it into the frame on the load of the document it put there, unprompted. A frame's window object survives a navigation, so a document that navigated itself in would ask with the same identity as the one that was loaded; a second load is therefore read as a navigation, and the frame is thrown away and built again rather than handed anything.
|
|
175
230
|
- **A workspace file is served jailed, whatever it is.** Every answer from the raw route carries `Content-Security-Policy: sandbox allow-scripts allow-forms` and `X-Content-Type-Options: nosniff`, on every media type and on refusals too - a list of scriptable types would be one enumeration away from being wrong. The route is frame-and-subresource only: the jail's second mechanism is the sandbox of whatever embeds the file, and a top-level document has no embedder, so a request that says it is one is refused - and so is a request that will not say, since reading silence as "not a document" would hand the refusal to whoever leaves the header off. The cost of that is a browser with no Fetch Metadata, where workspace files do not render at all.
|
|
176
231
|
- **No other origin may read a workspace file.** A jailed document can read its own URL, and that URL carries the raw token; if the files that token opens were readable across origins, one bad render would be every spec in the repository, posted anywhere. So nothing on that route says anything may be read - a frame and an image need no permission - and what the tool itself reads as text comes back through the data routes instead, where a header is what opens it.
|
|
177
|
-
- **Only
|
|
232
|
+
- **Only a tool's own data routes answer a cross-origin preflight.** A tool's key is a header, which is what makes its requests ask permission first; nothing else on this origin gives that permission, and a route of another tool is not an answer either. The tool's own client and assets are served open and readable, which is a plain GET rather than an answer to a preflight, and they hold nothing secret and drive nothing. They do carry `frame-ancestors 'self'`, so a page elsewhere cannot frame a tool's document without the sandbox this interface puts around it.
|
|
178
233
|
- **A workspace document is walked before it is put on the page.** Markdown from the workspace is rendered, and then every node of what came out is walked, element by element and attribute by attribute, against written-out lists: anything not on them is removed, and a URL is decided by parsing it rather than by matching its text. What survives is moved into the page node by node, so no HTML string exists after the walk - a serialize-and-reparse is the shape most sanitizer bypasses take, and there is not one here. See `packages/pane-apps/src/lib/sanitize.ts`.
|
|
179
234
|
- **A tool's own document names no origin in what decides execution.** It is served ahead of the open statics, under a policy of its own: `default-src 'none'`, a per-response nonce for its one script and its one stylesheet, and the origin it was loaded from named only where bytes are fetched. `'self'` is not usable here and would not be safe if it were - the document has an opaque origin, where `'self'` matches nothing, and this origin also serves workspace bytes through the raw route, so a host source in `script-src` would be a way to load a workspace file as code. This is the second mechanism over workspace markdown, and it is tested against the same payload list the walk is. See `tool-document.js`.
|
|
180
|
-
- **A delivery is a pointer and a sentence the server owns.** What the tool sends is a kind of event, one line of detail, and a file it wrote. The sentence is picked from
|
|
181
|
-
- **A delivery is bounded, and refuses out loud.** The detail has a length limit checked on the text as it arrived, so an over-limit message is refused with a reason rather than cut into half a sentence. A session takes one delivery per second and only so many for as long as the container runs - a tool in a loop would otherwise drive an agent for days at one line a second. Every refusal is a sentence the tool shows the reader, so nothing is quietly dropped.
|
|
182
|
-
- **The helper's door is not the pane's.** `POST
|
|
183
|
-
- **A delivery cannot choose a terminal.** The message rides the window's own socket and the server resolves the session from it, exactly as the pane's own routes do. Two locks stand in front of that: the shell forwards a delivery only from the frame's own window, and only when it carries
|
|
235
|
+
- **A delivery is a pointer and a sentence the server owns.** What the tool sends is a kind of event, one line of detail, and a file it wrote. The sentence is picked by that event key from the tool's own table in `tools/<tool>.js`, so a tool chooses which of a few sentences is typed and never what it says. A delivery that types a command instead is picked the same way, from `tools/commands.js`, which holds one row per delivery that types a command and one spelling per agent - and a session whose agent has no spelling is refused with a reason rather than sent another agent's word. That table is also what chamba's injector reads to write opencode's command files, so the word a button types and the word the container answers to are the one table. The path has to resolve to one of that tool's own files - inside a `.specs/` or a `.reviews/`, or one of the two artifacts the Specs tool renders into a spec directory, `intake.md` and `README.md` - or the line names none. The detail is the one part that came from somewhere else, and it is reduced before it reaches a terminal: everything that is not a visible character becomes a space, invisible padding and tag characters are dropped, and the quote characters that would close the frame become plain ones. The reduction knows which agent the line is for. codex reads `$name` as a command anywhere in a line rather than at the head of one, so for a codex session every dollar becomes the fullwidth one. opencode expands a backticked shell command inside a prompt, and places the argument before it scans for one, so a backtick becomes a modifier letter that opens nothing - and the dollar goes with it, because the argument is placed with a call where `$&` and the quote forms beside it rebuild text out of the template. All three read `@path` as a file to pull in, so that one is mapped for every agent. An agent the table does not name gets every rule there is, so a fourth CLI loses a character rather than a guarantee. Each replacement reads the same and starts nothing. So a document read in the pane cannot write a line that reads as a second voice, cannot hide a tail behind the width of the row, and cannot smuggle a command into a sentence. See `typed-line.js`.
|
|
236
|
+
- **A delivery is bounded, and refuses out loud.** The detail has a length limit checked on the text as it arrived, so an over-limit message is refused with a reason rather than cut into half a sentence. A session takes one delivery per second from each tool and only so many for as long as the container runs - a tool in a loop would otherwise drive an agent for days at one line a second. The count is kept for each tool on its own, so a busy review never spends another tab's allowance. Every refusal is a sentence the tool shows the reader, so nothing is quietly dropped.
|
|
237
|
+
- **The helper's door is not the pane's.** Each tool's `POST /<tool>/agent` takes the master key like every other helper route, and the caller has to be a process inside a session, found by walking up the process tree from the pid it sends. It takes no session name from the caller - a name would be a value the server has to trust, and everything in this container could send one. A tool's key opens the pane's data routes and not this door, and this door opens none of those.
|
|
238
|
+
- **A delivery cannot choose a terminal.** The message rides the window's own socket and the server resolves the session from it, exactly as the pane's own routes do. Two locks stand in front of that: the shell forwards a delivery only from the frame's own window, and only when it carries that tool's key, so a workspace document nested inside the tool has neither, and one that navigated the frame has the window and not the key.
|
|
184
239
|
- **The key gate confines the browser, not the container.** `/tmp/webterm.key` is readable by `devuser`, because the `webpane` helper has to read it, and everything in the container is `devuser`. So a script an agent writes can do anything a browser window can: attach to another session, type into it, publish into its pane. That is the same boundary the container has always had - one agent session can already reach another's files - and it is why the isolation that matters is the container's, not the key's.
|
|
185
240
|
- Runs as the non-root `devuser`; the agent inherits the same sandbox and auth it has in the terminal.
|
|
186
241
|
|
|
@@ -190,9 +245,9 @@ A window the account does not have takes its whole meter off the strip rather th
|
|
|
190
245
|
|
|
191
246
|
## Config
|
|
192
247
|
|
|
193
|
-
`config.js` holds the knobs (port, agent list and default agent, key and key file, upload dir, size limit, cleanup age/interval, paste framing, resume stamp, state file, session limit, keepalive interval, stop timings, claude context file, workspace root and directory-scan limits, the pane's directory, limits, scan interval, feedback bounds and agent stores, the status strip's scan interval,
|
|
194
|
-
Env overrides: `WEBTERM_PORT`, `WEBTERM_CWD`, `WEBTERM_AGENT`, `WEBTERM_AGENT_ARGS`, `WEBTERM_KEY`, `WEBTERM_KEY_FILE`, `WEBTERM_RESUME_STAMP`, `WEBTERM_STATE_FILE`, `WEBTERM_MAX_SESSIONS`, `WEBTERM_PING_INTERVAL_MS`, `WEBTERM_WORKSPACE`, `WEBTERM_CONTEXT_FILE`, `WEBTERM_PANE_DIR`, `WEBTERM_MAX_PAGE_BYTES`, `WEBTERM_MAX_PANE_BYTES`, `WEBTERM_MAX_PANE_FILES`, `WEBTERM_PANE_SCAN_MS`, `WEBTERM_FEEDBACK_MIN_INTERVAL_MS`, `WEBTERM_MAX_FEEDBACK_PER_PAGE`, `WEBTERM_STATUS_SCAN_MS`, `WEBTERM_SUBMIT_DELAY_MS`, `WEBTERM_SPECS_ROOT`, `WEBTERM_SPECS_KEY`, `WEBTERM_SPECS_RAW_TOKEN`, `WEBTERM_MAX_SPECS_LINE_LENGTH`, `WEBTERM_SPECS_LINE_MIN_INTERVAL_MS`, `WEBTERM_MAX_SPECS_LINES_PER_SESSION`, `WEBTERM_SPECS_NEW_SESSION_DELAY_MS`, and the four `WEBTERM_*_DIR` agent-store paths, plus `WEBTERM_PROC_ROOT`, which belongs to `proc.js` rather than to `config.js`.
|
|
195
|
-
`WEBTERM_KEY` pins the key instead of creating one, and
|
|
248
|
+
`config.js` holds the knobs (port, agent list and default agent, key and key file, upload dir, size limit, cleanup age/interval, paste framing, resume stamp, state file, colour file, session limit, keepalive interval, stop timings, claude context file, workspace root and directory-scan limits, the pane's directory, limits, scan interval, feedback bounds and agent stores, the status strip's scan interval, the root and the two scoped credentials each pane tool is mounted with, Home's cache directory, and the bounds on a delivery, per tool).
|
|
249
|
+
Env overrides: `WEBTERM_PORT`, `WEBTERM_CWD`, `WEBTERM_AGENT`, `WEBTERM_AGENT_ARGS`, `WEBTERM_KEY`, `WEBTERM_KEY_FILE`, `WEBTERM_RESUME_STAMP`, `WEBTERM_STATE_FILE`, `WEBTERM_COLOR_FILE`, `WEBTERM_MAX_SESSIONS`, `WEBTERM_PING_INTERVAL_MS`, `WEBTERM_WORKSPACE`, `WEBTERM_CONTEXT_FILE`, `WEBTERM_PANE_DIR`, `WEBTERM_MAX_PAGE_BYTES`, `WEBTERM_MAX_PANE_BYTES`, `WEBTERM_MAX_PANE_FILES`, `WEBTERM_PANE_SCAN_MS`, `WEBTERM_FEEDBACK_MIN_INTERVAL_MS`, `WEBTERM_MAX_FEEDBACK_PER_PAGE`, `WEBTERM_STATUS_SCAN_MS`, `WEBTERM_SUBMIT_DELAY_MS`, `WEBTERM_SPECS_ROOT`, `WEBTERM_SPECS_KEY`, `WEBTERM_SPECS_RAW_TOKEN`, `WEBTERM_MAX_SPECS_LINE_LENGTH`, `WEBTERM_SPECS_LINE_MIN_INTERVAL_MS`, `WEBTERM_MAX_SPECS_LINES_PER_SESSION`, `WEBTERM_SPECS_NEW_SESSION_DELAY_MS`, the same seven for the Code Reviews tool - `WEBTERM_REVIEWS_ROOT`, `WEBTERM_REVIEWS_KEY`, `WEBTERM_REVIEWS_RAW_TOKEN`, `WEBTERM_MAX_REVIEWS_LINE_LENGTH`, `WEBTERM_REVIEWS_LINE_MIN_INTERVAL_MS`, `WEBTERM_MAX_REVIEWS_LINES_PER_SESSION` and `WEBTERM_REVIEWS_NEW_SESSION_DELAY_MS` - `WEBTERM_HOME_CACHE_DIR`, `WEBTERM_HOME_KEY` and `WEBTERM_HOME_RAW_TOKEN`, which are the whole of Home's own set, and the four `WEBTERM_*_DIR` agent-store paths, plus `WEBTERM_PROC_ROOT`, which belongs to `proc.js` rather than to `config.js`.
|
|
250
|
+
`WEBTERM_KEY` pins the key instead of creating one, and each tool's two scoped credentials the same way, which is for tests and hand-run debugging - there is no way to turn any of those gates off.
|
|
196
251
|
`WEBTERM_CWD` is where new sessions start, not where they must stay: the browser can name another directory per session, and `POST /cwd` moves the default.
|
|
197
252
|
`WEBTERM_AGENT` is the same shape: the agent new sessions start with, which the browser can override per session and `POST /agent` moves. A value that is not one of the three falls back to the first, so nothing arbitrary can be spawned through it.
|
|
198
253
|
`WEBTERM_AGENT_ARGS` belongs to `WEBTERM_AGENT` alone - it comes from the same launcher run - so any other agent is spawned bare.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// color.js - the accent colour the user picked for this workspace, as the container keeps it.
|
|
2
|
+
//
|
|
3
|
+
// One line in the workspace's own state directory, which is bind-mounted from the host, so the choice
|
|
4
|
+
// outlives the container it was made in. The file holds the colour's id and never a hex: the palette belongs
|
|
5
|
+
// to the browser, and storing an id is what lets a later version move a shade without stranding a workspace
|
|
6
|
+
// on a value nobody chose.
|
|
7
|
+
//
|
|
8
|
+
// Which also means nothing here can say whether an id names a real colour, and nothing here tries. The size
|
|
9
|
+
// and the shape are checked, the browser decides that an id it does not know means nothing is stored, and a
|
|
10
|
+
// window that sends something else is corrected by the next sessions frame it receives.
|
|
11
|
+
//
|
|
12
|
+
// No globals and nothing done at load, so the tests import this file and run it. That is the whole reason it
|
|
13
|
+
// is not inside server.js, which binds a port and pulls in node-pty.
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
16
|
+
|
|
17
|
+
// Lowercase letters, digits and dashes, up to sixteen of them. The value is file content and never a path
|
|
18
|
+
// segment, so size and shape are the whole of it.
|
|
19
|
+
const COLOR_ID = /^[a-z0-9-]{1,16}$/;
|
|
20
|
+
|
|
21
|
+
/** Whether an id may be stored. The empty string is not one: it asks for automatic, which stores nothing. */
|
|
22
|
+
export function isColorId(value) {
|
|
23
|
+
return typeof value === "string" && COLOR_ID.test(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The stored id, or "" when nothing is stored. A missing file, an empty file, and a file this server cannot
|
|
28
|
+
* read or will not accept all mean the same thing: the workspace wears the colour the port gives it.
|
|
29
|
+
*/
|
|
30
|
+
export function readColor(file) {
|
|
31
|
+
if (!file) return "";
|
|
32
|
+
try {
|
|
33
|
+
const raw = readFileSync(file, "utf8").trim();
|
|
34
|
+
return isColorId(raw) ? raw : "";
|
|
35
|
+
} catch {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Store an id, or delete the file when the id is the empty string, and answer with what now applies.
|
|
42
|
+
*
|
|
43
|
+
* Answers null for a value this server refuses, which is the caller's signal to write nothing and broadcast
|
|
44
|
+
* nothing. A failure of the write itself is warned about and never thrown, the way publishAgent() handles its
|
|
45
|
+
* own file: the colour still applies in every open window, which is what the user asked for, and the
|
|
46
|
+
* interface has nothing useful to say to them about a state directory it cannot write.
|
|
47
|
+
*/
|
|
48
|
+
export function storeColor(file, value) {
|
|
49
|
+
if (value !== "" && !isColorId(value)) return null;
|
|
50
|
+
if (!file) return value;
|
|
51
|
+
try {
|
|
52
|
+
if (value === "") {
|
|
53
|
+
if (existsSync(file)) unlinkSync(file);
|
|
54
|
+
} else {
|
|
55
|
+
writeFileSync(file, `${value}\n`);
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.warn(`[webterm] could not write ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|