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 plumbingpull/20309/head
parent
722904fe4f
commit
3a47b0ed90
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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] ?? []
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue