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,154 @@
|
|
|
1
|
+
// =========================================================================================================================================
|
|
2
|
+
// tools/specs.js - What the Specs tool is, as one table.
|
|
3
|
+
//
|
|
4
|
+
// The mount beside this file holds every rule that is about a request: the credentials, the preflight, the jail on raw bytes and
|
|
5
|
+
// the helper's door. What is here is the tool itself - where its module comes from and what it is pointed at, which routes the pane
|
|
6
|
+
// may call, which verbs its helper carries, which files a browser loads by their own address, what its framed sentences say, and
|
|
7
|
+
// what it is limited to.
|
|
8
|
+
//
|
|
9
|
+
// The two tables are the boundary of this tool. What the user does is in `routes`, and what the agent does is in `verbs`: neither
|
|
10
|
+
// one holds the other's half, so an agent cannot approve its own gate and the pane cannot post progress.
|
|
11
|
+
// =========================================================================================================================================
|
|
12
|
+
|
|
13
|
+
import { BadRequest, createSpecsModule, statusFor } from "../../pane-apps/server/specs.mjs";
|
|
14
|
+
import {
|
|
15
|
+
MAX_SPECS_BYTES,
|
|
16
|
+
MAX_SPECS_LINE_LENGTH,
|
|
17
|
+
MAX_SPECS_LINES_PER_SESSION,
|
|
18
|
+
SPECS_KEY,
|
|
19
|
+
SPECS_LINE_MIN_INTERVAL_MS,
|
|
20
|
+
SPECS_NEW_SESSION_DELAY_MS,
|
|
21
|
+
SPECS_RAW_TOKEN,
|
|
22
|
+
SPECS_ROOT,
|
|
23
|
+
} from "../config.js";
|
|
24
|
+
import { field } from "./mount.js";
|
|
25
|
+
|
|
26
|
+
// The module maker, re-exported so the path to the built dist is written once. webterm resolves the pane apps as a sibling
|
|
27
|
+
// directory, which is where the image bakes them and where a checkout stages them.
|
|
28
|
+
export { createSpecsModule };
|
|
29
|
+
|
|
30
|
+
// The media types the data routes hand back as text, which is what this client reads rather than renders. Everything else - a page,
|
|
31
|
+
// a diagram, a stylesheet, an image - is something a browser renders for itself, and is loaded from the workspace-file path, where a
|
|
32
|
+
// frame and an image reach it without sending a header. An allowlist rather than a family, because `text/html` is in the family and
|
|
33
|
+
// is exactly the wrong side of this line.
|
|
34
|
+
const TEXTUAL = new Set(["text/markdown", "text/plain", "application/json"]);
|
|
35
|
+
|
|
36
|
+
/** One workspace file as text. Anything a page renders rather than reads is refused to this route. */
|
|
37
|
+
function textOf(module, path) {
|
|
38
|
+
const read = module.file(path);
|
|
39
|
+
const type = read.mediaType.split(";")[0].trim();
|
|
40
|
+
if (!TEXTUAL.has(type)) throw new BadRequest(`${path} is not read as text - load it as a workspace file`);
|
|
41
|
+
return { path, mediaType: read.mediaType, size: read.size, modified: read.modified, text: read.bytes.toString("utf8") };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const SPECS_TABLE = {
|
|
45
|
+
id: "specs",
|
|
46
|
+
/** What a refusal calls the tool. The word on the tab is the client's own, in `tool-host.js`. */
|
|
47
|
+
label: "Specs",
|
|
48
|
+
key: SPECS_KEY,
|
|
49
|
+
rawToken: SPECS_RAW_TOKEN,
|
|
50
|
+
statusFor,
|
|
51
|
+
|
|
52
|
+
/** What the module is pointed at, and who to tell when the disk moves. */
|
|
53
|
+
create: ({ onChange }) => createSpecsModule({ root: SPECS_ROOT, onChange }),
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* What a delivery may say it is, in the server's own words.
|
|
57
|
+
*
|
|
58
|
+
* A sentence carries information to an agent already inside the flow, and every event here is one an agent only hears mid-flow;
|
|
59
|
+
* anything that starts a flow is a command in `commands.js` instead, so no framed sentence ever reaches an agent that is not
|
|
60
|
+
* allowed to act on it.
|
|
61
|
+
*/
|
|
62
|
+
events: {
|
|
63
|
+
annotations: "Annotations sent",
|
|
64
|
+
answers: "A round of answers was submitted",
|
|
65
|
+
amendment: "The steps for this work were changed",
|
|
66
|
+
gate: "A gate was answered",
|
|
67
|
+
question: "A question about something waiting - nothing was answered",
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* What a delivery may be, and how much of this channel one session gets.
|
|
72
|
+
*
|
|
73
|
+
* The length is of the source text, checked before the line is reduced, so an over-limit message is refused with a reason
|
|
74
|
+
* rather than cut into half a sentence. The gap is what a person delivering things looks like. The total is what the gap alone
|
|
75
|
+
* cannot stop. And the wait is for a session this delivery had to start, because an agent CLI takes a moment to draw its
|
|
76
|
+
* prompt.
|
|
77
|
+
*/
|
|
78
|
+
limits: {
|
|
79
|
+
bytes: MAX_SPECS_BYTES,
|
|
80
|
+
line: MAX_SPECS_LINE_LENGTH,
|
|
81
|
+
interval: SPECS_LINE_MIN_INTERVAL_MS,
|
|
82
|
+
perSession: MAX_SPECS_LINES_PER_SESSION,
|
|
83
|
+
newSession: SPECS_NEW_SESSION_DELAY_MS,
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
/** A workspace file, which this tool serves by its own path. */
|
|
87
|
+
raw: (module, path) => module.file(path),
|
|
88
|
+
|
|
89
|
+
// The data set, as one table. The gate, the preflight answer and the router all read it, so they cannot disagree about which
|
|
90
|
+
// routes exist or which methods each one takes.
|
|
91
|
+
routes: (module) => ({
|
|
92
|
+
dashboard: { GET: () => module.dashboard() },
|
|
93
|
+
entry: { GET: ({ query }) => module.entry(field(query.path)) },
|
|
94
|
+
state: {
|
|
95
|
+
GET: ({ query }) => module.state(field(query.entry)),
|
|
96
|
+
PUT: ({ query, body }) => {
|
|
97
|
+
module.writeState(field(query.entry), body);
|
|
98
|
+
return { ok: true };
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
// The annotations queued and not sent for one spec. The spec's own, not the window's, so every window and every session
|
|
102
|
+
// open on it draws the same list and a restart loses nothing.
|
|
103
|
+
pending: {
|
|
104
|
+
GET: ({ query }) => module.pending(field(query.entry)),
|
|
105
|
+
PUT: ({ query, body }) => {
|
|
106
|
+
module.writePending(field(query.entry), body);
|
|
107
|
+
return { ok: true };
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
feedback: {
|
|
111
|
+
GET: ({ query }) => ({ rounds: module.filed(field(query.entry)) }),
|
|
112
|
+
POST: ({ query, body }) => module.feedback(field(query.entry), body),
|
|
113
|
+
},
|
|
114
|
+
file: { GET: ({ query }) => textOf(module, field(query.path)) },
|
|
115
|
+
// The protocol, as the reader sees and answers it. What an agent posts is not here: it arrives at the helper's door below,
|
|
116
|
+
// which is gated by being inside a session rather than by holding this key.
|
|
117
|
+
stages: { GET: () => ({ stages: module.stages() }) },
|
|
118
|
+
protocol: { GET: ({ query }) => module.specState(field(query.entry)) },
|
|
119
|
+
// A declared round or gate as it was written. Read through the tool rather than as a workspace file: it lives in the
|
|
120
|
+
// dot-directory, which the file route refuses on purpose.
|
|
121
|
+
round: { GET: ({ query }) => module.round(field(query.entry), field(query.id)) },
|
|
122
|
+
intake: { POST: ({ body }) => module.intake(body) },
|
|
123
|
+
// The user signing a piece of work off. POST because it moves a directory, and the user's alone: being finished is a claim
|
|
124
|
+
// about the world, and no agent verb below files anything away.
|
|
125
|
+
archive: { POST: ({ query }) => module.archive(field(query.entry)) },
|
|
126
|
+
confirm: { POST: ({ query, body }) => module.confirm(field(query.entry), body) },
|
|
127
|
+
// The user shaping the run before it starts. A route rather than an agent verb, for the same reason confirming is one: the
|
|
128
|
+
// decisions bind the agent that reads them, so the agent is not their author.
|
|
129
|
+
decide: { POST: ({ query, body }) => module.decide(field(query.entry), body) },
|
|
130
|
+
// The user answering a gate, a round or a decision card. The agent's side of the same file is a verb at the helper's door,
|
|
131
|
+
// so neither caller can do the other's half.
|
|
132
|
+
answer: { POST: ({ query, body }) => module.answer(field(query.entry), field(query.id), body) },
|
|
133
|
+
}),
|
|
134
|
+
|
|
135
|
+
// The helper's verbs, which an agent reaches from inside a session. A verb that is not here is not one.
|
|
136
|
+
verbs: (module) => ({
|
|
137
|
+
// The first call any agent makes: it answers whether there is a pane at all, and where the specs this pane serves live. An
|
|
138
|
+
// agent writing into a spec needs a path, and the root is a mount parameter, so nothing it could work out for itself.
|
|
139
|
+
stages: () => ({ root: module.root, stages: module.stages() }),
|
|
140
|
+
state: (entry) => module.specState(entry),
|
|
141
|
+
intake: (_entry, payload) => module.intake(payload),
|
|
142
|
+
propose: (entry, payload) => module.propose(entry, payload),
|
|
143
|
+
// What the agent proposes for the run itself, which the execute form opens on. The user decides it through the route above,
|
|
144
|
+
// so this writes a recommendation and never a decision.
|
|
145
|
+
recommend: (entry, payload) => module.recommend(entry, payload),
|
|
146
|
+
await: (entry, payload) => module.declare(entry, payload),
|
|
147
|
+
// The other half of declaring: an ask that has been superseded leaves the list without an answer, so what the user is shown
|
|
148
|
+
// as open is only what is genuinely open.
|
|
149
|
+
withdraw: (entry, payload) => module.withdraw(entry, payload),
|
|
150
|
+
post: (entry, payload) => module.post(entry, payload),
|
|
151
|
+
review: (entry, payload) => module.review(entry, payload),
|
|
152
|
+
answers: (entry, payload) => ({ answer: module.answers(entry, field(payload?.id)) }),
|
|
153
|
+
}),
|
|
154
|
+
};
|
|
@@ -47,6 +47,43 @@ const BLANK = /[\p{Default_Ignorable_Code_Point}\u2800]/u;
|
|
|
47
47
|
const DOUBLE_QUOTES =
|
|
48
48
|
/["\u00ab\u00bb\u02ba\u02dd\u02ee\u02f6\u05f4\u201c\u201d\u201e\u201f\u2033\u2036\u275d\u275e\u2e42\u3003\u301d\u301e\u301f\uff02]|[\u{1f676}-\u{1f678}]/gu;
|
|
49
49
|
|
|
50
|
+
// All three CLIs read "@path" in a submitted line as a file to pull into the context, so no detail may carry
|
|
51
|
+
// one. The frame names a file in the one place it is allowed to - the path, which the server built and which
|
|
52
|
+
// is dropped unless the reduction leaves it whole - and a detail is not that place. So the at sign goes the
|
|
53
|
+
// way the quote lookalikes go, for every agent.
|
|
54
|
+
const FILE_MENTION = /@/g;
|
|
55
|
+
|
|
56
|
+
// The rules that are one CLI's own syntax, and nobody else's. The frame promises that a framed line instructs
|
|
57
|
+
// nobody, and the command line promises that its argument is a name and not a second instruction. Both
|
|
58
|
+
// promises rest on a character meaning something only where this server put it, and each CLI breaks that in
|
|
59
|
+
// its own way - so each cell below is one CLI's syntax, mapped to a code point that reads the same and means
|
|
60
|
+
// nothing to it. Every other agent is untouched.
|
|
61
|
+
//
|
|
62
|
+
// claude nothing of its own. The two rules above cover it.
|
|
63
|
+
// codex "$name" is a skill mention wherever it sits in the line.
|
|
64
|
+
// opencode a prompt expands a backticked shell command by running it. The expansion happens after the
|
|
65
|
+
// argument is placed in the command file, so a name typed by a button is scanned for one.
|
|
66
|
+
// Neutralizing the backtick is what ends it: the command form cannot be written without one.
|
|
67
|
+
// The dollar goes too: the argument is placed with String.replaceAll, where "$&" and the two
|
|
68
|
+
// quote forms beside it are replacement patterns that rebuild text out of the template.
|
|
69
|
+
//
|
|
70
|
+
// The replacements read the same and mean nothing to the CLI they are for. Two of the three are the
|
|
71
|
+
// compatibility forms, so a consumer that normalized the line to NFKC would fold them back - nothing in this
|
|
72
|
+
// chain normalizes, and this is the property to re-check if anything downstream ever does.
|
|
73
|
+
//
|
|
74
|
+
// Every agent has a row, empty where it needs nothing, and an agent with no row gets every rule in the table.
|
|
75
|
+
// A caller that omits the agent, or a fourth CLI added to the session side and not to this one, then loses a
|
|
76
|
+
// character rather than a guarantee.
|
|
77
|
+
const NO_RULES = [];
|
|
78
|
+
const CODEX_RULES = [[/\$/g, "\uff04"]]; // Fullwidth dollar sign
|
|
79
|
+
const OPENCODE_RULES = [[/`/g, "\u02cb"], ...CODEX_RULES]; // Modifier letter grave accent, then the dollar
|
|
80
|
+
const AGENT_RULES = {
|
|
81
|
+
claude: NO_RULES,
|
|
82
|
+
codex: CODEX_RULES,
|
|
83
|
+
opencode: OPENCODE_RULES,
|
|
84
|
+
};
|
|
85
|
+
const EVERY_RULE = [...OPENCODE_RULES];
|
|
86
|
+
|
|
50
87
|
/**
|
|
51
88
|
* One line of somebody else's text, safe to put inside the frame.
|
|
52
89
|
*
|
|
@@ -54,15 +91,24 @@ const DOUBLE_QUOTES =
|
|
|
54
91
|
* control, a separator or a format character, and a space is what it was standing in for. One that renders
|
|
55
92
|
* as nothing inside an allowed category is removed rather than spaced, because it was padding rather than a
|
|
56
93
|
* gap - which is what keeps a padded tail inside the quotes instead of pushed off the row.
|
|
94
|
+
*
|
|
95
|
+
* `agent` is the agent this text is being reduced for. It is what decides the per-agent rules above; the two
|
|
96
|
+
* rules before them hold for every agent, and an agent the table does not name gets all of them. The detail
|
|
97
|
+
* is not always the user's own words either: a directory name read off the workspace comes through here too.
|
|
57
98
|
*/
|
|
58
|
-
export function reduceLine(raw, max) {
|
|
99
|
+
export function reduceLine(raw, max, agent) {
|
|
59
100
|
if (typeof raw !== "string") return "";
|
|
60
101
|
let kept = "";
|
|
61
102
|
for (const ch of raw) {
|
|
62
103
|
if (BLANK.test(ch)) continue;
|
|
63
104
|
kept += VISIBLE.test(ch) ? ch : " ";
|
|
64
105
|
}
|
|
65
|
-
|
|
106
|
+
let text = kept.replace(DOUBLE_QUOTES, "'").replace(FILE_MENTION, "\uff20"); // Fullwidth commercial at
|
|
107
|
+
// Read as the table's own entries: the agent is validated at session creation, and this keeps a name
|
|
108
|
+
// off Object.prototype from reaching the loop as something that is not a list of rules.
|
|
109
|
+
const rules = Object.hasOwn(AGENT_RULES, agent) ? AGENT_RULES[agent] : EVERY_RULE;
|
|
110
|
+
for (const [pattern, readsAs] of rules) text = text.replace(pattern, readsAs);
|
|
111
|
+
return cleanLine(text, max);
|
|
66
112
|
}
|
|
67
113
|
|
|
68
114
|
/**
|
|
@@ -74,12 +120,29 @@ export function reduceLine(raw, max) {
|
|
|
74
120
|
*
|
|
75
121
|
* `path` is a path this server built, but it is not made of this server's words: every segment of it is a
|
|
76
122
|
* name off the filesystem, and a name may hold any byte but the separator. So it goes through the same
|
|
77
|
-
* reduction, and it is named only when the reduction changed nothing. A path that changed
|
|
78
|
-
* more - naming the reduced spelling would send the agent to a file that does not exist -
|
|
79
|
-
* anything the reduction removes is the one case where naming no file is the honest line.
|
|
123
|
+
* reduction, for the same agent, and it is named only when the reduction changed nothing. A path that changed
|
|
124
|
+
* is not a path any more - naming the reduced spelling would send the agent to a file that does not exist -
|
|
125
|
+
* and a path holding anything the reduction removes is the one case where naming no file is the honest line.
|
|
126
|
+
* A directory whose name holds one of the per-agent characters above is one of those, in that agent's session
|
|
127
|
+
* and in no other.
|
|
80
128
|
*/
|
|
81
|
-
export function framedLine({ tag, event, detail, path }) {
|
|
129
|
+
export function framedLine({ tag, event, detail, path, agent }) {
|
|
82
130
|
const said = detail ? `: "${detail}"` : "";
|
|
83
|
-
const named = path && reduceLine(path, path.length) === path ? path : "";
|
|
131
|
+
const named = path && reduceLine(path, path.length, agent) === path ? path : "";
|
|
84
132
|
return `[${tag}] ${event}${said}${named ? ` - read ${named}` : ""}`;
|
|
85
133
|
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The line for a button that starts a skill: the command itself, as the user would type it.
|
|
137
|
+
*
|
|
138
|
+
* A skill the user alone may start does not start from a framed sentence, because the sentence reaches an
|
|
139
|
+
* agent that is not allowed to act on it. The button click is the user's own act, so the line it types is
|
|
140
|
+
* the command, and the frame would only get in the way of it.
|
|
141
|
+
*
|
|
142
|
+
* `command` is this server's own word, like every event word above. `argument` is the one part that came
|
|
143
|
+
* from somewhere else, and it arrives reduced: every code point that renders as a double quote is a single
|
|
144
|
+
* one by then, so it stays one quoted word and can neither close the quote nor open a second line.
|
|
145
|
+
*/
|
|
146
|
+
export function commandLine(command, argument) {
|
|
147
|
+
return argument ? `${command} "${argument}"` : command;
|
|
148
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
- You can check your own context-window and rate-limit usage at any time: run `context-usage`.
|
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# =============================================================================
|
|
3
|
-
# context-usage -- prints the freshest Claude Code context/quota snapshot.
|
|
4
|
-
# Baked into the container image at /usr/local/share/chamba/context-usage.sh
|
|
5
|
-
# and symlinked to /usr/local/bin/context-usage.
|
|
6
|
-
#
|
|
7
|
-
# Snapshots are written by claude-statusline.sh to ~/.claude/context-usage/
|
|
8
|
-
# on every prompt render, one file per session - that script prints nothing, so
|
|
9
|
-
# the snapshot is the whole of what it does. Each snapshot records the pid
|
|
10
|
-
# (and start time) of the Claude CLI process that spawned the status line; this
|
|
11
|
-
# helper runs as a descendant of that SAME process, so matching them identifies
|
|
12
|
-
# the asking session's own snapshot deterministically. When pid resolution fails, the
|
|
13
|
-
# newest file by mtime is used instead (it is almost always the asking
|
|
14
|
-
# session's own: its status line re-rendered the moment the current prompt
|
|
15
|
-
# was submitted), and warnings cover the two ways that heuristic can mislead
|
|
16
|
-
# (stale snapshot, another session writing at the same time).
|
|
17
|
-
# =============================================================================
|
|
18
|
-
|
|
19
|
-
snap_dir="${HOME:-/home/devuser}/.claude/context-usage"
|
|
20
|
-
|
|
21
|
-
# All snapshots, newest first. Filenames are sanitized to [A-Za-z0-9-].json at
|
|
22
|
-
# write time, so ls output is line-safe and word-splitting the list below is
|
|
23
|
-
# safe too. An unmatched glob yields no stdout lines (error suppressed).
|
|
24
|
-
files=$(ls -t -- "$snap_dir"/*.json 2>/dev/null)
|
|
25
|
-
|
|
26
|
-
if [ -z "$files" ]; then
|
|
27
|
-
echo "No context snapshots found in $snap_dir." >&2
|
|
28
|
-
echo "Snapshots appear once Claude Code has rendered its first prompt in this session." >&2
|
|
29
|
-
exit 1
|
|
30
|
-
fi
|
|
31
|
-
|
|
32
|
-
# Resolve the pid of this session's Claude CLI process; mirrors find_claude_pid in
|
|
33
|
-
# claude-statusline.sh (see the comments there). Prints nothing when resolution fails.
|
|
34
|
-
find_claude_pid() {
|
|
35
|
-
fcp_pid=$$
|
|
36
|
-
while [ "$fcp_pid" -gt 1 ] 2>/dev/null; do
|
|
37
|
-
fcp_comm=""
|
|
38
|
-
read -r fcp_comm 2>/dev/null < "/proc/$fcp_pid/comm" || return
|
|
39
|
-
case "$fcp_comm" in
|
|
40
|
-
claude) printf '%s' "$fcp_pid"; return ;;
|
|
41
|
-
node*) grep -aq "claude-code" "/proc/$fcp_pid/cmdline" 2>/dev/null && { printf '%s' "$fcp_pid"; return; } ;;
|
|
42
|
-
esac
|
|
43
|
-
read -r fcp_stat 2>/dev/null < "/proc/$fcp_pid/stat" || return
|
|
44
|
-
fcp_rest="${fcp_stat##*) }"
|
|
45
|
-
set -- $fcp_rest
|
|
46
|
-
fcp_pid=$2
|
|
47
|
-
done
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
# Start time of a process (jiffies since host boot, /proc/<pid>/stat field 22); mirrors
|
|
51
|
-
# proc_start_time in claude-statusline.sh. Prints nothing when unavailable.
|
|
52
|
-
proc_start_time() {
|
|
53
|
-
read -r pst_stat 2>/dev/null < "/proc/$1/stat" || return
|
|
54
|
-
pst_rest="${pst_stat##*) }"
|
|
55
|
-
set -- $pst_rest
|
|
56
|
-
printf '%s' "${20}"
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
# CHAMBA_CLAUDE_PID and CHAMBA_CLAUDE_PID_START are test/debug overrides.
|
|
60
|
-
my_claude_pid="${CHAMBA_CLAUDE_PID:-$(find_claude_pid)}"
|
|
61
|
-
case "$my_claude_pid" in *[!0-9]*) my_claude_pid="" ;; esac
|
|
62
|
-
my_pid_start=""
|
|
63
|
-
[ -n "$my_claude_pid" ] && my_pid_start="${CHAMBA_CLAUDE_PID_START:-$(proc_start_time "$my_claude_pid")}"
|
|
64
|
-
case "$my_pid_start" in *[!0-9]*) my_pid_start="" ;; esac
|
|
65
|
-
|
|
66
|
-
# Deterministic selection first: the snapshot recorded by this session's own Claude
|
|
67
|
-
# process. The list is newest-first, so the first match also wins when one process
|
|
68
|
-
# wrote several session files over time (e.g. after /clear). Pids get recycled across
|
|
69
|
-
# container restarts while snapshots persist in the bind-mounted home, so when both
|
|
70
|
-
# sides carry a start time it must agree too; a missing start time on either side
|
|
71
|
-
# falls back to the pid alone. The while-read loop (heredoc, not a pipe, so $matched
|
|
72
|
-
# survives) keeps paths intact even if the directory prefix contains spaces.
|
|
73
|
-
newest=""
|
|
74
|
-
second=""
|
|
75
|
-
matched=""
|
|
76
|
-
if [ -n "$my_claude_pid" ]; then
|
|
77
|
-
while IFS= read -r f; do
|
|
78
|
-
pid_fields=$(jq -r '(.claude_pid // ""), (.claude_pid_start // "")' "$f" 2>/dev/null)
|
|
79
|
-
{
|
|
80
|
-
IFS= read -r fpid
|
|
81
|
-
IFS= read -r fstart
|
|
82
|
-
} <<INNER
|
|
83
|
-
$pid_fields
|
|
84
|
-
INNER
|
|
85
|
-
[ "$fpid" = "$my_claude_pid" ] || continue
|
|
86
|
-
if [ -n "$fstart" ] && [ -n "$my_pid_start" ] && [ "$fstart" != "$my_pid_start" ]; then
|
|
87
|
-
continue
|
|
88
|
-
fi
|
|
89
|
-
matched="$f"
|
|
90
|
-
break
|
|
91
|
-
done <<EOF
|
|
92
|
-
$files
|
|
93
|
-
EOF
|
|
94
|
-
fi
|
|
95
|
-
|
|
96
|
-
if [ -n "$matched" ]; then
|
|
97
|
-
newest="$matched"
|
|
98
|
-
else
|
|
99
|
-
# Heuristic fallback: newest by mtime, plus the runner-up for the ambiguity warning.
|
|
100
|
-
{
|
|
101
|
-
IFS= read -r newest
|
|
102
|
-
IFS= read -r second
|
|
103
|
-
} <<EOF
|
|
104
|
-
$files
|
|
105
|
-
EOF
|
|
106
|
-
fi
|
|
107
|
-
|
|
108
|
-
# Single jq pass over the snapshot, one field per line. Every path has a fallback so a
|
|
109
|
-
# missing field becomes an empty string (or 0) instead of failing the script.
|
|
110
|
-
parsed=$(jq -r '
|
|
111
|
-
.session_id // "",
|
|
112
|
-
.updated_at // "",
|
|
113
|
-
.context_tokens // 0,
|
|
114
|
-
.context_used_pct // 0,
|
|
115
|
-
.context_window_size // 0,
|
|
116
|
-
.model // "",
|
|
117
|
-
.effort // "",
|
|
118
|
-
.quota_left_pct // "",
|
|
119
|
-
.quota_resets_at // "",
|
|
120
|
-
.quota_week_left_pct // "",
|
|
121
|
-
.quota_week_resets_at // ""
|
|
122
|
-
' "$newest" 2>/dev/null)
|
|
123
|
-
|
|
124
|
-
if [ -z "$parsed" ]; then
|
|
125
|
-
echo "Snapshot $newest is unreadable." >&2
|
|
126
|
-
exit 1
|
|
127
|
-
fi
|
|
128
|
-
|
|
129
|
-
{
|
|
130
|
-
IFS= read -r session_id
|
|
131
|
-
IFS= read -r updated_at
|
|
132
|
-
IFS= read -r tokens
|
|
133
|
-
IFS= read -r used_pct
|
|
134
|
-
IFS= read -r window_size
|
|
135
|
-
IFS= read -r model
|
|
136
|
-
IFS= read -r effort
|
|
137
|
-
IFS= read -r quota_left
|
|
138
|
-
IFS= read -r quota_resets_at
|
|
139
|
-
IFS= read -r quota_week_left
|
|
140
|
-
IFS= read -r quota_week_resets_at
|
|
141
|
-
} <<EOF
|
|
142
|
-
$parsed
|
|
143
|
-
EOF
|
|
144
|
-
|
|
145
|
-
now=$(date +%s)
|
|
146
|
-
|
|
147
|
-
# Format a seconds delta as "Xd Yh", "Xh Ym", "Xm", or "Xs". The day unit is here for the seven-day
|
|
148
|
-
# rate-limit window, which is days away for most of its life; the hours are truncated rather than
|
|
149
|
-
# rounded so a delta of 4 days and 23h50m never reads "4d 24h".
|
|
150
|
-
fmt_delta() {
|
|
151
|
-
d=$1
|
|
152
|
-
if [ "$d" -ge 86400 ]; then
|
|
153
|
-
echo "$(( d / 86400 ))d $(( (d % 86400) / 3600 ))h"
|
|
154
|
-
elif [ "$d" -ge 3600 ]; then
|
|
155
|
-
echo "$(( d / 3600 ))h $(( (d % 3600) / 60 ))m"
|
|
156
|
-
elif [ "$d" -ge 60 ]; then
|
|
157
|
-
echo "$(( d / 60 ))m"
|
|
158
|
-
else
|
|
159
|
-
echo "${d}s"
|
|
160
|
-
fi
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
# Age of the snapshot from its own updated_at field (portable, and equal to file mtime).
|
|
164
|
-
# age stays empty when updated_at is missing or non-numeric.
|
|
165
|
-
age=""
|
|
166
|
-
age_label="unknown"
|
|
167
|
-
case "$updated_at" in
|
|
168
|
-
'' | *[!0-9]*) ;;
|
|
169
|
-
*)
|
|
170
|
-
age=$(( now - updated_at ))
|
|
171
|
-
[ "$age" -lt 0 ] && age=0
|
|
172
|
-
age_label="$(fmt_delta "$age") ago"
|
|
173
|
-
;;
|
|
174
|
-
esac
|
|
175
|
-
|
|
176
|
-
# Token counts read the way the strip in the web interface says them: 45.0k below
|
|
177
|
-
# 100k, 245k above, 1M above a million.
|
|
178
|
-
fmt_tokens() {
|
|
179
|
-
case "$1" in
|
|
180
|
-
'' | *[!0-9]*) printf '?' ;;
|
|
181
|
-
*) awk -v t="$1" 'BEGIN {
|
|
182
|
-
if (t == 0) printf "0k";
|
|
183
|
-
else if (t >= 1000000) { m = t/1000000; if (m == int(m)) printf "%dM", m; else printf "%.1fM", m; }
|
|
184
|
-
else if (t >= 100000) printf "%dk", int(t/1000 + 0.5);
|
|
185
|
-
else printf "%.1fk", t/1000;
|
|
186
|
-
}' ;;
|
|
187
|
-
esac
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
tokens_label=$(fmt_tokens "$tokens")
|
|
191
|
-
|
|
192
|
-
# The window the tokens sit in, when the snapshot knows how big it is. Unknown (an
|
|
193
|
-
# older snapshot, or a release that stopped reporting it) leaves the whole clause out,
|
|
194
|
-
# rather than naming a window with no size.
|
|
195
|
-
window_label=""
|
|
196
|
-
case "$window_size" in
|
|
197
|
-
'' | 0 | *[!0-9]*) ;;
|
|
198
|
-
*) window_label=" of $(fmt_tokens "$window_size") window" ;;
|
|
199
|
-
esac
|
|
200
|
-
|
|
201
|
-
# The "this session" marker means the snapshot was matched by pid and is guaranteed to
|
|
202
|
-
# describe the asking session; without it the newest-file heuristic picked the snapshot.
|
|
203
|
-
if [ -n "$matched" ]; then
|
|
204
|
-
printf 'session: %s (this session, updated %s)\n' "${session_id:-unknown}" "$age_label"
|
|
205
|
-
else
|
|
206
|
-
printf 'session: %s (updated %s)\n' "${session_id:-unknown}" "$age_label"
|
|
207
|
-
fi
|
|
208
|
-
printf 'context: %s tokens (%s%%%s)\n' "$tokens_label" "${used_pct:-?}" "$window_label"
|
|
209
|
-
|
|
210
|
-
# One rate-limit window as a line, printed only when the snapshot carried that window. Claude Code
|
|
211
|
-
# reports two - the five-hour one, which "quota" has always meant here, and the seven-day one - and an
|
|
212
|
-
# account may have both, one, or neither. The label is padded to the width the other lines use.
|
|
213
|
-
quota_line() {
|
|
214
|
-
ql_label=$1
|
|
215
|
-
ql_left=$2
|
|
216
|
-
ql_resets=$3
|
|
217
|
-
[ -n "$ql_left" ] || return 0
|
|
218
|
-
ql_reset_label=""
|
|
219
|
-
case "$ql_resets" in
|
|
220
|
-
'' | *[!0-9]*) ;;
|
|
221
|
-
*)
|
|
222
|
-
ql_delta=$(( ql_resets - now ))
|
|
223
|
-
if [ "$ql_delta" -gt 0 ]; then
|
|
224
|
-
ql_reset_label=", resets in $(fmt_delta "$ql_delta")"
|
|
225
|
-
else
|
|
226
|
-
ql_reset_label=", resets now"
|
|
227
|
-
fi
|
|
228
|
-
;;
|
|
229
|
-
esac
|
|
230
|
-
printf '%-8s %s%% remaining%s\n' "$ql_label" "$ql_left" "$ql_reset_label"
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
quota_line 'quota:' "$quota_left" "$quota_resets_at"
|
|
234
|
-
quota_line 'weekly:' "$quota_week_left" "$quota_week_resets_at"
|
|
235
|
-
|
|
236
|
-
if [ -n "$model" ]; then
|
|
237
|
-
effort_label=""
|
|
238
|
-
[ -n "$effort" ] && effort_label=" (effort $effort)"
|
|
239
|
-
printf 'model: %s%s\n' "$model" "$effort_label"
|
|
240
|
-
fi
|
|
241
|
-
|
|
242
|
-
# Warnings apply only to the heuristic path: a pid-matched snapshot is this session's own
|
|
243
|
-
# by construction, however old it is (it reflects the last render, i.e. the turn start).
|
|
244
|
-
|
|
245
|
-
# Freshness warning: a snapshot older than 5 minutes likely belongs to an idle session.
|
|
246
|
-
if [ -z "$matched" ] && [ -n "$age" ] && [ "$age" -gt 300 ]; then
|
|
247
|
-
printf 'warning: snapshot is %s old - it may not reflect the current session\n' "$(fmt_delta "$age")"
|
|
248
|
-
fi
|
|
249
|
-
|
|
250
|
-
# Ambiguity warning: another session wrote its snapshot around the same moment, so mtime
|
|
251
|
-
# ordering may not identify the asking session. Compare updated_at of the two newest files.
|
|
252
|
-
# Guarded on age being set, which implies updated_at is numeric. The explicit matched check
|
|
253
|
-
# keeps this off the pid path even if a future edit populates $second unconditionally.
|
|
254
|
-
if [ -z "$matched" ] && [ -n "$second" ] && [ -n "$age" ]; then
|
|
255
|
-
second_updated=$(jq -r '.updated_at // ""' "$second" 2>/dev/null)
|
|
256
|
-
case "$second_updated" in
|
|
257
|
-
'' | *[!0-9]*) ;;
|
|
258
|
-
*)
|
|
259
|
-
gap=$(( updated_at - second_updated ))
|
|
260
|
-
[ "$gap" -lt 0 ] && gap=$(( -gap ))
|
|
261
|
-
if [ "$gap" -lt 60 ]; then
|
|
262
|
-
printf 'warning: another session updated its snapshot within the last minute - this may not be your session (%s)\n' "$second"
|
|
263
|
-
fi
|
|
264
|
-
;;
|
|
265
|
-
esac
|
|
266
|
-
fi
|
|
@@ -1 +0,0 @@
|
|
|
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}
|