You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/08/29 08:45:19 UTC
(maka) branch main updated: fix(desktop): simplify partial history notice (#4140)
This is an automated email from the ASF dual-hosted git repository.
Astro-Han 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 01369b082 fix(desktop): simplify partial history notice (#4140)
01369b082 is described below
commit 01369b08248d8b66b6c5a1a6204f9683397a10a1
Author: ooopSok <81...@users.noreply.github.com>
AuthorDate: Sat Aug 29 16:45:14 2026 +0800
fix(desktop): simplify partial history notice (#4140)
* fix(desktop): simplify partial history notice
Generated-by: OpenAI Codex
* test(desktop): cover partial history presentation
Generated-by: OpenAI Codex
* docs: refresh Astryx surface inventory
---
apps/desktop/e2e/fixtures.ts | 12 ++
apps/desktop/e2e/partial-history-notice.spec.ts | 137 +++++++++++++++++++++
apps/desktop/src/main/e2e-fixture.ts | 13 ++
.../desktop/src/main/e2e-fixture/scenarios-chat.ts | 43 +++++++
apps/desktop/src/main/e2e-fixture/seed-helpers.ts | 1 +
apps/desktop/src/renderer/chat-message-surface.tsx | 1 -
.../src/renderer/locales/conversation-copy.ts | 5 +-
apps/desktop/src/renderer/styles/chat-message.css | 31 +----
apps/desktop/src/renderer/styles/prompt-rail.css | 18 ---
docs/astryx-surface-file-inventory.md | 2 +-
packages/core/src/e2e-fixture.ts | 1 +
.../ui/src/__tests__/prompt-anchor-rail.test.ts | 27 ++--
.../__tests__/transcript-history-notice.test.tsx | 16 +--
packages/ui/src/chat-view.tsx | 21 ++--
packages/ui/src/conversation-copy.ts | 6 +-
packages/ui/src/prompt-anchor-rail.tsx | 14 +--
16 files changed, 246 insertions(+), 102 deletions(-)
diff --git a/apps/desktop/e2e/fixtures.ts b/apps/desktop/e2e/fixtures.ts
index 16b077cdb..cff13431e 100644
--- a/apps/desktop/e2e/fixtures.ts
+++ b/apps/desktop/e2e/fixtures.ts
@@ -482,6 +482,7 @@ export const test = base.extend<{
parentRemovalWindow: Page;
railRenderWindow: Page;
promptRailWindow: Page;
+ partialHistoryWindow: Page;
promptRailMotionWindow: Page;
requestHeaderRowWindow: Page;
newTaskTargetWindow: Page;
@@ -588,6 +589,17 @@ export const test = base.extend<{
showWindow: true,
}, use);
},
+ // A transcript larger than the bounded Desktop range. Clicking an unloaded
+ // prompt exercises the real load-around path and its partial-history UI.
+ partialHistoryWindow: async ({}, use) => {
+ await withE2eWindow({
+ seed: false,
+ readinessSelector: '[data-turn-id]',
+ e2eFixtureScenario: 'chat-partial-history',
+ locale: 'zh',
+ showWindow: true,
+ }, use);
+ },
// The same transcript, scrolling the way the shipped app scrolls. Separate
// from `promptRailWindow` because it is only the jump that needs a scroll
// still in flight, and paying for one everywhere costs several seconds per
diff --git a/apps/desktop/e2e/partial-history-notice.spec.ts b/apps/desktop/e2e/partial-history-notice.spec.ts
new file mode 100644
index 000000000..9b70c12b6
--- /dev/null
+++ b/apps/desktop/e2e/partial-history-notice.spec.ts
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type { Page } from '@playwright/test';
+import { expect, test } from './fixtures';
+
+const NOTICE = '.maka-transcript-history-controls';
+
+async function waitForPaint(page: Page): Promise<void> {
+ await page.evaluate(() => new Promise<void>((resolve) => {
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
+ }));
+}
+
+async function noticePresentation(page: Page) {
+ return page.locator(NOTICE).evaluate((notice) => {
+ const style = getComputedStyle(notice);
+ const box = notice.getBoundingClientRect();
+ const composer = document.querySelector('.maka-composer-astryx');
+ if (!composer) throw new Error('the composer is missing');
+ const composerBox = composer.getBoundingClientRect();
+ return {
+ backgroundColor: style.backgroundColor,
+ borderWidths: [
+ style.borderTopWidth,
+ style.borderRightWidth,
+ style.borderBottomWidth,
+ style.borderLeftWidth,
+ ],
+ display: style.display,
+ flexWrap: style.flexWrap,
+ justifyContent: style.justifyContent,
+ widthDelta: Math.abs(box.width - composerBox.width),
+ centerDelta: Math.abs(
+ (box.left + box.right) / 2 - (composerBox.left + composerBox.right) / 2,
+ ),
+ fitsViewport: box.left >= 0 && box.right <= document.documentElement.clientWidth,
+ hasHorizontalOverflow: notice.scrollWidth > notice.clientWidth,
+ };
+ });
+}
+
+test('partial history is a quiet reading-column control with neutral rail ticks', async ({
+ partialHistoryWindow: page,
+}) => {
+ await page.setViewportSize({ width: 1_400, height: 800 });
+ await expect(page.locator(NOTICE)).toHaveCount(0);
+
+ const firstPrompt = page.locator(
+ '.maka-prompt-rail-tick[data-prompt-turn-id="turn-partial-history-1"]',
+ );
+ await expect(firstPrompt).toBeVisible();
+ await firstPrompt.click();
+
+ const notice = page.locator(NOTICE);
+ await expect(notice).toBeVisible();
+ await expect(notice).toContainText('正在查看较早的消息');
+ await expect(notice.getByRole('button', { name: '返回最新消息' })).toBeVisible();
+ await expect(notice).not.toContainText(/保存|加载/);
+
+ const regular = await noticePresentation(page);
+ expect(regular).toEqual({
+ backgroundColor: 'rgba(0, 0, 0, 0)',
+ borderWidths: ['0px', '0px', '0px', '0px'],
+ display: 'flex',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ widthDelta: expect.any(Number),
+ centerDelta: expect.any(Number),
+ fitsViewport: true,
+ hasHorizontalOverflow: false,
+ });
+ expect(regular.widthDelta).toBeLessThanOrEqual(1);
+ expect(regular.centerDelta).toBeLessThanOrEqual(1);
+
+ await page.mouse.move(0, 0);
+ await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur());
+ const railPresentation = await page.evaluate(() => {
+ const ticks = [...document.querySelectorAll<HTMLElement>('.maka-prompt-rail-tick')];
+ const presentation = (tick: HTMLElement) => {
+ const bar = tick.querySelector<HTMLElement>('.maka-prompt-rail-tick-bar');
+ if (!bar) throw new Error('a prompt rail tick is missing its bar');
+ const style = getComputedStyle(bar);
+ return {
+ backgroundColor: style.backgroundColor,
+ borderStyle: style.borderStyle,
+ borderWidth: style.borderWidth,
+ boxShadow: style.boxShadow,
+ };
+ };
+ const neutralPaint = ticks
+ .filter((tick) => tick.dataset.active !== 'true' && !tick.matches(':hover'))
+ .map(presentation);
+ const residentStyleRules = [...document.styleSheets].flatMap((sheet) =>
+ [...sheet.cssRules].filter((rule) => rule.cssText.includes('data-resident'))
+ );
+ return {
+ residentAttributeCount: document.querySelectorAll('[data-resident]').length,
+ residentStyleRuleCount: residentStyleRules.length,
+ neutralTickCount: neutralPaint.length,
+ neutralPaintCount: new Set(neutralPaint.map((paint) => JSON.stringify(paint))).size,
+ };
+ });
+ expect(railPresentation.residentAttributeCount).toBe(0);
+ expect(railPresentation.residentStyleRuleCount).toBe(0);
+ expect(railPresentation.neutralTickCount).toBeGreaterThan(1);
+ expect(railPresentation.neutralPaintCount).toBe(1);
+
+ await page.setViewportSize({ width: 520, height: 720 });
+ await waitForPaint(page);
+ const narrow = await noticePresentation(page);
+ expect(narrow.centerDelta).toBeLessThanOrEqual(1);
+ expect(narrow.fitsViewport).toBe(true);
+ expect(narrow.hasHorizontalOverflow).toBe(false);
+
+ await notice.getByRole('button', { name: '返回最新消息' }).click();
+ await expect(notice).toHaveCount(0);
+ await expect(
+ page.locator('[data-turn-id="turn-partial-history-8"]'),
+ ).toBeVisible();
+});
diff --git a/apps/desktop/src/main/e2e-fixture.ts b/apps/desktop/src/main/e2e-fixture.ts
index b8643cab1..dff4e6aab 100644
--- a/apps/desktop/src/main/e2e-fixture.ts
+++ b/apps/desktop/src/main/e2e-fixture.ts
@@ -34,11 +34,14 @@ import {
LONG_SIDEBAR_PROJECT_ID,
LONG_SIDEBAR_PROJECT_NAME,
LONG_SIDEBAR_SESSION_PREFIX,
+ PARTIAL_HISTORY_SESSION_ID,
PROMPT_RAIL_SESSION_ID,
TURN_SESSION_ID,
writeSession,
} from './e2e-fixture/seed-helpers.js';
import {
+ partialHistoryMessages,
+ partialHistorySession,
promptRailMessages,
promptRailSession,
turnMessages,
@@ -59,6 +62,7 @@ const E2E_FIXTURE_SCENARIOS = new Set<E2eFixtureScenario>([
'turn-narrative',
'turn-narrative-browser',
'chat-prompt-rail',
+ 'chat-partial-history',
'settings-data',
'settings-bots-onboarding',
'settings-general',
@@ -174,6 +178,8 @@ export function getE2eFixtureState(fixture: E2eFixture | null): E2eFixtureState
// this window scrolls smoothly is a per-launch choice (`scrollMotion`),
// because only the jump case needs it and it costs seconds of settling.
return { ...state, activeSessionId: PROMPT_RAIL_SESSION_ID, workbarCollapsed: true };
+ case 'chat-partial-history':
+ return { ...state, activeSessionId: PARTIAL_HISTORY_SESSION_ID, workbarCollapsed: true };
case 'settings-data':
return { ...state, activeSessionId: TURN_SESSION_ID, openSettingsSection: 'data' };
case 'settings-bots-onboarding':
@@ -225,6 +231,13 @@ export async function seedE2eFixture(input: {
if (scenario === 'chat-prompt-rail') {
await writeSession(input.workspaceRoot, promptRailSession(now), promptRailMessages(now));
}
+ if (scenario === 'chat-partial-history') {
+ await writeSession(
+ input.workspaceRoot,
+ partialHistorySession(now),
+ partialHistoryMessages(now),
+ );
+ }
if (scenario === 'sidebar-search-modal-open') {
for (const seed of longSidebarSessions(now)) {
await writeSession(input.workspaceRoot, seed.header, seed.messages);
diff --git a/apps/desktop/src/main/e2e-fixture/scenarios-chat.ts b/apps/desktop/src/main/e2e-fixture/scenarios-chat.ts
index 420ee2e51..760d9d588 100644
--- a/apps/desktop/src/main/e2e-fixture/scenarios-chat.ts
+++ b/apps/desktop/src/main/e2e-fixture/scenarios-chat.ts
@@ -20,6 +20,7 @@
import type { SessionHeader, StoredMessage } from '@maka/core/session';
import {
header,
+ PARTIAL_HISTORY_SESSION_ID,
PROMPT_RAIL_PROMPT_COUNT,
PROMPT_RAIL_SESSION_ID,
TURN_SESSION_ID,
@@ -148,3 +149,45 @@ export function promptRailMessages(now: number): StoredMessage[] {
}
return messages;
}
+
+export function partialHistorySession(now: number): SessionHeader {
+ return header({
+ id: PARTIAL_HISTORY_SESSION_ID,
+ name: '超长对话历史范围示例',
+ connection: 'zai-live',
+ model: 'glm-5.1',
+ now,
+ lastMessageAt: now - 60_000,
+ });
+}
+
+/**
+ * Eight turns whose durable transcript is well over the Desktop range budget.
+ * The whitespace is stored but collapses when rendered, keeping this a useful
+ * visual fixture while forcing the initial open to contain only the latest
+ * contiguous range.
+ */
+export function partialHistoryMessages(now: number): StoredMessage[] {
+ const messages: StoredMessage[] = [];
+ const rangePadding = ' '.repeat(180 * 1024);
+ for (let index = 1; index <= 8; index += 1) {
+ const turnId = `turn-partial-history-${index}`;
+ const ts = now - (9 - index) * 60_000;
+ messages.push({
+ type: 'user',
+ id: `msg-partial-history-user-${index}`,
+ turnId,
+ ts,
+ text: `第 ${index} 个问题:请概括这一阶段的实现进展。`,
+ });
+ messages.push({
+ type: 'assistant',
+ id: `msg-partial-history-assistant-${index}`,
+ turnId,
+ ts: ts + 1_000,
+ text: `第 ${index} 阶段已经完成关键实现,并通过了对应验证。${rangePadding}`,
+ modelId: 'glm-5.1',
+ });
+ }
+ return messages;
+}
diff --git a/apps/desktop/src/main/e2e-fixture/seed-helpers.ts b/apps/desktop/src/main/e2e-fixture/seed-helpers.ts
index 801267932..a6020dc29 100644
--- a/apps/desktop/src/main/e2e-fixture/seed-helpers.ts
+++ b/apps/desktop/src/main/e2e-fixture/seed-helpers.ts
@@ -34,6 +34,7 @@ export const E2E_FIXTURE_NOW = Date.UTC(2026, 4, 22, 3, 0, 0);
export const TURN_SESSION_ID = 'e2e-fixture-turn';
export const PROMPT_RAIL_SESSION_ID = 'e2e-fixture-prompt-rail';
+export const PARTIAL_HISTORY_SESSION_ID = 'e2e-fixture-partial-history';
/** Exceeds both the 64-tick rail and 100-turn mounted-window bounds. */
export const PROMPT_RAIL_PROMPT_COUNT = 120;
export const LONG_SIDEBAR_SESSION_PREFIX = 'e2e-fixture-sidebar-long-';
diff --git a/apps/desktop/src/renderer/chat-message-surface.tsx b/apps/desktop/src/renderer/chat-message-surface.tsx
index 87afd71ee..538913b29 100644
--- a/apps/desktop/src/renderer/chat-message-surface.tsx
+++ b/apps/desktop/src/renderer/chat-message-surface.tsx
@@ -249,7 +249,6 @@ export function ChatMessageSurface({
onLoadEarlierHistory={onLoadEarlierHistory}
returnToLatest={hasNewerHistory ? {
title: transcriptCopy.partialHistoryTitle,
- description: transcriptCopy.partialHistoryDescription,
label: transcriptCopy.returnLatest,
isPending: historyLoadPending,
onClick: onReturnToLatestHistory,
diff --git a/apps/desktop/src/renderer/locales/conversation-copy.ts b/apps/desktop/src/renderer/locales/conversation-copy.ts
index de6adcdb1..24ea1a230 100644
--- a/apps/desktop/src/renderer/locales/conversation-copy.ts
+++ b/apps/desktop/src/renderer/locales/conversation-copy.ts
@@ -58,7 +58,6 @@ export interface DesktopConversationCopy {
modelReboundDescription: (modelId?: string) => string;
messageReadFailedTitle: string;
partialHistoryTitle: string;
- partialHistoryDescription: string;
returnLatest: string;
scrollMainToBottom: string;
};
@@ -423,7 +422,7 @@ function enDetail(parts: readonly string[]): string {
const COPY = {
zh: {
- actions: { stopFailedTitle: '停止失败', stopFailedFallback: '任务操作失败,请稍后重试。', refreshSessionsFailedTitle: '刷新任务列表失败', refreshSessionsFailedFallback: '刷新任务列表失败,请稍后重试。', conversationErrorTitle: '任务出错', conversationErrorFallback: '任务运行失败,请稍后重试。', regenerateStartedTitle: '已发起重新生成', regenerateStartedDescription: '正在生成新的一轮回答', branchCreatedTitle: '已创建分支', branchCreatedDescription: (name) => `新任务 ${name}`, revisionStartedTitle: '已创建修改版草稿', revisionStartedDescription: '原任务仍会保留;修改后发送将在新版本中继续', rev [...]
+ actions: { stopFailedTitle: '停止失败', stopFailedFallback: '任务操作失败,请稍后重试。', refreshSessionsFailedTitle: '刷新任务列表失败', refreshSessionsFailedFallback: '刷新任务列表失败,请稍后重试。', conversationErrorTitle: '任务出错', conversationErrorFallback: '任务运行失败,请稍后重试。', regenerateStartedTitle: '已发起重新生成', regenerateStartedDescription: '正在生成新的一轮回答', branchCreatedTitle: '已创建分支', branchCreatedDescription: (name) => `新任务 ${name}`, revisionStartedTitle: '已创建修改版草稿', revisionStartedDescription: '原任务仍会保留;修改后发送将在新版本中继续', rev [...]
attachments: { tooMany: '附件数量超过 8 个', tooLarge: '附件大小超过 50MB', duplicate: '附件来源重复,请勿重复添加同一文件。' },
model: {
fakeBackendLabel: '本地模拟连接',
@@ -651,7 +650,7 @@ const COPY = {
turnError: { unknown: '出错了,原因不明。重新发消息重试。', contextOverflow: '上下文超出模型窗口限制,减少附件或开启新任务。', contextBudgetExhausted: '上下文已达上限,这个任务无法继续。换模型或开启新任务。', malformedSummary: '上下文压缩未能生成有效摘要。请检查模型的上下文窗口设置、切换模型,或开启新任务。', timeout: '模型请求超时,重新发消息重试。', auth: '模型鉴权失败,请到设置里重新连接或登录。', providerBilling: '模型服务计费受限,请检查账号余额或订阅状态。', providerCapacity: '模型服务暂时满载,等几分钟重试,或换一个模型。', rateLimit: '模型请求太频繁被限流了,等一会儿再发消息重试。', network: '网络连接失败,检查网络后重新发消息。', provider: '模型服务返回错误,稍后重试或换一个模型。', stepCap: '达到工具调用步数上限,�
��务可能没做完。发消息让它继 [...]
},
en: {
- actions: { stopFailedTitle: 'Failed to stop', stopFailedFallback: 'The task action failed. Try again later.', refreshSessionsFailedTitle: 'Failed to refresh tasks', refreshSessionsFailedFallback: 'The task list could not be refreshed. Try again later.', conversationErrorTitle: 'Task error', conversationErrorFallback: 'The task run failed. Try again later.', regenerateStartedTitle: 'Regeneration started', regenerateStartedDescription: 'Generating a new response', branchCreatedTitle: ' [...]
+ actions: { stopFailedTitle: 'Failed to stop', stopFailedFallback: 'The task action failed. Try again later.', refreshSessionsFailedTitle: 'Failed to refresh tasks', refreshSessionsFailedFallback: 'The task list could not be refreshed. Try again later.', conversationErrorTitle: 'Task error', conversationErrorFallback: 'The task run failed. Try again later.', regenerateStartedTitle: 'Regeneration started', regenerateStartedDescription: 'Generating a new response', branchCreatedTitle: ' [...]
attachments: { tooMany: 'You can attach at most 8 files', tooLarge: 'Attachments must be 50 MB or smaller', duplicate: 'This attachment was already added.' },
model: {
fakeBackendLabel: 'Local simulation',
diff --git a/apps/desktop/src/renderer/styles/chat-message.css b/apps/desktop/src/renderer/styles/chat-message.css
index 12f14e623..5ecb26749 100644
--- a/apps/desktop/src/renderer/styles/chat-message.css
+++ b/apps/desktop/src/renderer/styles/chat-message.css
@@ -318,33 +318,6 @@
padding: var(--space-0-5) var(--space-1-5);
}
.maka-transcript-history-controls {
- display: flex;
- box-sizing: border-box;
- align-items: center;
- justify-content: space-between;
- flex-wrap: wrap;
- gap: var(--space-2) var(--space-4);
- margin: var(--space-2) clamp(var(--space-3), 4vw, var(--space-10));
- padding: var(--space-2) var(--space-3);
- border: var(--border-width-hairline) solid var(--border-soft);
- border-radius: var(--radius-element);
- background: var(--surface-raised);
-}
-
-.maka-transcript-history-copy {
- display: flex;
- min-width: min(100%, 28rem);
- flex: 1 1 28rem;
- flex-direction: column;
- gap: var(--space-0-5);
-}
-
-.maka-transcript-history-title {
- font: var(--maka-text-label);
- color: var(--foreground);
-}
-
-.maka-transcript-history-description {
- font: var(--maka-text-supporting);
- color: var(--muted-foreground);
+ width: min(var(--maka-reading-measure), calc(100% - (2 * var(--space-6))));
+ margin: var(--space-2) auto 0;
}
diff --git a/apps/desktop/src/renderer/styles/prompt-rail.css b/apps/desktop/src/renderer/styles/prompt-rail.css
index c3c50b037..25fc0453d 100644
--- a/apps/desktop/src/renderer/styles/prompt-rail.css
+++ b/apps/desktop/src/renderer/styles/prompt-rail.css
@@ -179,20 +179,6 @@
transform: scaleX(1);
}
-/* Complete landmark indexes can outlive the bounded transcript bodies they
- point at. Keep unloaded prompts actionable, but draw them as outlines so the
- rail cannot imply that every landmark is already present in the DOM. */
-.maka-prompt-rail-tick[data-resident="false"] .maka-prompt-rail-tick-bar {
- box-sizing: border-box;
- border: var(--border-width-hairline) solid currentColor;
- background: transparent;
-}
-
-.maka-prompt-rail-tick[data-resident="false"]:is(:hover, [data-active="true"])
- .maka-prompt-rail-tick-bar {
- background: currentColor;
-}
-
/* HoverCard content. Astryx owns the card itself — surface, radius, shadow,
padding — so these rules only stack and clamp the two lines and set the two
text tiers, which stay on the same foreground aliases the transcript uses. */
@@ -222,7 +208,3 @@
-webkit-box-orient: vertical;
overflow: hidden;
}
-
-.maka-prompt-rail-preview-residency {
- color: var(--muted-foreground);
-}
diff --git a/docs/astryx-surface-file-inventory.md b/docs/astryx-surface-file-inventory.md
index 326a2af32..1588a9edb 100644
--- a/docs/astryx-surface-file-inventory.md
+++ b/docs/astryx-surface-file-inventory.md
@@ -197,7 +197,7 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi
| `packages/ui/src/chat-model-switcher.tsx` | shell-chrome-or-panel | Button | aligned — uses Astryx (Button) | 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, HStack, IconButton, Spinner, Token, Tooltip | aligned — uses Astryx (Badge, Banner, Button, HStack, IconButton, Spinner, Token, Tooltip) | aligned |
-| `packages/ui/src/chat-view.tsx` | shell-chrome-or-panel | Button, EmptyState, Spinner | aligned — uses Astryx (Button, EmptyState, Spinner) | aligned |
+| `packages/ui/src/chat-view.tsx` | shell-chrome-or-panel | Button, EmptyState, HStack, Spinner, Text | aligned — uses Astryx (Button, EmptyState, HStack, Spinner, Text) | aligned |
| `packages/ui/src/components.tsx` | ui-composition | none | aligned — no raw controls; no Astryx JSX usage | aligned |
| `packages/ui/src/composer-message-queue.tsx` | shell-chrome-or-panel | Button, IconButton, List, ListItem | raw `<textarea` (API Use-the-System) | polish |
| `packages/ui/src/composer.tsx` | shell-chrome-or-panel | Button, ChatComposer, IconButton, Lightbox, Token, Tooltip | aligned — uses Astryx (Button, ChatComposer, IconButton, Lightbox, Token, Tooltip) | aligned |
diff --git a/packages/core/src/e2e-fixture.ts b/packages/core/src/e2e-fixture.ts
index 065a8187f..b68429724 100644
--- a/packages/core/src/e2e-fixture.ts
+++ b/packages/core/src/e2e-fixture.ts
@@ -27,6 +27,7 @@ export type E2eFixtureScenario =
| 'turn-narrative'
| 'turn-narrative-browser'
| 'chat-prompt-rail'
+ | 'chat-partial-history'
| 'settings-data'
| 'settings-bots-onboarding'
| 'settings-general'
diff --git a/packages/ui/src/__tests__/prompt-anchor-rail.test.ts b/packages/ui/src/__tests__/prompt-anchor-rail.test.ts
index 1e0f1a080..3812b1144 100644
--- a/packages/ui/src/__tests__/prompt-anchor-rail.test.ts
+++ b/packages/ui/src/__tests__/prompt-anchor-rail.test.ts
@@ -268,7 +268,7 @@ test('keeps the active tick visible when the rail viewport resizes', () => {
assert.equal(disconnected, true);
});
-test('marks complete-index landmarks outside the resident transcript range', () => {
+test('merges complete-index landmarks with the resident transcript range', () => {
const turns = mergePromptAnchorRailTurns(
[
{ turnId: 'turn-1', label: 'Prompt 1', reply: 'Answer 1' },
@@ -287,26 +287,23 @@ test('marks complete-index landmarks outside the resident transcript range', ()
label: 'Prompt 1',
reply: 'Answer 1',
sequence: 0,
- isResident: true,
},
{
turnId: 'turn-2',
label: 'Prompt 2',
reply: '',
sequence: 2,
- isResident: false,
},
{
turnId: 'turn-3',
label: 'Prompt 3',
reply: 'Answer 3',
sequence: 4,
- isResident: true,
},
]);
});
-test('treats every projected turn as resident without a durable landmark index', () => {
+test('preserves every projected turn without a durable landmark index', () => {
assert.deepEqual(
mergePromptAnchorRailTurns([
{ turnId: 'overlay-turn', label: 'Streaming prompt', reply: '' },
@@ -315,12 +312,11 @@ test('treats every projected turn as resident without a durable landmark index',
turnId: 'overlay-turn',
label: 'Streaming prompt',
reply: '',
- isResident: true,
}],
);
});
-test('updates a landmark when its body enters a later resident range', () => {
+test('updates landmark content when its body enters a later resident range', () => {
const index = [
{ turnId: 'turn-1', sequence: 0, label: 'Prompt 1' },
{ turnId: 'turn-2', sequence: 2, label: 'Prompt 2' },
@@ -334,26 +330,27 @@ test('updates a landmark when its body enters a later resident range', () => {
index,
);
- assert.deepEqual(historical.map((turn) => turn.isResident), [true, false]);
- assert.deepEqual(intermediate.map((turn) => turn.isResident), [false, true]);
+ assert.deepEqual(historical.map((turn) => turn.reply), ['Answer 1', '']);
+ assert.deepEqual(intermediate.map((turn) => turn.reply), ['', 'Answer 2']);
});
-test('renders unloaded landmarks as actionable load targets', () => {
+test('keeps unloaded landmarks visually uniform and actionable', () => {
const markup = renderToStaticMarkup(createElement(LocaleProvider, {
locale: 'en',
children: createElement(PromptAnchorRail, {
turns: [
- { turnId: 'turn-1', label: 'Prompt 1', sequence: 0, isResident: true },
- { turnId: 'turn-2', label: 'Prompt 2', sequence: 2, isResident: false },
- { turnId: 'turn-3', label: 'Prompt 3', sequence: 4, isResident: true },
+ { turnId: 'turn-1', label: 'Prompt 1', sequence: 0 },
+ { turnId: 'turn-2', label: 'Prompt 2', sequence: 2 },
+ { turnId: 'turn-3', label: 'Prompt 3', sequence: 4 },
],
scrollRef: { current: null },
}),
}));
assert.match(markup, /data-prompt-turn-id="turn-2"/);
- assert.match(markup, /data-resident="false"/);
- assert.match(markup, /aria-label="Load and jump to prompt: Prompt 2"/);
+ assert.doesNotMatch(markup, /data-resident/);
+ assert.match(markup, /aria-label="Jump to prompt: Prompt 2"/);
+ assert.doesNotMatch(markup, /Not currently loaded/);
assert.doesNotMatch(markup, /aria-disabled="true"/);
});
diff --git a/packages/ui/src/__tests__/transcript-history-notice.test.tsx b/packages/ui/src/__tests__/transcript-history-notice.test.tsx
index 4d50cf313..2a26d78b3 100644
--- a/packages/ui/src/__tests__/transcript-history-notice.test.tsx
+++ b/packages/ui/src/__tests__/transcript-history-notice.test.tsx
@@ -25,8 +25,7 @@ import { TranscriptHistoryNotice } from '../chat-view.js';
function renderNotice(isPending: boolean): string {
return renderToStaticMarkup(
<TranscriptHistoryNotice
- title="Viewing an earlier part of this conversation"
- description="Newer messages are still saved, but are not currently loaded."
+ title="Viewing earlier messages"
actionLabel="Return to latest"
isPending={isPending}
onReturnToLatest={() => undefined}
@@ -34,22 +33,23 @@ function renderNotice(isPending: boolean): string {
);
}
-test('explains a partial historical range as persistent accessible status', () => {
+test('presents historical position as quiet persistent status', () => {
const markup = renderNotice(false);
assert.match(markup, /role="status"/);
assert.match(markup, /aria-live="polite"/);
assert.match(markup, /aria-atomic="true"/);
- assert.match(markup, /Viewing an earlier part of this conversation/);
- assert.match(markup, /Newer messages are still saved, but are not currently loaded\./);
+ assert.match(markup, /Viewing earlier messages/);
assert.match(markup, /Return to latest/);
+ assert.doesNotMatch(markup, /saved|loaded/);
+ assert.doesNotMatch(markup, /<strong/);
assert.doesNotMatch(markup, /disabled/);
});
-test('keeps the explanation visible while return-to-latest is pending', () => {
+test('keeps the position status visible while return-to-latest is pending', () => {
const markup = renderNotice(true);
- assert.match(markup, /Viewing an earlier part of this conversation/);
- assert.match(markup, /Newer messages are still saved, but are not currently loaded\./);
+ assert.match(markup, /Viewing earlier messages/);
+ assert.doesNotMatch(markup, /saved|loaded/);
assert.match(markup, /disabled/);
});
diff --git a/packages/ui/src/chat-view.tsx b/packages/ui/src/chat-view.tsx
index 0cd838973..2efc296d3 100644
--- a/packages/ui/src/chat-view.tsx
+++ b/packages/ui/src/chat-view.tsx
@@ -41,7 +41,7 @@ import type {
ShellRunUpdate,
} from '@maka/core/events';
import { isDeepResearchSession } from '@maka/core/explore-agent';
-import { Button, ButtonGroup, ChatMessageList, EmptyState, Spinner } from '@astryxdesign/core';
+import { Button, ButtonGroup, ChatMessageList, EmptyState, HStack, Spinner, Text } from '@astryxdesign/core';
import { useChatLayoutContext } from '@astryxdesign/core/Chat';
import { useLayer } from '@astryxdesign/core/Layer';
import { materializeChat } from './materialize.js';
@@ -71,31 +71,30 @@ export interface LiveContentActivationSnapshot {
export interface TranscriptHistoryNoticeProps {
title: string;
- description: string;
actionLabel: string;
isPending: boolean;
onReturnToLatest(): Promise<void> | void;
}
-/** Persistent explanation for a bounded range that omits newer durable messages. */
+/** Persistent navigation position with a direct path back to the transcript tail. */
export function TranscriptHistoryNotice({
title,
- description,
actionLabel,
isPending,
onReturnToLatest,
}: TranscriptHistoryNoticeProps) {
return (
- <div
+ <HStack
className="maka-transcript-history-controls"
+ gap={2}
+ hAlign="center"
+ vAlign="center"
+ wrap="wrap"
role="status"
aria-live="polite"
aria-atomic="true"
>
- <div className="maka-transcript-history-copy">
- <strong className="maka-transcript-history-title">{title}</strong>
- <span className="maka-transcript-history-description">{description}</span>
- </div>
+ <Text type="supporting" color="secondary">{title}</Text>
<Button
label={actionLabel}
variant="ghost"
@@ -105,7 +104,7 @@ export function TranscriptHistoryNotice({
void onReturnToLatest();
}}
/>
- </div>
+ </HStack>
);
}
@@ -265,7 +264,6 @@ export function ChatView(props: {
onLoadEarlierHistory?(): Promise<void> | void;
returnToLatest?: {
title: string;
- description: string;
label: string;
isPending: boolean;
onClick(): Promise<void> | void;
@@ -670,7 +668,6 @@ export function ChatView(props: {
{props.returnToLatest ? (
<TranscriptHistoryNotice
title={props.returnToLatest.title}
- description={props.returnToLatest.description}
actionLabel={props.returnToLatest.label}
isPending={props.returnToLatest.isPending}
onReturnToLatest={() =>
diff --git a/packages/ui/src/conversation-copy.ts b/packages/ui/src/conversation-copy.ts
index 39aac5c23..e8f2b94e1 100644
--- a/packages/ui/src/conversation-copy.ts
+++ b/packages/ui/src/conversation-copy.ts
@@ -420,8 +420,6 @@ export interface ConversationCopy {
promptRailAriaLabel: string;
emptyPrompt: string;
jumpToPrompt: (preview: string) => string;
- loadPrompt: (preview: string) => string;
- unloadedPrompt: string;
};
}
@@ -545,7 +543,7 @@ const CONVERSATION_COPY = {
sessions: {
status: { active: '可继续', running: '进行中', waiting_for_user: '等你确认', blocked: '需要处理', aborted: '已中止' },
blockedReason: { NO_REAL_CONNECTION: '等待配置可用模型连接', auth: '需要重新登录', permission_required: '等待权限确认', tool_failed: '工具调用失败', unknown: '运行中断,可重试' },
- listAriaLabel: '任务列表', showMore: '显示更多', showMoreAriaLabel: (count) => `显示 ${count} 条更多任务`, renameAriaLabel: '重命名任务', renameProjectTitle: '重命名项目', renameSubmit: '保存', respondingAriaLabel: '正在响应', respondingTitle: '任务正在流式响应中', staleTitle: '此任务使用的模型连接已不可用,发送时会切换到默认连接', staleAriaLabel: '任务已过期', stale: '已过期', unreadAriaLabel: '未读消息', actionsAriaLabel: (name) => `${name} 任务操作`, pin: '置顶', unpin: '取消置顶', rename: '重命名', archive: '归档', unarchive: '取消归档', delete: '删除', pinned: '置顶', recent: [...]
+ listAriaLabel: '任务列表', showMore: '显示更多', showMoreAriaLabel: (count) => `显示 ${count} 条更多任务`, renameAriaLabel: '重命名任务', renameProjectTitle: '重命名项目', renameSubmit: '保存', respondingAriaLabel: '正在响应', respondingTitle: '任务正在流式响应中', staleTitle: '此任务使用的模型连接已不可用,发送时会切换到默认连接', staleAriaLabel: '任务已过期', stale: '已过期', unreadAriaLabel: '未读消息', actionsAriaLabel: (name) => `${name} 任务操作`, pin: '置顶', unpin: '取消置顶', rename: '重命名', archive: '归档', unarchive: '取消归档', delete: '删除', pinned: '置顶', recent: [...]
},
},
en: {
@@ -693,7 +691,7 @@ const CONVERSATION_COPY = {
sessions: {
status: { active: 'Ready', running: 'Running', waiting_for_user: 'Waiting for you', blocked: 'Needs attention', aborted: 'Stopped' },
blockedReason: { NO_REAL_CONNECTION: 'Waiting for an available model connection', auth: 'Sign in again', permission_required: 'Waiting for permission', tool_failed: 'Tool call failed', unknown: 'Run interrupted; retry available' },
- listAriaLabel: 'Task list', showMore: 'Show more', showMoreAriaLabel: (count) => `Show ${count} more tasks`, renameAriaLabel: 'Rename task', renameProjectTitle: 'Rename project', renameSubmit: 'Save', respondingAriaLabel: 'Responding', respondingTitle: 'This task is streaming a response', staleTitle: 'This task\'s model connection is unavailable; sending will switch to the default connection', staleAriaLabel: 'Stale task', stale: 'Stale', unreadAriaLabel: 'Unread messages', actions [...]
+ listAriaLabel: 'Task list', showMore: 'Show more', showMoreAriaLabel: (count) => `Show ${count} more tasks`, renameAriaLabel: 'Rename task', renameProjectTitle: 'Rename project', renameSubmit: 'Save', respondingAriaLabel: 'Responding', respondingTitle: 'This task is streaming a response', staleTitle: 'This task\'s model connection is unavailable; sending will switch to the default connection', staleAriaLabel: 'Stale task', stale: 'Stale', unreadAriaLabel: 'Unread messages', actions [...]
},
},
} satisfies UiCatalog<ConversationCopy>;
diff --git a/packages/ui/src/prompt-anchor-rail.tsx b/packages/ui/src/prompt-anchor-rail.tsx
index 5cabad2ee..707a80340 100644
--- a/packages/ui/src/prompt-anchor-rail.tsx
+++ b/packages/ui/src/prompt-anchor-rail.tsx
@@ -214,7 +214,6 @@ export interface PromptAnchorRailTurn {
label: string;
reply?: string;
sequence?: number;
- isResident: boolean;
}
export function mergePromptAnchorRailTurns(
@@ -222,7 +221,7 @@ export function mergePromptAnchorRailTurns(
index?: ReadonlyArray<{ turnId: string; sequence: number; label: string }>,
): PromptAnchorRailTurn[] {
if (!index || index.length === 0) {
- return loadedTurns.map((turn) => ({ ...turn, isResident: true }));
+ return loadedTurns.map((turn) => ({ ...turn }));
}
const loadedByTurnId = new Map(loadedTurns.map((turn) => [turn.turnId, turn]));
return index.map((landmark) => {
@@ -234,7 +233,6 @@ export function mergePromptAnchorRailTurns(
reply: '',
}),
sequence: landmark.sequence,
- isResident: loaded !== undefined,
};
});
}
@@ -539,9 +537,6 @@ export const PromptAnchorRail = memo(function PromptAnchorRail({ turns, scrollRe
const isActive = turn.turnId === activeTurnIdRef.current;
const preview = turn.label.trim() || copy.emptyPrompt;
const replyPreview = (turn.reply ?? '').replace(/\s+/g, ' ').trim().slice(0, 140);
- const actionLabel = turn.isResident
- ? copy.jumpToPrompt(preview)
- : copy.loadPrompt(preview);
const proximity =
hoveredIndex === null
? HOVER_FALLOFF_TICKS
@@ -555,9 +550,7 @@ export const PromptAnchorRail = memo(function PromptAnchorRail({ turns, scrollRe
content={
<span className="maka-prompt-rail-preview">
<span className="maka-prompt-rail-preview-prompt">{preview}</span>
- {!turn.isResident ? (
- <span className="maka-prompt-rail-preview-residency">{copy.unloadedPrompt}</span>
- ) : replyPreview ? (
+ {replyPreview ? (
<span className="maka-prompt-rail-preview-reply">{replyPreview}</span>
) : null}
</span>
@@ -567,10 +560,9 @@ export const PromptAnchorRail = memo(function PromptAnchorRail({ turns, scrollRe
type="button"
variant="ghost"
size="sm"
- label={actionLabel}
+ label={copy.jumpToPrompt(preview)}
className="maka-prompt-rail-tick"
data-prompt-turn-id={turn.turnId}
- data-resident={turn.isResident ? 'true' : 'false'}
data-active={isActive ? 'true' : undefined}
aria-current={isActive ? 'true' : undefined}
onClick={() => jumpTo(turn)}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org