mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
Merge branch 'dev' into interleaved-thinking
This commit is contained in:
commit
8304bd418f
24 changed files with 1476 additions and 377 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
<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
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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) ?? ""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue