feat(workspace): tell the model about extension tools a live IDE bridge serves
Issue for this PR
Closes #1263
Type of change
- Bug fix
- New feature
- Refactor / code improvement
- Documentation
What does this PR do?
The ## Workspace integrations system-prompt section projected only the three warehouse capabilities per connection type, so extension-type tools the engine serves through a live VS Code bridge (the dbt project tools, sql-tools) never reached the model — even while they sat in its catalog. #1236 taught the presentation surfaces about them; this is the model-facing half.
declared()now also groups the extension keys under their catalog integration (Declared.extensions, optional — the flatkeys/extensionKeyslists remain the contract every existing reader was written against), and the attach outcome carries the groups so precedence never makes a second lookup.derivenames an extension tool only when two signals agree: its key materialised in the live catalog and a bridge for this project is live at this turn (liveBridge, the same read-only mirror #1236 uses). Either missing is silence, not a warning — a dormant bridge renders exactly what a workspace with no extension tools renders.- The groups ride the
nothing-materialisedsnapshot too, because a workspace can serve extension tools and no warehouse capability at all; that state now speaks when (and only when) it carries them. servedExtensionsapplies the same reachability filter asservedInventory, at projection time, so ananalystis never told about tools it may not call.- The section appends the groups under a fixed intro that names the condition they depend on (the window being open); the extension-only shape keeps the local-tools sentence so nothing is inferred from seeing
datamate_*keys listed. When the cap is hit, extension lines drop before any warehouse type.
Claims
- C1 — Byte-identical when nothing changes. Every snapshot without served extension tools renders the section exactly as before, including all silent disabled states and the unlinked-project case. The disabled-reason table is untouched;
EMPTY's shape is untouched unless extension tools are present. - C2 — Two signals, or silence. An extension tool is named only when its key is in the live engine catalog and
liveBridge(projectDirectory)is true for this turn. A dormant bridge, a catalog key no declared group names, or an outcome without groups (an older engine) all render the no-extension section. - C3 — No new lookup, inert names. Grouping and display names come from the catalog read the overlay already caches per attach; nothing is fetched per turn. Integration names pass through
inertWorkspaceNamebefore reaching the prompt. - C4 — Reachability respected. Tools the caller's ruleset denies are filtered at projection time; the
analystshape renders nothing. - C5 — Extension-only workspaces speak. With extension tools served and nothing shadowed, the
nothing-materialisedsnapshot carries the groups (and the bound id) and renders an extension-only section that keeps the local-tools sentence; the same snapshot without a live bridge is silent, as it always was. - C6 — Cap discipline. Extension lines are dropped before any warehouse type; an omission line states how many groups were dropped; a single oversized group cannot breach
MAX_SECTION_CHARS.
Residuals
- R1 — Bridge liveness is the presentation mirror (
liveBridge), not the engine's own connection state. A window closed after this step's snapshot refresh can leave the block one step stale — the same bound the per-step snapshot already has for everything else in the section. - R2 —
liveBridgepath matching usespath.relativeprefix semantics rather than the engine's platform-aware normalisation (inherited from #1236's R1); a Windows drive-letter case mismatch can under-report a live bridge, never over-report. - R3 — Tool keys are quoted raw from the engine catalog, as the warehouse lines already do; only integration names are sanitised.
- R4 — The engine discovers the bridge at spawn and does not re-list tools; a window opened after the engine started is not advertised until the engine restarts. Engine behaviour, unchanged here.
- R5 —
deriverises 17 → 20 cognitive (complexity appendix below). The three added breaks are the extension gate, the outcome-shape spread, and the bound-id spread on thenothing-materialisedreturn; extracting them would move the branches into a helper, not remove them, so the shape is recorded rather than reshuffled.
How did you verify your code works?
- E2E on the ralphtest rig (see the E2E comment): live bridge → 14 of 16 declared extension keys served and named; real model turn quotes the section verbatim; sidecar-aside control renders no extension block.
bun testover the eight workspace suites (awareness,precedence,engine-overlay,engine-probes,engine-types,datamate-list-integrations,plugin/workspace,engine-install-offer): 312 tests, all green;tsgo --noEmitclean.- New coverage: named under the integration quoting only materialised keys (declared-but-absent control); dormant bridge byte-identical to a no-extension section; catalog keys without a declared group not claimed; extension-only shape from
nothing-materialisedand silent again without the bridge;analystrenders nothing; hostile integration name inert; cap drops extension lines first; the attach outcome carries groups only when the allowlist names any;declared()grouping (name from catalog, id fallback, empty groups dropped) and the exact flat-only shape when no extension integration exists.
Screenshots / recordings
Not a UI change. The rendered system-prompt section is asserted verbatim in awareness.test.ts, and the E2E comment on this PR shows the live render from a real VS Code bridge (probe), the model quoting it back verbatim (one real turn), and the negative control with the bridge removed.
Checklist
- I have tested my changes locally
- I have not included unrelated changes in this PR
🤖 Generated with Claude Code
https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
Appendix — complexity delta (awareness: extension tools via live bridge)
89ddf85656 → 686b2e559c · only functions this diff touches · advisory, not a gate.
| Function | File | Cognitive | CCN | Status |
|---|---|---|---|---|
derive L555 | packages/opencode/src/altimate/workspace/precedence.ts | 17 → 20 ▲ | 16 → 17 ▲ | hard to follow |
systemSection L146 | packages/opencode/src/altimate/workspace/awareness.ts | new → 10 | new → 8 | new — easy |
declared L124 | packages/opencode/src/altimate/workspace/engine-probes.ts | 8 → 9 ▲ | 11 → 9 ▼ | easy |
extensionsServed L287 | packages/opencode/src/altimate/workspace/precedence.ts | new → 9 | new → 12 | new — easy |
Summary: 13 touched · 3 rose · 1 improved · 6 new (max cognitive 10) · net cognitive Δ +26
ℹ️ How to read these numbers
Cognitive (Sonar spec) counts breaks in linear reading flow — each if/loop/catch/ternary/boolean-operator switch adds 1, and nesting makes every further break cost more. It approximates how much you must hold in your head to follow the function: 0–5 trivial · 6–10 easy · 11–15 moderate (15 = Sonar's recommended per-function cap) · 16–25 hard to follow · >25 needs decomposition.
CCN (cyclomatic) counts independent paths — also the minimum number of test cases for full branch coverage of the function.
Only functions this diff touches are measured, as deltas — pre-existing complexity is not counted against this change. Rising numbers aren't automatically wrong; they're where review attention should go. Test files excluded.
Summary by CodeRabbit
- New Features
- Workspace awareness now includes tools provided by connected VS Code extensions.
- Extension tools appear only when the IDE bridge is active and tools are available.
- Extension names and capabilities are displayed alongside warehouse tools, including extension-only workspaces.
- Workspace identity displays the linked workspace name, including when no capabilities are materialized.
- Tool listings respect display limits, prioritizing warehouse directives and explaining omitted extension tools when applicable.
- Workspace status distinguishes available, unavailable, and omitted capabilities.