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
13 changes: 13 additions & 0 deletions apps/desktop/e2e/workhub-reconstruction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ test('WorkHub rebuilds delegated execution feedback after navigating away and ba
await workHubComposer.press('Enter');
const routedTurn = page.locator('.workhub-turn', { hasText: routedPrompt });
await expect(routedTurn.locator('.workhub-submitted')).toBeVisible();
await expect(routedTurn.locator('.workhub-message-identity')).toContainText(sessionName!);
const workIdentity = await routedTurn.getAttribute('data-work-session-id');
expect(workIdentity).toBeTruthy();
const identityColor = await routedTurn.evaluate((element) =>
getComputedStyle(element).getPropertyValue('--workhub-work-color'),
);
expect(identityColor).toContain('oklch');

await routedTurn.locator('.workhub-submitted > button').click();
await expect(page.getByRole('region', { name: 'WorkHub' })).toBeHidden();

Expand All @@ -74,6 +82,11 @@ test('WorkHub rebuilds delegated execution feedback after navigating away and ba
page.locator('.workhub-projected-turn', { hasText: routedPrompt })
.locator('.workhub-submitted-state'),
).toHaveText('关联有效 · 已完成');
await expect(routedTurn).toHaveAttribute('data-work-session-id', workIdentity!);
expect(await routedTurn.evaluate((element) =>
getComputedStyle(element).getPropertyValue('--workhub-work-color'),
)).toBe(identityColor);

});

