Skip to content

Commit a1214ff

Browse files
authored
Refactor agent loop (sst#4412)
1 parent 9fd43ec commit a1214ff

File tree

22 files changed

+1291
-1318
lines changed

22 files changed

+1291
-1318
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ dist
1313
.turbo
1414
**/.serena
1515
.serena/
16+
refs

.opencode/command/commit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
description: Git commit and push
3+
subtask: true
34
---
45

56
commit and push

.opencode/opencode.json

Lines changed: 0 additions & 4 deletions
This file was deleted.

.opencode/opencode.jsonc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"$schema": "https://opencode.ai/config.json",
3+
"plugin": ["opencode-openai-codex-auth"],
4+
"provider": {
5+
"opencode": {
6+
"options": {
7+
// "baseURL": "http://localhost:8080"
8+
},
9+
},
10+
},
11+
}

a.out

Whitespace-only changes.

packages/opencode/src/cli/cmd/debug/file.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { EOL } from "os"
22
import { File } from "../../../file"
33
import { bootstrap } from "../../bootstrap"
44
import { cmd } from "../cmd"
5+
import { Ripgrep } from "@/file/ripgrep"
56

67
const FileSearchCommand = cmd({
78
command: "search <query>",
@@ -62,6 +63,20 @@ const FileListCommand = cmd({
6263
},
6364
})
6465

66+
const FileTreeCommand = cmd({
67+
command: "tree [dir]",
68+
builder: (yargs) =>
69+
yargs.positional("dir", {
70+
type: "string",
71+
description: "Directory to tree",
72+
default: process.cwd(),
73+
}),
74+
async handler(args) {
75+
const files = await Ripgrep.tree({ cwd: args.dir, limit: 200 })
76+
console.log(files)
77+
},
78+
})
79+
6580
export const FileCommand = cmd({
6681
command: "file",
6782
builder: (yargs) =>
@@ -70,6 +85,7 @@ export const FileCommand = cmd({
7085
.command(FileStatusCommand)
7186
.command(FileListCommand)
7287
.command(FileSearchCommand)
88+
.command(FileTreeCommand)
7389
.demandCommand(),
7490
async handler() {},
7591
})

packages/opencode/src/cli/cmd/tui/context/sync.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
LspStatus,
1212
McpStatus,
1313
FormatterStatus,
14+
SessionStatus,
1415
} from "@opencode-ai/sdk"
1516
import { createStore, produce, reconcile } from "solid-js/store"
1617
import { useSDK } from "@tui/context/sdk"
@@ -33,6 +34,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
3334
}
3435
config: Config
3536
session: Session[]
37+
session_status: {
38+
[sessionID: string]: SessionStatus
39+
}
3640
session_diff: {
3741
[sessionID: string]: Snapshot.FileDiff[]
3842
}
@@ -58,6 +62,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
5862
command: [],
5963
provider: [],
6064
session: [],
65+
session_status: {},
6166
session_diff: {},
6267
todo: {},
6368
message: {},
@@ -140,6 +145,12 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
140145
}),
141146
)
142147
break
148+
149+
case "session.status": {
150+
setStore("session_status", event.properties.sessionID, event.properties.status)
151+
break
152+
}
153+
143154
case "message.updated": {
144155
const messages = store.message[event.properties.info.sessionID]
145156
if (!messages) {
@@ -240,6 +251,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
240251
sdk.client.lsp.status().then((x) => setStore("lsp", x.data!)),
241252
sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)),
242253
sdk.client.formatter.status().then((x) => setStore("formatter", x.data!)),
254+
sdk.client.session.status().then((x) => setStore("session_status", x.data!)),
243255
]).then(() => {
244256
setStore("status", "complete")
245257
})

packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

