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