Merge branch 'dev' into interleaved-thinking

This commit is contained in:
Aiden Cline 2025-12-09 15:54:28 -06:00
commit 8304bd418f
24 changed files with 1476 additions and 377 deletions

View file

@ -22,7 +22,7 @@ type State = {
ready: boolean
provider: Provider[]
agent: Agent[]
project: Project
project: string
config: Config
path: Path
session: Session[]
@ -60,11 +60,10 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
})
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
function child(directory: string) {
if (!children[directory]) {
setGlobalStore("children", directory, {
project: { id: "", worktree: "", time: { created: 0, initialized: 0, updated: 0 } },
project: "",
config: {},
path: { state: "", config: "", worktree: "", directory: "" },
ready: false,
@ -88,9 +87,29 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
const sdk = useGlobalSDK()
sdk.event.listen((e) => {
const directory = e.name
const [store, setStore] = child(directory)
const event = e.details
if (directory === "global") {
switch (event.type) {
case "project.updated": {
const result = Binary.search(globalStore.projects, event.properties.id, (s) => s.id)
if (result.found) {
setGlobalStore("projects", result.index, reconcile(event.properties))
break
}
setGlobalStore(
"projects",
produce((draft) => {
draft.splice(result.index, 0, event.properties)
}),
)
break
}
}
return
}
const [store, setStore] = child(directory)
switch (event.type) {
case "session.updated": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
@ -166,7 +185,7 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
"projects",
x
.data!.filter((x) => !x.worktree.includes("opencode-test") && x.vcs)
.sort((a, b) => b.time.created - a.time.created),
.sort((a, b) => a.id.localeCompare(b.id)),
),
),
]).then(() => setGlobalStore("ready", true))

View file

@ -12,7 +12,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const globalSync = useGlobalSync()
const [store, setStore] = makePersisted(
createStore({
projects: [] as { directory: string; expanded: boolean }[],
projects: [] as { worktree: string; expanded: boolean }[],
sidebar: {
opened: false,
width: 280,
@ -26,7 +26,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
}),
{
name: "default-layout.v4",
name: "default-layout.v6",
},
)
@ -43,32 +43,43 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
onMount(() => {
Promise.all(
store.projects.map(({ directory }) => {
return loadProjectSessions(directory)
store.projects.map(({ worktree }) => {
return loadProjectSessions(worktree)
}),
)
})
function enrich(project: { worktree: string; expanded: boolean }) {
const metadata = globalSync.data.projects.find((x) => x.worktree === project.worktree)
if (!metadata) return []
return [
{
...project,
...metadata,
},
]
}
return {
projects: {
list: createMemo(() => store.projects),
list: createMemo(() => store.projects.flatMap(enrich)),
open(directory: string) {
if (store.projects.find((x) => x.directory === directory)) return
if (store.projects.find((x) => x.worktree === directory)) return
loadProjectSessions(directory)
setStore("projects", (x) => [...x, { directory, expanded: true }])
setStore("projects", (x) => [...x, { worktree: directory, expanded: true }])
},
close(directory: string) {
setStore("projects", (x) => x.filter((x) => x.directory !== directory))
setStore("projects", (x) => x.filter((x) => x.worktree !== directory))
},
expand(directory: string) {
setStore("projects", (x) => x.map((x) => (x.directory === directory ? { ...x, expanded: true } : x)))
setStore("projects", (x) => x.map((x) => (x.worktree === directory ? { ...x, expanded: true } : x)))
},
collapse(directory: string) {
setStore("projects", (x) => x.map((x) => (x.directory === directory ? { ...x, expanded: false } : x)))
setStore("projects", (x) => x.map((x) => (x.worktree === directory ? { ...x, expanded: false } : x)))
},
move(directory: string, toIndex: number) {
setStore("projects", (projects) => {
const fromIndex = projects.findIndex((x) => x.directory === directory)
const fromIndex = projects.findIndex((x) => x.worktree === directory)
if (fromIndex === -1 || fromIndex === toIndex) return projects
const result = [...projects]
const [item] = result.splice(fromIndex, 1)

View file

@ -7,7 +7,6 @@ import { TextSelection } from "./local"
import { pipe, sumBy } from "remeda"
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
import { useParams } from "@solidjs/router"
import { base64Encode } from "@opencode-ai/util/encode"
import { useSDK } from "./sdk"
export type LocalPTY = {
@ -25,9 +24,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
const sdk = useSDK()
const params = useParams()
const sync = useSync()
const name = createMemo(
() => `${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}.v2`,
)
const name = createMemo(() => `${params.dir}/session${params.id ? "/" + params.id : ""}.v3`)
const [store, setStore] = makePersisted(
createStore<{

View file

@ -13,7 +13,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const [store, setStore] = globalSync.child(sdk.directory)
const load = {
project: () => sdk.client.project.current().then((x) => setStore("project", x.data!)),
project: () => sdk.client.project.current().then((x) => setStore("project", x.data!.id)),
provider: () => sdk.client.config.providers().then((x) => setStore("provider", x.data!.providers)),
path: () => sdk.client.path.get().then((x) => setStore("path", x.data!)),
agent: () => sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
@ -41,6 +41,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
get ready() {
return store.ready
},
get project() {
const match = Binary.search(globalSync.data.projects, store.project, (p) => p.id)
if (match.found) return globalSync.data.projects[match.index]
return undefined
},
session: {
get(sessionID: string) {
const match = Binary.search(store.session, sessionID, (s) => s.id)

View file

@ -7,6 +7,7 @@ import { useNavigate } from "@solidjs/router"
import { base64Encode } from "@opencode-ai/util/encode"
import { Icon } from "@opencode-ai/ui/icon"
import { usePlatform } from "@/context/platform"
import { DateTime } from "luxon"
export default function Home() {
const sync = useGlobalSync()
@ -47,8 +48,12 @@ export default function Home() {
</Button>
</Show>
</div>
<ol class="flex flex-col gap-2">
<For each={sync.data.projects.slice(0, 5)}>
<ul class="flex flex-col gap-2">
<For
each={sync.data.projects
.toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
.slice(0, 5)}
>
{(project) => (
<Button
size="large"
@ -57,11 +62,13 @@ export default function Home() {
onClick={() => openProject(project.worktree)}
>
{project.worktree}
<div class="text-14-regular text-text-weak">10m ago</div>
<div class="text-14-regular text-text-weak">
{DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
</div>
</Button>
)}
</For>
</ol>
</ul>
</div>
</Match>
<Match when={true}>

View file

@ -16,7 +16,7 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { getFilename } from "@opencode-ai/util/path"
import { Select } from "@opencode-ai/ui/select"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Session } from "@opencode-ai/sdk/v2/client"
import { Session, Project } from "@opencode-ai/sdk/v2/client"
import { usePlatform } from "@/context/platform"
import { createStore } from "solid-js/store"
import {
@ -106,8 +106,8 @@ export default function Layout(props: ParentProps) {
const { draggable, droppable } = event
if (draggable && droppable) {
const projects = layout.projects.list()
const fromIndex = projects.findIndex((p) => p.directory === draggable.id.toString())
const toIndex = projects.findIndex((p) => p.directory === droppable.id.toString())
const fromIndex = projects.findIndex((p) => p.worktree === draggable.id.toString())
const toIndex = projects.findIndex((p) => p.worktree === droppable.id.toString())
if (fromIndex !== toIndex && toIndex !== -1) {
layout.projects.move(draggable.id.toString(), toIndex)
}
@ -140,8 +140,8 @@ export default function Layout(props: ParentProps) {
return <></>
}
const ProjectVisual = (props: { directory: string; class?: string }): JSX.Element => {
const name = createMemo(() => getFilename(props.directory))
const ProjectVisual = (props: { project: Project & { expanded: boolean }; class?: string }): JSX.Element => {
const name = createMemo(() => getFilename(props.project.worktree))
return (
<Switch>
<Match when={layout.sidebar.opened()}>
@ -149,11 +149,16 @@ export default function Layout(props: ParentProps) {
as={"div"}
variant="ghost"
data-active
class="flex items-center justify-between gap-3 w-full px-1 self-stretch h-8 border-none"
class="flex items-center justify-between gap-3 w-full px-1 self-stretch h-8 border-none rounded-lg"
>
<div class="flex items-center gap-3 p-0 text-left min-w-0 grow">
<div class="size-6 shrink-0">
<Avatar fallback={name()} background="var(--surface-info-base)" class="size-full" />
<Avatar
fallback={name()}
src={props.project.icon?.url}
background={props.project.icon?.color ?? "var(--surface-info-base)"}
class="size-full"
/>
</div>
<span class="truncate text-14-medium text-text-strong">{name()}</span>
</div>
@ -163,12 +168,17 @@ export default function Layout(props: ParentProps) {
<Button
variant="ghost"
size="large"
class="flex items-center justify-center p-0 aspect-square border-none"
data-selected={props.directory === currentDirectory()}
onClick={() => navigateToProject(props.directory)}
class="flex items-center justify-center p-0 aspect-square border-none rounded-lg"
data-selected={props.project.worktree === currentDirectory()}
onClick={() => navigateToProject(props.project.worktree)}
>
<div class="size-6 shrink-0">
<Avatar fallback={name()} background="var(--surface-info-base)" class="size-full" />
<Avatar
fallback={name()}
src={props.project.icon?.url}
background={props.project.icon?.color ?? "var(--surface-info-base)"}
class="size-full"
/>
</div>
</Button>
</Match>
@ -176,11 +186,11 @@ export default function Layout(props: ParentProps) {
)
}
const SortableProject = (props: { project: { directory: string; expanded: boolean } }): JSX.Element => {
const sortable = createSortable(props.project.directory)
const [projectStore] = globalSync.child(props.project.directory)
const slug = createMemo(() => base64Encode(props.project.directory))
const name = createMemo(() => getFilename(props.project.directory))
const SortableProject = (props: { project: Project & { expanded: boolean } }): JSX.Element => {
const sortable = createSortable(props.project.worktree)
const [projectStore] = globalSync.child(props.project.worktree)
const slug = createMemo(() => base64Encode(props.project.worktree))
const name = createMemo(() => getFilename(props.project.worktree))
return (
// @ts-ignore
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
@ -190,13 +200,14 @@ export default function Layout(props: ParentProps) {
<Button
as={"div"}
variant="ghost"
class="group/session flex items-center justify-between gap-3 w-full px-1 self-stretch h-auto border-none"
class="group/session flex items-center justify-between gap-3 w-full px-1 self-stretch h-auto border-none rounded-lg"
>
<Collapsible.Trigger class="group/trigger flex items-center gap-3 p-0 text-left min-w-0 grow border-none">
<div class="size-6 shrink-0">
<Avatar
fallback={name()}
background="var(--surface-info-base)"
src={props.project.icon?.url}
background={props.project.icon?.color ?? "var(--surface-info-base)"}
class="size-full group-hover/session:hidden"
/>
<Icon
@ -212,7 +223,7 @@ export default function Layout(props: ParentProps) {
<DropdownMenu.Trigger as={IconButton} icon="dot-grid" variant="ghost" />
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item onSelect={() => closeProject(props.project.directory)}>
<DropdownMenu.Item onSelect={() => closeProject(props.project.worktree)}>
<DropdownMenu.ItemLabel>Close Project</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
@ -274,8 +285,8 @@ export default function Layout(props: ParentProps) {
</Collapsible>
</Match>
<Match when={true}>
<Tooltip placement="right" value={props.project.directory}>
<ProjectVisual directory={props.project.directory} />
<Tooltip placement="right" value={props.project.worktree}>
<ProjectVisual project={props.project} />
</Tooltip>
</Match>
</Switch>
@ -284,11 +295,12 @@ export default function Layout(props: ParentProps) {
}
const ProjectDragOverlay = (): JSX.Element => {
const project = createMemo(() => layout.projects.list().find((p) => p.worktree === store.activeDraggable))
return (
<Show when={store.activeDraggable}>
{(directory) => (
<Show when={project()}>
{(p) => (
<div class="bg-background-base rounded-md">
<ProjectVisual directory={directory()} />
<ProjectVisual project={p()} />
</div>
)}
</Show>
@ -315,7 +327,7 @@ export default function Layout(props: ParentProps) {
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<Select
options={layout.projects.list().map((project) => project.directory)}
options={layout.projects.list().map((project) => project.worktree)}
current={currentDirectory()}
label={(x) => getFilename(x)}
onSelect={(x) => (x ? navigateToProject(x) : undefined)}
@ -407,7 +419,7 @@ export default function Layout(props: ParentProps) {
<Button
variant="ghost"
size="large"
class="group/sidebar-toggle shrink-0 w-full text-left justify-start"
class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg"
onClick={layout.sidebar.toggle}
>
<div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
@ -443,7 +455,7 @@ export default function Layout(props: ParentProps) {
<DragDropSensors />
<ConstrainDragXAxis />
<div class="w-full min-w-8 flex flex-col gap-2 min-h-0 overflow-y-auto no-scrollbar">
<SortableProvider ids={layout.projects.list().map((p) => p.directory)}>
<SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
<For each={layout.projects.list()}>{(project) => <SortableProject project={project} />}</For>
</SortableProvider>
</div>
@ -456,7 +468,7 @@ export default function Layout(props: ParentProps) {
<Show when={platform.openDirectoryPickerDialog}>
<Tooltip placement="right" value="Open project" inactive={layout.sidebar.opened()}>
<Button
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px]"
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
variant="ghost"
size="large"
icon="folder-add-left"
@ -469,7 +481,7 @@ export default function Layout(props: ParentProps) {
<Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}>
<Button
disabled
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px]"
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
variant="ghost"
size="large"
icon="settings-gear"
@ -482,7 +494,7 @@ export default function Layout(props: ParentProps) {
as={"a"}
href="https://opencode.ai/desktop-feedback"
target="_blank"
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px]"
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
variant="ghost"
size="large"
icon="bubble-5"

View file

@ -28,7 +28,7 @@ import {
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
import type { JSX } from "solid-js"
import { useSync } from "@/context/sync"
import { useSession } from "@/context/session"
import { useSession, type LocalPTY } from "@/context/session"
import { useLayout } from "@/context/layout"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { Terminal } from "@/components/terminal"
@ -43,6 +43,7 @@ export default function Page() {
clickTimer: undefined as number | undefined,
fileSelectOpen: false,
activeDraggable: undefined as string | undefined,
activeTerminalDraggable: undefined as string | undefined,
})
let inputRef!: HTMLDivElement
@ -178,6 +179,49 @@ export default function Page() {
setStore("activeDraggable", undefined)
}
const handleTerminalDragStart = (event: unknown) => {
const id = getDraggableId(event)
if (!id) return
setStore("activeTerminalDraggable", id)
}
const handleTerminalDragOver = (event: DragEvent) => {
const { draggable, droppable } = event
if (draggable && droppable) {
const terminals = session.terminal.all()
const fromIndex = terminals.findIndex((t) => t.id === draggable.id.toString())
const toIndex = terminals.findIndex((t) => t.id === droppable.id.toString())
if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
session.terminal.move(draggable.id.toString(), toIndex)
}
}
}
const handleTerminalDragEnd = () => {
setStore("activeTerminalDraggable", undefined)
}
const SortableTerminalTab = (props: { terminal: LocalPTY }): JSX.Element => {
const sortable = createSortable(props.terminal.id)
return (
// @ts-ignore
<div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}>
<div class="relative h-full">
<Tabs.Trigger
value={props.terminal.id}
closeButton={
session.terminal.all().length > 1 && (
<IconButton icon="close" variant="ghost" onClick={() => session.terminal.close(props.terminal.id)} />
)
}
>
{props.terminal.title}
</Tabs.Trigger>
</div>
</div>
)
}
const FileVisual = (props: { file: LocalFile; active?: boolean }): JSX.Element => {
return (
<div class="flex items-center gap-x-1.5">
@ -401,15 +445,19 @@ export default function Page() {
<span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
</div>
</div>
<div class="flex justify-center items-center gap-3">
<Icon name="pencil-line" size="small" />
<div class="text-12-medium text-text-weak">
Last modified&nbsp;
<span class="text-text-strong">
{DateTime.fromMillis(sync.data.project.time.created).toRelative()}
</span>
</div>
</div>
<Show when={sync.project}>
{(project) => (
<div class="flex justify-center items-center gap-3">
<Icon name="pencil-line" size="small" />
<div class="text-12-medium text-text-weak">
Last modified&nbsp;
<span class="text-text-strong">
{DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()}
</span>
</div>
</div>
)}
</Show>
</div>
</Match>
</Switch>
@ -614,40 +662,54 @@ export default function Page() {
onResize={layout.terminal.resize}
onCollapse={layout.terminal.close}
/>
<Tabs variant="alt" value={session.terminal.active()} onChange={session.terminal.open}>
<Tabs.List class="h-10">
<DragDropProvider
onDragStart={handleTerminalDragStart}
onDragEnd={handleTerminalDragEnd}
onDragOver={handleTerminalDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragYAxis />
<Tabs variant="alt" value={session.terminal.active()} onChange={session.terminal.open}>
<Tabs.List class="h-10">
<SortableProvider ids={session.terminal.all().map((t) => t.id)}>
<For each={session.terminal.all()}>{(terminal) => <SortableTerminalTab terminal={terminal} />}</For>
</SortableProvider>
<div class="h-full flex items-center justify-center">
<Tooltip value="Open file" class="flex items-center">
<IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={session.terminal.new} />
</Tooltip>
</div>
</Tabs.List>
<For each={session.terminal.all()}>
{(terminal) => (
<Tabs.Trigger
value={terminal.id}
closeButton={
session.terminal.all().length > 1 && (
<IconButton icon="close" variant="ghost" onClick={() => session.terminal.close(terminal.id)} />
)
}
>
{terminal.title}
</Tabs.Trigger>
<Tabs.Content value={terminal.id}>
<Terminal
pty={terminal}
onCleanup={session.terminal.update}
onConnectError={() => session.terminal.clone(terminal.id)}
/>
</Tabs.Content>
)}
</For>
<div class="h-full flex items-center justify-center">
<Tooltip value="Open file" class="flex items-center">
<IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={session.terminal.new} />
</Tooltip>
</div>
</Tabs.List>
<For each={session.terminal.all()}>
{(terminal) => (
<Tabs.Content value={terminal.id}>
<Terminal
pty={terminal}
onCleanup={session.terminal.update}
onConnectError={() => session.terminal.clone(terminal.id)}
/>
</Tabs.Content>
)}
</For>
</Tabs>
</Tabs>
<DragOverlay>
<Show when={store.activeTerminalDraggable}>
{(draggedId) => {
const terminal = createMemo(() => session.terminal.all().find((t) => t.id === draggedId()))
return (
<Show when={terminal()}>
{(t) => (
<div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
{t().title}
</div>
)}
</Show>
)
}}
</Show>
</DragOverlay>
</DragDropProvider>
</div>
</Show>
</div>

View file

@ -1,4 +1,4 @@
import { FileDiff, Message, Model, Part, Session, SessionStatus } from "@opencode-ai/sdk"
import { FileDiff, Message, Model, Part, Session } from "@opencode-ai/sdk/v2"
import { fn } from "@opencode-ai/util/fn"
import { iife } from "@opencode-ai/util/iife"
import { Identifier } from "@opencode-ai/util/identifier"

View file

@ -1,4 +1,4 @@
import { FileDiff, Message, Model, Part, Session, SessionStatus, UserMessage } from "@opencode-ai/sdk"
import { FileDiff, Message, Model, Part, Session, SessionStatus, UserMessage } from "@opencode-ai/sdk/v2"
import { SessionTurn } from "@opencode-ai/ui/session-turn"
import { SessionReview } from "@opencode-ai/ui/session-review"
import { DataProvider } from "@opencode-ai/ui/context"

View file

@ -11,8 +11,8 @@ export namespace Flag {
export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS")
export const OPENCODE_DISABLE_AUTOCOMPACT = truthy("OPENCODE_DISABLE_AUTOCOMPACT")
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
export const OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH =
process.env["OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH"]
export const OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH = number("OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH")
export const OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT = number("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT")
// Experimental
export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")
@ -25,4 +25,11 @@ export namespace Flag {
const value = process.env[key]?.toLowerCase()
return value === "true" || value === "1"
}
function number(key: string) {
const value = process.env[key]
if (!value) return undefined
const parsed = Number(value)
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined
}
}

View file

@ -209,6 +209,60 @@ export namespace LSPServer {
},
}
export const Biome: Info = {
id: "biome",
root: NearestRoot(["biome.json", "biome.jsonc", "package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
extensions: [
".ts",
".tsx",
".js",
".jsx",
".mjs",
".cjs",
".mts",
".cts",
".json",
".jsonc",
".vue",
".astro",
".svelte",
".css",
".graphql",
".gql",
".html",
],
async spawn(root) {
const localBin = path.join(root, "node_modules", ".bin", "biome")
let bin: string | undefined
if (await Bun.file(localBin).exists()) bin = localBin
if (!bin) {
const found = Bun.which("biome")
if (found) bin = found
}
let args = ["lsp-proxy", "--stdio"]
if (!bin) {
const resolved = await Bun.resolve("biome", root).catch(() => undefined)
if (!resolved) return
bin = BunProc.which()
args = ["x", "biome", "lsp-proxy", "--stdio"]
}
const proc = spawn(bin, args, {
cwd: root,
env: {
...process.env,
BUN_BE_BUN: "1",
},
})
return {
process: proc,
}
},
}
export const Gopls: Info = {
id: "gopls",
root: async (file) => {

View file

@ -16,12 +16,8 @@ import { Flag } from "@/flag/flag.ts"
import path from "path"
import { iife } from "@/util/iife"
const DEFAULT_MAX_OUTPUT_LENGTH = 30_000
const MAX_OUTPUT_LENGTH = (() => {
const parsed = Number(Flag.OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH)
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_OUTPUT_LENGTH
})()
const DEFAULT_TIMEOUT = 2 * 60 * 1000
const MAX_OUTPUT_LENGTH = Flag.OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH || 30_000
const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT || 2 * 60 * 1000
const SIGKILL_TIMEOUT_MS = 200
export const log = Log.create({ service: "bash-tool" })

File diff suppressed because it is too large Load diff

View file

@ -13,6 +13,11 @@
color: oklch(from var(--avatar-bg) calc(l * 0.72) calc(c * 8) h);
}
[data-component="avatar"][data-has-image] {
background-color: transparent;
border: none;
}
[data-component="avatar"][data-size="small"] {
width: 1.25rem;
height: 1.25rem;

View file

@ -2,27 +2,31 @@ import { type ComponentProps, splitProps, Show } from "solid-js"
export interface AvatarProps extends ComponentProps<"div"> {
fallback: string
src?: string
background?: string
size?: "small" | "normal" | "large"
}
export function Avatar(props: AvatarProps) {
const [split, rest] = splitProps(props, ["fallback", "background", "size", "class", "classList", "style"])
const [split, rest] = splitProps(props, ["fallback", "src", "background", "size", "class", "classList", "style"])
return (
<div
{...rest}
data-component="avatar"
data-size={split.size || "normal"}
data-has-image={split.src ? "" : undefined}
classList={{
...(split.classList ?? {}),
[split.class ?? ""]: !!split.class,
}}
style={{
...(typeof split.style === "object" ? split.style : {}),
...(split.background ? { "--avatar-bg": split.background } : {}),
...(!split.src && split.background ? { "--avatar-bg": split.background } : {}),
}}
>
<Show when={split.fallback}>{split.fallback[0]}</Show>
<Show when={split.src} fallback={split.fallback?.[0]}>
{(src) => <img src={src()} draggable={false} class="size-full object-cover" />}
</Show>
</div>
)
}

View file

@ -4,7 +4,7 @@
justify-content: center;
border-style: solid;
border-width: 1px;
border-radius: var(--radius-lg);
border-radius: var(--radius-md);
text-decoration: none;
user-select: none;
cursor: default;

View file

@ -1,4 +1,4 @@
import { UserMessage } from "@opencode-ai/sdk"
import { UserMessage } from "@opencode-ai/sdk/v2"
import { ComponentProps, createMemo, For, Match, Show, splitProps, Switch } from "solid-js"
import { DiffChanges } from "./diff-changes"
import { Spinner } from "./spinner"

View file

@ -7,7 +7,7 @@ import {
TextPart,
ToolPart,
UserMessage,
} from "@opencode-ai/sdk"
} from "@opencode-ai/sdk/v2"
import { useDiffComponent } from "../context/diff"
import { BasicTool } from "./basic-tool"
import { GenericTool } from "./basic-tool"

View file

@ -2,7 +2,7 @@ import { For, JSXElement, Match, Show, Switch, createEffect, createMemo, createS
import { Part } from "./message-part"
import { Spinner } from "./spinner"
import { useData } from "../context/data"
import type { AssistantMessage as AssistantMessageType, ToolPart } from "@opencode-ai/sdk"
import type { AssistantMessage as AssistantMessageType, ToolPart } from "@opencode-ai/sdk/v2"
export interface MessageProgressProps {
assistantMessages: () => AssistantMessageType[]

View file

@ -1,4 +1,4 @@
import { UserMessage } from "@opencode-ai/sdk"
import { UserMessage } from "@opencode-ai/sdk/v2"
import { ComponentProps, Show, splitProps } from "solid-js"
import { MessageNav } from "./message-nav"
import "./session-message-rail.css"

View file

@ -8,7 +8,7 @@ import { useDiffComponent } from "../context/diff"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { For, Match, Show, Switch, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { type FileDiff } from "@opencode-ai/sdk"
import { type FileDiff } from "@opencode-ai/sdk/v2"
import { PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr"
import { Dynamic } from "solid-js/web"
import { checksum } from "@opencode-ai/util/encode"

View file

@ -1,4 +1,4 @@
import { AssistantMessage } from "@opencode-ai/sdk"
import { AssistantMessage } from "@opencode-ai/sdk/v2"
import { useData } from "../context"
import { useDiffComponent } from "../context/diff"
import { getDirectory, getFilename } from "@opencode-ai/util/path"

View file

@ -1,4 +1,4 @@
import type { Message, Session, Part, FileDiff, SessionStatus } from "@opencode-ai/sdk"
import type { Message, Session, Part, FileDiff, SessionStatus } from "@opencode-ai/sdk/v2"
import { createSimpleContext } from "./helper"
import { PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr"

View file

@ -1,4 +1,4 @@
import type { Part } from "@opencode-ai/sdk/client"
import type { Part } from "@opencode-ai/sdk/v2/client"
export const sanitize = (text: string | undefined, remove?: RegExp) => (remove ? text?.replace(remove, "") : text) ?? ""