You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by co...@apache.org on 2026/10/02 12:29:17 UTC
(maka) branch main updated: refactor(ui): move turn chrome onto Astryx layout (#5911)
This is an automated email from the ASF dual-hosted git repository.
Colafornia pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git
The following commit(s) were added to refs/heads/main by this push:
new 887924e14a refactor(ui): move turn chrome onto Astryx layout (#5911)
887924e14a is described below
commit 887924e14aa3fe78b746fd88a43548d7335b9573
Author: Colafornia <co...@gmail.com>
AuthorDate: Fri Oct 2 20:29:11 2026 +0800
refactor(ui): move turn chrome onto Astryx layout (#5911)
Generated-by: Devin
---
apps/desktop/src/renderer/styles/chat-detail.css | 7 +-
apps/desktop/stories/app-shell.stories.tsx | 43 +++++++++
docs/astryx-surface-file-inventory.md | 4 +-
packages/ui/src/chat-turn.tsx | 115 +++++++++++++++--------
packages/ui/src/chat-view.tsx | 9 +-
packages/ui/src/primitives/chat.tsx | 48 ++--------
packages/ui/src/styles.css | 29 ++----
7 files changed, 149 insertions(+), 106 deletions(-)
diff --git a/apps/desktop/src/renderer/styles/chat-detail.css b/apps/desktop/src/renderer/styles/chat-detail.css
index 8e9bb9c265..cb76ec561b 100644
--- a/apps/desktop/src/renderer/styles/chat-detail.css
+++ b/apps/desktop/src/renderer/styles/chat-detail.css
@@ -20,14 +20,11 @@
/* A turn reads as one unit because its internal gap is well under the
between-turn gap, which the transcript row in chat-view.tsx owns. */
.maka-turn {
- display: flex;
- flex-direction: column;
+ /* local: VStack owns display/direction/gap/width; the reading measure and
+ flex-item behavior have no props to move onto. */
flex: 0 0 auto;
- gap: var(--space-3);
max-width: var(--maka-reading-measure);
margin: 0 auto;
- width: 100%;
- box-sizing: border-box;
}
/* Search-highlight ring on the turn the search panel just navigated to.
diff --git a/apps/desktop/stories/app-shell.stories.tsx b/apps/desktop/stories/app-shell.stories.tsx
index c14f69b4bd..a936a81aff 100644
--- a/apps/desktop/stories/app-shell.stories.tsx
+++ b/apps/desktop/stories/app-shell.stories.tsx
@@ -583,6 +583,49 @@ export const UpdateDownloaded: Story = {
render: () => <ComposedShell updateReminder={{ state: 'downloaded', latestVersion: '0.1.7' }} />,
};
+const onLineageNavigation = fn();
+
+// Real path: open a session retaining an original reply and its regenerated
+// reply. Stored turn_state lineage is projected by ChatView and the production
+// deriveChatTurnPresentation, including the forward and reverse navigation.
+export const RegeneratedConversation: Story = {
+ render: () => <ComposedShell chat={{
+ scrollBehavior: 'auto',
+ onLineageBadgeClick: onLineageNavigation,
+ messages: [
+ user('lineage-original-user', 'lineage-original', 5, '解释一下这个方案。'),
+ assistant('lineage-original-answer', 'lineage-original', 4, '旧回答保留在原来的轮次中。'),
+ { type: 'turn_state', id: 'lineage-original-state', turnId: 'lineage-original', ts: NOW - 4 * 60_000, status: 'completed' },
+ user('lineage-new-user', 'lineage-new', 3, '解释一下这个方案。'),
+ assistant('lineage-new-answer', 'lineage-new', 2, '重新生成的回答与旧回答通过来源标记相连。'),
+ { type: 'turn_state', id: 'lineage-new-state', turnId: 'lineage-new', ts: NOW - 2 * 60_000, status: 'completed', regeneratedFromTurnId: 'lineage-original' },
+ ],
+ }} />,
+ play: async ({ canvasElement }) => {
+ onLineageNavigation.mockClear();
+ const canvas = within(canvasElement);
+ const forward = await canvas.findByRole('button', { name: '重新生成自旧回答' });
+ const reverse = await canvas.findByRole('button', { name: '已重新生成 → 新回答' });
+ const original = canvasElement.querySelector<HTMLElement>('[data-turn-id="lineage-original"]')!;
+ const regenerated = canvasElement.querySelector<HTMLElement>('[data-turn-id="lineage-new"]')!;
+ const sourceRow = forward.closest('.maka-turn-lineage-row')!;
+ const derivativeRow = reverse.closest('.maka-turn-lineage-row')!;
+ await document.fonts.ready;
+ expect(sourceRow.getBoundingClientRect().bottom).toBeLessThanOrEqual(regenerated.querySelector('.maka-user-message')!.getBoundingClientRect().top);
+ expect(derivativeRow.getBoundingClientRect().bottom).toBeLessThanOrEqual(original.querySelector('.maka-turn-footer')!.getBoundingClientRect().top);
+ for (const [button, row] of [[forward, sourceRow], [reverse, derivativeRow]] as const) {
+ expect(button.getBoundingClientRect().left).toBeCloseTo(row.getBoundingClientRect().left, 1);
+ expect(button.getBoundingClientRect().right).toBeLessThanOrEqual(row.getBoundingClientRect().right + 1);
+ }
+ forward.focus();
+ await expect(forward).toHaveFocus();
+ await userEvent.keyboard('{Enter}');
+ await expect(onLineageNavigation).toHaveBeenLastCalledWith('lineage-original');
+ await userEvent.click(reverse);
+ await expect(onLineageNavigation).toHaveBeenLastCalledWith('lineage-new');
+ },
+};
+
// Real path: the same download fails → same slot, muted variant, retry.
export const UpdateFailed: Story = {
render: () => <ComposedShell updateReminder={{ state: 'error', latestVersion: '0.1.7' }} />,
diff --git a/docs/astryx-surface-file-inventory.md b/docs/astryx-surface-file-inventory.md
index eb7ff9240c..e7a38a7c10 100644
--- a/docs/astryx-surface-file-inventory.md
+++ b/docs/astryx-surface-file-inventory.md
@@ -284,8 +284,8 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi
| `packages/ui/src/chat-empty-hero.tsx` | shell-chrome-or-panel | none | aligned — no raw controls; no Astryx JSX usage | aligned |
| `packages/ui/src/chat-model-switcher.tsx` | shell-chrome-or-panel | Button, Selector, SelectorOption | aligned — uses Astryx (Button, Selector, SelectorOption) | aligned |
| `packages/ui/src/chat-surface-layout.tsx` | shell-chrome-or-panel | ChatLayout | aligned — uses Astryx (ChatLayout) | aligned |
-| `packages/ui/src/chat-turn.tsx` | shell-chrome-or-panel | Badge, Banner, Button, ChatMessage, ChatMessageBubble, ChatMessageMetadata, ChatSystemMessage, HStack, Icon, IconButton, Spinner, Thumbnail, Timestamp, Token, Tooltip | aligned — uses Astryx (Badge, Banner, Button, ChatMessage, ChatMessageBubble, ChatMessageMetadata, ChatSystemMessage, HStack) | aligned |
-| `packages/ui/src/chat-view.tsx` | shell-chrome-or-panel | Button, ButtonGroup, ChatMessageList, EmptyState, HStack, Spinner | aligned — uses Astryx (Button, ButtonGroup, ChatMessageList, EmptyState, HStack, Spinner) | aligned |
+| `packages/ui/src/chat-turn.tsx` | shell-chrome-or-panel | Badge, Banner, Button, ChatMessage, ChatMessageBubble, ChatMessageMetadata, ChatSystemMessage, HStack, Icon, IconButton, Spinner, Thumbnail, Timestamp, Token, Tooltip, VStack | aligned — uses Astryx (Badge, Banner, Button, ChatMessage, ChatMessageBubble, ChatMessageMetadata, ChatSystemMessage, HStack) | aligned |
+| `packages/ui/src/chat-view.tsx` | shell-chrome-or-panel | Button, ButtonGroup, ChatMessageList, EmptyState, HStack, Spinner, VStack | aligned — uses Astryx (Button, ButtonGroup, ChatMessageList, EmptyState, HStack, Spinner, VStack) | aligned |
| `packages/ui/src/choice-panel.tsx` | shell-chrome-or-panel | Badge, Item | aligned — uses Astryx (Badge, Item) | aligned |
| `packages/ui/src/client-capability-prompt.tsx` | ui-composition | Button | aligned — uses Astryx (Button) | aligned |
| `packages/ui/src/client-plugin-runtime.tsx` | ui-composition | none | aligned — no raw controls; no Astryx JSX usage | aligned |
diff --git a/packages/ui/src/chat-turn.tsx b/packages/ui/src/chat-turn.tsx
index 9f29d6b572..c67f2954af 100644
--- a/packages/ui/src/chat-turn.tsx
+++ b/packages/ui/src/chat-turn.tsx
@@ -39,6 +39,7 @@ import {
Thumbnail,
Timestamp,
Token,
+ VStack,
useLightbox,
useMediaQuery,
} from '@astryxdesign/core';
@@ -61,7 +62,7 @@ import {
import { foldTimeline, reconcileFoldedEntries, type FoldedTimelineChild, type FoldedTimelineEntry } from './timeline-fold.js';
import { AttachmentKindIcon } from './attachment-kinds.js';
import { QuoteRefChip } from './quote-ref-chip.js';
-import { Marker, markerVariants } from './primitives/chat.js';
+import { markerVariants } from './primitives/chat.js';
import { ToolTrow } from './tool-activity.js';
import { formatBytes } from './tool-activity/preview-utils.js';
import { useUiLocale } from './locale-context.js';
@@ -361,6 +362,33 @@ function CopyButton(props: {
);
}
+/**
+ * Host-provenance note ("scheduled task", "agent graph", …): a non-interactive
+ * chip whose durable id stays in the tooltip instead of the transcript body.
+ * `aria-description` keeps that id reachable for assistive tech, matching the
+ * role `title` used to play before Astryx `Tooltip` took over the hover path.
+ */
+function HostOriginNote(props: { title: string; icon: ReactNode; label: string }) {
+ return (
+ <Tooltip content={props.title}>
+ <HStack
+ gap={1}
+ vAlign="center"
+ paddingBlock={0.5}
+ paddingInline={1.5}
+ width="fit-content"
+ className={markerVariants({ variant: 'host-origin' })}
+ data-slot="marker"
+ data-variant="host-origin"
+ role="note"
+ aria-description={props.title}
+ >
+ {props.icon}
+ <span>{props.label}</span>
+ </HStack>
+ </Tooltip>
+ );
+}
/**
* Renders one conversational turn: user message → tools used → assistant
@@ -495,7 +523,10 @@ export const TurnView = memo(function TurnView(props: {
[foldedTimeline, showAssistantMessage],
);
return (
- <section
+ <VStack
+ as="section"
+ gap={3}
+ width="100%"
className="maka-turn"
data-maka-contract="markdown-flow"
data-turn-id={turn.turnId}
@@ -503,7 +534,16 @@ export const TurnView = memo(function TurnView(props: {
tabIndex={props.searchHighlighted ? -1 : undefined}
>
{forwardBadges.length > 0 && (
- <Marker variant="lineage-row" aria-label={copy.sourceAriaLabel}>
+ <HStack
+ gap={0.5}
+ wrap="wrap"
+ vAlign="center"
+ width="100%"
+ className={markerVariants({ variant: 'lineage-row' })}
+ data-slot="marker"
+ data-variant="lineage-row"
+ aria-label={copy.sourceAriaLabel}
+ >
{forwardBadges.map((badge) => (
<UiButton
key={badge.id}
@@ -517,39 +557,30 @@ export const TurnView = memo(function TurnView(props: {
label={badge.label}
/>
))}
- </Marker>
+ </HStack>
)}
{/* Host provenance keeps non-user prompts from impersonating the user.
Durable ids stay in tooltips instead of the transcript body. */}
{turn.user?.hostOrigin?.kind === 'scheduled_task' && (
- <Marker
- variant="host-origin"
- role="note"
+ <HostOriginNote
title={copy.scheduledTaskTitle(turn.user.hostOrigin.scheduledTaskId)}
- >
- <Timer size={ICON_SIZE.meta} aria-hidden="true" />
- <span>{copy.scheduledTaskTriggered}</span>
- </Marker>
+ icon={<Timer size={ICON_SIZE.meta} aria-hidden="true" />}
+ label={copy.scheduledTaskTriggered}
+ />
)}
{turn.user?.hostOrigin?.kind === 'legacy_automation' && (
- <Marker
- variant="host-origin"
- role="note"
+ <HostOriginNote
title={copy.legacyAutomationTitle(turn.user.hostOrigin.automationId)}
- >
- <Timer size={ICON_SIZE.meta} aria-hidden="true" />
- <span>{copy.legacyAutomationTriggered}</span>
- </Marker>
+ icon={<Timer size={ICON_SIZE.meta} aria-hidden="true" />}
+ label={copy.legacyAutomationTriggered}
+ />
)}
{turn.user?.hostOrigin?.kind === 'goal' && (
- <Marker
- variant="host-origin"
- role="note"
+ <HostOriginNote
title={copy.goalTitle(turn.user.hostOrigin.goalId)}
- >
- <RefreshCcw size={ICON_SIZE.meta} aria-hidden="true" />
- <span>{copy.goalContinued}</span>
- </Marker>
+ icon={<RefreshCcw size={ICON_SIZE.meta} aria-hidden="true" />}
+ label={copy.goalContinued}
+ />
)}
{turn.user?.hostOrigin?.kind === 'workhub_result' && (
<ChatSystemMessage
@@ -561,14 +592,11 @@ export const TurnView = memo(function TurnView(props: {
</ChatSystemMessage>
)}
{turn.user?.hostOrigin?.kind === 'agent_graph' && (
- <Marker
- variant="host-origin"
- role="note"
+ <HostOriginNote
title={copy.agentGraphTitle(turn.user.hostOrigin.graphId)}
- >
- <GitBranch size={ICON_SIZE.meta} aria-hidden="true" />
- <span>{copy.agentGraphTriggered}</span>
- </Marker>
+ icon={<GitBranch size={ICON_SIZE.meta} aria-hidden="true" />}
+ label={copy.agentGraphTriggered}
+ />
)}
{props.transientMessages?.map((message) => (
<TransientUserMessage key={message.id} message={message} />
@@ -711,7 +739,7 @@ export const TurnView = memo(function TurnView(props: {
sender="assistant"
className="maka-chat-message maka-assistant-answer"
>
- <div className="maka-assistant-answer-content">
+ <VStack gap={2} width="100%" className="maka-assistant-answer-content">
{props.messageRail}
{/* The turn timeline is the rendering source of truth
(materialize.ts): each step's 深度思考 disclosure, answer bubble,
@@ -791,9 +819,18 @@ export const TurnView = memo(function TurnView(props: {
)}
</Banner>
)}
- </div>
+ </VStack>
{ownsTurnChrome && reverseBadges.length > 0 && (
- <Marker variant="lineage-row-reverse" aria-label={copy.derivativesAriaLabel}>
+ <HStack
+ gap={0.5}
+ wrap="wrap"
+ vAlign="center"
+ width="100%"
+ className={markerVariants({ variant: 'lineage-row-reverse' })}
+ data-slot="marker"
+ data-variant="lineage-row-reverse"
+ aria-label={copy.derivativesAriaLabel}
+ >
{reverseBadges.map((badge) => (
<UiButton
key={badge.id}
@@ -807,7 +844,7 @@ export const TurnView = memo(function TurnView(props: {
label={badge.label}
/>
))}
- </Marker>
+ </HStack>
)}
{ownsTurnChrome && (
<TurnFooter
@@ -833,7 +870,7 @@ export const TurnView = memo(function TurnView(props: {
</Fragment>
);
})}
- </section>
+ </VStack>
);
});
@@ -956,10 +993,10 @@ export function PendingTurnAnswer(props: {
sender="assistant"
className="maka-chat-message maka-assistant-answer"
>
- <div className="maka-assistant-answer-content">
+ <VStack gap={2} width="100%" className="maka-assistant-answer-content">
<TurnStatusBar status="running" running={props.running} startedAt={startedAt} providerRetry={props.providerRetry} />
{props.providerRetry && <ModelProviderRetryIndicator retry={props.providerRetry} />}
- </div>
+ </VStack>
<TurnFooter turnId={props.turnId} actions={[]} live context={context} />
</LocalizedChatMessage>
);
diff --git a/packages/ui/src/chat-view.tsx b/packages/ui/src/chat-view.tsx
index 824dfdd02e..9f1da9c58a 100644
--- a/packages/ui/src/chat-view.tsx
+++ b/packages/ui/src/chat-view.tsx
@@ -56,7 +56,7 @@ import type {
QuoteRef,
ShellRunUpdate,
} from '@maka/core/events';
-import { Button, ButtonGroup, ChatMessageList, EmptyState, HStack, Spinner } from '@astryxdesign/core';
+import { Button, ButtonGroup, ChatMessageList, EmptyState, HStack, Spinner, VStack } from '@astryxdesign/core';
import { useChatLayoutContext } from '@astryxdesign/core/Chat';
import { useLayer } from '@astryxdesign/core/Layer';
import { finalAssistantReplyText } from './materialize.js';
@@ -539,7 +539,10 @@ export function ChatView(props: {
// Tail rows have no Turn ancestor, so the reading measure that `.maka-turn`
// owns would not reach them: without it the bubble spans the full window.
const tail = hasPendingAnswer || tailTransientMessages.length > 0 ? (
- <section
+ <VStack
+ as="section"
+ gap={3}
+ width="100%"
className={hasPendingAnswer ? 'maka-turn maka-pending-turn' : 'maka-turn'}
data-awaiting-host={awaitingHost || undefined}
>
@@ -557,7 +560,7 @@ export function ChatView(props: {
providerRetry={activeContent?.providerRetry}
/>
)}
- </section>
+ </VStack>
) : null;
const { startMargin, listRef, measureStartMargin } = useTranscriptStartMargin(scrollRef);
const { highlightedTurnId, placed, commandTurnId, revealTurnAtStart, measurement } = useChatScroll({
diff --git a/packages/ui/src/primitives/chat.tsx b/packages/ui/src/primitives/chat.tsx
index eacc856277..4955d27110 100644
--- a/packages/ui/src/primitives/chat.tsx
+++ b/packages/ui/src/primitives/chat.tsx
@@ -19,13 +19,11 @@
"use client";
-import type React from "react";
-import { cn } from "../utils.js";
-
/**
- * `Marker` — the per-turn lineage / footer chrome (issue #332, PR2).
+ * `markerVariants` — the per-turn lineage / footer chrome classes
+ * (issue #332, PR2).
*
- * Retires the bespoke `.maka-turn-summary*`, `.maka-turn-lineage-*`, and
+ * Retired the bespoke `.maka-turn-summary*`, `.maka-turn-lineage-*`, and
* `.maka-turn-footer*` shell
* CSS (spread across `maka-tokens.css`, `styles/settings/models.css`, and the
* re-anchored measure-column block in `styles/tool-output.css`), moving each
@@ -33,12 +31,14 @@ import { cn } from "../utils.js";
*
* The measure-column geometry the old `tool-output.css` re-anchor applied to
* the summary / lineage rows / footer is gone rather than moved: `.maka-turn`
- * is the column, and every `Marker` renders inside one, so a second cap on the
- * chrome could only ever be the same edge stated twice.
+ * is the column, and every marked element renders inside one, so a second cap
+ * on the chrome could only ever be the same edge stated twice.
*
- * `markerVariants` is exported from THIS module as a local variant recipe
- * so the lineage badge + footer action — which render as `UiButton` and can't
- * be wrapped — apply the shell via `className`; `Button` runs it through
+ * Layout itself now rides Astryx primitives (`VStack` / `HStack` own the
+ * column, rows and chips); this recipe only maps each chrome slot to the
+ * semantic classes its remaining surface styling still selects on. The
+ * lineage badge + footer action — which render as `UiButton` and can't be
+ * wrapped — apply the shell via `className`; `Button` runs it through
* `cn` last so consumers can append their own product hook.
* It is intentionally kept OFF the `@maka/ui` package barrel (see `index.ts`):
* the only consumers import it by relative path, so the variant table stays an
@@ -68,34 +68,6 @@ function markerVariants({ variant }: { variant: MarkerVariant }): string {
export { markerVariants };
-export interface MarkerProps extends React.ComponentPropsWithoutRef<"div"> {
- variant: MarkerVariant;
- // The summary chips were authored as inline `<span>`s; the containers /
- // markers as `<div>`s. Keep the original tag so the semantic-class
- // conversion is structurally identical (zero behavioral change).
- as?: "div" | "span";
-}
-
-export function Marker({
- className,
- variant,
- as: Tag = "div",
- ...props
-}: MarkerProps): React.ReactElement {
- return (
- // `{...props}` first so the `data-slot` / `data-variant` hooks land last and
- // can't be clobbered by a consumer (mirrors Message / Bubble). The styling
- // `data-kind` / `data-state` / `data-direction` etc. flow through `...props`
- // and are read by the literalized `data-[…]:` variants above.
- <Tag
- {...props}
- data-slot="marker"
- data-variant={variant}
- className={cn(markerVariants({ variant }), className)}
- />
- );
-}
-
/**
* Tool-result preview surfaces (issue #332, PR4) — the semantic classes
* `DiffCodePreview` and the load-tool result card style through.
diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css
index 9b0439aee5..cbe5e05b59 100644
--- a/packages/ui/src/styles.css
+++ b/packages/ui/src/styles.css
@@ -252,7 +252,9 @@
flex: 1;
padding-block: 0 var(--space-1-5);
}
-.maka-assistant-answer-content { display: flex; width: 100%; min-width: 0; flex-direction: column; gap: 8px; }
+/* local: VStack owns the column and gap; this is only the flex-child floor
+ that lets long diagnostics shrink inside the ChatMessage column. */
+.maka-assistant-answer-content { min-width: 0; }
/* Holds the answer's place, unseen and unannounced, until the Host names the Turn. */
.maka-pending-turn[data-awaiting-host] .maka-assistant-answer { visibility: hidden; }
.maka-subagent-session-label,
@@ -905,16 +907,13 @@
.maka-sandbox-blocked-copy[data-copy-feedback="failed"] { color: var(--destructive); }
.maka-turn-host-origin {
+ /* local: HStack owns display/gap/padding/width; the chip surface (tone,
+ radius, right-edge self-alignment, rhythm) has no props to move onto. */
font: var(--maka-text-supporting);
- display: inline-flex;
- width: fit-content;
- align-items: center;
align-self: flex-end;
- gap: var(--space-1);
margin-inline: 0;
margin-bottom: var(--space-1);
border-radius: var(--radius-control);
- padding: var(--space-0-5) var(--space-1-5);
background: var(--foreground-5);
color: var(--muted-foreground);
}
@@ -943,16 +942,6 @@
}
.maka-turn-failed-banner { margin-block: var(--space-1) var(--space-0-5); }
-.maka-turn-lineage-row,
-.maka-turn-footer {
- display: flex;
- width: 100%;
- flex-wrap: wrap;
- align-items: center;
- justify-content: flex-start;
- gap: var(--space-0-5);
-}
-
.maka-turn-lineage-row {
margin-top: var(--space-0-5);
margin-bottom: var(--space-1);
@@ -962,10 +951,8 @@
.maka-turn-lineage-row-reverse { margin-top: var(--space-1); }
.maka-turn-lineage-badge {
- border: 0;
+ /* local: lineage navigation keeps its pill silhouette; Button has no radius prop. */
border-radius: var(--radius-pill);
- background: oklch(from var(--foreground) l c h / 0.05);
- color: var(--muted-foreground);
}
.maka-turn-lineage-badge[data-direction="forward"] {
@@ -979,6 +966,10 @@
}
.maka-turn-footer {
+ /* local: ChatMessageMetadata owns flex/centering, but has no width, wrap or gap props. */
+ width: 100%;
+ flex-wrap: wrap;
+ gap: var(--space-0-5);
min-height: var(--size-element-sm, 28px);
margin-top: var(--space-0-5);
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org