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
package/README.md
CHANGED
|
@@ -36,6 +36,7 @@ The optional fields - `shadow_paths`, `env`, `ports`, `profiles` - are all descr
|
|
|
36
36
|
- **Hidden paths.** Shadow paths (`node_modules`, `.env*` by default) are overlaid with empty container-local copies, so an agent never reads the real thing.
|
|
37
37
|
- **The current CLIs.** claude, opencode and codex are kept on their latest versions, each with its config directory bind-mounted to the workspace cache so conversations and memory survive rebuilds.
|
|
38
38
|
- **A web interface.** The browser front-end relays the real agent TUI over a websocket - every session on one page, several agents at once, images and dictation. It starts with the container. Claude sessions also get a status strip above the composer: model, context used, quota left.
|
|
39
|
+
- **A pane of tools beside the terminal.** Pages, where an agent publishes a page for you to read or a form for you to answer, and Specs, where a piece of work is read, annotated and steered. An agent reaches Specs with the `specs` helper on its path, and the `dx-spec` skills teach it the craft.
|
|
39
40
|
|
|
40
41
|
The menu has two doors into that container.
|
|
41
42
|
**Web interface** brings it up, opens the interface in your browser, and hands your terminal straight back - the work happens in the tab.
|
|
@@ -43,15 +44,21 @@ The menu has two doors into that container.
|
|
|
43
44
|
|
|
44
45
|
## The web pane
|
|
45
46
|
|
|
46
|
-
Beside the terminal is a pane
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
A page can also ask.
|
|
50
|
-
A form marked `data-feedback` is wired up by the interface, and what you submit is written as JSON next to the page, with one line typed into the agent's terminal saying where to read it.
|
|
47
|
+
Beside the terminal is a pane, there from the first second of a session, and the pane holds tools.
|
|
48
|
+
One tab each, and the width controls beside them: drag the divider, take the whole window, or fold the pane into the spine at its edge.
|
|
51
49
|
|
|
50
|
+
**Pages** is where an agent publishes.
|
|
51
|
+
`webpane report.html --title "Port rework"` puts a standalone HTML file in the pane as a chip: a plan, a comparison table, a long answer, anything worth reading at your own pace instead of scrolling back for.
|
|
52
|
+
A page can also ask - a form marked `data-feedback` is wired up by the interface, and what you submit is written as JSON next to the page, with one line typed into the agent's terminal saying where to read it.
|
|
52
53
|
Pages are kept, one directory per conversation, so resuming a conversation opens with them again.
|
|
53
54
|
They live under `~/.chamba/workspaces/<id>/agents/webpane/` on the host, and nothing deletes them.
|
|
54
55
|
|
|
56
|
+
**Specs** is where a piece of work is read and steered.
|
|
57
|
+
It renders the specs directory of your repository: the work, its artifacts, and where each piece stands.
|
|
58
|
+
Read an artifact as one continuous document, select any part of it to comment on or ask for its removal, and answer what the run asks - a gate on a screen, a round of questions, a decision that needs your judgment - as a form beside the thing it is about.
|
|
59
|
+
Everything is a file in your repository, so two windows agree, a session that ends loses nothing, and git sees the work.
|
|
60
|
+
The agent's half is `specs` on its path inside the container, and the `dx-spec` skills carry the craft: `/dx-spec` builds a specification, `/dx-spec-execute` runs its plan, and `/dx-spec-config` keeps the settings beside them.
|
|
61
|
+
|
|
55
62
|
## Where state lives
|
|
56
63
|
|
|
57
64
|
`chamba.yaml` lives in the workspace and can be committed.
|
|
@@ -62,7 +69,7 @@ Everything else is on your machine under `~/.chamba/`: one directory per workspa
|
|
|
62
69
|
```bash
|
|
63
70
|
pnpm install
|
|
64
71
|
pnpm start # run the CLI from source (host only - Docker is not available in the dev container)
|
|
65
|
-
pnpm check # typecheck, lint, repo checks, tests
|
|
72
|
+
pnpm check # build, typecheck, lint, repo checks, tests
|
|
66
73
|
pnpm release # check, bump, commit and publish to npm
|
|
67
74
|
```
|
|
68
75
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// Claude Code: https://docs.anthropic.com/en/docs/claude-code
|
|
9
9
|
// OpenCode: https://github.com/opencode-ai/opencode
|
|
10
10
|
// Codex: https://github.com/openai/codex
|
|
11
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
13
|
import { AGENTS_DIR, CLAUDE_STATUSLINE_PATH, CONTAINER_HOME, GIT_MODE, PACKAGE_ROOT } from "./constants.js";
|
|
14
14
|
export const AGENT_MOUNTS = [
|
|
@@ -145,8 +145,12 @@ export function ensureClaudeStatusLine(workspaceDir) {
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
// --- Claude skills injection -------------------------------------------------------------------------------------------------------------
|
|
148
|
-
//
|
|
149
|
-
//
|
|
148
|
+
// Copies every templates/skills/<name>/ into agents/claude/skills/<name>/, whole. Drop a new directory
|
|
149
|
+
// under templates/skills/ to ship a new skill - no other code changes needed.
|
|
150
|
+
//
|
|
151
|
+
// The directory rather than the one file, because a skill of any size keeps its craft in references/ and
|
|
152
|
+
// points at those files by name. A SKILL.md that reaches a session without them names files that are not
|
|
153
|
+
// there, which is worse than shipping nothing.
|
|
150
154
|
export function injectClaudeSkills(workspaceDir) {
|
|
151
155
|
const templatesSkillsDir = join(PACKAGE_ROOT, "templates", "skills");
|
|
152
156
|
if (!existsSync(templatesSkillsDir))
|
|
@@ -158,11 +162,33 @@ export function injectClaudeSkills(workspaceDir) {
|
|
|
158
162
|
for (const entry of readdirSync(templatesSkillsDir, { withFileTypes: true })) {
|
|
159
163
|
if (!entry.isDirectory())
|
|
160
164
|
continue;
|
|
161
|
-
|
|
162
|
-
if (!existsSync(sourcePath))
|
|
165
|
+
if (!existsSync(join(templatesSkillsDir, entry.name, "SKILL.md")))
|
|
163
166
|
continue;
|
|
164
|
-
|
|
165
|
-
|
|
167
|
+
copySkillDir(join(templatesSkillsDir, entry.name), join(targetSkillsDir, entry.name), vars);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// One skill's files, at whatever depth they sit. Markdown is rendered with the skill variables, since a
|
|
171
|
+
// reference states the same placeholders SKILL.md does; anything else is copied byte for byte.
|
|
172
|
+
function copySkillDir(from, to, vars) {
|
|
173
|
+
for (const entry of readdirSync(from, { withFileTypes: true })) {
|
|
174
|
+
const source = join(from, entry.name);
|
|
175
|
+
const target = join(to, entry.name);
|
|
176
|
+
// A link is copied as what it points at. The container gets a directory rather than a tree of links,
|
|
177
|
+
// and a shared reference brought in as a link still ships - a skill naming a file this walk skipped
|
|
178
|
+
// is exactly the broken skill that copying the whole directory exists to prevent.
|
|
179
|
+
const kind = statSync(source);
|
|
180
|
+
if (kind.isDirectory()) {
|
|
181
|
+
copySkillDir(source, target, vars);
|
|
182
|
+
}
|
|
183
|
+
else if (kind.isFile()) {
|
|
184
|
+
if (entry.name.endsWith(".md")) {
|
|
185
|
+
writeFileEnsuringDir(target, renderTemplate(readFileSync(source, "utf8"), vars));
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
189
|
+
copyFileSync(source, target);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
166
192
|
}
|
|
167
193
|
}
|
|
168
194
|
// --- Inject agent context ----------------------------------------------------------------------------------------------------------------
|
|
@@ -74,6 +74,7 @@ export const BAKED_TEMPLATE_FILES = [
|
|
|
74
74
|
"npmrc",
|
|
75
75
|
"pnpm-config.yaml",
|
|
76
76
|
"runtime-constants.mjs",
|
|
77
|
+
"specs.sh",
|
|
77
78
|
"startup-git-mode.mjs",
|
|
78
79
|
"startup.mjs",
|
|
79
80
|
"webpane.sh",
|
|
@@ -82,7 +83,7 @@ export const BAKED_TEMPLATE_FILES = [
|
|
|
82
83
|
// Directories (relative to the templates dir) baked into the image via a directory COPY. Hashed
|
|
83
84
|
// recursively by computeBuildHash so editing any file inside triggers a rebuild prompt, with
|
|
84
85
|
// node_modules skipped - a local install there must never flip the hash (it is .dockerignore'd too).
|
|
85
|
-
export const BAKED_TEMPLATE_DIRS = ["webterm"];
|
|
86
|
+
export const BAKED_TEMPLATE_DIRS = ["pane-apps", "webterm"];
|
|
86
87
|
// Relative paths of every regular file under dir (sorted, node_modules subtrees skipped). Missing
|
|
87
88
|
// dirs yield [] for the same reason computeBuildHash tolerates missing files: minimal test contexts.
|
|
88
89
|
function walkTemplateDir(root, prefix = "") {
|
package/dist/lib/safe-rm.js
CHANGED
|
@@ -7,20 +7,30 @@
|
|
|
7
7
|
import { rmSync } from "node:fs";
|
|
8
8
|
import { homedir, tmpdir } from "node:os";
|
|
9
9
|
import { basename, join, resolve, sep } from "node:path";
|
|
10
|
-
import { CHAMBA_DIR, CHAMBA_YAML, CONTAINER_NAME_PREFIX } from "./constants.js";
|
|
10
|
+
import { CHAMBA_DIR, CHAMBA_YAML, CONTAINER_NAME_PREFIX, PACKAGE_ROOT } from "./constants.js";
|
|
11
11
|
const CHAMBA_HOME = resolve(join(homedir(), CHAMBA_DIR));
|
|
12
12
|
const TEST_TMP_PREFIX = join(tmpdir(), `${CONTAINER_NAME_PREFIX}test-`);
|
|
13
|
+
// chamba's own build outputs, inside the package itself. The build stages the pane apps into the
|
|
14
|
+
// templates tree, and it clears the previous staging before it copies: every asset in that dist is
|
|
15
|
+
// named by its own content hash, so merging would keep every past build's files for ever, and the
|
|
16
|
+
// image build hash covers each of them.
|
|
17
|
+
const BUILD_OUTPUTS = [resolve(join(PACKAGE_ROOT, "dist")), resolve(join(PACKAGE_ROOT, "templates", "pane-apps"))];
|
|
13
18
|
/**
|
|
14
19
|
* Safe wrapper around rmSync. Throws if the path is outside a chamba-owned location:
|
|
15
20
|
* - ~/.chamba/ (workspace caches, agents, shadows, global config)
|
|
16
21
|
* - A file named chamba.yaml (workspace config file in any user workspace root)
|
|
17
22
|
* - <tmpdir>/chamba-test-* (test temp directories)
|
|
23
|
+
* - chamba's own build outputs inside the package (dist/, templates/pane-apps/)
|
|
18
24
|
*/
|
|
19
25
|
export function safeRmSync(path, options) {
|
|
20
26
|
const r = resolve(path);
|
|
21
|
-
const ok = r === CHAMBA_HOME ||
|
|
27
|
+
const ok = r === CHAMBA_HOME ||
|
|
28
|
+
r.startsWith(CHAMBA_HOME + sep) ||
|
|
29
|
+
basename(r) === CHAMBA_YAML ||
|
|
30
|
+
r.startsWith(TEST_TMP_PREFIX) ||
|
|
31
|
+
BUILD_OUTPUTS.some((output) => r === output || r.startsWith(output + sep));
|
|
22
32
|
if (!ok) {
|
|
23
|
-
throw new Error(`safeRmSync: refusing to delete '${r}' - must be under ~/.chamba/, named chamba.yaml,
|
|
33
|
+
throw new Error(`safeRmSync: refusing to delete '${r}' - must be under ~/.chamba/, named chamba.yaml, a test temp dir, or a build output of this package`);
|
|
24
34
|
}
|
|
25
35
|
rmSync(r, options);
|
|
26
36
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chamba",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Run AI coding agents in a container, from your browser",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"js-yaml": "^4.1.1"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@biomejs/biome": "
|
|
16
|
+
"@biomejs/biome": "2.5.5",
|
|
17
17
|
"@types/js-yaml": "^4.0.9",
|
|
18
18
|
"@types/node": "^25.4.0",
|
|
19
19
|
"tsx": "^4.21.0",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"test:docker": "node --import tsx/esm --test 'tests/docker/**/*.test.ts'",
|
|
31
31
|
"test:all": "pnpm test && pnpm test:docker",
|
|
32
32
|
"validate": "pnpm typecheck && pnpm lint && tsx scripts/check.ts && pnpm test",
|
|
33
|
-
"check": "pnpm
|
|
33
|
+
"check": "pnpm re:build && pnpm validate",
|
|
34
34
|
"release": "tsx scripts/release.ts",
|
|
35
35
|
"prepublishOnly": "pnpm re:build"
|
|
36
36
|
},
|
package/templates/Dockerfile
CHANGED
|
@@ -157,7 +157,17 @@ RUN chmod +x /usr/local/share/chamba/webterm.sh && \
|
|
|
157
157
|
ln -sf /usr/local/share/chamba/webterm.sh /usr/local/bin/webterm
|
|
158
158
|
|
|
159
159
|
# ---------------------------------------------------------------------------
|
|
160
|
-
# Layer 14 - Bake the
|
|
160
|
+
# Layer 14 - Bake the pane apps (the tool UIs the pane hosts, and their servers)
|
|
161
|
+
# One built dist from the private pane-apps package: a client per tool and the
|
|
162
|
+
# module webterm mounts for it. Beside webterm rather than inside it, because
|
|
163
|
+
# webterm resolves it as a sibling and its own npm ci layer stays untouched by
|
|
164
|
+
# a UI change. Nothing to install - each server is bundled with no dependency
|
|
165
|
+
# outside node's own builtins.
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
COPY pane-apps/ /usr/local/share/chamba/pane-apps/
|
|
168
|
+
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
# Layer 15 - Bake the web pane publish helper
|
|
161
171
|
# Publishes an HTML file as a page in the calling session's pane. On PATH via
|
|
162
172
|
# the symlink, so any process inside a session can use it.
|
|
163
173
|
# ---------------------------------------------------------------------------
|
|
@@ -165,6 +175,15 @@ COPY webpane.sh /usr/local/share/chamba/webpane.sh
|
|
|
165
175
|
RUN chmod +x /usr/local/share/chamba/webpane.sh && \
|
|
166
176
|
ln -sf /usr/local/share/chamba/webpane.sh /usr/local/bin/webpane
|
|
167
177
|
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
# Layer 16 - Bake the Specs helper
|
|
180
|
+
# The agent's half of the Specs channel: the verbs that move a piece of work.
|
|
181
|
+
# Gated like webpane, so only a process inside a session may use it.
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
COPY specs.sh /usr/local/share/chamba/specs.sh
|
|
184
|
+
RUN chmod +x /usr/local/share/chamba/specs.sh && \
|
|
185
|
+
ln -sf /usr/local/share/chamba/specs.sh /usr/local/bin/specs
|
|
186
|
+
|
|
168
187
|
WORKDIR /workspace
|
|
169
188
|
|
|
170
189
|
# ---------------------------------------------------------------------------
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--bg:#0d1117;--panel:#161b22;--fg:#e6edf3;--muted:#8b949e;--border:#30363d;--accent:#58a6ff;--ok:#3fb950;--warn:#d29922;--sunken:#0b0e13;--sel:#1f6feb33}:root[data-theme=light]{--bg:#fff;--panel:#f6f8fa;--fg:#1f2328;--muted:#656d76;--border:#d0d7de;--accent:#0969da;--ok:#1a7f37;--warn:#9a6700;--sunken:#f6f8fa;--sel:#ddf4ff}*{box-sizing:border-box}html,body,#root{height:100%}body{background:var(--bg);color:var(--fg);margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-size:14px;line-height:1.6}@media (prefers-reduced-motion:no-preference){html{scroll-behavior:smooth}}.workspace{grid-template-columns:minmax(180px,220px) minmax(0,1fr) minmax(220px,280px);height:100%;display:grid}.nav,.rail{background:var(--panel);font-size:12.5px;overflow:auto}.nav{border-right:1px solid var(--border);padding:10px 0 24px}.rail{border-left:1px solid var(--border);flex-direction:column;gap:18px;padding:12px 14px 24px;display:flex}.nav-head,.rail-head{letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin:12px 0 5px;padding:0 14px;font-size:10.5px;font-weight:700}.rail-head{padding:0}.nav-list{margin:0;padding:0;list-style:none}.nav-list button{text-align:left;width:100%;color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;border-left:2px solid #0000;padding:3px 14px;display:block}.nav-list button:hover{color:var(--accent)}.nav-list button[aria-current]{background:var(--sel);border-left-color:var(--accent)}.nav-list button.nav-artifact{padding-left:22px}.nav-list button.nav-spec{align-items:baseline;gap:6px;display:flex}.nav-spec-name{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.nav-spec-status{color:var(--muted);letter-spacing:.04em;text-transform:uppercase;flex:none;font-size:10px}.nav-spec-awaiting{background:var(--accent);min-width:15px;color:var(--bg);text-align:center;border-radius:7px;flex:none;padding:0 4px;font-size:10px;font-weight:700}.nav-dir{color:var(--muted);letter-spacing:.04em;margin:8px 0 2px;padding:0 14px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10.5px}.nav-sections{margin:0 0 4px;padding:0;font-size:12px;list-style:none}.nav-sections button{color:var(--muted);padding-left:32px}.nav-sections button[aria-current]{color:var(--fg);font-weight:600}.nav-sections button.is-read{opacity:.55}.nav-changed{color:var(--warn);vertical-align:middle;margin-left:6px;font-size:9px}.doc{padding:20px 40px 96px;overflow:auto}.doc-facts{color:var(--muted);align-items:center;gap:10px;margin-bottom:14px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;display:flex}.doc-section{scroll-margin-top:12px;position:relative}.doc-section.is-read{opacity:.62}.doc-section h2{margin:22px 0 8px;font-size:1.15em}.sec-mark{position:absolute;top:24px;left:-26px}.mark{border:1px solid var(--border);background:var(--bg);color:#0000;cursor:pointer;border-radius:5px;width:20px;height:20px;padding:0;font-size:11px;line-height:1}.doc-section:hover .mark,.mark:focus-visible{color:var(--muted)}.mark[aria-pressed=true]{color:var(--ok);border-color:var(--ok)}.sec-changed{color:var(--warn);margin-left:10px;font-size:11px;font-weight:400}.art-frame{border:1px solid var(--border);background:var(--bg);border-radius:8px;width:100%;height:calc(100vh - 90px)}.art-image img{max-width:100%}.rail-top{justify-content:flex-end;display:flex}.rail-fact{margin:0 0 2px}.rail-changed{color:var(--warn)}.theme-toggle{border:1px solid var(--border);color:var(--muted);font:inherit;cursor:pointer;background:0 0;border-radius:999px;padding:2px 12px;font-size:11px}.notice{color:var(--muted);padding:20px 24px}.notice-failure{border-left:3px solid var(--warn);color:var(--fg)}.muted{color:var(--muted)}.visually-hidden{clip-path:inset(50%);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}.plain{white-space:pre-wrap;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;overflow-x:auto}.prose{overflow-wrap:anywhere;font-size:13.5px}.prose h1{margin:18px 0 8px;font-size:1.4em}.prose h2{margin:18px 0 8px;font-size:1.2em}.prose h3{margin:16px 0 6px;font-size:1.08em}.prose h4,.prose h5,.prose h6{color:var(--muted);margin:14px 0 6px;font-size:1em}.prose p{margin:0 0 10px}.prose ul,.prose ol{margin:0 0 10px;padding-left:22px}.prose li{margin:2px 0}.prose li.task{margin-left:-20px;list-style:none}.task-box{border:1px solid var(--muted);vertical-align:baseline;border-radius:3px;width:11px;height:11px;margin-right:8px;display:inline-block}.task-box.is-done{border-color:var(--ok);background:var(--ok)}.prose a{color:var(--accent)}.prose code{background:var(--sunken);border:1px solid var(--border);border-radius:5px;padding:1px 5px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.88em}.prose blockquote{border-left:3px solid var(--border);color:var(--muted);margin:0 0 12px;padding:2px 14px}.prose blockquote>:last-child{margin-bottom:0}.prose hr{border:0;border-top:1px solid var(--border);margin:16px 0}.prose img{max-width:100%}.prose table{border-collapse:collapse;margin:0 0 12px;font-size:.95em;display:block;overflow-x:auto}.prose th,.prose td{border:1px solid var(--border);text-align:left;padding:4px 10px}.prose th{background:var(--sunken)}.prose .cell-left{text-align:left}.prose .cell-center{text-align:center}.prose .cell-right{text-align:right}.prose pre{background:var(--sunken);border:1px solid var(--border);border-radius:8px;margin:0 0 12px;padding:12px;overflow-x:auto}.prose pre code{background:0 0;border:0;padding:0}.annotate{z-index:2;border:1px solid var(--border);background:var(--panel);border-radius:7px;gap:4px;padding:3px 6px;display:flex;position:absolute;box-shadow:0 4px 14px #00000059}.annotate button{color:var(--accent);font:inherit;cursor:pointer;background:0 0;border:none;border-radius:5px;padding:2px 5px;font-size:13px;line-height:1}.annotate button:hover{background:var(--sel)}.annotate-whole{border:1px solid var(--border);color:var(--accent);font:inherit;cursor:pointer;background:0 0;border-radius:999px;padding:0 9px;font-size:10.5px}.queue{margin:0 0 8px;padding:0;list-style:none}.filed{margin:0;padding:0;list-style:none}.filed li{border-bottom:1px solid var(--border);align-items:baseline;gap:6px;padding:3px 0;font-size:11.5px;display:flex}.filed li:last-child{border-bottom:none}.filed-round{flex:none;font-weight:600}.filed-count{color:var(--muted);flex:1}.filed-when{color:var(--muted);flex:none;font-size:10.5px}.queued{border:1px solid var(--border);border-left:3px solid var(--accent);background:var(--bg);border-radius:7px;margin-bottom:7px;padding:7px 9px}.queued.is-remove{border-left-color:var(--warn)}.queued-target{color:var(--muted);margin:0;font-size:11px;font-style:italic}.queued-kind{color:var(--warn);margin:3px 0 0;font-size:11.5px}.queued textarea{background:var(--panel);width:100%;min-height:42px;color:inherit;border:1px solid var(--border);font:inherit;resize:vertical;border-radius:6px;margin-top:4px;padding:5px 7px;font-size:12px}.queued-acts{gap:10px;margin-top:4px;display:flex}.queued-acts button{color:var(--accent);font:inherit;cursor:pointer;background:0 0;border:none;padding:0;font-size:11px}.queued-acts button:disabled{color:var(--muted);cursor:default}.send{background:var(--accent);width:100%;color:var(--bg);font:inherit;cursor:pointer;border:none;border-radius:7px;padding:6px 0;font-size:12px}.send:disabled{background:var(--border);color:var(--muted);cursor:default}.rail-continue{border:1px solid var(--border);color:var(--accent);font:inherit;cursor:pointer;background:0 0;border-radius:7px;margin-top:6px;padding:3px 10px;font-size:11.5px}.dialog-shade{z-index:10;background:#00000080;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.dialog{border:1px solid var(--border);background:var(--panel);border-radius:10px;max-width:340px;padding:16px 18px}.dialog p{margin:0 0 12px}.dialog-acts{justify-content:flex-end;gap:10px;display:flex}.dialog-acts button{border:1px solid var(--border);color:inherit;font:inherit;cursor:pointer;background:0 0;border-radius:7px;padding:4px 12px;font-size:12px}.dialog-acts .send{border-color:var(--accent);width:auto}.nav-top{justify-content:space-between;align-items:baseline;gap:8px;display:flex}.nav-new{border:1px solid var(--border);color:var(--accent);font:inherit;cursor:pointer;background:0 0;border-radius:7px;padding:2px 9px;font-size:11px}.board-list{margin:0;padding:0;list-style:none}.board-stage{padding:2px 0;font-size:12px}.board-stage-name{color:var(--muted);margin:0}.board-stage.is-running .board-stage-name,.board-stage.is-done .board-stage-name{color:var(--text)}.board-mark{width:14px;color:var(--muted);display:inline-block}.board-stage.is-running .board-mark{color:var(--accent)}.board-artifacts{margin:0 0 4px 14px;padding:0;list-style:none}.board-artifacts button{color:var(--accent);font:inherit;cursor:pointer;text-align:left;background:0 0;border:none;padding:1px 0;font-size:11.5px}.board-activity{color:var(--accent);margin:6px 0 0;font-size:11.5px}.board-awaiting{gap:6px;margin:10px 0 0;padding:0;list-style:none;display:grid}.form{padding-bottom:40px}.form .lede{color:var(--muted)}.choices{gap:12px;margin:0;padding:0;list-style:none;display:grid}.choices li{border:1px solid var(--border);border-radius:8px;padding:10px 12px}.choices li.is-recommended{border-color:var(--accent)}.choices label{cursor:pointer;align-items:center;gap:8px;display:flex}.choice-title{font-weight:600}.choice-flag{color:var(--accent);text-transform:uppercase;letter-spacing:.06em;margin-left:8px;font-size:10.5px;font-weight:400}.choice-about{color:var(--muted);margin:4px 0 0 22px;font-size:12.5px}.choice-why{margin:4px 0 0 22px;font-size:12.5px}.choice-mode{cursor:pointer;align-items:flex-start;gap:8px;display:flex}.field{margin-bottom:12px;display:block}.field>span{color:var(--muted);margin-bottom:4px;font-size:11.5px;display:block}.field input,.field-dump{box-sizing:border-box;background:var(--bg);border:1px solid var(--border);width:100%;color:inherit;font:inherit;resize:vertical;border-radius:7px;padding:7px 9px;font-size:13px}.field-dump{min-height:140px}.attached{gap:4px;margin:0 0 12px;padding:0;font-size:12px;list-style:none;display:grid}.attached li{justify-content:space-between;gap:8px;display:flex}.attached button{color:var(--muted);font:inherit;cursor:pointer;background:0 0;border:none;font-size:11px}.form-acts{justify-content:flex-end;gap:10px;margin-top:20px;display:flex}.form-acts button{border:1px solid var(--border);color:inherit;font:inherit;cursor:pointer;background:0 0;border-radius:7px;padding:5px 14px;font-size:12.5px}.form-acts .send{border-color:var(--accent);width:auto;color:var(--accent)}.dialog-wide{width:90%;max-width:560px}.doc-and-ask{flex-direction:column;min-width:0;display:flex;overflow-y:auto}.doc-and-ask .doc{flex:1 0 auto;overflow-y:visible}.ask{border:1px solid var(--accent);background:var(--panel);border-radius:10px;width:calc(100% - 80px);max-width:780px;margin:0 auto 40px;padding:16px 20px}.ask-head h2{margin:0;font-size:15px}.ask-about{color:var(--muted);margin:2px 0 10px;font-size:11.5px}.ask-evidence{margin:0 0 8px}.ask-tradeoff{color:var(--muted);margin:0 0 12px}.questions{gap:18px;margin:0 0 14px;padding-left:18px;display:grid}.question-ask{margin:0 0 8px;font-weight:600}.question-said{box-sizing:border-box;background:var(--bg);border:1px solid var(--border);width:100%;min-height:48px;color:inherit;font:inherit;resize:vertical;border-radius:7px;margin-top:8px;padding:6px 8px;font-size:12.5px}.board-review{padding:3px 0;font-size:12px}
|