You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/09/01 14:11:52 UTC

(maka) 06/11: fix(desktop): streamline collaboration invitation transfer

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

M4n5ter pushed a commit to branch fix/session-share-peer-route-liveness
in repository https://gitbox.apache.org/repos/asf/maka.git

commit 14b12f92c4190447a9f221eaa3f89af954dda69d
Author: Wang <m4...@gmail.com>
AuthorDate: Tue Sep 1 18:57:49 2026 +0800

    fix(desktop): streamline collaboration invitation transfer
---
 .../runtime-host-guest-session-mounts.test.ts      | 40 ++++++++++++++++++++
 apps/desktop/src/main/runtime-host-boot.ts         |  6 ++-
 .../main/runtime-host-collaboration-invitation.ts  |  7 +++-
 .../src/main/runtime-host-guest-session-mounts.ts  | 14 ++++++-
 apps/desktop/src/preload/bridge-contract.d.ts      |  2 +
 apps/desktop/src/preload/preload.ts                |  3 ++
 .../features/session-collaboration/ports.ts        |  1 +
 .../ui/session-collaboration-join-dialog.tsx       | 44 ++++++++++++++++++++--
 .../renderer/locales/session-collaboration-copy.ts |  6 +++
 .../create-session-collaboration-services.ts       |  1 +
 .../src/renderer/session-collaboration-dialog.tsx  | 20 +++++++++-
 .../src/renderer/styles/settings/runtime-host.css  |  5 +++
 12 files changed, 140 insertions(+), 9 deletions(-)

diff --git a/apps/desktop/src/main/__tests__/runtime-host-guest-session-mounts.test.ts b/apps/desktop/src/main/__tests__/runtime-host-guest-session-mounts.test.ts
index 3d8102b844..7f86dd6f72 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-guest-session-mounts.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-guest-session-mounts.test.ts
@@ -25,6 +25,7 @@ import {
   createDesktopGuestSessionMountService,
   type GuestSessionMount,
   type GuestSessionMountStore,
+  registerDesktopGuestSessionMountIpc,
 } from '../runtime-host-guest-session-mounts.js';
 import { RuntimeHostPairingFinalizationInterruptedError } from '../runtime-host-desktop-manager.js';
 
@@ -273,6 +274,45 @@ test('cancels an in-flight import and removes its durable mount desire', async (
   await mounts.close();
 });
 
