feat(claude): attach workspace files as @-mention in Claude mode#327
Merged
Merged
Conversation
In Claude Code mode, workspace files attached via the picker no longer
have their contents read client-side and injected as a fenced code
block. The backend emits a short prose message with the file path
prefixed by @, letting the agent's Read tool decide what (and how
much) to read.
This generalizes the path-based pattern that was already in place for
pasted images and unblocks three cases that the content-injection
path couldn't handle:
- Images in the workspace: previously rejected by
serializeWorkspaceFileContent's binary check; now picker-eligible,
agent uses vision via Read.
- Large files: stop silently truncating at the 80% context-window
budget; the agent can grep and then read the relevant slice.
- Notebooks (.ipynb): Claude Code's notebook-aware Read surfaces cells
natively instead of dumping the raw JSON.
Backend (notebook_intelligence/extension.py):
A new branch in the additional-context loop fires when
is_claude_code_mode is true, after the existing is_image branch and
before the legacy _build_additional_context_message path. It emits
"The user attached @<workspace-relative-path>. Read it if relevant to
the request." Workspace files use path.relpath against the realpath'd
root (computed once before the loop); uploads outside the workspace
use the absolute server-temp path (parity with the existing
pasted-image branch).
When the context entry carries currentCellContents (the active
notebook cell, no text selection), the cell-pointer prose ("currently
selected cell input is ... output is ... If user asks a question
about 'this' cell ...") is appended so deictic references still have
a referent. When the entry carries a multi-line selection
(endLine > startLine), a "Their selection spans lines N-M." pointer
is appended instead. The bulk selection text is intentionally not
echoed; the agent reads the file via the @-mention and is told where
to focus.
Filename codepoint guard: defense-in-depth on a path that already
passed the workspace sandbox. Reuses the
has_dangerous_text_codepoints helper extracted in util.py against the
existing _DISALLOWED_URI_CODEPOINTS set (C0/DEL/C1, NEL/NBSP/LS/PS,
BOM, ZWSP, bidi-override controls). A filename containing newlines or
U+202E would otherwise split or visually impersonate the prose
envelope once it reached the agent.
Frontend (src/chat-sidebar.tsx):
handleWorkspaceFileSelection short-circuits in Claude mode and skips
the contentsManager.get(..., {content: true}) call (which throws on
binary), attaching with content: '' and lineCount: 0. Single dispatch
at the bottom; content/lineCount populated only on the non-Claude
branch. The binary-rejection guard remains for non-Claude providers
via the same call site.
The pasted-image branch (existing is_image + is_claude_code_mode
case) is intentionally untouched: it already does the right thing for
uploaded images and changing its wording is out of scope. The
structured cell-output bundle path (outputContext, from PR plmbr#160) also
remains untouched — it short-circuits at the top of the additional-
context loop before reaching the new branch, so the right-click
"Explain / Ask / Troubleshoot" menu's vision-block delivery continues
to work for images and dataframes.
Closes plmbr#326
32220d3 to
2d4eedb
Compare
mbektas
approved these changes
May 19, 2026
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 #326. In Claude Code mode, workspace files attached via the picker no longer get their contents read client-side and injected as a fenced code block. Instead the backend emits a short prose message with the file path prefixed by
@, letting the agent's Read tool decide what (and how much) to load.This unblocks three cases the content-injection path couldn't handle:
serializeWorkspaceFileContent's binary check, now picker-eligible (the agent uses vision via Read)..ipynb): Claude Code's notebook-aware Read surfaces cells natively instead of dumping the raw JSON.Solution
Backend (
notebook_intelligence/extension.py)A new branch in the additional-context loop fires when
is_claude_code_modeis true, after the existingis_imagebranch and before the legacy_build_additional_context_messagepath. It emits"The user attached @<workspace-relative-path>. Read it if relevant to the request."Workspace files usepath.relpathagainst the realpath'd root (computed once before the loop); uploads outside the workspace use the absolute server-temp path (parity with the existing pasted-image branch).The deictic-reference pointer prose the legacy path produced is preserved as a tail on the @-mention message:
currentCellContentsis present (active notebook cell, no text selected), append the cell input/output prose so "explain this cell" still has a referent.endLine > startLine), append"Their selection spans lines N-M."The bulk selection text is intentionally not echoed; the agent reads the file via the @-mention and is told where to focus.Filename codepoint guard: defense-in-depth on a path that already passed the workspace sandbox. The new
has_dangerous_text_codepointshelper (extracted inutil.py) wraps the existing_DISALLOWED_URI_CODEPOINTSset already used bysafe_anchor_uri(C0/DEL/C1, NEL/NBSP/LS/PS, BOM, ZWSP, bidi-override controls). A filename containing newlines or U+202E would otherwise split or visually impersonate the prose envelope.Frontend (
src/chat-sidebar.tsx)handleWorkspaceFileSelectionshort-circuits in Claude mode and skips thecontentsManager.get(..., { content: true })call (which throws on binary), attaching withcontent: ''andlineCount: 0. One fewer HTTP round-trip per pick. The binary-rejection guard is preserved for non-Claude providers via the same call site.What stays the same
is_image && is_claude_code_mode) is intentionally untouched: it already does the right thing for uploaded images, and changing its wording is out of scope.outputContext, from PR Extend cell-output context with structured bundles and image support #160) is also untouched. It short-circuits at the top of the additional-context loop before reaching the new branch, so the right-click "Explain / Ask / Troubleshoot" menu continues to deliver images and dataframe HTML as proper vision blocks.Testing
Seven new tests in
tests/test_websocket_handler_integration.pypin the contract:is_imagebranch unchanged in Claude mode (the new branch must not shadow it).../../etc/passwd) rejected by the pre-existing sandbox before the @-mention branch can format them.is_upload=Truenon-image attachments emit the absolute upload path rather than relpath'ing against the workspace root.currentCellContentsis present (the cell input/output and "this cell" referent prose all land in chat history)."lines N-M"pointer; bulk selection content does not leak. Whole-document / no-selection cases emit no pointer.Each new regression test verified to fail against its stashed pre-fix code (cell-pointer test, selection-range test).
Full suite:
pytest tests/ --ignore=tests/test_claude_client.py(1050 passed),jlpm tsc --noEmit,jlpm lint:check,jlpm jest(254 passed).Manual: dropped a binary image from the workspace picker in Claude mode and confirmed it attaches without the "Binary files cannot be attached" error.
Risks / follow-ups
@-mention format ("It is saved at this path: '<abs>'"vs"@<rel>"). Intentional: pasted-image uploads live outside the workspace where@<abs>wouldn't resolve via Claude Code's workspace-anchored mention parser. Could be unified in a follow-up if uploads land insidejupyter_root.{role: user, content: ...}entry per file. The SDK joins them with\non the wire, so a single combined message would be byte-equivalent today; left as separate messages for clarity.safe_jupyter_path(it falls through to the generic per-tool user confirmation). Pre-existing gap, not introduced by this change; worth a separate PR./, the join logic inclaude.py(query_lines[-1].startswith('/')) drops every prior user-role message, including the new @-mention context lines. Pre-existing behavior, documented inline near the @-mention emit so a future reader doesn't chase the @-mention silently disappearing when paired with a slash-command.Issue linkage
Closes #326.