diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx
index 11336d5002..b6e36b1953 100644
--- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx
@@ -54,6 +54,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
session_status: {
[sessionID: string]: SessionStatus
}
+ suggest_debug: {
+ [sessionID: string]: { state: string; detail?: string; time: number }
+ }
session_diff: {
[sessionID: string]: Snapshot.FileDiff[]
}
@@ -95,6 +98,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
provider_default: {},
session: [],
session_status: {},
+ suggest_debug: {},
session_diff: {},
todo: {},
message: {},
@@ -237,6 +241,15 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
break
}
+ case "session.suggest_debug": {
+ setStore("suggest_debug", event.properties.sessionID, {
+ state: event.properties.state,
+ detail: event.properties.detail,
+ time: Date.now(),
+ })
+ break
+ }
+
case "message.updated": {
const messages = store.message[event.properties.info.sessionID]
if (!messages) {
diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/suggest.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/suggest.tsx
new file mode 100644
index 0000000000..7a6f543d21
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/suggest.tsx
@@ -0,0 +1,71 @@
+import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
+import { createMemo, createSignal, onCleanup } from "solid-js"
+
+const id = "internal:sidebar-suggest"
+
+function View(props: { api: TuiPluginApi; session_id: string }) {
+ const theme = () => props.api.theme.current
+ const debug = createMemo(
+ () =>
+ (props.api.state.session as any).suggestDebug(props.session_id) as
+ | { state: string; detail?: string; time: number }
+ | undefined,
+ )
+
+ const [now, setNow] = createSignal(Date.now())
+ const timer = setInterval(() => setNow(Date.now()), 1000)
+ onCleanup(() => clearInterval(timer))
+
+ const age = createMemo(() => {
+ const d = debug()
+ if (!d) return ""
+ const ms = now() - d.time
+ if (ms < 1000) return "just now"
+ return `${Math.floor(ms / 1000)}s ago`
+ })
+
+ const color = createMemo(() => {
+ const state = debug()?.state
+ if (state === "generating") return theme().brand
+ if (state === "done") return theme().textSuccess ?? "green"
+ if (state === "error") return theme().textDanger ?? "red"
+ if (state === "refused") return theme().textWarning ?? "yellow"
+ return theme().textMuted
+ })
+
+ return (
+
+
+ Suggest
+
+ {debug() ? (
+ <>
+
+ {debug()!.state} {age()}
+
+ {debug()!.detail ? {debug()!.detail!.slice(0, 38)} : null}
+ >
+ ) : (
+ waiting
+ )}
+
+ )
+}
+
+const tui: TuiPlugin = async (api) => {
+ api.slots.register({
+ order: 50,
+ slots: {
+ sidebar_content(_ctx, props) {
+ return
+ },
+ },
+ })
+}
+
+const plugin: TuiPluginModule & { id: string } = {
+ id,
+ tui,
+}
+
+export default plugin
diff --git a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx
index 529c50cfa3..53e690f3ec 100644
--- a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx
+++ b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx
@@ -173,6 +173,9 @@ function stateApi(sync: ReturnType): TuiPluginApi["state"] {
status(sessionID) {
return sync.data.session_status[sessionID]
},
+ suggestDebug(sessionID) {
+ return sync.data.suggest_debug[sessionID]
+ },
permission(sessionID) {
return sync.data.permission[sessionID] ?? []
},
diff --git a/packages/opencode/src/cli/cmd/tui/plugin/internal.ts b/packages/opencode/src/cli/cmd/tui/plugin/internal.ts
index 856ee0ebb1..5ffc6ffaa5 100644
--- a/packages/opencode/src/cli/cmd/tui/plugin/internal.ts
+++ b/packages/opencode/src/cli/cmd/tui/plugin/internal.ts
@@ -6,6 +6,7 @@ import SidebarLsp from "../feature-plugins/sidebar/lsp"
import SidebarTodo from "../feature-plugins/sidebar/todo"
import SidebarFiles from "../feature-plugins/sidebar/files"
import SidebarFooter from "../feature-plugins/sidebar/footer"
+import SidebarSuggest from "../feature-plugins/sidebar/suggest"
import PluginManager from "../feature-plugins/system/plugins"
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
@@ -17,6 +18,7 @@ export type InternalTuiPlugin = TuiPluginModule & {
export const INTERNAL_TUI_PLUGINS: InternalTuiPlugin[] = [
HomeFooter,
HomeTips,
+ SidebarSuggest,
SidebarContext,
SidebarMcp,
SidebarLsp,
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index a339deecf8..68a7ca4b66 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -250,6 +250,9 @@ export namespace SessionPrompt {
)
})
+ const suggestDebug = (sessionID: SessionID, state: SessionStatus.SuggestState, detail?: string) =>
+ bus.publish(SessionStatus.Event.SuggestDebug, { sessionID, state, detail })
+
const suggest = Effect.fn("SessionPrompt.suggest")(function* (input: {
session: Session.Info
sessionID: SessionID
@@ -272,6 +275,8 @@ export namespace SessionPrompt {
const ag = yield* agents.get(message.agent ?? "code")
if (!ag) return
+ yield* suggestDebug(input.sessionID, "generating")
+
// Full message history so the cached KV from the main conversation is reused
const msgs = yield* MessageV2.filterCompactedEffect(input.sessionID)
const real = (item: MessageV2.WithParts) =>
@@ -287,7 +292,7 @@ export namespace SessionPrompt {
const modelMsgs = yield* Effect.promise(() => MessageV2.toModelMessages(msgs, model))
const system = [...env, ...(skills ? [skills] : []), ...instructions]
- const text = yield* Effect.promise(async (signal) => {
+ const exit = yield* Effect.promise(async (signal) => {
const result = await LLM.stream({
agent: ag,
user,
@@ -303,7 +308,14 @@ export namespace SessionPrompt {
messages: [...modelMsgs, { role: "user" as const, content: PROMPT_SUGGEST_NEXT }],
})
return result.text
- })
+ }).pipe(Effect.exit)
+
+ if (Exit.isFailure(exit)) {
+ const err = Cause.squash(exit.cause)
+ yield* suggestDebug(input.sessionID, "error", err instanceof Error ? err.message : String(err))
+ return
+ }
+ const text = exit.value
const line = text
.replace(/[\s\S]*?<\/think>\s*/g, "")
@@ -311,17 +323,24 @@ export namespace SessionPrompt {
.map((item) => item.trim())
.find((item) => item.length > 0)
?.replace(/^["'`]+|["'`]+$/g, "")
- if (!line) return
+ if (!line) {
+ yield* suggestDebug(input.sessionID, "refused", "empty response")
+ return
+ }
const tag = line
.toUpperCase()
.replace(/[\s-]+/g, "_")
.replace(/[^A-Z_]/g, "")
- if (tag === "NO_SUGGESTION") return
+ if (tag === "NO_SUGGESTION") {
+ yield* suggestDebug(input.sessionID, "refused", "NO_SUGGESTION")
+ return
+ }
const suggestion = line.length > 240 ? line.slice(0, 237) + "..." : line
if ((yield* status.get(input.sessionID)).type !== "idle") return
yield* status.suggest(input.sessionID, suggestion)
+ yield* suggestDebug(input.sessionID, "done", suggestion)
})
const insertReminders = Effect.fn("SessionPrompt.insertReminders")(function* (input: {
diff --git a/packages/opencode/src/session/status.ts b/packages/opencode/src/session/status.ts
index 6a85c77127..bc31792d2e 100644
--- a/packages/opencode/src/session/status.ts
+++ b/packages/opencode/src/session/status.ts
@@ -28,6 +28,9 @@ export namespace SessionStatus {
})
export type Info = z.infer
+ export const SuggestState = z.enum(["generating", "done", "refused", "error"])
+ export type SuggestState = z.infer
+
export const Event = {
Status: BusEvent.define(
"session.status",
@@ -36,6 +39,14 @@ export namespace SessionStatus {
status: Info,
}),
),
+ SuggestDebug: BusEvent.define(
+ "session.suggest_debug",
+ z.object({
+ sessionID: SessionID.zod,
+ state: SuggestState,
+ detail: z.string().optional(),
+ }),
+ ),
// deprecated
Idle: BusEvent.define(
"session.idle",