Repository navigation
feat(mcp): open a child pane in another project, still parented on the caller (0228 C S2) - #546
Draft
nt-nerdtechnic wants to merge 19 commits into
Draft
nt-nerdtechnic wants to merge 19 commits into
nt-nerdtechnic wants to merge 19 commits into
Conversation
ui_invoke("ui.pane.close", {paneId: <own>}) closed the caller's own pane:
only cli_close_agent refused self. Add one self-scope check derived from
the caller's credential alone (pane id from its token, workspace from the
registry, windows mirroring that workspace's panes) and apply it to every
close route ui_invoke reaches: ui.pane.close -> self-close,
ui.workspace.close -> self-workspace-close, ui.window.close ->
self-window-close. A ui.pane.reclaim batch drops the caller's own pane and
reports it refused (reason "self"), reclaiming the rest.
The window repeats the same rule in useUiActionBus using caller_pane_id and
the new caller_workspace_path field, which the backend fills from the
registry, never from args.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…IGN-C S3) Agents could address panes in other workspaces but had no view of Navide's windows. A window is now named by its page-load id (ui.invoke.ready window_id): - window_list asks every main window to describe itself (ui.window.describe, addressed to that window alone, in parallel); a window that misses its budget is listed with answered false instead of holding the others up. is_yours marks the caller's own windows for display only; no refusal is decided on it (the self-scope guard is S1's). - window_focus asks one window to switch to a project it already holds (through ui.workspace.switch, so its refusals are the answer) and to bring itself to the front. Main's new window:focusSelf focuses the sender window and takes no window argument. - cli_list_targets / cli_whoami rows carry window_id when a main window with an id mirrors the pane; absent otherwise, so other rows are unchanged. - agent_window_actions records every window action with the calling pane (memory + rotating jsonl in app data); the focused window shows which pane did it. No count, size or rate limit is added. - window_ is reserved as a plugin tool prefix and the two tools join the pinned core tool names (Python, TypeScript, fixture, contract doc). Window helpers (_window_session, _window_request, _describe_windows) stay in server.py for the cross-workspace spawn (S2) to use. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…folders not strings HIGH-1: workbench.action.closeWindow is reachable through ui_invoke and runs window.close() in the window that answers, which for a pane caller is its own. Refused in the backend whenever the request lands in one of the caller's windows, and again in the window (hosts the caller or holds its workspace). MED-1: workbench.action.closeActivePane and rebuildFocusedPane act on the focused pane; the window refuses them while that pane is the caller. MED-2: own workspace is compared by folder identity (st_dev, st_ino), so a case variant on a case-insensitive volume is still the caller's own; the window compares case-folded. MED-3: the S4 contract for an empty ui.workspace.close path is recorded as a TODO comment and a skipped test. LOW: a pipeline slot cannot abort, reset, restart or restart_from its own run (window side); a reclaim batch keeps the caller's refused row when the window answers with an error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… only via their tools - A main window holding the caller's workspace with none of its panes in it (they all sit in detached group windows) is now part of the caller's own windows in the backend too. Windows announce the workspaces they hold with ui.invoke.ready (new optional `workspaces` field, re-announced on change; [] from a detached group window). - ui_invoke answers ui.window.close / ui.workspace.close with ui_dedicated_tool: only window_close / workspace_close record the action and tell the user which pane did it (review #541 MED-1). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ndow; closeWindow via window_close - ui.pipeline.reset ends every pane in the window that runs it (onKillAll), manual panes included and without a run. Refused for a pane caller when it lands in one of the caller's windows: in pipeline_reset and ui_invoke (backend, via _lands_in_own_window) and in the window whenever it hosts the caller. - workbench.action.closeWindow joins the dedicated close actions: ui_invoke answers ui_dedicated_tool and points at window_close, so closing anyone's window leaves a record. - _lands_in_own_window also reads the workspaces windows announce; _same_directory falls back to a case-folded compare on macOS/Windows for a folder that cannot be stat'ed. test_pipeline_window_requests_address_the_panes_own_workspace now expects the reset from a pane of that window to be refused. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ented on the caller A pane caller's workspace_path used to be ignored. Now, naming another project opens the child there (cwd, CLAUDE.md, memory, Plans, git) with the caller as its parent: it reports to the caller's <folder>/<pane> address and the caller reclaims it by the returned address. Naming its own workspace, or omitting it, sends exactly the old request. - window "current" (default): the caller's window adopts the project in the background (ui.workspace.adopt, no switch); "new": a window of its own, shown without taking focus (ui.workspace.open background). - One project, one window: a project another window holds is refused with workspace-in-other-window and holding_window_id; a missing folder is workspace-not-found; window_not_ready says what was already prepared. - The window holding the target creates the pane (cross_workspace spawn branch), arms the report to the qualified parent address, and the stand-in report reaches a parent in another workspace. - The child's sidebar row says "Opened by <folder>/<pane>". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…at/0228-c1-cross-workspace-spawn
… feat/0228-c1-cross-workspace-spawn
…ss-parent flag, action record - MED-1: a pane's workspace_path must be absolute (or start with "~"): workspace-path-not-absolute. - MED-2: own-workspace and holder checks compare folders (dev, inode); a target is sent under the spelling a window already holds; "new" also checks the registry. Main's window lookup uses realpathSync.native (workspace-identity.ts), so a case variant cannot open one checkout in two windows. - MED-3: "Opened by" and the cross-project stand-in report follow an explicit flag set only by the cross-workspace spawn (persisted as PaneRecord.report_cross), never the shape of a name. Names holding "/" are refused by cli_open_agent (invalid-name) and the spawn gate; a user rename keeps its title but its messaging handle gets "-" for "/". - Design section 5: a cross-workspace spawn (and a refused one) is written to the window action record with who, where, what was prepared and how to undo; the target window records a diagnostic next to its existing spawn toast. ui.workspace.adopt is reachable only through cli_open_agent (ui_dedicated_tool). Merges #535 (S3) for agent_window_actions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n sys.platform CI's test_osplat ratchet (TestNoScatteredPlatformBranches) failed on macOS and Windows: _same_directory read sys.platform in mcp_server/server.py. The decision now sits behind a new osplat seam, paths_ignore_case. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…copies (S3) CI found two places the new window tools had to be recorded: - test_plugin_agent_tools_proxy: window_list / window_focus join the tools added after the pinned digest, and the digest is re-pinned because the cli_list_targets / cli_whoami descriptions now document window_id (with their old descriptions the list still hashes to the old pin). - The Registry ships byte-identical copies of agent_tools_contract.py (registry/ and registry/plugin_contract/); both now carry the window_ prefix and core names. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ge-load id Same definition as DESIGN-C S3 (#535) uses, so the two slices share one function instead of defining it twice (test_module_level_duplicate_defs). Every caller here already skips a window without an id. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…at/0228-c1-cross-workspace-spawn
… feat/0228-c1-cross-workspace-spawn
…indows - #534 and #535 each added app.main_window_sessions(); with both merged test_module_level_duplicate_defs failed. #534 now carries the window_id filter #535 used; the second copy is dropped here (C2 drops it from #535 when #534 lands). - test_plugin_agent_tools_proxy: MAIN_TOOLS_LIST_SHA256 re-pinned at this tip for cli_open_agent's description and `window` parameter (DESIGN-C S2). With main's cli_open_agent entry the digest is the previous pin. - test_a_tilde_path_is_expanded_before_it_is_used: expanduser reads USERPROFILE on Windows, so the test sets it next to HOME. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This branch has not been deployed
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.
Slice S2 of DESIGN-C:
cli_open_agentfrom a pane can open a child in another project. The caller stays its parent and chooses the window.Stacked on #534 (S1) and #535 (S3): both are merged into this branch (merges, not rebases). Merge order: #534 → #535 → this PR; this diff then shrinks to S2 only. #535 is merged in for
agent_window_actions, so the cross-workspace action record uses C2's module instead of a second copy. S2's own commits:e8c9136fbandd07921abc(review #546).Review #546 fixes (
d07921abc)workspace_pathmust be absolute (or start with~). Otherwiseworkspace-path-not-absolute, and nothing is prepared._same_directory, dev + inode)._known_spelling, from the announced held workspaces and the registry), so the window's own string match finds it."new"also refuses when the registry shows another window holding the project.findMainWindowForWorkspacenow keys onworkspaceIdentityKey(realpathSync.native, newsrc/main/workspace-identity.ts).normalizeWorkspacePathitself is unchanged, because it also produces the plugin and Plans root strings, and changing their spelling is outside this slice.spawnedCrossWorkspaceflag. Only the cross-workspace spawn sets it, and it is persisted asPaneRecord.report_cross(default false)./proves nothing.cli_open_agentrefuses names containing/(invalid-name), and so does the spawn gate (SPAWN blocks).-in place of/. Existing panes are untouched.agent_window_actions: who, target project, window choice, what was prepared, and undo steps (cli_reclaim_agent, plusworkspace_closewhen the project was adopted).agent.window-actiondiagnostic.ui.workspace.adopt:ui_invokeanswersui_dedicated_tool; it is reached only throughcli_open_agent, which checks the folder and records the action.test_an_ordinary_spawn_writes_no_recordwas green both before and after.What changed
cli_open_agent,mcp_server/server.py):workspace_pathnaming another project is now honoured. Naming its own workspace (any spelling) or omitting it takes the old path unchanged.window:"current"(default) or"new".existing+window_idis left for C2 to add after this merges, as agreed with the coordinator.workspace-not-foundinvalid-windowworkspace-in-other-window, withholding_window_id(decision (a): refuse and name the window; never duplicate, never redirect)window-cannot-holdwindow_not_readynow also carriespreparedworkspace_path,prepared(already-open|adopted|new-window),parentandwindow_id, on the cross-workspace path only.cross_workspace: trueandparent_address.App.vue):ui.workspace.adopt: adopts the project into this window's sidebar without switching; refuses a project another window holds (asks main) and refuses in a detached group window.ui.workspace.openpassesbackground.handleMcpSpawnRequest: the window holding the target creates the pane, parented on the requester, which may be in another window.settleSpawnReportsends the stand-in report to a parent in another workspace.workspace:holderIPC (title of another window holding a folder);openMainWindow(..., background)→showInactive().<folder>/<pane>" (pane.opened-byin en-US, ja-JP, zh-TW) when the parent is in another project.docs/en-US/inter-cli-messaging.md.Red → green
test_mcp_cross_workspace_spawn.py. 8 of its first 10 tests were red before the change; the other 2 are the RULE ZERO tests and are green on main too. 11/11 green now, including a later test that a group is checked against the target project.App.crossWorkspaceSpawn.test.ts. 8 of 9 were red with the App.vue change reverted; the one that passed checks that a window not holding the project stays silent. 9/9 green now.Existing tests whose expectations changed (each tied to an approved design decision)
test_plan_mcp_cli_send.py::test_tools_are_registered_without_a_ctx_argumentcli_open_agentproperties setwindowwindowparameter; the design was approved 10-21 01:00test_plan_mcp_open_agent_group.py::test_pane_group_is_validated_in_its_own_workspaceworkspace_path="/beta", which is ignored"/alpha", so the old request goes out unchanged. The test still pins "a pane's group is validated in its own workspace"workspace_pathnaming another project is no longer ignored. §7 lists this as the one intended behaviour change. The cross-project case is covered in the new filetest_plan_mcp_open_agent_group.py::test_invalid_or_foreign_group_never_dispatches[rg-missing, rg-beta, " ", " rg-alpha "] (4 cases)"/beta"ignored"/alpha", still refused withunknown-run-group"/beta"honoured, a non-existent folder would now be refused asworkspace-not-foundbefore the group check, which would hide what the test is aboutThe golden fixture
mcp_core_tools_main.jsonis updated forcli_open_agent: its description and the newwindowproperty.Local verification (targeted only), after the review fixes
-k "test_mcp_cross_workspace_spawn or test_mcp_self_scope or test_mcp_window_tools or open_agent or resume_session or test_share_mcp_tools or workspace_open or test_plan_mcp_whoami or cli_send or test_plan_mcp_ui_invoke or reclaim or pipeline or subdir_workspace or test_projects_pane" -n 2→ 747 passed, 1 skipped, fake-security 0.pnpm typecheck→ 0 errors.ruff→ clean.Local verification before the review (first push)
safe-pytest.sh <wt> -k "test_mcp_cross_workspace_spawn or test_mcp_self_scope or open_agent or resume_session or test_share_mcp_tools or workspace_open or test_plan_mcp_whoami or cli_send or test_plan_mcp_ui_invoke or reclaim or pipeline or subdir_workspace" -n 2→ 621 passed, 1 skipped (S4 TODO), fake-security calls 0.pnpm vitest runon the cross-workspace, mcpSpawnGroup, spawnKickoff, resumeSession, spawnAdvisories, uiWorkspaceSwitch and useUiActionBus test files,--maxWorkers 1→ 144/144.locales.test.ts,glossary.test.ts) → 16/16.pnpm typecheck→ 0 errors.ruff→ clean.RULE ZERO
workspace_path, or naming its own workspace, sends a byte-identicalagent_spawn.requestand gets the same answer keys. Locked bytest_naming_your_own_workspace_sends_the_same_request_as_omitting_itandtest_an_ordinary_spawn_carries_none_of_the_new_keys.test_host_uses_the_requested_workspacestill passes.ui.workspace.openwithoutbackgroundmakes the old call.Protection check
cli_open_agent(..., window="")createRequestedPane(parent: Pick<ActivePane, 'id' | 'workspacePath' | 'runGroupId'>, …): it reads only those three fieldskickoffRequestedPane(…, opts: { resume?, crossParent? })createWindow(params, opts?: { bounds?, background? })openMainWindow(workspacePath = '', background = false)_SPAWN_WINDOW_CHOICES,_cross_workspace_path,_caller_address,_holding_window_id,_in_other_window,_prepare_cross_workspace, theui.workspace.adoptcommand, theworkspace:holderIPC,ActivePaneView.openedBy.Known follow-ups (not in this slice)
window: "existing"+window_id: C2, after this merges.🤖 Generated with Claude Code