fix(tui): remove feature flag, fix stale suggestion, limit to 110 chars

- Remove OPENCODE_EXPERIMENTAL_NEXT_PROMPT gate so suggest always runs
- Use reconcile() in sync store to clear stale suggestion on status change
- Limit suggestion to 110 characters and instruct model to be concise
- Remove temporary debug sidebar panel and plumbing
pull/20309/head
Ryan Vogel 2026-04-06 14:31:03 +00:00
parent 722904fe4f
commit 3a47b0ed90
7 changed files with 12 additions and 133 deletions

View File

@ -54,9 +54,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
session_status: { session_status: {
[sessionID: string]: SessionStatus [sessionID: string]: SessionStatus
} }
suggest_debug: {
[sessionID: string]: { state: string; detail?: string; time: number }
}
session_diff: { session_diff: {
[sessionID: string]: Snapshot.FileDiff[] [sessionID: string]: Snapshot.FileDiff[]
} }
@ -98,7 +95,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
provider_default: {}, provider_default: {},
session: [], session: [],
session_status: {}, session_status: {},
suggest_debug: {},
session_diff: {}, session_diff: {},
todo: {}, todo: {},
message: {}, message: {},
@ -237,16 +233,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
} }
case "session.status": { case "session.status": {
setStore("session_status", event.properties.sessionID, event.properties.status) setStore("session_status", event.properties.sessionID, reconcile(event.properties.status))
break
}
case "session.suggest_debug": {
setStore("suggest_debug", event.properties.sessionID, {
state: event.properties.state,
detail: event.properties.detail,
time: Date.now(),
})
break break
} }

View File

@ -1,71 +0,0 @@
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 (
<box>
<text fg={theme().text}>
<b>Suggest</b>
</text>
{debug() ? (
<>
<text fg={color()}>
{debug()!.state} {age()}
</text>
{debug()!.detail ? <text fg={theme().textMuted}>{debug()!.detail!.slice(0, 38)}</text> : null}
</>
) : (
<text fg={theme().textMuted}>waiting</text>
)}
</box>
)
}
const tui: TuiPlugin = async (api) => {
api.slots.register({
order: 50,
slots: {
sidebar_content(_ctx, props) {
return <View api={api} session_id={props.session_id} />
},
},
})
}
const plugin: TuiPluginModule & { id: string } = {
id,
tui,
}
export default plugin

View File

@ -173,9 +173,6 @@ function stateApi(sync: ReturnType<typeof useSync>): TuiPluginApi["state"] {
status(sessionID) { status(sessionID) {
return sync.data.session_status[sessionID] return sync.data.session_status[sessionID]
}, },
suggestDebug(sessionID) {
return sync.data.suggest_debug[sessionID]
},
permission(sessionID) { permission(sessionID) {
return sync.data.permission[sessionID] ?? [] return sync.data.permission[sessionID] ?? []
}, },

View File

@ -6,7 +6,6 @@ import SidebarLsp from "../feature-plugins/sidebar/lsp"
import SidebarTodo from "../feature-plugins/sidebar/todo" import SidebarTodo from "../feature-plugins/sidebar/todo"
import SidebarFiles from "../feature-plugins/sidebar/files" import SidebarFiles from "../feature-plugins/sidebar/files"
import SidebarFooter from "../feature-plugins/sidebar/footer" import SidebarFooter from "../feature-plugins/sidebar/footer"
import SidebarSuggest from "../feature-plugins/sidebar/suggest"
import PluginManager from "../feature-plugins/system/plugins" import PluginManager from "../feature-plugins/system/plugins"
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui" import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
@ -18,7 +17,6 @@ export type InternalTuiPlugin = TuiPluginModule & {
export const INTERNAL_TUI_PLUGINS: InternalTuiPlugin[] = [ export const INTERNAL_TUI_PLUGINS: InternalTuiPlugin[] = [
HomeFooter, HomeFooter,
HomeTips, HomeTips,
SidebarSuggest,
SidebarContext, SidebarContext,
SidebarMcp, SidebarMcp,
SidebarLsp, SidebarLsp,

View File

@ -250,9 +250,6 @@ 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: { const suggest = Effect.fn("SessionPrompt.suggest")(function* (input: {
session: Session.Info session: Session.Info
sessionID: SessionID sessionID: SessionID
@ -275,8 +272,6 @@ export namespace SessionPrompt {
const ag = yield* agents.get(message.agent ?? "code") const ag = yield* agents.get(message.agent ?? "code")
if (!ag) return if (!ag) return
yield* suggestDebug(input.sessionID, "generating")
// Full message history so the cached KV from the main conversation is reused // Full message history so the cached KV from the main conversation is reused
const msgs = yield* MessageV2.filterCompactedEffect(input.sessionID) const msgs = yield* MessageV2.filterCompactedEffect(input.sessionID)
const real = (item: MessageV2.WithParts) => const real = (item: MessageV2.WithParts) =>
@ -292,7 +287,7 @@ export namespace SessionPrompt {
const modelMsgs = yield* Effect.promise(() => MessageV2.toModelMessages(msgs, model)) const modelMsgs = yield* Effect.promise(() => MessageV2.toModelMessages(msgs, model))
const system = [...env, ...(skills ? [skills] : []), ...instructions] const system = [...env, ...(skills ? [skills] : []), ...instructions]
const exit = yield* Effect.promise(async (signal) => { const text = yield* Effect.promise(async (signal) => {
const result = await LLM.stream({ const result = await LLM.stream({
agent: ag, agent: ag,
user, user,
@ -308,14 +303,7 @@ export namespace SessionPrompt {
messages: [...modelMsgs, { role: "user" as const, content: PROMPT_SUGGEST_NEXT }], messages: [...modelMsgs, { role: "user" as const, content: PROMPT_SUGGEST_NEXT }],
}) })
return result.text 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 const line = text
.replace(/<think>[\s\S]*?<\/think>\s*/g, "") .replace(/<think>[\s\S]*?<\/think>\s*/g, "")
@ -323,24 +311,17 @@ export namespace SessionPrompt {
.map((item) => item.trim()) .map((item) => item.trim())
.find((item) => item.length > 0) .find((item) => item.length > 0)
?.replace(/^["'`]+|["'`]+$/g, "") ?.replace(/^["'`]+|["'`]+$/g, "")
if (!line) { if (!line) return
yield* suggestDebug(input.sessionID, "refused", "empty response")
return
}
const tag = line const tag = line
.toUpperCase() .toUpperCase()
.replace(/[\s-]+/g, "_") .replace(/[\s-]+/g, "_")
.replace(/[^A-Z_]/g, "") .replace(/[^A-Z_]/g, "")
if (tag === "NO_SUGGESTION") { if (tag === "NO_SUGGESTION") return
yield* suggestDebug(input.sessionID, "refused", "NO_SUGGESTION")
return
}
const suggestion = line.length > 240 ? line.slice(0, 237) + "..." : line const suggestion = line.length > 110 ? line.slice(0, 107) + "..." : line
if ((yield* status.get(input.sessionID)).type !== "idle") return if ((yield* status.get(input.sessionID)).type !== "idle") return
yield* status.suggest(input.sessionID, suggestion) yield* status.suggest(input.sessionID, suggestion)
yield* suggestDebug(input.sessionID, "done", suggestion)
}) })
const insertReminders = Effect.fn("SessionPrompt.insertReminders")(function* (input: { const insertReminders = Effect.fn("SessionPrompt.insertReminders")(function* (input: {
@ -1414,13 +1395,11 @@ NOTE: At any point in time through this workflow you should feel free to ask the
if (input.noReply === true) return message if (input.noReply === true) return message
const result = yield* loop({ sessionID: input.sessionID }) const result = yield* loop({ sessionID: input.sessionID })
if (Flag.OPENCODE_EXPERIMENTAL_NEXT_PROMPT) { yield* suggest({
yield* suggest({ session,
session, sessionID: input.sessionID,
sessionID: input.sessionID, message: result,
message: result, }).pipe(Effect.ignore, Effect.forkIn(scope))
}).pipe(Effect.ignore, Effect.forkIn(scope))
}
return result return result
}, },
) )

View File

@ -4,7 +4,7 @@ Goal:
- Suggest a useful next step that keeps momentum. - Suggest a useful next step that keeps momentum.
Rules: Rules:
- Output exactly one line. - Output exactly one line, 110 characters max. Be concise.
- Write as the user speaking to the assistant (for example: "Can you...", "Help me...", "Let's..."). - Write as the user speaking to the assistant (for example: "Can you...", "Help me...", "Let's...").
- Match the user's tone and language; keep it natural and human. - Match the user's tone and language; keep it natural and human.
- Prefer a concrete action over a broad question. - Prefer a concrete action over a broad question.

View File

@ -28,9 +28,6 @@ export namespace SessionStatus {
}) })
export type Info = z.infer<typeof Info> export type Info = z.infer<typeof Info>
export const SuggestState = z.enum(["generating", "done", "refused", "error"])
export type SuggestState = z.infer<typeof SuggestState>
export const Event = { export const Event = {
Status: BusEvent.define( Status: BusEvent.define(
"session.status", "session.status",
@ -39,14 +36,6 @@ export namespace SessionStatus {
status: Info, status: Info,
}), }),
), ),
SuggestDebug: BusEvent.define(
"session.suggest_debug",
z.object({
sessionID: SessionID.zod,
state: SuggestState,
detail: z.string().optional(),
}),
),
// deprecated // deprecated
Idle: BusEvent.define( Idle: BusEvent.define(
"session.idle", "session.idle",