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
2 changes: 1 addition & 1 deletion apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -873,7 +873,7 @@
"react": 1
},
"importSpecifiers": 109,
"nonTriviaTokens": 13749
"nonTriviaTokens": 13746
},
"src/renderer/use-app-shell-composer-quotes.ts": {
"importDeclarations": 2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ function renderNoSessionChatView(
...props,
} as ComponentProps<typeof ChatView>);
const layout = createElement(ChatSurfaceLayout, {
scrollOwner: 'host',
composer: null,
children: view,
});
Expand Down
36 changes: 33 additions & 3 deletions apps/desktop/src/main/__tests__/streaming-handoff.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import { createElement, type ReactNode } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { parseHTML } from 'linkedom';
import type { SessionEvent } from '@maka/core/events';
import {
armLiveTurn,
Expand Down Expand Up @@ -87,6 +88,31 @@ function renderLiveTurn(liveTurn: LiveTurnProjection): string {
}

describe('single live-turn handoff', () => {
it('keeps activity in the answer footer before the session or Turn arrives', () => {
const session: NonNullable<Parameters<typeof ChatView>[0]['activeSession']> = {
id: 'session-1', name: 'pending', status: 'running' as const, backend: 'ai-sdk',
labels: [], isFlagged: false, isArchived: false, hasUnread: false,
llmConnectionSlug: 'conn', connectionLocked: false, model: 'model', permissionMode: 'ask' as const,
};
for (const activeSession of [undefined, session]) {
const markup = renderWithLocale(createElement(ChatView, {
activeSession,
messages: [],
transientMessages: [{
id: 'message-pending', ts: 1, text: 'send now',
transientPlacement: 'current_turn',
}],
runningStatus: true,
scrollBehavior: 'smooth',
onNew() {},
} satisfies Parameters<typeof ChatView>[0]));
const { document } = parseHTML(markup);
const status = document.querySelector('.maka-assistant-answer [role="status"]');
assert.ok(status?.closest('.maka-turn-footer'), 'activity must occupy the shared footer');
assert.equal(document.querySelector('.maka-assistant-answer [role="toolbar"]'), null);
}
});

it('renders a transient user message without manufacturing a Turn', () => {
const markup = renderWithLocale(createElement(ChatView, {
activeSession: {
Expand Down Expand Up @@ -164,7 +190,9 @@ describe('single live-turn handoff', () => {
} satisfies Parameters<typeof ChatView>[0]));

assert.doesNotMatch(markup, /maka-chat-message-loading/);
assert.ok(markup.indexOf('send now') < markup.indexOf('data-turn-id="turn-1"'));
const answerIndex = markup.indexOf('maka-assistant-answer');
assert.ok(answerIndex >= 0);
assert.ok(markup.indexOf('send now') < answerIndex);
assert.equal((markup.match(/data-transient-message-id="turn-1"/g) ?? []).length, 1);
assert.equal((markup.match(/data-transcript-turn-id="turn-1"/g) ?? []).length, 1);
});
Expand Down Expand Up @@ -200,8 +228,10 @@ describe('single live-turn handoff', () => {
onNew() {},
} satisfies Parameters<typeof ChatView>[0]));

assert.ok(markup.indexOf('send now') < markup.indexOf('data-turn-id="host-turn"'));
assert.ok(markup.indexOf('do this next') > markup.indexOf('data-turn-id="host-turn"'));
const answerIndex = markup.indexOf('maka-assistant-answer');
assert.ok(answerIndex >= 0);
assert.ok(markup.indexOf('send now') < answerIndex);
assert.ok(markup.indexOf('do this next') > answerIndex);
assert.equal((markup.match(/data-transient-message-id=/g) ?? []).length, 2);
});

Expand Down
11 changes: 10 additions & 1 deletion apps/desktop/src/main/__tests__/transcript-send-viewport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ function viewportFixture(options: { returnButton?: boolean } = {}) {
CSS: globalThis.CSS, document: globalThis.document, window: globalThis.window,
Element: globalThis.Element, HTMLElement: globalThis.HTMLElement, Node: globalThis.Node,
MutationObserver: globalThis.MutationObserver, ResizeObserver: globalThis.ResizeObserver,
requestAnimationFrame: globalThis.requestAnimationFrame,
IS_REACT_ACT_ENVIRONMENT: (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT,
};
const { document, window } = parseHTML('<main id="mount"></main><section id="scroller"></section>');
Expand Down Expand Up @@ -232,6 +233,7 @@ function viewportFixture(options: { returnButton?: boolean } = {}) {
CSS: { escape: (value: string) => value }, document, window,
Element: window.Element, HTMLElement: window.HTMLElement, Node: window.Node,
MutationObserver: TestMutationObserver, ResizeObserver: TestResizeObserver,
requestAnimationFrame: window.requestAnimationFrame,
IS_REACT_ACT_ENVIRONMENT: true,
});
const messages: StoredMessage[] = [];
Expand Down Expand Up @@ -307,7 +309,14 @@ function viewportFixture(options: { returnButton?: boolean } = {}) {
await act(async () => { authority!.releasePin(); await commands.current!.loadHistory(direction); });
},
async readAt(offset: number) {
await act(() => { scroller.scrollTop = offset; scroller.dispatchEvent(new window.Event('scroll')); });
await act(() => {
const input = new window.Event('wheel');
Object.assign(input, { deltaY: offset - scroller.scrollTop });
scroller.dispatchEvent(input);
scroller.scrollTop = offset;
scroller.dispatchEvent(new window.Event('scroll'));
scroller.dispatchEvent(new window.Event('scrollend'));
});
await render();
},
async append(id: string, size: number) {
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/renderer/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2505,7 +2505,6 @@ function AppShellContent({
// following the tail and the moves the reader asks for are one
// authority there, and the composer never remounts for any of
// them — its contenteditable DOM carries the live draft.
scrollOwner="host"
data-maka-onboarding={showOnboardingHero ? 'true' : undefined}
scrollToBottomLabel={
desktopConversationCopy.actions.scrollMainToBottom
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,6 @@ export function QuoteCompanionPanel(props: {
return (
<div className="maka-quote-companion">
<ChatSurfaceLayout
scrollOwner="host"
scrollToBottomLabel={copy.scrollToBottom}
composer={
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ export function WorkHubRoot() {
<ChatSurfaceLayout
scrollButton={showConversation ? undefined : null}
style={!showConversation ? { height: expandedLayoutHeight, flex: 'none', position: 'absolute', bottom: 0, width: '100%' } : undefined}
scrollOwner="host"
onReturnToTail={transcript.hasNewer ? controller.loadLatest : undefined}
composer={
<div className="workHubComposerSurface" ref={composerSurface}>
Expand Down
7 changes: 7 additions & 0 deletions apps/desktop/src/renderer/styles/chat-message.css
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@
contain-intrinsic-block-size: auto 96px;
}

/* A long prompt can put even the first answer beyond the relevance margin.
The streaming frontier must contribute its real height before following;
completed blocks in the same active Turn still retain lazy layout. */
.maka-chat-message-list [data-maka-transcript-boundary][data-live-streaming='true'] {
content-visibility: visible;
}

/* Container blocks — a Processing sequence, a linked-agent list — hold many
entries, so their first-paint estimate stays multi-line. It remains an
estimate rather than a clamp: the block grows to its measured size after
Expand Down
Loading