Count the system prompt in the consumers view; let the lanes fill the terminal - #7
Merged
Merged
Conversation
The strip stopped short of the right edge for two independent reasons in one expression: const laneWidth = () => Math.max(10, Math.min(width() - 61, 80)) A hard cap of 80 cells, so past ~150 columns it stopped growing at all — 71 blank columns on a 200-column terminal, 111 on a 240. And a reserve of 61 columns for chrome that actually prints 49, so even below the cap there was a constant 16-column gap. Part of that reserve was stale: it was sized for a mode legend that included "3 calls", and stayed 61 when that mode was removed. The strip is now width() + 2 - LANE_CHROME. The +2 is the "│ " a row draws outside its own width, which a lane label carries inside its 12, so the lane rows end on the same column as the rows and the status line rather than two short. No ceiling: the strip is a window onto an unbounded layout, so more cells is more events visible and less scrolling, and laneOverview already hides the overview track once nothing is off-screen. LANE_CHROME is derived from the label and legend strings, which now live in core/lanes.ts, instead of being written down — that is the actual fix for this class of bug. Four tests pin the invariants it rests on: the legend is the same width whichever mode is selected, the label column is fixed width for every lane name with or without a cue, the suffix is fixed width with or without a cue, and LANE_CHROME equals what a lane row really spends off-strip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrwAnTfTmJSmFwVn3DDFxr
Consumers walked the transcript only, so it omitted the system prompt and its total could not be reconciled with the `ctx …` gauge two lines above it — which reads tokens.input and therefore does include it. On an agent with a large base prompt and an AGENTS.md that is a silently missing 5-15k in the one view whose whole job is "where did my window go". experimental.chat.system.transform is the only place the plugin can see it. The server half snapshots output.system there, before pushing its own note — counting our note as part of the user's prompt would be a small lie in exactly the wrong view — names each part by a shallow text heuristic (AGENTS.md, CLAUDE.md, environment, else base prompt), and writes system-<sessionID>.json. That file is overwritten each request, never appended: it is current state outside the message tree, not a mutation with history, so /undo has nothing to do with it and it must not grow the way the journal does. The TUI reads it on the same poll as the tree. It renders as one `≡ system prompt` bucket with an entry per part, croppable: false and a note, reusing the mechanism `(thinking)` already had. `y` in the consumers panel copies the selected part in full — the only way to read one, since it is not a message and so has no row and no inspector of its own. Absence means unknown, never zero: a session whose prompt we have not seen yet has no bucket rather than a 0. The capture is defensive in every direction and logs what it actually saw (debug "system.captured", parts + chars), which is also how to confirm on a live server that output.system arrives carrying OpenCode's own parts rather than empty — the assumption the whole feature rests on, and one this container cannot check because opencode serve will not bind here. Tool-definition schemas remain uncounted: client.tool.list gives their descriptions but not what the provider is really sent, so that estimate would be rough enough to mislead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrwAnTfTmJSmFwVn3DDFxr
The capture was gated on the session already being in a tree, but a session is only registered by a branch, a fork or adoption — so a plain session never captured its prompt and the consumers bucket silently never appeared. That is the common case, not the edge one. The capture is now unconditional; only the "Context notes:" push keeps the tree guard, since that note explains ◆/✂ markers that exist only in managed sessions. Since this hook runs on every request, an in-memory per-session shape check skips the write when nothing changed. Adds the e2e that settles the assumption the feature rests on — that output.system arrives carrying OpenCode's own parts rather than empty. It drives the real TUI on a session that never branches and asserts the snapshot exists with real content, that our own note is NOT in it (the capture runs before the push), and that "≡ system prompt" reaches the consumers view. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrwAnTfTmJSmFwVn3DDFxr
The first run of this test failed with a bare scandir ENOENT on the context-tree directory, which says only "it did not happen" — not whether the hook never fired or fired with an empty output.system. The test now runs the TUI with CTREE_DEBUG and, when the directory is absent, fails with the capture's own log lines. That distinguishes the two causes on the spot: a "system.captured" line with parts: 0 means the array arrives empty, and no line at all means the hook does not fire in this OpenCode version. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrwAnTfTmJSmFwVn3DDFxr
"Did the server plugin load at all?" is the first question in any bug report about it, and there was no way to answer it — every other debug line sits behind a hook that only fires for sessions already in a tree, so an empty log meant nothing. No-op unless CTREE_DEBUG is set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrwAnTfTmJSmFwVn3DDFxr
…tles it The system-prompt capture could not be verified in the web container: its opencode never runs the plugin's server half at all, which was established by reproducing the crop e2e failure on clean main in a separate worktree. This file records that finding, marks 8adb027 as do-not-merge until one command is run, and reads out the three possible outcomes — including that a bad result would mean the plugin's existing "Context notes:" system push has never been delivered. Delete before merging. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrwAnTfTmJSmFwVn3DDFxr
Three real defects found reviewing the branch before opening it.
`y` handed you the wrong text. `nameSystemPart` is a substring heuristic, so a
global and a project `AGENTS.md` both come back "AGENTS.md" — and the copy path
looked its part up *by name*, in original order, while the entries are shown
sorted biggest-first. Selecting the 4k part copied the 12k part, silently.
Entries now carry the index of the part they came from.
The "has this prompt changed?" key was `name:chars`, so a change of equal length
never rewrote the file: the `<env>` part's date rolling 2026-09-04 → 2026-09-05
is the same length and the same name, and the snapshot would serve yesterday's
text for the life of the server process. It hashes the content now.
`system-${sessionID}.json` put a host-supplied id straight into a path we write,
where a `/` or `..` escapes the plugin dir. Refused rather than sanitised.
Also: the snapshot read was an eager memo doing a readFileSync — a thrown and
caught ENOENT, in the common no-snapshot case — on every poll tick whether or
not the consumers panel was ever opened; it is gated on the panel now. Dropped
the pretty-printing that doubled every snapshot on disk. Four consumers tests
were indented as if inside their `describe` but sat outside it.
And the e2e that proves the capture end to end now asserts against the
pyte-rendered screen: the raw pty stream drops cells from an incrementally
repainted panel, so it read `≡ yem prompt` and failed a working feature.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two user-visible changes, plus the bug fix and the verification that had to happen before either could be trusted.
The consumers view counts the system prompt
swalked the transcript only, so its total could never be reconciled with thectx …gauge two lines above it — that readstokens.input, which does include the system prompt. On an agent with a large base prompt and anAGENTS.md, 5–15k was silently missing.The server half now snapshots what the provider is really sent, in
experimental.chat.system.transform, before appending its own note — counting our note as part of the user's prompt would be a small lie in the one view that exists to say where the context went. The view shows a≡ system promptbucket with one entry per part, so you can see that your rules file costs 4k. Not croppable;ycopies a part in full. A session whose prompt we have not seen shows no bucket, never a misleading0.The lanes fill the terminal
They were capped at 80 cells and reserved a hardcoded 61 columns for chrome that prints 49, so a 200-column terminal left ~71 blank columns and even a 130-column one left 16.
LANE_CHROMEis now measured from the label and legend strings rather than written down — that is the fix for the bug class, not just the instance: shortening the legend now widens the strip instead of leaving a gap, which is exactly what went wrong when the· 3 callsmode was removed and the 61 stayed 61.The bug that would have made the feature never appear
The capture was gated on
stateForSession(sessionID), but a session is only registered by a branch/fork/adoption — so on a plain session that never branches (the common case) it would never have fired and the bucket would silently never have shown up. The capture is ungated now; theContext notes:push keeps its gate, since that note only makes sense for sessions the plugin manages.What was verified, and what it settled
The branch arrived with the whole feature resting on an untested assumption: that
experimental.chat.system.transformhands usoutput.systemalready carrying OpenCode's own parts. It does. A new e2e drives a real TUI on a plain, never-branched session and asserts the snapshot exists with real content, that our own note is not in it, and that the bucket reaches the screen.That opened a worse question — nothing had ever tested that this hook fires at all, which would have meant the plugin's
Context notes:had never been delivered andapplyCropshad been shipping[cropped: …]stubs the model was never told how to read. It is delivered: a new server e2e asserts the note reaches the provider's system message on a branched session, and correctly does not on one outside the tree.Review fixes (last commit)
ycopied the wrong system part. Names are a substring heuristic, so twoAGENTS.mdparts share one; the lookup matched by name in original order while entries render sorted biggest-first, so picking the 4k part copied the 12k part's text. Entries carry their part index now.name:chars, so an equal-length edit never rewrote — the<env>date rolling2026-09-04→2026-09-05would serve yesterday's text for the life of the process. Hashed by content now.system-${sessionID}.jsoninterpolated a host-supplied id into a path we write.readFileSyncper poll tick), pretty-printing dropped, four tests moved inside thedescribethey were indented into.Tests
CTREE_E2E=1, both files): 15 pass, 1 fail.The one failure is
crops the completed bash tool output, which is pre-existing onmain— it fails deterministically 3/3 in a clean worktree at2266966with the same 30s timeout andConnectionRefused. It is a probe of the OpenCode API through a scratch "spike" plugin, not of this plugin; the real crop path is covered by a passing test that asserts the provider sees the stub. Tracked separately.Known gaps, deliberately left
client.tool.listgives descriptions, not what the provider receives, so the estimate would mislead. This is the last unattributed chunk between the consumers total and thectxgauge.system prompt N.