mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-22 02:04:22 +01:00
wip: desktop work
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { Part, AssistantMessage, ReasoningPart, TextPart, ToolPart } from "@opencode-ai/sdk"
|
||||
import type { Part, AssistantMessage, ReasoningPart, TextPart, ToolPart, Message } from "@opencode-ai/sdk"
|
||||
import { children, Component, createMemo, For, Match, Show, Switch, type JSX } from "solid-js"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import { Markdown } from "./markdown"
|
||||
@@ -17,47 +17,36 @@ import type { WriteTool } from "opencode/tool/write"
|
||||
import type { TodoWriteTool } from "opencode/tool/todo"
|
||||
import { DiffChanges } from "./diff-changes"
|
||||
|
||||
export function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; lastToolOnly?: boolean }) {
|
||||
export function AssistantMessage(props: { message: AssistantMessage; parts: Part[] }) {
|
||||
const filteredParts = createMemo(() => {
|
||||
let tool = false
|
||||
return props.parts?.filter((x) => {
|
||||
if (x.type === "tool" && props.lastToolOnly && tool) return false
|
||||
if (x.type === "tool") tool = true
|
||||
if (x.type === "reasoning") return false
|
||||
return x.type !== "tool" || x.tool !== "todoread"
|
||||
})
|
||||
})
|
||||
return (
|
||||
<div class="w-full flex flex-col items-start gap-4">
|
||||
<For each={filteredParts()}>
|
||||
{(part) => {
|
||||
const component = createMemo(() => PART_MAPPING[part.type as keyof typeof PART_MAPPING])
|
||||
return (
|
||||
<Show when={component()}>
|
||||
<Dynamic component={component()} part={part as any} message={props.message} />
|
||||
</Show>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
<For each={filteredParts()}>{(part) => <Part part={part} message={props.message} />}</For>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Part(props: { part: Part; message: Message; readonly?: boolean }) {
|
||||
const component = createMemo(() => PART_MAPPING[props.part.type as keyof typeof PART_MAPPING])
|
||||
return (
|
||||
<Show when={component()}>
|
||||
<Dynamic component={component()} part={props.part as any} message={props.message} readonly={props.readonly} />
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
const PART_MAPPING = {
|
||||
text: TextPart,
|
||||
tool: ToolPart,
|
||||
reasoning: ReasoningPart,
|
||||
}
|
||||
|
||||
function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage }) {
|
||||
return null
|
||||
// return (
|
||||
// <Show when={props.part.text.trim()}>
|
||||
// <div>{props.part.text}</div>
|
||||
// </Show>
|
||||
// )
|
||||
}
|
||||
|
||||
function TextPart(props: { part: TextPart; message: AssistantMessage }) {
|
||||
function ReasoningPart(props: { part: ReasoningPart; message: Message }) {
|
||||
return (
|
||||
<Show when={props.part.text.trim()}>
|
||||
<Markdown text={props.part.text.trim()} />
|
||||
@@ -65,17 +54,19 @@ function TextPart(props: { part: TextPart; message: AssistantMessage }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ToolPart(props: { part: ToolPart; message: AssistantMessage }) {
|
||||
// const sync = useSync()
|
||||
function TextPart(props: { part: TextPart; message: Message }) {
|
||||
return (
|
||||
<Show when={props.part.text.trim()}>
|
||||
<Markdown text={props.part.text.trim()} />
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function ToolPart(props: { part: ToolPart; message: Message; readonly?: boolean }) {
|
||||
const component = createMemo(() => {
|
||||
const render = ToolRegistry.render(props.part.tool) ?? GenericTool
|
||||
|
||||
const metadata = props.part.state.status === "pending" ? {} : (props.part.state.metadata ?? {})
|
||||
const input = props.part.state.status === "completed" ? props.part.state.input : {}
|
||||
// const permissions = sync.data.permission[props.message.sessionID] ?? []
|
||||
// const permissionIndex = permissions.findIndex((x) => x.callID === props.part.callID)
|
||||
// const permission = permissions[permissionIndex]
|
||||
|
||||
return (
|
||||
<Dynamic
|
||||
@@ -83,8 +74,8 @@ function ToolPart(props: { part: ToolPart; message: AssistantMessage }) {
|
||||
input={input}
|
||||
tool={props.part.tool}
|
||||
metadata={metadata}
|
||||
// permission={permission?.metadata ?? {}}
|
||||
output={props.part.state.status === "completed" ? props.part.state.output : undefined}
|
||||
readonly={props.readonly}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@@ -106,7 +97,12 @@ const isTriggerTitle = (val: any): val is TriggerTitle => {
|
||||
return typeof val === "object" && val !== null && "title" in val && !(val instanceof Node)
|
||||
}
|
||||
|
||||
function BasicTool(props: { icon: IconProps["name"]; trigger: TriggerTitle | JSX.Element; children?: JSX.Element }) {
|
||||
function BasicTool(props: {
|
||||
icon: IconProps["name"]
|
||||
trigger: TriggerTitle | JSX.Element
|
||||
children?: JSX.Element
|
||||
readonly?: boolean
|
||||
}) {
|
||||
const resolved = children(() => props.children)
|
||||
return (
|
||||
<Collapsible>
|
||||
@@ -161,13 +157,13 @@ function BasicTool(props: { icon: IconProps["name"]; trigger: TriggerTitle | JSX
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={resolved()}>
|
||||
<Show when={resolved() && !props.readonly}>
|
||||
<Collapsible.Arrow />
|
||||
</Show>
|
||||
</div>
|
||||
</Collapsible.Trigger>
|
||||
<Show when={props.children}>
|
||||
<Collapsible.Content>{props.children}</Collapsible.Content>
|
||||
<Show when={resolved() && !props.readonly}>
|
||||
<Collapsible.Content>{resolved()}</Collapsible.Content>
|
||||
</Show>
|
||||
</Collapsible>
|
||||
// <>
|
||||
@@ -177,15 +173,15 @@ function BasicTool(props: { icon: IconProps["name"]; trigger: TriggerTitle | JSX
|
||||
}
|
||||
|
||||
function GenericTool(props: ToolProps<any>) {
|
||||
return <BasicTool icon="mcp" trigger={{ title: props.tool }} />
|
||||
return <BasicTool icon="mcp" trigger={{ title: props.tool }} readonly={props.readonly} />
|
||||
}
|
||||
|
||||
type ToolProps<T extends Tool.Info> = {
|
||||
input: Partial<Tool.InferParameters<T>>
|
||||
metadata: Partial<Tool.InferMetadata<T>>
|
||||
// permission: Record<string, any>
|
||||
tool: string
|
||||
output?: string
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
const ToolRegistry = (() => {
|
||||
|
||||
@@ -33,7 +33,7 @@ import { Code } from "@/components/code"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { ProgressCircle } from "@/components/progress-circle"
|
||||
import { AssistantMessage } from "@/components/assistant-message"
|
||||
import { AssistantMessage, Part } from "@/components/assistant-message"
|
||||
import { type AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk"
|
||||
import { DiffChanges } from "@/components/diff-changes"
|
||||
|
||||
@@ -178,6 +178,8 @@ export default function Page() {
|
||||
}
|
||||
|
||||
const handleDiffTriggerClick = (event: MouseEvent) => {
|
||||
// disabling scroll to diff for now
|
||||
return
|
||||
const target = event.currentTarget as HTMLElement
|
||||
queueMicrotask(() => {
|
||||
if (target.getAttribute("aria-expanded") !== "true") return
|
||||
@@ -636,6 +638,7 @@ export default function Page() {
|
||||
<div class="flex flex-col items-start gap-50 pb-50">
|
||||
<For each={local.session.userMessages()}>
|
||||
{(message) => {
|
||||
const [expanded, setExpanded] = createSignal(false)
|
||||
const title = createMemo(() => message.summary?.title)
|
||||
const prompt = createMemo(() => local.session.getMessageText(message))
|
||||
const summary = createMemo(() => message.summary?.body)
|
||||
@@ -649,15 +652,12 @@ export default function Page() {
|
||||
if (!last) return false
|
||||
return !last.time.completed
|
||||
})
|
||||
const lastWithContent = createMemo(() =>
|
||||
assistantMessages().findLast((m) => {
|
||||
const parts = sync.data.part[m.id]
|
||||
return parts?.find((p) => p.type === "text" || p.type === "tool")
|
||||
}),
|
||||
)
|
||||
|
||||
return (
|
||||
<div data-message={message.id} class="flex flex-col items-start self-stretch gap-8">
|
||||
<div
|
||||
data-message={message.id}
|
||||
class="flex flex-col items-start self-stretch gap-8 min-h-[calc(100vh-15rem)]"
|
||||
>
|
||||
{/* Title */}
|
||||
<div class="py-2 flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger">
|
||||
<h1 class="text-14-medium text-text-strong overflow-hidden text-ellipsis min-w-0">
|
||||
@@ -665,14 +665,19 @@ export default function Page() {
|
||||
</h1>
|
||||
</div>
|
||||
<Show when={title}>
|
||||
<div class="-mt-5 text-12-regular text-text-base line-clamp-3">{prompt()}</div>
|
||||
<div class="-mt-8 text-12-regular text-text-base line-clamp-3">{prompt()}</div>
|
||||
</Show>
|
||||
{/* Response */}
|
||||
<div class="w-full flex flex-col gap-2">
|
||||
<Collapsible variant="ghost">
|
||||
<Collapsible variant="ghost" open={expanded()} onOpenChange={setExpanded}>
|
||||
<Collapsible.Trigger class="text-text-weak hover:text-text-strong">
|
||||
<div class="flex items-center gap-1 self-stretch">
|
||||
<h2 class="text-12-medium">Show steps</h2>
|
||||
<h2 class="text-12-medium">
|
||||
<Switch>
|
||||
<Match when={expanded()}>Hide steps</Match>
|
||||
<Match when={!expanded()}>Show steps</Match>
|
||||
</Switch>
|
||||
</h2>
|
||||
<Collapsible.Arrow />
|
||||
</div>
|
||||
</Collapsible.Trigger>
|
||||
@@ -687,11 +692,63 @@ export default function Page() {
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
<Show when={working() && lastWithContent()}>
|
||||
{(last) => {
|
||||
const lastParts = createMemo(() => sync.data.part[last().id])
|
||||
<Show when={working() && !expanded()}>
|
||||
{(_) => {
|
||||
const lastMessageWithText = createMemo(() =>
|
||||
assistantMessages().findLast((m) => {
|
||||
const parts = sync.data.part[m.id]
|
||||
return parts?.find((p) => p.type === "text")
|
||||
}),
|
||||
)
|
||||
const lastMessageWithReasoning = createMemo(() =>
|
||||
assistantMessages().findLast((m) => {
|
||||
const parts = sync.data.part[m.id]
|
||||
return parts?.find((p) => p.type === "reasoning")
|
||||
}),
|
||||
)
|
||||
const lastMessageWithTool = createMemo(() =>
|
||||
assistantMessages().findLast((m) => {
|
||||
const parts = sync.data.part[m.id]
|
||||
return parts?.find(
|
||||
(p) => p.type === "tool" && p.state.status === "completed",
|
||||
)
|
||||
}),
|
||||
)
|
||||
return (
|
||||
<AssistantMessage lastToolOnly message={last()} parts={lastParts()} />
|
||||
<div class="w-full flex flex-col gap-2">
|
||||
<Switch>
|
||||
<Match when={lastMessageWithText()}>
|
||||
{(last) => {
|
||||
const lastTextPart = createMemo(() =>
|
||||
sync.data.part[last().id].findLast((p) => p.type === "text"),
|
||||
)
|
||||
return <Part message={last()} part={lastTextPart()!} readonly />
|
||||
}}
|
||||
</Match>
|
||||
<Match when={lastMessageWithReasoning()}>
|
||||
{(last) => {
|
||||
const lastReasoningPart = createMemo(() =>
|
||||
sync.data.part[last().id].findLast(
|
||||
(p) => p.type === "reasoning",
|
||||
),
|
||||
)
|
||||
return (
|
||||
<Part message={last()} part={lastReasoningPart()!} readonly />
|
||||
)
|
||||
}}
|
||||
</Match>
|
||||
</Switch>
|
||||
<Show when={lastMessageWithTool()}>
|
||||
{(last) => {
|
||||
const lastToolPart = createMemo(() =>
|
||||
sync.data.part[last().id].findLast(
|
||||
(p) => p.type === "tool" && p.state.status === "completed",
|
||||
),
|
||||
)
|
||||
return <Part message={last()} part={lastToolPart()!} readonly />
|
||||
}}
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
[data-slot="diff-hunk-separator-content"] {
|
||||
position: sticky;
|
||||
background-color: var(--surface-diff-hidden-base);
|
||||
color: var(--text-base);
|
||||
width: var(--pjs-column-content-width);
|
||||
left: var(--pjs-column-number-width);
|
||||
padding-left: 8px;
|
||||
|
||||
@@ -48,35 +48,6 @@
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.scroller {
|
||||
--fade-height: 1.5rem;
|
||||
|
||||
--mask-top: linear-gradient(to bottom, transparent, black var(--fade-height));
|
||||
--mask-bottom: linear-gradient(to top, transparent, black var(--fade-height));
|
||||
|
||||
mask-image: var(--mask-top), var(--mask-bottom);
|
||||
mask-repeat: no-repeat;
|
||||
|
||||
mask-size: 100% var(--fade-height);
|
||||
|
||||
animation: adjust-masks linear;
|
||||
animation-timeline: scroll(self);
|
||||
}
|
||||
|
||||
@keyframes adjust-masks {
|
||||
from {
|
||||
mask-position:
|
||||
0 calc(0% - var(--fade-height)),
|
||||
0 100%;
|
||||
}
|
||||
|
||||
to {
|
||||
mask-position:
|
||||
0 0,
|
||||
0 calc(100% + var(--fade-height));
|
||||
}
|
||||
}
|
||||
|
||||
.truncate-start {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
Reference in New Issue
Block a user