| Status | Implemented |
| Modules | :core:term, :core:distro, :app |
| Primary sources | core/term/src/main/java/dev/blamspot/jcode/core/term/TerminalSessionManager.kt (GUEST_SHELL_INTEGRATION, GUEST_OPEN_URL_SHIM, NESTED_SHELL_WRAPPER_TEMPLATE, the reader's oscHandler), core/term/src/main/java/dev/blamspot/jcode/core/term/VtParser.kt, native/vt/src/vt_parser.c, app/src/main/java/dev/blamspot/jcode/workbench/SetupTerminalRunner.kt, core/distro/src/main/java/dev/blamspot/jcode/core/distro/DistroService.kt (CATALOG_SHELL_HELPERS) |
| Verified against | commit cea581c, 2026-08-09 |
The private wire protocol between programs running inside the guest Linux environment and the JCode
host. It is carried as OSC (Operating System Command) escape sequences in the terminal's own
output stream, which means it works through pipes, over su, inside scripts, and from any language
that can printf — no socket, no port, no extra file descriptor.
This document is the reference for the sequence formats. It is normative for anyone writing a guest script or extension that wants to talk to the IDE.
ESC ] <code> ; <payload> BEL
ESC=0x1B,]=0x5D,BEL=0x07.- In shell:
printf '\033]<code>;<payload>\007' … - The native parser queues each event;
VtParser.drainOsc()returns them asList<Pair<Int, String>>(code, payload). - Native entries are encoded
"<code>;<payload>"and split at the first;only — payloads may themselves contain;.
The device's native terminal implementation allowlists OSC 7711–7716. Codes outside that range are not delivered.
| Code | Name | Payload | Emitted by | Host action |
|---|---|---|---|---|
52 |
Clipboard write | c;<base64> |
Any program (standard xterm OSC 52) | onClipboardWrite(text) → Android clipboard |
7711 |
Open file | <absolutePath>[:line[:col]] |
jcode / code shell function |
onOpenFileRequest(path) → open and focus in the editor |
7712 |
Tab title | <programName> |
DEBUG trap + PROMPT_COMMAND |
onTitleChange(id, title); sets Session.foreground |
7713 |
Task complete | <token>;<exitCode> |
printf appended to a task |
onTaskComplete(id, payload) |
7714 |
Open URL | <url> |
The xdg-open shim |
onOpenUrlRequest(url) → web preview or chosen browser |
7715 |
Nested shell open | open;<token>;<label> |
The nested-shell wrapper | onNestedShellOpen(parentId, payload) |
7716 |
Task progress | <token>;<percent>;<label> |
jcode_progress helper |
onTaskProgress(id, payload) → determinate progress bar |
Standard xterm semantics, with one deliberate restriction:
A payload of
?is a clipboard read query. JCode never answers it — the guest must not see the user's clipboard uninvited.
Writes are c;<base64>; the host base64-decodes as UTF-8 and sets the Android clipboard. This is
what makes copy-on-select work in guest CLIs such as Claude Code.
Pasted text going the other way is shell-quoted by TerminalView.shellQuote before being written to
the PTY.
Installed as /etc/profile.d/jcode-open.sh, sourced through /etc/profile:
jcode() {
local a p
for a in "$@"; do
[ -z "$a" ] && continue
case "$a" in
/*) p="$a" ;;
*) p="$PWD/$a" ;;
esac
printf '\033]7711;%s\007' "$p"
done
}
command -v code >/dev/null 2>&1 || code() { jcode "$@"; }Relative paths are resolved against $PWD before emission, so the host always receives an absolute
guest path. code is aliased only when the distro has no real code binary.
if [ -t 1 ]; then
__jcode_tab() { printf '\033]7712;%s\007' "$1"; }
__jcode_tab_cmd() {
case "$BASH_COMMAND" in __jcode_*) return ;; esac
local w=${BASH_COMMAND%% *}
__jcode_tab "${w##*/}"
}
__jcode_tab_reset() { __jcode_tab terminal; }
trap '__jcode_tab_cmd' DEBUG
case ";${PROMPT_COMMAND};" in
*";__jcode_tab_reset;"*) ;;
*) PROMPT_COMMAND="__jcode_tab_reset;${PROMPT_COMMAND}" ;;
esac
fiThree details that matter:
[ -t 1 ]guard — the emitter stays silent when stdout is captured, so$(bash -c …)does not return escape bytes in its output.- The literal title
terminal(or an empty payload) means "back at the prompt, no foreground program". The host maps it toSession.foreground = null, which is what makes idle reaping and the close-a-busy-terminal warning correct. - Via
BASH_ENVthis also runs insidebash run-*.sh, so a Run tab shows the actual tool (npm,vite,dotnet) rather than the wrapper script.
The PROMPT_COMMAND injection is idempotent — it checks for itself before prepending.
ESC ] 7713 ; <token> ; <exitCode> BEL
Appended by the host to a command it wants to observe. Two producers:
Setup terminal (SetupTerminalRunner) — toolchain installs and project scaffolds run in one
shared "Setup" session so the user can watch and scroll real output. Tasks are serialized because
the session is a single shell:
<command>; printf '\033]7713;<token>;%s\007' "$?"; rm -f <guestScript>Run configurations (JCodeShell) — each run terminal appends the same marker with the token
run, so the run is "done" once every terminal has reported.
The token disambiguates a stale marker from an abandoned run.
The shim is installed at /usr/local/bin/{xdg-open,open,sensible-browser,…} and $BROWSER is
pointed at it:
#!/bin/sh
printf '\033]7714;%s\007' "$1" >/dev/tty 2>/dev/null || printf '\033]7714;%s\007' "$1"Writing to /dev/tty first is what makes it work even when the caller redirects stdout. The host
routes the URL to the in-app web preview or to the user's chosen browser (Settings → Web preview).
ESC ] 7715 ; open ; <token> ; <label> BEL
token matches [A-Za-z0-9_]+ (it is interpolated into guest and host paths). label is the shell
name.
The full relocation handshake, including the FIFO ACK/exit protocol and the 3-second watchdog, is specified in Terminal, PTY and VT §5.5.
VtParser.drainOsc's KDoc describes a longer payload (open;<token>;<b64label>;<b64cwd>;<b64user>). That form is not emitted or parsed anywhere. The live format is the three-field one above.
ESC ] 7716 ; <token> ; <percent> ; <label> BEL
Emitted by jcode_progress, one of three shell helpers DistroService prepends to every
catalog install/uninstall script (CATALOG_SHELL_HELPERS).
These are injected unconditionally, so a script may call them on either execution path. Without a
PTY, JCODE_PROGRESS_TOKEN is unset and the marker is simply not emitted.
jcode_progress() {
__jc_p=$1
case "$__jc_p" in ''|*[!0-9]*) return 0 ;; esac
[ "$__jc_p" -gt 100 ] && __jc_p=100
if [ -n "${JCODE_PROGRESS_TOKEN:-}" ]; then
printf '\033]7716;%s;%s;%s\007' "$JCODE_PROGRESS_TOKEN" "$__jc_p" "$2"
fi
if [ "$__jc_p" != "$__jc_last_pct" ]; then
__jc_last_pct=$__jc_p
printf '[%3d%%] %s\n' "$__jc_p" "$2"
fi
return 0
}It emits the OSC marker and a plain [ 42%] … line, so progress is readable in the terminal
itself as well as on the progress bar. Non-numeric input is ignored; values are clamped to 100.
Downloads a file and reports true byte-level progress across the [from, to] slice of the bar. It
exists because curl's own meter is a carriage-return animation from which no percentage can be
recovered once it has passed through a PTY; jcode_fetch reads Content-Length up front and
computes the percentage itself.
apt-get update plus apt-get install, reporting apt's own machine-readable APT::Status-Fd
percentages (on fd 3), so an apt-based toolchain gets a genuine bar rather than three hand-placed
milestones. It preserves apt's exit status via a temp file, because the exit status of a pipeline is
its last member's.
Every helper ends with return 0. A script running under set -e would otherwise abort on a
progress call whose last comparison happened to be false. Any new helper must do the same.
| Variable | Set by | Meaning |
|---|---|---|
JCODE_PROGRESS_TOKEN |
DistroService when running a catalog script |
Names the task an OSC 7716 marker belongs to. Exported explicitly on the su - <user> -c … path too, since a login shell would otherwise drop it |
JCODE_NSH_TOP |
The host, on a tab's own top shell | Prevents that shell from relocating itself; cleared by the wrapper so its own sub-shells can still relocate |
JCODE_NSH_INLINE |
The user | Per-invocation opt-out of nested-shell relocation |
BROWSER |
TerminalSessionManager |
Points at the OSC 7714 shim |
ANDROID_HOME |
TerminalSessionManager (androidSdkEnvVars) |
Present when the Android SDK is installed |
ANDROID_SERIAL, JCODE_ADB_PORT |
TerminalSessionManager (adbEnvVars) |
Present when the ADB bridge is up |
- Guard emitters on
[ -t 1 ](or write to/dev/tty) so captured output is not polluted. - Payloads are split at the first
;only — a payload may contain further separators. - New codes must stay within the allowlisted 7711–7716 range, or extend the native allowlist first.
- OSC 52 read queries are never answered.
- Tokens interpolated into paths must match
[A-Za-z0-9_]+. - Helper functions must
return 0. - The literal
terminaltitle is protocol, not cosmetics — idle reaping depends on it. - Sequences must be raw bytes. In Kotlin source, write them as
printfoctal escapes rather than embedding a literalESC/BELbyte, which is invisible to text tooling.
| Failure | Effect |
|---|---|
| Marker emitted with stdout captured | Escape bytes appear in command output; prevented by the tty guard |
| Stale token from an abandoned run | Host ignores the marker — the token is exactly the disambiguator |
Helper called under set -e with a false last comparison |
Would abort the script; prevented by return 0 |
| Program emits an OSC code outside 7711–7716 | Not delivered |
| Nested-shell FIFO never ACKed | 3-second watchdog aborts and the wrapper runs the shell inline |