diff --git a/packages/app/src/session/timeline/message-menu.ts b/packages/app/src/session/timeline/message-menu.ts deleted file mode 100644 index 5b99101006a..00000000000 --- a/packages/app/src/session/timeline/message-menu.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { motionTuning } from "@opencode/ui/motion-tuning" -import { createSignal } from "solid-js" - -const toggle = motionTuning.toggle("session.messageMenu", { - label: "Message menu", - group: "Composer", - experiment: true, - value: false, -}) - -const [enabled, setEnabled] = createSignal(toggle()) - -motionTuning.subscribe(() => setEnabled(toggle())) - -/** Experiment: a ⋯ menu per message in a 28px trailing gutter replaces the hover info line, and turns sit 24px apart. */ -export const messageMenu = enabled diff --git a/packages/app/src/session/timeline/message-timeline.tsx b/packages/app/src/session/timeline/message-timeline.tsx index 429f6e519af..93d7d6bf6a7 100644 --- a/packages/app/src/session/timeline/message-timeline.tsx +++ b/packages/app/src/session/timeline/message-timeline.tsx @@ -36,7 +36,6 @@ import { import { SessionHeaderSpacer } from "@/session/header/session-header" import { SessionHeaderGap } from "@/session/header/drag-area" import { SessionWorkingIndicator } from "@/session/header/session-working-indicator" -import { messageMenu } from "@/session/timeline/message-menu" type BlockingTask = { type: "shell" | "subagent"; partID: string; id?: string; label?: string } @@ -172,8 +171,7 @@ function MessageTimelineView( if (!directory) return void data.location.vcs.sync({ directory }).catch(() => undefined) }) - // The message menu experiment reserves a 28px trailing gutter (24px button + 4px gap) for its ⋯ button. - const turnPadding = () => (messageMenu() ? "ps-4 pe-11 md:ps-6 md:pe-13" : "px-4 md:px-6") + const turnPadding = () => "px-4 md:px-6" const showHeader = createMemo(() => !props.hideHeader && (props.data.showHeader() || workspaceSession())) const pinned = createMemo(() => props.pinned) const messageByID = projection.messageByID diff --git a/packages/app/src/shell/routes/session-ui-provider.tsx b/packages/app/src/shell/routes/session-ui-provider.tsx index b2b3ce465f2..4a7a2f3b2f3 100644 --- a/packages/app/src/shell/routes/session-ui-provider.tsx +++ b/packages/app/src/shell/routes/session-ui-provider.tsx @@ -1,7 +1,5 @@ import { DataProvider } from "@opencode/session-ui/context" import { MarkdownProvider, type ReadMarkdownImage } from "@opencode/session-ui/context/markdown" -import { MessageMenuProvider } from "@opencode/session-ui/context/message-menu" -import { messageMenu } from "@/session/timeline/message-menu" import { useNavigate, useParams } from "@solidjs/router" import { createMemo, type ParentProps } from "solid-js" import { useProviders } from "@/providers/catalog/providers" @@ -142,7 +140,7 @@ export function SessionUIProvider( } > - {props.children} + {props.children} diff --git a/packages/session-ui/src/components/message-part.css b/packages/session-ui/src/components/message-part.css index 534a5d4c259..a0ca4a19f14 100644 --- a/packages/session-ui/src/components/message-part.css +++ b/packages/session-ui/src/components/message-part.css @@ -1740,53 +1740,3 @@ color: var(--v2-text-text-muted); transition: none; } - -/* The message options button sits in the 28px gutter the timeline reserves on the trailing side. */ -[data-component="user-message"], -[data-component="text-part"] { - position: relative; - - [data-slot="message-options"] { - position: absolute; - top: calc(0.5lh - 12px); - inset-inline-end: -28px; - z-index: 1; - display: flex; - - @media (hover: hover) { - opacity: 0; - } - } - - &:hover [data-slot="message-options"], - &:focus-within [data-slot="message-options"], - [data-slot="message-options"]:has([data-expanded]) { - opacity: 1; - } -} - -/* Centered on the bubble's first line. */ -[data-component="user-message"] [data-slot="message-options"] { - top: calc(8px + 0.5lh - 12px); -} - -[data-slot="message-options-info"] { - display: flex; - flex-direction: column; - gap: 2px; - max-width: 340px; - padding: 4px 8px; - font-family: var(--v2-font-family-sans); - font-size: 12px; - font-weight: 440; - line-height: var(--line-height-compact); - color: var(--v2-text-text-faint); - user-select: text; - cursor: default; - - > span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } -} diff --git a/packages/session-ui/src/context/message-menu.tsx b/packages/session-ui/src/context/message-menu.tsx deleted file mode 100644 index 3ae9e036bcb..00000000000 --- a/packages/session-ui/src/context/message-menu.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { createContext, useContext, type ParentProps } from "solid-js" - -const context = createContext<() => boolean>(() => false) - -/** Experiment: a ⋯ menu per message in a trailing gutter replaces the hover info line, and turns sit further apart. */ -export function MessageMenuProvider(props: ParentProps<{ enabled: boolean }>) { - return props.enabled}>{props.children} -} - -export function useMessageMenu() { - return useContext(context) -} diff --git a/packages/session-ui/src/message/message-content.tsx b/packages/session-ui/src/message/message-content.tsx index 2b28398ada9..b7d52dfe877 100644 --- a/packages/session-ui/src/message/message-content.tsx +++ b/packages/session-ui/src/message/message-content.tsx @@ -21,7 +21,6 @@ import { Markdown } from "../components/markdown" import { linkSegments } from "../components/link-segments" import { sessionReferenceText, setupSessionLinks } from "../components/markdown-session-links" import { useMarkdown } from "../context/markdown" -import { useMessageMenu } from "../context/message-menu" import { useSources } from "../context/sources" import { SourcesChip } from "../sources/sources-chip" import type { TurnSources } from "../sources/sources" @@ -100,12 +99,6 @@ function modelItems(data: ReturnType, model: SessionMessageAssis return [{ label: name }, { label: model.variant[0]?.toUpperCase() + model.variant.slice(1), variant: true }] } -function metaText(items: MetaItem[]) { - return items - .map((item, index) => (index === 0 ? item.label : item.variant ? ` ${item.label}` : ` \u00B7 ${item.label}`)) - .join("") -} - function MetaItems(props: { items: MetaItem[]; slot: "user-message" | "text-part" }) { return ( @@ -153,35 +146,6 @@ function MessageActionButton( ) } -function MessageOptions(props: { info: string[]; children: JSX.Element }) { - const i18n = useI18n() - - return ( -
- - } - size="normal" - variant="ghost-muted" - aria-label={i18n.t("ui.message.options")} - /> - - - 0}> -
- {(line) => {line}} -
- -
- {props.children} -
-
-
-
- ) -} - function UserMessageTransition(props: { active: boolean; children: JSX.Element }) { return ( - [ - waiting() ? i18n.t("ui.message.pending") : "", - metaText(labels()), - fulltimefmt().format(props.message.time.created), - ].filter(Boolean), - ) // In a run, only the last message keeps the meta line; the others offer their actions beside the bubble. // Bubble corners tighten where two bubbles meet, as in the iOS app's grouped corners. @@ -667,29 +623,6 @@ export function CurrentUserMessageDisplay(props: { }} data-timeline-part-id={props.text ? `${props.message.id}:text:0` : undefined} > - 0 || pending())}> - - - void updatePending("queue")}> - {i18n.t("ui.message.moveToQueue")} - - void updatePending("remove")}> - {i18n.t("ui.message.deletePending")} - - - - void revert()}> - {i18n.t("ui.message.revertMessage")} - - - - void copy(props.text)}>{i18n.t("ui.message.copyMessage")} - - - void copy(props.message.text)}>{i18n.t("ui.message.copyFullMessage")} - - - {renderAttachments()} - +
{renderActions()}
{/* Like the TUI, a pending steer of only attachments still offers its actions. */} - {/* With the message menu, a pending steer still keeps its Pending label and actions in view. */} - 0 || pending()}> + 0 || pending()}>
@@ -965,7 +897,6 @@ export function AssistantTextContent(props: { }) { const data = useData() const i18n = useI18n() - const menu = useMessageMenu() const sourcesHost = useSources() const numfmt = createMemo(() => new Intl.NumberFormat(i18n.locale())) // A footer that arrives after the reply rendered belongs to a turn that just finished, so its chip fades in. @@ -1032,17 +963,7 @@ export function AssistantTextContent(props: {
- - - void copy()}>{i18n.t("ui.message.copyResponse")} - - - {copyFooter()}}> + {copyFooter()}}> {(value) => (
props.onOpenSources?.()} /> - {copyFooter()} + {copyFooter()}
)} diff --git a/packages/session-ui/src/timeline/session-timeline-row.tsx b/packages/session-ui/src/timeline/session-timeline-row.tsx index de7b8f2e2f5..99b120b5868 100644 --- a/packages/session-ui/src/timeline/session-timeline-row.tsx +++ b/packages/session-ui/src/timeline/session-timeline-row.tsx @@ -25,7 +25,6 @@ import { springStyle } from "@opencode/ui/motion/solid" import { Dynamic } from "solid-js/web" import type { SessionUserActions, SessionUserAttachmentReference, SessionUserComment } from "../actions" import { useData } from "../context" -import { useMessageMenu } from "../context/message-menu" import { useSources } from "../context/sources" import { turnSources } from "../sources/sources" import { TimelineSeparator } from "../components/timeline-separator" @@ -104,7 +103,6 @@ export function createSessionTimelineRowRenderer(input: { }) { const i18n = useI18n() const data = useData() - const menu = useMessageMenu() const sourcesHost = useSources() // Cached timelines retain file-change subgroup identities alongside their disclosure choices. const patchGroupKeys = input.disclosure.patchGroupKeys ?? new Map() @@ -897,10 +895,9 @@ export function createSessionTimelineRowRenderer(input: { function TurnGap(props: { messageID: string }) { let element: HTMLDivElement | undefined const joined = run(props.messageID) - const turn = () => (menu() ? 24 : 16) springStyle( () => element, - () => ({ height: joined()?.above ? 4 : turn() }), + () => ({ height: joined()?.above ? 4 : 16 }), (el, value) => { el.style.height = `${value.height}px` }, @@ -913,7 +910,7 @@ export function createSessionTimelineRowRenderer(input: { ref={element} data-timeline-row="TurnGap" aria-hidden="true" - style={{ height: untrack(joined)?.above ? "4px" : `${untrack(turn)}px` }} + style={{ height: untrack(joined)?.above ? "4px" : "16px" }} /> ) } @@ -1191,8 +1188,10 @@ export function createSessionTimelineRowRenderer(input: { // A budget pauses paid models, or only the ones it covers, until it resets at the start of a month. const budgetLimit = createMemo(() => { const kind = current().kind + if (kind !== "budget-limit" && kind !== "model-budget-limit") return const at = current().resetAt + const time = at === undefined ? undefined @@ -1203,6 +1202,7 @@ export function createSessionTimelineRowRenderer(input: { minute: "2-digit", timeZoneName: "shortOffset", }).format(at) + if (kind === "budget-limit") return { text: time @@ -1210,6 +1210,7 @@ export function createSessionTimelineRowRenderer(input: { : i18n.t("ui.sessionTurn.budgetLimit.allUntilReset"), action: i18n.t("ui.sessionTurn.budgetLimit.useFreeModel"), } + return { text: time ? i18n.t("ui.sessionTurn.budgetLimit.models", { time }) diff --git a/packages/ui/src/i18n/en.ts b/packages/ui/src/i18n/en.ts index 7eede14a2ca..37178d5f9a1 100644 --- a/packages/ui/src/i18n/en.ts +++ b/packages/ui/src/i18n/en.ts @@ -285,7 +285,6 @@ const source = { "ui.message.expand": "Expand message", "ui.message.collapse": "Collapse message", "ui.message.copy": "Copy", - "ui.message.options": "Message options", "ui.message.copyMessage": "Copy message", "ui.message.copyFullMessage": "Copy full message", "ui.message.forkMessage": "Fork from here",