Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5e6353c
fix: add session status management and fix TypeScript errors
thdxr Nov 12, 2025
c56d2fd
sync
thdxr Nov 13, 2025
ddbcb00
fix
thdxr Nov 13, 2025
d6f2864
sync
thdxr Nov 13, 2025
7718aec
sync
thdxr Nov 13, 2025
961210f
core: refactor session state management to improve reliability
thdxr Nov 13, 2025
f4480fe
fix: resolve TypeScript errors in session status and lock usage
thdxr Nov 13, 2025
cd61185
Merge dev into agent-loop, keeping clean loop implementation
thdxr Nov 14, 2025
f0bacb2
sync
thdxr Nov 14, 2025
1f968d8
Merge branch 'dev' into agent-loop
thdxr Nov 14, 2025
d0277fa
core: extract session processor to handle streaming responses and too…
thdxr Nov 14, 2025
063ce15
Merge branch 'dev' into agent-loop
thdxr Nov 14, 2025
295c662
fix
thdxr Nov 14, 2025
5853d46
Merge branch 'dev' into agent-loop
thdxr Nov 14, 2025
ec4ce5f
progress-ish
thdxr Nov 15, 2025
4379270
progress
thdxr Nov 16, 2025
1f436aa
sync
thdxr Nov 16, 2025
f7bf7ec
core: add subtask support to session system for delegating work to sp…
thdxr Nov 16, 2025
e1e6a10
core: clean up imports and session message handling in prompt system
thdxr Nov 16, 2025
65dfd31
core: fix subtask message handling and tool execution flow
thdxr Nov 17, 2025
c682d7b
core: improve subtask prompt resolution and template handling
thdxr Nov 17, 2025
0b35b1b
core: add subtask message display to user conversation history
thdxr Nov 17, 2025
9790277
core: fix assistant message completion time and finish state handling
thdxr Nov 17, 2025
163d777
core: fix tool part type assertion in session metadata update
thdxr Nov 17, 2025
d024bf6
tui: fix subtask message formatting to show tool execution instead of…
thdxr Nov 17, 2025
5f77002
tui: fix completed state property name from type to status in session…
thdxr Nov 17, 2025
9026705
core: add debug logging for assistant message loop exit conditions
thdxr Nov 17, 2025
b84672d
core: add debug logging for session cancellation and message loop steps
thdxr Nov 17, 2025
47e0e0a
core: remove debug logging from session prompt and message loop
thdxr Nov 17, 2025
eea9346
core: fix session cancellation cleanup to prevent memory leaks in tas…
thdxr Nov 17, 2025
5dde596
core: fix tool part state management to properly handle completion st…
thdxr Nov 17, 2025
bbcb7a2
core: extract overflow check logic into separate function for better …
thdxr Nov 17, 2025
857c083
Merge branch 'dev' into agent-loop
thdxr Nov 17, 2025
e6d5494
sync
thdxr Nov 17, 2025
f9fc8ed
Merge branch 'dev' into agent-loop
thdxr Nov 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ dist
.turbo
**/.serena
.serena/
refs
1 change: 1 addition & 0 deletions .opencode/command/commit.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
description: Git commit and push
subtask: true
---

commit and push
Expand Down
4 changes: 0 additions & 4 deletions .opencode/opencode.json

This file was deleted.

11 changes: 11 additions & 0 deletions .opencode/opencode.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-openai-codex-auth"],
"provider": {
"opencode": {
"options": {
// "baseURL": "http://localhost:8080"
},
},
},
}
Empty file added a.out
Empty file.
16 changes: 16 additions & 0 deletions packages/opencode/src/cli/cmd/debug/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { EOL } from "os"
import { File } from "../../../file"
import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd"
import { Ripgrep } from "@/file/ripgrep"

