Summary
The LLM chat module works without artificial restrictions when atty is run locally and the user enters a remote shell via ssh remote-host. Alt+A / Alt+C / Alt+S / Alt+Shift+C are intercepted by the local atty proxy and never reach the remote shell. API calls originate locally. OSC 133 markers from remote shells (if atty init bash is installed on the remote) flow through transparently and drive the local capture chain. There is one semantic caveat worth documenting: ghost text fires on keystrokes typed at the remote prompt using local-context history, which can mislead users into thinking suggestions are aware of remote state.
This issue captures the investigation; no code change is required, but two small doc additions are recommended.
What works (verified)
- Keybinding interception —
src/proxy.zig dispatches LLM actions via D.dispatchAction(&runtimes, &ctx, act) before stdin bytes are forwarded; swallow_after_binding consumes the meta bytes. Independent of foreground subprocess, so ssh remote doesn't shadow Alt+letter bindings.
- OSC 133 parser —
src/osc133.zig is a byte-state machine with no PID/local-shell gating. SSH is 8-bit clean, so \x1b]133;[ABCD]…\x07 from the remote arrives intact and is parsed identically to local markers. Sticky-on-first-marker (line ~31) means once any shell — local or remote — emits one, the parser is active.
- Subprocess detection —
src/subprocess.zig recognizes the ssh command, pushes a .ssh frame on ;C, and tags follow-up history as ssh://host/path. No LLM module code reads currentKind() to gate features. Confirmed via grep through src/modules/llm/.
- LLM API call origin —
src/modules/llm/worker.zig uses the local std.Io passed at attach time. HTTP requests + subprocess providers run on the local machine; nothing routes through the SSH tunnel.
- Chat persistence —
src/modules/llm/chat_persist.zig writes to the local chat_persist_dir; remote filesystem is never touched.
- Multi-level SSH nesting — Subprocess stack has
max_depth = 8; nothing in the LLM module gates on depth. local → ssh1 → ssh2 → … should preserve LLM features at every level.
- Inline chat panel painting —
src/modules/llm/hooks.zig and paint.zig do not check alt-screen status or subprocess context. Standard DECSTBM collision semantics apply (same as locally — TUIs on the remote that take alt-screen will cooperate the same way as local TUIs).
What's degraded / semantically off
1. Ghost text uses local-context history for remote-typed commands
src/line_state.zig tracks every printable byte arriving at atty's stdin, regardless of whether the user is at the local prompt or typing into ssh remote's remote shell. Ghost providers (atuin, history) then surface completions based on the local machine's history and shell function names — not the remote host's.
- Impact: UX confusion. The user sees a ghost suggestion for
ls -la based on a recent local invocation, while typing into a remote shell where that history is irrelevant.
- Workaround: Disable ghost text inside ssh, or configure providers to suppress when the subprocess frame is
.ssh.
- Fix scope: Moderate — propagate subprocess context into ghost providers and add host-aware filtering. Not a blocker; defer until users report it.
2. line_state doesn't reset on Ctrl+C forwarded to the remote shell
When the user presses Ctrl+C at a remote prompt, the byte is forwarded but atty's local line_state.reset() doesn't fire. Stale buffer content can leave brief ghost suggestions until the next ;A/;B cycle.
- Impact: Minimal — the uncertain-flag gating in
line_state suppresses most bad suggestions.
- Fix scope: Small — proxy hook to reset
line_state on certain forwarded control bytes. Defer unless reported.
Untested but architecturally expected to work
- Remote shell with
atty init bash >> ~/.bashrc on the remote — markers flow through, parser activates, dialog mode and OSC 7 cwd updates should land correctly.
- Alt+S dialog where the captured command output came from a remote shell (
;D close).
- Nested ssh (ssh → ssh → ssh): subprocess stack supports up to depth 8.
Security note
The OSC 133 parser does not verify marker origin. A compromised remote shell could emit fake ;C/;D to skew atty's subprocess stack or line-state. This is a design choice — same as for a compromised local shell — and the cost of mitigating (signing markers, refusing remote-origin markers) outweighs the benefit. Worth a one-liner in docs/security.md if/when that lands.
Recommendations
- Add a "Caveats" section to
docs/modules.md (or a new docs/ssh.md) explaining the ghost-text-context mismatch and noting that remote atty init bash is supported and beneficial. Effort: low. ← actionable doc-only change.
- Defer the ghost-context-filtering work — track here. Not a blocker. Subprocess context would need to thread to the providers; doable but adds API surface.
- No code changes needed for ssh-as-foreground — atty's local interception layer already handles this correctly.
Where to look in the code
| Concern |
File |
| Keybinding intercept |
src/proxy.zig (dispatchAction site) |
| OSC 133 parser |
src/osc133.zig |
| Subprocess stack |
src/subprocess.zig |
| LLM hooks (Alt+letter) |
src/modules/llm/hooks.zig |
| LLM HTTP worker |
src/modules/llm/worker.zig |
| Line state + ghost feeds |
src/line_state.zig, src/ghost.zig |
| Chat persistence (local-only) |
src/modules/llm/chat_persist.zig |
Summary
The LLM chat module works without artificial restrictions when atty is run locally and the user enters a remote shell via
ssh remote-host. Alt+A / Alt+C / Alt+S / Alt+Shift+C are intercepted by the local atty proxy and never reach the remote shell. API calls originate locally. OSC 133 markers from remote shells (ifatty init bashis installed on the remote) flow through transparently and drive the local capture chain. There is one semantic caveat worth documenting: ghost text fires on keystrokes typed at the remote prompt using local-context history, which can mislead users into thinking suggestions are aware of remote state.This issue captures the investigation; no code change is required, but two small doc additions are recommended.
What works (verified)
src/proxy.zigdispatches LLM actions viaD.dispatchAction(&runtimes, &ctx, act)before stdin bytes are forwarded;swallow_after_bindingconsumes the meta bytes. Independent of foreground subprocess, sossh remotedoesn't shadow Alt+letter bindings.src/osc133.zigis a byte-state machine with no PID/local-shell gating. SSH is 8-bit clean, so\x1b]133;[ABCD]…\x07from the remote arrives intact and is parsed identically to local markers. Sticky-on-first-marker (line ~31) means once any shell — local or remote — emits one, the parser is active.src/subprocess.zigrecognizes thesshcommand, pushes a.sshframe on;C, and tags follow-up history asssh://host/path. No LLM module code readscurrentKind()to gate features. Confirmed via grep throughsrc/modules/llm/.src/modules/llm/worker.ziguses the localstd.Iopassed at attach time. HTTP requests + subprocess providers run on the local machine; nothing routes through the SSH tunnel.src/modules/llm/chat_persist.zigwrites to the localchat_persist_dir; remote filesystem is never touched.max_depth = 8; nothing in the LLM module gates on depth.local → ssh1 → ssh2 → …should preserve LLM features at every level.src/modules/llm/hooks.zigandpaint.zigdo not check alt-screen status or subprocess context. Standard DECSTBM collision semantics apply (same as locally — TUIs on the remote that take alt-screen will cooperate the same way as local TUIs).What's degraded / semantically off
1. Ghost text uses local-context history for remote-typed commands
src/line_state.zigtracks every printable byte arriving at atty's stdin, regardless of whether the user is at the local prompt or typing intossh remote's remote shell. Ghost providers (atuin, history) then surface completions based on the local machine's history and shell function names — not the remote host's.ls -labased on a recent local invocation, while typing into a remote shell where that history is irrelevant..ssh.2.
line_statedoesn't reset on Ctrl+C forwarded to the remote shellWhen the user presses Ctrl+C at a remote prompt, the byte is forwarded but atty's local
line_state.reset()doesn't fire. Stale buffer content can leave brief ghost suggestions until the next;A/;Bcycle.line_statesuppresses most bad suggestions.line_stateon certain forwarded control bytes. Defer unless reported.Untested but architecturally expected to work
atty init bash >> ~/.bashrcon the remote — markers flow through, parser activates, dialog mode and OSC 7 cwd updates should land correctly.;Dclose).Security note
The OSC 133 parser does not verify marker origin. A compromised remote shell could emit fake
;C/;Dto skew atty's subprocess stack or line-state. This is a design choice — same as for a compromised local shell — and the cost of mitigating (signing markers, refusing remote-origin markers) outweighs the benefit. Worth a one-liner indocs/security.mdif/when that lands.Recommendations
docs/modules.md(or a newdocs/ssh.md) explaining the ghost-text-context mismatch and noting that remoteatty init bashis supported and beneficial.Effort: low.← actionable doc-only change.Where to look in the code
src/proxy.zig(dispatchAction site)src/osc133.zigsrc/subprocess.zigsrc/modules/llm/hooks.zigsrc/modules/llm/worker.zigsrc/line_state.zig,src/ghost.zigsrc/modules/llm/chat_persist.zig