From afec40e8dabdd193334afbd8796902c3dbdf9a9b Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 2 Feb 2026 10:40:40 -0500 Subject: [PATCH 1/4] feat: make plan mode the default, remove experimental flag - Remove OPENCODE_EXPERIMENTAL_PLAN_MODE flag from flag.ts - Update prompt.ts to always use plan mode logic - Update registry.ts to always include plan tools in CLI - Remove flag documentation from cli.mdx --- packages/opencode/src/flag/flag.ts | 2 +- packages/opencode/src/session/prompt.ts | 28 +------------------------ packages/opencode/src/tool/registry.ts | 2 +- packages/web/src/content/docs/cli.mdx | 1 - 4 files changed, 3 insertions(+), 30 deletions(-) diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 64ae801d18ff..1d65703f5975 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -44,7 +44,7 @@ export namespace Flag { export const OPENCODE_EXPERIMENTAL_LSP_TY = truthy("OPENCODE_EXPERIMENTAL_LSP_TY") export const OPENCODE_EXPERIMENTAL_LSP_TOOL = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_LSP_TOOL") export const OPENCODE_DISABLE_FILETIME_CHECK = truthy("OPENCODE_DISABLE_FILETIME_CHECK") - export const OPENCODE_EXPERIMENTAL_PLAN_MODE = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE") + export const OPENCODE_EXPERIMENTAL_MARKDOWN = truthy("OPENCODE_EXPERIMENTAL_MARKDOWN") export const OPENCODE_MODELS_URL = process.env["OPENCODE_MODELS_URL"] export const OPENCODE_MODELS_PATH = process.env["OPENCODE_MODELS_PATH"] diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 98dce97ba90d..f95a0b282e45 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1232,33 +1232,7 @@ export namespace SessionPrompt { const userMessage = input.messages.findLast((msg) => msg.info.role === "user") if (!userMessage) return input.messages - // Original logic when experimental plan mode is disabled - if (!Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE) { - if (input.agent.name === "plan") { - userMessage.parts.push({ - id: Identifier.ascending("part"), - messageID: userMessage.info.id, - sessionID: userMessage.info.sessionID, - type: "text", - text: PROMPT_PLAN, - synthetic: true, - }) - } - const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan") - if (wasPlan && input.agent.name === "build") { - userMessage.parts.push({ - id: Identifier.ascending("part"), - messageID: userMessage.info.id, - sessionID: userMessage.info.sessionID, - type: "text", - text: BUILD_SWITCH, - synthetic: true, - }) - } - return input.messages - } - - // New plan mode logic when flag is enabled + // Plan mode logic const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant") // Switching from plan mode to build mode diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 7b3a45889728..4989e5c2d8db 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -117,7 +117,7 @@ export namespace ToolRegistry { ApplyPatchTool, ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []), ...(config.experimental?.batch_tool === true ? [BatchTool] : []), - ...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [PlanExitTool, PlanEnterTool] : []), + ...(Flag.OPENCODE_CLIENT === "cli" ? [PlanExitTool, PlanEnterTool] : []), ...custom, ] } diff --git a/packages/web/src/content/docs/cli.mdx b/packages/web/src/content/docs/cli.mdx index 7fb948f50541..fc42999e77f9 100644 --- a/packages/web/src/content/docs/cli.mdx +++ b/packages/web/src/content/docs/cli.mdx @@ -594,4 +594,3 @@ These environment variables enable experimental features that may change or be r | `OPENCODE_EXPERIMENTAL_EXA` | boolean | Enable experimental Exa features | | `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Enable experimental LSP type checking | | `OPENCODE_EXPERIMENTAL_MARKDOWN` | boolean | Enable experimental markdown features | -| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Enable plan mode | From bb4d978684a54368583b5899a10468abba6ab4e4 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 3 Feb 2026 15:48:40 -0600 Subject: [PATCH 2/4] feat: update tui model dialog to utilize model family to reduce noise in list --- .../cli/cmd/tui/component/dialog-model.tsx | 41 +++++++++++++++++-- packages/opencode/src/config/config.ts | 1 + packages/sdk/js/src/v2/gen/types.gen.ts | 4 ++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index 4ad92eeb8395..c5c932ad4699 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -7,6 +7,27 @@ import { useDialog } from "@tui/ui/dialog" import { createDialogProviderOptions, DialogProvider } from "./dialog-provider" import { useKeybind } from "../context/keybind" import * as fuzzysort from "fuzzysort" +import type { Provider } from "@opencode-ai/sdk/v2" + +function pickLatest(models: [string, Provider["models"][string]][]) { + const picks: Record = {} + for (const item of models) { + const model = item[0] + const info = item[1] + const key = info.family ?? model + const prev = picks[key] + if (!prev) { + picks[key] = item + continue + } + if (info.release_date !== prev[1].release_date) { + if (info.release_date > prev[1].release_date) picks[key] = item + continue + } + if (model > prev[0]) picks[key] = item + } + return Object.values(picks) +} export function useConnected() { const sync = useSync() @@ -22,6 +43,7 @@ export function DialogModel(props: { providerID?: string }) { const keybind = useKeybind() const [ref, setRef] = createSignal>() const [query, setQuery] = createSignal("") + const [all, setAll] = createSignal(false) const connected = useConnected() const providers = createDialogProviderOptions() @@ -117,12 +139,16 @@ export function DialogModel(props: { providerID?: string }) { (provider) => provider.id !== "opencode", (provider) => provider.name, ), - flatMap((provider) => - pipe( + flatMap((provider) => { + const items = pipe( provider.models, entries(), filter(([_, info]) => info.status !== "deprecated"), filter(([_, info]) => (props.providerID ? info.providerID === props.providerID : true)), + ) + const list = all() ? items : pickLatest(items) + return pipe( + list, map(([model, info]) => { const value = { providerID: provider.id, @@ -168,8 +194,8 @@ export function DialogModel(props: { providerID?: string }) { (x) => x.footer !== "Free", (x) => x.title, ), - ), - ), + ) + }), ) const popularProviders = !connected() @@ -222,6 +248,13 @@ export function DialogModel(props: { providerID?: string }) { local.model.toggleFavorite(option.value as { providerID: string; modelID: string }) }, }, + { + keybind: keybind.all.model_show_all_toggle?.[0], + title: all() ? "Show latest only" : "Show all models", + onTrigger: () => { + setAll((value) => !value) + }, + }, ]} ref={setRef} onFilter={setQuery} diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index dae6db6f9690..201cdd1ca03f 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -732,6 +732,7 @@ export namespace Config { stash_delete: z.string().optional().default("ctrl+d").describe("Delete stash entry"), model_provider_list: z.string().optional().default("ctrl+a").describe("Open provider list from model dialog"), model_favorite_toggle: z.string().optional().default("ctrl+f").describe("Toggle model favorite status"), + model_show_all_toggle: z.string().optional().default("ctrl+o").describe("Toggle showing all models"), session_share: z.string().optional().default("none").describe("Share current session"), session_unshare: z.string().optional().default("none").describe("Unshare current session"), session_interrupt: z.string().optional().default("escape").describe("Interrupt current session"), diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 0cf70241ef6f..9fe76c8495df 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1009,6 +1009,10 @@ export type KeybindsConfig = { * Toggle model favorite status */ model_favorite_toggle?: string + /** + * Toggle showing all models + */ + model_show_all_toggle?: string /** * Share current session */ From a90e8de050637c9f2aa7f80511fbc9ea53bd6db8 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:24:17 +1000 Subject: [PATCH 3/4] add missing return --- packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index 9f25be795384..b210f8ed353e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -127,6 +127,7 @@ export function DialogModel(props: { providerID?: string }) { (x) => x.title, ), ) + return items }), ) From 399dd8afb1701467eb1310ebaa2ba04e299e4719 Mon Sep 17 00:00:00 2001 From: Vadim Fint Date: Mon, 16 Feb 2026 17:39:49 +0300 Subject: [PATCH 4/4] fix(tui): stop streaming markdown/code after message completes TextPart passed streaming={true} unconditionally, causing opentui to always skip the last table row (treated as potentially incomplete). Now streaming is derived from message completion state, so completed messages render all table rows correctly. --- packages/opencode/src/cli/cmd/tui/routes/session/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 1202e3280568..a4f97ee71b9e 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1384,6 +1384,7 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass function TextPart(props: { last: boolean; part: TextPart; message: AssistantMessage }) { const ctx = use() const { theme, syntax } = useTheme() + const isStreaming = () => !props.message.time.completed return ( @@ -1391,7 +1392,7 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess @@ -1400,7 +1401,7 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess