Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
595f447
fix(ui): make queued message delivery understandable
Astro-Han Sep 20, 2026
bc595d4
fix(ui): keep queued messages on a single line
Astro-Han Sep 20, 2026
a858c4f
fix(ui): render local sends as an outbox, not queue entries
Astro-Han Sep 20, 2026
42c418f
fix(ui): flatten the composer staging area into one row-per-message list
Astro-Han Sep 20, 2026
4c5067d
fix(ui): keep the queue plate's help icon reachable by pointer
Astro-Han Sep 20, 2026
2798532
fix(ui): render queued steering in the transcript, not the plate
Astro-Han Sep 20, 2026
237b93e
fix(ui): drop the queue plate's shortcut help icon
Astro-Han Sep 20, 2026
ac0dd64
fix(desktop): derive queued steering bubbles from the Host snapshot
Astro-Han Sep 20, 2026
75b8c20
docs(ui): drop Tooltip from the queue plate's surface inventory
Astro-Han Sep 20, 2026
933249e
refactor(desktop): move the queue surface into the conversation feature
Astro-Han Sep 20, 2026
68db1d6
test(desktop): adapt queue tests to the rebased service port and Astr…
Astro-Han Sep 21, 2026
70540ea
fix(desktop): restore full queued-message content on edit, keyed to i…
Astro-Han Sep 21, 2026
b0f1099
fix(desktop): keep queue-admitted message ids for reseed Turn binding
Astro-Han Sep 22, 2026
332d300
fix(desktop): resolve queue state lost while a surface was not observing
Astro-Han Sep 22, 2026
3349d0d
refactor(desktop): cut dead queue-edit surface flagged by simplificat…
Astro-Han Sep 22, 2026
0c3a11a
test(cli): adapt session driver tests to unconditional queue seed
Astro-Han Sep 22, 2026
e706021
refactor(desktop): fold the message queue into the composer staging d…
Astro-Han Sep 22, 2026
e2db92d
refactor(desktop): share the queued-entry retract-to-draft path
Astro-Han Sep 22, 2026
640cff2
fix(desktop): reconcile queue delivery with the rebased Host queue pr…
Astro-Han Sep 27, 2026
67f1903
refactor(desktop): collapse queued-steering bubble plumbing onto the …
Astro-Han Sep 27, 2026
3d69e94
fix(desktop): keep steering in the transcript from the moment it is sent
Astro-Han Sep 27, 2026
8d29f65
chore(desktop): refresh renderer architecture ledger after rebase
Astro-Han Sep 27, 2026
380d7b4
fix(desktop): restore quotes through the composer quote store after #…
Astro-Han Sep 27, 2026
af69eec
chore(desktop): refresh renderer architecture ledger after rebase
Astro-Han Sep 27, 2026
0d89d5c
test(desktop): play the queued-message lifecycle through the shell story
Astro-Han Sep 27, 2026
9bb5b3b
fix(runtime-host): seed an empty queue only for queue-rendering obser…
Astro-Han Sep 27, 2026
7eb3eb5
fix(desktop): carry restored attachments through WorkHub and Side Cha…
Astro-Han Sep 27, 2026
1030b1e
refactor(desktop): let shared Sessions write in the one chat Composer
Astro-Han Sep 27, 2026
c9b1e31
chore(desktop): refresh the Astryx surface inventory after the Guest …
Astro-Han Sep 27, 2026
39189fb
test(desktop): give the session query gate fixture the queue service …
Astro-Han Sep 27, 2026
f32ac1f
refactor(desktop): make every queued-message edit retract back into t…
Astro-Han Sep 27, 2026
c84abaa
fix(ui): clear a completed send's draft only while it still reads as …
Astro-Han Sep 27, 2026
b8d638e
test(desktop): pin AppShell to the one Composer inside GuestTurnRequests
Astro-Han Sep 27, 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
13 changes: 1 addition & 12 deletions apps/desktop/e2e/workhub-layout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ test('WorkHub keeps the submitted prompt visible while its agent is still runnin
await expect(prompt).toHaveCount(1);
await expect(workhub.locator('.maka-bubble-streaming')).toContainText('Fake backend waiting');
await expect(stop).toBeVisible();
const followups = workhub.locator('[data-queue-placement="next_turn"] .maka-composer-queue-text');
const followups = workhub.locator('.maka-composer-queue .maka-composer-queue-text');
const queuedTexts = ['下一轮整理测试结果', '再下一轮补充使用说明'] as const;
await workhub.locator(COMPOSER_INPUT).fill(queuedTexts[0]);
await workhub.getByRole('button', { name: /^(发送|Send)$/ }).click();
Expand All @@ -428,17 +428,6 @@ test('WorkHub keeps the submitted prompt visible while its agent is still runnin
await awaitSendReady(workhub);
await workhub.locator(COMPOSER_INPUT).press('Enter');
await expect(followups).toHaveText(queuedTexts);
const shortcuts = workhub.getByRole('button', { name: '发送快捷键', exact: true });
await expect(shortcuts).toHaveCount(1);
await shortcuts.hover();
const shortcutHint = workhub.getByRole('tooltip');
const steerModifier = process.platform === 'darwin' ? 'Cmd' : 'Ctrl';
await expect(shortcutHint).toHaveText(`${steerModifier}+Enter:转向(Steering)\nEnter:下一轮(Follow-up)\nShift+Enter:换行`);
await expect.poll(() => shortcutHint.evaluate((element) => {
const bounds = element.getBoundingClientRect();
return bounds.left >= 0 && bounds.right <= innerWidth && bounds.top >= 0 && bounds.bottom <= innerHeight;
})).toBe(true);
await workhub.screenshot({ path: testInfo.outputPath('workhub-queue-shortcuts.png') });
for (const text of queuedTexts) {
await expect(workhub.locator('.maka-user-message').filter({ hasText: text })).toHaveCount(0);
}
Expand Down
25 changes: 4 additions & 21 deletions apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
"src/renderer/chat-message-surface.tsx",
"src/renderer/chat-recovery-notice.tsx",
"src/renderer/command-palette-commands.ts",
"src/renderer/composer-attachments.ts",
"src/renderer/composer-defaults.ts",
"src/renderer/composer-mentions.tsx",
"src/renderer/computer-use-overlay/engine/cursor-candidate-grid.ts",
Expand Down Expand Up @@ -219,7 +218,6 @@
"src/renderer/features/workbar/tools/side-chat/quote-companion-core.ts -> src/renderer/model-connection-errors",
"src/renderer/features/workbar/tools/side-chat/quote-companion-core.ts -> src/renderer/session-copy-attempt",
"src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx -> src/renderer/attachment-preflight",
"src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx -> src/renderer/composer-attachments",
"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/use-quote-companion.ts -> src/renderer/settled-message-merge",
Expand Down Expand Up @@ -322,7 +320,7 @@
"@maka/ui": 1
},
"importSpecifiers": 10,
"nonTriviaTokens": 3565
"nonTriviaTokens": 3525
},
"src/renderer/app-shell-chrome-actions.tsx": {
"importDeclarations": 1,
Expand Down Expand Up @@ -595,7 +593,7 @@
"@maka/ui": 1
},
"importSpecifiers": 7,
"nonTriviaTokens": 2603
"nonTriviaTokens": 2590
},
"src/renderer/app-shell-session-ui-state.ts": {
"importDeclarations": 0,
Expand Down Expand Up @@ -663,11 +661,7 @@
"window.maka.sessions.compact": 1,
"window.maka.sessions.listActiveInteractions": 1,
"window.maka.sessions.listTurnLandmarks": 1,
"window.maka.sessions.promoteQueueEntry": 1,
"window.maka.sessions.reorderQueueEntries": 1,
"window.maka.sessions.retractQueueEntry": 1,
"window.maka.sessions.subscribeActiveInteractions": 1,
"window.maka.sessions.updateQueueEntry": 1,
"window.maka.settings.getClient": 1,
"window.maka.settings.subscribeClientChanged": 1
},
Expand All @@ -693,7 +687,7 @@
"useNewTaskChoice": 1,
"useOnboardingSnapshot": 1,
"usePlanModeState": 1,
"useRef": 13,
"useRef": 12,
"useSessionEventHealthPolling": 1,
"useSessionNavigationReads": 1,
"useSessionSettingIntent": 1,
Expand Down Expand Up @@ -792,7 +786,7 @@
"react": 1
},
"importSpecifiers": 92,
"nonTriviaTokens": 12373
"nonTriviaTokens": 12152
},
"src/renderer/use-app-shell-session-list.ts": {
"importDeclarations": 4,
Expand Down Expand Up @@ -1061,17 +1055,6 @@
"@maka/ui/icons": 1
}
},
"src/renderer/composer-attachments.ts": {
"bridgePaths": {},
"environmentCapabilities": {},
"hookCalls": {},
"lifecycleMethods": {},
"unresolvedDependencies": 0,
"actionFactories": [],
"dependencyPaths": {
"./features/conversation/index.js": 1
}
},
"src/renderer/composer-defaults.ts": {
"bridgePaths": {},
"environmentCapabilities": {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import { MAX_ATTACHMENT_COUNT } from '@maka/core/attachments';
import { strict as assert } from 'node:assert';
import { test } from 'node:test';
import type { PendingAttachment } from '../../renderer/composer-attachments.js';
import type { PendingAttachment } from '@maka/ui/composer-attachments';
import { createAppShellChatActions } from '../../renderer/app-shell-chat-actions.js';
import { getShellCopy } from '../../renderer/locales/shell-copy.js';
import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,11 @@ describe('busy-raced send settlement', () => {
} finally { restoreWindow(); }
});

it('keeps steering pending while the Host admits it', async () => {
it('shows steering in the transcript while the Host admits it', async () => {
const transient = new Map<string, TransientUserMessageProjection>();
const restoreWindow = installWindow({ sessions: {
submitMessage: async (_sessionId: string, _placement: string, _command: unknown) => {
assert.equal([...transient.values()][0]?.transientPlacement, 'steering');
assert.equal([...transient.values()][0]?.transientPlacement, 'transcript');
return { ok: true, disposition: 'steering', attachments: [], inlineReferences: [], skillInvocation: EMPTY_SKILL_INVOCATION };
},
} });
Expand All @@ -96,7 +96,7 @@ describe('busy-raced send settlement', () => {
updateTransientMessage: (_sessionId, message) => transient.set(message.id, message),
});
assert.equal(await actions.enqueueMessage('session-a', 'steer', 'current_turn'), true);
assert.equal([...transient.values()][0]?.transientPlacement, 'steering');
assert.equal([...transient.values()][0]?.transientPlacement, 'transcript');
} finally { restoreWindow(); }
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,16 @@ import { describe, it } from 'node:test';
import { act, createElement } from 'react';
import type { StoredMessage } from '@maka/core/session';
import { cleanupFakeDom, installReactRenderer } from './fake-dom.js';
import { useAppShellSessionUiState } from '../../renderer/features/conversation/index.js';
import {
ConversationServicesProvider,
useAppShellSessionUiState,
} from '../../renderer/features/conversation/index.js';
import { createSessionCatalogController } from '../../renderer/application/contracts/session-catalog/session-catalog-state.js';

import type { LiveTurnProjection } from '@maka/ui';
import { LocaleProvider, ToastProvider, type LiveTurnProjection } from '@maka/ui';
import type { DesktopTranscriptRangeController } from '../../renderer/platform/desktop/desktop-transcript-range-store.js';
import { createAppShellChatActions } from '../../renderer/app-shell-chat-actions.js';
import { prepareTranscriptForSend } from '../../renderer/features/conversation/testing.js';
import { prepareTranscriptForSend, stubConversationServices } from '../../renderer/features/conversation/testing.js';

import {
createActionsDeps,
Expand Down Expand Up @@ -715,6 +718,7 @@ describe('composer first-send cleanup', () => {
const { root } = installReactRenderer();
let publication!: ReturnType<typeof useAppShellSessionUiState>['publication'];
const catalog = createSessionCatalogController();
const services = stubConversationServices();
function Probe(): null {
publication = useAppShellSessionUiState(
catalog, undefined, deps.activeIdRef,
Expand All @@ -723,7 +727,16 @@ describe('composer first-send cleanup', () => {
return null;
}
try {
act(() => root.render(createElement(Probe)));
act(() => root.render(
createElement(LocaleProvider, {
locale: 'en',
children: createElement(ToastProvider, {
children: createElement(ConversationServicesProvider, {
services, children: createElement(Probe),
}),
}),
}),
));
const actions = createAppShellChatActions({
...deps, transcriptRangeRef: { current: controller },
isMessagePublished: publication.isMessagePublished,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,7 @@ test('ordinary sends stay in ChatView while queued prompts stay in the composer'
'the ordinary prompt remains in the transcript');
}

for (const transientPlacement of ['steering', 'follow_up'] as const) {
const document = render({ ...OPTIMISTIC_BUBBLE, transientPlacement });
assert.ok(document.querySelector('.maka-composer-queue')?.textContent?.includes(OPTIMISTIC_BUBBLE.text));
assert.equal(document.querySelector('.maka-user-message'), null, `${transientPlacement} stays out of the transcript`);
}
const followUp = render({ ...OPTIMISTIC_BUBBLE, transientPlacement: 'follow_up' });
assert.ok(followUp.querySelector('.maka-composer-queue')?.textContent?.includes(OPTIMISTIC_BUBBLE.text));
assert.equal(followUp.querySelector('.maka-user-message'), null, 'a follow-up stays out of the transcript');
});
10 changes: 10 additions & 0 deletions apps/desktop/src/main/__tests__/composer-directories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,13 @@ test('IPC validates directory references without turning them into attachments o
}), /Invalid directory references/);
}
});

test('restoreDirectories stages references under another draft for a later visit', async () => {
const probe = await mount();
await act(() => probe.state().restoreDirectories('draft-b', [reference]));
assert.deepEqual(probe.state().pendingDirectories, [], 'draft-a stays untouched');
await probe.render({ draftKey: 'draft-b' });
assert.deepEqual(probe.state().pendingDirectories, [reference]);
await act(() => probe.state().restoreDirectories('draft-b', [reference, reference]));
assert.deepEqual(probe.state().pendingDirectories, [reference], 'duplicates stay out');
});
30 changes: 4 additions & 26 deletions apps/desktop/src/main/__tests__/composer-mentions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,8 @@ import {
useComposerMentionsContext,
type ComposerMentions,
} from '../../renderer/composer-mentions.js';
import {
ConversationServicesProvider,
type ConversationServices,
} from '../../renderer/features/conversation/index.js';
import { ConversationServicesProvider } from '../../renderer/features/conversation/index.js';
import { stubConversationServices } from '../../renderer/features/conversation/testing.js';
import {
createSessionCatalogController,
SessionCatalogContext,
Expand Down Expand Up @@ -80,33 +78,13 @@ function installCatalogRenderer(t: TestContext) {
sessionId: string;
resolve(skills: InvocableSkillEntry[]): void;
}> = [];
const services: ConversationServices = {
listMessages: async () => [],
cancelMessage: async () => undefined,
reconcileMessage: async () => undefined,
subscribeChanges: () => () => undefined,
const services = stubConversationServices({
skills: {
listInvocable: (sessionId: string) => new Promise<InvocableSkillEntry[]>((resolve) => {
pending.push({ sessionId, resolve });
}),
},
sessions: {
readSnapshot: async () => {
throw new Error('Session snapshot is not used in catalog tests');
},
readExecutionBoundary: async () => {
throw new Error('Execution boundary is not used in catalog tests');
},
},
runtimeHosts: { subscribeChanges: () => () => undefined },
workspace: { searchFiles: async () => ({ ok: false, reason: 'no_project' }) },
newTasks: {
subscribeChanges: () => () => undefined,
listInvocableSkills: async () => [],
searchFiles: async () => ({ ok: false, reason: 'no_project' }),
},
mcp: { subscribeChanges: () => () => undefined },
};
});

const sessionCatalog = createSessionCatalogController();
const observations: CatalogObservation[] = [];
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/main/__tests__/goals-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,8 @@ describe('Goals feature boundary', () => {
for (const [source, required] of [
[composer, 'ComposerGoalProjectionConsumer,'],
[composer, '<ComposerGoalProjectionConsumer>'],
[composer, 'goalActive={goalProjection.goalActive}'],
[composer, 'onSetGoal={goalProjection.onSetGoal}'],
[composer, 'goalActive: goalProjection.goalActive,'],
[composer, 'onSetGoal: goalProjection.onSetGoal,'],
[messageSurface, 'ChatViewGoalProjectionConsumer,'],
[messageSurface, '<ChatViewGoalProjectionConsumer>'],
[messageSurface, 'goalIndicator={goalProjection.goalIndicator}'],
Expand Down
Loading