From 5fe44d00b805aafd0b2499eb5d8ce6266e7343b4 Mon Sep 17 00:00:00 2001 From: Wang Date: Sun, 30 Aug 2026 09:43:33 +0800 Subject: [PATCH 01/14] fix(desktop): connect shared sessions through Peer Mesh Join managed Runtime Hosts through explicit Client and Host Mesh targets, recover stale authority routes over management channels, and expose managed Host Mesh actions directly. Persist signed endpoint and Mesh names, keep stable identifiers copyable, and make Mesh rosters collapsible without exposing internal revisions. Generated-by: Codex --- .../runtime-host-local-operator.test.ts | 52 ++ apps/desktop/src/main/runtime-host-boot.ts | 3 + .../src/main/runtime-host-local-operator.ts | 87 ++- .../main/runtime-host-peer-mesh-management.ts | 183 ++++- .../src/main/runtime-host-ssh-terminal.ts | 6 + apps/desktop/src/preload/bridge-contract.d.ts | 2 + apps/desktop/src/preload/preload.ts | 2 + .../locales/session-collaboration-copy.ts | 4 + .../renderer/session-collaboration-dialog.tsx | 22 +- .../runtime-host-management-dialog.tsx | 10 + .../runtime-host-peer-mesh-dialog.tsx | 683 ++++++++++++++---- .../runtime-host-profiles-section.tsx | 48 +- .../renderer/styles/settings/runtime-host.css | 149 +++- apps/desktop/src/renderer/styles/sidebar.css | 12 + docs/astryx-surface-file-inventory.md | 4 +- .../runtime-host-operator-command.test.ts | 6 + packages/cli/src/cli-core.ts | 1 + packages/cli/src/runtime-host-cli.ts | 44 +- ...ntime-host-peer-mesh-management-command.ts | 18 + .../src/__tests__/peer-mesh.test.ts | 51 +- .../operator/peer-mesh-management-frame.ts | 22 +- .../src/peer-mesh/display-name.ts | 33 + packages/runtime-host/src/peer-mesh/model.ts | 58 +- packages/runtime-host/src/peer-mesh/node.ts | 117 ++- packages/runtime-host/src/peer-mesh/owner.ts | 2 + packages/runtime-host/src/peer-mesh/store.ts | 24 +- packages/runtime-host/src/protocol/index.ts | 6 +- .../runtime-host/src/protocol/peer-mesh.ts | 83 ++- .../src/server/execution-service.ts | 1 + .../src/server/peer-mesh-authority.ts | 19 + packages/ui/src/session-history-list.tsx | 6 +- 31 files changed, 1535 insertions(+), 223 deletions(-) create mode 100644 packages/runtime-host/src/peer-mesh/display-name.ts diff --git a/apps/desktop/src/main/__tests__/runtime-host-local-operator.test.ts b/apps/desktop/src/main/__tests__/runtime-host-local-operator.test.ts index 8b9e7082d5..ff0cfce3b0 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-local-operator.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-local-operator.test.ts @@ -24,6 +24,7 @@ import { EventEmitter } from 'node:events'; import { PassThrough } from 'node:stream'; import test from 'node:test'; import { + encodeRuntimeHostPeerMeshManagementFrame, encodeRuntimeHostServiceManagementFrame, encodeRuntimeHostSetupFrame, RUNTIME_HOST_OPERATOR_PROJECT_DIRECTORY_CONFIGURATION_REQUEST_ENV, @@ -206,3 +207,54 @@ test('local update runs the selected package against the exact managed deploymen ); assert.deepEqual(phases, ['staging']); }); + +test('local Peer Mesh join sends the invitation to the managed operator over stdin', async (t) => { + let args: readonly string[] | undefined; + let input = ''; + const spawnProcess = ((_command, commandArgs) => { + args = Array.isArray(commandArgs) ? commandArgs : undefined; + const child = new EventEmitter() as ReturnType; + const stdin = new PassThrough(); + const stdout = new PassThrough(); + const stderr = new PassThrough(); + stdin.on('data', (chunk: Buffer) => { input += chunk.toString('utf8'); }); + Object.assign(child, { pid: 1234, stdin, stdout, stderr, kill: () => true }); + process.nextTick(() => { + stdout.end( + encodeRuntimeHostPeerMeshManagementFrame({ kind: 'input', action: 'join' }) + + encodeRuntimeHostPeerMeshManagementFrame({ + kind: 'result', + action: 'join', + result: { available: false, meshes: [] }, + }), + ); + stderr.end(); + child.emit('close', 0, null); + }); + return child; + }) as typeof spawn; + const operator = createDesktopRuntimeHostLocalOperator({ spawnProcess }); + t.after(() => operator.close()); + const invitation = JSON.stringify({ secret: 'one-time-mesh-secret' }); + + await operator.runPeerMesh({ + operatorPath: '/tmp/maka/operator', + action: 'join', + target: { + serviceId: 'b'.repeat(64), + rootPath: '/tmp/maka/root', + rootId: 'a'.repeat(64), + deploymentId: '00000000-0000-4000-8000-000000000001', + }, + invitation, + }); + + assert.deepEqual(args, [ + 'mesh', 'join', '--framed', + '--expected-service-id', 'b'.repeat(64), + '--expected-root-path', '/tmp/maka/root', + '--expected-root-id', 'a'.repeat(64), + '--expected-deployment-id', '00000000-0000-4000-8000-000000000001', + ]); + assert.equal(input, `${invitation}\n`); +}); diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts index 382b68b170..d568112b0f 100644 --- a/apps/desktop/src/main/runtime-host-boot.ts +++ b/apps/desktop/src/main/runtime-host-boot.ts @@ -245,6 +245,7 @@ if (runtimeHostPeerConfiguration) { runtimeHostPeerOwner = await openRuntimeHostPeerMeshOwner({ ...runtimeHostPeerConfiguration, dataRoot: join(userDataDir, 'peer-mesh'), + endpointKind: 'client', }); runtimeHostPeerClient = runtimeHostPeerOwner.client; runtimeHostPeerMesh = runtimeHostPeerOwner.mesh; @@ -594,6 +595,8 @@ const runtimeHostManagement = createDesktopRuntimeHostManagement({ const runtimeHostPeerMeshManagement = createDesktopRuntimeHostPeerMeshManagement({ ipcMain, localMesh: () => runtimeHostPeerMesh, + localHost: localRuntimeHostRemoteAccess, + runLocal: localRuntimeHostOperator.runPeerMesh, profiles: runtimeHostProfileService, runRemote: runtimeHostSshTerminal.runPeerMeshManagement, }); diff --git a/apps/desktop/src/main/runtime-host-local-operator.ts b/apps/desktop/src/main/runtime-host-local-operator.ts index 6657e788fc..4488536903 100644 --- a/apps/desktop/src/main/runtime-host-local-operator.ts +++ b/apps/desktop/src/main/runtime-host-local-operator.ts @@ -29,6 +29,7 @@ import { import { decodeRuntimeHostAccessManagementFrame, decodeRuntimeHostPeerManagementFrame, + decodeRuntimeHostPeerMeshManagementFrame, decodeRuntimeHostServiceManagementFrame, decodeRuntimeHostSetupFrame, RUNTIME_HOST_OPERATOR_PROJECT_DIRECTORY_CONFIGURATION_REQUEST_ENV, @@ -36,12 +37,15 @@ import { RUNTIME_HOST_OPERATOR_CAPABILITY_REQUEST_ENV, RUNTIME_HOST_ACCESS_MANAGEMENT_FRAME_PREFIX, RUNTIME_HOST_PEER_MANAGEMENT_FRAME_PREFIX, + RUNTIME_HOST_PEER_MESH_MANAGEMENT_FRAME_PREFIX, RUNTIME_HOST_SERVICE_MANAGEMENT_FRAME_PREFIX, RUNTIME_HOST_SETUP_FRAME_PREFIX, RUNTIME_HOST_SETUP_SOURCE_PACKAGE_INTEGRITY_ENV, type RuntimeHostAccessManagementFrame, type RuntimeHostManagedUpdatePolicy, type RuntimeHostPeerManagementFrame, + type RuntimeHostPeerMeshManagementAction, + type RuntimeHostPeerMeshManagementFrame, type RuntimeHostServiceManagementFrame, type RuntimeHostServiceUpdatePhase, type RuntimeHostSetupFrame, @@ -170,6 +174,16 @@ export function createDesktopRuntimeHostLocalOperator(input: { readonly allowInterruptActiveTasks?: boolean; readonly signal?: AbortSignal; }): Promise; + runPeerMesh(input: { + readonly operatorPath: string; + readonly action: RuntimeHostPeerMeshManagementAction; + readonly target: DesktopRuntimeHostLocalServiceTarget; + readonly meshId?: string | null; + readonly peerId?: string; + readonly displayName?: string | null; + readonly invitation?: string; + readonly signal?: AbortSignal; + }): Promise>; runAccess(input: { readonly operatorPath: string; readonly target: DesktopRuntimeHostLocalServiceTarget; @@ -284,6 +298,39 @@ export function createDesktopRuntimeHostLocalOperator(input: { active, }).then((frame) => requirePeerFrame(frame, command.action, command.target)); }, + runPeerMesh(command) { + if (closed) throw new Error('Local Runtime Host operator is closed'); + return runPeerMeshFrameProcess({ + command: { + executable: command.operatorPath, + args: [ + 'mesh', + command.action, + '--framed', + ...(typeof command.meshId === 'string' + ? ['--mesh', command.meshId] + : command.meshId === null + ? ['--off'] + : []), + ...(command.peerId ? ['--peer', command.peerId] : []), + ...(command.displayName === null + ? ['--clear-name'] + : command.displayName + ? ['--name', command.displayName] + : []), + ...managedTargetArgs(command.target), + ], + }, + environment: input.environment ?? process.env, + spawnProcess: input.spawnProcess ?? spawn, + timeoutMs: input.setupTimeoutMs ?? SETUP_TIMEOUT_MS, + terminate, + signal: combinedSignal(command.signal, closing.signal), + active, + ...(command.invitation ? { inputLine: command.invitation } : {}), + action: command.action, + }); + }, runAccess(command) { if (closed) throw new Error('Local Runtime Host operator is closed'); return runSingleFrameProcess({ @@ -572,6 +619,41 @@ function runServiceFrameProcess(input: { }); } +function runPeerMeshFrameProcess(input: { + readonly command: DesktopRuntimeHostLocalSetupCommand; + readonly environment: NodeJS.ProcessEnv; + readonly spawnProcess: typeof spawn; + readonly timeoutMs: number; + readonly terminate: typeof terminateChildProcessTree; + readonly signal?: AbortSignal; + readonly active: Set; + readonly action: RuntimeHostPeerMeshManagementAction; + readonly inputLine?: string; +}): Promise> { + let result: Exclude | undefined; + let failure: Error | undefined; + return runFramedProcess({ + ...input, + prefix: RUNTIME_HOST_PEER_MESH_MANAGEMENT_FRAME_PREFIX, + decode: decodeRuntimeHostPeerMeshManagementFrame, + label: 'Local Runtime Host Peer Mesh management', + onFrame(frame) { + if (frame.action !== input.action) { + failure = new Error('Local Runtime Host Peer Mesh management returned an unrelated result'); + } else if (frame.kind !== 'input') { + if (result) { + failure = new Error('Local Runtime Host Peer Mesh management returned multiple results'); + } else { + result = frame; + } + } + }, + result: () => result, + failure: () => failure, + acceptNonzeroResult: true, + }); +} + function combinedSignal( operation: AbortSignal | undefined, closing: AbortSignal, @@ -613,6 +695,7 @@ function runSingleFrameProcess(input: { readonly terminate: typeof terminateChildProcessTree; readonly signal?: AbortSignal; readonly active: Set; + readonly inputLine?: string; }): Promise { let result: Frame | undefined; let failure: Error | undefined; @@ -693,6 +776,7 @@ function runFramedProcess(input: { readonly result: () => Result | undefined; readonly failure: () => Error | undefined; readonly acceptNonzeroResult?: boolean; + readonly inputLine?: string; }): Promise { input.signal?.throwIfAborted(); return new Promise((resolve, reject) => { @@ -700,10 +784,11 @@ function runFramedProcess(input: { ...(input.cwd ? { cwd: input.cwd } : {}), detached: process.platform !== 'win32', env: input.environment, - stdio: ['ignore', 'pipe', 'pipe'], + stdio: [input.inputLine === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'], windowsHide: true, }); input.active.add(child); + if (input.inputLine !== undefined) child.stdin?.end(`${input.inputLine}\n`); let filterFailure: Error | undefined; let stopFailure: Error | undefined; let stderr = ''; diff --git a/apps/desktop/src/main/runtime-host-peer-mesh-management.ts b/apps/desktop/src/main/runtime-host-peer-mesh-management.ts index f56c8f2046..885bc43d74 100644 --- a/apps/desktop/src/main/runtime-host-peer-mesh-management.ts +++ b/apps/desktop/src/main/runtime-host-peer-mesh-management.ts @@ -33,13 +33,32 @@ import type { DesktopRuntimeHostSshPeerMeshManagementInput, createDesktopRuntimeHostSshTerminal, } from './runtime-host-ssh-terminal.js'; +import type { createDesktopRuntimeHostLocalOperator } from './runtime-host-local-operator.js'; +import type { + DesktopRuntimeHostLocalManagementTarget, + DesktopLocalRuntimeHostRemoteAccess, +} from './runtime-host-local-remote-access.js'; type SshTerminal = ReturnType; +type LocalOperator = ReturnType; type PeerMeshAction = DesktopRuntimeHostSshPeerMeshManagementInput['action']; +type PeerMeshResult = PeerMeshQueryResult | PeerMeshInvitationResult; + +interface ManagedPeerMeshCommand { + readonly action: PeerMeshAction; + readonly meshId?: string | null; + readonly peerId?: string; + readonly invitation?: string; + readonly displayName?: string | null; +} + +type RunManagedPeerMeshCommand = (command: ManagedPeerMeshCommand) => Promise; export function createDesktopRuntimeHostPeerMeshManagement(input: { readonly ipcMain: Pick; readonly localMesh?: () => PeerMeshNode | undefined; + readonly localHost: Pick; + readonly runLocal: LocalOperator['runPeerMesh']; readonly profiles: Pick; readonly runRemote: SshTerminal['runPeerMeshManagement']; }): { close(): void } { @@ -49,6 +68,7 @@ export function createDesktopRuntimeHostPeerMeshManagement(input: { meshIdValue?: unknown, peerIdValue?: unknown, invitationValue?: unknown, + displayNameValue?: unknown, ): Promise => { const target = requireTarget(targetValue); const action = requireAction(actionValue); @@ -60,8 +80,33 @@ export function createDesktopRuntimeHostPeerMeshManagement(input: { : undefined; const peerId = action === 'remove' ? requireIdentifier(peerIdValue, 'Peer ID') : undefined; const invitation = action === 'join' ? requireInvitation(invitationValue) : undefined; + const displayName = + action === 'rename' || action === 'rename-mesh' + ? requireDisplayName(displayNameValue) + : undefined; if (target.kind === 'desktop') { - return executeLocal(input.localMesh?.(), action, meshId, peerId, invitation); + return executeLocal(input.localMesh?.(), action, meshId, peerId, invitation, displayName); + } + if (target.kind === 'local_host') { + return input.localHost.inspectManaged(async (managed) => { + const run: RunManagedPeerMeshCommand = async (command) => { + const response = await input.runLocal({ + operatorPath: managed.operatorPath, + target: managedTarget(managed), + ...command, + }); + if (response.kind === 'error') throw new Error(response.error.message); + return response.result; + }; + if (action === 'reconcile') return reconcileManagedTarget(input.localMesh?.(), run); + return run({ + action, + ...(meshId !== undefined ? { meshId } : {}), + ...(peerId ? { peerId } : {}), + ...(invitation ? { invitation: JSON.stringify(invitation) } : {}), + ...(displayName !== undefined ? { displayName } : {}), + }); + }); } const managed = await input.profiles.resolveManagedService(target.profileId); if ( @@ -72,43 +117,104 @@ export function createDesktopRuntimeHostPeerMeshManagement(input: { ) { throw new Error('This Runtime Host does not have an active SSH management channel'); } - const response = await input.runRemote({ - destination: managed.profile.transport.destination, - ...(managed.profile.transport.sshPort === undefined - ? {} - : { sshPort: managed.profile.transport.sshPort }), - operatorPath: managed.control.operatorPath, + const transport = managed.profile.transport; + const run: RunManagedPeerMeshCommand = async (command) => { + const response = await input.runRemote({ + destination: transport.destination, + ...(transport.sshPort === undefined ? {} : { sshPort: transport.sshPort }), + operatorPath: managed.control.operatorPath, + expectedTarget: { + serviceId: managed.deployment.id, + rootPath: managed.deployment.rootPath, + rootId: managed.profile.rootId, + deploymentId: managed.deployment.deploymentId, + }, + ...command, + }); + if (response.kind === 'error') throw new Error(response.error.message); + if (response.action !== command.action) { + throw new Error('Runtime Host returned an unrelated Mesh result'); + } + return response.result; + }; + if (action === 'reconcile') return reconcileManagedTarget(input.localMesh?.(), run); + return run({ action, - expectedTarget: { - serviceId: managed.deployment.id, - rootPath: managed.deployment.rootPath, - rootId: managed.profile.rootId, - deploymentId: managed.deployment.deploymentId, - }, ...(meshId !== undefined ? { meshId } : {}), ...(peerId ? { peerId } : {}), ...(invitation ? { invitation: JSON.stringify(invitation) } : {}), + ...(displayName !== undefined ? { displayName } : {}), }); - if (response.kind === 'error') throw new Error(response.error.message); - if (response.action !== action) throw new Error('Runtime Host returned an unrelated Mesh result'); - return response.result; }; const channel = 'runtime-host-peer-mesh:execute'; input.ipcMain.handle( channel, - (_event, target, action, meshId, peerId, invitation) => - execute(target, action, meshId, peerId, invitation), + (_event, target, action, meshId, peerId, invitation, displayName) => + execute(target, action, meshId, peerId, invitation, displayName), ); return { close: () => input.ipcMain.removeHandler(channel) }; } +async function reconcileManagedTarget( + desktopMesh: PeerMeshNode | undefined, + run: RunManagedPeerMeshCommand, +): Promise { + if (!desktopMesh) return requireQueryResult(await run({ action: 'reconcile' })); + + const desktop = projectPeerMeshQuery(desktopMesh); + const managed = requireQueryResult(await run({ action: 'status' })); + const managedById = new Map(managed.meshes.map((mesh) => [mesh.meshId, mesh])); + let recovered = false; + for (const desktopMembership of desktop.meshes) { + const managedMembership = managedById.get(desktopMembership.meshId); + if (!managedMembership) continue; + if ( + desktopMembership.role === 'authority' && + managedMembership.role === 'member' && + authorityRouteNeedsRecovery(managedMembership) + ) { + const invitation = await desktopMesh.invite(desktopMembership.meshId); + await run({ action: 'join', invitation: JSON.stringify(invitation) }); + recovered = true; + continue; + } + if ( + desktopMembership.role === 'member' && + managedMembership.role === 'authority' && + authorityRouteNeedsRecovery(desktopMembership) + ) { + const invited = await run({ action: 'invite', meshId: managedMembership.meshId }); + const invitation = requireInvitationResult(invited).invitation; + await desktopMesh.join(invitation); + recovered = true; + } + } + return requireQueryResult(await run({ action: recovered ? 'status' : 'reconcile' })); +} + +function authorityRouteNeedsRecovery(mesh: PeerMeshQueryResult['meshes'][number]): boolean { + const authority = mesh.members.find(({ peerId }) => peerId === mesh.authorityPeerId); + return authority === undefined || authority.state === 'unknown' || authority.state === 'stale'; +} + +function requireQueryResult(result: PeerMeshResult): PeerMeshQueryResult { + if ('available' in result) return result; + throw new Error('Runtime Host returned an unrelated Peer Mesh result'); +} + +function requireInvitationResult(result: PeerMeshResult): PeerMeshInvitationResult { + if ('invitation' in result) return result; + throw new Error('Runtime Host returned an unrelated Peer Mesh result'); +} + async function executeLocal( mesh: PeerMeshNode | undefined, action: PeerMeshAction, meshId: string | null | undefined, peerId: string | undefined, invitation: ReturnType | undefined, + displayName: string | null | undefined, ): Promise { if (!mesh) { if (action === 'status') return { available: false, meshes: [] }; @@ -143,9 +249,29 @@ async function executeLocal( case 'transit': await mesh.setTransitMesh(meshId ?? null); return snapshot(); + case 'rename': + await mesh.setDisplayName(requiredDisplayName(displayName)); + return snapshot(); + case 'rename-mesh': + await mesh.setMeshDisplayName( + requiredValue(meshId, 'Mesh ID'), + requiredDisplayName(displayName), + ); + return snapshot(); } } +function requiredDisplayName(value: string | null | undefined): string | null { + if (value === undefined) throw new Error('Display name is required'); + return value; +} + +function requireDisplayName(value: unknown): string | null { + if (value === null) return null; + if (typeof value !== 'string') throw new Error('Peer Mesh display name is invalid'); + return value; +} + function requiredValue(value: T | null | undefined, label: string): NonNullable { if (value === undefined || value === null) throw new Error(`${label} is required`); return value; @@ -157,6 +283,9 @@ function requireTarget(value: unknown): DesktopRuntimeHostPeerMeshTarget { } const record = value as Record; if (record.kind === 'desktop' && Object.keys(record).length === 1) return { kind: 'desktop' }; + if (record.kind === 'local_host' && Object.keys(record).length === 1) { + return { kind: 'local_host' }; + } if ( record.kind === 'managed_host' && Object.keys(record).length === 2 && @@ -169,6 +298,17 @@ function requireTarget(value: unknown): DesktopRuntimeHostPeerMeshTarget { throw new Error('Peer Mesh target is invalid'); } +function managedTarget( + target: DesktopRuntimeHostLocalManagementTarget, +): Omit { + return { + serviceId: target.serviceId, + rootPath: target.rootPath, + rootId: target.rootId, + deploymentId: target.deploymentId, + }; +} + function requireAction(value: unknown): PeerMeshAction { if ( value === 'status' || value === 'create' || value === 'invite' || value === 'join' || @@ -176,7 +316,9 @@ function requireAction(value: unknown): PeerMeshAction { value === 'leave' || value === 'close' || value === 'reconcile' || - value === 'transit' + value === 'transit' || + value === 'rename' || + value === 'rename-mesh' ) return value; throw new Error('Peer Mesh action is invalid'); } @@ -187,7 +329,8 @@ function actionNeedsMesh(action: PeerMeshAction): boolean { action === 'remove' || action === 'leave' || action === 'close' || - action === 'transit' + action === 'transit' || + action === 'rename-mesh' ); } diff --git a/apps/desktop/src/main/runtime-host-ssh-terminal.ts b/apps/desktop/src/main/runtime-host-ssh-terminal.ts index fb4be4acca..64b15ade82 100644 --- a/apps/desktop/src/main/runtime-host-ssh-terminal.ts +++ b/apps/desktop/src/main/runtime-host-ssh-terminal.ts @@ -191,6 +191,7 @@ export interface DesktopRuntimeHostSshPeerMeshManagementInput { readonly expectedTarget: DesktopRuntimeHostSshManagementInput['expectedTarget']; readonly meshId?: string | null; readonly peerId?: string; + readonly displayName?: string | null; readonly invitation?: string; readonly signal?: AbortSignal; } @@ -1371,6 +1372,11 @@ function runtimeHostPeerMeshManagementRemoteCommand( ? ['--off'] : []), ...(input.peerId ? ['--peer', input.peerId] : []), + ...(input.displayName === null + ? ['--clear-name'] + : input.displayName + ? ['--name', input.displayName] + : []), ...managedServiceTargetArgs(input.expectedTarget), ].map(quotePosix).join(' '); return `exec "\${SHELL:-/bin/sh}" -lic ${quotePosix(`exec ${command}`)}`; diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts index df55b7d7aa..26dc4b0368 100644 --- a/apps/desktop/src/preload/bridge-contract.d.ts +++ b/apps/desktop/src/preload/bridge-contract.d.ts @@ -556,6 +556,7 @@ export interface DesktopRuntimeHostDirectPeerSnapshot { export type DesktopRuntimeHostPeerMeshTarget = | { readonly kind: 'desktop' } + | { readonly kind: 'local_host' } | { readonly kind: 'managed_host'; readonly profileId: string }; export type DesktopRuntimeHostPeerMeshAction = @@ -816,6 +817,7 @@ export interface MakaBridge { readonly meshId?: string | null; readonly peerId?: string; readonly invitation?: string; + readonly displayName?: string | null; }, ): Promise; }; diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index c3f7025879..5cae5216f3 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -1469,6 +1469,7 @@ const makaBridge = { readonly meshId?: string | null; readonly peerId?: string; readonly invitation?: string; + readonly displayName?: string | null; } = {}, ) { return ipcRenderer.invoke( @@ -1478,6 +1479,7 @@ const makaBridge = { input.meshId, input.peerId, input.invitation, + input.displayName, ); }, }, diff --git a/apps/desktop/src/renderer/locales/session-collaboration-copy.ts b/apps/desktop/src/renderer/locales/session-collaboration-copy.ts index 9e9a71b851..e88984197e 100644 --- a/apps/desktop/src/renderer/locales/session-collaboration-copy.ts +++ b/apps/desktop/src/renderer/locales/session-collaboration-copy.ts @@ -52,8 +52,10 @@ const ZH = { joinDescription: '粘贴邀请码,建立独立的访客连接。', code: '邀请码', join: '加入', + joining: '正在连接共享任务…', invalidCode: '邀请码无效', connectionFailed: '无法加入共享任务', + directPathUnavailable: '未能连接到任务所在的 Runtime Host。请确认此设备与该 Runtime Host 加入了同一个 Peer Mesh,或使用其他可达的连接路径。', insecureTitle: '此连接未加密', insecureBody: '访客凭据、完整任务内容和轮次请求可能被同一网络中的第三方截获。仅在你了解并接受风险时继续。', shareInsecure: '接受风险并创建', @@ -114,8 +116,10 @@ const EN = { joinDescription: 'Paste an invitation to create an independent Guest connection.', code: 'Invitation code', join: 'Join', + joining: 'Connecting to the shared task…', invalidCode: 'The invitation code is invalid', connectionFailed: 'Could not join the shared task', + directPathUnavailable: 'Could not reach the Runtime Host for this task. Make sure this device and the Runtime Host have joined the same Peer Mesh, or use another reachable connection path.', insecureTitle: 'This connection is not encrypted', insecureBody: 'The Guest credential, complete task content, and Turn requests may be intercepted by others on the network. Continue only if you understand and accept the risk.', shareInsecure: 'Accept risk and create', diff --git a/apps/desktop/src/renderer/session-collaboration-dialog.tsx b/apps/desktop/src/renderer/session-collaboration-dialog.tsx index eb7e2b06fd..115b394764 100644 --- a/apps/desktop/src/renderer/session-collaboration-dialog.tsx +++ b/apps/desktop/src/renderer/session-collaboration-dialog.tsx @@ -23,6 +23,7 @@ import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout'; import { SegmentedControl, SegmentedControlItem } from '@astryxdesign/core'; import { Button, + Banner, FormLayout, Text, TextArea, @@ -389,9 +390,11 @@ function JoinSharedSessionDialog(props: Extract(); async function join(allowInsecure = false): Promise { setWorking(true); + setFailure(undefined); try { const result = await window.maka.sessionCollaboration.importInvitation({ code: code.trim(), @@ -409,13 +412,17 @@ function JoinSharedSessionDialog(props: Extract + {working ? : null} + {failure ? : null}