Skip to content

[FEATURE]: Session-scoped terminals + optional terminal context for the model #43758

Open

Description

@iyernaveenr

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request hasn't been suggested before.

Describe the enhancement you want to request

Problem: The GUI shows a terminal beside each chat, but terminals are workspace-scoped (since #12468) -- all tabs in a directory share them -- and nothing typed there is visible to the model. To ask "why did that fail?" you have to paste output or rerun the command through the shell tool. The disconnect goes both ways: the model's own shell commands run in hidden subprocesses, so you can't watch them execute or reuse the resulting shell state.

Proposal:

  1. Key PTYs to the owning chat session (sessionID on the PTY schema + connect-ticket scope, session-keyed terminal stores in the app). Each tab keeps its own shells across switches; unowned PTYs keep today's behavior.
  2. Capture commands via shell integration markers (OSC 133, same mechanism as VS Code) into a bounded, in-memory, per-session ring.
  3. Expose via a terminal_context config: off (default -- exactly today's behavior, no capture) / tool-only / ambient / ambient-full.
  4. Use the same plumbing in the reverse direction: a shell_in_terminal config: off (default -- today's isolated subprocess) / visible -- the model's shell commands are typed into the session's own terminal and output is read back through the same capture, with subprocess fallback when the terminal is absent or busy.

I have a working implementation of all four parts with tests, split-able into small PRs if the direction is acceptable. Happy to adjust the design.

Activity

  1. iyernaveenr commented on Aug 29, 2026

    @iyernaveenr
    Author

    Any thoughts on the direction here? Happy to open the first small PR (the session-ownership part) if that is easier to review than discussing in the abstract.

  2. ryanwyler commented on Aug 30, 2026

    @ryanwyler
    Contributor

    I wrote runner-mcp that I use with the TUI version of opencode, claude and codex that solves this problem for me. I'm not sure if this is similar to what you're talking about.

    https://github.com/gignit/runner-mcp

  3. iyernaveenr commented on Aug 30, 2026

    @iyernaveenr
    Author

    I wrote runner-mcp that I use with the TUI version of opencode, claude and codex that solves this problem for me. I'm not sure if this is similar to what you're talking about.

    https://github.com/gignit/runner-mcp

    No - an MCP server cannot provide this. The proposal changes the GUI app itself: the terminal pane each chat tab already renders becomes part of that chat session. Each tab owns its PTYs (today they are shared across all tabs in a workspace), commands the user types there become opt-in context for the model, and in the opt-in reverse mode the model's commands run in that same visible interactive shell (shared cwd/env/history) instead of a hidden subprocess. The pane-to-session binding and the per-session context injection are app-internal state, which is why this needs changes in the app's session/server/PTY code rather than external tooling. No installs; both options default off.

  4. iyernaveenr commented on Sep 7, 2026

    @iyernaveenr
    Author

    Following the CONTRIBUTING guidance to base on v2, the first part (session ownership of PTYs) is now implemented against upstream/v2: https://github.com/iyernaveenr/opencode/tree/naveen_r_iyer/v2-session-terminals

    Single commit, 19 files. Optional sessionID on PTY create; session-aware endpoints accept a sessionID claim and answer 404 for PTYs owned by another session (no existence leak); the GUI keys its terminal store per chat tab so each conversation keeps its own shells across tab switches. Callers that send no sessionID see today's behavior unchanged. Tests cover core, HTTP ownership end to end, and the app store keys.

    Not opening a PR until the direction is confirmed, per the design-review guardrail. Happy to adjust the approach.

    One question on direction, having read the persistent-pty work (#44832, #44971): the daemon is already keyed by session for the TUI, while the desktop app still uses the workspace-scoped /api/pty. If the plan is to migrate the app to persistent-pty, I would rather build the session-terminal work on that (its terminal/read endpoint is a natural source for the terminal-context part) than extend the legacy surface as the linked branch does. Either way works for me.

  5. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the detailed proposal, @iyernaveenr! To restate it: you'd like each chat session to own its terminals instead of sharing them across the workspace. Commands run there would be captured with OSC 133 shell-integration markers, and the model could optionally see them (terminal_context: off / tool-only / ambient / ambient-full). In the other direction, the model's shell commands could optionally run visibly in that session's terminal (shell_in_terminal: off / visible). Both would be off by default, so nothing changes unless you opt in.

    Maintainers will look at the direction and let you know whether it fits before you open PRs. Related issues:

    Most of this can't be done as a plugin today. Plugins can only read persistent terminals; they can't own PTYs per session or send the shell tool's commands to a terminal.

  6. mleivaDesarrollos commented on Nov 1, 2026

    @mleivaDesarrollos

    Strong +1, and a proposal to frame the same idea one level up: the missing thing is not a command — it is a channel between a pane and a conversation.

    Today the TUI has two buffers that both hold shell output and no way to move text between them:

    • the pane — a persistent PTY, visible to the human, opaque to the model;
    • the conversation — visible to the model, and until it is pasted in, unaware the pane exists.

    Everything users do about that (select-and-paste, retyping the command into chat, writing a client, this thread) is a workaround for the absent primitive. Two data points from the plumbing today, because it is closer to a channel than it looks:

    1. A pane's retained buffer is already readable: GET /api/experimental/persistent-pty/{ptyID}/snapshot returns .data.text — the whole buffer, not the visible rows (a 120-line seq in a 12-row pane came back as 120 lines), without attaching or taking control. GET /api/experimental/session/{sessionID}/terminal lists the session's panes.
    2. That buffer is the only one in the product that cannot be paged. PersistentPty.Info tracks absolute byte cursors (output.head / output.tail), yet no endpoint reads by them, while the agent's own shell output already does exactly that: GET /api/shell/{id}/output?cursor=&limit= → {output, cursor, size, truncated}, "page through captured combined output by absolute byte cursor". The chat side has messages.copy / session.copy. The pane has no equivalent — not for the agent, not for the human.

    So the request, in the shape I would build it:

    a. terminal.read with a cursor, not just a tail. {after?: cursor, limit?} → {text, cursor, size, truncated}, ANSI stripped by default with an explicit raw mode. Cursor paging is what makes a pane followable: read the delta of a running build or tail -f without re-reading the whole buffer, and without flooding context. This is the one piece that makes the channel worth having rather than a novelty.

    b. terminal.search <regex>. For long output the useful question is rarely "give me 500 lines", it is "which line failed". A bounded, server-side match with a little context returns a dozen lines instead of a megabyte, and it is the difference between a mechanism that fits in a context window and one that does not.

    c. terminal.last_command — your part 2 (OSC 133). Command boundaries are the unit people actually mean: exit status plus that command's output range. It subsumes most of (b) in practice, and it is the only version that answers "why did that fail?" without heuristics.

    d. The human end of the same channel. There is no terminal.copy binding at all — the pane exposes select / toggle / close / suspend / title, while messages.copy and session.copy exist for chat. Copy or export of a pane buffer should be a command, and it would also be the natural place to send a pane's output into the conversation deliberately, rather than by paste.

    Guardrails I would expect, and would want as a user: pull-only by default (nothing streams into context unprompted), hard caps with explicit truncation reporting, the same permission action that governs shell so a repo or org can deny it, and no input injection — writing into a pane must never race the user or answer a password prompt (that is #53781's problem, and one I have hit in exactly that form).

    On the workaround, to be explicit: I am not proposing that a script ships. I wrote one — a ~60-line client over those two GETs, session id from $OPENCODE_SESSION_ID — only because (a)–(d) do not exist, and I would drop it the day one of them does. Two things it cannot fix and a first-class version can:

    Also relevant: #61053 — the pane's viewport is frozen during a selection drag and the wheel is dropped in the mouse parser, so a human cannot select across screens. Same buffer, same gap, other direction.

    Happy to prototype (a) or (b) against the existing endpoints and report back, and to test parts 2/4 of your proposal on a real setup (TUI over SSH) if that is useful.

  7. mleivaDesarrollos commented on Nov 2, 2026

    @mleivaDesarrollos

    Scoping note for part 4 of the proposal, found while checking upstream: the "send pane output to the conversation deliberately" half already shipped for the desktop app — #58420, merged 2026-10-23, "quote selected terminal output into the composer": selecting output in the integrated terminal shows a Quote toolbar that adds a quote chip to the composer.

    So the concept is not missing, the TUI equivalent is (the keybind reference has session.copy and messages.copy but nothing for the pane). Worth folding in as "same affordance in the TUI" rather than designing a second mechanism — which leaves the cursor-paged read from my earlier comment as the one genuinely new piece on the TUI side.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions