|
|
@@ -11,10 +11,8 @@ import {
|
|
|
type Accessor,
|
|
|
type JSX,
|
|
|
} from "solid-js"
|
|
|
-import { createStore, produce } from "solid-js/store"
|
|
|
+import { createStore } from "solid-js/store"
|
|
|
import { Dynamic } from "solid-js/web"
|
|
|
-import { useNavigate } from "@solidjs/router"
|
|
|
-import { useMutation } from "@tanstack/solid-query"
|
|
|
import { createVirtualizer, defaultRangeExtractor, elementScroll, type VirtualItem } from "@tanstack/solid-virtual"
|
|
|
import { Accordion } from "@opencode-ai/ui/accordion"
|
|
|
import { Button } from "@opencode-ai/ui/button"
|
|
|
@@ -35,8 +33,6 @@ import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
|
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
|
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
|
|
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
|
|
-import { Dialog } from "@opencode-ai/ui/dialog"
|
|
|
-import { DialogFooter, DialogHeader, DialogTitleGroup, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2"
|
|
|
import { InlineInput } from "@opencode-ai/ui/inline-input"
|
|
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
|
|
import { SessionRetry } from "@opencode-ai/session-ui/session-retry"
|
|
|
@@ -45,43 +41,22 @@ import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
|
|
|
import { TextField } from "@opencode-ai/ui/text-field"
|
|
|
import { TextReveal } from "@opencode-ai/ui/text-reveal"
|
|
|
import { TextShimmer } from "@opencode-ai/ui/text-shimmer"
|
|
|
-import type {
|
|
|
- AssistantMessage,
|
|
|
- Message as MessageType,
|
|
|
- Part as PartType,
|
|
|
- ToolPart,
|
|
|
- UserMessage,
|
|
|
-} from "@opencode-ai/sdk/v2"
|
|
|
-import { showToast } from "@/utils/toast"
|
|
|
+import type { AssistantMessage, ToolPart, UserMessage } from "@opencode-ai/sdk/v2"
|
|
|
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
|
|
import { Popover as KobaltePopover } from "@kobalte/core/popover"
|
|
|
import { normalize } from "@opencode-ai/session-ui/session-diff"
|
|
|
import { useFileComponent } from "@opencode-ai/ui/context/file"
|
|
|
import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
|
|
|
import { SessionContextUsage } from "@/components/session-context-usage"
|
|
|
-import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|
|
import { useLanguage } from "@/context/language"
|
|
|
-import { useSessionKey } from "@/pages/session/session-layout"
|
|
|
-import { useServerSDK } from "@/context/server-sdk"
|
|
|
-import { usePlatform } from "@/context/platform"
|
|
|
-import { useSettings } from "@/context/settings"
|
|
|
-import { useTabs } from "@/context/tabs"
|
|
|
-import { legacySessionHref, requireServerKey, sessionHref } from "@/utils/session-route"
|
|
|
-import { useSDK } from "@/context/sdk"
|
|
|
-import { useSync } from "@/context/sync"
|
|
|
-import { notifySessionTabsRemoved } from "@/components/titlebar-session-events"
|
|
|
-import { sessionTitle } from "@/utils/session-title"
|
|
|
import { scheduleConnectedMeasure } from "./measure"
|
|
|
import { observeElementOffsetReconnectAware } from "./observe-element-offset"
|
|
|
-import { createTimelineProjection } from "./projection"
|
|
|
import { MessageComment, SummaryDiff, TimelineRow, TimelineRowMap } from "./rows"
|
|
|
import { filterVirtualIndexes } from "./virtual-items"
|
|
|
+import { createTimelineController, type TimelineController } from "./controller"
|
|
|
|
|
|
-const emptyMessages: MessageType[] = []
|
|
|
-const emptyParts: PartType[] = []
|
|
|
const emptyTools: ToolPart[] = []
|
|
|
const emptyAssistantMessages: AssistantMessage[] = []
|
|
|
-const idle = { type: "idle" as const }
|
|
|
|
|
|
type FramedTimelineRow = Exclude<TimelineRow.TimelineRow, { _tag: "TurnGap" }>
|
|
|
type TimelineRowByTag<T extends TimelineRow.TimelineRow["_tag"]> = Extract<TimelineRow.TimelineRow, { _tag: T }>
|
|
|
@@ -89,14 +64,6 @@ type TimelineRowByTag<T extends TimelineRow.TimelineRow["_tag"]> = Extract<Timel
|
|
|
const timelineFallbackItemSize = 60
|
|
|
const timelineCache = new Map<string, { measurements: VirtualItem[]; toolOpen: Record<string, boolean | undefined> }>()
|
|
|
|
|
|
-const taskDescription = (part: PartType, sessionID: string) => {
|
|
|
- if (part.type !== "tool" || part.tool !== "task") return
|
|
|
- const metadata = "metadata" in part.state ? part.state.metadata : undefined
|
|
|
- if (metadata?.sessionId !== sessionID) return
|
|
|
- const value = part.state.input?.description
|
|
|
- if (typeof value === "string" && value) return value
|
|
|
-}
|
|
|
-
|
|
|
const boundaryTarget = (root: HTMLElement, target: EventTarget | null) => {
|
|
|
const current = target instanceof Element ? target : undefined
|
|
|
const nested = current?.closest("[data-scrollable]")
|
|
|
@@ -237,7 +204,7 @@ function TimelineDiffView(props: { diff: SummaryDiff }) {
|
|
|
)
|
|
|
}
|
|
|
|
|
|
-export function MessageTimeline(props: {
|
|
|
+type MessageTimelineProps = {
|
|
|
actions?: UserActions
|
|
|
scroll: { overflow: boolean; bottom: boolean; jump: boolean }
|
|
|
onResumeScroll: () => void
|
|
|
@@ -257,88 +224,42 @@ export function MessageTimeline(props: {
|
|
|
setRevealMessage?: (fn: (id: string) => void) => void
|
|
|
setScrollToEnd?: (fn: () => void) => void
|
|
|
setHistoryAnchor?: (handlers: { capture: () => void; restore: (done: boolean) => void }) => void
|
|
|
-}) {
|
|
|
- let touchGesture: number | undefined
|
|
|
+}
|
|
|
|
|
|
- const navigate = useNavigate()
|
|
|
- const serverSDK = useServerSDK()
|
|
|
- const sdk = useSDK()
|
|
|
- const sync = useSync()
|
|
|
- const settings = useSettings()
|
|
|
- const tabs = useTabs()
|
|
|
- const dialog = useDialog()
|
|
|
+export function MessageTimeline(props: MessageTimelineProps) {
|
|
|
+ const controller = createTimelineController({ userMessages: () => props.userMessages })
|
|
|
+ return (
|
|
|
+ <MessageTimelineView {...props} data={controller.data} action={controller.action} pending={controller.pending} />
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+function MessageTimelineView(
|
|
|
+ props: MessageTimelineProps & {
|
|
|
+ data: TimelineController["data"]
|
|
|
+ action: TimelineController["action"]
|
|
|
+ pending: TimelineController["pending"]
|
|
|
+ },
|
|
|
+) {
|
|
|
+ let touchGesture: number | undefined
|
|
|
const language = useLanguage()
|
|
|
- const { params, sessionKey } = useSessionKey()
|
|
|
- const ownerSessionKey = sessionKey()
|
|
|
+ const ownerSessionKey = props.data.sessionKey()
|
|
|
const cached = timelineCache.get(ownerSessionKey)
|
|
|
const initialMeasurements = cached?.measurements
|
|
|
const coldBottomMount = !initialMeasurements?.length && props.shouldAnchorBottom()
|
|
|
- const platform = usePlatform()
|
|
|
|
|
|
const [listRoot, setListRoot] = createSignal<HTMLDivElement>()
|
|
|
- const sessionID = createMemo(() => params.id)
|
|
|
- const sessionStatus = createMemo(() => {
|
|
|
- const id = sessionID()
|
|
|
- if (!id) return idle
|
|
|
- return sync().data.session_status[id] ?? idle
|
|
|
- })
|
|
|
- const sessionMessages = createMemo(() => (sessionID() ? (sync().data.message[sessionID()!] ?? []) : []))
|
|
|
- const projectedMessages = createMemo(() => {
|
|
|
- const id = sessionID()
|
|
|
- if (!id) return []
|
|
|
- const visible = new Set(props.userMessages.map((message) => message.id))
|
|
|
- const boundary = sessionMessages().find((message) => message.role === "user" && !visible.has(message.id))?.id
|
|
|
- const messages = sync().data.session_message[id] ?? []
|
|
|
- return boundary ? messages.filter((message) => message.id < boundary) : messages
|
|
|
- })
|
|
|
- const info = createMemo(() => {
|
|
|
- const id = sessionID()
|
|
|
- if (!id) return
|
|
|
- return sync().session.get(id)
|
|
|
- })
|
|
|
- const titleValue = createMemo(() => info()?.title)
|
|
|
- const titleLabel = createMemo(() => sessionTitle(titleValue()))
|
|
|
- const shareUrl = createMemo(() => info()?.share?.url)
|
|
|
- const shareEnabled = createMemo(() => sync().data.config.share !== "disabled")
|
|
|
- const parentID = createMemo(() => info()?.parentID)
|
|
|
- const parent = createMemo(() => {
|
|
|
- const id = parentID()
|
|
|
- if (!id) return
|
|
|
- return sync().session.get(id)
|
|
|
- })
|
|
|
- const parentMessages = createMemo(() => {
|
|
|
- const id = parentID()
|
|
|
- if (!id) return emptyMessages
|
|
|
- return sync().data.message[id] ?? emptyMessages
|
|
|
- })
|
|
|
- const parentTitle = createMemo(() => sessionTitle(parent()?.title) ?? language.t("command.session.new"))
|
|
|
- const getMsgParts = (msgId: string) => sync().data.part[msgId] ?? emptyParts
|
|
|
- const getMsgPart = (messageID: string, partID: string) => getMsgParts(messageID).find((part) => part.id === partID)
|
|
|
- const childTaskDescription = createMemo(() => {
|
|
|
- const id = sessionID()
|
|
|
- if (!id) return
|
|
|
- return parentMessages()
|
|
|
- .flatMap((message) => getMsgParts(message.id))
|
|
|
- .map((part) => taskDescription(part, id))
|
|
|
- .findLast((value): value is string => !!value)
|
|
|
- })
|
|
|
- const childTitle = createMemo(() => {
|
|
|
- if (!parentID()) return titleLabel() ?? ""
|
|
|
- if (childTaskDescription()) return childTaskDescription()
|
|
|
- const value = titleLabel()?.replace(/\s+\(@[^)]+ subagent\)$/, "")
|
|
|
- if (value) return value
|
|
|
- return language.t("command.session.new")
|
|
|
- })
|
|
|
- const showHeader = createMemo(() => !!(titleValue() || parentID()))
|
|
|
- const projection = createTimelineProjection({
|
|
|
- messages: sessionMessages,
|
|
|
- userMessages: () => props.userMessages,
|
|
|
- sessionMessages: projectedMessages,
|
|
|
- parts: getMsgParts,
|
|
|
- status: sessionStatus,
|
|
|
- showReasoningSummaries: settings.general.showReasoningSummaries,
|
|
|
- inlineComments: settings.general.newLayoutDesigns,
|
|
|
- })
|
|
|
+ const sessionID = props.data.sessionID
|
|
|
+ const sessionStatus = props.data.status
|
|
|
+ const titleLabel = props.data.titleLabel
|
|
|
+ const shareUrl = props.data.shareUrl
|
|
|
+ const shareEnabled = props.data.shareEnabled
|
|
|
+ const parentID = props.data.parentID
|
|
|
+ const parentTitle = props.data.parentTitle
|
|
|
+ const childTitle = props.data.childTitle
|
|
|
+ const showHeader = props.data.showHeader
|
|
|
+ const getMsgParts = props.data.parts
|
|
|
+ const getMsgPart = props.data.part
|
|
|
+ const projection = props.data.projection
|
|
|
const activeMessageID = projection.activeMessageID
|
|
|
const assistantMessagesByParent = projection.assistantMessagesByParent
|
|
|
const lastAssistantGroupKey = projection.lastAssistantGroupKey
|
|
|
@@ -528,9 +449,9 @@ export function MessageTimeline(props: {
|
|
|
virtualizer.scrollToEnd()
|
|
|
}
|
|
|
|
|
|
- let measuredSessionKey = sessionKey()
|
|
|
+ let measuredSessionKey = props.data.sessionKey()
|
|
|
createEffect(() => {
|
|
|
- const key = sessionKey()
|
|
|
+ const key = props.data.sessionKey()
|
|
|
timelineRows().length
|
|
|
if (measuredSessionKey !== key) {
|
|
|
measuredSessionKey = key
|
|
|
@@ -643,88 +564,6 @@ export function MessageTimeline(props: {
|
|
|
props.setScrollRef(undefined)
|
|
|
})
|
|
|
|
|
|
- const viewShare = () => {
|
|
|
- const url = shareUrl()
|
|
|
- if (!url) return
|
|
|
- platform.openLink(url)
|
|
|
- }
|
|
|
-
|
|
|
- const errorMessage = (err: unknown) => {
|
|
|
- if (err && typeof err === "object" && "data" in err) {
|
|
|
- const data = (err as { data?: { message?: string } }).data
|
|
|
- if (data?.message) return data.message
|
|
|
- }
|
|
|
- if (err instanceof Error) return err.message
|
|
|
- return language.t("common.requestFailed")
|
|
|
- }
|
|
|
-
|
|
|
- const shareMutation = useMutation(() => ({
|
|
|
- mutationFn: (id: string) => serverSDK().client.session.share({ sessionID: id }),
|
|
|
- onError: (err) => {
|
|
|
- console.error("Failed to share session", err)
|
|
|
- },
|
|
|
- }))
|
|
|
-
|
|
|
- const unshareMutation = useMutation(() => ({
|
|
|
- mutationFn: (id: string) => serverSDK().client.session.unshare({ sessionID: id }),
|
|
|
- onError: (err) => {
|
|
|
- console.error("Failed to unshare session", err)
|
|
|
- },
|
|
|
- }))
|
|
|
-
|
|
|
- const titleMutation = useMutation(() => ({
|
|
|
- mutationFn: (input: { id: string; title: string }) =>
|
|
|
- sdk().api.session.rename({ sessionID: input.id, title: input.title }),
|
|
|
- onSuccess: (_, input) => {
|
|
|
- sync().set(
|
|
|
- produce((draft) => {
|
|
|
- const index = draft.session.findIndex((s) => s.id === input.id)
|
|
|
- if (index !== -1) draft.session[index].title = input.title
|
|
|
- }),
|
|
|
- )
|
|
|
- setTitle("editing", false)
|
|
|
- },
|
|
|
- onError: (err) => {
|
|
|
- showToast({
|
|
|
- title: language.t("common.requestFailed"),
|
|
|
- description: errorMessage(err),
|
|
|
- })
|
|
|
- },
|
|
|
- }))
|
|
|
-
|
|
|
- const shareSession = () => {
|
|
|
- const id = sessionID()
|
|
|
- if (!id || shareMutation.isPending) return
|
|
|
- if (!shareEnabled()) return
|
|
|
- shareMutation.mutate(id)
|
|
|
- }
|
|
|
-
|
|
|
- const unshareSession = () => {
|
|
|
- const id = sessionID()
|
|
|
- if (!id || unshareMutation.isPending) return
|
|
|
- if (!shareEnabled()) return
|
|
|
- unshareMutation.mutate(id)
|
|
|
- }
|
|
|
- const copyShareUrl = () => {
|
|
|
- const url = shareUrl()
|
|
|
- if (!url) return
|
|
|
- void navigator.clipboard
|
|
|
- .writeText(url)
|
|
|
- .then(() =>
|
|
|
- showToast({
|
|
|
- variant: "success",
|
|
|
- icon: "circle-check",
|
|
|
- title: language.t("session.share.copy.copied"),
|
|
|
- description: url,
|
|
|
- }),
|
|
|
- )
|
|
|
- .catch((err: unknown) =>
|
|
|
- showToast({
|
|
|
- title: language.t("common.requestFailed"),
|
|
|
- description: errorMessage(err),
|
|
|
- }),
|
|
|
- )
|
|
|
- }
|
|
|
const selectShareUrlText: JSX.EventHandler<HTMLDivElement, MouseEvent> = (event) => {
|
|
|
const selection = window.getSelection()
|
|
|
if (!selection) return
|
|
|
@@ -736,7 +575,7 @@ export function MessageTimeline(props: {
|
|
|
|
|
|
createEffect(
|
|
|
on(
|
|
|
- sessionKey,
|
|
|
+ props.data.sessionKey,
|
|
|
() =>
|
|
|
setTitle({
|
|
|
draft: "",
|
|
|
@@ -749,18 +588,6 @@ export function MessageTimeline(props: {
|
|
|
),
|
|
|
)
|
|
|
|
|
|
- createEffect(
|
|
|
- on(
|
|
|
- () => [parentID(), childTaskDescription()] as const,
|
|
|
- ([id, description]) => {
|
|
|
- if (!id || description) return
|
|
|
- if (sync().data.message[id] !== undefined) return
|
|
|
- void sync().session.sync(id)
|
|
|
- },
|
|
|
- { defer: true },
|
|
|
- ),
|
|
|
- )
|
|
|
-
|
|
|
const openTitleEditor = () => {
|
|
|
if (!sessionID() || parentID()) return
|
|
|
setTitle({ editing: true, draft: titleLabel() ?? "" })
|
|
|
@@ -772,193 +599,12 @@ export function MessageTimeline(props: {
|
|
|
}
|
|
|
|
|
|
const closeTitleEditor = () => {
|
|
|
- if (titleMutation.isPending) return
|
|
|
+ if (props.pending.rename()) return
|
|
|
setTitle("editing", false)
|
|
|
}
|
|
|
|
|
|
- const saveTitleEditor = () => {
|
|
|
- const id = sessionID()
|
|
|
- if (!id) return
|
|
|
- if (titleMutation.isPending) return
|
|
|
-
|
|
|
- const next = title.draft.trim()
|
|
|
- if (!next || next === (titleLabel() ?? "")) {
|
|
|
- setTitle("editing", false)
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- titleMutation.mutate({ id, title: next })
|
|
|
- }
|
|
|
-
|
|
|
- const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => {
|
|
|
- if (params.id !== sessionID) return
|
|
|
- const href = (id: string) =>
|
|
|
- params.serverKey ? sessionHref(requireServerKey(params.serverKey), id) : legacySessionHref(sdk().directory, id)
|
|
|
- if (parentID) {
|
|
|
- navigate(href(parentID))
|
|
|
- return
|
|
|
- }
|
|
|
- if (nextSessionID) {
|
|
|
- navigate(href(nextSessionID))
|
|
|
- return
|
|
|
- }
|
|
|
- if (params.serverKey) {
|
|
|
- tabs.newDraft({ server: requireServerKey(params.serverKey), directory: sdk().directory })
|
|
|
- return
|
|
|
- }
|
|
|
- navigate(`/${params.dir}/session`)
|
|
|
- }
|
|
|
-
|
|
|
- const archiveSession = async (sessionID: string) => {
|
|
|
- const session = sync().session.get(sessionID)
|
|
|
- if (!session) return
|
|
|
- if ((await sdk().protocol) !== "v1") return
|
|
|
-
|
|
|
- const sessions = sync().data.session ?? []
|
|
|
- const index = sessions.findIndex((s) => s.id === sessionID)
|
|
|
- const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
|
|
|
-
|
|
|
- await sdk()
|
|
|
- .client.session.update({ sessionID, directory: sdk().directory, time: { archived: Date.now() } })
|
|
|
- .then(() => {
|
|
|
- sync().set(
|
|
|
- produce((draft) => {
|
|
|
- const index = draft.session.findIndex((s) => s.id === sessionID)
|
|
|
- if (index !== -1) draft.session.splice(index, 1)
|
|
|
- }),
|
|
|
- )
|
|
|
- sync().session.evict(sessionID)
|
|
|
- navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
|
|
|
- notifySessionTabsRemoved({ directory: sdk().directory, sessionIDs: [sessionID] })
|
|
|
- })
|
|
|
- .catch((err) => {
|
|
|
- showToast({
|
|
|
- title: language.t("common.requestFailed"),
|
|
|
- description: errorMessage(err),
|
|
|
- })
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- const deleteSession = async (sessionID: string) => {
|
|
|
- const session = sync().session.get(sessionID)
|
|
|
- if (!session) return false
|
|
|
-
|
|
|
- const sessions = (sync().data.session ?? []).filter((s) => !s.parentID && !s.time?.archived)
|
|
|
- const index = sessions.findIndex((s) => s.id === sessionID)
|
|
|
- const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
|
|
|
-
|
|
|
- const result = await sdk()
|
|
|
- .api.session.remove({ sessionID })
|
|
|
- .then(() => true)
|
|
|
- .catch((err) => {
|
|
|
- showToast({
|
|
|
- title: language.t("session.delete.failed.title"),
|
|
|
- description: errorMessage(err),
|
|
|
- })
|
|
|
- return false
|
|
|
- })
|
|
|
-
|
|
|
- if (!result) return false
|
|
|
-
|
|
|
- const removed = new Set<string>([sessionID])
|
|
|
- const byParent = new Map<string, string[]>()
|
|
|
- for (const item of sync().data.session) {
|
|
|
- const parentID = item.parentID
|
|
|
- if (!parentID) continue
|
|
|
- const existing = byParent.get(parentID)
|
|
|
- if (existing) {
|
|
|
- existing.push(item.id)
|
|
|
- continue
|
|
|
- }
|
|
|
- byParent.set(parentID, [item.id])
|
|
|
- }
|
|
|
-
|
|
|
- const stack = [sessionID]
|
|
|
- while (stack.length) {
|
|
|
- const parentID = stack.pop()
|
|
|
- if (!parentID) continue
|
|
|
-
|
|
|
- const children = byParent.get(parentID)
|
|
|
- if (!children) continue
|
|
|
-
|
|
|
- for (const child of children) {
|
|
|
- if (removed.has(child)) continue
|
|
|
- removed.add(child)
|
|
|
- stack.push(child)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
|
|
|
-
|
|
|
- sync().set(
|
|
|
- produce((draft) => {
|
|
|
- draft.session = draft.session.filter((s) => !removed.has(s.id))
|
|
|
- }),
|
|
|
- )
|
|
|
-
|
|
|
- for (const id of removed) {
|
|
|
- sync().session.evict(id)
|
|
|
- }
|
|
|
- notifySessionTabsRemoved({ directory: sdk().directory, sessionIDs: [...removed] })
|
|
|
- return true
|
|
|
- }
|
|
|
-
|
|
|
- const navigateParent = () => {
|
|
|
- const id = parentID()
|
|
|
- if (!id) return
|
|
|
- navigate(
|
|
|
- params.serverKey ? sessionHref(requireServerKey(params.serverKey), id) : legacySessionHref(sdk().directory, id),
|
|
|
- )
|
|
|
- }
|
|
|
-
|
|
|
- function DialogDeleteSession(props: { sessionID: string }) {
|
|
|
- const name = createMemo(
|
|
|
- () => sessionTitle(sync().session.get(props.sessionID)?.title) ?? language.t("command.session.new"),
|
|
|
- )
|
|
|
- const handleDelete = async () => {
|
|
|
- await deleteSession(props.sessionID)
|
|
|
- dialog.close()
|
|
|
- }
|
|
|
-
|
|
|
- if (settings.general.newLayoutDesigns())
|
|
|
- return (
|
|
|
- <DialogV2 fit>
|
|
|
- <DialogHeader hideClose>
|
|
|
- <DialogTitleGroup
|
|
|
- title={language.t("session.delete.title")}
|
|
|
- description={language.t("session.delete.confirm", { name: name() })}
|
|
|
- />
|
|
|
- </DialogHeader>
|
|
|
- <DialogFooter>
|
|
|
- <ButtonV2 variant="ghost" onClick={() => dialog.close()}>
|
|
|
- {language.t("common.cancel")}
|
|
|
- </ButtonV2>
|
|
|
- <ButtonV2 variant="danger" onClick={handleDelete}>
|
|
|
- {language.t("session.delete.button")}
|
|
|
- </ButtonV2>
|
|
|
- </DialogFooter>
|
|
|
- </DialogV2>
|
|
|
- )
|
|
|
-
|
|
|
- return (
|
|
|
- <Dialog title={language.t("session.delete.title")} fit>
|
|
|
- <div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
|
|
|
- <div class="flex flex-col gap-1">
|
|
|
- <span class="text-14-regular text-text-strong">
|
|
|
- {language.t("session.delete.confirm", { name: name() })}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- <div class="flex justify-end gap-2">
|
|
|
- <Button variant="ghost" size="large" onClick={() => dialog.close()}>
|
|
|
- {language.t("common.cancel")}
|
|
|
- </Button>
|
|
|
- <Button variant="primary" size="large" onClick={handleDelete}>
|
|
|
- {language.t("session.delete.button")}
|
|
|
- </Button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </Dialog>
|
|
|
- )
|
|
|
+ const saveTitleEditor = async () => {
|
|
|
+ if (await props.action.rename(title.draft)) setTitle("editing", false)
|
|
|
}
|
|
|
|
|
|
const workingTurn = (userMessageID: string) => sessionStatus().type !== "idle" && activeMessageID() === userMessageID
|
|
|
@@ -1037,7 +683,7 @@ export function MessageTimeline(props: {
|
|
|
const defaultOpen = createMemo(() => {
|
|
|
const item = part()
|
|
|
if (!item) return
|
|
|
- return partDefaultOpen(item, settings.general.shellToolPartsExpanded(), settings.general.editToolPartsExpanded())
|
|
|
+ return partDefaultOpen(item, props.data.shellToolPartsExpanded(), props.data.editToolPartsExpanded())
|
|
|
})
|
|
|
|
|
|
return (
|
|
|
@@ -1050,7 +696,7 @@ export function MessageTimeline(props: {
|
|
|
message={message()}
|
|
|
showAssistantCopyPartID={assistantCopyPartID(row().userMessageID)}
|
|
|
turnDurationMs={turnDurationMs(row().userMessageID)}
|
|
|
- useV2Actions={settings.general.newLayoutDesigns()}
|
|
|
+ useV2Actions={props.data.newLayoutDesigns()}
|
|
|
defaultOpen={defaultOpen()}
|
|
|
toolOpen={toolOpen[part().id] ?? defaultOpen()}
|
|
|
onToolOpenChange={(open) => setToolOpen(part().id, open)}
|
|
|
@@ -1113,8 +759,8 @@ export function MessageTimeline(props: {
|
|
|
<div
|
|
|
classList={{
|
|
|
"shrink-0 max-w-[260px] rounded-[6px] border-border-weak-base bg-background-stronger px-2.5 py-2": true,
|
|
|
- "border-[0.5px]": settings.general.newLayoutDesigns(),
|
|
|
- border: !settings.general.newLayoutDesigns(),
|
|
|
+ "border-[0.5px]": props.data.newLayoutDesigns(),
|
|
|
+ border: !props.data.newLayoutDesigns(),
|
|
|
}}
|
|
|
>
|
|
|
<div class="flex items-center gap-1.5 min-w-0 text-11-medium text-text-strong">
|
|
|
@@ -1149,7 +795,7 @@ export function MessageTimeline(props: {
|
|
|
if (m?.role === "user") return m
|
|
|
})
|
|
|
const messageComments = createMemo(() => {
|
|
|
- if (!settings.general.newLayoutDesigns()) return []
|
|
|
+ if (!props.data.newLayoutDesigns()) return []
|
|
|
return getMsgParts(userMessageRow().userMessageID).flatMap((part) => MessageComment.fromPart(part) ?? [])
|
|
|
})
|
|
|
return (
|
|
|
@@ -1162,7 +808,7 @@ export function MessageTimeline(props: {
|
|
|
message={message()}
|
|
|
parts={getMsgParts(userMessageRow().userMessageID)}
|
|
|
actions={props.actions}
|
|
|
- useV2Actions={settings.general.newLayoutDesigns()}
|
|
|
+ useV2Actions={props.data.newLayoutDesigns()}
|
|
|
comments={messageComments()}
|
|
|
/>
|
|
|
</div>
|
|
|
@@ -1210,7 +856,7 @@ export function MessageTimeline(props: {
|
|
|
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
|
|
<TimelineThinkingRow
|
|
|
reasoningHeading={thinkingRow().reasoningHeading}
|
|
|
- showReasoningSummaries={settings.general.showReasoningSummaries()}
|
|
|
+ showReasoningSummaries={props.data.showReasoningSummaries()}
|
|
|
/>
|
|
|
</div>
|
|
|
</TimelineRowFrame>
|
|
|
@@ -1326,16 +972,16 @@ export function MessageTimeline(props: {
|
|
|
<div
|
|
|
class="absolute left-1/2 -translate-x-1/2 z-[60] pointer-events-none transition-all duration-200 ease-out"
|
|
|
classList={{
|
|
|
- "bottom-8": settings.general.newLayoutDesigns(),
|
|
|
- "bottom-6": !settings.general.newLayoutDesigns(),
|
|
|
+ "bottom-8": props.data.newLayoutDesigns(),
|
|
|
+ "bottom-6": !props.data.newLayoutDesigns(),
|
|
|
"opacity-100 translate-y-0 scale-100": props.scroll.overflow && props.scroll.jump,
|
|
|
"opacity-0 translate-y-2 pointer-events-none": !props.scroll.overflow || !props.scroll.jump,
|
|
|
- "scale-[0.8]": (!props.scroll.overflow || !props.scroll.jump) && settings.general.newLayoutDesigns(),
|
|
|
- "scale-95": (!props.scroll.overflow || !props.scroll.jump) && !settings.general.newLayoutDesigns(),
|
|
|
+ "scale-[0.8]": (!props.scroll.overflow || !props.scroll.jump) && props.data.newLayoutDesigns(),
|
|
|
+ "scale-95": (!props.scroll.overflow || !props.scroll.jump) && !props.data.newLayoutDesigns(),
|
|
|
}}
|
|
|
>
|
|
|
<Show
|
|
|
- when={settings.general.newLayoutDesigns()}
|
|
|
+ when={props.data.newLayoutDesigns()}
|
|
|
fallback={
|
|
|
<button
|
|
|
type="button"
|
|
|
@@ -1398,22 +1044,22 @@ export function MessageTimeline(props: {
|
|
|
classList={{
|
|
|
"sticky top-0 z-30": true,
|
|
|
"bg-[linear-gradient(to_bottom,var(--v2-background-bg-base)_48px,transparent)]":
|
|
|
- settings.general.newLayoutDesigns(),
|
|
|
+ props.data.newLayoutDesigns(),
|
|
|
"bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]":
|
|
|
- !settings.general.newLayoutDesigns(),
|
|
|
+ !props.data.newLayoutDesigns(),
|
|
|
"w-full": true,
|
|
|
"pb-4": true,
|
|
|
"pr-3": true,
|
|
|
- "pl-2.5": settings.general.newLayoutDesigns(),
|
|
|
- "pl-2 md:pl-4": !settings.general.newLayoutDesigns(),
|
|
|
- "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered && !settings.general.newLayoutDesigns(),
|
|
|
+ "pl-2.5": props.data.newLayoutDesigns(),
|
|
|
+ "pl-2 md:pl-4": !props.data.newLayoutDesigns(),
|
|
|
+ "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered && !props.data.newLayoutDesigns(),
|
|
|
}}
|
|
|
>
|
|
|
<div class="h-12 w-full flex items-center justify-between gap-2">
|
|
|
<div
|
|
|
classList={{
|
|
|
"flex items-center gap-1 min-w-0 flex-1": true,
|
|
|
- "pr-3": !settings.general.newLayoutDesigns(),
|
|
|
+ "pr-3": !props.data.newLayoutDesigns(),
|
|
|
}}
|
|
|
>
|
|
|
<div class="flex items-center min-w-0 flex-1 w-full">
|
|
|
@@ -1422,7 +1068,7 @@ export function MessageTimeline(props: {
|
|
|
type="button"
|
|
|
data-slot="session-title-parent"
|
|
|
class="min-w-0 max-w-[40%] truncate pl-2 text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-faint transition-colors hover:text-v2-text-text-muted"
|
|
|
- onClick={navigateParent}
|
|
|
+ onClick={props.action.navigateParent}
|
|
|
>
|
|
|
{parentTitle()}
|
|
|
</button>
|
|
|
@@ -1443,8 +1089,8 @@ export function MessageTimeline(props: {
|
|
|
classList={{
|
|
|
"truncate text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-base": true,
|
|
|
"w-fit rounded-[6px] px-2 py-1 hover:bg-v2-overlay-simple-overlay-hover":
|
|
|
- settings.general.newLayoutDesigns(),
|
|
|
- "grow-1 min-w-0": !settings.general.newLayoutDesigns(),
|
|
|
+ props.data.newLayoutDesigns(),
|
|
|
+ "grow-1 min-w-0": !props.data.newLayoutDesigns(),
|
|
|
}}
|
|
|
onClick={openTitleEditor}
|
|
|
>
|
|
|
@@ -1458,15 +1104,14 @@ export function MessageTimeline(props: {
|
|
|
}}
|
|
|
data-slot="session-title-child"
|
|
|
value={title.draft}
|
|
|
- disabled={titleMutation.isPending}
|
|
|
+ disabled={props.pending.rename()}
|
|
|
classList={{
|
|
|
"block text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-base": true,
|
|
|
- "w-full flex-1 grow-1 min-w-0 pl-1 -ml-1 rounded-[6px]": !settings.general.newLayoutDesigns(),
|
|
|
- "field-sizing-content self-start rounded-[6px] px-2 py-1 ":
|
|
|
- settings.general.newLayoutDesigns(),
|
|
|
+ "w-full flex-1 grow-1 min-w-0 pl-1 -ml-1 rounded-[6px]": !props.data.newLayoutDesigns(),
|
|
|
+ "field-sizing-content self-start rounded-[6px] px-2 py-1 ": props.data.newLayoutDesigns(),
|
|
|
}}
|
|
|
style={{
|
|
|
- "--inline-input-shadow": settings.general.newLayoutDesigns()
|
|
|
+ "--inline-input-shadow": props.data.newLayoutDesigns()
|
|
|
? "none"
|
|
|
: "var(--shadow-xs-border-select)",
|
|
|
}}
|
|
|
@@ -1494,17 +1139,17 @@ export function MessageTimeline(props: {
|
|
|
<div
|
|
|
classList={{
|
|
|
"shrink-0 flex items-center": true,
|
|
|
- "gap-2": settings.general.newLayoutDesigns(),
|
|
|
- "gap-3": !settings.general.newLayoutDesigns(),
|
|
|
+ "gap-2": props.data.newLayoutDesigns(),
|
|
|
+ "gap-3": !props.data.newLayoutDesigns(),
|
|
|
}}
|
|
|
>
|
|
|
<SessionContextUsage
|
|
|
placement="bottom"
|
|
|
- buttonAppearance={settings.general.newLayoutDesigns() ? "v2" : "default"}
|
|
|
+ buttonAppearance={props.data.newLayoutDesigns() ? "v2" : "default"}
|
|
|
/>
|
|
|
<Show when={!parentID()}>
|
|
|
<Show
|
|
|
- when={settings.general.newLayoutDesigns()}
|
|
|
+ when={props.data.newLayoutDesigns()}
|
|
|
fallback={
|
|
|
<DropdownMenu
|
|
|
gutter={4}
|
|
|
@@ -1567,13 +1212,11 @@ export function MessageTimeline(props: {
|
|
|
</DropdownMenu.ItemLabel>
|
|
|
</DropdownMenu.Item>
|
|
|
</Show>
|
|
|
- <DropdownMenu.Item onSelect={() => void archiveSession(id)}>
|
|
|
+ <DropdownMenu.Item onSelect={() => void props.action.archive(id)}>
|
|
|
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
|
|
</DropdownMenu.Item>
|
|
|
<DropdownMenu.Separator />
|
|
|
- <DropdownMenu.Item
|
|
|
- onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id} />)}
|
|
|
- >
|
|
|
+ <DropdownMenu.Item onSelect={() => props.action.showDelete(id)}>
|
|
|
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
|
|
|
</DropdownMenu.Item>
|
|
|
</DropdownMenu.Content>
|
|
|
@@ -1638,11 +1281,11 @@ export function MessageTimeline(props: {
|
|
|
{language.t("session.share.action.share")}...
|
|
|
</MenuV2.Item>
|
|
|
</Show>
|
|
|
- <MenuV2.Item onSelect={() => void archiveSession(id)}>
|
|
|
+ <MenuV2.Item onSelect={() => void props.action.archive(id)}>
|
|
|
{language.t("common.archive")}
|
|
|
</MenuV2.Item>
|
|
|
<MenuV2.Separator />
|
|
|
- <MenuV2.Item onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id} />)}>
|
|
|
+ <MenuV2.Item onSelect={() => props.action.showDelete(id)}>
|
|
|
{language.t("common.delete")}...
|
|
|
</MenuV2.Item>
|
|
|
</MenuV2.Content>
|
|
|
@@ -1654,7 +1297,7 @@ export function MessageTimeline(props: {
|
|
|
open={share.open}
|
|
|
anchorRef={() => more}
|
|
|
placement="bottom-end"
|
|
|
- gutter={settings.general.newLayoutDesigns() ? 6 : 4}
|
|
|
+ gutter={props.data.newLayoutDesigns() ? 6 : 4}
|
|
|
modal={false}
|
|
|
onOpenChange={(open) => {
|
|
|
if (open) setShare("dismiss", null)
|
|
|
@@ -1666,7 +1309,7 @@ export function MessageTimeline(props: {
|
|
|
data-component="popover-content"
|
|
|
classList={{
|
|
|
"flex w-80 max-w-none flex-col items-start gap-3 rounded-[10px] border-0 bg-v2-background-bg-layer-01 p-3 shadow-[var(--v2-elevation-floating)]":
|
|
|
- settings.general.newLayoutDesigns(),
|
|
|
+ props.data.newLayoutDesigns(),
|
|
|
}}
|
|
|
style={{ "min-width": "320px" }}
|
|
|
onEscapeKeyDown={(event) => {
|
|
|
@@ -1686,7 +1329,7 @@ export function MessageTimeline(props: {
|
|
|
}}
|
|
|
>
|
|
|
<Show
|
|
|
- when={settings.general.newLayoutDesigns()}
|
|
|
+ when={props.data.newLayoutDesigns()}
|
|
|
fallback={
|
|
|
<div class="flex flex-col p-3">
|
|
|
<div class="flex flex-col gap-1">
|
|
|
@@ -1707,10 +1350,10 @@ export function MessageTimeline(props: {
|
|
|
size="large"
|
|
|
variant="primary"
|
|
|
class="w-full"
|
|
|
- onClick={shareSession}
|
|
|
- disabled={shareMutation.isPending}
|
|
|
+ onClick={() => void props.action.share()}
|
|
|
+ disabled={props.pending.share()}
|
|
|
>
|
|
|
- {shareMutation.isPending
|
|
|
+ {props.pending.share()
|
|
|
? language.t("session.share.action.publishing")
|
|
|
: language.t("session.share.action.publish")}
|
|
|
</Button>
|
|
|
@@ -1730,10 +1373,10 @@ export function MessageTimeline(props: {
|
|
|
size="large"
|
|
|
variant="secondary"
|
|
|
class="w-full shadow-none border border-border-weak-base"
|
|
|
- onClick={unshareSession}
|
|
|
- disabled={unshareMutation.isPending}
|
|
|
+ onClick={() => void props.action.unshare()}
|
|
|
+ disabled={props.pending.unshare()}
|
|
|
>
|
|
|
- {unshareMutation.isPending
|
|
|
+ {props.pending.unshare()
|
|
|
? language.t("session.share.action.unpublishing")
|
|
|
: language.t("session.share.action.unpublish")}
|
|
|
</Button>
|
|
|
@@ -1741,8 +1384,8 @@ export function MessageTimeline(props: {
|
|
|
size="large"
|
|
|
variant="primary"
|
|
|
class="w-full"
|
|
|
- onClick={viewShare}
|
|
|
- disabled={unshareMutation.isPending}
|
|
|
+ onClick={props.action.viewShare}
|
|
|
+ disabled={props.pending.unshare()}
|
|
|
>
|
|
|
{language.t("session.share.action.view")}
|
|
|
</Button>
|
|
|
@@ -1770,10 +1413,10 @@ export function MessageTimeline(props: {
|
|
|
<ButtonV2
|
|
|
variant="contrast"
|
|
|
class="w-full"
|
|
|
- onClick={shareSession}
|
|
|
- disabled={shareMutation.isPending}
|
|
|
+ onClick={() => void props.action.share()}
|
|
|
+ disabled={props.pending.share()}
|
|
|
>
|
|
|
- {shareMutation.isPending
|
|
|
+ {props.pending.share()
|
|
|
? language.t("session.share.action.publishing")
|
|
|
: language.t("session.share.action.publish")}
|
|
|
</ButtonV2>
|
|
|
@@ -1799,7 +1442,7 @@ export function MessageTimeline(props: {
|
|
|
variant="ghost-muted"
|
|
|
icon={<IconV2 name="outline-copy" />}
|
|
|
aria-label={language.t("session.share.copy.copyLink")}
|
|
|
- onClick={copyShareUrl}
|
|
|
+ onClick={() => void props.action.copyShareUrl()}
|
|
|
/>
|
|
|
<IconButtonV2
|
|
|
type="button"
|
|
|
@@ -1807,18 +1450,18 @@ export function MessageTimeline(props: {
|
|
|
variant="ghost-muted"
|
|
|
icon={<IconV2 name="outline-square-arrow" />}
|
|
|
aria-label={language.t("session.share.action.view")}
|
|
|
- onClick={viewShare}
|
|
|
- disabled={unshareMutation.isPending}
|
|
|
+ onClick={props.action.viewShare}
|
|
|
+ disabled={props.pending.unshare()}
|
|
|
/>
|
|
|
</div>
|
|
|
<div class="flex w-full">
|
|
|
<ButtonV2
|
|
|
variant="outline"
|
|
|
class="w-full"
|
|
|
- onClick={unshareSession}
|
|
|
- disabled={unshareMutation.isPending}
|
|
|
+ onClick={() => void props.action.unshare()}
|
|
|
+ disabled={props.pending.unshare()}
|
|
|
>
|
|
|
- {unshareMutation.isPending
|
|
|
+ {props.pending.unshare()
|
|
|
? language.t("session.share.action.unpublishing")
|
|
|
: language.t("session.share.action.unpublish")}
|
|
|
</ButtonV2>
|