summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-11-03 14:44:25 -0600
committerAdam <[email protected]>2025-11-03 15:42:10 -0600
commit3d43214075b1f4ba0a3c71d5fea799532bbb7bd9 (patch)
tree46e6bc7cf9535f638eb2fa27fd5edcaedf0dc579 /packages/desktop/src
parent178a14ce3ed622b2470079ed317490b796ba64fe (diff)
downloadopencode-3d43214075b1f4ba0a3c71d5fea799532bbb7bd9.tar.gz
opencode-3d43214075b1f4ba0a3c71d5fea799532bbb7bd9.zip
wip: desktop work
Diffstat (limited to 'packages/desktop/src')
-rw-r--r--packages/desktop/src/components/file-tree.tsx4
-rw-r--r--packages/desktop/src/context/local.tsx278
-rw-r--r--packages/desktop/src/pages/index.tsx663
3 files changed, 433 insertions, 512 deletions
diff --git a/packages/desktop/src/components/file-tree.tsx b/packages/desktop/src/components/file-tree.tsx
index d10328136..a5d19f51e 100644
--- a/packages/desktop/src/components/file-tree.tsx
+++ b/packages/desktop/src/components/file-tree.tsx
@@ -19,7 +19,7 @@ export default function FileTree(props: {
component={p.as ?? "div"}
classList={{
"p-0.5 w-full flex items-center gap-x-2 hover:bg-background-element": true,
- "bg-background-element": local.file.active()?.path === p.node.path,
+ // "bg-background-element": local.file.active()?.path === p.node.path,
[props.nodeClass ?? ""]: !!props.nodeClass,
}}
style={`padding-left: ${level * 10}px`}
@@ -55,7 +55,7 @@ export default function FileTree(props: {
"text-xs whitespace-nowrap truncate": true,
"text-text-muted/40": p.node.ignored,
"text-text-muted/80": !p.node.ignored,
- "!text-text": local.file.active()?.path === p.node.path,
+ // "!text-text": local.file.active()?.path === p.node.path,
"!text-primary": local.file.changed(p.node.path),
}}
>
diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx
index 2b844536d..283dddc2c 100644
--- a/packages/desktop/src/context/local.tsx
+++ b/packages/desktop/src/context/local.tsx
@@ -1,16 +1,7 @@
import { createStore, produce, reconcile } from "solid-js/store"
import { batch, createEffect, createMemo } from "solid-js"
-import { pipe, sumBy, uniqueBy } from "remeda"
-import type {
- FileContent,
- FileNode,
- Model,
- Provider,
- File as FileStatus,
- Part,
- Message,
- AssistantMessage,
-} from "@opencode-ai/sdk"
+import { uniqueBy } from "remeda"
+import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk"
import { createSimpleContext } from "./helper"
import { useSDK } from "./sdk"
import { useSync } from "./sync"
@@ -204,18 +195,18 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const file = (() => {
const [store, setStore] = createStore<{
node: Record<string, LocalFile>
- opened: string[]
- active?: string
+ // opened: string[]
+ // active?: string
}>({
node: Object.fromEntries(sync.data.node.map((x) => [x.path, x])),
- opened: [],
+ // opened: [],
})
- const active = createMemo(() => {
- if (!store.active) return undefined
- return store.node[store.active]
- })
- const opened = createMemo(() => store.opened.map((x) => store.node[x]))
+ // const active = createMemo(() => {
+ // if (!store.active) return undefined
+ // return store.node[store.active]
+ // })
+ // const opened = createMemo(() => store.opened.map((x) => store.node[x]))
const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path)))
const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b)))
@@ -303,16 +294,16 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const open = async (path: string, options?: { pinned?: boolean; view?: LocalFile["view"] }) => {
const relativePath = relative(path)
if (!store.node[relativePath]) await fetch(path)
- setStore("opened", (x) => {
- if (x.includes(relativePath)) return x
- return [
- ...opened()
- .filter((x) => x.pinned)
- .map((x) => x.path),
- relativePath,
- ]
- })
- setStore("active", relativePath)
+ // setStore("opened", (x) => {
+ // if (x.includes(relativePath)) return x
+ // return [
+ // ...opened()
+ // .filter((x) => x.pinned)
+ // .map((x) => x.path),
+ // relativePath,
+ // ]
+ // })
+ // setStore("active", relativePath)
context.addActive()
if (options?.pinned) setStore("node", path, "pinned", true)
if (options?.view && store.node[relativePath].view === undefined) setStore("node", path, "view", options.view)
@@ -363,22 +354,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})
return {
- active,
- opened,
node: (path: string) => store.node[path],
update: (path: string, node: LocalFile) => setStore("node", path, reconcile(node)),
open,
load,
init,
- close(path: string) {
- setStore("opened", (opened) => opened.filter((x) => x !== path))
- if (store.active === path) {
- const index = store.opened.findIndex((f) => f === path)
- const previous = store.opened[Math.max(0, index - 1)]
- setStore("active", previous)
- }
- resetNode(path)
- },
expand(path: string) {
setStore("node", path, "expanded", true)
if (store.node[path].loaded) return
@@ -394,17 +374,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
scroll(path: string, scrollTop: number) {
setStore("node", path, "scrollTop", scrollTop)
},
- move(path: string, to: number) {
- const index = store.opened.findIndex((f) => f === path)
- if (index === -1) return
- setStore(
- "opened",
- produce((opened) => {
- opened.splice(to, 0, opened.splice(index, 1)[0])
- }),
- )
- setStore("node", path, "pinned", true)
- },
view(path: string): View {
const n = store.node[path]
return n && n.view ? n.view : "raw"
@@ -444,14 +413,48 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
},
search,
relative,
+ // active,
+ // opened,
+ // close(path: string) {
+ // setStore("opened", (opened) => opened.filter((x) => x !== path))
+ // if (store.active === path) {
+ // const index = store.opened.findIndex((f) => f === path)
+ // const previous = store.opened[Math.max(0, index - 1)]
+ // setStore("active", previous)
+ // }
+ // resetNode(path)
+ // },
+ // move(path: string, to: number) {
+ // const index = store.opened.findIndex((f) => f === path)
+ // if (index === -1) return
+ // setStore(
+ // "opened",
+ // produce((opened) => {
+ // opened.splice(to, 0, opened.splice(index, 1)[0])
+ // }),
+ // )
+ // setStore("node", path, "pinned", true)
+ // },
}
})()
const session = (() => {
const [store, setStore] = createStore<{
active?: string
- activeMessage?: string
- }>({})
+ tabs: Record<
+ string,
+ {
+ active?: string
+ opened: string[]
+ }
+ >
+ }>({
+ tabs: {
+ "": {
+ opened: [],
+ },
+ },
+ })
const active = createMemo(() => {
if (!store.active) return undefined
@@ -461,134 +464,69 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
createEffect(() => {
if (!store.active) return
sync.session.sync(store.active)
- })
- const valid = (part: Part) => {
- if (!part) return false
- switch (part.type) {
- case "step-start":
- case "step-finish":
- case "file":
- case "patch":
- return false
- case "text":
- return !part.synthetic && part.text.trim()
- case "reasoning":
- return part.text.trim()
- case "tool":
- switch (part.tool) {
- case "todoread":
- case "todowrite":
- case "list":
- case "grep":
- return false
- }
- return true
- default:
- return true
+ if (!store.tabs[store.active]) {
+ setStore("tabs", store.active, {
+ opened: [],
+ })
}
- }
-
- const hasValidParts = (message: Message) => {
- return sync.data.part[message.id]?.filter(valid).length > 0
- }
- // const hasTextPart = (message: Message) => {
- // return !!sync.data.part[message.id]?.filter(valid).find((p) => p.type === "text")
- // }
-
- const messages = createMemo(() => (store.active ? (sync.data.message[store.active] ?? []) : []))
- const messagesWithValidParts = createMemo(() => messages().filter(hasValidParts) ?? [])
- const userMessages = createMemo(() =>
- messages()
- .filter((m) => m.role === "user")
- .sort((a, b) => b.id.localeCompare(a.id)),
- )
-
- const cost = createMemo(() => {
- const total = pipe(
- messages(),
- sumBy((x) => (x.role === "assistant" ? x.cost : 0)),
- )
- return new Intl.NumberFormat("en-US", {
- style: "currency",
- currency: "USD",
- }).format(total)
- })
-
- const last = createMemo(() => {
- return messages().findLast((x) => x.role === "assistant") as AssistantMessage
- })
-
- const lastUserMessage = createMemo(() => {
- return userMessages()?.at(0)
- })
-
- const activeMessage = createMemo(() => {
- if (!store.active || !store.activeMessage) return lastUserMessage()
- return sync.data.message[store.active]?.find((m) => m.id === store.activeMessage)
- })
-
- const model = createMemo(() => {
- if (!last()) return
- const model = sync.data.provider.find((x) => x.id === last().providerID)?.models[last().modelID]
- return model
})
- const tokens = createMemo(() => {
- if (!last()) return
- const tokens = last().tokens
- const total = tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write
- return new Intl.NumberFormat("en-US", {
- notation: "compact",
- compactDisplay: "short",
- }).format(total)
- })
-
- const context = createMemo(() => {
- if (!last()) return
- if (!model()?.limit.context) return 0
- const tokens = last().tokens
- const total = tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write
- return Math.round((total / model()!.limit.context) * 100)
- })
-
- const getMessageText = (message: Message | Message[] | undefined): string => {
- if (!message) return ""
- if (Array.isArray(message)) return message.map((m) => getMessageText(m)).join(" ")
- return sync.data.part[message.id]
- ?.filter((p) => p.type === "text")
- ?.filter((p) => !p.synthetic)
- .map((p) => p.text)
- .join(" ")
- }
+ const tabs = createMemo(() => store.tabs[store.active ?? ""])
return {
active,
- activeMessage,
- lastUserMessage,
- cost,
- last,
- model,
- tokens,
- context,
- messages,
- messagesWithValidParts,
- userMessages,
- // working,
- getMessageText,
setActive(sessionId: string | undefined) {
setStore("active", sessionId)
- setStore("activeMessage", undefined)
},
clearActive() {
setStore("active", undefined)
- setStore("activeMessage", undefined)
},
- setActiveMessage(messageId: string | undefined) {
- setStore("activeMessage", messageId)
+ tabs,
+ copyTabs(from: string, to: string) {
+ setStore("tabs", to, {
+ opened: store.tabs[from]?.opened ?? [],
+ })
+ },
+ setActiveTab(tab: string | undefined) {
+ setStore("tabs", store.active ?? "", "active", tab)
+ },
+ async open(tab: string) {
+ if (tab !== "chat") {
+ await file.open(tab)
+ }
+ if (!tabs()?.opened?.includes(tab)) {
+ setStore("tabs", store.active ?? "", "opened", [...(tabs()?.opened ?? []), tab])
+ }
+ setStore("tabs", store.active ?? "", "active", tab)
},
- clearActiveMessage() {
- setStore("activeMessage", undefined)
+ close(tab: string) {
+ batch(() => {
+ if (!tabs()) return
+ setStore("tabs", store.active ?? "", {
+ active: tabs()!.active,
+ opened: tabs()!.opened.filter((x) => x !== tab),
+ })
+ if (tabs()!.active === tab) {
+ const index = tabs()!.opened.findIndex((f) => f === tab)
+ const previous = tabs()!.opened[Math.max(0, index - 1)]
+ setStore("tabs", store.active ?? "", "active", previous)
+ }
+ })
+ },
+ move(tab: string, to: number) {
+ if (!tabs()) return
+ const index = tabs()!.opened.findIndex((f) => f === tab)
+ if (index === -1) return
+ setStore(
+ "tabs",
+ store.active ?? "",
+ "opened",
+ produce((opened) => {
+ opened.splice(to, 0, opened.splice(index, 1)[0])
+ }),
+ )
+ // setStore("node", path, "pinned", true)
},
}
})()
@@ -611,9 +549,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
all() {
return store.items
},
- active() {
- return store.activeTab ? file.active() : undefined
- },
+ // active() {
+ // return store.activeTab ? file.active() : undefined
+ // },
addActive() {
setStore("activeTab", true)
},
diff --git a/packages/desktop/src/pages/index.tsx b/packages/desktop/src/pages/index.tsx
index dc3436720..7bba17cba 100644
--- a/packages/desktop/src/pages/index.tsx
+++ b/packages/desktop/src/pages/index.tsx
@@ -10,10 +10,10 @@ import {
Diff,
Collapsible,
DiffChanges,
- ProgressCircle,
Message,
Typewriter,
Card,
+ Code,
} from "@opencode-ai/ui"
import { FileIcon } from "@/ui"
import FileTree from "@/components/file-tree"
@@ -35,7 +35,6 @@ import {
} from "@thisbeyond/solid-dnd"
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
import type { JSX } from "solid-js"
-import { Code } from "@/components/code"
import { useSync } from "@/context/sync"
import { useSDK } from "@/context/sdk"
import { type AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk"
@@ -54,14 +53,6 @@ export default function Page() {
let messageScrollElement!: HTMLDivElement
const [activeItem, setActiveItem] = createSignal<string | undefined>(undefined)
- createEffect(() => {
- // Set first message as active if none selected
- const userMessages = local.session.userMessages()
- if (userMessages.length > 0 && !local.session.activeMessage()) {
- local.session.setActiveMessage(userMessages[0].id)
- }
- })
-
const MOD = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) ? "Meta" : "Control"
onMount(() => {
@@ -91,26 +82,26 @@ export default function Page() {
return
}
- if (local.file.active()) {
- const active = local.file.active()!
- if (event.key === "Enter" && active.selection) {
- local.context.add({
- type: "file",
- path: active.path,
- selection: { ...active.selection },
- })
- return
- }
-
- if (event.getModifierState(MOD)) {
- if (event.key.toLowerCase() === "a") {
- return
- }
- if (event.key.toLowerCase() === "c") {
- return
- }
- }
- }
+ // if (local.file.active()) {
+ // const active = local.file.active()!
+ // if (event.key === "Enter" && active.selection) {
+ // local.context.add({
+ // type: "file",
+ // path: active.path,
+ // selection: { ...active.selection },
+ // })
+ // return
+ // }
+ //
+ // if (event.getModifierState(MOD)) {
+ // if (event.key.toLowerCase() === "a") {
+ // return
+ // }
+ // if (event.key.toLowerCase() === "c") {
+ // return
+ // }
+ // }
+ // }
if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) {
inputRef?.focus()
@@ -140,21 +131,22 @@ export default function Page() {
}
}
- const navigateChange = (dir: 1 | -1) => {
- const active = local.file.active()
- if (!active) return
- const current = local.file.changeIndex(active.path)
- const next = current === undefined ? (dir === 1 ? 0 : -1) : current + dir
- local.file.setChangeIndex(active.path, next)
- }
+ // const navigateChange = (dir: 1 | -1) => {
+ // const active = local.file.active()
+ // if (!active) return
+ // const current = local.file.changeIndex(active.path)
+ // const next = current === undefined ? (dir === 1 ? 0 : -1) : current + dir
+ // local.file.setChangeIndex(active.path, next)
+ // }
const handleTabChange = (path: string) => {
- if (path === "chat" || path === "review") return
- local.file.open(path)
+ local.session.setActiveTab(path)
+ if (path === "chat") return
+ local.session.open(path)
}
const handleTabClose = (file: LocalFile) => {
- local.file.close(file.path)
+ local.session.close(file.path)
}
const handleDragStart = (event: unknown) => {
@@ -166,11 +158,11 @@ export default function Page() {
const handleDragOver = (event: DragEvent) => {
const { draggable, droppable } = event
if (draggable && droppable) {
- const currentFiles = local.file.opened().map((file) => file.path)
- const fromIndex = currentFiles.indexOf(draggable.id.toString())
- const toIndex = currentFiles.indexOf(droppable.id.toString())
- if (fromIndex !== toIndex) {
- local.file.move(draggable.id.toString(), toIndex)
+ const currentFiles = local.session.tabs()?.opened.map((file) => file)
+ const fromIndex = currentFiles?.indexOf(draggable.id.toString())
+ const toIndex = currentFiles?.indexOf(droppable.id.toString())
+ if (fromIndex !== toIndex && toIndex !== undefined) {
+ local.session.move(draggable.id.toString(), toIndex)
}
}
}
@@ -179,20 +171,20 @@ export default function Page() {
setActiveItem(undefined)
}
- const scrollDiffItem = (element: HTMLElement) => {
- element.scrollIntoView({ block: "start", behavior: "instant" })
- }
+ // const scrollDiffItem = (element: HTMLElement) => {
+ // element.scrollIntoView({ block: "start", behavior: "instant" })
+ // }
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
- const item = target.closest('[data-slot="accordion-item"]') as HTMLElement | null
- if (!item) return
- scrollDiffItem(item)
- })
+ // const target = event.currentTarget as HTMLElement
+ // queueMicrotask(() => {
+ // if (target.getAttribute("aria-expanded") !== "true") return
+ // const item = target.closest('[data-slot="accordion-item"]') as HTMLElement | null
+ // if (!item) return
+ // scrollDiffItem(item)
+ // })
}
const handlePromptSubmit = async (parts: ContentPart[]) => {
@@ -205,6 +197,10 @@ export default function Page() {
if (!session) return
local.session.setActive(session.id)
+ if (!existingSession) {
+ local.session.copyTabs("", session.id)
+ }
+ local.session.setActiveTab(undefined)
const toAbsolutePath = (path: string) => (path.startsWith("/") ? path : sync.absolute(path))
const text = parts.map((part) => part.content).join("")
@@ -427,20 +423,26 @@ export default function Page() {
>
<DragDropSensors />
<ConstrainDragYAxis />
- <Tabs onChange={handleTabChange}>
+ <Tabs value={local.session.tabs()?.active ?? "chat"} onChange={handleTabChange}>
<div class="sticky top-0 shrink-0 flex">
<Tabs.List>
<Tabs.Trigger value="chat" class="flex gap-x-4 items-center">
<div>Chat</div>
- <Tooltip value={`${local.session.tokens() ?? 0} Tokens`} class="flex items-center gap-1.5">
- <ProgressCircle percentage={local.session.context() ?? 0} />
- <div class="text-14-regular text-text-weak text-right">{local.session.context() ?? 0}%</div>
- </Tooltip>
+ {/* <Tooltip value={`${local.session.tokens() ?? 0} Tokens`} class="flex items-center gap-1.5"> */}
+ {/* <ProgressCircle percentage={local.session.context() ?? 0} /> */}
+ {/* <div class="text-14-regular text-text-weak text-right">{local.session.context() ?? 0}%</div> */}
+ {/* </Tooltip> */}
</Tabs.Trigger>
{/* <Tabs.Trigger value="review">Review</Tabs.Trigger> */}
- <SortableProvider ids={local.file.opened().map((file) => file.path)}>
- <For each={local.file.opened()}>
- {(file) => <SortableTab file={file} onTabClick={handleFileClick} onTabClose={handleTabClose} />}
+ <SortableProvider ids={local.session.tabs()?.opened ?? []}>
+ <For each={local.session.tabs()?.opened ?? []}>
+ {(file) => (
+ <SortableTab
+ file={local.file.node(file)}
+ onTabClick={handleFileClick}
+ onTabClose={handleTabClose}
+ />
+ )}
</For>
</SortableProvider>
<div class="bg-background-base h-full flex items-center justify-center border-b border-border-weak-base px-3">
@@ -452,64 +454,6 @@ export default function Page() {
/>
</div>
</Tabs.List>
- <div class="hidden shrink-0 h-full _flex items-center gap-1 px-2 border-b border-border-subtle/40">
- <Show when={local.file.active() && local.file.active()!.content?.diff}>
- {(() => {
- const activeFile = local.file.active()!
- const view = local.file.view(activeFile.path)
- return (
- <div class="flex items-center gap-1">
- <Show when={view !== "raw"}>
- <div class="mr-1 flex items-center gap-1">
- <Tooltip value="Previous change" placement="bottom">
- <IconButton icon="arrow-up" variant="ghost" onClick={() => navigateChange(-1)} />
- </Tooltip>
- <Tooltip value="Next change" placement="bottom">
- <IconButton icon="arrow-down" variant="ghost" onClick={() => navigateChange(1)} />
- </Tooltip>
- </div>
- </Show>
- <Tooltip value="Raw" placement="bottom">
- <IconButton
- icon="file-text"
- variant="ghost"
- classList={{
- "text-text": view === "raw",
- "text-text-muted/70": view !== "raw",
- "bg-background-element": view === "raw",
- }}
- onClick={() => local.file.setView(activeFile.path, "raw")}
- />
- </Tooltip>
- <Tooltip value="Unified diff" placement="bottom">
- <IconButton
- icon="checklist"
- variant="ghost"
- classList={{
- "text-text": view === "diff-unified",
- "text-text-muted/70": view !== "diff-unified",
- "bg-background-element": view === "diff-unified",
- }}
- onClick={() => local.file.setView(activeFile.path, "diff-unified")}
- />
- </Tooltip>
- <Tooltip value="Split diff" placement="bottom">
- <IconButton
- icon="columns"
- variant="ghost"
- classList={{
- "text-text": view === "diff-split",
- "text-text-muted/70": view !== "diff-split",
- "bg-background-element": view === "diff-split",
- }}
- onClick={() => local.file.setView(activeFile.path, "diff-split")}
- />
- </Tooltip>
- </div>
- )
- })()}
- </Show>
- </div>
</div>
<Tabs.Content value="chat" class="@container select-text flex flex-col flex-1 min-h-0 overflow-y-hidden">
<div class="relative px-6 pt-12 max-w-2xl w-full mx-auto flex flex-col flex-1 min-h-0">
@@ -537,250 +481,289 @@ export default function Page() {
</div>
}
>
- {(activeSession) => (
- <div class="pt-3 flex flex-col flex-1 min-h-0">
- <div class="flex-1 min-h-0">
- <Show when={local.session.userMessages().length > 1}>
- <ul
- role="list"
- class="absolute right-full mr-8 hidden w-60 shrink-0 @7xl:flex flex-col items-start gap-1"
- >
- <For each={local.session.userMessages()}>
+ {(session) => {
+ const [store, setStore] = createStore<{
+ messageId?: string
+ }>()
+
+ const messages = createMemo(() => sync.data.message[session().id] ?? [])
+ const userMessages = createMemo(() =>
+ messages()
+ .filter((m) => m.role === "user")
+ .sort((a, b) => b.id.localeCompare(a.id)),
+ )
+ const lastUserMessage = createMemo(() => {
+ return userMessages()?.at(0)
+ })
+ const activeMessage = createMemo(() => {
+ if (!store.messageId) return lastUserMessage()
+ return userMessages()?.find((m) => m.id === store.messageId)
+ })
+
+ return (
+ <div class="pt-3 flex flex-col flex-1 min-h-0">
+ <div class="flex-1 min-h-0">
+ <Show when={userMessages().length > 1}>
+ <ul
+ role="list"
+ class="absolute right-full mr-8 hidden w-60 shrink-0 @7xl:flex flex-col items-start gap-1"
+ >
+ <For each={userMessages()}>
+ {(message) => {
+ const diffs = createMemo(() => message.summary?.diffs ?? [])
+ const assistantMessages = createMemo(() => {
+ return sync.data.message[session().id]?.filter(
+ (m) => m.role === "assistant" && m.parentID == message.id,
+ ) as AssistantMessageType[]
+ })
+ const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
+ const working = createMemo(() => !message.summary?.body && !error())
+
+ return (
+ <li class="group/li flex items-center self-stretch">
+ <button
+ class="flex items-center self-stretch w-full gap-x-2 py-1 cursor-default"
+ onClick={() => setStore("messageId", message.id)}
+ >
+ <Switch>
+ <Match when={working()}>
+ <Spinner class="text-text-base shrink-0 w-[18px] aspect-square" />
+ </Match>
+ <Match when={true}>
+ <DiffChanges diff={diffs()} variant="bars" />
+ </Match>
+ </Switch>
+ <div
+ data-active={activeMessage()?.id === message.id}
+ classList={{
+ "text-14-regular text-text-weak whitespace-nowrap truncate min-w-0": true,
+ "text-text-weak data-[active=true]:text-text-strong group-hover/li:text-text-base": true,
+ }}
+ >
+ <Show when={message.summary?.title} fallback="New message">
+ {message.summary?.title}
+ </Show>
+ </div>
+ </button>
+ </li>
+ )
+ }}
+ </For>
+ </ul>
+ </Show>
+ <div ref={messageScrollElement} class="grow min-w-0 h-full overflow-y-auto no-scrollbar">
+ <For each={userMessages()}>
{(message) => {
- const diffs = createMemo(() => message.summary?.diffs ?? [])
+ const isActive = createMemo(() => activeMessage()?.id === message.id)
+ const [titled, setTitled] = createSignal(!!message.summary?.title)
const assistantMessages = createMemo(() => {
- return sync.data.message[activeSession().id]?.filter(
+ return sync.data.message[session().id]?.filter(
(m) => m.role === "assistant" && m.parentID == message.id,
) as AssistantMessageType[]
})
const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
- const working = createMemo(() => !message.summary?.body && !error())
+ const [completed, setCompleted] = createSignal(!!message.summary?.body || !!error())
+ const [expanded, setExpanded] = createSignal(false)
+ const parts = createMemo(() => sync.data.part[message.id])
+ const title = createMemo(() => message.summary?.title)
+ const summary = createMemo(() => message.summary?.body)
+ const diffs = createMemo(() => message.summary?.diffs ?? [])
+ const hasToolPart = createMemo(() =>
+ assistantMessages()
+ ?.flatMap((m) => sync.data.part[m.id])
+ .some((p) => p?.type === "tool"),
+ )
+ const working = createMemo(() => !summary() && !error())
+
+ // allowing time for the animations to finish
+ createEffect(() => {
+ title()
+ setTimeout(() => setTitled(!!title()), 10_000)
+ })
+ createEffect(() => {
+ const complete = !!summary() || !!error()
+ setTimeout(() => setCompleted(complete), 1200)
+ })
return (
- <li class="group/li flex items-center self-stretch">
- <button
- class="flex items-center self-stretch w-full gap-x-2 py-1 cursor-default"
- onClick={() => local.session.setActiveMessage(message.id)}
+ <Show when={isActive()}>
+ <div
+ data-message={message.id}
+ class="flex flex-col items-start self-stretch gap-8 pb-50"
>
- <Switch>
- <Match when={working()}>
- <Spinner class="text-text-base shrink-0 w-[18px] aspect-square" />
- </Match>
- <Match when={true}>
- <DiffChanges diff={diffs()} variant="bars" />
- </Match>
- </Switch>
- <div
- data-active={local.session.activeMessage()?.id === message.id}
- classList={{
- "text-14-regular text-text-weak whitespace-nowrap truncate min-w-0": true,
- "text-text-weak data-[active=true]:text-text-strong group-hover/li:text-text-base": true,
- }}
- >
- <Show when={message.summary?.title} fallback="New message">
- {message.summary?.title}
- </Show>
- </div>
- </button>
- </li>
- )
- }}
- </For>
- </ul>
- </Show>
- <div ref={messageScrollElement} class="grow min-w-0 h-full overflow-y-auto no-scrollbar">
- <For each={local.session.userMessages()}>
- {(message) => {
- const isActive = createMemo(() => local.session.activeMessage()?.id === message.id)
- const [titled, setTitled] = createSignal(!!message.summary?.title)
- const assistantMessages = createMemo(() => {
- return sync.data.message[activeSession().id]?.filter(
- (m) => m.role === "assistant" && m.parentID == message.id,
- ) as AssistantMessageType[]
- })
- const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
- const [completed, setCompleted] = createSignal(!!message.summary?.body || !!error())
- const [expanded, setExpanded] = createSignal(false)
- const parts = createMemo(() => sync.data.part[message.id])
- const title = createMemo(() => message.summary?.title)
- const summary = createMemo(() => message.summary?.body)
- const diffs = createMemo(() => message.summary?.diffs ?? [])
- const hasToolPart = createMemo(() =>
- assistantMessages()
- ?.flatMap((m) => sync.data.part[m.id])
- .some((p) => p?.type === "tool"),
- )
- const working = createMemo(() => !summary() && !error())
-
- // allowing time for the animations to finish
- createEffect(() => {
- title()
- setTimeout(() => setTitled(!!title()), 10_000)
- })
- createEffect(() => {
- const complete = !!summary() || !!error()
- setTimeout(() => setCompleted(complete), 1200)
- })
-
- return (
- <Show when={isActive()}>
- <div
- data-message={message.id}
- class="flex flex-col items-start self-stretch gap-8 pb-50"
- >
- {/* Title */}
- <div class="py-2 flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger z-10">
- <div class="w-full text-14-medium text-text-strong">
- <Show
- when={titled()}
- fallback={
- <Typewriter
- as="h1"
- text={title()}
- class="overflow-hidden text-ellipsis min-w-0 text-nowrap"
- />
- }
- >
- <h1 class="overflow-hidden text-ellipsis min-w-0 text-nowrap">{title()}</h1>
- </Show>
- </div>
- </div>
- <div class="-mt-8">
- <Message message={message} parts={parts()} />
- </div>
- {/* Summary */}
- <Show when={completed()}>
- <div class="w-full flex flex-col gap-6 items-start self-stretch">
- <div class="flex flex-col items-start gap-1 self-stretch">
- <h2 class="text-12-medium text-text-weak">
- <Switch>
- <Match when={diffs().length}>Summary</Match>
- <Match when={true}>Response</Match>
- </Switch>
- </h2>
- <Show when={summary()}>
- {(summary) => (
- <Markdown
- classList={{ "[&>*]:fade-up-text": !diffs().length }}
- text={summary()}
+ {/* Title */}
+ <div class="py-2 flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger z-10">
+ <div class="w-full text-14-medium text-text-strong">
+ <Show
+ when={titled()}
+ fallback={
+ <Typewriter
+ as="h1"
+ text={title()}
+ class="overflow-hidden text-ellipsis min-w-0 text-nowrap"
/>
- )}
+ }
+ >
+ <h1 class="overflow-hidden text-ellipsis min-w-0 text-nowrap">
+ {title()}
+ </h1>
</Show>
</div>
- <Accordion class="w-full" multiple>
- <For each={diffs()}>
- {(diff) => (
- <Accordion.Item value={diff.file}>
- <Accordion.Header>
- <Accordion.Trigger onClick={handleDiffTriggerClick}>
- <div class="flex items-center justify-between w-full gap-5">
- <div class="grow flex items-center gap-5 min-w-0">
- <FileIcon
- node={{ path: diff.file, type: "file" }}
- class="shrink-0 size-4"
- />
- <div class="flex grow min-w-0">
- <Show when={diff.file.includes("/")}>
- <span class="text-text-base truncate-start">
- {getDirectory(diff.file)}&lrm;
+ </div>
+ <div class="-mt-8">
+ <Message message={message} parts={parts()} />
+ </div>
+ {/* Summary */}
+ <Show when={completed()}>
+ <div class="w-full flex flex-col gap-6 items-start self-stretch">
+ <div class="flex flex-col items-start gap-1 self-stretch">
+ <h2 class="text-12-medium text-text-weak">
+ <Switch>
+ <Match when={diffs().length}>Summary</Match>
+ <Match when={true}>Response</Match>
+ </Switch>
+ </h2>
+ <Show when={summary()}>
+ {(summary) => (
+ <Markdown
+ classList={{ "[&>*]:fade-up-text": !diffs().length }}
+ text={summary()}
+ />
+ )}
+ </Show>
+ </div>
+ <Accordion class="w-full" multiple>
+ <For each={diffs()}>
+ {(diff) => (
+ <Accordion.Item value={diff.file}>
+ <Accordion.Header>
+ <Accordion.Trigger onClick={handleDiffTriggerClick}>
+ <div class="flex items-center justify-between w-full gap-5">
+ <div class="grow flex items-center gap-5 min-w-0">
+ <FileIcon
+ node={{ path: diff.file, type: "file" }}
+ class="shrink-0 size-4"
+ />
+ <div class="flex grow min-w-0">
+ <Show when={diff.file.includes("/")}>
+ <span class="text-text-base truncate-start">
+ {getDirectory(diff.file)}&lrm;
+ </span>
+ </Show>
+ <span class="text-text-strong shrink-0">
+ {getFilename(diff.file)}
</span>
- </Show>
- <span class="text-text-strong shrink-0">
- {getFilename(diff.file)}
- </span>
+ </div>
+ </div>
+ <div class="shrink-0 flex gap-4 items-center justify-end">
+ <DiffChanges diff={diff} />
+ <Icon name="chevron-grabber-vertical" size="small" />
</div>
</div>
- <div class="shrink-0 flex gap-4 items-center justify-end">
- <DiffChanges diff={diff} />
- <Icon name="chevron-grabber-vertical" size="small" />
- </div>
- </div>
- </Accordion.Trigger>
- </Accordion.Header>
- <Accordion.Content>
- <Diff
- before={{
- name: diff.file!,
- contents: diff.before!,
- }}
- after={{
- name: diff.file!,
- contents: diff.after!,
+ </Accordion.Trigger>
+ </Accordion.Header>
+ <Accordion.Content>
+ <Diff
+ before={{
+ name: diff.file!,
+ contents: diff.before!,
+ }}
+ after={{
+ name: diff.file!,
+ contents: diff.after!,
+ }}
+ />
+ </Accordion.Content>
+ </Accordion.Item>
+ )}
+ </For>
+ </Accordion>
+ </div>
+ </Show>
+ <Show when={error() && !expanded()}>
+ <Card variant="error" class="text-text-on-critical-base">
+ {error()?.data?.message as string}
+ </Card>
+ </Show>
+ {/* Response */}
+ <div class="w-full">
+ <Switch>
+ <Match when={!completed()}>
+ <MessageProgress
+ assistantMessages={assistantMessages}
+ done={!working()}
+ />
+ </Match>
+ <Match when={completed() && hasToolPart()}>
+ <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">
+ <div class="text-12-medium">
+ <Switch>
+ <Match when={expanded()}>Hide details</Match>
+ <Match when={!expanded()}>Show details</Match>
+ </Switch>
+ </div>
+ <Collapsible.Arrow />
+ </div>
+ </Collapsible.Trigger>
+ <Collapsible.Content>
+ <div class="w-full flex flex-col items-start self-stretch gap-3">
+ <For each={assistantMessages()}>
+ {(assistantMessage) => {
+ const parts = createMemo(
+ () => sync.data.part[assistantMessage.id],
+ )
+ return <Message message={assistantMessage} parts={parts()} />
}}
- />
- </Accordion.Content>
- </Accordion.Item>
- )}
- </For>
- </Accordion>
- </div>
- </Show>
- <Show when={error() && !expanded()}>
- <Card variant="error" class="text-text-on-critical-base">
- {error()?.data?.message as string}
- </Card>
- </Show>
- {/* Response */}
- <div class="w-full">
- <Switch>
- <Match when={!completed()}>
- <MessageProgress assistantMessages={assistantMessages} done={!working()} />
- </Match>
- <Match when={completed() && hasToolPart()}>
- <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">
- <div class="text-12-medium">
- <Switch>
- <Match when={expanded()}>Hide details</Match>
- <Match when={!expanded()}>Show details</Match>
- </Switch>
+ </For>
+ <Show when={error()}>
+ <Card variant="error" class="text-text-on-critical-base">
+ {error()?.data?.message as string}
+ </Card>
+ </Show>
</div>
- <Collapsible.Arrow />
- </div>
- </Collapsible.Trigger>
- <Collapsible.Content>
- <div class="w-full flex flex-col items-start self-stretch gap-3">
- <For each={assistantMessages()}>
- {(assistantMessage) => {
- const parts = createMemo(
- () => sync.data.part[assistantMessage.id],
- )
- return <Message message={assistantMessage} parts={parts()} />
- }}
- </For>
- <Show when={error()}>
- <Card variant="error" class="text-text-on-critical-base">
- {error()?.data?.message as string}
- </Card>
- </Show>
- </div>
- </Collapsible.Content>
- </Collapsible>
- </Match>
- </Switch>
+ </Collapsible.Content>
+ </Collapsible>
+ </Match>
+ </Switch>
+ </div>
</div>
- </div>
- </Show>
- )
- }}
- </For>
+ </Show>
+ )
+ }}
+ </For>
+ </div>
</div>
</div>
- </div>
- )}
+ )
+ }}
</Show>
</div>
</Tabs.Content>
{/* <Tabs.Content value="review" class="select-text"></Tabs.Content> */}
- <For each={local.file.opened()}>
+ <For each={local.session.tabs()?.opened}>
{(file) => (
- <Tabs.Content value={file.path} class="select-text">
+ <Tabs.Content value={file} class="select-text">
{(() => {
- const view = local.file.view(file.path)
- const showRaw = view === "raw" || !file.content?.diff
- const code = showRaw ? (file.content?.content ?? "") : (file.content?.diff ?? "")
- return <Code path={file.path} code={code} class="[&_code]:pb-60" />
+ {
+ /* const view = local.file.view(file) */
+ }
+ {
+ /* const showRaw = view === "raw" || !file.content?.diff */
+ }
+ {
+ /* const code = showRaw ? (file.content?.content ?? "") : (file.content?.diff ?? "") */
+ }
+ const node = local.file.node(file)
+ return (
+ <Code
+ file={{ name: node.path, contents: node.content?.content ?? "" }}
+ disableFileHeader
+ overflow="scroll"
+ class="pt-3 pb-40"
+ />
+ )
})()}
</Tabs.Content>
)}
@@ -847,7 +830,7 @@ export default function Page() {
items={local.file.search}
key={(x) => x}
onOpenChange={(open) => setStore("fileSelectOpen", open)}
- onSelect={(x) => (x ? local.file.open(x, { pinned: true }) : undefined)}
+ onSelect={(x) => (x ? local.session.open(x) : undefined)}
>
{(i) => (
<div