fix(claude-sessions): unwrap NBI preamble when joined with user prompt#331
Merged
mbektas merged 1 commit intoMay 21, 2026
Merged
Conversation
claude.py joins consecutive user-role chat-history entries with '\n'
before handing them to the Claude Agent SDK. extension.py emits the
NBI context preamble ("Additional context: Current directory open in
Jupyter is: ...") as its own user-role entry, so a single-turn
session records one combined user message whose first line is the
preamble and whose remainder is the user's actual prompt.
The previous skip filter checked startswith(_SKIPPABLE_PREFIXES) on
the whole text, matched the preamble, and dropped the entire message.
With nothing else in the transcript, the picker had no preview and
the session showed up titled only by its id + timestamp (issue plmbr#329).
The fix:
- New _strip_nbi_context_preamble() helper drops the preamble line
(everything up to and including the first newline) when the text
starts with NBI_CONTEXT_PREFIX. Returns the input unchanged
otherwise, and the empty string when the preamble is the only
content.
- _is_skippable_text() unwraps before applying the rest of the skip
rules so a joined preamble+prompt is judged by its real-prompt
content. The unwrap recurses at most once: NBI_CONTEXT_PREFIX is
single-line, so the tail can't itself lead with the preamble, and
the recursive call falls through to the legacy prefix / control-
command checks (or to the empty-stripped guard at the top).
- _extract_preview() unwraps before whitespace-collapsing so the
preview reads as the user's prompt, not the boilerplate.
Five new tests pin: joined plain-text preamble + prompt, the
Anthropic structured-content variant, joined preamble + skippable
prompt (recursion exercise), preamble alone without trailing newline,
and preamble followed by whitespace-only tail. The first two are
verified to fail against the pre-fix code.
Closes plmbr#329
pjdoland
added a commit
to pjdoland/notebook-intelligence
that referenced
this pull request
May 22, 2026
Promotes the [Unreleased] CHANGELOG snapshot to [5.0.0] - 2026-05-22 and expands it to cover everything merged into upstream/main after PR plmbr#287's docs refresh. Bumps package.json to 5.0.0. CHANGELOG additions cover the post-plmbr#287 surface: - Settings tabs: plugin marketplace picker (plmbr#284), plugin marketplace details + Update button (plmbr#303), per-workspace MCP disable (plmbr#286), JSON-paste path in Add MCP server (plmbr#285). - Launchers: hide-with-policy (plmbr#288), brand icons for Codex / opencode (plmbr#325, plmbr#333), per-launch directory picker (plmbr#332). - Chat sidebar and agentic UX: workspace @-mention in Claude mode (plmbr#327), reload-open-files-on-disk (plmbr#330), steered system prompt away from over-eager notebook creation (plmbr#336). - Skills: multi-manifest support (plmbr#321), tracks-upstream for user- imported skills (plmbr#322), HTTP kill switch for the reconciler (plmbr#291). - Accessibility: full sub-section covering plmbr#305-plmbr#320. - Security: shell-tool sandbox (plmbr#290), Claude UI-bridge sandbox (plmbr#323), 0o600 on encrypted token (plmbr#293), env-secret scrubbing (plmbr#295), MCP config shape validation (plmbr#299), XSS allowlist (plmbr#296), Copilot WS auth + origin (plmbr#301), GHE host detection (plmbr#292), fastmcp -> mcp SDK swap (plmbr#324). - Fixed: session listing unification (plmbr#310), session preview unwrap (plmbr#331), down-area runtime throw (plmbr#330 follow-up), WS message-handler leak (plmbr#294). - Removed: fastmcp dependency, history.jsonl session gate. Adds a Migration note covering the five behavior changes operators should review before upgrading from 4.x: fastmcp swap, path sandboxes, history.jsonl gate removal, workspace @-mention pointer shape, and the Copilot WebSocket auth/origin tightening. Two reviewer rounds (six personas each) applied: - Round 1 caught security overclaims (plmbr#293, plmbr#299, plmbr#323), the plmbr#284/plmbr#303 mis-attribution, missing migration note, 3 em dashes, and the stale `fastmcp==2.x.*` recommendation in the admin guide. - Round 2 caught the missing plmbr#301 migration bullet, missing version- matrix 5.0.x row, missing README TOC entry, and a couple of style nits (sub-heading overpromise, orphan bullet). Skipped (deferred to future PRs): - README first-run tour mention. - Admin guide HTTP kill-switch row in Failure-modes table. - Terminal drag-drop trust-model precision update after plmbr#327. - Cipher description nit in plmbr#293 (Fernet AES-128-CBC+HMAC, not AES-GCM).
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.
Summary
Closes #329. When a Claude Code session has only a single user turn, the chat-history picker showed no preview / summary for that session — only the id and timestamp. The cause was a skip filter that treated the joined preamble + prompt string as if it were only the preamble.
Solution
claude.pyjoins consecutive user-role chat-history entries with\nbefore handing them to the Claude Agent SDK.extension.pyemits the NBI context preamble ("Additional context: Current directory open in Jupyter is: ...") as its own user-role entry, so a single-turn session records ONE combined user message whose first line is the preamble and whose remainder is the user's actual prompt.The previous skip filter at
notebook_intelligence/claude_sessions.pycheckedstripped.startswith(_SKIPPABLE_PREFIXES)against the full text, matched the preamble, and dropped the whole message. The session's only user message was thereby skipped, leaving the picker preview blank.The load-bearing change is a new
_strip_nbi_context_preamblehelper that drops the preamble line (everything up to and including the first newline) when present. It plugs into the two consumers:_is_skippable_textunwraps before applying the rest of the skip rules so the message is judged by the real-prompt content. The unwrap recurses at most once becauseNBI_CONTEXT_PREFIXis single-line: the unwrapped tail can never itself lead with the preamble, and the recursive call falls through to the legacy prefix / control-command checks (or bottoms out on the empty-stripped guard)._extract_previewunwraps before whitespace-collapsing so the preview reads as the user's prompt rather than the boilerplate.The helper deliberately doesn't trim or normalize the unwrapped tail beyond stripping the preamble line: both callers normalize whitespace themselves (
_is_skippable_textvia.strip(),_extract_previewvia" ".join(text.split())), and trailing whitespace in the result belongs to the user's prompt either way.Testing
Five new tests in
tests/test_claude_sessions.pypin the contract:test_unwraps_joined_preamble_and_prompt_in_one_message— the exact bug from the issue body (preamble +\n+ prompt as a single string content).test_unwraps_joined_preamble_in_structured_content— same shape, Anthropic structured-content form with the joined string inside one text block.test_joined_preamble_with_skippable_prompt_still_skips— preamble joined with a control slash command (/exit); the combined message must remain skippable so the picker keeps scanning for a real prompt.test_joined_preamble_alone_without_newline_still_skips— preamble recorded with no trailing newline; helper returns empty string and the recursive call bottoms out on the empty-stripped guard.test_joined_preamble_with_empty_post_newline_still_skips— preamble followed by whitespace-only tail; same end state as preamble-alone.The first two are verified to fail against the pre-fix code; the latter three document downstream behavior the picker has always honored.
All four gates green:
pytest tests/ --ignore=tests/test_claude_client.py(1055 passed, 66 intest_claude_sessions.py),jlpm tsc --noEmit,jlpm lint:check,jlpm jest(254 passed).Risks / follow-ups
"Additional context: Current directory open in Jupyter is:"would have its first line stripped. The pre-existing_SKIPPABLE_PREFIXEStuple already had the same trade-off (called out in the existing comment at lines 47-49); the helper inherits it._is_skippable_user_messagewould need to mirror_extract_preview's block-join behavior. Filed mentally as a follow-up.NBI_CONTEXT_PREFIXconstant remains in_SKIPPABLE_PREFIXESas a backstop. The new early branch consumes the preamble first, but the tuple entry still handles edge cases where the preamble somehow reaches the fallthrough.Issue linkage
Closes #329.