chamba 0.5.0 → 0.6.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/dist/commands/onboard.js +2 -1
- package/dist/lib/agent-context.js +10 -3
- package/dist/lib/git-env.js +21 -0
- package/dist/lib/shadows.js +2 -0
- package/package.json +1 -1
- package/templates/claude-statusline.sh +7 -4
- package/templates/context/web-pane-craft.md +57 -0
- package/templates/context-usage.sh +30 -13
- package/templates/skills/chamba-statusline/SKILL.md +15 -5
- package/templates/skills/web-pane/SKILL.md +1 -55
- package/templates/webterm/README.md +6 -3
- package/templates/webterm/artifacts.js +14 -1
- package/templates/webterm/conversation.js +57 -13
- package/templates/webterm/pane.js +9 -1
- package/templates/webterm/public/app/composer.js +25 -4
- package/templates/webterm/public/app/dom.js +4 -0
- package/templates/webterm/public/app/frames.js +3 -1
- package/templates/webterm/public/app/main.js +5 -1
- package/templates/webterm/public/app/pane-arrival.js +36 -0
- package/templates/webterm/public/app/pane.js +98 -13
- package/templates/webterm/public/app/status-strip.js +26 -34
- package/templates/webterm/public/app/strip-format.js +36 -0
- package/templates/webterm/public/app/tabs.js +66 -4
- package/templates/webterm/public/index.html +23 -3
- package/templates/webterm/public/styles.css +159 -14
- package/templates/webterm/resume.js +14 -5
- package/templates/webterm/server.js +12 -2
- package/templates/webterm/snapshot.js +4 -2
- package/templates/skills/context-usage/SKILL.md +0 -53
|
@@ -252,6 +252,38 @@ body {
|
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
/* ---- A page is waiting in this session's pane ---- */
|
|
256
|
+
/*
|
|
257
|
+
Published while this window was looking at another session. Quieter than the alert above, and about the
|
|
258
|
+
pane rather than the terminal, so it is not the .unread state either: the tab takes the session's colour on
|
|
259
|
+
its border and grows a small page beside its age, and holds that until the session is visited and the page
|
|
260
|
+
opens itself. One flash on arrival, by the same negative-delay trick, since the bar is rebuilt from scratch
|
|
261
|
+
on every frame. PAGE_ARRIVAL_MS in app/tabs.js must match the duration.
|
|
262
|
+
*/
|
|
263
|
+
.tab.pages {
|
|
264
|
+
color: var(--text);
|
|
265
|
+
border-color: color-mix(in srgb, var(--sc) 55%, var(--border));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.tab .pagemark {
|
|
269
|
+
display: flex;
|
|
270
|
+
align-items: center;
|
|
271
|
+
color: var(--sc);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.tab.pages.page-arriving {
|
|
275
|
+
animation: pageFlash 1.4s ease-out 1;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
@keyframes pageFlash {
|
|
279
|
+
from {
|
|
280
|
+
background: color-mix(in srgb, var(--sc) 34%, #10151c);
|
|
281
|
+
}
|
|
282
|
+
to {
|
|
283
|
+
background: #10151c;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
255
287
|
/* Someone who has asked for less movement still gets both states, told without it: the working light holds
|
|
256
288
|
still on the bottom edge, and the alert simply appears already lit. */
|
|
257
289
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -270,6 +302,12 @@ body {
|
|
|
270
302
|
.tab.attention.arriving {
|
|
271
303
|
animation: none;
|
|
272
304
|
}
|
|
305
|
+
|
|
306
|
+
/* The border and the page beside the age say it already; the flash was only ever the part that carried
|
|
307
|
+
across the room. */
|
|
308
|
+
.tab.pages.page-arriving {
|
|
309
|
+
animation: none;
|
|
310
|
+
}
|
|
273
311
|
}
|
|
274
312
|
|
|
275
313
|
.tab .age {
|
|
@@ -599,6 +637,33 @@ body.dragging {
|
|
|
599
637
|
background: var(--bg);
|
|
600
638
|
}
|
|
601
639
|
|
|
640
|
+
/* A page arrived and opened itself: one pulse of the workspace colour along the pane's own edge, and nothing
|
|
641
|
+
moves. Inset, so it is drawn over the pane rather than pushing the layout about, and over the frame's edge
|
|
642
|
+
rather than under it. ARRIVAL_MS in app/pane.js must match the duration - it is what takes the class off
|
|
643
|
+
again, since this element, unlike a tab, is not rebuilt from one frame to the next. */
|
|
644
|
+
#pane.arriving {
|
|
645
|
+
animation: paneArrival 1s ease-out 1;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
@keyframes paneArrival {
|
|
649
|
+
0%,
|
|
650
|
+
60% {
|
|
651
|
+
box-shadow: inset 0 0 0 2px var(--ws);
|
|
652
|
+
}
|
|
653
|
+
100% {
|
|
654
|
+
box-shadow: inset 0 0 0 2px transparent;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/* The same moment, held instead of pulsed. Without this the highlight would never appear at all for someone
|
|
659
|
+
who asked for less movement - the class is on the pane for exactly as long either way. */
|
|
660
|
+
@media (prefers-reduced-motion: reduce) {
|
|
661
|
+
#pane.arriving {
|
|
662
|
+
box-shadow: inset 0 0 0 2px var(--ws);
|
|
663
|
+
animation: none;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
602
667
|
/* The pagination bar: this session's pages as chips, the arrows for a bar too narrow to hold them, and where
|
|
603
668
|
in the history the open page sits. Drawn by the interface, outside the page's frame, so it always says the
|
|
604
669
|
truth about which page is open. */
|
|
@@ -696,6 +761,55 @@ body.dragging {
|
|
|
696
761
|
font-size: 11.5px;
|
|
697
762
|
}
|
|
698
763
|
|
|
764
|
+
/* How big a page is drawn in this browser. One bordered group - minus, readout, plus - so the pair reads as a
|
|
765
|
+
single control and not as two loose buttons that happen to sit together. */
|
|
766
|
+
#page-size {
|
|
767
|
+
display: flex;
|
|
768
|
+
flex: none;
|
|
769
|
+
align-items: center;
|
|
770
|
+
border: 1px solid var(--border);
|
|
771
|
+
border-radius: 6px;
|
|
772
|
+
background: #1c222b;
|
|
773
|
+
overflow: hidden;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
#page-size button {
|
|
777
|
+
display: flex;
|
|
778
|
+
width: 24px;
|
|
779
|
+
height: 24px;
|
|
780
|
+
align-items: center;
|
|
781
|
+
justify-content: center;
|
|
782
|
+
border: 0;
|
|
783
|
+
background: transparent;
|
|
784
|
+
color: var(--muted);
|
|
785
|
+
font: inherit;
|
|
786
|
+
font-size: 14px;
|
|
787
|
+
line-height: 1;
|
|
788
|
+
cursor: pointer;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
#page-size button:hover:not(:disabled) {
|
|
792
|
+
color: var(--text);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/* A spent end of the range: the button keeps its place, and says plainly that it has nowhere left to go. */
|
|
796
|
+
#page-size button:disabled {
|
|
797
|
+
opacity: 0.3;
|
|
798
|
+
cursor: default;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
#page-size-val {
|
|
802
|
+
display: flex;
|
|
803
|
+
height: 24px;
|
|
804
|
+
align-items: center;
|
|
805
|
+
padding: 0 6px;
|
|
806
|
+
border-right: 1px solid var(--border);
|
|
807
|
+
border-left: 1px solid var(--border);
|
|
808
|
+
color: var(--muted);
|
|
809
|
+
font-size: 10.5px;
|
|
810
|
+
letter-spacing: 0.02em;
|
|
811
|
+
}
|
|
812
|
+
|
|
699
813
|
/* Out of the way, at the end of the bar: the pane is put away far more often than the chips are paged. */
|
|
700
814
|
#pane-collapse {
|
|
701
815
|
display: flex;
|
|
@@ -712,13 +826,19 @@ body.dragging {
|
|
|
712
826
|
}
|
|
713
827
|
|
|
714
828
|
/* The page itself: an agent's document, in a frame with an opaque origin (see app/pane-frame.js). Nothing
|
|
715
|
-
here styles its contents - the page brings its own.
|
|
829
|
+
here styles its contents - the page brings its own.
|
|
830
|
+
|
|
831
|
+
The one exception is how big it is drawn, and it is applied from out here on purpose: the frame's document
|
|
832
|
+
has an opaque origin and the shell never reaches into it. `zoom` rather than a transform, because zoom lays
|
|
833
|
+
the document out again at the new size, so a page reflows instead of growing a horizontal scrollbar. The
|
|
834
|
+
property itself sits on #pane, which survives the frame being torn down and rebuilt on every chip click. */
|
|
716
835
|
.page-frame {
|
|
717
836
|
display: block;
|
|
718
837
|
width: 100%;
|
|
719
838
|
height: 100%;
|
|
720
839
|
border: none;
|
|
721
840
|
background: var(--bg);
|
|
841
|
+
zoom: var(--page-zoom, 1);
|
|
722
842
|
}
|
|
723
843
|
|
|
724
844
|
/* A page is a document with an event loop of its own, and a pointer that crosses into it during a drag is a
|
|
@@ -1045,6 +1165,19 @@ body.offline #tabbar {
|
|
|
1045
1165
|
font-weight: 600;
|
|
1046
1166
|
}
|
|
1047
1167
|
|
|
1168
|
+
/* Which window a meter is for - "5h", "7d". Two quota meters side by side are the same shape and the same
|
|
1169
|
+
colours, so the tag is what tells them apart before the percentage is read. */
|
|
1170
|
+
.seg .tag {
|
|
1171
|
+
color: var(--muted);
|
|
1172
|
+
font-size: 10.5px;
|
|
1173
|
+
font-weight: 600;
|
|
1174
|
+
letter-spacing: 0.04em;
|
|
1175
|
+
padding: 1px 4px;
|
|
1176
|
+
border: 1px solid var(--border);
|
|
1177
|
+
border-radius: 4px;
|
|
1178
|
+
background: #12161d;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1048
1181
|
.sep {
|
|
1049
1182
|
color: #3a4250;
|
|
1050
1183
|
}
|
|
@@ -1057,6 +1190,12 @@ body.offline #tabbar {
|
|
|
1057
1190
|
background: #262c36;
|
|
1058
1191
|
}
|
|
1059
1192
|
|
|
1193
|
+
/* The quota meters, of which there can be two. They give up the width so the strip stays one line; the context
|
|
1194
|
+
meter keeps the full 74px, because it is the one read constantly. */
|
|
1195
|
+
.bar.narrow {
|
|
1196
|
+
width: 52px;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1060
1199
|
.bar .fill {
|
|
1061
1200
|
display: block;
|
|
1062
1201
|
height: 100%;
|
|
@@ -1088,18 +1227,23 @@ body.offline #tabbar {
|
|
|
1088
1227
|
#input {
|
|
1089
1228
|
display: block;
|
|
1090
1229
|
width: 100%;
|
|
1091
|
-
/* Tall enough by default to
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1230
|
+
/* Tall enough by default to clear the 2x2 button block (2 x 28px + 6px gap, plus 8px top and bottom
|
|
1231
|
+
insets = 78px), which is about three lines of typing. It grows with what is typed up to seven lines
|
|
1232
|
+
(7 x 21px = 147px, plus the 9px insets and the border = 167px, so 168px holds them with a pixel over)
|
|
1233
|
+
and scrolls past that.
|
|
1234
|
+
These two numbers live here and nowhere else: autoGrow() in composer.js reads them off the box. */
|
|
1235
|
+
min-height: 78px;
|
|
1236
|
+
max-height: 168px;
|
|
1095
1237
|
resize: none;
|
|
1096
1238
|
background: #0d1117;
|
|
1097
1239
|
color: var(--text);
|
|
1098
1240
|
border: 1px solid var(--border);
|
|
1099
1241
|
border-radius: 10px;
|
|
1100
|
-
/* Right padding reserves the button gutter so typed text wraps to the next line instead of
|
|
1101
|
-
|
|
1102
|
-
|
|
1242
|
+
/* Right padding reserves the button gutter so typed text wraps to the next line instead of sliding
|
|
1243
|
+
behind the buttons (the block is two 28px columns and a 6px gap at right: 8px, so 78px clears it).
|
|
1244
|
+
A textarea's padding is uniform, so this width comes off every line, not only the ones beside the
|
|
1245
|
+
buttons - the cost of the second column, accepted at the mocks gate. */
|
|
1246
|
+
padding: 9px 78px 9px 12px;
|
|
1103
1247
|
font:
|
|
1104
1248
|
14px / 1.5 ui-monospace,
|
|
1105
1249
|
"SF Mono",
|
|
@@ -1133,21 +1277,22 @@ body.offline #tabbar {
|
|
|
1133
1277
|
opacity: 0.5;
|
|
1134
1278
|
}
|
|
1135
1279
|
|
|
1136
|
-
/* Action cluster floats inside the textbox as a
|
|
1137
|
-
|
|
1280
|
+
/* Action cluster floats inside the textbox as a 2x2 block pinned to the bottom-right corner. The textarea's
|
|
1281
|
+
right padding keeps typed text out from behind it. The block fills row by row, so the order in index.html
|
|
1282
|
+
is the layout: attach and dictate down the left, the pane button above Send on the right - the two buttons
|
|
1283
|
+
that send together, on the side the pane itself is on, with Send in the corner the hand already knows. */
|
|
1138
1284
|
#composer-actions {
|
|
1139
1285
|
position: absolute;
|
|
1140
1286
|
right: 8px;
|
|
1141
1287
|
bottom: 8px;
|
|
1142
|
-
display:
|
|
1143
|
-
|
|
1144
|
-
align-items: center;
|
|
1288
|
+
display: grid;
|
|
1289
|
+
grid-template-columns: 28px 28px;
|
|
1145
1290
|
gap: 6px;
|
|
1146
1291
|
}
|
|
1147
1292
|
|
|
1148
1293
|
/* Semi-transparent so they stay unobtrusive when floating over typed text; solidify on hover. The workspace's
|
|
1149
1294
|
hue is in them from the start - a faint wash and a tinted edge - so the cluster belongs to the window it
|
|
1150
|
-
sits in instead of being
|
|
1295
|
+
sits in instead of being four grey squares. */
|
|
1151
1296
|
.icon-btn {
|
|
1152
1297
|
display: inline-flex;
|
|
1153
1298
|
align-items: center;
|
|
@@ -17,7 +17,15 @@
|
|
|
17
17
|
|
|
18
18
|
import { readFileSync } from "node:fs";
|
|
19
19
|
import { basename, dirname } from "node:path";
|
|
20
|
-
import { CLAUDE_TRANSCRIPT,
|
|
20
|
+
import { CLAUDE_TRANSCRIPT, CODEX_ANY_ROLLOUT, filesNewestFirst, hasAnyFile } from "./conversation.js";
|
|
21
|
+
|
|
22
|
+
// How far below a store's root a conversation can sit, per store, because the shapes are not alike and a walk
|
|
23
|
+
// deep enough for the deepest is wasted on the others. codex files rollouts under a date tree
|
|
24
|
+
// (sessions/2026/08/24/); opencode nests everything under storage/session/{info,message,part}/, where part/
|
|
25
|
+
// holds one file per message part and is the reason a bound matters at all; claude's transcripts sit exactly
|
|
26
|
+
// one level down, in a dir named after the directory the session ran in, and anything deeper there is a
|
|
27
|
+
// session's own scratch rather than this workspace's history.
|
|
28
|
+
const STORE_DEPTH = { claudeProjects: 1, codexSessions: 3, opencodeSessions: 2 };
|
|
21
29
|
|
|
22
30
|
// --- Claude ------------------------------------------------------------------------------------------------------------------------------
|
|
23
31
|
|
|
@@ -42,7 +50,7 @@ export function claudeProjectKey(containerPath) {
|
|
|
42
50
|
* the fallback, and for those the dir name is the only evidence there is.
|
|
43
51
|
*/
|
|
44
52
|
function claudeResumeId(cwd, stores) {
|
|
45
|
-
const transcripts = filesNewestFirst(stores.claudeProjects, CLAUDE_TRANSCRIPT);
|
|
53
|
+
const transcripts = filesNewestFirst(stores.claudeProjects, CLAUDE_TRANSCRIPT, STORE_DEPTH.claudeProjects);
|
|
46
54
|
const byRecordedDir = firstResumable(transcripts, cwd);
|
|
47
55
|
if (byRecordedDir !== null) return byRecordedDir;
|
|
48
56
|
const key = claudeProjectKey(cwd);
|
|
@@ -113,10 +121,11 @@ function transcriptFacts(transcriptPath) {
|
|
|
113
121
|
//
|
|
114
122
|
// These two are asked a shallower question than claude - does the store hold a conversation file at all -
|
|
115
123
|
// and their own flag picks which one. Reading each store's format to find out whether the newest
|
|
116
|
-
// conversation is really resumable would tie chamba to two more internal layouts for no gain.
|
|
124
|
+
// conversation is really resumable would tie chamba to two more internal layouts for no gain. codex is asked
|
|
125
|
+
// with the pattern that also matches a compressed rollout, because a cold history is still a history.
|
|
117
126
|
export const CONTINUE = {
|
|
118
127
|
opencode: { argv: ["opencode", "--continue"], store: "opencodeSessions", pattern: /\.json$/ },
|
|
119
|
-
codex: { argv: ["codex", "resume", "--last"], store: "codexSessions", pattern:
|
|
128
|
+
codex: { argv: ["codex", "resume", "--last"], store: "codexSessions", pattern: CODEX_ANY_ROLLOUT },
|
|
120
129
|
};
|
|
121
130
|
|
|
122
131
|
// --- The one entry point -----------------------------------------------------------------------------------------------------------------
|
|
@@ -136,7 +145,7 @@ export function resumeArgvFor(agent, cwd, stores) {
|
|
|
136
145
|
}
|
|
137
146
|
const known = CONTINUE[agent];
|
|
138
147
|
if (!known) return null;
|
|
139
|
-
return
|
|
148
|
+
return hasAnyFile(stores[known.store], known.pattern, STORE_DEPTH[known.store]) ? [...known.argv] : null;
|
|
140
149
|
} catch {
|
|
141
150
|
// A store that changed shape under us must never take the session down with it - starting fresh is
|
|
142
151
|
// a worse session than the user asked for, but it is a session.
|
|
@@ -418,7 +418,10 @@ function send(client, frame) {
|
|
|
418
418
|
// The session bar, personalised per window: the list is the same for everyone, "which one am I
|
|
419
419
|
// driving" is not.
|
|
420
420
|
function broadcastSessions() {
|
|
421
|
-
|
|
421
|
+
// Each entry carries one thing the registry does not know: how many pages of that session's pane nobody
|
|
422
|
+
// has opened. The bar is the only frame that reaches a window about a session it is not driving, so this
|
|
423
|
+
// is the whole route by which a page published in a background session marks its tab.
|
|
424
|
+
const list = registry.list().map((entry) => ({ ...entry, unreadPages: pane.unreadCount(entry.id) }));
|
|
422
425
|
for (const client of clients) {
|
|
423
426
|
send(client, {
|
|
424
427
|
t: "sessions",
|
|
@@ -495,9 +498,16 @@ const pane = createPane({
|
|
|
495
498
|
store: paneStore,
|
|
496
499
|
stores: AGENT_STORES,
|
|
497
500
|
sessions: paneSessions,
|
|
498
|
-
onChange: (sid) =>
|
|
501
|
+
onChange: (sid) => paneChanged(sid),
|
|
499
502
|
});
|
|
500
503
|
|
|
504
|
+
// A pane moved. The window driving that session gets the list itself; every window gets the session bar again,
|
|
505
|
+
// because the count of unopened pages rides on it and that is how a tab hears about a page at all.
|
|
506
|
+
function paneChanged(sid) {
|
|
507
|
+
sendPages(sid);
|
|
508
|
+
broadcastSessions();
|
|
509
|
+
}
|
|
510
|
+
|
|
501
511
|
// The pane as one frame: the list, and the notice when this session is not showing its conversation's own
|
|
502
512
|
// pages (a second window resumed a conversation another live session already holds).
|
|
503
513
|
function pagesFrame(sid) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// snapshot.js - What claude wrote down about a session, and which session it belongs to.
|
|
2
2
|
//
|
|
3
3
|
// The status line script runs on every prompt render and writes one small file per conversation: the model,
|
|
4
|
-
// how much context it is holding, what is left of
|
|
5
|
-
// wrote it. That file is the only place any of this exists - nothing here asks claude anything, and there is
|
|
4
|
+
// how much context it is holding, what is left of each rate-limit window, and the pid of the claude process
|
|
5
|
+
// that wrote it. That file is the only place any of this exists - nothing here asks claude anything, and there is
|
|
6
6
|
// nothing to ask - so it is what the strip above the composer is drawn from, and what tells the pane which
|
|
7
7
|
// conversation a session is having.
|
|
8
8
|
//
|
|
@@ -129,6 +129,8 @@ export function statusFor(session, dir) {
|
|
|
129
129
|
contextPct: num(snapshot.context_used_pct),
|
|
130
130
|
quotaPct: num(snapshot.quota_left_pct),
|
|
131
131
|
quotaResetsAt: num(snapshot.quota_resets_at),
|
|
132
|
+
quotaWeekPct: num(snapshot.quota_week_left_pct),
|
|
133
|
+
quotaWeekResetsAt: num(snapshot.quota_week_resets_at),
|
|
132
134
|
version: text(snapshot.version),
|
|
133
135
|
updatedAt: num(snapshot.updated_at),
|
|
134
136
|
};
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: context-usage
|
|
3
|
-
description: Report the current session's context-window and rate-limit (quota) usage. Use whenever the user asks how much context, tokens, or quota is used or left, how full the context window is, or when the rate limit resets.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# context-usage: Report context and quota usage
|
|
7
|
-
|
|
8
|
-
In a chamba container you CAN see your own context-window and rate-limit usage.
|
|
9
|
-
The status line script chamba installs writes a per-session snapshot on every prompt render (it renders nothing in the terminal - the web interface draws a strip from the same file); a bundled helper reads it back.
|
|
10
|
-
|
|
11
|
-
## Procedure
|
|
12
|
-
|
|
13
|
-
1. Run:
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
context-usage
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
Example output:
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
session: 78b4025b-... (this session, updated 3s ago)
|
|
23
|
-
context: 70.8k tokens (7% of 1M window)
|
|
24
|
-
quota: 84% remaining, resets in 21m
|
|
25
|
-
model: Fable 5 (effort high)
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
2. Answer in ONE short line, in exactly this shape:
|
|
29
|
-
|
|
30
|
-
> Context: 70.8k tokens (7%) - quota: 84% left, resets in 21m.
|
|
31
|
-
|
|
32
|
-
If the user asked only about context or only about quota, answer with just that half.
|
|
33
|
-
Expand beyond one line only when the user explicitly asks for more detail.
|
|
34
|
-
Do not add commentary, interpretation, or advice.
|
|
35
|
-
|
|
36
|
-
3. Caveats are the exception, not the norm.
|
|
37
|
-
The `(this session, ...)` marker on the session line means the snapshot is guaranteed to be this session's own - say nothing about it.
|
|
38
|
-
Only when the marker is absent AND the output contains a `warning:` line, append one short sentence relaying that warning.
|
|
39
|
-
|
|
40
|
-
## Interpretation notes
|
|
41
|
-
|
|
42
|
-
- The snapshot reflects usage as of the moment the current prompt was submitted; tokens consumed during the in-flight turn are not included yet.
|
|
43
|
-
No need to mention this unless asked.
|
|
44
|
-
- The `quota` line refers to the 5-hour rate-limit window and is absent when Claude Code did not report rate-limit data.
|
|
45
|
-
|
|
46
|
-
## Fallback
|
|
47
|
-
|
|
48
|
-
If the `context-usage` command is not found, read the snapshot directly: list `~/.claude/context-usage/*.json`, pick the most recently modified file, and interpret its fields - `context_tokens`, `context_used_pct`, `context_window_size`, `model`, `effort`, `quota_left_pct`, `quota_resets_at` (epoch seconds), `updated_at` (epoch seconds), `version`, `session_id`.
|
|
49
|
-
|
|
50
|
-
If the command does not exist AND there are no snapshot files either, the container image predates this feature.
|
|
51
|
-
In that case tell the user to rebuild the image: start a new chamba session on the host and accept the rebuild prompt.
|
|
52
|
-
|
|
53
|
-
If the command exists but no snapshot files do, no Claude session in this container has rendered a prompt yet; say so rather than guessing values.
|