const FileSearchCommand = cmd({
command: "search <query>",
Expand Down Expand Up @@ -62,6 +63,20 @@ const FileListCommand = cmd({
},
})

const FileTreeCommand = cmd({
command: "tree [dir]",
builder: (yargs) =>
yargs.positional("dir", {
type: "string",
description: "Directory to tree",
default: process.cwd(),
}),
async handler(args) {
const files = await Ripgrep.tree({ cwd: args.dir, limit: 200 })
console.log(files)
},
})

export const FileCommand = cmd({
command: "file",
builder: (yargs) =>
Expand All @@ -70,6 +85,7 @@ export const FileCommand = cmd({
.command(FileStatusCommand)
.command(FileListCommand)
.command(FileSearchCommand)
.command(FileTreeCommand)
.demandCommand(),
async handler() {},
})
12 changes: 12 additions & 0 deletions packages/opencode/src/cli/cmd/tui/context/sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
LspStatus,
McpStatus,
FormatterStatus,
SessionStatus,
} from "@opencode-ai/sdk"
import { createStore, produce, reconcile } from "solid-js/store"
import { useSDK } from "@tui/context/sdk"
Expand All @@ -33,6 +34,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
}
config: Config
session: Session[]
session_status: {
[sessionID: string]: SessionStatus
}
session_diff: {
[sessionID: string]: Snapshot.FileDiff[]
}
Expand All @@ -58,6 +62,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
command: [],
provider: [],
session: [],
session_status: {},
session_diff: {},
todo: {},
message: {},
Expand Down Expand Up @@ -140,6 +145,12 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
}),
)
break

case "session.status": {
setStore("session_status", event.properties.sessionID, event.properties.status)
break
}

