You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/09/25 08:12:36 UTC

(maka) branch main updated: refactor(desktop): move sharing dialog ownership below AppShell (#5505)

This is an automated email from the ASF dual-hosted git repository.

Astro-Han pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/main by this push:
     new b47fb74feb refactor(desktop): move sharing dialog ownership below AppShell (#5505)
b47fb74feb is described below

commit b47fb74febdcfc90e2b119abc96e3c2e1993acf3
Author: yum <ya...@chihum.dev>
AuthorDate: Fri Sep 25 16:12:31 2026 +0800

    refactor(desktop): move sharing dialog ownership below AppShell (#5505)
    
    Generated-by: Codex
---
 apps/desktop/renderer-architecture.json            |  51 +---
 .../__tests__/session-collaboration-dialog.test.ts | 326 +++++++++++++++++++++
 .../session-collaboration-join-dialog.test.ts      |   4 +
 .../session-collaboration-services-adapter.test.ts |  62 ++++
 .../session-turn-request-composer.test.ts          |   3 +
 apps/desktop/src/renderer/app-shell.tsx            |  22 +-
 .../features/session-collaboration/README.md       |  51 ++++
 .../controller/use-session-collaboration-dialog.ts |  35 +--
 .../features/session-collaboration/index.ts        |   8 +-
 .../dialog-projection.ts}                          |  35 +--
 .../features/session-collaboration/ports.ts        |  23 ++
 .../features/session-collaboration/testing.ts      |  35 +++
 .../ui/session-collaboration-dialog-root.tsx       |  39 +++
 .../ui}/session-collaboration-dialog.tsx           |  74 ++---
 .../create-session-collaboration-services.ts       |  15 +-
 .../stories/shared-session-guest.stories.tsx       |   2 +
 docs/astryx-surface-file-inventory.md              |   5 +-
 docs/astryx-surface-file-inventory.paths           |   3 +-
 scripts/check-app-shell-hooks.mjs                  |   1 -
 19 files changed, 637 insertions(+), 157 deletions(-)

diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json
index cd82bd38b3..56c539279c 100644
--- a/apps/desktop/renderer-architecture.json
+++ b/apps/desktop/renderer-architecture.json
@@ -98,7 +98,6 @@
     "src/renderer/project-path-display.ts",
     "src/renderer/remote-project-directory-dialog.tsx",
     "src/renderer/scroll-motion-policy.ts",
-    "src/renderer/session-collaboration-dialog.tsx",
     "src/renderer/session-copy-attempt.ts",
     "src/renderer/session-error-presentation.ts",
     "src/renderer/session-read-state.ts",
@@ -266,6 +265,13 @@
       "ownerSymbol": "OverlaysRoot",
       "count": 1
     },
+    {
+      "implementation": "src/renderer/features/session-collaboration/controller/use-session-collaboration-dialog.ts",
+      "symbol": "useSessionCollaborationDialog",
+      "owner": "src/renderer/features/session-collaboration/ui/session-collaboration-dialog-root.tsx",
+      "ownerSymbol": "SessionCollaborationDialogRoot",
+      "count": 1
+    },
     {
       "implementation": "src/renderer/features/task-entry/controller/use-task-entry-controller.ts",
       "symbol": "useTaskEntryController",
@@ -664,7 +670,7 @@
         "nonTriviaTokens": 1243
       },
       "src/renderer/app-shell.tsx": {
-        "importDeclarations": 59,
+        "importDeclarations": 58,
         "bridgePaths": {
           "window.maka.attachments": 1,
           "window.maka.attachments.readBytes": 1,
@@ -709,7 +715,6 @@
           "useOnboardingSnapshot": 1,
           "usePlanModeState": 1,
           "useRef": 14,
-          "useSessionCollaborationDialog": 1,
           "useSessionEventHealthPolling": 1,
           "useSessionNavigationReads": 1,
           "useSessionSettingIntent": 1,
@@ -780,7 +785,6 @@
           "./plan-mode-panel": 1,
           "./platform/desktop/default-runtime-host-operation.js": 1,
           "./scroll-motion-policy": 1,
-          "./session-collaboration-dialog": 1,
           "./session-workspace-errors": 1,
           "./settings/provider-brand-marks": 1,
           "./settings/provider-display": 1,
@@ -810,8 +814,8 @@
           "@maka/ui": 1,
           "react": 1
         },
-        "importSpecifiers": 95,
-        "nonTriviaTokens": 12663
+        "importSpecifiers": 94,
+        "nonTriviaTokens": 12649
       },
       "src/renderer/use-app-shell-composer-quotes.ts": {
         "importDeclarations": 0,
@@ -1808,41 +1812,6 @@
         "actionFactories": [],
         "dependencyPaths": {}
       },