Lines changed: 85 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import { useTheme } from "@tui/context/theme"
2020
import {
2121
BoxRenderable,
2222
ScrollBoxRenderable,
23-
TextAttributes,
2423
addDefaultParsers,
2524
MacOSScrollAccel,
2625
type ScrollAcceleration,
@@ -65,7 +64,6 @@ import { Editor } from "../../util/editor"
6564
import { Global } from "@/global"
6665
import fs from "fs/promises"
6766
import stripAnsi from "strip-ansi"
68-
import { LSP } from "@/lsp/index.ts"
6967

7068
addDefaultParsers(parsers.parsers)
7169

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

103101
const pending = createMemo(() => {
104-
return messages().findLast((x) => x.role === "assistant" && !x.time?.completed)?.id
102+
return messages().findLast((x) => x.role === "assistant" && !x.time.completed)?.id
103+
})
104+
105+
const lastUserMessage = createMemo(() => {
106+
const p = pending()
107+
return messages().findLast((x) => x.role === "user" && (!p || x.id < p)) as UserMessage
105108
})
106109

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

862+
const compaction = createMemo(() => props.parts.find((x) => x.type === "compaction"))
863+
859864
return (
860-
<Show when={text()}>
861-
<box
862-
id={props.message.id}
863-
onMouseOver={() => {
864-
setHover(true)
865-
}}
866-
onMouseOut={() => {
867-
setHover(false)
868-
}}
869-
onMouseUp={props.onMouseUp}
870-
border={["left"]}
871-
paddingTop={1}
872-
paddingBottom={1}
873-
paddingLeft={2}
874-
marginTop={props.index === 0 ? 0 : 1}
875-
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
876-
customBorderChars={SplitBorder.customBorderChars}
877-
borderColor={color()}
878-
flexShrink={0}
879-
>
880-
<text fg={theme.text}>{text()?.text}</text>
881-
<Show when={files().length}>
882-
<box flexDirection="row" paddingBottom={1} paddingTop={1} gap={1} flexWrap="wrap">
883-
<For each={files()}>
884-
{(file) => {
885-
const bg = createMemo(() => {
886-
if (file.mime.startsWith("image/")) return theme.accent
887-
if (file.mime === "application/pdf") return theme.primary
888-
return theme.secondary
889-
})
890-
return (
891-
<text fg={theme.text}>
892-
<span style={{ bg: bg(), fg: theme.background }}> {MIME_BADGE[file.mime] ?? file.mime} </span>
893-
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {file.filename} </span>
894-
</text>
895-
)
896-
}}
897-
</For>
898-
</box>
899-
</Show>
900-
<text fg={theme.text}>
901-
{sync.data.config.username ?? "You"}{" "}
902-
<Show
903-
when={queued()}
904-
fallback={<span style={{ fg: theme.textMuted }}>({Locale.time(props.message.time.created)})</span>}
905-
>
906-
<span style={{ bg: theme.accent, fg: theme.backgroundPanel, bold: true }}> QUEUED </span>
865+
<>
866+
<Show when={text()}>
867+
<box
868+
id={props.message.id}
869+
onMouseOver={() => {
870+
setHover(true)
871+
}}
872+
onMouseOut={() => {
873+
setHover(false)
874+
}}
875+
onMouseUp={props.onMouseUp}
876+
border={["left"]}
877+
paddingTop={1}
878+
paddingBottom={1}
879+
paddingLeft={2}
880+
marginTop={props.index === 0 ? 0 : 1}
881+
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
882+
customBorderChars={SplitBorder.customBorderChars}
883+
borderColor={color()}
884+
flexShrink={0}
885+
>
886+
<text fg={theme.text}>{text()?.text}</text>
887+
<Show when={files().length}>
888+
<box flexDirection="row" paddingBottom={1} paddingTop={1} gap={1} flexWrap="wrap">
889+
<For each={files()}>
890+
{(file) => {
891+
const bg = createMemo(() => {
892+
if (file.mime.startsWith("image/")) return theme.accent
893+
if (file.mime === "application/pdf") return theme.primary
894+
return theme.secondary
895+
})
896+
return (
897+
<text fg={theme.text}>
898+
<span style={{ bg: bg(), fg: theme.background }}> {MIME_BADGE[file.mime] ?? file.mime} </span>
899+
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {file.filename} </span>
900+
</text>
901+
)
902+
}}
903+
</For>
904+
</box>
907905
</Show>
908-
</text>
909-
</box>
910-
</Show>
906+
<text fg={theme.text}>
907+
{sync.data.config.username ?? "You"}{" "}
908+
<Show
909+
when={queued()}
910+
fallback={<span style={{ fg: theme.textMuted }}>({Locale.time(props.message.time.created)})</span>}
911+
>
912+
<span style={{ bg: theme.accent, fg: theme.backgroundPanel, bold: true }}> QUEUED </span>
913+
</Show>
914+
</text>
915+
</box>
916+
</Show>
917+
<Show when={compaction()}>
918+
<box
919+
marginTop={1}
920+
border={["top"]}
921+
title=" Compaction "
922+
titleAlignment="center"
923+
borderColor={theme.borderActive}
924+
/>
925+
</Show>
926+
</>
911927
)
912928
}
913929

914930
function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; last: boolean }) {
915931
const local = useLocal()
916932
const { theme } = useTheme()
933+
const sync = useSync()
934+
const status = createMemo(
935+
() =>
936+
sync.data.session_status[props.message.sessionID] ?? {
937+
type: "idle",
938+
},
939+
)
917940
return (
918941
<>
919942
<For each={props.parts}>
@@ -945,23 +968,15 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
945968
<text fg={theme.textMuted}>{props.message.error?.data.message}</text>
946969
</box>
947970
</Show>
948-
<Show
949-
when={
950-
!props.message.time.completed ||
951-
(props.last && props.parts.some((item) => item.type === "step-finish" && item.reason === "tool-calls"))
952-
}
953-
>
954-
<box
955-
paddingLeft={2}
956-
marginTop={1}
957-
flexDirection="row"
958-
gap={1}
959-
border={["left"]}
960-
customBorderChars={SplitBorder.customBorderChars}
961-
borderColor={theme.backgroundElement}
962-
>
971+
<Show when={props.last && status().type !== "idle"}>
972+
<box paddingLeft={3} flexDirection="row" gap={1} marginTop={1}>
963973
<text fg={local.agent.color(props.message.mode)}>{Locale.titlecase(props.message.mode)}</text>
964-
<Shimmer text={`${props.message.modelID}`} color={theme.text} />
974+
<Shimmer text={props.message.modelID} color={theme.text} />
975+
<Show when={status().type === "retry"}>
976+
<text fg={theme.error}>
977+
{(status() as any).message} [attempt #{(status() as any).attempt}]
978+
</text>
979+
</Show>
965980
</box>
966981
</Show>
967982
<Show

packages/opencode/src/file/ripgrep.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import { lazy } from "../util/lazy"
88
import { $ } from "bun"
99

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

1213
export namespace Ripgrep {
14+
const log = Log.create({ service: "ripgrep" })
1315
const Stats = z.object({
1416
elapsed: z.object({
1517
secs: z.number(),
@@ -254,6 +256,7 @@ export namespace Ripgrep {
254256
}
255257

256258
export async function tree(input: { cwd: string; limit?: number }) {
259+
log.info("tree", input)
257260
const files = await Array.fromAsync(Ripgrep.files({ cwd: input.cwd }))
258261
interface Node {
259262
path: string[]

0 commit comments

Comments
 (0)