feat(app): new session layout
This commit is contained in:
@@ -1296,7 +1296,13 @@ export default function Layout(props: ParentProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const SessionItem = (props: { session: Session; slug: string; mobile?: boolean; dense?: boolean }): JSX.Element => {
|
const SessionItem = (props: {
|
||||||
|
session: Session
|
||||||
|
slug: string
|
||||||
|
mobile?: boolean
|
||||||
|
dense?: boolean
|
||||||
|
popover?: boolean
|
||||||
|
}): JSX.Element => {
|
||||||
const notification = useNotification()
|
const notification = useNotification()
|
||||||
const notifications = createMemo(() => notification.session.unseen(props.session.id))
|
const notifications = createMemo(() => notification.session.unseen(props.session.id))
|
||||||
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
|
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
|
||||||
@@ -1335,6 +1341,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
)
|
)
|
||||||
const hoverReady = createMemo(() => sessionStore.message[props.session.id] !== undefined)
|
const hoverReady = createMemo(() => sessionStore.message[props.session.id] !== undefined)
|
||||||
const hoverAllowed = createMemo(() => !props.mobile && layout.sidebar.opened())
|
const hoverAllowed = createMemo(() => !props.mobile && layout.sidebar.opened())
|
||||||
|
const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed())
|
||||||
const isActive = createMemo(() => props.session.id === params.id)
|
const isActive = createMemo(() => props.session.id === params.id)
|
||||||
|
|
||||||
const messageLabel = (message: Message) => {
|
const messageLabel = (message: Message) => {
|
||||||
@@ -1370,23 +1377,14 @@ export default function Layout(props: ParentProps) {
|
|||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
<Tooltip
|
<InlineEditor
|
||||||
inactive={hoverAllowed()}
|
id={`session:${props.session.id}`}
|
||||||
placement="top-start"
|
value={() => props.session.title}
|
||||||
value={props.session.title}
|
onSave={(next) => renameSession(props.session, next)}
|
||||||
gutter={0}
|
class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate"
|
||||||
openDelay={3000}
|
displayClass="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate"
|
||||||
class="grow-1 min-w-0"
|
stopPropagation
|
||||||
>
|
/>
|
||||||
<InlineEditor
|
|
||||||
id={`session:${props.session.id}`}
|
|
||||||
value={() => props.session.title}
|
|
||||||
onSave={(next) => renameSession(props.session, next)}
|
|
||||||
class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate"
|
|
||||||
displayClass="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate"
|
|
||||||
stopPropagation
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Show when={props.session.summary}>
|
<Show when={props.session.summary}>
|
||||||
{(summary) => (
|
{(summary) => (
|
||||||
<div class="group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
|
<div class="group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
|
||||||
@@ -1396,7 +1394,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</A>
|
</A>
|
||||||
))
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -1405,8 +1403,12 @@ export default function Layout(props: ParentProps) {
|
|||||||
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active"
|
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active"
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
when={hoverAllowed()}
|
when={hoverEnabled()}
|
||||||
fallback={item}
|
fallback={
|
||||||
|
<Tooltip placement={props.mobile ? "bottom" : "right"} value={props.session.title} gutter={10}>
|
||||||
|
{item}
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<HoverCard openDelay={150} closeDelay={100} placement="right" gutter={12} trigger={item}>
|
<HoverCard openDelay={150} closeDelay={100} placement="right" gutter={12} trigger={item}>
|
||||||
<Show when={hoverReady()} fallback={<div class="text-12-regular text-text-weak">Loading messages…</div>}>
|
<Show when={hoverReady()} fallback={<div class="text-12-regular text-text-weak">Loading messages…</div>}>
|
||||||
@@ -1730,6 +1732,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
slug={base64Encode(props.project.worktree)}
|
slug={base64Encode(props.project.worktree)}
|
||||||
dense
|
dense
|
||||||
mobile={props.mobile}
|
mobile={props.mobile}
|
||||||
|
popover={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
@@ -1746,7 +1749,13 @@ export default function Layout(props: ParentProps) {
|
|||||||
</div>
|
</div>
|
||||||
<For each={sessions(directory)}>
|
<For each={sessions(directory)}>
|
||||||
{(session) => (
|
{(session) => (
|
||||||
<SessionItem session={session} slug={base64Encode(directory)} dense mobile={props.mobile} />
|
<SessionItem
|
||||||
|
session={session}
|
||||||
|
slug={base64Encode(directory)}
|
||||||
|
dense
|
||||||
|
mobile={props.mobile}
|
||||||
|
popover={false}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1233,6 +1233,7 @@ 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}
|
||||||
@@ -1241,8 +1242,7 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
classes={{
|
classes={{
|
||||||
root: "min-w-0 w-full relative",
|
root: "min-w-0 w-full relative",
|
||||||
content:
|
content: "flex flex-col justify-between !overflow-visible",
|
||||||
"flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
|
|
||||||
container: "w-full px-4 md:px-6",
|
container: "w-full px-4 md:px-6",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -295,13 +295,13 @@ export default function () {
|
|||||||
{(message) => (
|
{(message) => (
|
||||||
<SessionTurn
|
<SessionTurn
|
||||||
sessionID={data().sessionID}
|
sessionID={data().sessionID}
|
||||||
|
sessionTitle={info().title}
|
||||||
messageID={message.id}
|
messageID={message.id}
|
||||||
stepsExpanded={store.expandedSteps[message.id] ?? false}
|
stepsExpanded={store.expandedSteps[message.id] ?? false}
|
||||||
onStepsExpandedToggle={() => setStore("expandedSteps", message.id, (v) => !v)}
|
onStepsExpandedToggle={() => setStore("expandedSteps", message.id, (v) => !v)}
|
||||||
classes={{
|
classes={{
|
||||||
root: "min-w-0 w-full relative",
|
root: "min-w-0 w-full relative",
|
||||||
content:
|
content: "flex flex-col justify-between !overflow-visible",
|
||||||
"flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
|
|
||||||
container: "px-4",
|
container: "px-4",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -29,23 +29,6 @@
|
|||||||
gap: 28px;
|
gap: 28px;
|
||||||
overflow-anchor: none;
|
overflow-anchor: none;
|
||||||
|
|
||||||
[data-slot="session-turn-user-badges"] {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
display: flex;
|
|
||||||
gap: 6px;
|
|
||||||
padding-left: 16px;
|
|
||||||
background: linear-gradient(to right, transparent, var(--background-stronger) 12px);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.15s ease;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover [data-slot="session-turn-user-badges"] {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-turn-badge"] {
|
[data-slot="session-turn-badge"] {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -71,7 +54,7 @@
|
|||||||
|
|
||||||
[data-slot="session-turn-response-trigger"] {
|
[data-slot="session-turn-response-trigger"] {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 32px;
|
top: calc(var(--sticky-header-height, 0px));
|
||||||
background-color: var(--background-stronger);
|
background-color: var(--background-stronger);
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
width: calc(100% + 9px);
|
width: calc(100% + 9px);
|
||||||
@@ -88,10 +71,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="session-turn-message-content"] {
|
[data-slot="session-turn-message-content"] {
|
||||||
margin-top: -18px;
|
margin-top: 0;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-user-badges"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
[data-slot="session-turn-message-title"] {
|
[data-slot="session-turn-message-title"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: var(--font-size-large);
|
font-size: var(--font-size-large);
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ function AssistantMessageItem(props: {
|
|||||||
export function SessionTurn(
|
export function SessionTurn(
|
||||||
props: ParentProps<{
|
props: ParentProps<{
|
||||||
sessionID: string
|
sessionID: string
|
||||||
|
sessionTitle?: string
|
||||||
messageID: string
|
messageID: string
|
||||||
lastUserMessageID?: string
|
lastUserMessageID?: string
|
||||||
stepsExpanded?: boolean
|
stepsExpanded?: boolean
|
||||||
@@ -330,7 +331,9 @@ export function SessionTurn(
|
|||||||
|
|
||||||
const response = createMemo(() => lastTextPart()?.text)
|
const response = createMemo(() => lastTextPart()?.text)
|
||||||
const responsePartId = createMemo(() => lastTextPart()?.id)
|
const responsePartId = createMemo(() => lastTextPart()?.id)
|
||||||
const hasDiffs = createMemo(() => message()?.summary?.diffs?.length)
|
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 hideResponsePart = createMemo(() => !working() && !!responsePartId())
|
const hideResponsePart = createMemo(() => !working() && !!responsePartId())
|
||||||
|
|
||||||
const [responseCopied, setResponseCopied] = createSignal(false)
|
const [responseCopied, setResponseCopied] = createSignal(false)
|
||||||
@@ -376,6 +379,7 @@ export function SessionTurn(
|
|||||||
diffLimit: diffInit,
|
diffLimit: diffInit,
|
||||||
status: rawStatus(),
|
status: rawStatus(),
|
||||||
duration: duration(),
|
duration: duration(),
|
||||||
|
titleShown: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
@@ -389,6 +393,18 @@ 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) {
|
||||||
@@ -482,40 +498,53 @@ export function SessionTurn(
|
|||||||
<Part part={shellModePart()!} message={msg()} defaultOpen />
|
<Part part={shellModePart()!} message={msg()} defaultOpen />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
{/* Title (sticky) */}
|
<Show when={sessionTitle() && store.titleShown}>
|
||||||
<div ref={(el) => setStore("stickyTitleRef", el)} data-slot="session-turn-sticky-title">
|
<div ref={(el) => setStore("stickyTitleRef", el)} data-slot="session-turn-sticky-title">
|
||||||
<div data-slot="session-turn-message-header">
|
<div data-slot="session-turn-message-header">
|
||||||
<div data-slot="session-turn-message-title">
|
<div data-slot="session-turn-message-title">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={working()}>
|
<Match when={working()}>
|
||||||
<Typewriter as="h1" text={msg().summary?.title} data-slot="session-turn-typewriter" />
|
<Typewriter as="h1" text={sessionTitle()} data-slot="session-turn-typewriter" />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<h1>{msg().summary?.title}</h1>
|
<h1>{sessionTitle()}</h1>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
|
||||||
<span data-slot="session-turn-badge">{(msg() as UserMessage).variant || "default"}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 */}
|
{/* User Message */}
|
||||||
<div data-slot="session-turn-message-content">
|
<div data-slot="session-turn-message-content">
|
||||||
<Message message={msg()} parts={parts()} />
|
<Message message={msg()} parts={parts()} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Trigger (sticky) */}
|
{/* Trigger (sticky) */}
|
||||||
<Show when={working() || hasSteps()}>
|
<Show when={working() || hasSteps()}>
|
||||||
<div ref={(el) => setStore("stickyTriggerRef", el)} data-slot="session-turn-response-trigger">
|
<div ref={(el) => setStore("stickyTriggerRef", el)} data-slot="session-turn-response-trigger">
|
||||||
@@ -612,7 +641,7 @@ export function SessionTurn(
|
|||||||
setStore("diffsOpen", value)
|
setStore("diffsOpen", value)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<For each={(msg().summary?.diffs ?? []).slice(0, store.diffLimit)}>
|
<For each={(data.store.session_diff?.[props.sessionID] ?? []).slice(0, store.diffLimit)}>
|
||||||
{(diff) => (
|
{(diff) => (
|
||||||
<Accordion.Item value={diff.file}>
|
<Accordion.Item value={diff.file}>
|
||||||
<StickyAccordionHeader>
|
<StickyAccordionHeader>
|
||||||
@@ -658,13 +687,13 @@ export function SessionTurn(
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Show when={(msg().summary?.diffs?.length ?? 0) > store.diffLimit}>
|
<Show when={(data.store.session_diff?.[props.sessionID]?.length ?? 0) > store.diffLimit}>
|
||||||
<Button
|
<Button
|
||||||
data-slot="session-turn-accordion-more"
|
data-slot="session-turn-accordion-more"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const total = msg().summary?.diffs?.length ?? 0
|
const total = data.store.session_diff?.[props.sessionID]?.length ?? 0
|
||||||
setStore("diffLimit", (limit) => {
|
setStore("diffLimit", (limit) => {
|
||||||
const next = limit + diffBatch
|
const next = limit + diffBatch
|
||||||
if (next > total) return total
|
if (next > total) return total
|
||||||
@@ -672,7 +701,8 @@ export function SessionTurn(
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Show more changes ({(msg().summary?.diffs?.length ?? 0) - store.diffLimit})
|
Show more changes (
|
||||||
|
{(data.store.session_diff?.[props.sessionID]?.length ?? 0) - store.diffLimit})
|
||||||
</Button>
|
</Button>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user