case "message.updated": {
const messages = store.message[event.properties.info.sessionID]
if (!messages) {
Expand Down Expand Up @@ -240,6 +251,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
sdk.client.lsp.status().then((x) => setStore("lsp", x.data!)),
sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)),
sdk.client.formatter.status().then((x) => setStore("formatter", x.data!)),
sdk.client.session.status().then((x) => setStore("session_status", x.data!)),
]).then(() => {
setStore("status", "complete")
})
Expand Down
155 changes: 85 additions & 70 deletions packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { useTheme } from "@tui/context/theme"
import {
BoxRenderable,
ScrollBoxRenderable,
TextAttributes,
addDefaultParsers,
MacOSScrollAccel,
type ScrollAcceleration,
Expand Down Expand Up @@ -65,7 +64,6 @@ import { Editor } from "../../util/editor"
import { Global } from "@/global"
import fs from "fs/promises"
import stripAnsi from "strip-ansi"
import { LSP } from "@/lsp/index.ts"

addDefaultParsers(parsers.parsers)

Expand Down Expand Up @@ -101,7 +99,12 @@ export function Session() {
const permissions = createMemo(() => sync.data.permission[route.sessionID] ?? [])

const pending = createMemo(() => {
return messages().findLast((x) => x.role === "assistant" && !x.time?.completed)?.id
return messages().findLast((x) => x.role === "assistant" && !x.time.completed)?.id
})

const lastUserMessage = createMemo(() => {
const p = pending()
return messages().findLast((x) => x.role === "user" && (!p || x.id < p)) as UserMessage
})

const dimensions = useTerminalDimensions()
Expand Down Expand Up @@ -801,7 +804,7 @@ export function Session() {
</Match>
<Match when={message.role === "assistant"}>
<AssistantMessage
last={index() === messages().length - 1}
last={pending() === message.id}
message={message as AssistantMessage}
parts={sync.data.part[message.id] ?? []}
/>
Expand Down Expand Up @@ -856,64 +859,84 @@ function UserMessage(props: {
const queued = createMemo(() => props.pending && props.message.id > props.pending)
const color = createMemo(() => (queued() ? theme.accent : theme.secondary))

const compaction = createMemo(() => props.parts.find((x) => x.type === "compaction"))

return (
<Show when={text()}>
<box
id={props.message.id}
onMouseOver={() => {
setHover(true)
}}
onMouseOut={() => {
setHover(false)
}}
onMouseUp={props.onMouseUp}
border={["left"]}
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
marginTop={props.index === 0 ? 0 : 1}
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
customBorderChars={SplitBorder.customBorderChars}
borderColor={color()}
flexShrink={0}
>
<text fg={theme.text}>{text()?.text}</text>
<Show when={files().length}>
<box flexDirection="row" paddingBottom={1} paddingTop={1} gap={1} flexWrap="wrap">
<For each={files()}>
{(file) => {
const bg = createMemo(() => {
if (file.mime.startsWith("image/")) return theme.accent
if (file.mime === "application/pdf") return theme.primary
return theme.secondary
})
return (
<text fg={theme.text}>
<span style={{ bg: bg(), fg: theme.background }}> {MIME_BADGE[file.mime] ?? file.mime} </span>
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {file.filename} </span>
</text>
)
}}
</For>
</box>
</Show>
<text fg={theme.text}>
{sync.data.config.username ?? "You"}{" "}
<Show
when={queued()}
fallback={<span style={{ fg: theme.textMuted }}>({Locale.time(props.message.time.created)})</span>}
>
<span style={{ bg: theme.accent, fg: theme.backgroundPanel, bold: true }}> QUEUED </span>
<>
<Show when={text()}>
<box
id={props.message.id}
onMouseOver={() => {
setHover(true)
}}
onMouseOut={() => {
setHover(false)
}}
onMouseUp={props.onMouseUp}
border={["left"]}
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
marginTop={props.index === 0 ? 0 : 1}
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
customBorderChars={SplitBorder.customBorderChars}
borderColor={color()}
flexShrink={0}
>
<text fg={theme.text}>{text()?.text}</text>
<Show when={files().length}>
<box flexDirection="row" paddingBottom={1} paddingTop={1} gap={1} flexWrap="wrap">
<For each={files()}>
{(file) => {
const bg = createMemo(() => {
if (file.mime.startsWith("image/")) return theme.accent
if (file.mime === "application/pdf") return theme.primary
return theme.secondary
})
return (
<text fg={theme.text}>
<span style={{ bg: bg(), fg: theme.background }}> {MIME_BADGE[file.mime] ?? file.mime} </span>
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {file.filename} </span>
</text>
)
}}
</For>
</box>
</Show>
</text>
</box>
</Show>
<text fg={theme.text}>
{sync.data.config.username ?? "You"}{" "}
<Show
when={queued()}
fallback={<span style={{ fg: theme.textMuted }}>({Locale.time(props.message.time.created)})</span>}
>
<span style={{ bg: theme.accent, fg: theme.backgroundPanel, bold: true }}> QUEUED </span>
</Show>
</text>
</box>
</Show>
<Show when={compaction()}>
<box
marginTop={1}
border={["top"]}
title=" Compaction "
titleAlignment="center"
borderColor={theme.borderActive}
/>
</Show>
</>
)
}

function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; last: boolean }) {
const local = useLocal()
const { theme } = useTheme()
const sync = useSync()
const status = createMemo(
() =>
sync.data.session_status[props.message.sessionID] ?? {
type: "idle",
},
)
return (
<>
<For each={props.parts}>
Expand Down Expand Up @@ -945,23 +968,15 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
<text fg={theme.textMuted}>{props.message.error?.data.message}</text>
</box>
</Show>
<Show
when={
!props.message.time.completed ||
(props.last && props.parts.some((item) => item.type === "step-finish" && item.reason === "tool-calls"))
}
>
<box
paddingLeft={2}
marginTop={1}
flexDirection="row"
gap={1}
border={["left"]}
customBorderChars={SplitBorder.customBorderChars}
borderColor={theme.backgroundElement}
>
<Show when={props.last && status().type !== "idle"}>
<box paddingLeft={3} flexDirection="row" gap={1} marginTop={1}>
<text fg={local.agent.color(props.message.mode)}>{Locale.titlecase(props.message.mode)}</text>
<Shimmer text={`${props.message.modelID}`} color={theme.text} />
<Shimmer text={props.message.modelID} color={theme.text} />
<Show when={status().type === "retry"}>
<text fg={theme.error}>
{(status() as any).message} [attempt #{(status() as any).attempt}]
</text>
</Show>
</box>
</Show>
<Show
Expand Down
3 changes: 3 additions & 0 deletions packages/opencode/src/file/ripgrep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import { lazy } from "../util/lazy"
import { $ } from "bun"

import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js"
import { Log } from "@/util/log"

export namespace Ripgrep {
const log = Log.create({ service: "ripgrep" })
const Stats = z.object({
elapsed: z.object({
secs: z.number(),
Expand Down Expand Up @@ -254,6 +256,7 @@ export namespace Ripgrep {
}

export async function tree(input: { cwd: string; limit?: number }) {
log.info("tree", input)
const files = await Array.fromAsync(Ripgrep.files({ cwd: input.cwd }))
interface Node {
path: string[]
Expand Down
Loading