chamba 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/LICENSE +53 -14
  2. package/README.md +28 -8
  3. package/dist/commands/advanced.js +7 -278
  4. package/dist/commands/dev.js +11 -612
  5. package/dist/commands/doctor.js +1 -29
  6. package/dist/commands/menu.js +1 -80
  7. package/dist/commands/onboard.js +6 -230
  8. package/dist/commands/settings.js +15 -349
  9. package/dist/lib/agent-commands.js +2 -0
  10. package/dist/lib/agent-context.js +6 -210
  11. package/dist/lib/browser.js +1 -40
  12. package/dist/lib/chamba-yaml.js +10 -191
  13. package/dist/lib/constants.js +1 -125
  14. package/dist/lib/dockerfile-builder.js +41 -261
  15. package/dist/lib/env.js +2 -78
  16. package/dist/lib/git-env.js +1 -21
  17. package/dist/lib/global-config.js +4 -66
  18. package/dist/lib/pnpm-store.js +1 -19
  19. package/dist/lib/ports.js +1 -210
  20. package/dist/lib/safe-rm.js +1 -36
  21. package/dist/lib/sessions.js +2 -34
  22. package/dist/lib/shadows.js +1 -176
  23. package/dist/lib/skills.js +2 -0
  24. package/dist/lib/templates.js +1 -0
  25. package/dist/lib/webterm.js +1 -305
  26. package/dist/lib/workspace-identity.js +4 -260
  27. package/package.json +4 -4
  28. package/templates/Dockerfile +12 -19
  29. package/templates/claude-statusline.sh +5 -7
  30. package/templates/context/baseline.md +2 -0
  31. package/templates/pane-apps/client/assets/c-BNa-DeUl.json +3552 -0
  32. package/templates/pane-apps/client/assets/cpp-B-T4NRoH.json +20239 -0
  33. package/templates/pane-apps/client/assets/csharp-BbxHChhA.json +6010 -0
  34. package/templates/pane-apps/client/assets/css-CWCxuwmz.json +1860 -0
  35. package/templates/pane-apps/client/assets/diff-B-2N89g8.json +154 -0
  36. package/templates/pane-apps/client/assets/docker-DWTvYW26.json +103 -0
  37. package/templates/pane-apps/client/assets/files-D0nJgFAA.js +12 -0
  38. package/templates/pane-apps/client/assets/files-h0K1vZyQ.css +1 -0
  39. package/templates/pane-apps/client/assets/go-B2hyYeNg.json +3156 -0
  40. package/templates/pane-apps/client/assets/graphql-Z6Id2aNO.json +1292 -0
  41. package/templates/pane-apps/client/assets/html-BtzvlIfu.json +2627 -0
  42. package/templates/pane-apps/client/assets/ini-Bn3Twap1.json +108 -0
  43. package/templates/pane-apps/client/assets/java-C7t2dwLO.json +1885 -0
  44. package/templates/pane-apps/client/assets/javascript-DuC2v0C0.json +5996 -0
  45. package/templates/pane-apps/client/assets/json-CwdkWeV5.json +206 -0
  46. package/templates/pane-apps/client/assets/jsonc-MYWDbTU3.json +206 -0
  47. package/templates/pane-apps/client/assets/jsx-Cn8EZuIC.json +5996 -0
  48. package/templates/pane-apps/client/assets/kotlin-CFNOz1gw.json +580 -0
  49. package/templates/pane-apps/client/assets/make-BVnlFCcl.json +629 -0
  50. package/templates/pane-apps/client/assets/markdown-B4Xk2WM_.json +3263 -0
  51. package/templates/pane-apps/client/assets/php-CAQzQmmI.json +4188 -0
  52. package/templates/pane-apps/client/assets/python-CYQee8m4.json +4174 -0
  53. package/templates/pane-apps/client/assets/reviews-BsoGZ35r.css +1 -0
  54. package/templates/pane-apps/client/assets/reviews-iW0Mbn4e.js +153 -0
  55. package/templates/pane-apps/client/assets/ruby-D7R58tus.json +3114 -0
  56. package/templates/pane-apps/client/assets/rust-BFBbFiei.json +1103 -0
  57. package/templates/pane-apps/client/assets/scss-TQRhBkPG.json +1874 -0
  58. package/templates/pane-apps/client/assets/shellscript-DieAkC4W.json +2375 -0
  59. package/templates/pane-apps/client/assets/specs-D9z3neR-.js +13 -0
  60. package/templates/pane-apps/client/assets/sql-Bxd030LV.json +634 -0
  61. package/templates/pane-apps/client/assets/toml-ChrKGzsV.json +430 -0
  62. package/templates/pane-apps/client/assets/tsx-D2e90MFw.json +5996 -0
  63. package/templates/pane-apps/client/assets/typescript-DonXQ_n1.json +5747 -0
  64. package/templates/pane-apps/client/assets/xml-B17r2RXF.json +382 -0
  65. package/templates/pane-apps/client/assets/yaml-CKn4h1zy.json +627 -0
  66. package/templates/pane-apps/client/reviews/index.html +15 -0
  67. package/templates/pane-apps/client/specs/index.html +3 -2
  68. package/templates/pane-apps/server/reviews.mjs +20 -0
  69. package/templates/pane-apps/server/specs.mjs +5 -1588
  70. package/templates/skills/chamba-statusline/SKILL.md +7 -19
  71. package/templates/skills/dx-review/SKILL.md +123 -0
  72. package/templates/skills/dx-review/references/acts.md +162 -0
  73. package/templates/skills/dx-spec/SKILL.md +223 -27
  74. package/templates/skills/dx-spec/references/principles-template.md +2 -0
  75. package/templates/skills/dx-spec/references/review-guide.md +4 -50
  76. package/templates/skills/dx-spec/references/spec-guide.md +24 -4
  77. package/templates/skills/dx-spec/references/stages.md +143 -58
  78. package/templates/skills/dx-spec-execute/SKILL.md +148 -35
  79. package/templates/startup.mjs +10 -4
  80. package/templates/tool-helper.sh +166 -0
  81. package/templates/webterm/README.md +60 -26
  82. package/templates/webterm/color.js +61 -0
  83. package/templates/webterm/config.js +35 -4
  84. package/templates/webterm/context/claude.md +0 -1
  85. package/templates/webterm/public/app/alerts.js +4 -4
  86. package/templates/webterm/public/app/anchored.js +81 -0
  87. package/templates/webterm/public/app/connection.js +9 -0
  88. package/templates/webterm/public/app/dom.js +2 -0
  89. package/templates/webterm/public/app/frames.js +51 -17
  90. package/templates/webterm/public/app/main.js +22 -8
  91. package/templates/webterm/public/app/new-session.js +13 -41
  92. package/templates/webterm/public/app/palette.js +58 -0
  93. package/templates/webterm/public/app/pane-shape.js +167 -0
  94. package/templates/webterm/public/app/pane-shell.js +199 -134
  95. package/templates/webterm/public/app/pane.js +33 -24
  96. package/templates/webterm/public/app/reviews-host.js +15 -0
  97. package/templates/webterm/public/app/specs-host.js +12 -218
  98. package/templates/webterm/public/app/state.js +5 -0
  99. package/templates/webterm/public/app/tabs.js +6 -2
  100. package/templates/webterm/public/app/theme.js +75 -27
  101. package/templates/webterm/public/app/tool-host.js +302 -0
  102. package/templates/webterm/public/app/workspace-color.js +192 -0
  103. package/templates/webterm/public/index.html +17 -3
  104. package/templates/webterm/public/styles.css +143 -21
  105. package/templates/webterm/server.js +198 -150
  106. package/templates/webterm/tools/commands.js +61 -0
  107. package/templates/webterm/tools/index.js +25 -0
  108. package/templates/webterm/tools/mount.js +337 -0
  109. package/templates/webterm/tools/paths.js +34 -0
  110. package/templates/webterm/tools/reviews.js +133 -0
  111. package/templates/webterm/tools/specs.js +154 -0
  112. package/templates/webterm/typed-line.js +70 -7
  113. package/templates/context/context-usage.md +0 -1
  114. package/templates/context-usage.sh +0 -266
  115. package/templates/pane-apps/client/assets/specs-B1970L17.css +0 -1
  116. package/templates/pane-apps/client/assets/specs-cEee_SPn.js +0 -23
  117. package/templates/skills/dx-spec-config/SKILL.md +0 -313
  118. package/templates/skills/dx-spec-config/references/principles-template.md +0 -12
  119. package/templates/specs.sh +0 -106
  120. package/templates/webterm/specs.js +0 -358
@@ -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
- return cleanLine(kept.replace(DOUBLE_QUOTES, "'"), max);
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 is not a path any
78
- * more - naming the reduced spelling would send the agent to a file that does not exist - and a path holding
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}