Skip to content

desktop: Context tab hides system prompt or displays a system update instead #55046

Open

Description

@ChenMuDX

Description

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.

Relevant code in 2.0.27:

  • packages/gui-extensions/src/context/tab.tsx:154–163:
    messages().findLast((message) => message.type === "system")?.text
  • 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.

Related, but broader feature requests: #6152, #48252, #33333.

Plugins

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

  1. Open a session in Desktop whose stored message list has no
    type: "system" entry.
  2. Open the right-side Context tab.
  3. 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.

Activity

  1. opencode-agent commented on Oct 13, 2026

    @opencode-agent
    Contributor

    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()
    const systemPromptSelector = (messages: ReadonlyArray<any>) => {
      const system = messages.findLast((m) => m.type === "system")?.text
      if (!system) return undefined
      const trimmed = system.trim()
      return trimmed || undefined
    }
    
    export default OpenCodeDriver.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* () {
          const systems: string[] = []
          yield* llm.serve((request: any, index: number) => {
            const sys = JSON.stringify(request?.body?.system ?? request?.system ?? request?.body?.messages?.filter?.((m: any) => m.role === "system") ?? "")
            systems.push(sys)
            return Stream.make(Llm.text(`Reply ${index + 1}`))
          })
    
          yield* ui.submit("hello")
          yield* ui.waitFor("Reply 1")
    
          const sessions = yield* opencode.session.list()
          const sessionID = sessions.data[0]!.id
          const dump = (label: string) =>
            Effect.gen(function* () {
              const msgs = (yield* opencode.message.list({ sessionID, order: "asc" })).data as any[]
              console.log(`\n=== ${label} ===`)
              console.log("message types:", msgs.map((m) => m.type).join(", "))
              for (const m of msgs.filter((m) => m.type === "system"))
                console.log("system message metadata:", JSON.stringify(m.metadata), "text:", JSON.stringify(m.text))
              const shown = 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.

  2. argszero commented on Oct 13, 2026

    @argszero
    Contributor

    I'd like to work on this.

    I reproduced the selector problem directly against dev in the source, rather than taking the diagnosis on trust.

    packages/gui-extensions/src/context/tab.tsx (systemPrompt() memo) does:

    const system = messages().findLast((message) => message.type === "system")?.text

    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.
    • packages/core/src/session/execution/restart.ts:96 writes metadata: { 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() 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:

    1. 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").
    2. 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.
    3. 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.

    I'll share the diff once the tests pass.

  3. neodr90 commented on Nov 5, 2026

    @neodr90

    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.

    Additional evidence from a 2.2.5 session:

    • instruction_state tracks core/codemode, core/skill-guidance, core/instructions,
      core/date, core/environment.
    • 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.

    So the mislabeled case is still present on 2.2.5.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions