Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6a1c93e
refactor(workhub): simplify coordination and execution configuration
ARE404 Oct 2, 2026
3aff6d1
fix(workhub): propagate delegated execution policy and steering quest…
ARE404 Oct 2, 2026
c7cadcf
fix(runtime): support frozen stores in delegated execution policy
ARE404 Oct 10, 2026
8558232
feat(desktop): route remote chats through shared WorkHub
ARE404 Oct 10, 2026
72f965a
fix(core): simplify remote message source labels
ARE404 Oct 10, 2026
3204473
fix(desktop): keep message copy actions visible
ARE404 Oct 10, 2026
8a291b3
fix(desktop): size WorkHub rails to message bubbles
ARE404 Oct 10, 2026
d2ef899
feat(workhub): preserve intent and show clarification handoffs
ARE404 Oct 10, 2026
27386e9
fix(workhub): link result replies to their source task rails
ARE404 Oct 10, 2026
efcb74b
feat(workhub): add wn current-work and next-prompt shortcut
ARE404 Oct 10, 2026
5e38c91
fix(workhub): show progress and next steps for three recent works
ARE404 Oct 10, 2026
61c1a93
feat(workhub): place concise wn suggestions in composer draft
ARE404 Oct 10, 2026
9326efc
fix(workhub): simplify answer summaries and balance result spacing
ARE404 Oct 10, 2026
b3eab74
style(workhub): reduce spacing before result notices to 4px
ARE404 Oct 10, 2026
53b8325
refactor(ui): share transcript disclosure for selected answers and co…
ARE404 Oct 10, 2026
9773828
style(workhub): remove handoff content disclosure
ARE404 Oct 10, 2026
5100e78
style(workhub): remove gap before result notices
ARE404 Oct 10, 2026
9c8c9b2
style(ui): highlight focused choice instead of picker outline
ARE404 Oct 10, 2026
9419793
fix(ui): confirm question answers on option click
ARE404 Oct 10, 2026
997a18d
fix(desktop): localize WorkHub remote handling settings through catalog
ARE404 Oct 10, 2026
58e662d
fix(ci): refresh surface inventory for shared transcript disclosure
ARE404 Oct 10, 2026
6de75ad
test(desktop): retain pending choices with keyboard selection
ARE404 Oct 10, 2026
a75916d
fix(desktop): align story geometry with rails and timestamp sizing
ARE404 Oct 10, 2026
cd02338
fix(workhub): preserve steering policy across physical successors
ARE404 Oct 10, 2026
8b15c0a
test(desktop): reconcile WorkHub fixtures after main rebase
ARE404 Oct 10, 2026
c30c7da
fix(protocol): assign WorkHub an unused compatibility epoch
ARE404 Oct 10, 2026
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
4 changes: 3 additions & 1 deletion apps/desktop/e2e/workhub-pending-question.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ test('a pending WorkHub question preserves docked placement, choices and focus a
const choice = hub.getByRole('option', { name: /公开测试/ });
await expect(choice).toBeVisible();
await expect(hub.locator('.workHubLive')).toHaveAttribute('data-placement', 'docked');
await choice.click();
// Pointer activation advances immediately; keyboard selection keeps this
// question pending while Electron transfers focus between native windows.
await hub.locator('.maka-choice-panel').press('2');
await app.evaluate(async ({ app }) => { if (process.platform === 'darwin') await app.dock!.show(); });
await hub.evaluate(() => window.maka.workHubPresentation.detach());
await expect(hub.locator('.workHubLive')).toHaveAttribute('data-placement', 'floating');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,21 @@ describe('bot session lifecycle bindings', () => {
assert.deepEqual(replies, ['Maka 暂时无法处理这条消息:请求超时']);
});
});

test('WorkHub routing bypasses Bot Session creation, explore preparation and reset commands', async () => {
let routed = 0;
let closed = false;
const unexpected = async (): Promise<never> => { throw new Error('Bot Session must not be used'); };
const service = createBotIncomingMainService({
sessions: { createSession: unexpected, prepareSession: unexpected, runTurn: unexpected },
botRegistry: { sendMessage: unexpected } as unknown as BotRegistry,
workHub: {
async handle(_message, accept) { assert.equal(accept?.(), true); routed++; return true; },
async close() { closed = true; },
},
});
await service.handleBotIncomingMessage({ platform: 'qq', userId: 'user', userName: 'User', chatId: 'chat', isGroup: false, receivedAt: 1, text: '重置', sourceMessageId: 'source' });
assert.equal(routed, 1);
await service.close();
assert.equal(closed, true);
});
Original file line number Diff line number Diff line change
Expand Up @@ -43,57 +43,6 @@ test('registers the WorkHub Session projection as a reconnectable read', () => {
assert.equal(ordinary.has('workhub:getSession'), false);
});

test('stores new-work execution defaults separately from the coordination Session', async () => {
const handlers = new Map<string, IpcHandler>();
const client = { hostId: 'host-defaults' } as Parameters<
typeof registerRuntimeHostWorkHubIpc
>[0];
registerRuntimeHostWorkHubIpc(
client,
{
handle(channel, handler) {
handlers.set(channel, handler);
},
},
{},
);
const event = { sender: { id: 7 } } as Parameters<IpcHandler>[0];
const read = handlers.get('workhub:getNewWorkDefaults');
const write = handlers.get('workhub:setNewWorkDefaults');
assert.ok(read);
assert.ok(write);
assert.deepEqual(await read(event), {});
const defaults = {
executorId: 'codex.app-server',
executorModel: 'gpt-6-astra',
thinkingLevel: 'high',
};
await write(event, defaults);
assert.deepEqual(await read(event), defaults);
assert.throws(
() => write(event, { ...defaults, permissionMode: 'bypass' }),
/Invalid WorkHub new-work defaults/u,
);

const replacementHandlers = new Map<string, IpcHandler>();
registerRuntimeHostWorkHubIpc(
{ hostId: client.hostId } as Parameters<typeof registerRuntimeHostWorkHubIpc>[0],
{
handle(channel, handler) {
replacementHandlers.set(channel, handler);
},
},
{},
);
const replacementRead = replacementHandlers.get('workhub:getNewWorkDefaults');
assert.ok(replacementRead);
assert.deepEqual(
await replacementRead(event),
defaults,
'replacement clients for the same Host retain the execution defaults',
);
});

test('returns a structured WorkHub attachment rejection across IPC', async () => {
const handlers = new Map<string, IpcHandler>();
registerRuntimeHostWorkHubIpc(
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/main/__tests__/workhub-dock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { createRoot, type Root } from 'react-dom/client';
import { parseHTML } from 'linkedom';
import { deferred } from '@maka/core/test-only/async-primitives';
import { LocaleProvider } from '@maka/ui';
import { WorkHubEnablementProvider } from '../../renderer/application/contracts/workhub-workspace/workhub-enablement.js';
import { WorkHubDock, WorkHubServicesProvider, type WorkHubServices } from '../../renderer/features/workhub/index.js';
import type { WorkHubHost, WorkHubPresentationSnapshot } from '../../shared/workhub-presentation.js';

Expand Down Expand Up @@ -96,7 +97,8 @@ async function mount() {
} } as unknown as WorkHubServices;
root = createRoot(document.querySelector('#root')!);
await act(async () => root!.render(createElement(LocaleProvider, { locale: 'en', children:
createElement(WorkHubServicesProvider, { services }, createElement(WorkHubDock, { enabled: true, workbar: { bottomOpen: false, rightCollapsed: true } })),
createElement(WorkHubEnablementProvider, { value: { isEnabled: () => true, subscribe: () => () => {} } },
createElement(WorkHubServicesProvider, { services }, createElement(WorkHubDock, { workbar: { bottomOpen: false, rightCollapsed: true } }))),
})));
const frame = async (count = 1) => {
for (let index = 0; index < count; index++) await act(async () => {
Expand Down
86 changes: 84 additions & 2 deletions apps/desktop/src/main/__tests__/workhub-linked-work.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import test from "node:test";
import { createElement } from "react";
import { parseHTML } from 'linkedom';
import { ChatSurfaceLayout, LocaleProvider } from '@maka/ui';
import { WorkHubConversation, workHubLinkedWork } from '../../renderer/features/workhub/testing.js';
import { WorkHubConversation, workHubLinkedWork, workHubTurnContexts } from '../../renderer/features/workhub/testing.js';
import { renderTranscriptMarkup } from './transcript-test-dom.js';
import type { ToolCallMessage, ToolResultMessage } from '@maka/core/session';
import type { StoredMessage, ToolCallMessage, ToolResultMessage, WorkHubDelegationAssignedMessage } from '@maka/core/session';

test('WorkHub uses the common answer footer and failure presentation without prompt status or branch actions', async () => {
const ts = 1_800_000_000_000;
Expand Down Expand Up @@ -118,3 +118,85 @@ test('stop and resume keep their scoped Session identity through pending, succes
assert.deepEqual(workHubLinkedWork([{ ...call, toolName: 'unrelated' }, success], catalog, 'Work', coordination), []);
}
});


test('WorkHub restores actual answers and accepted handoffs without displaying proposed work as sent', async () => {
const call: ToolCallMessage = { type: 'tool_call', id: 'question', turnId: 'learn', ts: 2, toolName: 'AskUserQuestion', args: {} };
const answer: ToolResultMessage = { type: 'tool_result', id: 'answer', turnId: 'learn', ts: 3, toolUseId: call.id, isError: false, content: { kind: 'json', value: { answers: [
{ question: '怎么学?', answer: '直接开练作业' },
{ question: '基础?', answer: '会 Python,但没做过 LLM' },
{ question: '跳过的问题', answer: null },
] } } };
const assignment: WorkHubDelegationAssignedMessage = {
type: 'workhub_coordination', kind: 'delegation_assigned', schemaVersion: 1,
id: 'assignment', turnId: 'learn', coordinationTurnId: 'learn', ts: 5,
actionId: 'action', actionFingerprint: `sha256:${'a'.repeat(64)}`, delegationId: 'delegation',
targetSessionId: 'target', targetSessionName: 'CS336 学习', targetTurnId: 'target-turn', targetMessageId: 'target-message',
disposition: 'create_new', userText: '我想学一下 CS336 这门课程',
delegationText: '用户原话:我想学一下 CS336 这门课程。\n补充选择:直接开练作业;会 Python,但没做过 LLM。',
};
const messages: StoredMessage[] = [
{ type: 'user', id: 'user', turnId: 'learn', ts: 1, text: assignment.userText }, call, answer,
{ type: 'tool_call', id: 'proposal', turnId: 'learn', ts: 4, toolName: 'mcp__desktop_workhub__tasks', args: { request: { operation: 'create_new', text: 'UNACCEPTED PROPOSAL' } } },
assignment,
];
const context = workHubTurnContexts(messages).get('learn')!;
assert.equal(context.answers.length, 2);
assert.equal(context.handoffs[0]?.text, assignment.delegationText);
assert.equal(workHubTurnContexts([call, { ...answer, isError: true }]).size, 0);
assert.equal(workHubTurnContexts([{ ...call, toolName: 'OtherTool' }, answer]).size, 0);
assert.equal(workHubTurnContexts([call, { ...answer, turnId: 'other' }]).size, 0);
assert.equal(workHubTurnContexts([answer]).size, 0);
assert.deepEqual(workHubTurnContexts([...messages, answer, assignment]), workHubTurnContexts(messages));
assert.equal(workHubTurnContexts([{ ...assignment, delegationText: undefined }]).get('learn')?.handoffs[0]?.text, assignment.userText);
assert.equal(workHubTurnContexts([assignment, { ...assignment, id: 'second', delegationText: '第二次交接' }]).get('learn')?.handoffs.length, 2);
const handoffCall: ToolCallMessage = { ...call, id: 'handoff-call', toolName: 'mcp__desktop_workhub__tasks', args: { request: { operation: 'create_new', title: 'CS336 学习', text: assignment.delegationText } } };
const receipt = { actionId: assignment.actionId, disposition: 'create_new', targetSessionKey: 'target' };
const handoffResult: ToolResultMessage = { ...answer, id: 'handoff-result', toolUseId: handoffCall.id, content: { kind: 'json', value: { structuredContent: receipt } } };
assert.equal(workHubTurnContexts([handoffCall]).size, 0);
assert.equal(workHubTurnContexts([handoffCall, { ...handoffResult, isError: true }]).size, 0);
assert.equal(workHubTurnContexts([handoffCall, { ...handoffResult, content: { kind: 'json', value: { kind: 'cancelled' } } }]).size, 0);
assert.equal(workHubTurnContexts([handoffCall, handoffResult]).get('learn')?.handoffs[0]?.text, assignment.delegationText);
assert.equal(workHubTurnContexts([handoffCall, handoffResult, assignment]).get('learn')?.handoffs.length, 1);
assert.equal(workHubTurnContexts([handoffCall, { ...handoffResult, content: { kind: 'text', text: JSON.stringify(receipt) } }]).get('learn')?.handoffs[0]?.text, assignment.delegationText);
const markup = await renderTranscriptMarkup(createElement(LocaleProvider, { locale: 'zh-CN', children: null },
createElement(ChatSurfaceLayout, { composer: null, children: null }, createElement(WorkHubConversation, {
activeSession: { id: 'coordination', name: 'WorkHub', status: 'active', labels: [], isFlagged: false, isArchived: false, hasUnread: false, backend: 'ai-sdk', llmConnectionSlug: 'test', connectionLocked: false, model: 'test', permissionMode: 'ask' },
messages, workLinks: [], onNew: () => {}, onOpenWork: () => {}, scrollBehavior: 'auto',
})),
));
const { document } = parseHTML(markup);
assert.match(document.querySelector('.workhub-clarification-summary')!.textContent!, /直接开练作业/);
assert.doesNotMatch(document.querySelector('.workhub-turn-context')!.textContent!, /UNACCEPTED PROPOSAL|跳过的问题/);
assert.equal(document.querySelector('.workhub-handoff'), null);
});


test('task result replies keep their source Work rail even without a new delegation or older history', async () => {
const target = JSON.stringify(['host-a', 'task-a']);
const coordination = JSON.stringify(['host-a', 'maka_workhub_coordination']);
const messages: StoredMessage[] = [
{ type: 'user', id: 'notification', turnId: 'result-turn', ts: 1, text: 'Task result received',
origin: { kind: 'workhub_result', eventId: 'event', actionId: 'action', delegationId: 'delegation', targetSessionId: 'task-a', targetTurnId: 'task-turn' } },
{ type: 'assistant', id: 'summary', turnId: 'result-turn', ts: 2, text: 'The first exercise is ready.', modelId: 'test' },
];
const links = workHubLinkedWork(messages, [{ id: target, name: 'SQL practice', cwd: '/projects/sql' },
{ id: JSON.stringify(['host-b', 'task-a']), name: 'Other host' }], 'Work', coordination);
assert.equal(links.length, 1);
assert.equal(links[0]?.targetSessionId, target);
assert.equal(links[0]?.coordinationTurnId, 'result-turn');
assert.equal(links[0]?.workspaceName, 'sql');
assert.equal(workHubLinkedWork(messages, [], 'Work', coordination)[0]?.targetSessionId, target);
assert.deepEqual(workHubLinkedWork(messages, [], 'Work'), []);
const markup = await renderTranscriptMarkup(createElement(LocaleProvider, { locale: 'en', children: null },
createElement(ChatSurfaceLayout, { composer: null, children: null }, createElement(WorkHubConversation, {
activeSession: { id: coordination, name: 'WorkHub', status: 'active', labels: [], isFlagged: false, isArchived: false, hasUnread: false, backend: 'ai-sdk', llmConnectionSlug: 'test', connectionLocked: false, model: 'test', permissionMode: 'ask' },
messages, workLinks: links, onNew: () => {}, onOpenWork: () => {}, scrollBehavior: 'auto',
})),
));
const { document } = parseHTML(markup);
const rail = document.querySelector('.maka-assistant-answer .workhub-message-rail');
assert.ok(rail, 'the result reply must have the same clickable rail as its source Work');
assert.equal(rail.getAttribute('data-work-session-id'), target);
assert.doesNotMatch(rail.getAttribute('aria-label') ?? '', /Task result received|result-turn/);
});
Loading