Skip to content

feat(claude): attach workspace files as @-mention in Claude mode#327

Merged
mbektas merged 1 commit into
plmbr:mainfrom
pjdoland:feat/326-claude-mode-file-mention
May 19, 2026
Merged

feat(claude): attach workspace files as @-mention in Claude mode#327
mbektas merged 1 commit into
plmbr:mainfrom
pjdoland:feat/326-claude-mode-file-mention

Conversation

@pjdoland
Copy link
Copy Markdown
Collaborator

@pjdoland pjdoland commented May 19, 2026

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:

  • Images in the workspace: previously rejected by serializeWorkspaceFileContent's binary check, now picker-eligible (the agent uses vision via Read).
  • Large files: stop silently truncating at the 80% context-window budget; the agent can grep, then read the relevant slice.
  • Notebooks (.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_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).

The deictic-reference pointer prose the legacy path produced is preserved as a tail on the @-mention message:

  • When currentCellContents is present (active notebook cell, no text selected), append the cell input/output prose so "explain this cell" still has a referent.
  • When the entry carries a multi-line selection (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_codepoints helper (extracted in util.py) wraps the existing _DISALLOWED_URI_CODEPOINTS set already used by safe_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)

handleWorkspaceFileSelection short-circuits in Claude mode and skips the contentsManager.get(..., { content: true }) call (which throws on binary), attaching with content: '' and lineCount: 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

  • The pasted-image branch (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.
  • The structured cell-output bundle path (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.py pin the contract:

  • @-mention emitted in Claude mode; file contents not echoed, even with a 10MB content blob the legacy path would have truncated.
  • is_image branch unchanged in Claude mode (the new branch must not shadow it).
  • Out-of-workspace paths (../../etc/passwd) rejected by the pre-existing sandbox before the @-mention branch can format them.
  • is_upload=True non-image attachments emit the absolute upload path rather than relpath'ing against the workspace root.
  • Filename codepoint guard rejects three vectors: literal newline, U+2028 LINE SEPARATOR, U+202E RIGHT-TO-LEFT OVERRIDE.
  • Notebook cell-pointer prose flows through when currentCellContents is present (the cell input/output and "this cell" referent prose all land in chat history).
  • Multi-line selection produces a "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

  • The pasted-image branch's prose format diverges from the new @-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 inside jupyter_root.
  • The codepoint guard fires only in the new Claude branch. The existing pasted-image and upload-non-image branches embed filenames into prose without it. Worth a defense-in-depth sweep in a separate hardening PR.
  • Multi-file attachments emit one {role: user, content: ...} entry per file. The SDK joins them with \n on the wire, so a single combined message would be byte-equivalent today; left as separate messages for clarity.
  • The Claude Agent SDK's built-in Read tool is not yet path-sandboxed via 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.
  • When the user's prompt begins with /, the join logic in claude.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.

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
@pjdoland pjdoland force-pushed the feat/326-claude-mode-file-mention branch from 32220d3 to 2d4eedb Compare May 19, 2026 21:33
@pjdoland pjdoland added this to the 5.0.x milestone May 19, 2026
@pjdoland pjdoland added the enhancement New feature or request label May 19, 2026
@mbektas mbektas merged commit 5275517 into plmbr:main May 19, 2026
5 of 6 checks passed
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

In Claude Mode, Add file as context selection should be added as @-mention

2 participants