Skip to content

Commit 5b86fa9

Browse files
committed
wip: desktop work
1 parent aa7e008 commit 5b86fa9

File tree

4 files changed

+106
-81
lines changed

4 files changed

+106
-81
lines changed

packages/desktop/src/components/assistant-message.tsx

Lines changed: 33 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Part, AssistantMessage, ReasoningPart, TextPart, ToolPart } from "@opencode-ai/sdk"
1+
import type { Part, AssistantMessage, ReasoningPart, TextPart, ToolPart, Message } from "@opencode-ai/sdk"
22
import { children, Component, createMemo, For, Match, Show, Switch, type JSX } from "solid-js"
33
import { Dynamic } from "solid-js/web"
44
import { Markdown } from "./markdown"
@@ -17,74 +17,65 @@ import type { WriteTool } from "opencode/tool/write"
1717
import type { TodoWriteTool } from "opencode/tool/todo"
1818
import { DiffChanges } from "./diff-changes"
1919

20-
export function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; lastToolOnly?: boolean }) {
20+
export function AssistantMessage(props: { message: AssistantMessage; parts: Part[] }) {
2121
const filteredParts = createMemo(() => {
22-
let tool = false
2322
return props.parts?.filter((x) => {
24-
if (x.type === "tool" && props.lastToolOnly && tool) return false
25-
if (x.type === "tool") tool = true
23+
if (x.type === "reasoning") return false
2624
return x.type !== "tool" || x.tool !== "todoread"
2725
})
2826
})
2927
return (
3028
<div class="w-full flex flex-col items-start gap-4">
31-
<For each={filteredParts()}>
32-
{(part) => {
33-
const component = createMemo(() => PART_MAPPING[part.type as keyof typeof PART_MAPPING])
34-
return (
35-
<Show when={component()}>
36-
<Dynamic component={component()} part={part as any} message={props.message} />
37-
</Show>
38-
)
39-
}}
40-
</For>
29+
<For each={filteredParts()}>{(part) => <Part part={part} message={props.message} />}</For>
4130
</div>
4231
)
4332
}
4433

34+
export function Part(props: { part: Part; message: Message; readonly?: boolean }) {
35+
const component = createMemo(() => PART_MAPPING[props.part.type as keyof typeof PART_MAPPING])
36+
return (
37+
<Show when={component()}>
38+
<Dynamic component={component()} part={props.part as any} message={props.message} readonly={props.readonly} />
39+
</Show>
40+
)
41+
}
42+
4543
const PART_MAPPING = {
4644
text: TextPart,
4745
tool: ToolPart,
4846
reasoning: ReasoningPart,
4947
}
5048

51-
function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage }) {
52-
return null
53-
// return (
54-
// <Show when={props.part.text.trim()}>
55-
// <div>{props.part.text}</div>
56-
// </Show>
57-
// )
49+
function ReasoningPart(props: { part: ReasoningPart; message: Message }) {
50+
return (
51+
<Show when={props.part.text.trim()}>
52+
<Markdown text={props.part.text.trim()} />
53+
</Show>
54+
)
5855
}
5956

60-
function TextPart(props: { part: TextPart; message: AssistantMessage }) {
57+
function TextPart(props: { part: TextPart; message: Message }) {
6158
return (
6259
<Show when={props.part.text.trim()}>
6360
<Markdown text={props.part.text.trim()} />
6461
</Show>
6562
)
6663
}
6764

68-
function ToolPart(props: { part: ToolPart; message: AssistantMessage }) {
69-
// const sync = useSync()
70-
65+
function ToolPart(props: { part: ToolPart; message: Message; readonly?: boolean }) {
7166
const component = createMemo(() => {
7267
const render = ToolRegistry.render(props.part.tool) ?? GenericTool
73-
7468
const metadata = props.part.state.status === "pending" ? {} : (props.part.state.metadata ?? {})
7569
const input = props.part.state.status === "completed" ? props.part.state.input : {}
76-
// const permissions = sync.data.permission[props.message.sessionID] ?? []
77-
// const permissionIndex = permissions.findIndex((x) => x.callID === props.part.callID)
78-
// const permission = permissions[permissionIndex]
7970

8071
return (
8172
<Dynamic
8273
component={render}
8374
input={input}
8475
tool={props.part.tool}
8576
metadata={metadata}
86-
// permission={permission?.metadata ?? {}}
8777
output={props.part.state.status === "completed" ? props.part.state.output : undefined}
78+
readonly={props.readonly}
8879
/>
8980
)
9081
})
@@ -106,7 +97,12 @@ const isTriggerTitle = (val: any): val is TriggerTitle => {
10697
return typeof val === "object" && val !== null && "title" in val && !(val instanceof Node)
10798
}
10899

109-
function BasicTool(props: { icon: IconProps["name"]; trigger: TriggerTitle | JSX.Element; children?: JSX.Element }) {
100+
function BasicTool(props: {
101+
icon: IconProps["name"]
102+
trigger: TriggerTitle | JSX.Element
103+
children?: JSX.Element
104+
readonly?: boolean
105+
}) {
110106
const resolved = children(() => props.children)
111107
return (
112108
<Collapsible>
@@ -161,13 +157,13 @@ function BasicTool(props: { icon: IconProps["name"]; trigger: TriggerTitle | JSX
161157
</Switch>
162158
</div>
163159
</div>
164-
<Show when={resolved()}>
160+
<Show when={resolved() && !props.readonly}>
165161
<Collapsible.Arrow />
166162
</Show>
167163
</div>
168164
</Collapsible.Trigger>
169-
<Show when={props.children}>
170-
<Collapsible.Content>{props.children}</Collapsible.Content>
165+
<Show when={resolved() && !props.readonly}>
166+
<Collapsible.Content>{resolved()}</Collapsible.Content>
171167
</Show>
172168
</Collapsible>
173169
// <>
@@ -177,15 +173,15 @@ function BasicTool(props: { icon: IconProps["name"]; trigger: TriggerTitle | JSX
177173
}
178174

179175
function GenericTool(props: ToolProps<any>) {
180-
return <BasicTool icon="mcp" trigger={{ title: props.tool }} />
176+
return <BasicTool icon="mcp" trigger={{ title: props.tool }} readonly={props.readonly} />
181177
}
182178

183179
type ToolProps<T extends Tool.Info> = {
184180
input: Partial<Tool.InferParameters<T>>
185181
metadata: Partial<Tool.InferMetadata<T>>
186-
// permission: Record<string, any>
187182
tool: string
188183
output?: string
184+
readonly?: boolean
189185
}
190186

191187
const ToolRegistry = (() => {

packages/desktop/src/pages/index.tsx

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { Code } from "@/components/code"
3333
import { useSync } from "@/context/sync"
3434
import { useSDK } from "@/context/sdk"
3535
import { ProgressCircle } from "@/components/progress-circle"
36-
import { AssistantMessage } from "@/components/assistant-message"
36+
import { AssistantMessage, Part } from "@/components/assistant-message"
3737
import { type AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk"
3838
import { DiffChanges } from "@/components/diff-changes"
3939

@@ -178,6 +178,8 @@ export default function Page() {
178178
}
179179

180180
const handleDiffTriggerClick = (event: MouseEvent) => {
181+
// disabling scroll to diff for now
182+
return
181183
const target = event.currentTarget as HTMLElement
182184
queueMicrotask(() => {
183185
if (target.getAttribute("aria-expanded") !== "true") return
@@ -636,6 +638,7 @@ export default function Page() {
636638
<div class="flex flex-col items-start gap-50 pb-50">
637639
<For each={local.session.userMessages()}>
638640
{(message) => {
641+
const [expanded, setExpanded] = createSignal(false)
639642
const title = createMemo(() => message.summary?.title)
640643
const prompt = createMemo(() => local.session.getMessageText(message))
641644
const summary = createMemo(() => message.summary?.body)
@@ -649,30 +652,32 @@ export default function Page() {
649652
if (!last) return false
650653
return !last.time.completed
651654
})
652-
const lastWithContent = createMemo(() =>
653-
assistantMessages().findLast((m) => {
654-
const parts = sync.data.part[m.id]
655-
return parts?.find((p) => p.type === "text" || p.type === "tool")
656-
}),
657-
)
658655

659656
return (
660-
<div data-message={message.id} class="flex flex-col items-start self-stretch gap-8">
657+
<div
658+
data-message={message.id}
659+
class="flex flex-col items-start self-stretch gap-8 min-h-[calc(100vh-15rem)]"
660+
>
661661
{/* Title */}
662662
<div class="py-2 flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger">
663663
<h1 class="text-14-medium text-text-strong overflow-hidden text-ellipsis min-w-0">
664664
{title() ?? prompt()}
665665
</h1>
666666
</div>
667667
<Show when={title}>
668-
<div class="-mt-5 text-12-regular text-text-base line-clamp-3">{prompt()}</div>
668+
<div class="-mt-8 text-12-regular text-text-base line-clamp-3">{prompt()}</div>
669669
</Show>
670670
{/* Response */}
671671
<div class="w-full flex flex-col gap-2">
672-
<Collapsible variant="ghost">
672+
<Collapsible variant="ghost" open={expanded()} onOpenChange={setExpanded}>
673673
<Collapsible.Trigger class="text-text-weak hover:text-text-strong">
674674
<div class="flex items-center gap-1 self-stretch">
675-
<h2 class="text-12-medium">Show steps</h2>
675+
<h2 class="text-12-medium">
676+
<Switch>
677+
<Match when={expanded()}>Hide steps</Match>
678+
<Match when={!expanded()}>Show steps</Match>
679+
</Switch>
680+
</h2>
676681
<Collapsible.Arrow />
677682
</div>
678683
</Collapsible.Trigger>
@@ -687,11 +692,63 @@ export default function Page() {
687692
</div>
688693
</Collapsible.Content>
689694
</Collapsible>
690-
<Show when={working() && lastWithContent()}>
691-
{(last) => {
692-
const lastParts = createMemo(() => sync.data.part[last().id])
695+
<Show when={working() && !expanded()}>
696+
{(_) => {
697+
const lastMessageWithText = createMemo(() =>
698+
assistantMessages().findLast((m) => {
699+
const parts = sync.data.part[m.id]
700+
return parts?.find((p) => p.type === "text")
701+
}),
702+
)
703+
const lastMessageWithReasoning = createMemo(() =>
704+
assistantMessages().findLast((m) => {
705+
const parts = sync.data.part[m.id]
706+
return parts?.find((p) => p.type === "reasoning")
707+
}),
708+
)
709+
const lastMessageWithTool = createMemo(() =>
710+
assistantMessages().findLast((m) => {
711+
const parts = sync.data.part[m.id]
712+
return parts?.find(
713+
(p) => p.type === "tool" && p.state.status === "completed",
714+
)
715+
}),
716+
)
693717
return (
694-
<AssistantMessage lastToolOnly message={last()} parts={lastParts()} />
718+
<div class="w-full flex flex-col gap-2">
719+
<Switch>
720+
<Match when={lastMessageWithText()}>
721+
{(last) => {
722+
const lastTextPart = createMemo(() =>
723+
sync.data.part[last().id].findLast((p) => p.type === "text"),
724+
)
725+
return <Part message={last()} part={lastTextPart()!} readonly />
726+
}}
727+
</Match>
728+
<Match when={lastMessageWithReasoning()}>
729+
{(last) => {
730+
const lastReasoningPart = createMemo(() =>
731+
sync.data.part[last().id].findLast(
732+
(p) => p.type === "reasoning",
733+
),
734+
)
735+
return (
736+
<Part message={last()} part={lastReasoningPart()!} readonly />
737+
)
738+
}}
739+
</Match>
740+
</Switch>
741+
<Show when={lastMessageWithTool()}>
742+
{(last) => {
743+
const lastToolPart = createMemo(() =>
744+
sync.data.part[last().id].findLast(
745+
(p) => p.type === "tool" && p.state.status === "completed",
746+
),
747+
)
748+
return <Part message={last()} part={lastToolPart()!} readonly />
749+
}}
750+
</Show>
751+
</div>
695752
)
696753
}}
697754
</Show>

packages/ui/src/components/diff.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
[data-slot="diff-hunk-separator-content"] {
1919
position: sticky;
2020
background-color: var(--surface-diff-hidden-base);
21+
color: var(--text-base);
2122
width: var(--pjs-column-content-width);
2223
left: var(--pjs-column-number-width);
2324
padding-left: 8px;

packages/ui/src/styles/utilities.css

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -48,35 +48,6 @@
4848
border-width: 0;
4949
}
5050

51-
.scroller {
52-
--fade-height: 1.5rem;
53-
54-
--mask-top: linear-gradient(to bottom, transparent, black var(--fade-height));
55-
--mask-bottom: linear-gradient(to top, transparent, black var(--fade-height));
56-
57-
mask-image: var(--mask-top), var(--mask-bottom);
58-
mask-repeat: no-repeat;
59-
60-
mask-size: 100% var(--fade-height);
61-
62-
animation: adjust-masks linear;
63-
animation-timeline: scroll(self);
64-
}
65-
66-
@keyframes adjust-masks {
67-
from {
68-
mask-position:
69-
0 calc(0% - var(--fade-height)),
70-
0 100%;
71-
}
72-
73-
to {
74-
mask-position:
75-
0 0,
76-
0 calc(100% + var(--fade-height));
77-
}
78-
}
79-
8051
.truncate-start {
8152
text-overflow: ellipsis;
8253
overflow: hidden;

0 commit comments

Comments
 (0)