mirror of
https://github.com/aljazceru/opencode.git
synced 2026-01-08 18:34:59 +01:00
wip: desktop work
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { For, JSXElement, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
import { For, JSXElement, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
||||||
import { Markdown, Part } from "@opencode-ai/ui"
|
import { Part } from "@opencode-ai/ui"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import type { AssistantMessage as AssistantMessageType, Part as PartType, ToolPart } from "@opencode-ai/sdk"
|
import type { AssistantMessage as AssistantMessageType, ToolPart } from "@opencode-ai/sdk"
|
||||||
import { Spinner } from "./spinner"
|
import { Spinner } from "./spinner"
|
||||||
|
|
||||||
export function MessageProgress(props: { assistantMessages: () => AssistantMessageType[]; done?: boolean }) {
|
export function MessageProgress(props: { assistantMessages: () => AssistantMessageType[]; done?: boolean }) {
|
||||||
@@ -22,7 +22,6 @@ export function MessageProgress(props: { assistantMessages: () => AssistantMessa
|
|||||||
p.state.status === "running",
|
p.state.status === "running",
|
||||||
) as ToolPart,
|
) as ToolPart,
|
||||||
)
|
)
|
||||||
|
|
||||||
const resolvedParts = createMemo(() => {
|
const resolvedParts = createMemo(() => {
|
||||||
let resolved = parts()
|
let resolved = parts()
|
||||||
const task = currentTask()
|
const task = currentTask()
|
||||||
@@ -32,20 +31,18 @@ export function MessageProgress(props: { assistantMessages: () => AssistantMessa
|
|||||||
}
|
}
|
||||||
return resolved
|
return resolved
|
||||||
})
|
})
|
||||||
const currentText = createMemo(
|
// const currentText = createMemo(
|
||||||
() =>
|
// () =>
|
||||||
resolvedParts().findLast((p) => p?.type === "text")?.text ||
|
// resolvedParts().findLast((p) => p?.type === "text")?.text ||
|
||||||
resolvedParts().findLast((p) => p?.type === "reasoning")?.text,
|
// resolvedParts().findLast((p) => p?.type === "reasoning")?.text,
|
||||||
)
|
// )
|
||||||
const eligibleItems = createMemo(() => {
|
const eligibleItems = createMemo(() => {
|
||||||
return resolvedParts().filter((p) => p?.type === "tool" && p.state.status === "completed")
|
return resolvedParts().filter((p) => p?.type === "tool" && p.state.status === "completed") as ToolPart[]
|
||||||
})
|
})
|
||||||
const finishedItems = createMemo<(JSXElement | PartType)[]>(() => [
|
const finishedItems = createMemo<(JSXElement | ToolPart)[]>(() => [
|
||||||
|
<div class="h-8 w-full" />,
|
||||||
<div class="h-8 w-full" />,
|
<div class="h-8 w-full" />,
|
||||||
<div class="h-8 w-full" />,
|
<div class="h-8 w-full" />,
|
||||||
<div class="flex items-center gap-x-5 pl-3 text-text-base">
|
|
||||||
<Spinner /> <span class="text-12-medium">Thinking...</span>
|
|
||||||
</div>,
|
|
||||||
...eligibleItems(),
|
...eligibleItems(),
|
||||||
...(done() ? [<div class="h-8 w-full" />, <div class="h-8 w-full" />, <div class="h-8 w-full" />] : []),
|
...(done() ? [<div class="h-8 w-full" />, <div class="h-8 w-full" />, <div class="h-8 w-full" />] : []),
|
||||||
])
|
])
|
||||||
@@ -71,57 +68,120 @@ export function MessageProgress(props: { assistantMessages: () => AssistantMessa
|
|||||||
return `-${(total - 2) * 40 - 8}px`
|
return `-${(total - 2) * 40 - 8}px`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const lastPart = createMemo(() => resolvedParts().slice(-1)?.at(0))
|
||||||
|
const rawStatus = createMemo(() => {
|
||||||
|
const defaultStatus = "Working..."
|
||||||
|
const last = lastPart()
|
||||||
|
if (!last) return defaultStatus
|
||||||
|
|
||||||
|
if (last.type === "tool") {
|
||||||
|
switch (last.tool) {
|
||||||
|
case "task":
|
||||||
|
return "Delegating work..."
|
||||||
|
case "todowrite":
|
||||||
|
case "todoread":
|
||||||
|
return "Planning next steps..."
|
||||||
|
case "read":
|
||||||
|
return "Gathering context..."
|
||||||
|
case "list":
|
||||||
|
case "grep":
|
||||||
|
case "glob":
|
||||||
|
return "Searching the codebase..."
|
||||||
|
case "webfetch":
|
||||||
|
return "Searching the web..."
|
||||||
|
case "edit":
|
||||||
|
case "write":
|
||||||
|
return "Making edits..."
|
||||||
|
case "bash":
|
||||||
|
return "Running commands..."
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if (last.type === "reasoning") {
|
||||||
|
return "Thinking..."
|
||||||
|
} else if (last.type === "text") {
|
||||||
|
return "Gathering thoughts..."
|
||||||
|
}
|
||||||
|
return defaultStatus
|
||||||
|
})
|
||||||
|
|
||||||
|
const [status, setStatus] = createSignal(rawStatus())
|
||||||
|
let lastStatusChange = Date.now()
|
||||||
|
let statusTimeout: number | undefined
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const newStatus = rawStatus()
|
||||||
|
if (newStatus === status()) return
|
||||||
|
|
||||||
|
const timeSinceLastChange = Date.now() - lastStatusChange
|
||||||
|
|
||||||
|
if (timeSinceLastChange >= 1000) {
|
||||||
|
setStatus(newStatus)
|
||||||
|
lastStatusChange = Date.now()
|
||||||
|
if (statusTimeout) {
|
||||||
|
clearTimeout(statusTimeout)
|
||||||
|
statusTimeout = undefined
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (statusTimeout) clearTimeout(statusTimeout)
|
||||||
|
statusTimeout = setTimeout(() => {
|
||||||
|
setStatus(rawStatus())
|
||||||
|
lastStatusChange = Date.now()
|
||||||
|
statusTimeout = undefined
|
||||||
|
}, 1000 - timeSinceLastChange) as unknown as number
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<div
|
{/* <Show when={currentText()}> */}
|
||||||
class="h-30 overflow-hidden pointer-events-none pb-1
|
{/* {(text) => ( */}
|
||||||
|
{/* <div */}
|
||||||
|
{/* class="h-20 flex flex-col justify-end overflow-hidden py-3 */}
|
||||||
|
{/* mask-alpha mask-t-from-80% mask-t-from-background-base mask-t-to-transparent" */}
|
||||||
|
{/* > */}
|
||||||
|
{/* <Markdown text={text()} class="w-full shrink-0 overflow-visible" /> */}
|
||||||
|
{/* </div> */}
|
||||||
|
{/* )} */}
|
||||||
|
{/* </Show> */}
|
||||||
|
<div class="flex items-center gap-x-5 pl-3 border border-transparent text-text-base">
|
||||||
|
<Spinner /> <span class="text-12-medium">{status()}</span>
|
||||||
|
</div>
|
||||||
|
<Show when={eligibleItems().length > 0}>
|
||||||
|
<div
|
||||||
|
class="h-30 overflow-hidden pointer-events-none pb-1
|
||||||
mask-alpha mask-t-from-33% mask-t-from-background-base mask-t-to-transparent
|
mask-alpha mask-t-from-33% mask-t-from-background-base mask-t-to-transparent
|
||||||
mask-b-from-95% mask-b-from-background-base mask-b-to-transparent"
|
mask-b-from-95% mask-b-from-background-base mask-b-to-transparent"
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="w-full flex flex-col items-start self-stretch gap-2 py-8
|
|
||||||
transform transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]"
|
|
||||||
style={{ transform: `translateY(${translateY()})` }}
|
|
||||||
>
|
>
|
||||||
<For each={finishedItems()}>
|
|
||||||
{(part) => {
|
|
||||||
if (part && typeof part === "object" && "type" in part) {
|
|
||||||
const message = createMemo(() => sync.data.message[part.sessionID].find((m) => m.id === part.messageID))
|
|
||||||
return (
|
|
||||||
<div class="h-8 flex items-center w-full">
|
|
||||||
<Switch>
|
|
||||||
<Match when={part.type === "text" && part}>
|
|
||||||
{(p) => (
|
|
||||||
<div
|
|
||||||
textContent={p().text}
|
|
||||||
class="text-12-regular text-text-base whitespace-nowrap truncate w-full"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Match>
|
|
||||||
<Match when={part.type === "reasoning" && part}>
|
|
||||||
{(p) => <Part message={message()!} part={p()} />}
|
|
||||||
</Match>
|
|
||||||
<Match when={part.type === "tool" && part}>
|
|
||||||
{(p) => <Part message={message()!} part={p()} />}
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return <div class="h-8 flex items-center w-full">{part}</div>
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Show when={currentText()}>
|
|
||||||
{(text) => (
|
|
||||||
<div
|
<div
|
||||||
class="max-h-36 flex flex-col justify-end overflow-hidden py-3
|
class="w-full flex flex-col items-start self-stretch gap-2 py-8
|
||||||
mask-alpha mask-t-from-80% mask-t-from-background-base mask-t-to-transparent"
|
transform transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]"
|
||||||
|
style={{ transform: `translateY(${translateY()})` }}
|
||||||
>
|
>
|
||||||
<Markdown text={text()} class="w-full shrink-0 overflow-visible" />
|
<For each={finishedItems()}>
|
||||||
|
{(part) => (
|
||||||
|
<Switch>
|
||||||
|
<Match when={part && typeof part === "object" && "type" in part && part}>
|
||||||
|
{(p) => {
|
||||||
|
const part = p() as ToolPart
|
||||||
|
const message = createMemo(() =>
|
||||||
|
sync.data.message[part.sessionID].find((m) => m.id === part.messageID),
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<div class="h-8 flex items-center w-full">
|
||||||
|
<Part message={message()!} part={part} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<div class="h-8 flex items-center w-full">{part as JSXElement}</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -548,77 +548,6 @@ export default function Page() {
|
|||||||
{(message) => {
|
{(message) => {
|
||||||
const diffs = createMemo(() => message.summary?.diffs ?? [])
|
const diffs = createMemo(() => message.summary?.diffs ?? [])
|
||||||
const working = createMemo(() => !message.summary?.body)
|
const working = createMemo(() => !message.summary?.body)
|
||||||
const assistantMessages = createMemo(() => {
|
|
||||||
return sync.data.message[activeSession().id]?.filter(
|
|
||||||
(m) => m.role === "assistant" && m.parentID == message.id,
|
|
||||||
) as AssistantMessageType[]
|
|
||||||
})
|
|
||||||
const parts = createMemo(() =>
|
|
||||||
assistantMessages().flatMap((m) => sync.data.part[m.id]),
|
|
||||||
)
|
|
||||||
const lastPart = createMemo(() => parts().slice(-1)?.at(0))
|
|
||||||
const rawStatus = createMemo(() => {
|
|
||||||
const defaultStatus = "Working..."
|
|
||||||
const last = lastPart()
|
|
||||||
if (!last) return defaultStatus
|
|
||||||
|
|
||||||
if (last.type === "tool") {
|
|
||||||
switch (last.tool) {
|
|
||||||
case "task":
|
|
||||||
return "Delegating work..."
|
|
||||||
case "todowrite":
|
|
||||||
case "todoread":
|
|
||||||
return "Planning next steps..."
|
|
||||||
case "read":
|
|
||||||
return "Gathering context..."
|
|
||||||
case "list":
|
|
||||||
case "grep":
|
|
||||||
case "glob":
|
|
||||||
return "Searching the codebase..."
|
|
||||||
case "webfetch":
|
|
||||||
return "Searching the web..."
|
|
||||||
case "edit":
|
|
||||||
case "write":
|
|
||||||
return "Making edits..."
|
|
||||||
case "bash":
|
|
||||||
return "Running commands..."
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else if (last.type === "reasoning") {
|
|
||||||
return "Thinking..."
|
|
||||||
} else if (last.type === "text") {
|
|
||||||
return "Gathering thoughts..."
|
|
||||||
}
|
|
||||||
return defaultStatus
|
|
||||||
})
|
|
||||||
|
|
||||||
const [status, setStatus] = createSignal(rawStatus())
|
|
||||||
let lastStatusChange = Date.now()
|
|
||||||
let statusTimeout: number | undefined
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const newStatus = rawStatus()
|
|
||||||
if (newStatus === status()) return
|
|
||||||
|
|
||||||
const timeSinceLastChange = Date.now() - lastStatusChange
|
|
||||||
|
|
||||||
if (timeSinceLastChange >= 1000) {
|
|
||||||
setStatus(newStatus)
|
|
||||||
lastStatusChange = Date.now()
|
|
||||||
if (statusTimeout) {
|
|
||||||
clearTimeout(statusTimeout)
|
|
||||||
statusTimeout = undefined
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (statusTimeout) clearTimeout(statusTimeout)
|
|
||||||
statusTimeout = setTimeout(() => {
|
|
||||||
setStatus(rawStatus())
|
|
||||||
lastStatusChange = Date.now()
|
|
||||||
statusTimeout = undefined
|
|
||||||
}, 1000 - timeSinceLastChange) as unknown as number
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li class="group/li flex items-center self-stretch">
|
<li class="group/li flex items-center self-stretch">
|
||||||
@@ -641,10 +570,9 @@ export default function Page() {
|
|||||||
"text-text-weak data-[active=true]:text-text-strong group-hover/li:text-text-base": true,
|
"text-text-weak data-[active=true]:text-text-strong group-hover/li:text-text-base": true,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Switch>
|
<Show when={message.summary?.title} fallback="New message">
|
||||||
<Match when={working()}>{status()}</Match>
|
{message.summary?.title}
|
||||||
<Match when={true}>{message.summary?.title}</Match>
|
</Show>
|
||||||
</Switch>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
Reference in New Issue
Block a user