-      "src/renderer/session-collaboration-dialog.tsx": {
-        "bridgePaths": {
-          "window.maka.localRuntimeHostRemoteAccess.getSnapshot": 2,
-          "window.maka.sessionCollaboration.decideTurnRequest": 1,
-          "window.maka.sessionCollaboration.getAccess": 1,
-          "window.maka.sessionCollaboration.getTurnRequests": 1,
-          "window.maka.sessionCollaboration.prepareInvitation": 1,
-          "window.maka.sessionCollaboration.revokeGrant": 1,
-          "window.maka.sessionCollaboration.revokePrincipal": 1
-        },
-        "environmentCapabilities": {
-          "navigator.clipboard.writeText": 1,
-          "window.clearTimeout": 1,
-          "window.setTimeout": 1
-        },
-        "hookCalls": {
-          "useEffect": 1,
-          "useState": 6,
-          "useToast": 1,
-          "useUiLocale": 1
-        },
-        "lifecycleMethods": {},
-        "unresolvedDependencies": 0,
-        "actionFactories": [],
-        "dependencyPaths": {
-          "./application/contracts/operation-diagnostics.js": 1,
-          "./features/session-collaboration": 1,
-          "./locales/session-collaboration-copy.js": 1,
-          "@astryxdesign/core": 1,
-          "@astryxdesign/core/Dialog": 1,
-          "@astryxdesign/core/Layout": 1,
-          "@maka/ui": 1,
-          "react": 1
-        }
-      },
       "src/renderer/session-copy-attempt.ts": {
         "bridgePaths": {},
         "environmentCapabilities": {
diff --git a/apps/desktop/src/main/__tests__/session-collaboration-dialog.test.ts b/apps/desktop/src/main/__tests__/session-collaboration-dialog.test.ts
new file mode 100644
index 0000000000..ebfbcb8e65
--- /dev/null
+++ b/apps/desktop/src/main/__tests__/session-collaboration-dialog.test.ts
@@ -0,0 +1,326 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import assert from 'node:assert/strict';
+import { afterEach, test } from 'node:test';
+import { act, createElement, StrictMode } from 'react';
+import { createRoot, type Root } from 'react-dom/client';
+import { parseHTML } from 'linkedom';
+import { AstryxLocaleProvider, LocaleProvider, ToastProvider } from '@maka/ui';
+import type { CollaborationAccessQueryResult, SessionTurnAccessRequest } from '@maka/runtime-host/protocol';
+import { SettingsNavigationProvider } from '../../renderer/application/contracts/settings-presentation/settings-navigation.js';
+import {
+  createFakeSessionCollaborationServices,
+  SessionCollaborationDialogRoot,
+  SessionCollaborationServicesProvider,
+  type PreparedSessionInvitation,
+  type SessionCollaborationDialogProjection,
+  type SessionCollaborationServices,
+} from '../../renderer/features/session-collaboration/testing.js';
+
+const originalGlobals = Object.fromEntries([
+  'document', 'window', 'HTMLElement', 'HTMLIFrameElement', 'Event', 'Node', 'CSS',
+  'getComputedStyle', 'matchMedia', 'requestAnimationFrame', 'cancelAnimationFrame',
+  'IS_REACT_ACT_ENVIRONMENT',
+].map((key) => [key, (globalThis as unknown as Record<string, unknown>)[key]]));
+let root: Root | undefined;
+let latest: SessionCollaborationDialogProjection;
+let frameRenders = 0;
+const settingsSections: string[] = [];
+const animationFrames = new Map<number, FrameRequestCallback>();
+let nextFrameId = 0;
+const remoteSession = { id: 'session-a', name: 'Task A', profileKind: 'remote' };
+const principal = {
+  principalId: 'guest:alice', displayName: 'Alice', status: 'pending' as const,
+  createdAt: '2026-01-01T00:00:00Z',
+};
+const invitation: PreparedSessionInvitation = {
+  invitationCode: 'one-time-code', principalId: principal.principalId,
+  expiresAt: '2099-01-01T00:00:00Z', grants: [], connectivity: { kind: 'configured' },
+};
+
+afterEach(async () => {
+  if (root) await act(async () => root?.unmount());
+  root = undefined;
+  frameRenders = 0;
+  settingsSections.length = 0;
+  animationFrames.clear();
+  Object.assign(globalThis, originalGlobals);
+});
+
+function ShellFrame({ dialog }: { dialog: SessionCollaborationDialogProjection }) {
+  latest = dialog;
+  frameRenders += 1;
+  return createElement('main', null, 'shell');
+}
+
+async function mount(overrides: Partial<SessionCollaborationServices> = {}, strict = false) {
+  const { document, window } = parseHTML('<html><body><div id="root"></div></body></html>');
+  const matchMedia = (media: string) => ({
+    matches: false, media, onchange: null, addListener() {}, removeListener() {},
+    addEventListener() {}, removeEventListener() {}, dispatchEvent: () => false,
+  });
+  const getComputedStyle = () => ({ getPropertyValue: () => '' }) as unknown as CSSStyleDeclaration;
+  Object.assign(window, { matchMedia, getComputedStyle, scrollTo() {} });
+  Object.defineProperty(window, 'maka', {
+    configurable: true,
+    get() { throw new Error('The sharing feature must use injected services'); },
+  });
+  Object.assign(window.HTMLElement.prototype, {
+    showModal(this: HTMLElement) { this.setAttribute('open', ''); },
+    close(this: HTMLElement) { this.removeAttribute('open'); },
+  });
+  Object.assign(globalThis, {
+    document, window, matchMedia, getComputedStyle,
+    HTMLElement: window.HTMLElement, HTMLIFrameElement: window.HTMLIFrameElement ?? class {},
+    Event: window.Event, Node: window.Node, CSS: { escape: (value: string) => value },
+    requestAnimationFrame: (callback: FrameRequestCallback) => {
+      const id = ++nextFrameId;
+      animationFrames.set(id, callback);
+      return id;
+    },
+    cancelAnimationFrame: (id: number) => { animationFrames.delete(id); },
+    IS_REACT_ACT_ENVIRONMENT: true,
+  });
+  const container = document.querySelector('#root');
+  assert.ok(container);
+  root = createRoot(container);
+  const feature = createElement(SessionCollaborationDialogRoot, {
+    children: (dialog) => createElement(ShellFrame, { dialog }),
+  });
+  await act(async () => {
+    root!.render(createElement(LocaleProvider, {
+      locale: 'en',
+      children: createElement(AstryxLocaleProvider, {
+        children: createElement(ToastProvider, {
+          children: createElement(SettingsNavigationProvider, {
+            navigation: { openSettingsSection: (section) => { settingsSections.push(section); } },
+            children: createElement(SessionCollaborationServicesProvider, {
+              services: createFakeSessionCollaborationServices(overrides),
+              children: strict ? createElement(StrictMode, null, feature) : feature,
+            }),
+          }),
+        }),
+      }),
+    }));
+  });
+  return document;
+}
+
+async function open(session = remoteSession) {
+  await act(async () => latest.openSession(session));
+}
+
+async function click(document: Document, label: string) {
+  const button = [...document.querySelectorAll<HTMLButtonElement>('button')]
+    .find((candidate) => candidate.textContent === label);
+  assert.ok(button, `missing button: ${label}`);
+  assert.equal(button.disabled, false, `disabled button: ${label}`);
+  await act(async () => button.click());
+  // Toast confirmation deliberately opens/resolves on the next browser frame.
+  await act(async () => {
+    const callbacks = [...animationFrames.values()];
+    animationFrames.clear();
+    for (const callback of callbacks) callback(0);
+  });
+}
+
+test('owns the dialog below the shell and keeps target changes and polling reader-local', async (t) => {
+  t.mock.timers.enable({ apis: ['setTimeout'] });
+  const reads: string[] = [];
+  const document = await mount({ getAccess: async (sessionId) => {
+    reads.push(sessionId);
+    return { principals: [], grants: [] };
+  } });
+  assert.equal(latest.isOpen, false);
+  assert.deepEqual(reads, []);
+  const opener = latest.openSession;
+  await open();
+  assert.equal(latest.isOpen, true);
+  assert.match(document.body.textContent, /Task A/u);
+  const openProjection = latest;
+  const openRenders = frameRenders;
+  await act(async () => t.mock.timers.tick(2_000));
+  assert.deepEqual(reads, ['session-a', 'session-a']);
+  assert.equal(frameRenders, openRenders, 'polling does not re-render the shell');
+  await open({ ...remoteSession, id: 'session-b', name: 'Task B' });
+  assert.equal(latest, openProjection, 'target data is not part of the shell projection');
+  assert.equal(frameRenders, openRenders);
+  assert.match(document.body.textContent, /Task B/u);
+  await click(document, 'Done');
+  assert.equal(latest.isOpen, false);
+  assert.equal(latest.openSession, opener);
+  const readCount = reads.length;
+  await act(async () => t.mock.timers.tick(10_000));
+  assert.equal(reads.length, readCount, 'closing releases the poll timer');
+});
+
+test('does not publish an old target read or restart its poll after target replacement', async (t) => {
+  t.mock.timers.enable({ apis: ['setTimeout'] });
+  let resolveOld!: (access: CollaborationAccessQueryResult) => void;
+  const reads: string[] = [];
+  const document = await mount({ getAccess: (sessionId) => {
+    reads.push(sessionId);
+    return sessionId === 'session-a'
+      ? new Promise((resolve) => { resolveOld = resolve; })
+      : Promise.resolve({ principals: [], grants: [] });
+  } });
+  await open();
+  await open({ ...remoteSession, id: 'session-b', name: 'Task B' });
+  await act(async () => resolveOld({ principals: [principal], grants: [] }));
+  assert.doesNotMatch(document.body.textContent, /Alice/u);
+  await act(async () => t.mock.timers.tick(2_000));
+  assert.deepEqual(reads, ['session-a', 'session-b', 'session-b']);
+});
+
+test('cleans up in-flight polling on unmount under StrictMode', async (t) => {
+  t.mock.timers.enable({ apis: ['setTimeout'] });
+  const resolvers: Array<(access: CollaborationAccessQueryResult) => void> = [];
+  let reads = 0;
+  await mount({ getAccess: () => {
+    reads += 1;
+    return new Promise((resolve) => resolvers.push(resolve));
+  } }, true);
+  await open();
+  assert.ok(reads > 0);
+  await act(async () => root!.unmount());
+  root = undefined;
+  const atUnmount = reads;
+  await act(async () => { for (const resolve of resolvers) resolve({ principals: [], grants: [] }); });
+  await act(async () => t.mock.timers.tick(10_000));
+  assert.equal(reads, atUnmount);
+});
+
+test('closes local sharing and opens the existing Settings destination when remote access is off', async () => {
+  const document = await mount({
+    isLocalRemoteAccessEnabled: async () => false,
+    getAccess: async () => assert.fail('local access must be enabled before querying grants'),
+    prepareInvitation: async () => assert.fail('must not prepare before enabling remote access'),
+  });
+  await open({ ...remoteSession, profileKind: 'local' });
+  await click(document, 'Create invitation');
+  assert.equal(latest.isOpen, false);
+  assert.deepEqual(settingsSections, ['projects']);
+});
+
+test('rechecks local remote access immediately before invitation creation', async () => {
+  let enabled = true;
+  const document = await mount({
+    isLocalRemoteAccessEnabled: async () => enabled,
+    prepareInvitation: async () => assert.fail('access was disabled after the projection loaded'),
+  });
+  await open({ ...remoteSession, profileKind: 'local' });
+  enabled = false;
+  await click(document, 'Create invitation');
+  assert.equal(latest.isOpen, false);
+  assert.deepEqual(settingsSections, ['projects']);
+});
+
+test('requires explicit insecure confirmation, then copies the injected invitation', async () => {
+  const prepares: unknown[][] = [];
+  const copied: string[] = [];
+  const document = await mount({
+    isLocalRemoteAccessEnabled: async () => assert.fail('remote Sessions do not use local access'),
+    getAccess: async () => ({ principals: [principal], grants: [] }),
+    prepareInvitation: async (...args) => {
+      prepares.push(args);
+      return args[2]
+        ? { kind: 'prepared', invitation }
+        : { kind: 'insecure_confirmation_required' };
+    },
+    writeInvitationClipboard: async (text) => { copied.push(text); },
+  });
+  await open();
+  const shellRenders = frameRenders;
+  await click(document, 'Create invitation');
+  assert.deepEqual(prepares, [['session-a', 'observe', false]]);
+  await click(document, 'Accept risk and create');
+  assert.deepEqual(prepares, [['session-a', 'observe', false], ['session-a', 'observe', true]]);
+  await click(document, 'Copy invitation');
+  assert.deepEqual(copied, ['one-time-code']);
+  assert.equal(frameRenders, shellRenders, 'invitation and toast updates stay below the shell');
+});
+
+test('keeps grant revocation and turn decisions bound to the dialog target', async () => {
+  const calls: unknown[][] = [];
+  const grant = {
+    grantId: 'grant-a', principalId: principal.principalId, sessionId: remoteSession.id,
+    createdAt: principal.createdAt, kind: 'session_turn_request' as const,
+  };
+  const request: SessionTurnAccessRequest = {
+    requestId: 'request-a', principalId: principal.principalId, grantId: grant.grantId,
+    intent: { sessionId: remoteSession.id, turnId: 'turn-a', content: { text: 'Please run' } },
+    createdAt: principal.createdAt, state: { kind: 'pending' },
+  };
+  const document = await mount({
+    getAccess: async () => ({ principals: [principal], grants: [grant] }),
+    getTurnRequests: async () => ({ canRequestTurns: false, requests: [request] }),
+    revokeGrant: async (...args) => { calls.push(['grant', ...args]); return { revoked: true }; },
+    revokePrincipal: async (...args) => { calls.push(['principal', ...args]); return { revoked: true }; },
+    decideTurnRequest: async (...args) => { calls.push(['decision', ...args]); return { kind: 'not_found' }; },
+  });
+  await open();
+  await click(document, 'Revoke Turn requests');
+  await click(document, 'Revoke');
+  await click(document, 'Approve');
+  await click(document, 'Reject');
+  assert.deepEqual(calls, [
+    ['grant', 'session-a', 'grant-a'], ['principal', 'session-a', 'guest:alice'],
+    ['decision', 'session-a', 'request-a', 'approve'], ['decision', 'session-a', 'request-a', 'reject'],
+  ]);
+});
+
+test('discards the previous invitation immediately when the target changes', async () => {
+  const document = await mount({
+    getAccess: async () => ({ principals: [principal], grants: [] }),
+    prepareInvitation: async () => ({ kind: 'prepared', invitation }),
+  });
+  await open();
+  await click(document, 'Create invitation');
+  assert.equal(document.querySelector('textarea')?.value, 'one-time-code');
+  await open({ ...remoteSession, id: 'session-b', name: 'Task B' });
+  assert.equal(document.querySelector('textarea') === null, true, 'a new target has no previous invitation');
+  assert.match(document.body.textContent, /Task B/u);
+});
+
+test('does not retry invitation creation when insecure confirmation is declined', async () => {
+  let attempts = 0;
+  const document = await mount({
+    prepareInvitation: async () => {
+      attempts += 1;
+      return { kind: 'insecure_confirmation_required' };
+    },
+  });
+  await open();
+  await click(document, 'Create invitation');
+  const confirmation = document.querySelector('.maka-confirm-modal');
+  assert.ok(confirmation);
+  const cancel = [...confirmation.querySelectorAll<HTMLButtonElement>('button')]
+    .find((button) => button.textContent === 'Done');
+  assert.ok(cancel);
+  await act(async () => cancel.click());
+  await act(async () => {
+    const callbacks = [...animationFrames.values()];
+    animationFrames.clear();
+    for (const callback of callbacks) callback(0);
+  });
+  assert.equal(attempts, 1);
+  assert.equal(latest.isOpen, true);
+  assert.equal(document.querySelector('textarea') === null, true, 'declining does not create an invitation');
+});
diff --git a/apps/desktop/src/main/__tests__/session-collaboration-join-dialog.test.ts b/apps/desktop/src/main/__tests__/session-collaboration-join-dialog.test.ts
index ce48d83b32..3781becb98 100644
--- a/apps/desktop/src/main/__tests__/session-collaboration-join-dialog.test.ts
+++ b/apps/desktop/src/main/__tests__/session-collaboration-join-dialog.test.ts
@@ -27,6 +27,7 @@ import {
   SessionCollaborationJoinDialog,
   SessionCollaborationNavigation,
   SessionCollaborationServicesProvider,
+  createFakeSessionCollaborationServices,
   type SessionCollaborationServices,
 } from '../../renderer/features/session-collaboration/testing.js';
 
@@ -61,6 +62,7 @@ afterEach(async () => {
 test('keeps loading progress visible while an irreversible import settles', async () => {
   let reportProgress: Parameters<SessionCollaborationServices['importInvitation']>[1];
   const services: SessionCollaborationServices = {
+    ...createFakeSessionCollaborationServices(),
     importInvitation: async (_input, onProgress) => {
       reportProgress = onProgress;
       return new Promise(() => undefined);
@@ -125,6 +127,7 @@ test('closes as a retained background recovery instead of reporting a failed joi
   let imported = 0;
   let closed = 0;
   const services: SessionCollaborationServices = {
+    ...createFakeSessionCollaborationServices(),
     importInvitation: async () => ({ kind: 'recovering', mountId: 'shared-1' }),
     cancelImport: async () => 'settling',
     readInvitationClipboard: async () => '',
@@ -186,6 +189,7 @@ test('identifies a retained shared task and its selected peer transport', async
   let retried: string | undefined;
   let opened: string | undefined;
   const services: SessionCollaborationServices = {
+    ...createFakeSessionCollaborationServices(),
     importInvitation: async () => ({ kind: 'error', reason: 'incompatible_host', message: 'raw compatibility details' }),
     cancelImport: async () => 'settling',
     readInvitationClipboard: async () => '',
diff --git a/apps/desktop/src/main/__tests__/session-collaboration-services-adapter.test.ts b/apps/desktop/src/main/__tests__/session-collaboration-services-adapter.test.ts
new file mode 100644
index 0000000000..554e116481
--- /dev/null
+++ b/apps/desktop/src/main/__tests__/session-collaboration-services-adapter.test.ts
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import assert from 'node:assert/strict';
+import { test } from 'node:test';
+import {
+  createDesktopSessionCollaborationServices,
+  type DesktopSessionCollaborationBridge,
+} from '../../renderer/platform/desktop/create-session-collaboration-services.js';
+
+test('preserves sharing bridge arguments and adapts remote access and clipboard capabilities', async () => {
+  const calls: Array<{ name: string; args: unknown[] }> = [];
+  const result = { marker: 'bridge-result' };
+  let state = 'off';
+  const sessionCollaboration = new Proxy({}, {
+    get: (_target, property) => (...args: unknown[]) => {
+      calls.push({ name: String(property), args });
+      return Promise.resolve(result);
+    },
+  });
+  const services = createDesktopSessionCollaborationServices({
+    sessionCollaboration,
+    localRuntimeHostRemoteAccess: { getSnapshot: async () => ({ state }) },
+  } as unknown as DesktopSessionCollaborationBridge, {
+    writeText: async (text) => { calls.push({ name: 'clipboard', args: [text] }); },
+  });
+  assert.equal(await services.isLocalRemoteAccessEnabled(), false);
+  state = 'on';
+  assert.equal(await services.isLocalRemoteAccessEnabled(), true);
+  assert.equal(await services.getAccess('session-a'), result);
+  assert.equal(await services.prepareInvitation('session-a', 'request_turn', false), result);
+  assert.equal(await services.prepareInvitation('session-a', 'request_turn', true), result);
+  assert.equal(await services.revokeGrant('session-a', 'grant-a'), result);
+  assert.equal(await services.revokePrincipal('session-a', 'principal-a'), result);
+  assert.equal(await services.decideTurnRequest('session-a', 'request-a', 'approve'), result);
+  await services.writeInvitationClipboard('invitation-code');
+  assert.deepEqual(calls, [
+    { name: 'getAccess', args: ['session-a'] },
+    { name: 'prepareInvitation', args: ['session-a', 'request_turn', false] },
+    { name: 'prepareInvitation', args: ['session-a', 'request_turn', true] },
+    { name: 'revokeGrant', args: ['session-a', 'grant-a'] },
+    { name: 'revokePrincipal', args: ['session-a', 'principal-a'] },
+    { name: 'decideTurnRequest', args: ['session-a', 'request-a', 'approve'] },
+    { name: 'clipboard', args: ['invitation-code'] },
+  ]);
+});
diff --git a/apps/desktop/src/main/__tests__/session-turn-request-composer.test.ts b/apps/desktop/src/main/__tests__/session-turn-request-composer.test.ts
index b52d950552..71c5d23280 100644
--- a/apps/desktop/src/main/__tests__/session-turn-request-composer.test.ts
+++ b/apps/desktop/src/main/__tests__/session-turn-request-composer.test.ts
@@ -26,6 +26,7 @@ import { AstryxLocaleProvider, LocaleProvider, ToastProvider } from '@maka/ui';
 import type { SessionTurnAccessRequest } from '@maka/runtime-host/protocol';
 import {
   SessionCollaborationServicesProvider,
+  createFakeSessionCollaborationServices,
   SessionTurnRequestComposer,
   type SessionCollaborationServices,
 } from '../../renderer/features/session-collaboration/testing.js';
@@ -79,6 +80,7 @@ test('keeps a newer Guest draft across remount when an old request settles later
     state: { kind: 'pending' },
   };
   const services: SessionCollaborationServices = {
+    ...createFakeSessionCollaborationServices(),
     importInvitation: async () => ({ kind: 'connected', mountId: 'unused' }),
     cancelImport: async () => 'cancelled',
     readInvitationClipboard: async () => '',
@@ -174,6 +176,7 @@ test('resumes an in-flight Guest request across remount without submitting it tw
     state: { kind: 'pending' },
   };
   const services: SessionCollaborationServices = {
+    ...createFakeSessionCollaborationServices(),
     importInvitation: async () => ({ kind: 'connected', mountId: 'unused' }),
     cancelImport: async () => 'cancelled',
     readInvitationClipboard: async () => '',
diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx
index 841687800a..3485cffc30 100644
--- a/apps/desktop/src/renderer/app-shell.tsx
+++ b/apps/desktop/src/renderer/app-shell.tsx
@@ -93,8 +93,8 @@ import * as TaskEntry from './features/task-entry';
 import type { TaskEntryShellProjection } from './features/task-entry';
 import * as Overlays from './features/overlays/index.js';
 import type { OverlaysShellProjection } from './features/overlays/index.js';
-import { SessionCollaborationDialog } from './session-collaboration-dialog';
 import * as SessionCollaboration from './features/session-collaboration';
+import type { SessionCollaborationDialogProjection } from './features/session-collaboration';
 import { NEW_TASK_PENDING_KEY } from './pending-items';
 import {
   desktopSlashCommandAvailability,
@@ -210,7 +210,6 @@ type ComposerImportOwner = {
  * assistant stream slot when the primary post-commit signal is missed.
  */
 const SETTLE_FALLBACK_GRACE_MS = 1000;
-const { useSessionCollaborationDialog } = SessionCollaboration;
 export function AppShell() {
   const [uiLocalePreference, setUiLocalePreference] = useState<UiLocalePreference>('auto');
   const [uiLocaleOverride, setUiLocaleOverride] = useState<UiLocale | null>(null);
@@ -248,9 +247,13 @@ export function AppShell() {
                 {(taskEntry) => (
                   <Overlays.OverlaysRoot>
                     {(overlays) => (
-                      <AppShellContent
-                        {...{ taskEntry, overlays, uiLocale, uiLocaleOverride, setUiLocaleOverride, setUiLocalePreference }}
-                      />
+                      <SessionCollaboration.SessionCollaborationDialogRoot>
+                        {(sharedSessionDialog) => (
+                          <AppShellContent
+                            {...{ taskEntry, overlays, sharedSessionDialog, uiLocale, uiLocaleOverride, setUiLocaleOverride, setUiLocalePreference }}
+                          />
+                        )}
+                      </SessionCollaboration.SessionCollaborationDialogRoot>
                     )}
                   </Overlays.OverlaysRoot>
                 )}
@@ -277,6 +280,7 @@ const SESSION_RAIL = <SessionListPanel />;
 function AppShellContent({
   taskEntry,
   overlays,
+  sharedSessionDialog,
   uiLocale,
   uiLocaleOverride,
   setUiLocaleOverride,
@@ -284,13 +288,13 @@ function AppShellContent({
 }: {
   taskEntry: TaskEntryShellProjection;
   overlays: OverlaysShellProjection;
+  sharedSessionDialog: SessionCollaborationDialogProjection;
   uiLocale: UiLocale;
   uiLocaleOverride: UiLocale | null;
   setUiLocaleOverride: Dispatch<SetStateAction<UiLocale | null>>;
   setUiLocalePreference: Dispatch<SetStateAction<UiLocalePreference>>;
 }) {
   const toastApi = useToast();
-  const sharedSessionDialog = useSessionCollaborationDialog();
   const previousInterruptionShownRef = useRef(false);
   const {
     authoritativeSessionIds,
@@ -2637,12 +2641,6 @@ function AppShellContent({
       <Goals.GoalHost />
       <TaskEntry.TaskEntryHost />
       <RuntimeHostSshTerminalDialog />
-      <SessionCollaborationDialog
-        target={sharedSessionDialog.target}
-        onOpenRemoteAccessSettings={() => openSettingsSection('projects')}
-        onClose={sharedSessionDialog.close}
-      />
-
       <AppShellOverlays
         closeSettings={closeSettings}
         themePref={themePref}
diff --git a/apps/desktop/src/renderer/features/session-collaboration/README.md b/apps/desktop/src/renderer/features/session-collaboration/README.md
new file mode 100644
index 0000000000..1e861ee4e5
--- /dev/null
+++ b/apps/desktop/src/renderer/features/session-collaboration/README.md
@@ -0,0 +1,51 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+# Session collaboration
+
+This slice owns sharing and joining Sessions, Guest access management, and
+Owner approval of Guest Turn requests.
+
+## Sharing dialog ownership
+
+`SessionCollaborationDialogRoot` is the only production owner of
+`useSessionCollaborationDialog`, registered in `controllerOwners`. The public
+entry exports the root and its narrow shell projection, never the controller.
+
+The root owns the target and mounts the sharing dialog. The shell receives a
+stable `openSession` command, the localized action label, and `isOpen` for its
+existing modal coordination. Target changes and dialog polling, invitations,
+and access mutations do not invalidate that projection. Opening and closing
+still update the shell because its shortcuts and Workbar need modal visibility.
+
+A dialog instance belongs to one target Session. Changing targets remounts it,
+clears the previous invitation and access state, and releases its polling loop.
+Closing or unmounting ignores late projection reads and stops polling.
+
+The dialog obtains its capabilities from `SessionCollaborationServices`.
+The Desktop adapter owns bridge and clipboard access. Remote access is checked
+before reading local sharing controls and again before creating an invitation;
+unencrypted connections still require explicit confirmation. Opening remote
+access Settings closes sharing first and uses the shared Settings navigation
+contract to select `projects`.
+
+`testing.ts` provides injectable fake services. The sharing tests mount the
+production root and dialog without Electron, exercising modal projection,
+polling cleanup, invitation confirmation/copying, access revocation, and Turn
+approval/rejection. Session settings and Plan ownership are separate work.
diff --git a/apps/desktop/src/renderer/features/session-collaboration/controller/use-session-collaboration-dialog.ts b/apps/desktop/src/renderer/features/session-collaboration/controller/use-session-collaboration-dialog.ts
index d01c3109cd..ddca8e7f03 100644
--- a/apps/desktop/src/renderer/features/session-collaboration/controller/use-session-collaboration-dialog.ts
+++ b/apps/desktop/src/renderer/features/session-collaboration/controller/use-session-collaboration-dialog.ts
@@ -17,36 +17,33 @@
  * under the License.
  */
 
-import { useState } from 'react';
+import { useCallback, useMemo, useState } from 'react';
 import { useUiLocale } from '@maka/ui';
 import { getSessionCollaborationCopy } from '../../../locales/session-collaboration-copy.js';
 
-export interface SessionCollaborationDialogTarget {
-  readonly sessionId: string;
-  readonly sessionName: string;
-  readonly requiresRemoteAccess: boolean;
-}
+import type {
+  SessionCollaborationDialogProjection,
+  SessionCollaborationDialogTarget,
+} from '../model/dialog-projection.js';
 
 export function useSessionCollaborationDialog() {
   const [target, setTarget] = useState<SessionCollaborationDialogTarget>();
   const shareActionLabel = getSessionCollaborationCopy(useUiLocale()).shareAction;
-
-  return {
-    target,
-    shareActionLabel,
-    isOpen: target !== undefined,
-    open: setTarget,
-    openSession(session: {
-      readonly id: string;
-      readonly name: string;
-      readonly profileKind: string;
-    }) {
+  const openSession = useCallback<SessionCollaborationDialogProjection['openSession']>(
+    (session) => {
       setTarget({
         sessionId: session.id,
         sessionName: session.name,
         requiresRemoteAccess: session.profileKind === 'local',
       });
     },
-    close: () => setTarget(undefined),
-  };
+    [],
+  );
+  const close = useCallback(() => setTarget(undefined), []);
+  const isOpen = target !== undefined;
+  const shell = useMemo<SessionCollaborationDialogProjection>(
+    () => ({ isOpen, shareActionLabel, openSession }),
+    [isOpen, shareActionLabel, openSession],
+  );
+  return { target, close, shell };
 }
diff --git a/apps/desktop/src/renderer/features/session-collaboration/index.ts b/apps/desktop/src/renderer/features/session-collaboration/index.ts
index 762e7fa58d..2f63e696aa 100644
--- a/apps/desktop/src/renderer/features/session-collaboration/index.ts
+++ b/apps/desktop/src/renderer/features/session-collaboration/index.ts
@@ -18,17 +18,13 @@
  */
 
 export { SessionCollaborationServicesProvider } from './services-context';
-export { useSessionCollaborationDialog } from './controller/use-session-collaboration-dialog';
+export { SessionCollaborationDialogRoot } from './ui/session-collaboration-dialog-root.js';
+export type { SessionCollaborationDialogProjection } from './model/dialog-projection.js';
 export { SessionCollaborationJoinDialog } from './ui/session-collaboration-join-dialog';
 export { SessionCollaborationNavigation } from './ui/session-collaboration-navigation';
-export { SessionGuestAliasAction } from './ui/session-collaboration-alias-action';
 export { SessionGuestTurnActionBoundary } from './ui/session-guest-turn-actions';
 export { SessionTurnRequestComposer } from './ui/session-turn-request-composer';
 export { SessionTurnRequestApprovalForSession } from './ui/session-turn-request-approval';
 export { SessionTurnRequestBadge } from './ui/session-turn-request-badge';
 export { SessionTurnRequestInboxProvider } from './turn-request-inbox-context';
-export {
-  describeTurnRequestIntent,
-  turnRequestStateLabel,
-} from './model/turn-request-inbox';
 export type { SessionCollaborationServices } from './ports';
diff --git a/apps/desktop/src/renderer/features/session-collaboration/controller/use-session-collaboration-dialog.ts b/apps/desktop/src/renderer/features/session-collaboration/model/dialog-projection.ts
similarity index 54%
copy from apps/desktop/src/renderer/features/session-collaboration/controller/use-session-collaboration-dialog.ts
copy to apps/desktop/src/renderer/features/session-collaboration/model/dialog-projection.ts
index d01c3109cd..37d52b06db 100644
--- a/apps/desktop/src/renderer/features/session-collaboration/controller/use-session-collaboration-dialog.ts
+++ b/apps/desktop/src/renderer/features/session-collaboration/model/dialog-projection.ts
@@ -17,36 +17,19 @@
  * under the License.
  */
 
-import { useState } from 'react';
-import { useUiLocale } from '@maka/ui';
-import { getSessionCollaborationCopy } from '../../../locales/session-collaboration-copy.js';
-
 export interface SessionCollaborationDialogTarget {
   readonly sessionId: string;
   readonly sessionName: string;
   readonly requiresRemoteAccess: boolean;
 }
 
-export function useSessionCollaborationDialog() {
-  const [target, setTarget] = useState<SessionCollaborationDialogTarget>();
-  const shareActionLabel = getSessionCollaborationCopy(useUiLocale()).shareAction;
-
-  return {
-    target,
-    shareActionLabel,
-    isOpen: target !== undefined,
-    open: setTarget,
-    openSession(session: {
-      readonly id: string;
-      readonly name: string;
-      readonly profileKind: string;
-    }) {
-      setTarget({
-        sessionId: session.id,
-        sessionName: session.name,
-        requiresRemoteAccess: session.profileKind === 'local',
-      });
-    },
-    close: () => setTarget(undefined),
-  };
+/** The shell needs modal coordination and an opener, but not the target. */
+export interface SessionCollaborationDialogProjection {
+  readonly isOpen: boolean;
+  readonly shareActionLabel: string;
+  openSession(session: {
+    readonly id: string;
+    readonly name: string;
+    readonly profileKind: string;
+  }): void;
 }
diff --git a/apps/desktop/src/renderer/features/session-collaboration/ports.ts b/apps/desktop/src/renderer/features/session-collaboration/ports.ts
index a432698832..b3eafbe324 100644
--- a/apps/desktop/src/renderer/features/session-collaboration/ports.ts
+++ b/apps/desktop/src/renderer/features/session-collaboration/ports.ts
@@ -24,6 +24,10 @@ import type {
   SessionCollaborationMountSummary,
 } from '../../../shared/session-collaboration.js';
 import type {
+  CollaborationAccessQueryResult,
+  CollaborationInvitationPrepareResult,
+  CollaborationGrantRevokeResult,
+  CollaborationPrincipalRevokeResult,
   CollaborationTurnRequestAcknowledgeResult,
   CollaborationTurnRequestDecideResult,
   CollaborationTurnRequestQueryResult,
@@ -31,6 +35,12 @@ import type {
   SessionTurnAccessRequest,
 } from '@maka/runtime-host/protocol';
 
+export type PreparedSessionInvitation = CollaborationInvitationPrepareResult & {
+  readonly connectivity:
+    | { readonly kind: 'peer'; readonly coordinationRelayCount: number }
+    | { readonly kind: 'configured' };
+};
+
 export type {
   SessionCollaborationCancelResult,
   SessionCollaborationImportPhase,
@@ -39,6 +49,19 @@ export type {
 } from '../../../shared/session-collaboration.js';
 
 export interface SessionCollaborationServices {
+  isLocalRemoteAccessEnabled(): Promise<boolean>;
+  getAccess(sessionId: string): Promise<CollaborationAccessQueryResult>;
+  prepareInvitation(
+    sessionId: string,
+    preset: 'observe' | 'request_turn',
+    allowInsecure: boolean,
+  ): Promise<
+    | { readonly kind: 'prepared'; readonly invitation: PreparedSessionInvitation }
+    | { readonly kind: 'insecure_confirmation_required' }
+  >;
+  revokeGrant(sessionId: string, grantId: string): Promise<CollaborationGrantRevokeResult>;
+  revokePrincipal(sessionId: string, principalId: string): Promise<CollaborationPrincipalRevokeResult>;
+  writeInvitationClipboard(text: string): Promise<void>;
   importInvitation(input: {
     readonly code: string;
     readonly allowInsecure: boolean;
diff --git a/apps/desktop/src/renderer/features/session-collaboration/testing.ts b/apps/desktop/src/renderer/features/session-collaboration/testing.ts
index 0768269e19..4e9b235b13 100644
--- a/apps/desktop/src/renderer/features/session-collaboration/testing.ts
+++ b/apps/desktop/src/renderer/features/session-collaboration/testing.ts
@@ -17,6 +17,8 @@
  * under the License.
  */
 
+import type { SessionCollaborationServices } from './ports.js';
+
 export {
   groupPendingTurnRequests,
   describeOwnerTurnRequestIntent,
@@ -31,3 +33,36 @@ export { SessionCollaborationJoinDialog } from './ui/session-collaboration-join-
 export { SessionCollaborationNavigation } from './ui/session-collaboration-navigation.js';
 export { SessionTurnRequestComposer } from './ui/session-turn-request-composer.js';
 export { sessionCollaborationImportErrorMessage } from './ui/session-collaboration-join-dialog.js';
+export { SessionCollaborationDialogRoot } from './ui/session-collaboration-dialog-root.js';
+export type { SessionCollaborationDialogProjection } from './model/dialog-projection.js';
+export type { PreparedSessionInvitation } from './ports.js';
+
+export function createFakeSessionCollaborationServices(
+  overrides: Partial<SessionCollaborationServices> = {},
+): SessionCollaborationServices {
+  return {
+    isLocalRemoteAccessEnabled: async () => true,
+    getAccess: async () => ({ principals: [], grants: [] }),
+    prepareInvitation: async () => { throw new Error('Fake prepareInvitation is not configured'); },
+    revokeGrant: async () => ({ revoked: true }),
+    revokePrincipal: async () => ({ revoked: true }),
+    writeInvitationClipboard: async () => undefined,
+    importInvitation: async () => { throw new Error('Fake importInvitation is not configured'); },
+    cancelImport: async () => 'cancelled',
+    readInvitationClipboard: async () => '',
+    listMounts: async () => [],
+    subscribeMountChanges: () => () => undefined,
+    removeMount: async () => undefined,
+    retryMount: async () => undefined,
+    renameMount: async () => undefined,
+    renamePrincipal: async () => ({ renamed: true }),
+    requestTurn: async () => { throw new Error('Fake requestTurn is not configured'); },
+    getTurnRequests: async () => ({ canRequestTurns: false, requests: [] }),
+    acknowledgeTurnRequest: async () => ({ acknowledged: false }),
+    withdrawTurnRequest: async () => ({ withdrawn: false }),
+    getPendingTurnRequests: async () => [],
+    decideTurnRequest: async () => ({ kind: 'not_found' }),
+    createOperationId: () => 'operation-1',
+    ...overrides,
+  };
+}
diff --git a/apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-dialog-root.tsx b/apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-dialog-root.tsx
new file mode 100644
index 0000000000..431b571d91
--- /dev/null
+++ b/apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-dialog-root.tsx
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useMemo, type ReactNode } from 'react';
+import { useSessionCollaborationDialog } from '../controller/use-session-collaboration-dialog.js';
+import type { SessionCollaborationDialogProjection } from '../model/dialog-projection.js';
+import { SessionCollaborationDialog } from './session-collaboration-dialog.js';
+
+/** Owns the sharing target; the shell reads only the modal-open projection. */
+export function SessionCollaborationDialogRoot(props: {
+  readonly children: (dialog: SessionCollaborationDialogProjection) => ReactNode;
+}) {
+  const { target, close, shell } = useSessionCollaborationDialog();
+  const frame = useMemo(() => props.children(shell), [props.children, shell]);
+  return (
+    <>
+      {frame}
+      {target ? (
+        <SessionCollaborationDialog key={target.sessionId} {...target} onClose={close} />
+      ) : null}
+    </>
+  );
+}
diff --git a/apps/desktop/src/renderer/session-collaboration-dialog.tsx b/apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-dialog.tsx
similarity index 87%
rename from apps/desktop/src/renderer/session-collaboration-dialog.tsx
rename to apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-dialog.tsx
index 99f4305907..811537f807 100644
--- a/apps/desktop/src/renderer/session-collaboration-dialog.tsx
+++ b/apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-dialog.tsx
@@ -30,73 +30,52 @@ import {
   useToast,
   useUiLocale,
 } from '@maka/ui';
-import { reportUnexpectedError } from './application/contracts/operation-diagnostics.js';
+import { reportUnexpectedError } from '../../../application/contracts/operation-diagnostics.js';
+import { useSettingsNavigation } from '../../../application/contracts/settings-presentation/settings-navigation.js';
 import type {
   CollaborationAccessQueryResult,
-  CollaborationInvitationPrepareResult,
   SessionCollaborationGrant,
   SessionTurnAccessRequest,
 } from '@maka/runtime-host/protocol';
-import { getSessionCollaborationCopy } from './locales/session-collaboration-copy.js';
+import { getSessionCollaborationCopy } from '../../../locales/session-collaboration-copy.js';
 import {
   describeTurnRequestIntent,
   turnRequestStateLabel,
-  SessionGuestAliasAction,
-} from './features/session-collaboration';
+} from '../model/turn-request-inbox.js';
+import { SessionGuestAliasAction } from './session-collaboration-alias-action.js';
+import { useSessionCollaborationServices } from '../services-context.js';
+import type { PreparedSessionInvitation } from '../ports.js';
+import type { SessionCollaborationDialogTarget } from '../model/dialog-projection.js';
 
-type Props = {
-  readonly target?: {
-    readonly sessionId: string;
-    readonly sessionName: string;
-    readonly requiresRemoteAccess: boolean;
-  };
-  readonly onOpenRemoteAccessSettings: () => void;
+type Props = SessionCollaborationDialogTarget & {
   readonly onClose: () => void;
 };
 
-type ShareSessionDialogProps = NonNullable<Props['target']> & Omit<Props, 'target'>;
-
 type CollaborationAuthorityState =
   | 'loading'
   | 'available'
   | 'remote_access_off'
   | 'unavailable';
 
-type PreparedInvitation = CollaborationInvitationPrepareResult & {
-  readonly connectivity:
-    | { readonly kind: 'peer'; readonly coordinationRelayCount: number }
-    | { readonly kind: 'configured' };
-};
-
 export function SessionCollaborationDialog(props: Props) {
-  if (!props.target) return null;
-  return (
-    <ShareSessionDialog
-      {...props.target}
-      onOpenRemoteAccessSettings={props.onOpenRemoteAccessSettings}
-      onClose={props.onClose}
-    />
-  );
-}
-
-function ShareSessionDialog(props: ShareSessionDialogProps) {
+  const services = useSessionCollaborationServices();
+  const settingsNavigation = useSettingsNavigation();
   const copy = getSessionCollaborationCopy(useUiLocale());
   const toast = useToast();
   const [preset, setPreset] = useState<'observe' | 'request_turn'>('observe');
   const [access, setAccess] = useState<CollaborationAccessQueryResult>();
-  const [invitation, setInvitation] = useState<PreparedInvitation>();
+  const [invitation, setInvitation] = useState<PreparedSessionInvitation>();
   const [turnRequests, setTurnRequests] = useState<readonly SessionTurnAccessRequest[]>();
   const [authorityState, setAuthorityState] = useState<CollaborationAuthorityState>('loading');
   const [working, setWorking] = useState(false);
 
   async function readProjection() {
     if (props.requiresRemoteAccess) {
-      const remoteAccess = await window.maka.localRuntimeHostRemoteAccess.getSnapshot();
-      if (remoteAccess.state !== 'on') return { kind: 'remote_access_off' } as const;
+      if (!(await services.isLocalRemoteAccessEnabled())) return { kind: 'remote_access_off' } as const;
     }
     const [nextAccess, nextRequests] = await Promise.all([
-      window.maka.sessionCollaboration.getAccess(props.sessionId),
-      window.maka.sessionCollaboration.getTurnRequests(props.sessionId),
+      services.getAccess(props.sessionId),
+      services.getTurnRequests(props.sessionId),
     ]);
     return {
       kind: 'available',
@@ -132,7 +111,7 @@ function ShareSessionDialog(props: ShareSessionDialogProps) {
 
   useEffect(() => {
     let disposed = false;
-    let timer: number | undefined;
+    let timer: ReturnType<typeof setTimeout> | undefined;
     const poll = async () => {
       try {
         const projection = await readProjection();
@@ -140,15 +119,15 @@ function ShareSessionDialog(props: ShareSessionDialogProps) {
       } catch {
         if (!disposed) setAuthorityState('unavailable');
       } finally {
-        if (!disposed) timer = window.setTimeout(() => void poll(), 2_000);
+        if (!disposed) timer = setTimeout(() => void poll(), 2_000);
       }
     };
     void poll();
     return () => {
       disposed = true;
-      if (timer !== undefined) window.clearTimeout(timer);
+      if (timer !== undefined) clearTimeout(timer);
     };
-  }, [props.sessionId, props.requiresRemoteAccess]);
+  }, [props.sessionId, props.requiresRemoteAccess, services]);
 
   async function createInvitation(allowInsecure = false): Promise<void> {
     setWorking(true);
@@ -159,13 +138,12 @@ function ShareSessionDialog(props: ShareSessionDialogProps) {
       }
       if (authorityState !== 'available') return;
       if (props.requiresRemoteAccess) {
-        const access = await window.maka.localRuntimeHostRemoteAccess.getSnapshot();
-        if (access.state !== 'on') {
+        if (!(await services.isLocalRemoteAccessEnabled())) {
           openRemoteAccessSettings();
           return;
         }
       }
-      const created = await window.maka.sessionCollaboration.prepareInvitation(
+      const created = await services.prepareInvitation(
         props.sessionId,
         preset,
         allowInsecure,
@@ -193,13 +171,13 @@ function ShareSessionDialog(props: ShareSessionDialogProps) {
   function openRemoteAccessSettings(): void {
     props.onClose();
     toast.info(copy.enableRemoteAccessTitle, copy.enableRemoteAccessBody);
-    props.onOpenRemoteAccessSettings();
+    settingsNavigation.openSettingsSection('projects');
   }
 
   async function copyInvitation(): Promise<void> {
     if (!invitation) return;
     try {
-      await navigator.clipboard.writeText(invitation.invitationCode);
+      await services.writeInvitationClipboard(invitation.invitationCode);
       toast.success(copy.copied);
     } catch (error) {
       reportUnexpectedError('session-collaboration:copy', error);
@@ -210,7 +188,7 @@ function ShareSessionDialog(props: ShareSessionDialogProps) {
   async function revokePrincipal(principalId: string): Promise<void> {
     setWorking(true);
     try {
-      await window.maka.sessionCollaboration.revokePrincipal(props.sessionId, principalId);
+      await services.revokePrincipal(props.sessionId, principalId);
       await refresh();
     } catch (error) {
       reportUnexpectedError('session-collaboration:revoke-principal', error);
@@ -223,7 +201,7 @@ function ShareSessionDialog(props: ShareSessionDialogProps) {
   async function revokeGrant(grant: SessionCollaborationGrant): Promise<void> {
     setWorking(true);
     try {
-      await window.maka.sessionCollaboration.revokeGrant(
+      await services.revokeGrant(
         props.sessionId,
         grant.grantId,
       );
@@ -242,7 +220,7 @@ function ShareSessionDialog(props: ShareSessionDialogProps) {
   ): Promise<void> {
     setWorking(true);
     try {
-      await window.maka.sessionCollaboration.decideTurnRequest(
+      await services.decideTurnRequest(
         props.sessionId,
         request.requestId,
         decision,
diff --git a/apps/desktop/src/renderer/platform/desktop/create-session-collaboration-services.ts b/apps/desktop/src/renderer/platform/desktop/create-session-collaboration-services.ts
index 3f25ebb682..92073d7a10 100644
--- a/apps/desktop/src/renderer/platform/desktop/create-session-collaboration-services.ts
+++ b/apps/desktop/src/renderer/platform/desktop/create-session-collaboration-services.ts
@@ -22,13 +22,26 @@ import type { SessionCollaborationServices } from '../../features/session-collab
 
 export type DesktopSessionCollaborationBridge = Pick<
   MakaBridge,
-  'sessionCollaboration'
+  'sessionCollaboration' | 'localRuntimeHostRemoteAccess'
 >;
 
 export function createDesktopSessionCollaborationServices(
   bridge: DesktopSessionCollaborationBridge = window.maka,
+  clipboard: Pick<Clipboard, 'writeText'> = {
+    writeText: (text) => navigator.clipboard.writeText(text),
+  },
 ): SessionCollaborationServices {
   return {
+    isLocalRemoteAccessEnabled: async () =>
+      (await bridge.localRuntimeHostRemoteAccess.getSnapshot()).state === 'on',
+    getAccess: (sessionId) => bridge.sessionCollaboration.getAccess(sessionId),
+    prepareInvitation: (sessionId, preset, allowInsecure) =>
+      bridge.sessionCollaboration.prepareInvitation(sessionId, preset, allowInsecure),
+    revokeGrant: (sessionId, grantId) =>
+      bridge.sessionCollaboration.revokeGrant(sessionId, grantId),
+    revokePrincipal: (sessionId, principalId) =>
+      bridge.sessionCollaboration.revokePrincipal(sessionId, principalId),
+    writeInvitationClipboard: (text) => clipboard.writeText(text),
     importInvitation: (input, onProgress) =>
       bridge.sessionCollaboration.importInvitation(input, onProgress),
     cancelImport: (operationId) => bridge.sessionCollaboration.cancelImport(operationId),
diff --git a/apps/desktop/stories/shared-session-guest.stories.tsx b/apps/desktop/stories/shared-session-guest.stories.tsx
index 9687141135..4ecdc55a2f 100644
--- a/apps/desktop/stories/shared-session-guest.stories.tsx
+++ b/apps/desktop/stories/shared-session-guest.stories.tsx
@@ -25,6 +25,7 @@ import {
   SessionTurnRequestComposer,
   type SessionCollaborationServices,
 } from '../src/renderer/features/session-collaboration/index.js';
+import { createFakeSessionCollaborationServices } from '../src/renderer/features/session-collaboration/testing.js';
 
 const SESSION_ID = 'shared-session-story';
 const REQUESTS: readonly SessionTurnAccessRequest[] = [
@@ -70,6 +71,7 @@ function services(
   ) => Promise<{ readonly withdrawn: boolean }> = async () => ({ withdrawn: true }),
 ): SessionCollaborationServices {
   return {
+    ...createFakeSessionCollaborationServices(),
     importInvitation: async () => {
       throw new Error('unused');
     },
diff --git a/docs/astryx-surface-file-inventory.md b/docs/astryx-surface-file-inventory.md
index 87be1d8f3e..da88e64fcf 100644
--- a/docs/astryx-surface-file-inventory.md
+++ b/docs/astryx-surface-file-inventory.md
@@ -6,7 +6,7 @@ Generated against `@astryxdesign/core@0.6.2` (195 component exports).
 
 Wiki bar: Design Conventions · API Use-the-System · Theming · Container Padding.
 
-**Totals:** 297 files — blocker 0, reimplementation 0, polish 4, aligned 293.
+**Totals:** 298 files — blocker 0, reimplementation 0, polish 4, aligned 294.
 
 ## Exclusions (explicit)
 
@@ -95,6 +95,8 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi
 | `apps/desktop/src/renderer/features/session-collaboration/services-context.tsx` | shell-chrome-or-panel | none | aligned — no raw controls; no Astryx JSX usage | aligned |
 | `apps/desktop/src/renderer/features/session-collaboration/turn-request-inbox-context.tsx` | shell-chrome-or-panel | none | aligned — no raw controls; no Astryx JSX usage | aligned |
 | `apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-alias-action.tsx` | shell-chrome-or-panel | Button, Dialog, DialogHeader, Layout, LayoutContent, LayoutFooter, TextInput | aligned — uses Astryx (Button, Dialog, DialogHeader, Layout, LayoutContent, LayoutFooter, TextInput) | aligned |
+| `apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-dialog-root.tsx` | dialog-overlay | none | aligned — no raw controls; no Astryx JSX usage | aligned |
+| `apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-dialog.tsx` | dialog-overlay | Banner, Button, Dialog, DialogHeader, FormLayout, Layout, LayoutContent, LayoutFooter, SegmentedControl, SegmentedControlItem, Text, TextArea | aligned — uses Astryx (Banner, Button, Dialog, DialogHeader, FormLayout, Layout, LayoutContent, LayoutFooter) | aligned |
 | `apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-join-dialog.tsx` | dialog-overlay | Badge, Banner, Button, Dialog, DialogHeader, FormLayout, HStack, Layout, LayoutContent, LayoutFooter, List, ListItem, TextArea, Tooltip, VStack | aligned — uses Astryx (Badge, Banner, Button, Dialog, DialogHeader, FormLayout, HStack, Layout) | aligned |
 | `apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-navigation.tsx` | shell-chrome-or-panel | SideNavItem | aligned — uses Astryx (SideNavItem) | aligned |
 | `apps/desktop/src/renderer/features/session-collaboration/ui/session-guest-turn-actions.tsx` | shell-chrome-or-panel | none | aligned — no raw controls; no Astryx JSX usage | aligned |
@@ -140,7 +142,6 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi
 | `apps/desktop/src/renderer/plan-mode-panel.tsx` | shell-chrome-or-panel | Badge, Banner, Button, Collapsible | aligned — uses Astryx (Badge, Banner, Button, Collapsible) | aligned |
 | `apps/desktop/src/renderer/reference-shell.css` | styles | n/a (css) | aligned — no off-rhythm control heights flagged | aligned |
 | `apps/desktop/src/renderer/remote-project-directory-dialog.tsx` | dialog-overlay | Button, Dialog, DialogHeader, DropdownMenu, DropdownMenuItem, HStack, Layout, LayoutContent, LayoutFooter, Text | aligned — uses Astryx (Button, Dialog, DialogHeader, DropdownMenu, DropdownMenuItem, HStack, Layout, LayoutContent) | aligned |
-| `apps/desktop/src/renderer/session-collaboration-dialog.tsx` | dialog-overlay | Banner, Button, Dialog, DialogHeader, FormLayout, Layout, LayoutContent, LayoutFooter, SegmentedControl, SegmentedControlItem, Text, TextArea | aligned — uses Astryx (Banner, Button, Dialog, DialogHeader, FormLayout, Layout, LayoutContent, LayoutFooter) | aligned |
 | `apps/desktop/src/renderer/settings/about-settings-page.tsx` | settings-page | Banner, Button, Link, Text, VStack | aligned — uses Astryx (Banner, Button, Link, Text, VStack) | aligned |
 | `apps/desktop/src/renderer/settings/appearance-settings-page.tsx` | settings-page | Button, Grid, HStack, NumberInput, SelectableCard, Switch, Text, VStack | aligned — uses Astryx (Button, Grid, HStack, NumberInput, SelectableCard, Switch, Text, VStack) | aligned |
 | `apps/desktop/src/renderer/settings/bot-chat-detail.tsx` | settings-module | Banner, Button, Card, FormLayout, MetadataList, MetadataListItem, SegmentedControl, SegmentedControlItem, Selector, StatusDot, Switch, Text, TextArea, TextInput, VStack | aligned — uses Astryx (Banner, Button, Card, FormLayout, MetadataList, MetadataListItem, SegmentedControl, SegmentedControlItem) | aligned |
diff --git a/docs/astryx-surface-file-inventory.paths b/docs/astryx-surface-file-inventory.paths
index cd9fe33ccc..d8944ec5d0 100644
--- a/docs/astryx-surface-file-inventory.paths
+++ b/docs/astryx-surface-file-inventory.paths
@@ -65,6 +65,8 @@ apps/desktop/src/renderer/features/session-bundle/session-bundle-tasks.tsx
 apps/desktop/src/renderer/features/session-collaboration/services-context.tsx
 apps/desktop/src/renderer/features/session-collaboration/turn-request-inbox-context.tsx
 apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-alias-action.tsx
+apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-dialog-root.tsx
+apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-dialog.tsx
 apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-join-dialog.tsx
 apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-navigation.tsx
 apps/desktop/src/renderer/features/session-collaboration/ui/session-guest-turn-actions.tsx
@@ -110,7 +112,6 @@ apps/desktop/src/renderer/onboarding-hero.tsx
 apps/desktop/src/renderer/plan-mode-panel.tsx
 apps/desktop/src/renderer/reference-shell.css
 apps/desktop/src/renderer/remote-project-directory-dialog.tsx
-apps/desktop/src/renderer/session-collaboration-dialog.tsx
 apps/desktop/src/renderer/settings/about-settings-page.tsx
 apps/desktop/src/renderer/settings/appearance-settings-page.tsx
 apps/desktop/src/renderer/settings/bot-chat-detail.tsx
diff --git a/scripts/check-app-shell-hooks.mjs b/scripts/check-app-shell-hooks.mjs
index 5277d098ce..110da5f88d 100644
--- a/scripts/check-app-shell-hooks.mjs
+++ b/scripts/check-app-shell-hooks.mjs
@@ -133,7 +133,6 @@ export const ALLOWED = {
     // replaces put three `useState`, four effects and a `useStableActions`
     // facade on this fiber.
     useSessionNavigationReads: 1,
-    useSessionCollaborationDialog: 1,
     useSessionSettingIntent: 1,
     useShellAppearance: 1,
     useShellChatModel: 1,


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org