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