Skip to content

Add get_selection / highlight_selection: the agent-to-viewport selection bridge (#189, #190) - #191

Merged
gsdali merged 2 commits into
mainfrom
issue-189-190-selection-bridge
Aug 21, 2026
Merged

Add get_selection / highlight_selection: the agent-to-viewport selection bridge (#189, #190)#191
gsdali merged 2 commits into
mainfrom
issue-189-190-selection-bridge

Conversation

@gsdali

@gsdali gsdali commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

What & why

Implements get_selection and highlight_selection, the read and write halves of the
agent-to-viewport-host selection bridge, against the refined-spec acceptance criteria on both
issues (posted 2026-08-21), not the original pre-refinement issue bodies. One PR for both since
they share all the plumbing (the host.lock liveness probe, the resolved output directory,
the wire format).

Neither tool calls into a live selection directly: OCCTMCP is a stdio MCP process with no
persistent link to any app's live InteractiveContext/ViewportService. Both instead speak the
sidecar-file wire format from SecondMouseAU/OCCTSwiftInteraction#17 (the ADR, in review in
parallel with this work) in the resolved output directory, the same convention every other
OCCTMCP sidecar already uses. No host implements the writer/watcher side yet
(OCCTSwiftInteraction#16/ACADStudio#16 are still upstream), so every test here plays the host
itself against hand-written fixture files, exactly as both refined-spec comments describe.

get_selection: reads selection.json + host.lock. Three-state result (noHost /
hostRunning with an empty or populated selections array, never a bare boolean or empty-array
collapse). Each selection resolves against this server's own scene the same way select_topology
resolves a match, reusing the existing SelectionTools.graphIndex(...) convention and
SelectionRegistry.record(...), so the minted selectionId composes with
remap_selection/measure_distance/etc. A per-entry resolution failure (bad bodyId,
out-of-range index) is reported inline without failing the rest of the response; a running
host whose selection.json is missing or malformed is reported as an explicit tool error, since
the host is expected to maintain that sidecar for its whole lifetime.

highlight_selection: writes highlight_requests/<id>.json atomically
(Data.write(to:options:.atomic), temp name + rename) and polls
highlight_requests/handled/<id>.json for a bounded timeout, returning the host's real
applied/rejected/superseded outcome, an explicit timeout, or noHost immediately (no
request written) when no host is running at all. kind/scheme are validated against their
closed wire-format enums before writing; bodyId/index are written through unvalidated against
the live scene, since this tool has no other access to check them (a bad reference still comes
back as the host's own rejected outcome through the same poll).

Closes #189, Closes #190

Checklist

  • New or changed behavior is covered by a unit test in the same PR: 13 new fixture-driven
    tests in SwiftTests/OCCTMCPCoreTests/SelectionBridgeToolsTests.swift, covering every
    acceptance-criteria checkbox on both issues (the three liveness states, per-entry resolution
    failures, missing/malformed selection.json, the atomic request write, handled/ polling
    for both applied and rejected outcomes, the timeout path, the noHost fast path,
    unvalidated-scene-reference writes, and client-side kind/scheme enum rejection). Full
    suite: 232/232 passing (swift build + swift test).

Notes for the reviewer

Judgment calls the refined-spec comments didn't fully pin down:

  • highlight_selection's id generation. The spec says "generating id itself so retries are
    idempotent (per the ADR's Section 6.1 mitigation)" without the ADR text available to consult
    (it's in review in a separate repo). I generate a fresh UUID() per call rather than a
    deterministic hash of the request content: a content-derived id would make a genuinely repeated
    call (not a retry) with identical arguments silently short-circuit against a stale handled/
    file from an earlier, already-completed call, which is wrong for scheme: "xor" in particular.
    "Idempotent" here is interpreted as "the tool owns id generation so a caller never needs to
    invent or coordinate its own id across concurrent calls," combined with the atomic write making
    a single call's own request write safe to retry. Flagging this in case the ADR's actual Section
    6.1 intends the deterministic-hash reading instead.
  • A running host with selection.json missing entirely. Treated the same as malformed (an
    explicit tool error), not as hostRunning(selections: []), on the reasoning that the ADR expects
    a live host to maintain this sidecar for its whole lifetime, so its total absence while
    host.lock is held is anomalous rather than "nothing selected yet." The refined spec's bullet
    groups "missing/malformed" together, which reads consistently with this, but it's worth a second
    look.
  • highlight_selection's liveness check happens before writing. When host.lock shows no
    host, the tool returns outcome: "noHost" immediately without writing a request file at all,
    rather than writing it and then timing out. This matches "rather than hanging... when nothing is
    consuming highlight_requests/" and avoids leaving an orphaned request file nobody will ever
    read.
  • Wire-index interpretation. selection.json/highlight_requests entries carry a plain
    index: Int with no stated enumeration space. Treated it as an ordinal into the same enumeration
    select_topology already uses (Shape.faces()/.edges()/subShapes(ofType: .vertex)),
    converting to a BRepGraph node index via the existing graphIndex(...) helper before minting
    the anchor, matching the "resolved the same way select_topology already resolves them"
    language and the SubShapeRef.ordinal/Shape.faces() convention OCCTSwiftInteraction
    settled on fleet-wide (per the Re-key SelectionRegistry on GraphUID #182 write-up in this repo's own SelectionRegistry.swift).
  • New test file naming. Feature: get_selection tool for reading human selection with durable GraphUIDs #189's spec says the round-trip test should live in "SelectionTools's
    test suite"; there's no existing SelectionToolsTests.swift to add to, so both tools' tests live
    in a new SelectionBridgeToolsTests.swift alongside the new SelectionBridgeTools.swift source
    file, per the code-structure policy's one-file-per-tool-family convention.
  • Docs sweep. Bumped the tool count (77 -> 79) everywhere it's stated (README.md, CLAUDE.md,
    the docs site, okf/), and added both tools to docs/reference/selection.md +
    its Families table. Left other, unrelated pre-existing doc drift in those same files untouched
    (e.g. docs/reference/README.md said "75-tool" before this PR touched it for an unrelated
    reason; fixed that one inline since I was already editing that exact line, but did not do a
    wider unrelated cleanup pass).

Not merging — for review.

🤖 Generated with Claude Code

…ion bridge (#189, #190)

Implements the two new MCP tools per both issues' refined-spec comments,
against the wire format in SecondMouseAU/OCCTSwiftInteraction#17 (in review
in parallel; schema treated as final regardless of later wording changes
there). No host implements the writer/watcher side yet, so this only
consumes hand-written fixture sidecar files.

get_selection reads selection.json + host.lock from the resolved output
directory (Paths.swift) and returns a three-state result: noHost
(selections: null), hostRunning with selections: [] (host live, nothing
selected), or hostRunning with selections: [...]. Each entry is resolved
against this server's own scene the same way select_topology resolves a
match, reusing SelectionTools.graphIndex(...) and SelectionRegistry.record
so the minted selectionId composes with remap_selection/measure_distance/
etc. A per-entry resolution failure (bad bodyId, out-of-range index) is
reported inline without failing the whole response; a running host with
selection.json missing or malformed is reported as an explicit tool error,
never swallowed into an empty result.

highlight_selection writes highlight_requests/<id>.json atomically
(Data.write(to:options:.atomic)) and polls highlight_requests/handled/<id>.json
for a bounded timeout, returning the host's real applied/rejected/superseded
outcome, an explicit timeout, or noHost immediately (no request written) when
no host is running. kind/scheme are validated against their closed wire-format
enums before writing; bodyId/index are written through unvalidated against
the live scene, since this tool has no other access to check them.

Shared plumbing: HostLock.checkLiveness(outputDir:) probes host.lock with a
non-blocking shared flock (success means no host, EWOULDBLOCK means one is
running), added in the new Sources/OCCTMCPCore/Tools/SelectionBridgeTools.swift
per the code-structure policy (one file per tool family).

Registers both tools in Server.swift's catalog + dispatch. Updates the tool
count (77 -> 79) across README.md, CLAUDE.md, and the docs site, adds the two
tools to docs/reference/selection.md and its Families table, and documents
the new file in CLAUDE.md's Tools/ list.

13 new fixture-driven tests in SwiftTests/OCCTMCPCoreTests/SelectionBridgeToolsTests.swift
cover all three liveness states, per-entry resolution failures, missing/malformed
selection.json, atomic request writes, handled/ polling (applied + rejected),
the timeout path, the noHost fast path, unvalidated-scene-reference writes, and
client-side kind/scheme enum rejection. Fixes PingTests' hardcoded tool count
to match. Full suite: 232/232 passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread Sources/OCCTMCPCore/Tools/SelectionBridgeTools.swift
Comment thread Sources/OCCTMCPCore/Tools/SelectionBridgeTools.swift Outdated
Comment thread Sources/OCCTMCPCore/Tools/SelectionBridgeTools.swift
@kilo-code-bot

kilo-code-bot Bot commented Aug 21, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (2 files)
  • Sources/OCCTMCPCore/Tools/SelectionBridgeTools.swift - All previous issues resolved
  • SwiftTests/OCCTMCPCoreTests/SelectionBridgeToolsTests.swift - New tests added for fixes
Previous Review Summary (commit 812e64c)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 812e64c)

Status: 3 Issues Found | Recommendation: Address warnings before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
Sources/OCCTMCPCore/Tools/SelectionBridgeTools.swift 69 flock failure handling imprecise - treats ALL failures as "host running"
Sources/OCCTMCPCore/Tools/SelectionBridgeTools.swift 487 readHandledOutcome silently ignores malformed host responses

SUGGESTION

File Line Issue
Sources/OCCTMCPCore/Tools/SelectionBridgeTools.swift 475 Polling loop discards Task cancellation
Files Reviewed (6 files)
  • Sources/OCCTMCPCore/Tools/SelectionBridgeTools.swift - 3 issues
  • Sources/OCCTMCPCore/Server.swift - No issues (correct tool registration & dispatch)
  • SwiftTests/OCCTMCPCoreTests/SelectionBridgeToolsTests.swift - No issues (comprehensive test coverage)
  • docs/reference/selection.md - No issues (accurate documentation)
  • docs/guides/architecture.md / getting-started.md / index.md / _config.yml / okf/ - No issues (consistent tool count updates)
  • SwiftTests/OCCTMCPCoreTests/PingTests.swift - No issues (tool count updated)

Fix these issues in Kilo Cloud


Reviewed by nemotron-3-ultra-550b-a55b:free · Input: 159.7K · Output: 2.3K · Cached: 280.8K

…, cancellation

Three review findings on SelectionBridgeTools.swift, all applied as real
correctness/robustness fixes rather than dismissed:

- HostLock.checkLiveness treated ANY flock failure as hostRunning, not just
  EWOULDBLOCK (the actual "something holds the exclusive lock" signal). A
  failure for another reason (locking unsupported on the filesystem, an
  interrupted call) now falls open to noHost, matching the file's own
  already-stated design philosophy for the open() failure case just above it.
- readHandledOutcome silently treated a handled/<id>.json that exists but
  fails to decode the same as "not there yet", so a malformed host response
  read as a plain timeout instead of surfacing that the host actually
  responded. Replaced with pollHandledOutcome returning a three-case
  HandledPoll (pending/decoded/malformed) so the poll loop reports outcome
  "error" immediately instead of waiting out the whole deadline.
- The poll loop's try? await Task.sleep(...) swallowed CancellationError, so
  a cancelled call kept polling until the timeout instead of exiting early.
  Added an explicit Task.isCancelled check after each sleep, returning
  outcome "cancelled".

Two new tests cover the malformed-handled-file and cancellation cases.
The flock errno branch has no practical way to force a non-EWOULDBLOCK
failure through the public API in a portable test, so it's covered by
review/inspection rather than a new test; the existing HostLock coverage
(held lock -> hostRunning, no lock -> noHost) is unaffected and still
passes.

swift build + swift test: 234/234 passing (232 previous + 2 new).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@gsdali
gsdali merged commit 8b3958a into main Aug 21, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant