Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,20 @@ test('requires Owner confirmation before issuing a plaintext collaboration invit
const prepare = handlers.get('session-collaboration:prepare');
assert.ok(prepare);

assert.deepEqual(await prepare({} as Parameters<IpcHandler>[0], 'session-1', false), {
kind: 'insecure_confirmation_required',
});
assert.deepEqual(
await prepare({} as Parameters<IpcHandler>[0], 'session-1', 'observe', false),
{
kind: 'insecure_confirmation_required',
},
);
assert.equal(prepareCalls, 0);

const result = await prepare({} as Parameters<IpcHandler>[0], 'session-1', true);
const result = await prepare(
{} as Parameters<IpcHandler>[0],
'session-1',
'observe',
true,
);
assert.equal(prepareCalls, 1);
assert.equal((result as { kind?: unknown }).kind, 'prepared');
const invitation = (result as {
Expand Down
16 changes: 15 additions & 1 deletion apps/desktop/src/main/runtime-host-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,10 @@ import {
type SessionCatalogProjection,
type SharedSessionCatalogProjection,
type CollaborationAccessQueryResult,
type CollaborationGrantRevokeResult,
type CollaborationInvitationPrepareResult,
type CollaborationPrincipalRevokeResult,
type CollaborationTurnRequestAcknowledgeResult,
type CollaborationTurnRequestDecideResult,
type CollaborationTurnRequestQueryResult,
type SessionCollaborationGrantKind,
Expand Down Expand Up @@ -294,20 +296,32 @@ export class DesktopRuntimeHostClient {
);
}

revokeCollaborationGrant(grantId: string): Promise<CollaborationGrantRevokeResult> {
return this.request('collaboration.grant.revoke', { grantId });
}

revokeCollaborationPrincipal(
principalId: string,
): Promise<CollaborationPrincipalRevokeResult> {
return this.request('collaboration.principal.revoke', { principalId });
}

createCollaborationTurnRequest(intent: SessionTurnRequestIntent): Promise<SessionTurnAccessRequest> {
createCollaborationTurnRequest(
intent: SessionTurnRequestIntent,
): Promise<SessionTurnAccessRequest> {
return this.request('collaboration.turn-request.create', { intent });
}

queryCollaborationTurnRequests(sessionId: string): Promise<CollaborationTurnRequestQueryResult> {
return this.request('collaboration.turn-request.query', { sessionId });
}

acknowledgeCollaborationTurnRequest(
requestId: string,
): Promise<CollaborationTurnRequestAcknowledgeResult> {
return this.request('collaboration.turn-request.acknowledge', { requestId });
}

decideCollaborationTurnRequest(
requestId: string,
decision: 'approve' | 'reject',
Expand Down
61 changes: 59 additions & 2 deletions apps/desktop/src/main/runtime-host-collaboration-ipc-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ export function registerRuntimeHostCollaborationIpc(
client: Pick<
DesktopRuntimeHostClient,
| 'prepareCollaborationInvitation'
| 'acknowledgeCollaborationTurnRequest'
| 'createCollaborationTurnRequest'
| 'decideCollaborationTurnRequest'
| 'queryCollaborationTurnRequests'
| 'queryCollaborationAccess'
| 'revokeCollaborationGrant'
| 'revokeCollaborationPrincipal'
>,
ipcMain: ReconnectableReadIpcMain,
Expand All @@ -41,14 +46,21 @@ export function registerRuntimeHostCollaborationIpc(
): void {
ipcMain.handle(
'session-collaboration:prepare',
async (_event, sessionId: unknown, allowInsecure: unknown) => {
async (
_event,
sessionId: unknown,
preset: unknown,
allowInsecure: unknown,
) => {
const target = await resolveConnectionTarget();
if (target.transport.kind === 'plaintext' && allowInsecure !== true) {
return { kind: 'insecure_confirmation_required' } as const;
}
const prepared = await client.prepareCollaborationInvitation(
requiredId(sessionId, 'Session'),
['session_observation'],
requiredPreset(preset) === 'request_turn'
? ['session_observation', 'session_turn_request']
: ['session_observation'],
);
return {
kind: 'prepared',
Expand All @@ -62,12 +74,43 @@ export function registerRuntimeHostCollaborationIpc(
};
},
);
ipcMain.handle(
'session-collaboration:turn-request:create',
(_event, intent: unknown) =>
client.createCollaborationTurnRequest(intent as Parameters<
DesktopRuntimeHostClient['createCollaborationTurnRequest']
>[0]),
);
handleReconnectableRead(
ipcMain,
'session-collaboration:turn-request:query',
(_event, sessionId: unknown) =>
client.queryCollaborationTurnRequests(requiredId(sessionId, 'Session')),
);
ipcMain.handle(
'session-collaboration:turn-request:acknowledge',
(_event, requestId: unknown) =>
client.acknowledgeCollaborationTurnRequest(requiredId(requestId, 'Turn request')),
);
ipcMain.handle(
'session-collaboration:turn-request:decide',
(_event, requestId: unknown, decision: unknown) =>
client.decideCollaborationTurnRequest(
requiredId(requestId, 'Turn request'),
requiredDecision(decision),
),
);
handleReconnectableRead(
ipcMain,
'session-collaboration:getAccess',
(_event, sessionId: unknown) =>
client.queryCollaborationAccess(requiredId(sessionId, 'Session')),
);
ipcMain.handle(
'session-collaboration:revokeGrant',
(_event, grantId: unknown) =>
client.revokeCollaborationGrant(requiredId(grantId, 'Grant')),
);
ipcMain.handle(
'session-collaboration:revokePrincipal',
(_event, principalId: unknown) =>
Expand All @@ -86,3 +129,17 @@ function requiredId(value: unknown, label: string): string {
}
return value;
}

function requiredPreset(value: unknown): 'observe' | 'request_turn' {
if (value !== 'observe' && value !== 'request_turn') {
throw new Error('Invalid collaboration invitation preset');
}
return value;
}

function requiredDecision(value: unknown): 'approve' | 'reject' {
if (value !== 'approve' && value !== 'reject') {
throw new Error('Invalid collaboration Turn request decision');
}
return value;
}
14 changes: 7 additions & 7 deletions apps/desktop/src/main/runtime-host-desktop-candidate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -811,14 +811,14 @@ export async function createDesktopRuntimeHostCandidate(
ipc,
);
}
registerRuntimeHostCollaborationIpc(client, ipc, async () => {
if (collaborationConnectionTarget) return collaborationConnectionTarget;
if (target.kind === 'local' && deps.resolveLocalCollaborationConnectionTarget) {
return deps.resolveLocalCollaborationConnectionTarget();
}
throw new Error('This Runtime Host does not have a shareable connection target');
});
if (target.access === 'owner') {
registerRuntimeHostCollaborationIpc(client, ipc, async () => {
if (collaborationConnectionTarget) return collaborationConnectionTarget;
if (target.kind === 'local' && deps.resolveLocalCollaborationConnectionTarget) {
return deps.resolveLocalCollaborationConnectionTarget();
}
throw new Error('This Runtime Host does not have a shareable connection target');
});
registerRuntimeHostWorkHubIpc(client, ipc, {
resolveCreateProject: () => deps.resolveSessionCreateProject({}, target),
emitSessionsChanged,
Expand Down
24 changes: 24 additions & 0 deletions apps/desktop/src/preload/bridge-contract.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,13 @@ import type {
} from '@maka/runtime-host/protocol';
import type {
CollaborationAccessQueryResult,
CollaborationGrantRevokeResult,
CollaborationInvitationPrepareResult,
CollaborationPrincipalRevokeResult,
CollaborationTurnRequestAcknowledgeResult,
CollaborationTurnRequestDecideResult,
CollaborationTurnRequestQueryResult,
SessionTurnAccessRequest,
} from '@maka/runtime-host/protocol';
import type { AgentGraphEpochDirectory } from '@maka/runtime-host/client';
import type {
Expand Down Expand Up @@ -676,9 +681,14 @@ export interface MakaBridge {
sessionCollaboration: {
prepareInvitation(
sessionId: string,
preset: 'observe' | 'request_turn',
allowInsecure?: boolean,
): Promise<DesktopSessionCollaborationPrepareResult>;
getAccess(sessionId: string): Promise<CollaborationAccessQueryResult>;
revokeGrant(
sessionId: string,
grantId: string,
): Promise<CollaborationGrantRevokeResult>;
revokePrincipal(
sessionId: string,
principalId: string,
Expand All @@ -687,6 +697,20 @@ export interface MakaBridge {
readonly code: string;
readonly allowInsecure?: boolean;
}): Promise<DesktopSessionCollaborationImportResult>;
requestTurn(
sessionId: string,
input: { readonly turnId: string; readonly text: string },
): Promise<SessionTurnAccessRequest>;
getTurnRequests(sessionId: string): Promise<CollaborationTurnRequestQueryResult>;
acknowledgeTurnRequest(
sessionId: string,
requestId: string,
): Promise<CollaborationTurnRequestAcknowledgeResult>;
decideTurnRequest(
sessionId: string,
requestId: string,
decision: 'approve' | 'reject',
): Promise<CollaborationTurnRequestDecideResult>;
};

runtimeHost: {
Expand Down
48 changes: 47 additions & 1 deletion apps/desktop/src/preload/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1180,12 +1180,13 @@ const browserSelection = createBrowserSelectionCoordinator(runtimeHostSessionRef
const makaBridge = {
runtimeHost,
sessionCollaboration: {
async prepareInvitation(sessionId, allowInsecure = false) {
async prepareInvitation(sessionId, preset, allowInsecure = false) {
const session = await runtimeHostSessionRef(sessionId);
return ipcRenderer.invoke(
'session-collaboration:prepare',
session.scope,
session.sessionId,
preset,
allowInsecure,
);
},
Expand All @@ -1205,9 +1206,54 @@ const makaBridge = {
principalId,
);
},
async revokeGrant(sessionId, grantId) {
const session = await runtimeHostSessionRef(sessionId);
return ipcRenderer.invoke(
'session-collaboration:revokeGrant',
session.scope,
grantId,
);
},
importInvitation({ code, allowInsecure = false }) {
return ipcRenderer.invoke('session-collaboration:import', code, allowInsecure);
},
async requestTurn(sessionId, input) {
const session = await runtimeHostSessionRef(sessionId);
return ipcRenderer.invoke(
'session-collaboration:turn-request:create',
session.scope,
{
sessionId: session.sessionId,
turnId: input.turnId,
content: { text: input.text },
},
);
},
async getTurnRequests(sessionId) {
const session = await runtimeHostSessionRef(sessionId);
return ipcRenderer.invoke(
'session-collaboration:turn-request:query',
session.scope,
session.sessionId,
);
},
async acknowledgeTurnRequest(sessionId, requestId) {
const session = await runtimeHostSessionRef(sessionId);
return ipcRenderer.invoke(
'session-collaboration:turn-request:acknowledge',
session.scope,
requestId,
);
},
async decideTurnRequest(sessionId, requestId, decision) {
const session = await runtimeHostSessionRef(sessionId);
return ipcRenderer.invoke(
'session-collaboration:turn-request:decide',
session.scope,
requestId,
decision,
);
},
},
runtimeHostProfiles: {
getSnapshot() {
Expand Down
38 changes: 27 additions & 11 deletions apps/desktop/src/renderer/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ import {
} from './features/task-entry';
import { useNewTaskChoice } from './use-new-task-choice';
import { SessionCollaborationDialog } from './session-collaboration-dialog';
import { SessionTurnRequestComposer } from './session-turn-request-composer.js';
import { getSessionCollaborationCopy } from './locales/session-collaboration-copy';
import { useSessionCollaborationDialog } from './use-session-collaboration-dialog';
import { NEW_TASK_PENDING_KEY } from './pending-items';
Expand Down Expand Up @@ -835,6 +836,13 @@ function AppShellContent({
const activeMessageQueue = activeId ? messageQueueBySession[activeId] : undefined;
const activeMessageSubmitting = transientMessages.length > 0;
const activeDesktopSession = activeSession;
function openSessionSharing(session: DesktopSessionSummary): void {
sharedSessionDialog.open({
sessionId: session.id,
sessionName: session.name,
requiresRemoteAccess: session.profileKind === 'local',
});
}
// The shell's reading of the active live turn: streaming/settled flags, the
// in-flight tool signal, and the #646 turn-wait cues, all derived from the
// semantic snapshot rather than the projection (#1985).
Expand Down Expand Up @@ -2779,13 +2787,16 @@ function AppShellContent({
key={activeSessionForView.id}
sessionName={activeSessionForView.name}
readOnly={sharedSessionActive}
action={sharedSessionActive ? undefined : {
label: getSessionCollaborationCopy(uiLocale).shareAction,
onClick: () => sharedSessionDialog.open({
sessionId: activeSessionForView.id,
sessionName: activeSessionForView.name,
}),
}}
action={
sharedSessionActive ||
!activeDesktopSession ||
activeDesktopSession.profileKind === 'environment'
? undefined
: {
label: getSessionCollaborationCopy(uiLocale).shareAction,
onClick: () => void openSessionSharing(activeDesktopSession),
}
}
onRenameSession={(name) => {
void sessionNavigationCommandsRef.current?.renameSession(activeSessionForView.id, name);
}}
Expand Down Expand Up @@ -2918,10 +2929,8 @@ function AppShellContent({
onClick={openWorkHub}
/>
) : null}
{sharedSessionActive ? (
<div className="sessionCollaborationReadOnly">
{getSessionCollaborationCopy(uiLocale).observeHelp}
</div>
{sharedSessionActive && activeId ? (
<SessionTurnRequestComposer key={activeId} sessionId={activeId} />
) : (
<ChatComposerRegion
workspacePicker={workspacePicker}
Expand Down Expand Up @@ -3252,6 +3261,13 @@ function AppShellContent({
mode="share"
sessionId={sharedSessionDialog.target.sessionId}
sessionName={sharedSessionDialog.target.sessionName}
requiresRemoteAccess={sharedSessionDialog.target.requiresRemoteAccess}
onEnableRemoteAccess={() => {
const copy = getSessionCollaborationCopy(uiLocale);
sharedSessionDialog.close();
toastApi.info(copy.enableRemoteAccessTitle, copy.enableRemoteAccessBody);
openSettingsSection('projects');
}}
onClose={sharedSessionDialog.close}
/>
) : null}
Expand Down
Loading
Loading