fix(app): new layout improvements
This commit is contained in:
@@ -18,6 +18,7 @@ import { useCodeComponent } from "@opencode-ai/ui/context/code"
|
|||||||
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||||
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
||||||
import { SessionReview } from "@opencode-ai/ui/session-review"
|
import { SessionReview } from "@opencode-ai/ui/session-review"
|
||||||
|
import { Mark } from "@opencode-ai/ui/logo"
|
||||||
|
|
||||||
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
|
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
|
||||||
import type { DragEvent } from "@thisbeyond/solid-dnd"
|
import type { DragEvent } from "@thisbeyond/solid-dnd"
|
||||||
@@ -787,17 +788,14 @@ export default function Page() {
|
|||||||
.filter((tab) => tab !== "context"),
|
.filter((tab) => tab !== "context"),
|
||||||
)
|
)
|
||||||
|
|
||||||
const reviewTab = createMemo(() => hasReview() || tabs().active() === "review")
|
const mobileReview = createMemo(() => !isDesktop() && view().reviewPanel.opened() && store.mobileTab === "review")
|
||||||
const mobileReview = createMemo(() => !isDesktop() && hasReview() && store.mobileTab === "review")
|
|
||||||
|
|
||||||
const showTabs = createMemo(
|
const showTabs = createMemo(() => view().reviewPanel.opened())
|
||||||
() => view().reviewPanel.opened() && (hasReview() || tabs().all().length > 0 || contextOpen()),
|
|
||||||
)
|
|
||||||
|
|
||||||
const activeTab = createMemo(() => {
|
const activeTab = createMemo(() => {
|
||||||
const active = tabs().active()
|
const active = tabs().active()
|
||||||
if (active) return active
|
if (active) return active
|
||||||
if (reviewTab()) return "review"
|
if (hasReview()) return "review"
|
||||||
|
|
||||||
const first = openedTabs()[0]
|
const first = openedTabs()[0]
|
||||||
if (first) return first
|
if (first) return first
|
||||||
@@ -1095,8 +1093,8 @@ export default function Page() {
|
|||||||
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
|
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
|
||||||
<SessionHeader />
|
<SessionHeader />
|
||||||
<div class="flex-1 min-h-0 flex flex-col md:flex-row">
|
<div class="flex-1 min-h-0 flex flex-col md:flex-row">
|
||||||
{/* Mobile tab bar - only shown on mobile when there are diffs */}
|
{/* Mobile tab bar - only shown on mobile when user opened review */}
|
||||||
<Show when={!isDesktop() && hasReview()}>
|
<Show when={!isDesktop() && view().reviewPanel.opened()}>
|
||||||
<Tabs class="h-auto">
|
<Tabs class="h-auto">
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
@@ -1113,7 +1111,10 @@ export default function Page() {
|
|||||||
classes={{ button: "w-full" }}
|
classes={{ button: "w-full" }}
|
||||||
onClick={() => setStore("mobileTab", "review")}
|
onClick={() => setStore("mobileTab", "review")}
|
||||||
>
|
>
|
||||||
{reviewCount()} Files Changed
|
<Switch>
|
||||||
|
<Match when={hasReview()}>{reviewCount()} Files Changed</Match>
|
||||||
|
<Match when={true}>Review</Match>
|
||||||
|
</Switch>
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@@ -1138,26 +1139,36 @@ export default function Page() {
|
|||||||
when={!mobileReview()}
|
when={!mobileReview()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="relative h-full overflow-hidden">
|
<div class="relative h-full overflow-hidden">
|
||||||
<Show
|
<Switch>
|
||||||
when={diffsReady()}
|
<Match when={hasReview()}>
|
||||||
fallback={<div class="px-4 py-4 text-text-weak">Loading changes...</div>}
|
<Show
|
||||||
>
|
when={diffsReady()}
|
||||||
<SessionReviewTab
|
fallback={<div class="px-4 py-4 text-text-weak">Loading changes...</div>}
|
||||||
diffs={diffs}
|
>
|
||||||
view={view}
|
<SessionReviewTab
|
||||||
diffStyle="unified"
|
diffs={diffs}
|
||||||
onViewFile={(path) => {
|
view={view}
|
||||||
const value = file.tab(path)
|
diffStyle="unified"
|
||||||
tabs().open(value)
|
onViewFile={(path) => {
|
||||||
file.load(path)
|
const value = file.tab(path)
|
||||||
}}
|
tabs().open(value)
|
||||||
classes={{
|
file.load(path)
|
||||||
root: "pb-[calc(var(--prompt-height,8rem)+24px)]",
|
}}
|
||||||
header: "px-4",
|
classes={{
|
||||||
container: "px-4",
|
root: "pb-[calc(var(--prompt-height,8rem)+32px)]",
|
||||||
}}
|
header: "px-4",
|
||||||
/>
|
container: "px-4",
|
||||||
</Show>
|
}}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<div class="px-4 pt-18 pb-6 flex flex-col items-center justify-center text-center gap-3">
|
||||||
|
<Mark class="w-6 opacity-40" />
|
||||||
|
<div class="text-13-regular text-text-weak max-w-56">No changes in this session yet.</div>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -1170,11 +1181,29 @@ export default function Page() {
|
|||||||
}}
|
}}
|
||||||
onClick={autoScroll.handleInteraction}
|
onClick={autoScroll.handleInteraction}
|
||||||
class="relative min-w-0 w-full h-full overflow-y-auto no-scrollbar"
|
class="relative min-w-0 w-full h-full overflow-y-auto no-scrollbar"
|
||||||
|
style={{ "--session-title-height": info()?.title ? "40px" : "0px" }}
|
||||||
>
|
>
|
||||||
|
<Show when={info()?.title}>
|
||||||
|
<div
|
||||||
|
classList={{
|
||||||
|
"sticky top-0 z-30 bg-background-stronger": true,
|
||||||
|
"w-full": true,
|
||||||
|
"px-4 md:px-6": true,
|
||||||
|
"md:max-w-200 md:mx-auto": !showTabs(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="h-10 flex items-center">
|
||||||
|
<h1 class="text-16-medium text-text-strong truncate">{info()?.title}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={autoScroll.contentRef}
|
ref={autoScroll.contentRef}
|
||||||
class="flex flex-col gap-32 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
|
class="flex flex-col gap-32 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
|
||||||
classList={{
|
classList={{
|
||||||
|
"w-full": true,
|
||||||
|
"md:max-w-200 md:mx-auto": !showTabs(),
|
||||||
"mt-0.5": !showTabs(),
|
"mt-0.5": !showTabs(),
|
||||||
"mt-0": showTabs(),
|
"mt-0": showTabs(),
|
||||||
}}
|
}}
|
||||||
@@ -1225,6 +1254,7 @@ export default function Page() {
|
|||||||
data-message-id={message.id}
|
data-message-id={message.id}
|
||||||
classList={{
|
classList={{
|
||||||
"min-w-0 w-full max-w-full": true,
|
"min-w-0 w-full max-w-full": true,
|
||||||
|
"md:max-w-200": !showTabs(),
|
||||||
"last:min-h-[calc(100vh-5.5rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-4.5rem-var(--prompt-height,10rem)-64px)]":
|
"last:min-h-[calc(100vh-5.5rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-4.5rem-var(--prompt-height,10rem)-64px)]":
|
||||||
platform.platform !== "desktop",
|
platform.platform !== "desktop",
|
||||||
"last:min-h-[calc(100vh-7rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-6rem-var(--prompt-height,10rem)-64px)]":
|
"last:min-h-[calc(100vh-7rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-6rem-var(--prompt-height,10rem)-64px)]":
|
||||||
@@ -1233,7 +1263,6 @@ export default function Page() {
|
|||||||
>
|
>
|
||||||
<SessionTurn
|
<SessionTurn
|
||||||
sessionID={params.id!}
|
sessionID={params.id!}
|
||||||
sessionTitle={info()?.title}
|
|
||||||
messageID={message.id}
|
messageID={message.id}
|
||||||
lastUserMessageID={lastUserMessage()?.id}
|
lastUserMessageID={lastUserMessage()?.id}
|
||||||
stepsExpanded={store.expanded[message.id] ?? false}
|
stepsExpanded={store.expanded[message.id] ?? false}
|
||||||
@@ -1333,7 +1362,7 @@ export default function Page() {
|
|||||||
<Tabs value={activeTab()} onChange={openTab}>
|
<Tabs value={activeTab()} onChange={openTab}>
|
||||||
<div class="sticky top-0 shrink-0 flex">
|
<div class="sticky top-0 shrink-0 flex">
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
<Show when={reviewTab()}>
|
<Show when={true}>
|
||||||
<Tabs.Trigger value="review">
|
<Tabs.Trigger value="review">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<Show when={diffs()}>
|
<Show when={diffs()}>
|
||||||
@@ -1386,26 +1415,36 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
</div>
|
</div>
|
||||||
<Show when={reviewTab()}>
|
<Show when={true}>
|
||||||
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
|
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||||
<Show when={activeTab() === "review"}>
|
<Show when={activeTab() === "review"}>
|
||||||
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
||||||
<Show
|
<Switch>
|
||||||
when={diffsReady()}
|
<Match when={hasReview()}>
|
||||||
fallback={<div class="px-6 py-4 text-text-weak">Loading changes...</div>}
|
<Show
|
||||||
>
|
when={diffsReady()}
|
||||||
<SessionReviewTab
|
fallback={<div class="px-6 py-4 text-text-weak">Loading changes...</div>}
|
||||||
diffs={diffs}
|
>
|
||||||
view={view}
|
<SessionReviewTab
|
||||||
diffStyle={layout.review.diffStyle()}
|
diffs={diffs}
|
||||||
onDiffStyleChange={layout.review.setDiffStyle}
|
view={view}
|
||||||
onViewFile={(path) => {
|
diffStyle={layout.review.diffStyle()}
|
||||||
const value = file.tab(path)
|
onDiffStyleChange={layout.review.setDiffStyle}
|
||||||
tabs().open(value)
|
onViewFile={(path) => {
|
||||||
file.load(path)
|
const value = file.tab(path)
|
||||||
}}
|
tabs().open(value)
|
||||||
/>
|
file.load(path)
|
||||||
</Show>
|
}}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<div class="px-6 pt-18 pb-6 flex flex-col items-center justify-center text-center gap-3">
|
||||||
|
<Mark class="w-6 opacity-40" />
|
||||||
|
<div class="text-13-regular text-text-weak max-w-56">No changes in this session yet.</div>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import { checksum } from "@opencode-ai/util/encode"
|
|||||||
import { Tooltip } from "./tooltip"
|
import { Tooltip } from "./tooltip"
|
||||||
import { IconButton } from "./icon-button"
|
import { IconButton } from "./icon-button"
|
||||||
import { createAutoScroll } from "../hooks"
|
import { createAutoScroll } from "../hooks"
|
||||||
|
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||||
|
|
||||||
interface Diagnostic {
|
interface Diagnostic {
|
||||||
range: {
|
range: {
|
||||||
@@ -297,6 +298,23 @@ export function AssistantMessageDisplay(props: { message: AssistantMessage; part
|
|||||||
export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) {
|
export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const [copied, setCopied] = createSignal(false)
|
const [copied, setCopied] = createSignal(false)
|
||||||
|
const [expanded, setExpanded] = createSignal(false)
|
||||||
|
const [canExpand, setCanExpand] = createSignal(false)
|
||||||
|
let textRef: HTMLDivElement | undefined
|
||||||
|
|
||||||
|
const updateCanExpand = () => {
|
||||||
|
const el = textRef
|
||||||
|
if (!el) return
|
||||||
|
if (expanded()) return
|
||||||
|
setCanExpand(el.scrollHeight > el.clientHeight + 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
createResizeObserver(
|
||||||
|
() => textRef,
|
||||||
|
() => {
|
||||||
|
updateCanExpand()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const textPart = createMemo(
|
const textPart = createMemo(
|
||||||
() => props.parts?.find((p) => p.type === "text" && !(p as TextPart).synthetic) as TextPart | undefined,
|
() => props.parts?.find((p) => p.type === "text" && !(p as TextPart).synthetic) as TextPart | undefined,
|
||||||
@@ -304,6 +322,11 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
|
|||||||
|
|
||||||
const text = createMemo(() => textPart()?.text || "")
|
const text = createMemo(() => textPart()?.text || "")
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
text()
|
||||||
|
updateCanExpand()
|
||||||
|
})
|
||||||
|
|
||||||
const files = createMemo(() => (props.parts?.filter((p) => p.type === "file") as FilePart[]) ?? [])
|
const files = createMemo(() => (props.parts?.filter((p) => p.type === "file") as FilePart[]) ?? [])
|
||||||
|
|
||||||
const attachments = createMemo(() =>
|
const attachments = createMemo(() =>
|
||||||
@@ -335,7 +358,7 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-component="user-message">
|
<div data-component="user-message" data-expanded={expanded()} data-can-expand={canExpand()}>
|
||||||
<Show when={attachments().length > 0}>
|
<Show when={attachments().length > 0}>
|
||||||
<div data-slot="user-message-attachments">
|
<div data-slot="user-message-attachments">
|
||||||
<For each={attachments()}>
|
<For each={attachments()}>
|
||||||
@@ -365,8 +388,16 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={text()}>
|
<Show when={text()}>
|
||||||
<div data-slot="user-message-text">
|
<div data-slot="user-message-text" ref={(el) => (textRef = el)}>
|
||||||
<HighlightedText text={text()} references={inlineFiles()} agents={agents()} />
|
<HighlightedText text={text()} references={inlineFiles()} agents={agents()} />
|
||||||
|
<button
|
||||||
|
data-slot="user-message-expand"
|
||||||
|
type="button"
|
||||||
|
aria-label={expanded() ? "Collapse message" : "Expand message"}
|
||||||
|
onClick={() => setExpanded((v) => !v)}
|
||||||
|
>
|
||||||
|
<Icon name="chevron-down" size="small" />
|
||||||
|
</button>
|
||||||
<div data-slot="user-message-copy-wrapper">
|
<div data-slot="user-message-copy-wrapper">
|
||||||
<Tooltip value={copied() ? "Copied!" : "Copy"} placement="top" gutter={8}>
|
<Tooltip value={copied() ? "Copied!" : "Copy"} placement="top" gutter={8}>
|
||||||
<IconButton icon={copied() ? "check" : "copy"} variant="secondary" onClick={handleCopy} />
|
<IconButton icon={copied() ? "check" : "copy"} variant="secondary" onClick={handleCopy} />
|
||||||
|
|||||||
@@ -44,23 +44,33 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="session-turn-sticky-title"] {
|
[data-slot="session-turn-sticky"] {
|
||||||
width: 100%;
|
width: calc(100% + 9px);
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: var(--session-title-height, 0px);
|
||||||
|
z-index: 20;
|
||||||
background-color: var(--background-stronger);
|
background-color: var(--background-stronger);
|
||||||
z-index: 21;
|
margin-left: -9px;
|
||||||
|
padding-left: 9px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: var(--background-stronger);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="session-turn-response-trigger"] {
|
[data-slot="session-turn-response-trigger"] {
|
||||||
position: sticky;
|
width: fit-content;
|
||||||
top: calc(var(--sticky-header-height, 0px));
|
|
||||||
background-color: var(--background-stronger);
|
|
||||||
z-index: 20;
|
|
||||||
width: calc(100% + 9px);
|
|
||||||
margin-left: -9px;
|
|
||||||
padding-left: 9px;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="session-turn-message-header"] {
|
[data-slot="session-turn-message-header"] {
|
||||||
@@ -75,6 +85,61 @@
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-component="user-message"] [data-slot="user-message-text"] {
|
||||||
|
max-height: var(--user-message-collapsed-height, 64px);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="user-message"][data-expanded="true"] [data-slot="user-message-text"] {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="user-message"][data-can-expand="true"] [data-slot="user-message-text"] {
|
||||||
|
padding-right: 36px;
|
||||||
|
padding-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="user-message"] [data-slot="user-message-text"] [data-slot="user-message-expand"] {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 6px;
|
||||||
|
right: 6px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="user-message"][data-can-expand="true"]
|
||||||
|
[data-slot="user-message-text"]
|
||||||
|
[data-slot="user-message-expand"],
|
||||||
|
[data-component="user-message"][data-expanded="true"]
|
||||||
|
[data-slot="user-message-text"]
|
||||||
|
[data-slot="user-message-expand"] {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 22px;
|
||||||
|
width: 22px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-weak);
|
||||||
|
|
||||||
|
[data-slot="icon-svg"] {
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="user-message"][data-expanded="true"]
|
||||||
|
[data-slot="user-message-text"]
|
||||||
|
[data-slot="user-message-expand"]
|
||||||
|
[data-slot="icon-svg"] {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="user-message"] [data-slot="user-message-text"] [data-slot="user-message-expand"]:hover {
|
||||||
|
background: var(--surface-raised-base);
|
||||||
|
color: var(--text-base);
|
||||||
|
}
|
||||||
|
|
||||||
[data-slot="session-turn-user-badges"] {
|
[data-slot="session-turn-user-badges"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -266,11 +331,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-component="sticky-accordion-header"] {
|
[data-component="sticky-accordion-header"] {
|
||||||
top: var(--sticky-header-height, 40px);
|
position: static;
|
||||||
|
|
||||||
&[data-expanded]::before {
|
|
||||||
top: calc(-1 * var(--sticky-header-height, 40px));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="session-turn-accordion-trigger-content"] {
|
[data-slot="session-turn-accordion-trigger-content"] {
|
||||||
|
|||||||
@@ -13,17 +13,13 @@ import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
|||||||
|
|
||||||
import { Binary } from "@opencode-ai/util/binary"
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
import { createEffect, createMemo, createSignal, For, Match, on, onCleanup, ParentProps, Show, Switch } from "solid-js"
|
import { createEffect, createMemo, createSignal, For, Match, on, onCleanup, ParentProps, Show, Switch } from "solid-js"
|
||||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
|
||||||
import { DiffChanges } from "./diff-changes"
|
import { DiffChanges } from "./diff-changes"
|
||||||
import { Typewriter } from "./typewriter"
|
|
||||||
import { Message, Part } from "./message-part"
|
import { Message, Part } from "./message-part"
|
||||||
import { Markdown } from "./markdown"
|
import { Markdown } from "./markdown"
|
||||||
import { Accordion } from "./accordion"
|
import { Accordion } from "./accordion"
|
||||||
import { StickyAccordionHeader } from "./sticky-accordion-header"
|
import { StickyAccordionHeader } from "./sticky-accordion-header"
|
||||||
import { FileIcon } from "./file-icon"
|
import { FileIcon } from "./file-icon"
|
||||||
import { Icon } from "./icon"
|
import { Icon } from "./icon"
|
||||||
import { ProviderIcon } from "./provider-icon"
|
|
||||||
import type { IconName } from "./provider-icons/types"
|
|
||||||
import { IconButton } from "./icon-button"
|
import { IconButton } from "./icon-button"
|
||||||
import { Tooltip } from "./tooltip"
|
import { Tooltip } from "./tooltip"
|
||||||
import { Card } from "./card"
|
import { Card } from "./card"
|
||||||
@@ -331,8 +327,6 @@ export function SessionTurn(
|
|||||||
|
|
||||||
const response = createMemo(() => lastTextPart()?.text)
|
const response = createMemo(() => lastTextPart()?.text)
|
||||||
const responsePartId = createMemo(() => lastTextPart()?.id)
|
const responsePartId = createMemo(() => lastTextPart()?.id)
|
||||||
const sessionInfo = createMemo(() => data.store.session.find((item) => item.id === props.sessionID))
|
|
||||||
const sessionTitle = createMemo(() => props.sessionTitle ?? sessionInfo()?.title)
|
|
||||||
const hasDiffs = createMemo(() => (data.store.session_diff?.[props.sessionID]?.length ?? 0) > 0)
|
const hasDiffs = createMemo(() => (data.store.session_diff?.[props.sessionID]?.length ?? 0) > 0)
|
||||||
const hideResponsePart = createMemo(() => !working() && !!responsePartId())
|
const hideResponsePart = createMemo(() => !working() && !!responsePartId())
|
||||||
|
|
||||||
@@ -371,15 +365,11 @@ export function SessionTurn(
|
|||||||
const diffBatch = 20
|
const diffBatch = 20
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
stickyTitleRef: undefined as HTMLDivElement | undefined,
|
|
||||||
stickyTriggerRef: undefined as HTMLDivElement | undefined,
|
|
||||||
stickyHeaderHeight: 0,
|
|
||||||
retrySeconds: 0,
|
retrySeconds: 0,
|
||||||
diffsOpen: [] as string[],
|
diffsOpen: [] as string[],
|
||||||
diffLimit: diffInit,
|
diffLimit: diffInit,
|
||||||
status: rawStatus(),
|
status: rawStatus(),
|
||||||
duration: duration(),
|
duration: duration(),
|
||||||
titleShown: false,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
@@ -393,18 +383,6 @@ export function SessionTurn(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!sessionTitle()) {
|
|
||||||
setStore("titleShown", false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (store.titleShown) return
|
|
||||||
const first = allMessages().find((item) => item?.role === "user")
|
|
||||||
if (!first) return
|
|
||||||
if (first.id !== props.messageID) return
|
|
||||||
setStore("titleShown", true)
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const r = retry()
|
const r = retry()
|
||||||
if (!r) {
|
if (!r) {
|
||||||
@@ -420,22 +398,6 @@ export function SessionTurn(
|
|||||||
onCleanup(() => clearInterval(timer))
|
onCleanup(() => clearInterval(timer))
|
||||||
})
|
})
|
||||||
|
|
||||||
createResizeObserver(
|
|
||||||
() => store.stickyTitleRef,
|
|
||||||
({ height }) => {
|
|
||||||
const triggerHeight = store.stickyTriggerRef?.offsetHeight ?? 0
|
|
||||||
setStore("stickyHeaderHeight", height + triggerHeight + 8)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
createResizeObserver(
|
|
||||||
() => store.stickyTriggerRef,
|
|
||||||
({ height }) => {
|
|
||||||
const titleHeight = store.stickyTitleRef?.offsetHeight ?? 0
|
|
||||||
setStore("stickyHeaderHeight", titleHeight + height + 8)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
setStore("duration", duration())
|
setStore("duration", duration())
|
||||||
@@ -491,99 +453,58 @@ export function SessionTurn(
|
|||||||
data-message={msg().id}
|
data-message={msg().id}
|
||||||
data-slot="session-turn-message-container"
|
data-slot="session-turn-message-container"
|
||||||
class={props.classes?.container}
|
class={props.classes?.container}
|
||||||
style={{ "--sticky-header-height": `${store.stickyHeaderHeight}px` }}
|
|
||||||
>
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={isShellMode()}>
|
<Match when={isShellMode()}>
|
||||||
<Part part={shellModePart()!} message={msg()} defaultOpen />
|
<Part part={shellModePart()!} message={msg()} defaultOpen />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<Show when={sessionTitle() && store.titleShown}>
|
<div data-slot="session-turn-sticky">
|
||||||
<div ref={(el) => setStore("stickyTitleRef", el)} data-slot="session-turn-sticky-title">
|
{/* User Message */}
|
||||||
<div data-slot="session-turn-message-header">
|
<div data-slot="session-turn-message-content">
|
||||||
<div data-slot="session-turn-message-title">
|
<Message message={msg()} parts={parts()} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Trigger (sticky) */}
|
||||||
|
<Show when={working() || hasSteps()}>
|
||||||
|
<div data-slot="session-turn-response-trigger">
|
||||||
|
<Button
|
||||||
|
data-expandable={assistantMessages().length > 0}
|
||||||
|
data-slot="session-turn-collapsible-trigger-content"
|
||||||
|
variant="ghost"
|
||||||
|
size="small"
|
||||||
|
onClick={props.onStepsExpandedToggle ?? (() => {})}
|
||||||
|
>
|
||||||
|
<Show when={working()}>
|
||||||
|
<Spinner />
|
||||||
|
</Show>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={working()}>
|
<Match when={retry()}>
|
||||||
<Typewriter as="h1" text={sessionTitle()} data-slot="session-turn-typewriter" />
|
<span data-slot="session-turn-retry-message">
|
||||||
</Match>
|
{(() => {
|
||||||
<Match when={true}>
|
const r = retry()
|
||||||
<h1>{sessionTitle()}</h1>
|
if (!r) return ""
|
||||||
|
return r.message.length > 60 ? r.message.slice(0, 60) + "..." : r.message
|
||||||
|
})()}
|
||||||
|
</span>
|
||||||
|
<span data-slot="session-turn-retry-seconds">
|
||||||
|
· retrying {store.retrySeconds > 0 ? `in ${store.retrySeconds}s ` : ""}
|
||||||
|
</span>
|
||||||
|
<span data-slot="session-turn-retry-attempt">(#{retry()?.attempt})</span>
|
||||||
</Match>
|
</Match>
|
||||||
|
<Match when={working()}>{store.status ?? "Considering next steps"}</Match>
|
||||||
|
<Match when={props.stepsExpanded}>Hide steps</Match>
|
||||||
|
<Match when={!props.stepsExpanded}>Show steps</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
<span>·</span>
|
||||||
|
<span>{store.duration}</span>
|
||||||
|
<Show when={assistantMessages().length > 0}>
|
||||||
|
<Icon name="chevron-grabber-vertical" size="small" />
|
||||||
|
</Show>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Show>
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show
|
|
||||||
when={
|
|
||||||
(msg() as UserMessage).agent ||
|
|
||||||
(msg() as UserMessage).model?.modelID ||
|
|
||||||
(msg() as UserMessage).variant
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div data-slot="session-turn-user-badges">
|
|
||||||
<Show when={(msg() as UserMessage).agent}>
|
|
||||||
<span data-slot="session-turn-badge">{(msg() as UserMessage).agent}</span>
|
|
||||||
</Show>
|
|
||||||
<Show when={(msg() as UserMessage).model?.modelID}>
|
|
||||||
<span data-slot="session-turn-badge" class="inline-flex items-center gap-1">
|
|
||||||
<ProviderIcon
|
|
||||||
id={(msg() as UserMessage).model!.providerID as IconName}
|
|
||||||
class="size-3.5 shrink-0"
|
|
||||||
/>
|
|
||||||
{(msg() as UserMessage).model?.modelID}
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
<Show when={(msg() as UserMessage).variant}>
|
|
||||||
<span data-slot="session-turn-badge">{(msg() as UserMessage).variant}</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
{/* User Message */}
|
|
||||||
<div data-slot="session-turn-message-content">
|
|
||||||
<Message message={msg()} parts={parts()} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Trigger (sticky) */}
|
|
||||||
<Show when={working() || hasSteps()}>
|
|
||||||
<div ref={(el) => setStore("stickyTriggerRef", el)} data-slot="session-turn-response-trigger">
|
|
||||||
<Button
|
|
||||||
data-expandable={assistantMessages().length > 0}
|
|
||||||
data-slot="session-turn-collapsible-trigger-content"
|
|
||||||
variant="ghost"
|
|
||||||
size="small"
|
|
||||||
onClick={props.onStepsExpandedToggle ?? (() => {})}
|
|
||||||
>
|
|
||||||
<Show when={working()}>
|
|
||||||
<Spinner />
|
|
||||||
</Show>
|
|
||||||
<Switch>
|
|
||||||
<Match when={retry()}>
|
|
||||||
<span data-slot="session-turn-retry-message">
|
|
||||||
{(() => {
|
|
||||||
const r = retry()
|
|
||||||
if (!r) return ""
|
|
||||||
return r.message.length > 60 ? r.message.slice(0, 60) + "..." : r.message
|
|
||||||
})()}
|
|
||||||
</span>
|
|
||||||
<span data-slot="session-turn-retry-seconds">
|
|
||||||
· retrying {store.retrySeconds > 0 ? `in ${store.retrySeconds}s ` : ""}
|
|
||||||
</span>
|
|
||||||
<span data-slot="session-turn-retry-attempt">(#{retry()?.attempt})</span>
|
|
||||||
</Match>
|
|
||||||
<Match when={working()}>{store.status ?? "Considering next steps"}</Match>
|
|
||||||
<Match when={props.stepsExpanded}>Hide steps</Match>
|
|
||||||
<Match when={!props.stepsExpanded}>Show steps</Match>
|
|
||||||
</Switch>
|
|
||||||
<span>·</span>
|
|
||||||
<span>{store.duration}</span>
|
|
||||||
<Show when={assistantMessages().length > 0}>
|
|
||||||
<Icon name="chevron-grabber-vertical" size="small" />
|
|
||||||
</Show>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
{/* Response */}
|
{/* Response */}
|
||||||
<Show when={props.stepsExpanded && assistantMessages().length > 0}>
|
<Show when={props.stepsExpanded && assistantMessages().length > 0}>
|
||||||
<div data-slot="session-turn-collapsible-content-inner">
|
<div data-slot="session-turn-collapsible-content-inner">
|
||||||
|
|||||||
Reference in New Issue
Block a user