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
[FEATURE]: Session-scoped terminals + optional terminal context for the model #43758
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:
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.
Capture commands via shell integration markers (OSC 133, same mechanism as VS Code) into a bounded, in-memory, per-session ring.
Expose via a terminal_context config: off (default -- exactly today's behavior, no capture) / tool-only / ambient / ambient-full.
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.
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.
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.
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.
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.
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.
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.
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:
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.
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:
GET .../terminal/read?lines=N returns null unless the PTY is the current one attached to a view — easy to misread as "no output". snapshot has no such condition; worth documenting or fixing either way.
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.
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.
Activity
iyernaveenr commentedon Aug 29, 2026
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.
ryanwyler commentedon Aug 30, 2026
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
iyernaveenr commentedon Aug 30, 2026
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.
feat(core): session-owned PTYs with command capture
feat(opencode): enforce PTY session ownership and expose terminal con…
feat(app): session-scoped terminal stores
iyernaveenr commentedon Sep 7, 2026
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.
feat(core): session-owned PTYs with command capture
feat(opencode): enforce PTY session ownership and expose terminal con…
feat(app): session-scoped terminal stores
opencodejr commentedon Oct 28, 2026
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:
shell_in_terminal: visibleidea.yescommand pegs the server, grows memory without bound, and starves input #53473: PTY flow control and unbounded memory use, which may matter for the capture buffer.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.
mleivaDesarrollos commentedon Nov 1, 2026
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:
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:
GET /api/experimental/persistent-pty/{ptyID}/snapshotreturns.data.text— the whole buffer, not the visible rows (a 120-lineseqin a 12-row pane came back as 120 lines), without attaching or taking control.GET /api/experimental/session/{sessionID}/terminallists the session's panes.PersistentPty.Infotracks 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 hasmessages.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.readwith 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 ortail -fwithout 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.copybinding at all — the pane exposes select / toggle / close / suspend / title, whilemessages.copyandsession.copyexist 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
shellso 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:maxScrollbackdefault, never passed by the TUI). In the session I measured, 27,669 bytes had streamed and 5,626 were still readable. Filed against OpenTUI as core: maxScrollback is constructor-only and unreachable from Solid/React elements, so hosts cannot configure embedded terminal history opentui#1910, since the option is constructor-only and unreachable from a framework element.GET .../terminal/read?lines=Nreturnsnullunless the PTY is the current one attached to a view — easy to misread as "no output".snapshothas no such condition; worth documenting or fixing either way.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.
mleivaDesarrollos commentedon Nov 2, 2026
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.copyandmessages.copybut 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.