+test('reads an invitation from the clipboard only on explicit IPC invocation', async () => {
+  type IpcHandler = Parameters<Pick<Electron.IpcMain, 'handle'>['handle']>[1];
+  const handlers = new Map<string, IpcHandler>();
+  let clipboardReads = 0;
+  let clipboardText = '  invitation-code  ';
+  const mounts = service(memoryStore());
+  const dispose = registerDesktopGuestSessionMountIpc(
+    {
+      handle(channel, handler) {
+        handlers.set(channel, handler);
+      },
+      removeHandler(channel) {
+        handlers.delete(channel);
+      },
+    },
+    mounts,
+    () => {
+      clipboardReads += 1;
+      return clipboardText;
+    },
+  );
+
+  assert.equal(clipboardReads, 0);
+  const handler = handlers.get('session-collaboration:invitation:read-clipboard');
+  assert.ok(handler);
+  assert.equal(await handler({} as never), 'invitation-code');
+  assert.equal(clipboardReads, 1);
+
+  clipboardText = 'x'.repeat(32 * 1024 + 1);
+  assert.throws(
+    () => handler({} as never),
+    /Clipboard content is too large to be a shared Session invitation/,
+  );
+
+  dispose();
+  assert.equal(handlers.size, 0);
+  await mounts.close();
+});
+
 function service(
   store: GuestSessionMountStore,
   overrides: {
diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts
index f4dc7cd64d..893f54e62f 100644
--- a/apps/desktop/src/main/runtime-host-boot.ts
+++ b/apps/desktop/src/main/runtime-host-boot.ts
@@ -1597,7 +1597,11 @@ function registerPersistentClientIpc(): void {
   });
   registerMarkdownSaveIpc({ ipcMain, mainWindowController });
   registerDesktopRuntimeHostProfileIpc(ipcMain, runtimeHostProfileService);
-  registerDesktopGuestSessionMountIpc(ipcMain, guestSessionMountService);
+  registerDesktopGuestSessionMountIpc(
+    ipcMain,
+    guestSessionMountService,
+    () => clipboard.readText(),
+  );
   registerClientSettingsIpc({
     ipcMain,
     settingsStore,
diff --git a/apps/desktop/src/main/runtime-host-collaboration-invitation.ts b/apps/desktop/src/main/runtime-host-collaboration-invitation.ts
index 7b9890c4fc..3137dc2d74 100644
--- a/apps/desktop/src/main/runtime-host-collaboration-invitation.ts
+++ b/apps/desktop/src/main/runtime-host-collaboration-invitation.ts
@@ -24,7 +24,7 @@ import {
 import { decodeCollaborationInvitationCode } from '@maka/runtime-host/protocol';
 
 const SCHEMA_VERSION = 1;
-const CODE_MAX_BYTES = 32 * 1024;
+export const DESKTOP_COLLABORATION_INVITATION_CODE_MAX_BYTES = 32 * 1024;
 
 export interface DesktopCollaborationConnectionTarget {
   readonly name: string;
@@ -54,7 +54,10 @@ export function encodeDesktopCollaborationInvitation(
 export function decodeDesktopCollaborationInvitation(
   code: string,
 ): DesktopCollaborationInvitation {
-  if (!code || Buffer.byteLength(code, 'utf8') > CODE_MAX_BYTES) {
+  if (
+    !code ||
+    Buffer.byteLength(code, 'utf8') > DESKTOP_COLLABORATION_INVITATION_CODE_MAX_BYTES
+  ) {
     throw new Error('Invalid Desktop collaboration invitation');
   }
   let value: unknown;
diff --git a/apps/desktop/src/main/runtime-host-guest-session-mounts.ts b/apps/desktop/src/main/runtime-host-guest-session-mounts.ts
index 0b890f4872..24dd4feff1 100644
--- a/apps/desktop/src/main/runtime-host-guest-session-mounts.ts
+++ b/apps/desktop/src/main/runtime-host-guest-session-mounts.ts
@@ -33,7 +33,10 @@ import type {
   SessionCollaborationImportResult,
   SessionCollaborationMountSummary,
 } from '../shared/session-collaboration.js';
-import { decodeDesktopCollaborationInvitation } from './runtime-host-collaboration-invitation.js';
+import {
+  decodeDesktopCollaborationInvitation,
+  DESKTOP_COLLABORATION_INVITATION_CODE_MAX_BYTES,
+} from './runtime-host-collaboration-invitation.js';
 import { RuntimeHostPairingFinalizationInterruptedError } from './runtime-host-desktop-manager.js';
 
 const STORE_SCHEMA_VERSION = 1;
@@ -419,12 +422,14 @@ export function createDesktopGuestSessionMountService(input: {
 export function registerDesktopGuestSessionMountIpc(
   ipcMain: Pick<Electron.IpcMain, 'handle' | 'removeHandler'>,
   service: DesktopGuestSessionMountService,
+  readClipboardText: () => string,
 ): () => void {
   const channels = [
     'session-collaboration:import',
     'session-collaboration:import:cancel',
     'session-collaboration:mount:list',
     'session-collaboration:mount:remove',
+    'session-collaboration:invitation:read-clipboard',
   ] as const;
   ipcMain.handle(
     channels[0],
@@ -441,6 +446,13 @@ export function registerDesktopGuestSessionMountIpc(
     service.cancelImport(requireOperationId(operationIdValue)));
   ipcMain.handle(channels[2], () => service.list());
   ipcMain.handle(channels[3], (_event, mountId: string) => service.remove(mountId));
+  ipcMain.handle(channels[4], () => {
+    const value = readClipboardText().trim();
+    if (Buffer.byteLength(value, 'utf8') > DESKTOP_COLLABORATION_INVITATION_CODE_MAX_BYTES) {
+      throw new Error('Clipboard content is too large to be a shared Session invitation');
+    }
+    return value;
+  });
   return () => {
     for (const channel of channels) ipcMain.removeHandler(channel);
   };
diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts
index 609c881e0c..b9a2b43ff4 100644
--- a/apps/desktop/src/preload/bridge-contract.d.ts
+++ b/apps/desktop/src/preload/bridge-contract.d.ts
@@ -748,6 +748,8 @@ export interface MakaBridge {
       readonly operationId: string;
     }, onProgress?: (phase: DesktopSessionCollaborationImportPhase) => void): Promise<DesktopSessionCollaborationImportResult>;
     cancelImport(operationId: string): Promise<DesktopSessionCollaborationCancelResult>;
+    /** Reads only after the user invokes the invitation paste action. */
+    readInvitationClipboard(): Promise<string>;
     listMounts(): Promise<readonly DesktopGuestSessionMountSummary[]>;
     removeMount(mountId: string): Promise<void>;
     requestTurn(
diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts
index 466c15f8a7..8d0894cb22 100644
--- a/apps/desktop/src/preload/preload.ts
+++ b/apps/desktop/src/preload/preload.ts
@@ -1341,6 +1341,9 @@ const makaBridge = {
     cancelImport(operationId) {
       return ipcRenderer.invoke('session-collaboration:import:cancel', operationId);
     },
+    readInvitationClipboard() {
+      return ipcRenderer.invoke('session-collaboration:invitation:read-clipboard');
+    },
     listMounts() {
       return ipcRenderer.invoke('session-collaboration:mount:list');
     },
diff --git a/apps/desktop/src/renderer/features/session-collaboration/ports.ts b/apps/desktop/src/renderer/features/session-collaboration/ports.ts
index c8c045f679..7e65b3a5ac 100644
--- a/apps/desktop/src/renderer/features/session-collaboration/ports.ts
+++ b/apps/desktop/src/renderer/features/session-collaboration/ports.ts
@@ -42,6 +42,7 @@ export interface SessionCollaborationServices {
     readonly operationId: string;
   }, onProgress?: (phase: SessionCollaborationImportPhase) => void): Promise<SessionCollaborationImportResult>;
   cancelImport(operationId: string): Promise<SessionCollaborationCancelResult>;
+  readInvitationClipboard(): Promise<string>;
   listMounts(): Promise<readonly SessionCollaborationMountSummary[]>;
   removeMount(mountId: string): Promise<void>;
   getPendingTurnRequests(): Promise<readonly SessionTurnAccessRequest[]>;
diff --git a/apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-join-dialog.tsx b/apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-join-dialog.tsx
index 414158f9ed..d047c09b2a 100644
--- a/apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-join-dialog.tsx
+++ b/apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-join-dialog.tsx
@@ -45,6 +45,9 @@ export interface SessionCollaborationJoinCopy {
   readonly invalidCode: string;
   readonly directPathUnavailable: string;
   readonly code: string;
+  readonly pasteInvitation: string;
+  readonly clipboardEmpty: string;
+  readonly clipboardUnavailable: string;
   readonly join: string;
   readonly validatingInvitation: string;
   readonly discoveringHost: string;
@@ -66,6 +69,7 @@ export function SessionCollaborationJoinDialog(props: {
   const services = useSessionCollaborationServices();
   const toast = useToast();
   const [code, setCode] = useState('');
+  const [readingClipboard, setReadingClipboard] = useState(false);
   const [mounts, setMounts] = useState<readonly SessionCollaborationMountSummary[]>([]);
   const [removingMountId, setRemovingMountId] = useState<string>();
   const activeOperationId = useRef<string | undefined>(undefined);
@@ -198,6 +202,27 @@ export function SessionCollaborationJoinDialog(props: {
     }
   }
 
+  async function pasteInvitation(): Promise<void> {
+    if (working || readingClipboard) return;
+    setReadingClipboard(true);
+    try {
+      const invitation = await services.readInvitationClipboard();
+      if (!open.current) return;
+      if (!invitation) {
+        toast.info(props.copy.joinTitle, props.copy.clipboardEmpty);
+        return;
+      }
+      setCode(invitation);
+      setJoinState({ kind: 'idle' });
+    } catch {
+      if (open.current) {
+        toast.error(props.copy.joinTitle, props.copy.clipboardUnavailable);
+      }
+    } finally {
+      if (open.current) setReadingClipboard(false);
+    }
+  }
+
   return (
     <Dialog
       isOpen
@@ -235,9 +260,22 @@ export function SessionCollaborationJoinDialog(props: {
                 value={code}
                 rows={6}
                 hasSpellCheck={false}
-                isDisabled={working}
-                onChange={setCode}
+                isDisabled={working || readingClipboard}
+                onChange={(value) => {
+                  setCode(value);
+                  if (joinState.kind === 'failed') setJoinState({ kind: 'idle' });
+                }}
               />
+              <div className="sessionCollaborationInvitationAction">
+                <Button
+                  variant="secondary"
+                  size="sm"
+                  label={props.copy.pasteInvitation}
+                  isDisabled={working}
+                  isLoading={readingClipboard}
+                  onClick={() => void pasteInvitation()}
+                />
+              </div>
               {mounts.length > 0 ? (
                 <List density="compact" hasDividers aria-label={props.copy.retainedTasks}>
                   {mounts.map((mount) => (
@@ -272,7 +310,7 @@ export function SessionCollaborationJoinDialog(props: {
             <Button
               variant="primary"
               label={props.copy.join}
-              isDisabled={working || !code.trim()}
+              isDisabled={working || readingClipboard || !code.trim()}
               isLoading={working}
               onClick={() => void join()}
             />
diff --git a/apps/desktop/src/renderer/locales/session-collaboration-copy.ts b/apps/desktop/src/renderer/locales/session-collaboration-copy.ts
index 9e07ced8d3..001d206b22 100644
--- a/apps/desktop/src/renderer/locales/session-collaboration-copy.ts
+++ b/apps/desktop/src/renderer/locales/session-collaboration-copy.ts
@@ -57,6 +57,9 @@ const ZH = {
     joinTitle: '加入共享任务',
     joinDescription: '粘贴邀请码,建立独立的访客连接。',
     code: '邀请码',
+    pasteInvitation: '粘贴邀请码',
+    clipboardEmpty: '剪贴板中没有可粘贴的邀请码。',
+    clipboardUnavailable: '无法读取剪贴板;请手动粘贴邀请码。',
     join: '加入',
     validatingInvitation: '正在验证邀请码…',
     discoveringHost: '正在查找任务所在的 Runtime Host…',
@@ -146,6 +149,9 @@ const EN = {
     joinTitle: 'Join shared task',
     joinDescription: 'Paste an invitation to create an independent Guest connection.',
     code: 'Invitation code',
+    pasteInvitation: 'Paste invitation',
+    clipboardEmpty: 'There is no invitation to paste from the clipboard.',
+    clipboardUnavailable: 'Could not read the clipboard. Paste the invitation manually.',
     join: 'Join',
     validatingInvitation: 'Validating the invitation…',
     discoveringHost: 'Finding the Runtime Host for this task…',
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 850084e191..8cb25a7688 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
@@ -29,6 +29,7 @@ export function createDesktopSessionCollaborationServices(
     importInvitation: (input, onProgress) =>
       bridge.sessionCollaboration.importInvitation(input, onProgress),
     cancelImport: (operationId) => bridge.sessionCollaboration.cancelImport(operationId),
+    readInvitationClipboard: () => bridge.sessionCollaboration.readInvitationClipboard(),
     listMounts: () => bridge.sessionCollaboration.listMounts(),
     removeMount: (mountId) => bridge.sessionCollaboration.removeMount(mountId),
     getPendingTurnRequests: () => bridge.sessionCollaboration.getPendingTurnRequests(),
diff --git a/apps/desktop/src/renderer/session-collaboration-dialog.tsx b/apps/desktop/src/renderer/session-collaboration-dialog.tsx
index 06c85e9634..c8628268ca 100644
--- a/apps/desktop/src/renderer/session-collaboration-dialog.tsx
+++ b/apps/desktop/src/renderer/session-collaboration-dialog.tsx
@@ -306,7 +306,6 @@ function ShareSessionDialog(props: ShareSessionDialogProps) {
                       />
                     )
                   ) : null}
-                  <Button variant="secondary" label={copy.copy} onClick={() => void copyInvitation()} />
                 </FormLayout>
               ) : (
                 <Button
@@ -403,7 +402,24 @@ function ShareSessionDialog(props: ShareSessionDialogProps) {
             </div>
           </LayoutContent>
         )}
-        footer={<LayoutFooter><Button variant="secondary" label={copy.close} isDisabled={working} onClick={props.onClose} /></LayoutFooter>}
+        footer={(
+          <LayoutFooter>
+            <Button
+              variant="secondary"
+              label={copy.close}
+              isDisabled={working}
+              onClick={props.onClose}
+            />
+            {invitation ? (
+              <Button
+                variant="primary"
+                label={copy.copy}
+                isDisabled={working}
+                onClick={() => void copyInvitation()}
+              />
+            ) : null}
+          </LayoutFooter>
+        )}
       />
     </Dialog>
   );
diff --git a/apps/desktop/src/renderer/styles/settings/runtime-host.css b/apps/desktop/src/renderer/styles/settings/runtime-host.css
index d1d28b5e58..cb19a2bb5b 100644
--- a/apps/desktop/src/renderer/styles/settings/runtime-host.css
+++ b/apps/desktop/src/renderer/styles/settings/runtime-host.css
@@ -835,6 +835,11 @@
   gap: var(--space-4);
 }
 
+.sessionCollaborationInvitationAction {
+  display: flex;
+  justify-content: flex-end;
+}
+
 .sessionCollaborationDisclosure {
   display: flex;
   flex-direction: column;


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