From f1ac2226037c80e3e9125da726be409a2cf533e6 Mon Sep 17 00:00:00 2001 From: DoSun Date: Thu, 16 Jul 2026 18:08:54 +0800 Subject: [PATCH 1/6] perf(app-ai-native): tighten timeline gap and shed per-message DOM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the gap-3 that was forcing 12px between every message and only apply mt-3 at turn boundaries (user messages after the first), so assistants sit tight against their parent user message again. Strip per-message wrappers in TimelineMessage: the fake scroll-content slot, the interaction shim, and the duplicate container around the assistant slot are gone. The assistant branch now reuses the session-turn-assistant-content slot directly. Drop createAutoScroll from TimelineMessage entirely — it was creating a store, two effects, and a ResizeObserver per message for a scroll viewport that lives on the outer ScrollView. Also drop the now-unused MessageTransition wrapper and dead queued memo in MessageTimeline. Co-Authored-By: CoStrict --- .../src/pages/session/message-timeline.tsx | 43 +-- .../ui/src/components/timeline-message.tsx | 309 ++++++++---------- 2 files changed, 162 insertions(+), 190 deletions(-) diff --git a/packages/app-ai-native/src/pages/session/message-timeline.tsx b/packages/app-ai-native/src/pages/session/message-timeline.tsx index 98cca9275..4d483db4e 100644 --- a/packages/app-ai-native/src/pages/session/message-timeline.tsx +++ b/packages/app-ai-native/src/pages/session/message-timeline.tsx @@ -20,7 +20,6 @@ import { useLanguage } from "@/context/language" import { useSettings } from "@/context/settings" import { useSessionChat } from "@/context/session-chat" import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note" -import { MessageTransition } from "./message-transition" type MessageComment = { path: string @@ -511,7 +510,7 @@ export function MessageTimeline(props: {
0 || props.historyMore}> -
+
- - - +
) }} diff --git a/packages/ui/src/components/timeline-message.tsx b/packages/ui/src/components/timeline-message.tsx index e0eb408a8..79e7e07ad 100644 --- a/packages/ui/src/components/timeline-message.tsx +++ b/packages/ui/src/components/timeline-message.tsx @@ -15,7 +15,6 @@ import { Icon } from "./icon" import { TextShimmer } from "./text-shimmer" import { SessionRetry } from "./session-retry" import { TextReveal } from "./text-reveal" -import { createAutoScroll } from "../hooks" import { useI18n } from "../context/i18n" import { getDirectory, getFilename } from "@opencode-ai/util/path" @@ -127,8 +126,7 @@ export function TimelineMessage(props: { showReasoningSummaries?: boolean shellToolDefaultOpen?: boolean editToolDefaultOpen?: boolean - onUserInteracted?: () => void - classes?: { root?: string; content?: string; container?: string } + classes?: { root?: string; container?: string } }) { const data = useData() const i18n = useI18n() @@ -281,178 +279,161 @@ export function TimelineMessage(props: { return t.completed - t.created }) - const autoScroll = createAutoScroll({ - working, - onUserInteracted: props.onUserInteracted, - overflowAnchor: "dynamic", - }) - return (
-
-
- -
-
- -
- -
- -
-
- -
- - - - -
+ +
+
+ +
+ +
+ +
+
+ +
+ + + - - 0 && !working()}> -
- setDiffState("open", value)} - variant="ghost" - > - -
-
- - {i18n.t("ui.sessionReview.change.modified")} - - - {edited()} {i18n.t(edited() === 1 ? "ui.common.file.one" : "ui.common.file.other")} - -
- - -
-
+
+ + + 0 && !working()}> +
+ setDiffState("open", value)} + variant="ghost" + > + +
+
+ + {i18n.t("ui.sessionReview.change.modified")} + + + {edited()} {i18n.t(edited() === 1 ? "ui.common.file.one" : "ui.common.file.other")} + +
+ +
- - - -
- - setDiffState("expanded", Array.isArray(value) ? value : value ? [value] : []) - } - > - - {(diff) => { - const active = createMemo(() => diffState.expanded.includes(diff.file)) - const [visible, setVisible] = createSignal(false) - createEffect( - on( - active, - (value) => { - if (!value) { - setVisible(false) - return - } - requestAnimationFrame(() => { - if (!active()) return - setVisible(true) - }) - }, - { defer: true }, - ), - ) - return ( - - - -
- - - - {`\u202A${getDirectory(diff.file)}\u202C`} - - - - {getFilename(diff.file)} - +
+
+ + + +
+ + setDiffState("expanded", Array.isArray(value) ? value : value ? [value] : []) + } + > + + {(diff) => { + const active = createMemo(() => diffState.expanded.includes(diff.file)) + const [visible, setVisible] = createSignal(false) + createEffect( + on( + active, + (value) => { + if (!value) { + setVisible(false) + return + } + requestAnimationFrame(() => { + if (!active()) return + setVisible(true) + }) + }, + { defer: true }, + ), + ) + return ( + + + +
+ + + + {`\u202A${getDirectory(diff.file)}\u202C`} -
- - - - - - -
-
-
-
- - -
- -
-
-
-
- ) - }} -
-
-
-
-
- -
- + + + {getFilename(diff.file)} + + +
+ + + + + + +
+
+ + + + +
+ +
+
+
+ + ) + }} + + +
+
+ +
+
+
- -
-
- -
- -
- -
-
- - - {errorText()} - - + +
+ + +
+
+ + + {errorText()} + +
-
+
) } From 80a32f45f90745b06f10b59e42c4c175c9d44d55 Mon Sep 17 00:00:00 2001 From: DoSun Date: Thu, 16 Jul 2026 19:53:28 +0800 Subject: [PATCH 2/6] fix(ui): compute per-assistant duration from prior message anchor Assistant messages stamp time.created === time.completed at finalization, so completed - created is always 0. Walk backwards from each assistant to the previous message's anchor (prior assistant's completed, or the user message's created) so consecutive assistants show distinct durations instead of a shared turn value or zero. Co-Authored-By: CoStrict --- .../src/pages/session/message-timeline.tsx | 7 ++-- packages/ui/src/components/message-part.tsx | 19 ++++------ packages/ui/src/components/session-turn.tsx | 16 --------- .../ui/src/components/timeline-message.tsx | 35 +++++++++++++++---- 4 files changed, 38 insertions(+), 39 deletions(-) diff --git a/packages/app-ai-native/src/pages/session/message-timeline.tsx b/packages/app-ai-native/src/pages/session/message-timeline.tsx index 4d483db4e..4982d2452 100644 --- a/packages/app-ai-native/src/pages/session/message-timeline.tsx +++ b/packages/app-ai-native/src/pages/session/message-timeline.tsx @@ -510,7 +510,7 @@ export function MessageTimeline(props: {
0 || props.historyMore}> -
+
+ +
+ + {t("workspace.device.createWorkspace")} + + +
- -
- - - - + +
+ {t("workspace.createFromDevice")} +
+ + + + + + +
- -
- {t("workspace.createFromDevice")} -
- - - - - - + 0} + fallback={ +
+ + {t("workspace.createFromDevice.empty")}
-
- 0} - fallback={ -
- - {t("workspace.createFromDevice.empty")} -
- } - > -
    - - {(device) => { - const state = () => upgrade.upgradeMap()[device.deviceId] - const hasUpgrade = () => - !!device.canUpdate && - device.status === "online" && - !state() && - !upgrade.isRecentlyUpgraded(device.deviceId) - return ( - - ) - }} - -
