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
11 changes: 6 additions & 5 deletions apps/desktop/e2e/prompt-rail.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,9 @@ test('the first click of a session lands on its prompt and holds', async ({
expect((await landing())?.offset).toBeLessThan(24);
expect((await landing())?.tickIsCurrent).toBe(true);

// And stays: the fill runs on idle callbacks after the jump, so a jump that
// only wins the first frame reads as landing and then sliding away.
// And stays: turns keep resolving their content and remeasuring after the
// jump, so a jump that only wins the first frame reads as landing and then
// sliding away.
await page.waitForTimeout(1_200);
const settled = await landing();
expect(settled?.offset).toBeGreaterThan(-24);
Expand All @@ -265,7 +266,7 @@ test('long transcripts keep a bounded mounted turn window', async ({
promptRailWindow: page,
}) => {
const count = async () => page.locator('[data-virtual-turn-id]').count();
await page.locator('[data-chat-scroll-container="true"][data-turn-window="ready"]').waitFor();
await page.locator('[data-virtual-turn-id]').first().waitFor();
await loadPromptRailBeyondVirtualWindow(page);
expect(await page.evaluate(() => {
const transcript = document.querySelector<HTMLElement>('.maka-chat-message-list');
Expand All @@ -288,8 +289,8 @@ test('long transcripts keep a bounded mounted turn window', async ({
test('evicting a turn-owned sibling interaction hands focus back to the transcript', async ({
promptRailWindow: page,
}) => {
const scroller = page.locator('[data-chat-scroll-container="true"][data-turn-window="ready"]');
await scroller.waitFor();
const scroller = page.locator('[data-chat-scroll-container="true"]');
await page.locator('[data-virtual-turn-id]').first().waitFor();
await loadPromptRailBeyondVirtualWindow(page);
await scrollTranscriptTo(page, 'bottom');
await expect(page.locator('[data-virtual-turn-id="turn-prompt-rail-120"]')).toHaveCount(1);
Expand Down
525 changes: 525 additions & 0 deletions apps/desktop/e2e/transcript-scroll.spec.ts

Large diffs are not rendered by default.

17 changes: 13 additions & 4 deletions apps/desktop/src/renderer/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,11 @@ function AppShellContent({
const [newTaskPermissionChoice, setNewTaskPermissionChoice, clearNewTaskPermissionChoice] =
useNewTaskChoice<ChatDefaultPermissionMode>(currentNewTaskDraftKey);
const [historyLoadPendingSessionId, setHistoryLoadPendingSessionId] = useState<string>();
// The state above is what the transcript renders; this is what the guard
// reads. A scroller can ask twice in one task — two scroll events before
// React has re-rendered anything — and a state read is still the old value
// for both of them.
const historyLoadPendingRef = useRef(false);
const [transcriptTurnIndex, setTranscriptTurnIndex] = useState<{
sessionId: string;
throughSequence: number | null;
Expand Down Expand Up @@ -2580,7 +2585,8 @@ function AppShellContent({
async function loadTranscriptHistory(target: 'earlier' | 'latest') {
const controller = transcriptRangeRef.current;
const sessionId = activeId;
if (!controller || !sessionId || historyLoadPendingSessionId) return;
if (!controller || !sessionId || historyLoadPendingRef.current) return;
historyLoadPendingRef.current = true;
setHistoryLoadPendingSessionId(sessionId);
try {
if (target === 'earlier') {
Expand All @@ -2605,6 +2611,7 @@ function AppShellContent({
),
);
} finally {
historyLoadPendingRef.current = false;
setHistoryLoadPendingSessionId((current) => current === sessionId ? undefined : current);
}
}
Expand Down Expand Up @@ -2843,9 +2850,11 @@ function AppShellContent({
)
) : (
<ChatSurfaceLayout
// Reset conversation-owned scroll state without remounting the
// composer: its contenteditable DOM carries the live draft.
conversationKey={activeId}
// ChatView positions this transcript: switching conversations,
// 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"
scrollToBottomLabel={
desktopConversationCopy.actions.scrollMainToBottom
}
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/renderer/chat-message-surface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,6 @@ export function ChatMessageSurface({
deepResearchRun={deepResearchRun}
emptyOverride={emptyOverride}
hasOlderHistory={hasOlderHistory}
historyLoadPending={historyLoadPending}
onLoadEarlierHistory={onLoadEarlierHistory}
returnToLatest={hasNewerHistory ? {
title: transcriptCopy.partialHistoryTitle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ export function QuoteCompanionPanel(props: {
data-preparing={companion.preparing || undefined}
>
<ChatSurfaceLayout
conversationKey={companion.companionSession?.id ?? props.sourceSession?.id}
scrollOwner="host"
scrollToBottomLabel={copy.scrollToBottom}
composer={
<>
Expand Down
10 changes: 10 additions & 0 deletions apps/desktop/src/renderer/styles/chat-message.css
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@
width: 100%;
}

/* Inserting earlier turns above the reader must not move what they are
reading. The browser's scroll anchoring does exactly that, so state the
dependency on the scroller that runs it rather than inheriting the `auto`
default: Maka reads no geometry and restores no position of its own. The
one case anchoring declines is a scroller sitting at zero, compensated in
useChatScroll after the turns land. */
[data-chat-scroll-container='true'] {
overflow-anchor: auto;
}

.maka-turn-virtual-item {
display: flex;
width: 100%;
Expand Down
2 changes: 0 additions & 2 deletions apps/desktop/src/renderer/workhub-surface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,6 @@ export function WorkHubSurface(props: {
return (
<ChatSurfaceLayout
className="workhub-surface"
conversationKey="workhub"
composer={(
<Composer
draftKey="workhub"
Expand Down Expand Up @@ -473,7 +472,6 @@ export function WorkHubCoordinationStatus(props: {
return (
<ChatSurfaceLayout
className="workhub-surface"
conversationKey="workhub-coordination-status"
composer={(
<Composer
draftKey="workhub"
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/stories/app-shell.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,7 @@ function ComposedShell(props: {
(<div className="maka-detail-with-artifacts">
<div className="mainColumn">
<ChatSurfaceLayout
scrollOwner="host"
composer={
<Composer
{...baseComposerProps}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/stories/onboarding.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function DetailPane(props: { children?: ReactNode }) {
>
<div className="maka-detail-with-artifacts">
<div className="mainColumn" data-home-surface="true">
<ChatSurfaceLayout composer={null}>
<ChatSurfaceLayout scrollOwner="host" composer={null}>
<ChatView messages={[]} scrollBehavior="smooth" onNew={() => undefined} emptyOverride={emptyOverride} />
</ChatSurfaceLayout>
</div>
Expand Down
3 changes: 2 additions & 1 deletion docs/astryx-surface-file-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Each row is one on-disk product surface file. Regenerated inventory must stay in

Wiki bar: Design Conventions · API Use-the-System · Theming · Container Padding.

**Totals:** 221 files — blocker 0, polish 1, aligned 220.
**Totals:** 222 files — blocker 0, polish 1, aligned 221.

## Exclusions (explicit)

Expand Down Expand Up @@ -244,6 +244,7 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi
| `packages/ui/src/tool-activity/diff-code-preview.tsx` | ui-composition | none | aligned — no raw controls; no Astryx JSX usage | aligned |
| `packages/ui/src/tool-activity/tool-code-block.tsx` | ui-composition | none | aligned — no raw controls; no Astryx JSX usage | aligned |
| `packages/ui/src/tool-activity/tool-result-preview.tsx` | ui-composition | Button | aligned — uses Astryx (Button) | aligned |
| `packages/ui/src/transcript-scroll-authority.tsx` | ui-composition | none | aligned — no raw controls; no Astryx JSX usage | aligned |
| `packages/ui/src/ui.tsx` | ui-composition | none | aligned — no raw controls; no Astryx JSX usage | aligned |
| `packages/ui/src/user-question-prompt.tsx` | ui-composition | Button, TextInput | aligned — uses Astryx (Button, TextInput) | aligned |
| `packages/ui/src/workspace-picker.tsx` | ui-composition | none | aligned — no raw controls; no Astryx JSX usage | aligned |
Expand Down
1 change: 1 addition & 0 deletions docs/astryx-surface-file-inventory.paths
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ packages/ui/src/tool-activity/agent-preview.tsx
packages/ui/src/tool-activity/diff-code-preview.tsx
packages/ui/src/tool-activity/tool-code-block.tsx
packages/ui/src/tool-activity/tool-result-preview.tsx
packages/ui/src/transcript-scroll-authority.tsx
packages/ui/src/ui.tsx
packages/ui/src/user-question-prompt.tsx
packages/ui/src/workspace-picker.tsx
Loading