test('WorkHub replaces the exact linked delegation across Sessions', async ({
Expand Down
42 changes: 10 additions & 32 deletions apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,6 @@
"src/renderer/use-app-shell-session-list.ts",
"src/renderer/use-app-shell-session-ui-reads.ts",
"src/renderer/use-app-shell-session-workspace.ts",
"src/renderer/use-composer-attachments.ts",
"src/renderer/use-deep-research-run.ts",
"src/renderer/use-delayed-flag.ts",
"src/renderer/use-external-store-selector.ts",
Expand Down Expand Up @@ -281,7 +280,6 @@
"src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx -> src/renderer/composer-mentions",
"src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx -> src/renderer/scroll-motion-policy",
"src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx -> src/renderer/turn-footer-actions",
"src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx -> src/renderer/use-composer-attachments",
"src/renderer/features/workbar/tools/side-chat/use-quote-companion.ts -> src/renderer/settled-message-merge",
"src/renderer/features/workbar/tools/terminal/session-terminal-panel.tsx -> src/renderer/theme",
"src/renderer/features/workbar/ui/workbar-surface.tsx -> src/renderer/work-board-panel"
Expand Down Expand Up @@ -718,7 +716,7 @@
"nonTriviaTokens": 1395
},
"src/renderer/app-shell.tsx": {
"importDeclarations": 79,
"importDeclarations": 77,
"bridgePaths": {
"window.maka.attachments": 1,
"window.maka.attachments.readBytes": 1,
Expand Down Expand Up @@ -830,6 +828,7 @@
"./error-boundary": 1,
"./features/app-update/index.js": 1,
"./features/conversation": 1,
"./features/conversation/index.js": 1,
"./features/goals": 1,
"./features/module-hub": 1,
"./features/session-collaboration": 1,
Expand Down Expand Up @@ -860,7 +859,6 @@
"./use-app-shell-composer-quotes": 1,
"./use-app-shell-session-ui-reads": 1,
"./use-app-shell-session-workspace": 1,
"./use-composer-attachments": 1,
"./use-new-task-choice": 1,
"./use-onboarding-snapshot": 1,
"./use-project-context": 1,
Expand Down Expand Up @@ -891,11 +889,10 @@
"@maka/core/slash-command-catalog": 1,
"@maka/core/ui-locale": 1,
"@maka/ui": 1,
"@maka/ui/icons": 1,
"react": 1
},
"importSpecifiers": 121,
"nonTriviaTokens": 14630
"importSpecifiers": 116,
"nonTriviaTokens": 14544
},
"src/renderer/use-app-shell-composer-quotes.ts": {
"importDeclarations": 2,
Expand Down Expand Up @@ -1237,7 +1234,9 @@
"lifecycleMethods": {},
"unresolvedDependencies": 0,
"actionFactories": [],
"dependencyPaths": {}
"dependencyPaths": {
"./features/conversation/index.js": 1
}
},
"src/renderer/composer-defaults.ts": {
"bridgePaths": {},
Expand Down Expand Up @@ -2036,7 +2035,9 @@
"lifecycleMethods": {},
"unresolvedDependencies": 0,
"actionFactories": [],
"dependencyPaths": {}
"dependencyPaths": {
"./features/conversation/index.js": 1
}
},
"src/renderer/pending-session-view.ts": {
"bridgePaths": {},
Expand Down Expand Up @@ -4168,29 +4169,6 @@
"react": 1
}
},
"src/renderer/use-composer-attachments.ts": {
"bridgePaths": {},
"environmentCapabilities": {},
"hookCalls": {
"useEffect": 2,
"useRef": 2,
"useState": 2,
"useUiLocale": 1
},
"lifecycleMethods": {},
"unresolvedDependencies": 0,
"actionFactories": [],
"dependencyPaths": {
"./composer-attachments.js": 1,
"./locales/conversation-copy.js": 1,
"./locales/shell-copy.js": 1,
"./pending-items.js": 1,
"@maka/core/attachments": 1,
"@maka/core/events": 1,
"@maka/ui": 1,
"react": 1
}
},
"src/renderer/use-deep-research-run.ts": {
"bridgePaths": {
"window.maka.deepResearch.get": 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { normalizeSessionSendCommand } from '../permission-response-guard.js';
import {
useComposerAttachments,
type ComposerAttachmentService,
} from '../../renderer/use-composer-attachments.js';
} from '../../renderer/features/conversation/index.js';
import { cleanupFakeDom, installReactRenderer } from './fake-dom.js';

afterEach(cleanupFakeDom);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { getDesktopConversationCopy } from '../../renderer/locales/conversation-
import {
useComposerAttachments,
type ComposerAttachmentService,
} from '../../renderer/use-composer-attachments.js';
} from '../../renderer/features/conversation/index.js';
import { useAppShellComposerQuotes } from '../../renderer/use-app-shell-composer-quotes.js';
import {
composerModelSupportsVision,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ test('projects WorkHub coordination resolution through its dedicated IPC domain'
assert.deepEqual(
await handlers.get('workhub:act')?.({}, {
actionId: 'create-action',
newWorkDefaults: { permissionMode: 'bypass' },
userText: 'Start accessibility review',
proposal: { disposition: 'create_new', title: 'Accessibility review' },
create: {
Expand All @@ -112,6 +113,7 @@ test('projects WorkHub coordination resolution through its dedicated IPC domain'
);
assert.deepEqual(actions, [{
actionId: 'create-action',
newWorkDefaults: { permissionMode: 'bypass' },
userText: 'Start accessibility review',
proposal: { disposition: 'create_new', title: 'Accessibility review' },
create: {
Expand Down
19 changes: 19 additions & 0 deletions apps/desktop/src/main/__tests__/workhub-controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3273,3 +3273,22 @@ test('deterministic routing preserves executable instructions after the model te
assert.equal(result.kind, 'submitted');
if (result.kind === 'submitted') assert.equal(result.target.sessionId, 'ledger');
});


test('composer defaults apply only to creation while attachments follow explicit and automatic routing', async () => {
const actions: WorkHubCoordinationActInput[] = [];
const newWorkDefaults = { model: { llmConnectionId: 'chosen', llmConnectionSlug: 'chosen', model: 'chosen-model' }, permissionMode: 'bypass' as const };
const attachments: NonNullable<WorkHubCoordinationActInput['attachments']> = [{ name: 'requirements.txt', kind: 'other', mimeType: 'text/plain', bytes: 12, ref: { kind: 'session_file', sessionId: 'maka_workhub_coordination', relativePath: 'file-1' } }];
const existing = createWorkHubController({ sessions: port([session('payments')]), onAct: (input) => actions.push(input) });
await existing.submit({ requestId: 'explicit-composer', text: 'Continue payments', explicitTarget: { sessionId: 'payments' }, newWorkDefaults, attachments });
assert.equal(actions[0]?.proposal.disposition, 'delegate_existing');
assert.equal(actions[0]?.newWorkDefaults, undefined);
assert.deepEqual(actions[0]?.attachments, attachments);
const freshPort = port([]);
freshPort.create = async () => session('created-work');
const fresh = createWorkHubController({ sessions: freshPort, onAct: (input) => actions.push(input) });
await fresh.submit({ requestId: 'new-composer', text: 'Create a new Session for an accessibility audit', newWorkDefaults, attachments });
assert.equal(actions[1]?.proposal.disposition, 'create_new');
assert.deepEqual(actions[1]?.newWorkDefaults, newWorkDefaults);
assert.deepEqual(actions[1]?.attachments, attachments);
});
3 changes: 3 additions & 0 deletions apps/desktop/src/main/__tests__/workhub-surface-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ test('durable delegation renders every projected target state as a navigable res
);
assert.match(markup, /<button/u);
assert.match(markup, /Payments/u);
assert.match(markup, /data-work-session-id="payment"/u);
assert.match(markup, /workhub-message-identity/u);
assert.match(markup, /--workhub-work-hue:/u);
assert.match(markup, /Active link/u);
assert.match(markup, new RegExp(label, 'u'));
assert.match(markup, new RegExp(`data-state="${state}"`, 'u'));
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/main/runtime-host-desktop-candidate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,7 @@ export async function createDesktopRuntimeHostCandidate(
});
if (target.access === 'owner') {
registerRuntimeHostWorkHubIpc(client, ipc, {
attachmentIngest: { approvals: deps.attachmentApprovals, stat: deps.stat, resizeImage: deps.resizeImage },
resolveCreateProject: () => deps.resolveSessionCreateProject({}, target),
emitSessionsChanged,
});
Expand Down
16 changes: 16 additions & 0 deletions apps/desktop/src/main/runtime-host-workhub-ipc-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@ import type {
WorkspaceTarget,
} from '@maka/runtime-host/protocol';
import { RuntimeHostOperationError } from '@maka/runtime-host/client';
import { WORKHUB_COORDINATION_SESSION_ID } from '@maka/core/session';
import { prepareIngestItems, resolveAttachmentRefs } from './attachment-ingest.js';
import type { DesktopRuntimeHostClient } from './runtime-host-client.js';
import type { ReconnectableReadIpcMain } from './ipc-reconnect-policy.js';

type RuntimeHostWorkHubClient = Pick<
DesktopRuntimeHostClient,
| 'ingestAttachment'
| 'actWorkHubCoordination'
| 'listWorkHubCoordinationCandidates'
| 'recordWorkHubCoordination'
Expand All @@ -39,6 +42,7 @@ type RuntimeHostWorkHubClient = Pick<
type RendererWorkHubActionInput = Omit<WorkHubCoordinationActInput, 'create'>;

export interface RuntimeHostWorkHubIpcOptions {
attachmentIngest?: Pick<Parameters<typeof prepareIngestItems>[0], 'approvals' | 'stat'> & { resizeImage?: (bytes: Uint8Array) => Promise<Uint8Array> };
resolveCreateProject(): Promise<WorkspaceTarget>;
emitSessionsChanged(reason: 'created' | 'status-change', sessionId: string): void;
}
Expand All @@ -56,13 +60,24 @@ export function registerRuntimeHostWorkHubIpc(
client.recordWorkHubCoordination(input),
);
ipcMain.handle('workhub:candidates', () => client.listWorkHubCoordinationCandidates());
ipcMain.handle('workhub:prepareAttachments', async (event, items: unknown) => {
if (!options.attachmentIngest) throw new Error('WorkHub attachments are unavailable');
const prepared = await prepareIngestItems({ ...options.attachmentIngest, senderId: event.sender.id, items });
const refs = await resolveAttachmentRefs({
files: prepared.files,
resizeImage: options.attachmentIngest.resizeImage,
snapshot: ({ name, mimeType, content }) => client.ingestAttachment({ sessionId: WORKHUB_COORDINATION_SESSION_ID, name, mimeType, content }),
});
return prepared.commit(() => refs);
});
ipcMain.handle('workhub:act', async (_event, rawInput: RendererWorkHubActionInput) => {
try {
const proposal = rawInput?.proposal;
const base = {
actionId: rawInput?.actionId,
userText: rawInput?.userText,
proposal,
...(rawInput?.attachments ? { attachments: rawInput.attachments } : {}),
...(rawInput?.confirmation === undefined
? {}
: { confirmation: rawInput.confirmation }),
Expand All @@ -78,6 +93,7 @@ export function registerRuntimeHostWorkHubIpc(
if (createsTarget) {
result = await client.actWorkHubCoordination({
...base,
...(rawInput.newWorkDefaults ? { newWorkDefaults: rawInput.newWorkDefaults } : {}),
create: {
workspace: await options.resolveCreateProject(),
},
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/preload/bridge-contract.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,7 @@ export interface MakaBridge {
): () => void;
};
workHub: {
prepareAttachments(coordinationSessionId: string, items: RendererIngestInput[]): Promise<AttachmentRef[]>;
/** Resolve the active Runtime Host's stable coordination conversation. */
resolveCoordinationSession(): Promise<string>;
/** Persist one deterministic clarification or routing summary. */
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/preload/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2000,6 +2000,10 @@ const makaBridge = {
},
},
workHub: {
async prepareAttachments(coordinationSessionId: string, items: Parameters<MakaBridge['workHub']['prepareAttachments']>[1]) {
const scope = await resolveDesktopWorkHubCoordinationCreateScope(coordinationSessionId, runtimeHostSessionRef);
return ipcRenderer.invoke('workhub:prepareAttachments', scope, await encodeIngestItems(items));
},
resolveCoordinationSession(): Promise<string> {
return resolveDesktopWorkHubCoordinationSession(
activeRuntimeHostRef,
Expand Down
41 changes: 11 additions & 30 deletions apps/desktop/src/renderer/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import type {
} from '@maka/core/events';
import type { OrchestrationMode } from '@maka/core/orchestration';
import type { ChatDefaultPermissionMode } from '@maka/core/settings';
import type { SlashCommandIdForSurface } from '@maka/core/slash-command-catalog';
import type { UiLocale, UiLocalePreference } from '@maka/core/ui-locale';
import { collapseSessionRevisions } from '@maka/core/session-revisions';
import { isLinkedSubagentSession } from '@maka/core/session';
Expand Down Expand Up @@ -66,7 +65,6 @@ import {
reconcileInteractions,
} from '@maka/ui';
import type { ConnectionEvent } from '@maka/core/connections';
import { GitBranch, MessageCircleQuestion, Minimize2, Network } from '@maka/ui/icons';
import { Button } from '@astryxdesign/core/Button';
import { useKeyboardHelp } from './keyboard-help';
import { useCommandPalette } from './command-palette';
Expand Down Expand Up @@ -195,7 +193,7 @@ import {
import * as liveContent from './live-content-seed';
import { loadComposerDefaults, saveComposerDefaults } from './composer-defaults';
import { useTurnActionRegistry } from './use-turn-action-registry';
import { useComposerAttachments } from './use-composer-attachments';
import { useComposerAttachments, desktopSlashCommandPresentation } from './features/conversation/index.js';
import { useAppShellComposerQuotes } from './use-app-shell-composer-quotes';
import {
type ComposerMentionsSurfaceInput,
Expand Down Expand Up @@ -552,7 +550,7 @@ function AppShellContent({
const sessionHostConnections = useShellConnections({
toastApi,
uiLocale,
target: { kind: 'session', sessionId: ownerActiveId },
target: { kind: 'session', sessionId: workHubActive ? workHubCoordinationSessionId : ownerActiveId },
});
const startupConnectionSnapshot = onboarding.snapshot;
const newTaskUsesDefaultHost = taskEntry.selectors.usesDefaultHost;
Expand All @@ -568,7 +566,7 @@ function AppShellContent({
}
: defaultHostConnections.snapshot;
}
const activeConnectionSnapshot = activeId
const activeConnectionSnapshot = workHubActive || activeId

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Invalidate the connection projection WorkHub now consumes

On a normal configuration-change path, WorkHub can be open with no ordinary ownerActiveId. It now reads sessionHostConnections, but refreshConnectionProjections and handleConnectionEvent below still skip that projection without an owner. Adding/removing a connection therefore refreshes only the default/new-task projections, leaving WorkHub's model choices stale.

The Coordination lifecycle subscription does not cover this: its availability callback only retries a failed generation, not a healthy resolved one. Please include the active Coordination Host in the existing refresh/event routing. A connection-change regression with WorkHub open and no ordinary active Session should observe the updated choices.

简体中文

[P2] 让失效事件覆盖 WorkHub 当前读取的连接投影

正常配置变更路径中,WorkHub 可以在没有普通 ownerActiveId 时打开。这里改为读取 sessionHostConnections,但下面的 refreshConnectionProjections 和 handleConnectionEvent 仍在没有 owner 时跳过它。新增/删除连接只刷新 default 和 new-task 投影,WorkHub 保留过期模型选项。

Coordination lifecycle 的 availability callback 只重试失败代际,不能刷新健康且已解析的 WorkHub。建议把当前 Coordination Host 纳入已有刷新/事件转发;回归覆盖“WorkHub 打开、无普通活动 Session、连接目录变化”。

? sessionHostConnections.snapshot
: newTaskConnectionSnapshot;
const connections = activeConnectionSnapshot.connections;
Expand Down Expand Up @@ -1222,31 +1220,7 @@ function AppShellContent({
streaming: turnActive || activeStreamingLive,
}),
);
const presentation: Record<
SlashCommandIdForSurface<'desktop'>,
Omit<ComposerSlashCommandOption, 'id'>
> = {
compact: {
...shellCopy.slashCommands.compact,
keywords: ['compact', 'context', '压缩', '上下文'],
Icon: Minimize2,
},
side: {
...shellCopy.slashCommands.side,
keywords: ['side', 'btw', '侧聊', '追问'],
Icon: MessageCircleQuestion,
},
swarm: {
...shellCopy.slashCommands.swarm,
keywords: ['swarm', 'multi-agent', '多智能体'],
Icon: Network,
},
graph: {
...shellCopy.slashCommands.graph,
keywords: ['graph', 'agent graph', '智能体图'],
Icon: GitBranch,
},
};
const presentation = desktopSlashCommandPresentation(shellCopy.slashCommands);
return availableCommands.map(({ id }) => ({ id, ...presentation[id] }));
},
[activeId, activeStreamingLive, shellCopy.slashCommands, turnActive],
Expand Down Expand Up @@ -2655,6 +2629,13 @@ function AppShellContent({
locale={uiLocale}
{...(activeId ? { initialFocusSessionId: activeId } : {})}
onOpenSession={openSessionInChat}
composerServices={{
sessions,
modelChoices: chatModelChoices,
defaults: { model: newChatModel, permissionMode: newTaskPermissionMode },
confirmBypass: () => confirmBypassPermission(toastApi, uiLocale),
onOpenModelSettings: () => openSettingsSection('models'),
}}
/>
) : (
<WorkHubCoordinationStatus
Expand Down
Loading