Skip to content

Commit 6173b69

Browse files
committed
wip(share): more styling
1 parent fc72cfe commit 6173b69

File tree

11 files changed

+673
-643
lines changed

11 files changed

+673
-643
lines changed

packages/desktop/src/context/session.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useSync } from "./sync"
55
import { makePersisted } from "@solid-primitives/storage"
66
import { TextSelection } from "./local"
77
import { pipe, sumBy } from "remeda"
8-
import { AssistantMessage } from "@opencode-ai/sdk"
8+
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk"
99
import { useParams } from "@solidjs/router"
1010
import { base64Encode } from "@/utils"
1111

@@ -123,8 +123,8 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
123123
user: userMessages,
124124
last: lastUserMessage,
125125
active: activeMessage,
126-
setActive(id: string | undefined) {
127-
setStore("messageId", id)
126+
setActive(message: UserMessage | undefined) {
127+
setStore("messageId", message?.id)
128128
},
129129
},
130130
usage: {

packages/desktop/src/pages/session.tsx

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { For, onCleanup, onMount, Show, Match, Switch, createResource } from "solid-js"
1+
import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo } from "solid-js"
22
import { useLocal, type LocalFile } from "@/context/local"
33
import { createStore } from "solid-js/store"
44
import { getDirectory, getFilename } from "@/utils"
@@ -12,7 +12,8 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
1212
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
1313
import { Tabs } from "@opencode-ai/ui/tabs"
1414
import { Code } from "@opencode-ai/ui/code"
15-
import { SessionTimeline } from "@opencode-ai/ui/session-timeline"
15+
import { SessionTurn } from "@opencode-ai/ui/session-turn"
16+
import { MessageNav } from "@opencode-ai/ui/message-nav"
1617
import { SessionReview } from "@opencode-ai/ui/session-review"
1718
import { SelectDialog } from "@opencode-ai/ui/select-dialog"
1819
import {
@@ -255,6 +256,8 @@ export default function Page() {
255256
return typeof draggable.id === "string" ? draggable.id : undefined
256257
}
257258

259+
const wide = createMemo(() => layout.review.state() === "tab" || !session.diffs().length)
260+
258261
return (
259262
<div class="relative bg-background-base size-full overflow-x-hidden">
260263
<DragDropProvider
@@ -330,14 +333,26 @@ export default function Page() {
330333
flex: layout.review.state() === "pane",
331334
}}
332335
>
333-
<div class="relative shrink-0 px-6 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full max-w-xl mx-auto">
336+
<div class="relative shrink-0 px-6 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full max-w-2xl mx-auto">
334337
<Switch>
335338
<Match when={session.id}>
336-
<SessionTimeline
337-
sessionID={session.id!}
338-
expanded={layout.review.state() === "tab" || !session.diffs().length}
339-
classes={{ root: "pb-20", container: "pb-20" }}
340-
/>
339+
<div class="flex items-start justify-start h-full min-h-0">
340+
<Show when={session.messages.user().length > 1}>
341+
<MessageNav
342+
classList={{ "mt-1.5 mr-3": wide(), "mt-3 mr-8": !wide() }}
343+
messages={session.messages.user()}
344+
current={session.messages.active()}
345+
onMessageSelect={session.messages.setActive}
346+
size={wide() ? "normal" : "compact"}
347+
working={session.working()}
348+
/>
349+
</Show>
350+
<SessionTurn
351+
sessionID={session.id!}
352+
messageID={session.messages.active()?.id!}
353+
classes={{ root: "pb-20 flex-1 min-w-0", content: "pb-20" }}
354+
/>
355+
</div>
341356
</Match>
342357
<Match when={true}>
343358
<div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch">
@@ -456,7 +471,7 @@ export default function Page() {
456471
</DragOverlay>
457472
</DragDropProvider>
458473
<Show when={session.layout.tabs.active}>
459-
<div class="absolute inset-x-0 px-6 max-w-2xl flex flex-col justify-center items-center z-50 mx-auto bottom-6">
474+
<div class="absolute inset-x-0 px-6 max-w-2xl flex flex-col justify-center items-center z-50 mx-auto bottom-8">
460475
<PromptInput
461476
ref={(el) => {
462477
inputRef = el

packages/enterprise/src/routes/share/[sessionID].tsx

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { FileDiff, Message, Part, Session, SessionStatus } from "@opencode-ai/sdk"
2-
import { SessionTimeline } from "@opencode-ai/ui/session-timeline"
1+
import { FileDiff, Message, Part, Session, SessionStatus, UserMessage } from "@opencode-ai/sdk"
2+
import { SessionTurn } from "@opencode-ai/ui/session-turn"
33
import { SessionReview } from "@opencode-ai/ui/session-review"
44
import { DataProvider, useData } from "@opencode-ai/ui/context"
55
import { createAsync, query, RouteDefinition, useParams } from "@solidjs/router"
@@ -10,6 +10,8 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
1010
import { iife } from "@opencode-ai/util/iife"
1111
import { Binary } from "@opencode-ai/util/binary"
1212
import { DateTime } from "luxon"
13+
import { MessageNav } from "@opencode-ai/ui/message-nav"
14+
import { createStore } from "solid-js/store"
1315

1416
const getData = query(async (sessionID) => {
1517
const data = await Share.data(sessionID)
@@ -40,7 +42,6 @@ const getData = query(async (sessionID) => {
4042
message: {},
4143
part: {},
4244
}
43-
4445
for (const item of data) {
4546
switch (item.type) {
4647
case "session":
@@ -82,14 +83,28 @@ export default function () {
8283
<DataProvider data={data()}>
8384
{iife(() => {
8485
const data = useData()
86+
const [store, setStore] = createStore({
87+
messageId: undefined as string | undefined,
88+
})
8589
const match = createMemo(() => Binary.search(data.session, params.sessionID!, (s) => s.id))
8690
if (!match().found) throw new Error(`Session ${params.sessionID} not found`)
8791
const info = createMemo(() => data.session[match().index])
88-
const firstUserMessage = createMemo(() =>
89-
data.message[params.sessionID!]?.filter((m) => m.role === "user")?.at(0),
92+
const messages = createMemo(() =>
93+
params.sessionID ? (data.message[params.sessionID]?.filter((m) => m.role === "user") ?? []) : [],
94+
)
95+
const firstUserMessage = createMemo(() => messages().at(0))
96+
const activeMessage = createMemo(
97+
() => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(),
9098
)
91-
const provider = createMemo(() => firstUserMessage()?.model?.providerID)
92-
const model = createMemo(() => firstUserMessage()?.model?.modelID)
99+
function setActiveMessage(message: UserMessage | undefined) {
100+
if (message) {
101+
setStore("messageId", message.id)
102+
} else {
103+
setStore("messageId", undefined)
104+
}
105+
}
106+
const provider = createMemo(() => activeMessage()?.model?.providerID)
107+
const model = createMemo(() => activeMessage()?.model?.modelID)
93108
const diffs = createMemo(() => data.session_diff[params.sessionID!] ?? [])
94109

95110
return (
@@ -145,15 +160,26 @@ export default function () {
145160
</div>
146161
<div class="text-left text-16-medium text-text-strong">{info().title}</div>
147162
</div>
148-
<SessionTimeline
149-
sessionID={params.sessionID!}
150-
classes={{ root: "grow", content: "flex flex-col justify-between", container: "pb-20" }}
151-
expanded
152-
>
153-
<div class="flex items-center justify-center pb-8 shrink-0">
154-
<Logo class="w-58.5 opacity-12" />
155-
</div>
156-
</SessionTimeline>
163+
<div class="flex items-start justify-start h-full min-h-0">
164+
<Show when={messages().length > 1}>
165+
<MessageNav
166+
classList={{ "mt-3 mr-3": true }}
167+
messages={messages()}
168+
current={activeMessage()}
169+
onMessageSelect={setActiveMessage}
170+
size={!diffs().length ? "normal" : "compact"}
171+
/>
172+
</Show>
173+
<SessionTurn
174+
sessionID={params.sessionID!}
175+
messageID={store.messageId ?? firstUserMessage()!.id!}
176+
classes={{ root: "grow", content: "flex flex-col justify-between", container: "pb-20" }}
177+
>
178+
<div class="flex items-center justify-center pb-8 shrink-0">
179+
<Logo class="w-58.5 opacity-12" />
180+
</div>
181+
</SessionTurn>
182+
</div>
157183
</div>
158184
<Show when={diffs().length}>
159185
<div class="relative grow px-6 pt-14 flex-1 min-h-0 border-l border-border-weak-base">
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
[data-component="message-nav"] {
2+
/* margin-right: 32px; */
3+
/* margin-top: 12px; */
4+
flex-shrink: 0;
5+
display: flex;
6+
flex-direction: column;
7+
align-items: flex-start;
8+
padding-left: 0;
9+
list-style: none;
10+
11+
&[data-size="normal"] {
12+
position: absolute;
13+
right: 100%;
14+
width: 240px;
15+
/* margin-top: 12px; */
16+
17+
@media (min-width: 80rem) {
18+
gap: 8px;
19+
/* margin-top: 4px; */
20+
}
21+
}
22+
}
23+
24+
[data-slot="message-nav-item"] {
25+
display: flex;
26+
align-items: center;
27+
align-self: stretch;
28+
justify-content: flex-end;
29+
30+
[data-component="message-nav"][data-size="normal"] & {
31+
justify-content: flex-start;
32+
}
33+
}
34+
35+
[data-slot="message-nav-tick-button"] {
36+
display: flex;
37+
align-items: center;
38+
justify-content: flex-start;
39+
height: 8px;
40+
width: 32px;
41+
/* margin-right: -12px; */
42+
cursor: pointer;
43+
border: none;
44+
background: none;
45+
padding: 0;
46+
47+
&[data-active] [data-slot="message-nav-tick-line"] {
48+
background-color: var(--icon-strong-base);
49+
width: 100%;
50+
}
51+
}
52+
53+
[data-slot="message-nav-tick-line"] {
54+
height: 1px;
55+
width: 20px;
56+
background-color: var(--icon-base);
57+
transition:
58+
width 0.2s,
59+
background-color 0.2s;
60+
}
61+
62+
[data-slot="message-nav-tick-button"]:hover [data-slot="message-nav-tick-line"] {
63+
width: 100%;
64+
background-color: var(--icon-strong-base);
65+
}
66+
67+
[data-slot="message-nav-message-button"] {
68+
display: flex;
69+
align-items: center;
70+
align-self: stretch;
71+
width: 100%;
72+
column-gap: 8px;
73+
cursor: default;
74+
border: none;
75+
background: none;
76+
padding: 0;
77+
}
78+
79+
[data-slot="message-nav-title-preview"] {
80+
font-size: 14px; /* text-14-regular */
81+
color: var(--text-weak);
82+
white-space: nowrap;
83+
overflow: hidden;
84+
text-overflow: ellipsis;
85+
min-width: 0;
86+
text-align: left;
87+
88+
&[data-active] {
89+
color: var(--text-strong);
90+
}
91+
}
92+
93+
[data-slot="message-nav-item"]:hover [data-slot="message-nav-title-preview"] {
94+
color: var(--text-base);
95+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { UserMessage } from "@opencode-ai/sdk"
2+
import { ComponentProps, createMemo, For, Match, Show, splitProps, Switch } from "solid-js"
3+
import { DiffChanges } from "./diff-changes"
4+
import { Spinner } from "./spinner"
5+
6+
export function MessageNav(
7+
props: ComponentProps<"ul"> & {
8+
messages: UserMessage[]
9+
current?: UserMessage
10+
size: "normal" | "compact"
11+
working?: boolean
12+
onMessageSelect: (message: UserMessage) => void
13+
},
14+
) {
15+
const [local, others] = splitProps(props, ["messages", "current", "size", "working", "onMessageSelect"])
16+
const lastUserMessage = createMemo(() => {
17+
return local.messages?.at(0)
18+
})
19+
20+
return (
21+
<ul role="list" data-component="message-nav" data-size={local.size} {...others}>
22+
<For each={local.messages}>
23+
{(message) => {
24+
const messageWorking = createMemo(() => message.id === lastUserMessage()?.id && local.working)
25+
const handleClick = () => local.onMessageSelect(message)
26+
27+
return (
28+
<li data-slot="message-nav-item">
29+
<Switch>
30+
<Match when={local.size === "compact"}>
31+
<button
32+
data-slot="message-nav-tick-button"
33+
data-active={message.id === local.current?.id || undefined}
34+
onClick={handleClick}
35+
>
36+
<div data-slot="message-nav-tick-line" />
37+
</button>
38+
</Match>
39+
<Match when={local.size === "normal"}>
40+
<button data-slot="message-nav-message-button" onClick={handleClick}>
41+
<Switch>
42+
<Match when={messageWorking()}>
43+
<Spinner />
44+
</Match>
45+
<Match when={true}>
46+
<DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
47+
</Match>
48+
</Switch>
49+
<div
50+
data-slot="message-nav-title-preview"
51+
data-active={message.id === local.current?.id || undefined}
52+
>
53+
<Show when={message.summary?.title} fallback="New message">
54+
{message.summary?.title}
55+
</Show>
56+
</div>
57+
</button>
58+
</Match>
59+
</Switch>
60+
</li>
61+
)
62+
}}
63+
</For>
64+
</ul>
65+
)
66+
}

packages/ui/src/components/message-progress.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { Part } from "./message-part"
33
import { Spinner } from "./spinner"
44
import { useData } from "../context/data"
55
import type { AssistantMessage as AssistantMessageType, ToolPart } from "@opencode-ai/sdk"
6-
import "./message-progress.css"
76

87
export function MessageProgress(props: { assistantMessages: () => AssistantMessageType[]; done?: boolean }) {
98
const data = useData()

0 commit comments

Comments
 (0)