-
- - -
+ } + > +
    + + {(device) => { + const state = () => upgrade.upgradeMap()[device.deviceId] + const hasUpgrade = () => + !!device.canUpdate && + device.status === "online" && + !state() && + !upgrade.isRecentlyUpgraded(device.deviceId) + return ( + + ) + }} + +
+ +
+
@@ -302,7 +270,7 @@ export function WorkspaceSidebar(props: { hide?: () => void } = {}) {
{(id) => } - +
{t("workspace.empty")} From a72087370fda58f71006778cc3f921b6567268df Mon Sep 17 00:00:00 2001 From: DoSun Date: Thu, 16 Jul 2026 20:50:40 +0800 Subject: [PATCH 6/6] fix(app-ai-native): clarify workspace sidebar empty-state hints Reword the device-registration prompt to point at the home page guide and note that only online idle workspaces can be activated. Co-Authored-By: CoStrict --- packages/app-ai-native/src/i18n/en.ts | 4 ++-- packages/app-ai-native/src/i18n/zh.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/app-ai-native/src/i18n/en.ts b/packages/app-ai-native/src/i18n/en.ts index b855e9840..197104453 100644 --- a/packages/app-ai-native/src/i18n/en.ts +++ b/packages/app-ai-native/src/i18n/en.ts @@ -2105,7 +2105,7 @@ export const dict = { "workspace.running": "Running", "workspace.idle": "Idle", "workspace.running.empty": "No running workspaces", - "workspace.running.emptyHint": "Click an idle workspace below to activate", + "workspace.running.emptyHint": "Click an online idle workspace below to activate", "workspace.empty": "No workspaces", "workspace.emptyHint": "Click the + button above to create one", "workspace.close": "Close", @@ -2128,7 +2128,7 @@ export const dict = { "workspace.content.closeAll": "Close All", "workspace.createFromDevice": "Create from", - "workspace.createFromDevice.empty": "Please register a device first", + "workspace.createFromDevice.empty": "Follow the guide on the home page to connect your device and complete registration", "workspace.device.list": "Device List", "workspace.device.search": "Search devices...", "workspace.device.notFound": "No devices found", diff --git a/packages/app-ai-native/src/i18n/zh.ts b/packages/app-ai-native/src/i18n/zh.ts index e0c79b7f7..eabc0307b 100644 --- a/packages/app-ai-native/src/i18n/zh.ts +++ b/packages/app-ai-native/src/i18n/zh.ts @@ -1850,7 +1850,7 @@ export const dict = { "workspace.running": "运行中", "workspace.idle": "空闲", "workspace.running.empty": "暂无运行中工作空间", - "workspace.running.emptyHint": "点击下方空闲工作空间以激活", + "workspace.running.emptyHint": "点击下方在线的空闲工作空间以激活", "workspace.empty": "暂无工作空间", "workspace.emptyHint": "点击上方的 + 按钮创建", "workspace.close": "关闭", @@ -1873,7 +1873,7 @@ export const dict = { "workspace.content.closeAll": "关闭全部", "workspace.createFromDevice": "创建自", - "workspace.createFromDevice.empty": "请先完成设备注册", + "workspace.createFromDevice.empty": "请根据主页的引导连接开发设备,完成设备注册", "workspace.device.list": "设备列表", "workspace.device.search": "搜索设备...", "workspace.device.notFound": "未找到设备",