mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 03:56:55 +03:00
fix(app): remove the message menu experiment (#58867)
This commit is contained in:
committed by
GitHub
Unverified
parent
80dc5235f5
commit
fc7af90caf
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
}
|
||||
>
|
||||
<MarkdownProvider readImage={readImage()} openSession={openReferencedSession} sessionTitle={sessionTitle}>
|
||||
<MessageMenuProvider enabled={messageMenu()}>{props.children}</MessageMenuProvider>
|
||||
{props.children}
|
||||
</MarkdownProvider>
|
||||
</DataProvider>
|
||||
</LocalProvider>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <context.Provider value={() => props.enabled}>{props.children}</context.Provider>
|
||||
}
|
||||
|
||||
export function useMessageMenu() {
|
||||
return useContext(context)
|
||||
}
|
||||
@@ -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<typeof useData>, 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 (
|
||||
<For each={props.items}>
|
||||
@@ -153,35 +146,6 @@ function MessageActionButton(
|
||||
)
|
||||
}
|
||||
|
||||
function MessageOptions(props: { info: string[]; children: JSX.Element }) {
|
||||
const i18n = useI18n()
|
||||
|
||||
return (
|
||||
<div data-slot="message-options">
|
||||
<Menu gutter={4} modal={false} placement="bottom-end">
|
||||
<Menu.Trigger
|
||||
as={IconButton}
|
||||
icon={<Icon name="outline-dots" size="small" />}
|
||||
size="normal"
|
||||
variant="ghost-muted"
|
||||
aria-label={i18n.t("ui.message.options")}
|
||||
/>
|
||||
<Menu.Portal>
|
||||
<Menu.Content>
|
||||
<Show when={props.info.length > 0}>
|
||||
<div data-slot="message-options-info">
|
||||
<For each={props.info}>{(line) => <span dir="auto">{line}</span>}</For>
|
||||
</div>
|
||||
<Menu.Separator />
|
||||
</Show>
|
||||
{props.children}
|
||||
</Menu.Content>
|
||||
</Menu.Portal>
|
||||
</Menu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function UserMessageTransition(props: { active: boolean; children: JSX.Element }) {
|
||||
return (
|
||||
<span
|
||||
@@ -356,7 +320,6 @@ export function CurrentUserMessageDisplay(props: {
|
||||
const data = useData()
|
||||
const dialog = useDialog()
|
||||
const i18n = useI18n()
|
||||
const menu = useMessageMenu()
|
||||
|
||||
const [state, setState] = createStore({ copied: false, reverting: false, forking: false, updating: false })
|
||||
|
||||
@@ -395,13 +358,6 @@ export function CurrentUserMessageDisplay(props: {
|
||||
return i18n.t(RELATIVE_TIME_KEYS[relative.unit], { count: numfmt().format(relative.count) })
|
||||
})
|
||||
|
||||
const info = createMemo(() =>
|
||||
[
|
||||
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}
|
||||
>
|
||||
<Show when={menu() && (props.text || comments().length > 0 || pending())}>
|
||||
<MessageOptions info={info()}>
|
||||
<Show when={props.actions?.pending && pending()}>
|
||||
<Menu.Item disabled={state.updating} onSelect={() => void updatePending("queue")}>
|
||||
{i18n.t("ui.message.moveToQueue")}
|
||||
</Menu.Item>
|
||||
<Menu.Item disabled={state.updating} onSelect={() => void updatePending("remove")}>
|
||||
{i18n.t("ui.message.deletePending")}
|
||||
</Menu.Item>
|
||||
</Show>
|
||||
<Show when={props.actions?.revert && !pending()}>
|
||||
<Menu.Item disabled={state.reverting} onSelect={() => void revert()}>
|
||||
{i18n.t("ui.message.revertMessage")}
|
||||
</Menu.Item>
|
||||
</Show>
|
||||
<Show when={props.text}>
|
||||
<Menu.Item onSelect={() => void copy(props.text)}>{i18n.t("ui.message.copyMessage")}</Menu.Item>
|
||||
</Show>
|
||||
<Show when={props.voice}>
|
||||
<Menu.Item onSelect={() => void copy(props.message.text)}>{i18n.t("ui.message.copyFullMessage")}</Menu.Item>
|
||||
</Show>
|
||||
</MessageOptions>
|
||||
</Show>
|
||||
{renderAttachments()}
|
||||
<Show
|
||||
when={props.text}
|
||||
@@ -713,14 +646,13 @@ export function CurrentUserMessageDisplay(props: {
|
||||
<UserMessageComments comments={comments()} bounded />
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={!menu() && folded()}>
|
||||
<Show when={folded()}>
|
||||
<div data-slot="user-message-run-actions">{renderActions()}</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
{/* 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. */}
|
||||
<Show when={menu() ? waiting() : props.text || comments().length > 0 || pending()}>
|
||||
<Show when={props.text || comments().length > 0 || pending()}>
|
||||
<div data-slot="user-message-copy-wrapper">
|
||||
<span data-slot="user-message-meta-wrap">
|
||||
<span data-slot="user-message-meta">
|
||||
@@ -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: {
|
||||
<div data-slot="text-part-body">
|
||||
<PacedMarkdown text={props.text} cacheKey={props.id} streaming={props.message.time.completed === undefined} />
|
||||
</div>
|
||||
<Show when={menu() && props.showCopy}>
|
||||
<MessageOptions
|
||||
info={[
|
||||
metaText(labels()),
|
||||
[duration(), interrupted() ? i18n.t("ui.message.interrupted") : ""].filter(Boolean).join(" \u00B7 "),
|
||||
].filter(Boolean)}
|
||||
>
|
||||
<Menu.Item onSelect={() => void copy()}>{i18n.t("ui.message.copyResponse")}</Menu.Item>
|
||||
</MessageOptions>
|
||||
</Show>
|
||||
<Show when={sources()} fallback={<Show when={!menu() && props.showCopy}>{copyFooter()}</Show>}>
|
||||
<Show when={sources()} fallback={<Show when={props.showCopy}>{copyFooter()}</Show>}>
|
||||
{(value) => (
|
||||
<div data-slot="text-part-footer">
|
||||
<SourcesChip
|
||||
@@ -1051,7 +972,7 @@ export function AssistantTextContent(props: {
|
||||
appear={rendered}
|
||||
onOpen={() => props.onOpenSources?.()}
|
||||
/>
|
||||
<Show when={!menu()}>{copyFooter()}</Show>
|
||||
{copyFooter()}
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
@@ -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<string, string>()
|
||||
@@ -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 })
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user