From 372116115d3e7eee4e706c37063e4e1f2bfea696 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 13 May 2026 14:01:08 +0700 Subject: [PATCH 1/6] Tune chat feed spacing --- packages/ui/src/components/message-part.css | 4 ++-- packages/ui/src/components/session-turn.css | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index c84a36892242..ff1d689cadc6 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -132,7 +132,7 @@ overflow: hidden; background: var(--surface-base); border: 1px solid var(--border-weak-base); - padding: 8px 12px; + padding: 6px 12px; border-radius: 6px; [data-highlight="file"] { @@ -209,7 +209,7 @@ [data-component="text-part"] { width: 100%; - margin-top: 24px; + margin-top: 4px; [data-slot="text-part-body"] { margin-top: 0; diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index 54076f3f8939..25b3d9d1d4ad 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -227,5 +227,5 @@ } [data-slot="session-turn-list"] { - gap: 24px; + gap: 4px; } From 9c5e727ff633a89e4ff5392f49639d5c66529f0d Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 13 May 2026 14:01:09 +0700 Subject: [PATCH 2/6] Refine tool summaries and chat spacing --- packages/app/src/components/prompt-input.tsx | 2 +- packages/ui/src/components/basic-tool.css | 105 ++++++++++ packages/ui/src/components/basic-tool.tsx | 52 +++++ packages/ui/src/components/collapsible.css | 8 + packages/ui/src/components/message-part.css | 10 +- packages/ui/src/components/message-part.tsx | 190 ++++++++++-------- packages/ui/src/components/session-turn.css | 2 +- .../ui/src/components/tool-error-card.tsx | 4 +- 8 files changed, 278 insertions(+), 95 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 77a18430d249..09c5cf12441a 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -134,7 +134,7 @@ export const PromptInput: Component = (props) => { let slashPopoverRef!: HTMLDivElement const mirror = { input: false } - const inset = 56 + const inset = 44 const space = `${inset}px` const scrollCursorIntoView = () => { diff --git a/packages/ui/src/components/basic-tool.css b/packages/ui/src/components/basic-tool.css index 198412dcb9f9..e910adb19354 100644 --- a/packages/ui/src/components/basic-tool.css +++ b/packages/ui/src/components/basic-tool.css @@ -33,6 +33,17 @@ gap: 8px; } + &[data-multiline="true"] { + align-items: flex-start; + + [data-slot="basic-tool-tool-trigger-content"], + [data-slot="basic-tool-tool-info"] { + flex: 1 1 auto; + width: 100%; + max-width: 100%; + } + } + [data-slot="basic-tool-tool-indicator"] { width: 16px; height: 16px; @@ -181,6 +192,100 @@ } } +[data-component="tool-summary-trigger"] { + display: flex; + flex-direction: column; + gap: 0; + width: 100%; + min-width: 0; + max-width: 100%; + font-family: var(--font-family-mono); + font-size: 14px; + line-height: var(--line-height-large); + color: var(--text-base); + text-align: left; + + [data-slot="tool-summary-main"] { + display: flex; + align-items: baseline; + gap: 8px; + min-width: 0; + } + + [data-slot="tool-summary-dot"] { + width: 8px; + height: 8px; + border-radius: 999px; + flex-shrink: 0; + background: var(--icon-weak-base); + transform: translateY(-1px); + } + + &[data-state="success"] [data-slot="tool-summary-dot"] { + background: var(--icon-success-base); + } + + &[data-state="error"] [data-slot="tool-summary-dot"] { + background: var(--icon-critical-base); + } + + &[data-state="running"] [data-slot="tool-summary-dot"] { + background: var(--icon-warning-base); + } + + [data-slot="tool-summary-call"] { + display: flex; + align-items: baseline; + min-width: 0; + max-width: 100%; + } + + [data-slot="tool-summary-name"] { + flex-shrink: 0; + font-weight: var(--font-weight-medium); + color: var(--text-strong); + } + + [data-slot="tool-summary-paren"] { + flex-shrink: 0; + color: var(--text-base); + } + + [data-slot="tool-summary-subject"] { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--text-base); + } + + [data-slot="tool-summary-preview"] { + display: grid; + grid-template-columns: 16px minmax(0, 1fr); + gap: 4px; + align-items: start; + padding-left: 4px; + font-size: 13px; + color: var(--text-base); + } + + [data-slot="tool-summary-branch"] { + width: 10px; + height: 13px; + border-left: 1px solid var(--border-weak-hover); + border-bottom: 1px solid var(--border-weak-hover); + transform: translate(4px, -4px); + } + + [data-slot="tool-summary-preview-text"] { + justify-self: start; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} + [data-component="task-tool-card"] { width: 100%; min-width: 0; diff --git a/packages/ui/src/components/basic-tool.tsx b/packages/ui/src/components/basic-tool.tsx index 27ad7c3c7010..152a279a6b21 100644 --- a/packages/ui/src/components/basic-tool.tsx +++ b/packages/ui/src/components/basic-tool.tsx @@ -37,10 +37,59 @@ export interface BasicToolProps { onTriggerClick?: JSX.EventHandlerUnion triggerHref?: string clickable?: boolean + multilineTrigger?: boolean +} + +export interface ToolSummaryTriggerProps { + title: string + subject?: string + preview?: JSX.Element + status?: string + failed?: boolean } const SPRING = { type: "spring" as const, visualDuration: 0.35, bounce: 0 } +export function ToolSummaryTrigger(props: ToolSummaryTriggerProps) { + const pending = () => props.status === "pending" || props.status === "running" + const state = () => { + if (props.failed || props.status === "error") return "error" + if (pending()) return "running" + if (props.status === "completed") return "success" + return "neutral" + } + + return ( +
+
+ + + + + + + {(subject) => ( + <> + ( + {subject()} + ) + + )} + + +
+ + {(preview) => ( +
+
+ )} +
+
+ ) +} + export function BasicTool(props: BasicToolProps) { const [state, setState] = createStore({ open: props.defaultOpen ?? false, @@ -129,6 +178,7 @@ export function BasicTool(props: BasicToolProps) { data-component="tool-trigger" data-clickable={props.clickable ? "true" : undefined} data-hide-details={props.hideDetails ? "true" : undefined} + data-multiline={props.multilineTrigger ? "true" : undefined} >
@@ -202,6 +252,7 @@ export function BasicTool(props: BasicToolProps) { fallback={ {trigger()} @@ -213,6 +264,7 @@ export function BasicTool(props: BasicToolProps) { as="a" href={href()} data-hide-details={props.hideDetails ? "true" : undefined} + data-multiline={props.multilineTrigger ? "true" : undefined} onClick={props.onTriggerClick} > {trigger()} diff --git a/packages/ui/src/components/collapsible.css b/packages/ui/src/components/collapsible.css index 82c133f738aa..298837e8e34d 100644 --- a/packages/ui/src/components/collapsible.css +++ b/packages/ui/src/components/collapsible.css @@ -67,6 +67,14 @@ align-items: stretch; } + &[data-multiline="true"] { + height: auto; + min-height: 36px; + align-items: flex-start; + padding: 2px 0; + text-align: left; + } + [data-slot="collapsible-arrow"] { flex-shrink: 0; width: 24px; diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index ff1d689cadc6..6698cfc2cd51 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -132,7 +132,7 @@ overflow: hidden; background: var(--surface-base); border: 1px solid var(--border-weak-base); - padding: 6px 12px; + padding: 4px 10px; border-radius: 6px; [data-highlight="file"] { @@ -147,8 +147,8 @@ } [data-slot="user-message-copy-wrapper"] { - min-height: 24px; - margin-top: 4px; + min-height: 20px; + margin-top: 2px; display: flex; align-items: center; justify-content: flex-end; @@ -216,8 +216,8 @@ } [data-slot="text-part-copy-wrapper"] { - min-height: 24px; - margin-top: 4px; + min-height: 20px; + margin-top: 2px; display: flex; align-items: center; justify-content: flex-start; diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 9c0c90c00076..291aa4264e7a 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -5,7 +5,6 @@ import { createSignal, For, Match, - onMount, Show, Switch, onCleanup, @@ -34,7 +33,7 @@ import { useData } from "../context" import { useFileComponent } from "../context/file" import { useDialog } from "../context/dialog" import { type UiI18n, useI18n } from "../context/i18n" -import { BasicTool, GenericTool } from "./basic-tool" +import { BasicTool, GenericTool, ToolSummaryTrigger } from "./basic-tool" import { Accordion } from "./accordion" import { StickyAccordionHeader } from "./sticky-accordion-header" import { Collapsible } from "./collapsible" @@ -53,44 +52,10 @@ import { Spinner } from "./spinner" import { TextShimmer } from "./text-shimmer" import { AnimatedCountList } from "./tool-count-summary" import { ToolStatusTitle } from "./tool-status-title" -import { patchFiles } from "./apply-patch-file" -import { animate } from "motion" +import { patchFiles, type ApplyPatchFile } from "./apply-patch-file" import { useLocation } from "@solidjs/router" import { attached, inline, kind } from "./message-file" -function ShellSubmessage(props: { text: string; animate?: boolean }) { - let widthRef: HTMLSpanElement | undefined - let valueRef: HTMLSpanElement | undefined - - onMount(() => { - if (!props.animate) return - requestAnimationFrame(() => { - if (widthRef) { - animate(widthRef, { width: "auto" }, { type: "spring", visualDuration: 0.25, bounce: 0 }) - } - if (valueRef) { - animate(valueRef, { opacity: 1, filter: "blur(0px)" }, { duration: 0.32, ease: [0.16, 1, 0.3, 1] }) - } - }) - }) - - return ( - - - - - {props.text} - - - - - ) -} - interface Diagnostic { range: { start: { line: number; character: number } @@ -376,7 +341,7 @@ export function getToolInfo(tool: string, input: any = {}): ToolInfo { case "bash": return { icon: "console", - title: i18n.t("ui.tool.shell"), + title: "Bash", subtitle: input.description, } case "edit": @@ -422,6 +387,73 @@ export function getToolInfo(tool: string, input: any = {}): ToolInfo { } } +function stringValue(value: unknown) { + return typeof value === "string" ? value : undefined +} + +function firstOutputLine(value: unknown) { + const text = stringValue(value) + if (!text) return undefined + return stripAnsi(text) + .split(/\r?\n/) + .map((line) => line.trim()) + .find((line) => line.length > 0) +} + +function toolFailed(status: string | undefined, metadata: Record) { + if (status === "error") return true + const exit = metadata.exit ?? metadata.exitCode + return typeof exit === "number" && exit !== 0 +} + +function plural(count: number, singular: string, plural = `${singular}s`) { + return `${count} ${count === 1 ? singular : plural}` +} + +function patchChangeSummary(file: ApplyPatchFile) { + const changes = [] + if (file.additions) changes.push(plural(file.additions, "addition")) + if (file.deletions) changes.push(plural(file.deletions, "removal")) + return changes.length ? changes.join(" and ") : "no line changes" +} + +function applyPatchSummary(file: ApplyPatchFile | undefined, count: number, output: unknown) { + if (!file) { + return { + title: "apply_patch", + subject: count ? plural(count, "file") : undefined, + preview: count ? `Updated ${plural(count, "file")}` : firstOutputLine(output), + } + } + + switch (file.type) { + case "add": + return { + title: "apply_patch", + subject: file.relativePath, + preview: `Created ${file.relativePath} with ${plural(file.additions, "line")}`, + } + case "delete": + return { + title: "apply_patch", + subject: file.relativePath, + preview: `Deleted ${file.relativePath}`, + } + case "move": + return { + title: "apply_patch", + subject: file.relativePath, + preview: `Moved ${file.relativePath}`, + } + case "update": + return { + title: "apply_patch", + subject: file.relativePath, + preview: `Updated ${file.relativePath} with ${patchChangeSummary(file)}`, + } + } +} + function urls(text: string | undefined) { if (!text) return [] const seen = new Set() @@ -1821,12 +1853,13 @@ ToolRegistry.register({ name: "bash", render(props) { const i18n = useI18n() - const pending = () => props.status === "pending" || props.status === "running" - const sawPending = pending() + const command = createMemo(() => stringValue(props.input.command) ?? stringValue(props.metadata.command) ?? "") + const output = createMemo(() => stringValue(props.output) ?? stringValue(props.metadata.output) ?? "") + const preview = createMemo(() => firstOutputLine(output())) + const failed = createMemo(() => toolFailed(props.status, props.metadata)) const text = createMemo(() => { - const cmd = props.input.command ?? props.metadata.command ?? "" - const out = stripAnsi(props.output || props.metadata.output || "") - return `$ ${cmd}${out ? "\n\n" + out : ""}` + const out = stripAnsi(output()) + return `$ ${command()}${out ? "\n\n" + out : ""}` }) const [copied, setCopied] = createSignal(false) @@ -1842,17 +1875,15 @@ ToolRegistry.register({ -
- - - - - - -
-
+ } >
@@ -2020,12 +2051,13 @@ ToolRegistry.register({ const i18n = useI18n() const fileComponent = useFileComponent() const files = createMemo(() => patchFiles(props.metadata.files)) - const pending = createMemo(() => props.status === "pending" || props.status === "running") const single = createMemo(() => { const list = files() if (list.length !== 1) return return list[0] }) + const summary = createMemo(() => applyPatchSummary(single(), files().length, props.output)) + const failed = createMemo(() => toolFailed(props.status, props.metadata)) const [expanded, setExpanded] = createSignal([]) let seeded = false @@ -2037,12 +2069,6 @@ ToolRegistry.register({ setExpanded(list.filter((f) => f.type !== "delete").map((f) => f.filePath)) }) - const subtitle = createMemo(() => { - const count = files().length - if (count === 0) return "" - return `${count} ${i18n.t(count > 1 ? "ui.common.file.other" : "ui.common.file.one")}` - }) - return ( + } > 0}> -
-
- - - - - {getFilename(single()!.relativePath)} - -
- -
- {getDirectory(single()!.relativePath)} -
-
-
-
- - - -
-
+ } > Date: Wed, 13 May 2026 14:01:09 +0700 Subject: [PATCH 3/6] Stabilize Android keyboard scrolling --- packages/android/index.html | 2 +- .../gen/android/app/src/main/AndroidManifest.xml | 1 + packages/android/src/entry-android.tsx | 12 ++++++++++++ packages/app/src/pages/session/message-timeline.tsx | 9 ++++++--- packages/ui/src/components/scroll-view.css | 5 +++++ packages/ui/src/hooks/create-auto-scroll.tsx | 11 +++++++++++ 6 files changed, 36 insertions(+), 4 deletions(-) diff --git a/packages/android/index.html b/packages/android/index.html index 115bc63b96ab..caf3d000ca96 100644 --- a/packages/android/index.html +++ b/packages/android/index.html @@ -20,7 +20,7 @@
diff --git a/packages/android/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/packages/android/src-tauri/gen/android/app/src/main/AndroidManifest.xml index e217244b4b95..0a8802b8eb01 100644 --- a/packages/android/src-tauri/gen/android/app/src/main/AndroidManifest.xml +++ b/packages/android/src-tauri/gen/android/app/src/main/AndroidManifest.xml @@ -15,6 +15,7 @@ android:launchMode="singleTask" android:label="@string/main_activity_title" android:name=".MainActivity" + android:windowSoftInputMode="adjustResize" android:exported="true"> diff --git a/packages/android/src/entry-android.tsx b/packages/android/src/entry-android.tsx index 9c080fac9eaa..ede875096400 100644 --- a/packages/android/src/entry-android.tsx +++ b/packages/android/src/entry-android.tsx @@ -262,6 +262,12 @@ const App = () => { document.documentElement.dataset.platform = "android" void refreshVoice() + const syncViewport = () => { + const height = window.visualViewport?.height ?? window.innerHeight + document.documentElement.style.setProperty("--android-viewport-height", `${height}px`) + } + syncViewport() + const handleClick = (event: MouseEvent) => { const link = (event.target as HTMLElement | null)?.closest("a.external-link") as HTMLAnchorElement | null if (!link?.href) return @@ -291,10 +297,16 @@ const App = () => { document.addEventListener("click", handleClick) window.addEventListener("focus", onFocus) + window.addEventListener("resize", syncViewport) + window.visualViewport?.addEventListener("resize", syncViewport) + window.visualViewport?.addEventListener("scroll", syncViewport) document.addEventListener("visibilitychange", onVisible) onCleanup(() => { document.removeEventListener("click", handleClick) window.removeEventListener("focus", onFocus) + window.removeEventListener("resize", syncViewport) + window.visualViewport?.removeEventListener("resize", syncViewport) + window.visualViewport?.removeEventListener("scroll", syncViewport) document.removeEventListener("visibilitychange", onVisible) stopListening() stopVoiceState() diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index e64fa9fc2a4c..551fbaec6b63 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -244,6 +244,7 @@ export function MessageTimeline(props: { const language = useLanguage() const { params, sessionKey } = useSessionKey() const platform = usePlatform() + const nativeMobile = platform.platform === "ios" || platform.platform === "android" const rendered = createMemo(() => props.renderedUserMessages.map((message) => message.id)) const sessionID = createMemo(() => params.id) @@ -1001,12 +1002,14 @@ export function MessageTimeline(props: {
0 || props.historyMore}> @@ -1048,8 +1051,8 @@ export function MessageTimeline(props: { "md:max-w-200 2xl:max-w-[1000px]": props.centered, }} style={{ - "content-visibility": active() ? undefined : "auto", - "contain-intrinsic-size": active() ? undefined : "auto 500px", + "content-visibility": active() || nativeMobile ? undefined : "auto", + "contain-intrinsic-size": active() || nativeMobile ? undefined : "auto 500px", }} > 0}> diff --git a/packages/ui/src/components/scroll-view.css b/packages/ui/src/components/scroll-view.css index 1ebc4e9496a5..fe67d0aa415c 100644 --- a/packages/ui/src/components/scroll-view.css +++ b/packages/ui/src/components/scroll-view.css @@ -17,6 +17,11 @@ flex-direction: unset; } +:root[data-platform="ios"] .scroll-view__viewport, +:root[data-platform="android"] .scroll-view__viewport { + overscroll-behavior-y: contain; +} + .scroll-view__viewport::-webkit-scrollbar { display: none; } diff --git a/packages/ui/src/hooks/create-auto-scroll.tsx b/packages/ui/src/hooks/create-auto-scroll.tsx index 1c4f1257a545..328d7e782601 100644 --- a/packages/ui/src/hooks/create-auto-scroll.tsx +++ b/packages/ui/src/hooks/create-auto-scroll.tsx @@ -191,6 +191,17 @@ export function createAutoScroll(options: AutoScrollOptions) { }, ) + createResizeObserver( + () => store.scrollRef, + () => { + const el = store.scrollRef + if (!el || !canScroll(el)) return + if (!active()) return + if (store.userScrolled) return + scrollToBottom(false) + }, + ) + createEffect( on(options.working, (working: boolean) => { settling = false From d64444ae5f6b1ddde3bbec25af6ae2a158175f9e Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 13 May 2026 14:01:09 +0700 Subject: [PATCH 4/6] Refine mobile session header --- packages/app/src/components/prompt-input.tsx | 16 + packages/app/src/pages/session.tsx | 42 +- .../src/pages/session/message-timeline.tsx | 1128 ++++++++++------- 3 files changed, 704 insertions(+), 482 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 09c5cf12441a..289795d67424 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -28,6 +28,7 @@ import { Select } from "@opencode-ai/ui/select" import { useDialog } from "@opencode-ai/ui/context/dialog" import { ModelSelectorPopover } from "@/components/dialog-select-model" import { useProviders } from "@/hooks/use-providers" +import { getSessionContextMetrics } from "@/components/session/session-context-metrics" import { useCommand } from "@/context/command" import { Persist, persisted } from "@/utils/persist" import { usePermission } from "@/context/permission" @@ -1127,6 +1128,11 @@ export const PromptInput: Component = (props) => { }) const variants = createMemo(() => ["default", ...local.model.variant.list()]) + const sessionMessages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) + const contextTokens = createMemo( + () => getSessionContextMetrics(sessionMessages(), providers.all()).context?.total ?? 0, + ) + const contextTokenLabel = createMemo(() => contextTokens().toLocaleString(language.intl())) const accepting = createMemo(() => { const id = params.id if (!id) return permission.isAutoAcceptingDirectory(sdk.directory) @@ -1745,6 +1751,16 @@ export const PromptInput: Component = (props) => {
+ +
+ Context: + {contextTokenLabel()} + t +
+
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 133cce4e1482..e20ba4852296 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -23,7 +23,6 @@ import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange import { createStore } from "solid-js/store" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Select } from "@opencode-ai/ui/select" -import { Tabs } from "@opencode-ai/ui/tabs" import { createAutoScroll } from "@opencode-ai/ui/hooks" import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge" import { Button } from "@opencode-ai/ui/button" @@ -1961,34 +1960,6 @@ export default function Page() { {sessionSync() ?? ""}
- - {/* UPSTREAM-DIVERGENCE: Mobile uses a compact chat switcher so the primary flow keeps more vertical space. */} - - - setStore("mobileTab", "session")} - > - {language.t("session.tab.session")} - - setStore("mobileTab", "changes")} - > - {hasReview() - ? `${reviewCount()} ${language.t( - reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other", - )}` - : language.t("session.review.change.other")} - - - - - {/* Session panel */}
setStore("mobileTab", value), + } + : undefined + } mobileFallback={reviewContent({ diffStyle: "unified", classes: { diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index 551fbaec6b63..70639054964f 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -12,6 +12,7 @@ import { InlineInput } from "@opencode-ai/ui/inline-input" import { Spinner } from "@opencode-ai/ui/spinner" import { SessionTurn } from "@opencode-ai/ui/session-turn" import { ScrollView } from "@opencode-ai/ui/scroll-view" +import { Tabs } from "@opencode-ai/ui/tabs" import { TextField } from "@opencode-ai/ui/text-field" import type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2" import { showToast } from "@opencode-ai/ui/toast" @@ -213,6 +214,11 @@ function createTimelineStaging(input: TimelineStageInput) { export function MessageTimeline(props: { mobileChanges: boolean mobileFallback: JSX.Element + mobileTabs?: { + value: "session" | "changes" + changesLabel: string + onChange: (value: "session" | "changes") => void + } actions?: UserActions scroll: { overflow: boolean; bottom: boolean; jump: boolean } onResumeScroll: () => void @@ -352,6 +358,7 @@ export function MessageTimeline(props: { pendingRename: false, pendingShare: false, }) + const showTimelineHeader = createMemo(() => showHeader() && (!props.mobileTabs || title.editing)) let titleRef: HTMLInputElement | undefined const [share, setShare] = createStore({ @@ -627,498 +634,713 @@ export function MessageTimeline(props: { ) } - return ( - {props.mobileFallback}
} - > -
-
- -
- { - const root = e.currentTarget - const delta = normalizeWheelDelta({ - deltaY: e.deltaY, - deltaMode: e.deltaMode, - rootHeight: root.clientHeight, - }) - if (!delta) return - markBoundaryGesture({ root, target: e.target, delta, onMarkScrollGesture: props.onMarkScrollGesture }) - }} - onTouchStart={(e) => { - touchGesture = e.touches[0]?.clientY - }} - onTouchMove={(e) => { - const next = e.touches[0]?.clientY - const prev = touchGesture - touchGesture = next - if (next === undefined || prev === undefined) return - - const delta = prev - next - if (!delta) return - - const root = e.currentTarget - markBoundaryGesture({ root, target: e.target, delta, onMarkScrollGesture: props.onMarkScrollGesture }) - }} - onTouchEnd={() => { - touchGesture = undefined - }} - onTouchCancel={() => { - touchGesture = undefined - }} - onPointerDown={(e) => { - if (e.target !== e.currentTarget) return - props.onMarkScrollGesture(e.currentTarget) - }} - onScroll={(e) => { - props.onScheduleScrollState(e.currentTarget) - props.onTurnBackfillScroll() - if (!props.hasScrollGesture()) return - props.onUserScroll() - props.onAutoScrollHandleScroll() - props.onMarkScrollGesture(e.currentTarget) - }} - onClick={props.onAutoScrollInteraction} - class="relative min-w-0 w-full h-full" - style={{ - "--session-title-height": showHeader() ? "40px" : "0px", - "--sticky-accordion-top": showHeader() ? "48px" : "0px", - }} - > -
- -
{ - head = el - setBar("ms", pace(el.clientWidth)) - }} - data-session-title + - -