Workflows
Creating and Interacting with an Integrated Terminal
How Mountain spawns a native PTY, streams output to Sky via Tauri events, and routes keystrokes back to the shell process.
The integrated terminal is a native PTY process managed entirely inside Mountain. Sky renders the terminal via xterm.js, receiving output as Tauri events. Cocoon receives the same output as gRPC notifications so extensions can observe terminal data. User keystrokes take the reverse path: Sky → Mountain IPC → PTY master → shell stdin.
Phase 1 - Creation request (Cocoon or Wind → Mountain)
1. Request origin
The request originates from one of two places:
- Extension:
vscode.window.createTerminal(...)in Cocoon sends a$createTerminalgRPC request to Mountain. - User command:
workbench.action.terminal.newfrom the Command Palette in Wind reaches Mountain as a Tauri IPC call.
Both paths resolve to the same Common::terminal::CreateTerminal Effect executed by Mountain’s TerminalProvider.
For the extension path, Cocoon routes through its gRPC terminal service. For the user path, Wind dispatches the action through track.rs which creates and runs the effect.
Phase 2 - Native PTY spawning (Mountain)
2. TerminalProvider.CreateTerminal
TerminalProvider.CreateTerminal() (Mountain) allocates a new TerminalId from AppState and determines the shell to launch (from request options or the system default).
Mountain uses the portable-pty crate to open a pseudo-terminal pair:
PtySystem.openpty(size) // returns master + slave handlesA CommandBuilder is constructed for the shell binary (e.g. bash) and the shell process is spawned as a child of the PTY slave. The shell process believes it is talking to a real terminal.
A TerminalStateDto is created holding the TerminalId, the OS-level pid, and clones of the PTY master reader and writer handles. It is stored in AppState.ActiveTerminals.
3. I/O task spawning
Three Tokio tasks are spawned for the terminal’s lifetime:
- Writer Task - holds the receiver end of a
tokio::mpscchannel; when a string arrives it writes the bytes to the PTY master writer, delivering input to the shell. The sender end of the channel is stored in theTerminalStateDto. - Reader Task - loops reading from the PTY master reader; for each chunk of output it both sends a
$acceptTerminalProcessDatagRPC notification to Cocoon (containing theTerminalIdand data string) and emits a Tauri event to Sky:sky://terminal/data { id, data }. - Waiter Task - awaits shell process exit; on termination it sends a
$acceptTerminalClosedgRPC notification to Cocoon and removes the entry fromAppState.ActiveTerminals.
4. Initial notifications
Mountain sends $acceptTerminalOpened (with TerminalId and name) and $acceptTerminalProcessId (with OS-level pid) gRPC notifications to Cocoon. Cocoon’s notification handler adds the terminal to the __terminals cache and fires the onDidOpenTerminal event to any listening extensions.
Mountain also emits the same event to Sky via AppHandle.emit so the workbench UI can register the new terminal entry.
Finally, Mountain returns the creation result (ID, name, PID) to the caller.
Phase 3 - UI rendering and state sync (Mountain → Cocoon + Sky)
5. Cocoon terminal service
Cocoon’s terminal service (Cocoon) receives the $acceptTerminalOpened, $acceptTerminalProcessId, and $acceptTerminalProcessData notifications from Mountain.
It creates a local Terminal proxy object that represents the terminal to extensions. When it receives data, it fires the onDidWriteData event for that specific terminal instance, allowing extensions to “listen” to a terminal’s output.
6. Mountain → Wind/Sky bridge
In addition to notifying Cocoon, Mountain’s Reader Task (Phase 2, step 3) notifies the frontend UI via Tauri events. It emits:
AppHandle.emit("sky://terminal/data", { Id: TerminalId, Data: ... })7. Terminal UI component (Wind/Sky)
Sky’s terminal component listens for Tauri events. When it receives a sky://terminal/create event (triggered at creation time), it instantiates a new xterm.js instance for the TerminalId.
As the Reader Task emits sky://terminal/data events, Sky finds the correct xterm.js instance by TerminalId and calls xterm.write(data). The shell prompt appears in the panel.
Phase 4 - User input loop (Sky → Mountain → shell)
8. Terminal UI captures input
The user types ls -la in the terminal panel. xterm.js captures the keystrokes via its onData handler and calls:
TauriInvoke("mountain://terminal/send-text", {
id: terminalId,
text: "ls -la\r",
});9. track.rs and TerminalLogic handler (Mountain)
The mountain://terminal/send-text command is dispatched to track, which creates and runs the Common::terminal::SendTextToTerminal effect.
The SendTextToTerminalLogic handler is executed:
- It looks up the
TerminalStateDtofor the givenTerminalIdinAppState.ActiveTerminals. - It sends the input string (
ls -la\r) to thempscsender channel stored in theTerminalStateDto.
10. Writer Task delivers input to shell
The Writer Task (from Phase 2, step 3) was waiting on its receiver. It immediately receives the string and writes its bytes to the PTY master writer. The OS delivers the input to the waiting shell.
11. Shell execution and output loop
The shell (bash/powershell) receives the input, executes the ls -la command, and writes the resulting directory listing to stdout. The PTY slave captures it and makes it available on the PTY master.
The Reader Task reads the output and the loop from Phase 3 repeats - the listing appears in xterm.js and is also forwarded to Cocoon via $acceptTerminalProcessData.
Terminal lifecycle events
When a terminal closes (shell exits or the user explicitly closes it):
- The Waiter Task fires
$acceptTerminalClosedto Cocoon, which removes the terminal from its__terminalscache and firesonDidCloseTerminal. - Mountain emits the closure to Sky via Tauri event so the workbench removes the terminal tab.
The full lifecycle event set available to extensions:
| Event | Trigger |
|---|---|
onDidOpenTerminal | $acceptTerminalOpened gRPC from Mountain |
onDidCloseTerminal | $acceptTerminalClosed gRPC from Mountain |
onDidChangeActiveTerminal | Focus change notification from Wind/Sky |
Shell integration events (OSC 633)
Modern shells emit OSC 633 escape sequences to mark the boundaries of each command. Land processes these sequences through the PTY Reader Task and routes them as IPC calls:
| Sequence | Meaning | IPC routing |
|---|---|---|
OSC 633 ; A | Prompt start | Decoration marker only (no IPC) |
OSC 633 ; B | Prompt end | Decoration marker only (no IPC) |
OSC 633 ; C | Command start | localPty:shellExecutionStart IPC, stored in InflightExecution Map, $acceptTerminalShellExecutionStart gRPC → Cocoon |
OSC 633 ; D[;<exit>] | Command end | localPty:shellExecutionEnd IPC, $acceptTerminalShellExecutionEnd gRPC → Cocoon, $acceptExecutedTerminalCommand gRPC → Cocoon |
OSC 633 ; E;<line> | Line capture | Stored per-terminal in InflightExecution Map, associated with next OSC 633 ; D flush |
Extension event mapping
Extension subscribers receive these events via the following window API:
window.onDidStartTerminalShellExecution- fires onOSC 633 ; Cwindow.onDidEndTerminalShellExecution- fires onOSC 633 ; Dwindow.onDidExecuteTerminalCommand- fires afterOSC 633 ; Dwith the captured command line and exit code
Detailed OSC 633 flow
When the Reader Task detects an OSC 633 sequence in the PTY output stream, it does not forward the raw escape sequence to xterm.js. Instead:
- Prompt markers (A, B): Stripped from the output. They exist solely for shell integration decoration in the xterm.js renderer and are reconstructed by the frontend from IPC signals.
- Command start (C): Mountain stores an
InflightExecutionentry in a per-terminal map keyed by sequence number. AlocalPty:shellExecutionStartIPC event fires to Sky (for UI decoration like the run icon in the gutter) and a$acceptTerminalShellExecutionStartgRPC notification fires to Cocoon. - Line capture (E): The captured command line text is stored in the
InflightExecutionmap, associated with the most recentOSC 633 ; Centry. MultipleEsequences may arrive before the correspondingDflush. - Command end (D): Mountain looks up the
InflightExecutionentry, pairs it with any captured command lines, fires$acceptTerminalShellExecutionEndand$acceptExecutedTerminalCommandgRPC notifications to Cocoon, and sendslocalPty:shellExecutionEndto Sky. The exit code (after;in theDsequence) is included in the notification payload.
PTY data flow model
The PTY reader operates on a raw byte stream that interleaves terminal control sequences, OSC 633 markers, and actual shell output. The Reader Task in Mountain’s TerminalProvider must:
- Accumulate bytes from the PTY master reader until a complete OSC sequence boundary is identified.
- Parse
ESC ] 633 ; ... STsequences (OSC 633) and route them to the shell integration handler. - Forward all non-OSC-633 output (standard terminal data, ANSI escape sequences, other XTerm escape codes) as-is to the
sky://terminal/dataTauri event for xterm.js rendering. - Send the same forwarded data and OSC 633 IPC notifications to Cocoon via gRPC so extensions receive both raw terminal data and structured command execution events.
This design ensures that xterm.js receives the same byte stream as a real terminal (minus the stripped OSC 633 markers) while extensions get structured execution boundaries through the onDidStartTerminalShellExecution and onDidEndTerminalShellExecution APIs.