You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
desktop: Context tab hides system prompt or displays a system update instead #55046
In OpenCode Desktop 2.0.27, the Context tab's “System prompt” section
above “Raw messages” is absent in a session without a stored system
message.
Source inspection shows that this section reads the text of the last
stored system message, rather than the separately assembled system
prompt. This also means an incremental system update can be mislabeled
as the system prompt.
Same file, lines 297–306: <Show when={systemPrompt()}> hides the entire section when absent.
packages/core/src/session/model-request.ts:100–119: baseTranscript assembles system separately from messages.
Observed API evidence:
The affected session's /context and /message responses contained no
system message.
A separate session contained a system message with metadata.notice="instructions", whose text was a Code Mode tool
catalog update—not the full initial system prompt.
Expected: show the assembled system prompt with clear provenance
(currently prepared versus last actually sent), or explicitly indicate
that it is unavailable. Do not label the last incremental system update
as the full system prompt.
This report concerns inspection/display only; it does not establish
that system instructions were omitted from model requests.
Built-in plugins active, including Code Mode and browser support.
Third-party plugin involvement has not been independently isolated.
OpenCode version
2.0.27, installed Desktop release; server also reports 2.0.27.
The reporter confirms the inspected source matches the installed build.
Steps to reproduce
Open a session in Desktop whose stored message list has no type: "system" entry.
Open the right-side Context tab.
Observe that “Raw messages” is present but “System prompt” is absent.
Additional source-derived case: when a stored system message contains
only an incremental instruction update, the selector above will display
that update under “System prompt”. This case was verified in API data,
not independently confirmed visually.
Screenshot and/or share link
Not included to avoid exposing private session content.
Source locations and sanitized observations are provided above.
Operating System
Windows, build 10.0.26300, x64.
Terminal
Desktop app (Electron).
Diagnostic shell: Windows PowerShell; ComSpec: cmd.exe.
Thanks for the detailed report! I could reproduce both cases on 2.0.27, and they still happen on the latest dev (b8f925e).
The script below starts a session with a simulated model and reads the same stored messages the Context tab reads. It then runs the tab's systemPrompt() logic on them:
Fresh session, one turn: the stored messages are user, assistant, idle, with no system entry. The "System prompt" section would be hidden, even though the model request did include the full system prompt (about 10 KB).
After editing AGENTS.md and a second turn: a system message with metadata.notice: "instructions" is stored, containing only "The instructions changed: …". The tab would show that update under "System prompt", while the model still received the full system prompt.
The sandbox has no browser, so I couldn't render the Desktop/Web UI itself. The selector logic in packages/gui-extensions/src/context/tab.tsx is the same in 2.0.27 and dev. As you said, this affects only what the tab displays; system instructions are still sent to the model.
Reproduction script (drive.ts)
// Reproduces #55046: the Desktop/Web Context tab derives "System prompt" from// the last stored `type: "system"` message. This script inspects the server data// that the tab reads (session.message.list) and applies the tab's exact selector// (packages/gui-extensions/src/context/tab.tsx, systemPrompt memo).import{Effect,Stream}from"effect"import{Llm,OpenCodeDriver}from"opencode-drive"import{writeFileSync}from"node:fs"import{join}from"node:path"// Same logic as tab.tsx systemPrompt()constsystemPromptSelector=(messages: ReadonlyArray<any>)=>{constsystem=messages.findLast((m)=>m.type==="system")?.textif(!system)returnundefinedconsttrimmed=system.trim()returntrimmed||undefined}exportdefaultOpenCodeDriver.use({
...(process.env.OC_DEV ? {opencode: {dev: process.env.OC_DEV}} : {}),project: {git: true,files: {"AGENTS.md": "# Rules\nAlways answer briefly.\n"}},},({ ui, llm, opencode, artifacts })=>Effect.gen(function*(){constsystems: string[]=[]yield*llm.serve((request: any,index: number)=>{constsys=JSON.stringify(request?.body?.system??request?.system??request?.body?.messages?.filter?.((m: any)=>m.role==="system")??"")systems.push(sys)returnStream.make(Llm.text(`Reply ${index+1}`))})yield*ui.submit("hello")yield*ui.waitFor("Reply 1")constsessions=yield*opencode.session.list()constsessionID=sessions.data[0]!.idconstdump=(label: string)=>Effect.gen(function*(){constmsgs=(yield*opencode.message.list({ sessionID,order: "asc"})).dataasany[]console.log(`\n=== ${label} ===`)console.log("message types:",msgs.map((m)=>m.type).join(", "))for(constmofmsgs.filter((m)=>m.type==="system"))console.log("system message metadata:",JSON.stringify(m.metadata),"text:",JSON.stringify(m.text))constshown=systemPromptSelector(msgs)console.log("Context tab 'System prompt' section:",shown===undefined ? "HIDDEN (no stored system message)" : `SHOWN with text: ${JSON.stringify(shown)}`)})yield*dump("after first turn")console.log("system prompt length actually sent to the model on turn 1:",systems[0]?.length)// Change instructions mid-session -> incremental system update is stored.writeFileSync(join(artifacts,"files","AGENTS.md"),"# Rules\nAlways answer briefly.\nAlso: use British spelling.\n")yield*ui.submit("hello again")yield*ui.waitFor("Reply 2")yield*dump("after editing AGENTS.md and a second turn")console.log("system prompt length actually sent to the model on turn 2:",systems[1]?.length)}),)
Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.
That takes any stored type: "system" message. On v2 the only system messages ever written are incremental ones:
packages/core/src/session/message-updater.ts:157 writes metadata: { notice: "instructions", instructionSources: [...] } when the instruction set changes mid-session.
So both the "hidden" case and the "mislabeled" case follow from the same cause: the section is driven by whatever system message happens to be last, while the prompt the model actually receives is assembled separately in packages/core/src/session/model-request.ts — baseTranscript() builds system from the agent prompt plus the initial instructions and keeps it apart from messages. A fresh session stores no system message at all, which is why the section disappears even though the request carried the full prompt.
Plan:
Stop treating notice-carrying system messages as the system prompt. Select only a message that actually represents it, and keep the incremental notices out of that section (they are useful, but not as "System prompt").
When no such message is stored, show the section with an explicit "prepared per request / not stored" state instead of hiding it, per the expected behaviour in the report.
Cover the three cases the reproduction names — fresh session, an instruction update after AGENTS.md changes, and a restart notice — in the extension's own test setup.
One question for the maintainers, since it decides how far step 2 should go: if the assembled prompt should be shown verbatim with its provenance, that needs a source for it on the client (the tab only reads stored messages today). I'm happy to plumb that through if you'd rather have the real text than a placeholder — otherwise I'll keep the change to the tab and the two states above.
Confirming this still reproduces on Desktop 2.2.5 (Windows 10.0.19045, latest channel).
The shipped bundle (out/renderer/assets/tab-BABfxPkK.js inside app.asar) still does messages.findLast((m) => m.type === "system")?.text, and the section is only rendered
when that value exists.
The core/instructions blob is byte-identical to ~/.config/opencode/AGENTS.md
(452 chars), and its hash is identical in initial_values and current_values.
The only stored system message is at seq 19 with instructionSources: ["core/codemode","core/skill-guidance"] (9131 chars) and no AGENTS.md text.
Result: the panel shows the Code Mode / skill-guidance update and never the AGENTS.md content, while the request system prompt does contain Instructions from: ~/.config/opencode/AGENTS.md.
Activity
opencode-agent commentedon Oct 13, 2026
Thanks for the detailed report! I could reproduce both cases on 2.0.27, and they still happen on the latest
dev(b8f925e).The script below starts a session with a simulated model and reads the same stored messages the Context tab reads. It then runs the tab's
systemPrompt()logic on them:user, assistant, idle, with nosystementry. The "System prompt" section would be hidden, even though the model request did include the full system prompt (about 10 KB).AGENTS.mdand a second turn: asystemmessage withmetadata.notice: "instructions"is stored, containing only "The instructions changed: …". The tab would show that update under "System prompt", while the model still received the full system prompt.The sandbox has no browser, so I couldn't render the Desktop/Web UI itself. The selector logic in
packages/gui-extensions/src/context/tab.tsxis the same in 2.0.27 anddev. As you said, this affects only what the tab displays; system instructions are still sent to the model.Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.argszero commentedon Oct 13, 2026
I'd like to work on this.
I reproduced the selector problem directly against
devin the source, rather than taking the diagnosis on trust.packages/gui-extensions/src/context/tab.tsx(systemPrompt()memo) does:That takes any stored
type: "system"message. On v2 the only system messages ever written are incremental ones:packages/core/src/session/message-updater.ts:157writesmetadata: { notice: "instructions", instructionSources: [...] }when the instruction set changes mid-session.packages/core/src/session/execution/restart.ts:96writesmetadata: { notice: "restart" }.So both the "hidden" case and the "mislabeled" case follow from the same cause: the section is driven by whatever system message happens to be last, while the prompt the model actually receives is assembled separately in
packages/core/src/session/model-request.ts—baseTranscript()buildssystemfrom the agent prompt plus the initial instructions and keeps it apart frommessages. A fresh session stores nosystemmessage at all, which is why the section disappears even though the request carried the full prompt.Plan:
AGENTS.mdchanges, and a restart notice — in the extension's own test setup.One question for the maintainers, since it decides how far step 2 should go: if the assembled prompt should be shown verbatim with its provenance, that needs a source for it on the client (the tab only reads stored messages today). I'm happy to plumb that through if you'd rather have the real text than a placeholder — otherwise I'll keep the change to the tab and the two states above.
I'll share the diff once the tests pass.
fix(gui-extensions): stop reporting instruction updates as the system…
neodr90 commentedon Nov 5, 2026
Confirming this still reproduces on Desktop 2.2.5 (Windows 10.0.19045,
latestchannel).The shipped bundle (
out/renderer/assets/tab-BABfxPkK.jsinsideapp.asar) still doesmessages.findLast((m) => m.type === "system")?.text, and the section is only renderedwhen that value exists.
Additional evidence from a 2.2.5 session:
instruction_statetrackscore/codemode,core/skill-guidance,core/instructions,core/date,core/environment.core/instructionsblob is byte-identical to~/.config/opencode/AGENTS.md(452 chars), and its hash is identical in
initial_valuesandcurrent_values.systemmessage is at seq 19 withinstructionSources: ["core/codemode","core/skill-guidance"](9131 chars) and noAGENTS.mdtext.AGENTS.mdcontent, while the request system prompt does containInstructions from: ~/.config/opencode/AGENTS.md.So the mislabeled case is still present on 2.2.5.