Repository navigation
chore(persona): terminal-renderer local skill files #168
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
1bdca94
chore(persona): write local skill files for terminal-renderer
miyaontherelay dd35e37
docs(persona): address review feedback on terminal-renderer skill files
miyaontherelay 7897b07
fix(persona): repoint terminal-renderer local skills to repo-root-rel…
khaliqgant 57308ef
fix(persona): add SKILL frontmatter to terminal-renderer skill files
khaliqgant File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
157 changes: 157 additions & 0 deletions
157
.agentworkforce/workforce/personas/__assets/terminal-renderer/ansi-vt-sequences.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| --- | ||
| name: ansi-vt-sequences | ||
| description: Use when reading raw escape sequences from instrumentation or reasoning about cursor/clear/scroll-region/DECSET behavior and the three TUI redraw patterns (cursor-positioning in place, alt-screen full-redraw, synchronized output) behind a terminal rendering bug. | ||
| --- | ||
|
|
||
| # ansi-vt-escape-sequences | ||
|
|
||
| The byte-level command vocabulary TUIs use to drive the terminal. When you can read the chunks, you can name the bug. | ||
|
|
||
| ## Sequence taxonomy | ||
|
|
||
| All escape sequences start with `\x1b` (ESC, often shown as `\e` in logs). Then: | ||
|
|
||
| - `\e[ ...` — **CSI** (Control Sequence Introducer). The bulk of cursor / color / mode commands. | ||
| - `\e]...` — **OSC** (Operating System Command). Window title, hyperlinks, cursor color. | ||
| - `\eP...` — **DCS** (Device Control String). Sixel graphics, terminfo queries. | ||
| - `\eN ...`, `\eO ...` — **SS2 / SS3**. Single-shift to alternate character sets. | ||
| - `\e(B`, `\e(0` — **Character set designation** (no space between `(` and the final byte). `B` = US-ASCII, `0` = DEC special graphics. | ||
| - `\eM`, `\eD`, `\eE` — **Cursor controls outside CSI**. Reverse index, index, next line. | ||
|
|
||
| CSI subforms: | ||
| - `\e[ <params> <final>` — standard | ||
| - `\e[ ? <params> <final>` — private (DECSET / DECRST family) | ||
| - `\e[ > <params> <final>` — secondary device attributes | ||
| - `\e[ ! ...` — soft terminal reset family | ||
|
|
||
| ## Cursor movement | ||
|
|
||
| | Sequence | Meaning | | ||
| |---|---| | ||
| | `\e[A`, `\e[<n>A` | Cursor up 1 or n rows | | ||
| | `\e[B`, `\e[<n>B` | Cursor down n rows | | ||
| | `\e[C`, `\e[<n>C` | Cursor right n columns | | ||
| | `\e[D`, `\e[<n>D` | Cursor left n columns | | ||
| | `\e[<r>;<c>H` | Absolute position (1-indexed) | | ||
| | `\e[H` | Position (1,1) — home | | ||
| | `\e[s` / `\e[u` | Save / restore cursor | | ||
| | `\e[6n` | Query cursor position (terminal responds `\e[<r>;<c>R`) | | ||
|
|
||
| **Cursor up beyond row 1 pegs at row 1.** It does NOT scroll history into view. Cursor up from row 1 = cursor stays at row 1. | ||
|
|
||
| **Absolute positioning** is what TUIs use for in-place redraws. The TUI's notion of "the card is at rows 5-15" depends on the row count never changing under it. If pear's viewport scrolls or buffer trims a row off, the TUI's `\e[5;1H` lands at a different visible row than it expected → stacked redraws. | ||
|
|
||
| ## Line / screen clearing | ||
|
|
||
| | Sequence | Meaning | | ||
| |---|---| | ||
| | `\e[K` | Clear from cursor to end of line | | ||
| | `\e[1K` | Clear from start of line to cursor | | ||
| | `\e[2K` | Clear entire line | | ||
| | `\e[J` | Clear from cursor to end of screen | | ||
| | `\e[1J` | Clear from start of screen to cursor | | ||
| | `\e[2J` | Clear entire screen | | ||
| | `\e[3J` | Clear scrollback (xterm extension) | | ||
|
|
||
| `\e[2J` does NOT reset the cursor. Many TUIs follow it with `\e[H` for the clear-and-home idiom. | ||
|
|
||
| ## Scroll region | ||
|
|
||
| `\e[<top>;<bottom>r` — set the scroll region to rows `top` through `bottom` (1-indexed). All subsequent scrolling is contained within this region. Used by TUIs to keep a status bar at the bottom while content scrolls above it. | ||
|
|
||
| ## DECSET / DECRST modes (private CSI ?) | ||
|
|
||
| The modes that change behavior xterm-wide. Format: `\e[?<n>h` to set, `\e[?<n>l` to reset. | ||
|
|
||
| | Mode | Effect | Bug class | | ||
| |---|---|---| | ||
| | `?25` | Cursor visibility (h = show, l = hide) | Hidden cursor on tab return = TUI forgot to re-show after losing focus | | ||
| | `?1000` | X10 mouse tracking | Mouse clicks emit as bytes to the PTY | | ||
| | `?1006` | SGR mouse encoding (modern) | Same | | ||
| | `?1004` | **Focus event reporting** | TUI receives `\e[I` / `\e[O` on focusin / focusout — **stacked redraws on `term.focus()` calls** | | ||
| | `?1049` | Alt-screen with state save | vim/htop entry/exit. Main buffer untouched. | | ||
| | `?2004` | Bracketed paste | Pasted text wrapped in `\e[200~` / `\e[201~` so TUI distinguishes from typed input | | ||
| | `?2026` | **Synchronized output mode** | TUI wraps a multi-write frame in begin/end markers; terminal defers paint until end marker. Avoids tearing on multi-chunk redraws. | | ||
| | `?7` | Auto-wrap mode | Off = cursor pegs at right edge, doesn't auto-wrap | | ||
|
|
||
| **?1004 + programmatic focus = stacked redraws.** This is the bug class behind "+1 card per tab switch back" when a TUI like Claude Code's Ink renderer is running. Fix is not to suppress the mode; fix is not to call `term.focus()` on every visibility change in the host. | ||
|
|
||
| **?1049 vs main buffer**: alt-screen is a clean canvas. The TUI draws once, exits, and the main buffer is exactly as it was. vim and htop are the canonical users. If you see "the TUI's output is in scrollback after I exited it", the TUI didn't use alt-screen — it drew directly into the main buffer. This is the failure mode behind Claude Code's tool-card history. | ||
|
|
||
| **?2026 sync mode**: codex-1's TUI uses this. Each redraw frame is bracketed: | ||
|
|
||
| ```text | ||
| \e[?2026h ← begin sync | ||
| \e[H ← home cursor | ||
| <card content> | ||
| \e[?2026l ← end sync, atomic paint | ||
| ``` | ||
|
|
||
| xterm.js supports it in recent versions. If you don't see card stacking on codex-1 but do on Claude Code, sync mode is one of the differences. | ||
|
|
||
| ## OSC (Operating System Command) | ||
|
|
||
| Terminated by `\x07` (BEL) or `\e\\` (ST). | ||
|
|
||
| | Sequence | Meaning | | ||
| |---|---| | ||
| | `\e]0;<title>\x07` | Set window title | | ||
| | `\e]8;;<url>\e\\<text>\e]8;;\e\\` | Hyperlink (clickable text) | | ||
| | `\e]10;<color>\x07` / `\e]11;...` | Set fg / bg color | | ||
| | `\e]52;c;<base64>\x07` | OSC 52 clipboard write — security-relevant | | ||
|
|
||
| ## DCS (Device Control String) | ||
|
|
||
| Terminated by `\e\\` (ST). | ||
|
|
||
| Most common: sixel graphics `\eP<params>q<data>\e\\`. Not all xterm builds support it. Image addon required. | ||
|
|
||
| ## Character sets | ||
|
|
||
| `\e(B` = US-ASCII (default). `\e(0` = DEC special graphics (box-drawing). Some legacy TUIs switch to the special set to draw `┌─┐│` style boxes; if you see weird non-ASCII characters in scrollback after a TUI exits without restoring `\e(B`, the set wasn't restored. | ||
|
|
||
| ## TUI redraw patterns | ||
|
|
||
| Three flavors, each with a different failure mode: | ||
|
|
||
| ### 1. Redraw-in-place via cursor positioning | ||
|
|
||
| The TUI emits a card, then later moves cursor back up and rewrites the same lines. Example: Claude Code's tool cards. | ||
|
|
||
| - **Works when**: the cursor's row in the buffer hasn't moved (no intervening scroll, no buffer trim). | ||
| - **Fails when**: viewport scrolled OR buffer trimmed OR the TUI's notion of row count is stale (no SIGWINCH on resize). | ||
| - **Symptom**: stacked duplicate cards in scrollback. | ||
|
|
||
| ### 2. Alt-screen full-redraw | ||
|
|
||
| The TUI enters `?1049h`, draws everything, exits on close. Example: vim, htop, less. | ||
|
|
||
| - **Works**: always cleanly. Main buffer preserved. | ||
| - **Fails**: rarely. Alt-screen state can leak if the TUI is killed without sending `?1049l` (resize the terminal, scrollback shows alt-screen contents instead of main). | ||
|
|
||
| ### 3. Synchronized output (?2026) | ||
|
|
||
| The TUI brackets each frame between `?2026h` / `?2026l`. Example: codex-1's TUI. | ||
|
|
||
| - **Works**: tear-free atomic frames. | ||
| - **Fails**: xterm versions without ?2026 support ignore the brackets and the frame content streams in normally — typically still works because the cursor-positioning model is the same as pattern 1. | ||
|
|
||
| ## How to read a chunk preview | ||
|
|
||
| When debugging a chunk stream, replace escapes with readable markers: | ||
|
|
||
| ```ts | ||
| function previewChunk(chunk: string): string { | ||
| return chunk.slice(0, 80) | ||
| .replace(/\n/g, '\\n') | ||
| .replace(/\r/g, '\\r') | ||
| .replace(/\x1b/g, '\\e') | ||
| } | ||
| ``` | ||
|
|
||
| A chunk like `"\\e[H\\e[5;1H\\e[K"` reads as: "home cursor, move to row 5 col 1, clear to end of line". Now you can match it against the bug class catalogue. | ||
|
|
||
| ## Companion reading | ||
|
|
||
| - `pty-broker-pipeline.md` — how chunks arrive | ||
| - `bug-class-triage.md` — symptoms ↔ specific sequences |
109 changes: 109 additions & 0 deletions
109
.agentworkforce/workforce/personas/__assets/terminal-renderer/bug-class-triage.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| --- | ||
| name: bug-class-triage | ||
| description: Use BEFORE reading source when a terminal rendering symptom is reported — maps the symptom (duplicate text, ghosting, smeared glyphs, cursor drift, blank canvas) by timing and shape to the most-likely cause and the file to open first. | ||
| --- | ||
|
|
||
| # renderer-bug-class-triage | ||
|
|
||
| The lookup table from observed symptom to specific code path. Built from the PR #158 catalogue of fixes and the bugs found while shipping it. Use this **before** reading source — it disambiguates which file to open. | ||
|
|
||
| ## Triage axis 1: timing | ||
|
|
||
| | Timing pattern | Implies bug class | | ||
| |---|---| | ||
| | Immediately on first attach | Snapshot vs replay race, font-settle race | | ||
| | Only after N tab switches | Side-effect of visibility change OR listener leak | | ||
| | Only after window cmd-tab | Window-focus handler programmatic focus | | ||
| | Only at first redraw of a TUI | Initial-mount focus cascade, no SIGWINCH bounce | | ||
| | Only under heavy streaming | Trim cap accounting, predictive echo backpressure | | ||
| | After resize | Spurious SIGWINCH, font measure / metric drift | | ||
| | After theme change | WebGL glyph atlas not invalidated | | ||
| | Only on display:none return | Stale WebGL frame, no `term.refresh()` | | ||
|
|
||
| ## Triage axis 2: symptom shape | ||
|
|
||
| ### Duplicate text | ||
|
|
||
| | Specific shape | Most likely cause | | ||
| |---|---| | ||
| | Exact byte-for-byte duplicate immediately after attach | **Snapshot-vs-replay race.** The snapshot contains chunks 1-50; the buffer also contains chunks 1-50; the subscriber sliced from 0 and replayed them. Fix: `flushPtyChunksNow(key)` + `writtenChunks = buffer.length` before subscribe. | | ||
| | Duplicate only after N tab switches; multiplier increases with N | **Listener leak.** Each tab switch added a new subscriber. Look for `subscribePtyBuffer` calls without paired `unsub`. | | ||
| | +1 identical Claude Code tool-card per tab switch back | **Focus events to a `?1004` TUI.** The visibility-effect fires `term.focus()` → focusin → `\e[I` → TUI redraws. Drop the auto-focus on visibility change. | | ||
| | +1 card per cmd-tab back to pear | **Window-focus handler** fires the same `term.focus()`. Drop it for the same reason. | | ||
| | ~5 cards on first agent spawn | **Initial-mount focus cascade.** Multiple setTimeouts firing `focusTerminal` at 0/50/150/300ms. Each emits `\e[I`. Reduce to one. | | ||
| | Identical message twice in chat under load | **Optimistic-vs-canonical chat dedup gap.** User's optimistic local-UUID record and the broker's canonical event_id record both survived id-based dedup. Fix: scope `local: true` flag + check content-window match for the canonical-of-optimistic case. | | ||
| | Same agent message twice in chat under load | **Cross-stream broker replay.** Same logical message delivered via `relay_inbound` AND `reconcileMessages` snapshot with mismatched event_ids. Fix: `isDuplicateAgentEcho` content-window guard with strict project equality. | | ||
|
|
||
| ### Ghosting / scroll trail | ||
|
|
||
| | Specific shape | Most likely cause | | ||
| |---|---| | ||
| | Trail when sliding split pages | **Canvas under CSS transform.** `translateX` animation over a WebGL canvas produces ghost composites. Use `display: none/block` instead, paired with `runtime.refreshOnShow()`. | | ||
| | Smear during pane drag | **Backdrop-filter blur stacked over hot path.** Remove blur from the hot-path surfaces (terminal/chat). Keep blur only on transient dialogs. | | ||
| | Stale frame on tab return | **WebGL stale paint.** `display: none` skipped GPU paints; on return the canvas shows the last paint. Call `term.refresh(0, term.rows - 1)` on visibility flip. | | ||
|
|
||
| ### Smeared / drifting glyphs | ||
|
|
||
| | Specific shape | Cause | | ||
| |---|---| | ||
| | Glyphs slightly misaligned on initial mount; fix themselves at next resize | **Font measurement racing font load.** `term.open()` measured fallback font; `document.fonts.load()` resolved later. Fix: `await document.fonts.load('13px "JetBrains Mono"')` before fit + refresh. | | ||
| | Glyphs paint with previous colors after theme change | **WebGL glyph atlas stale.** Theme swap didn't invalidate the texture atlas. Call `webglAddon.clearTextureAtlas()` (where supported) or accept stale glyphs until next major repaint. | | ||
| | Single frame of mis-sized glyphs on initial mount | **Font-settle race against WebGL load.** `loadWebgl` scheduled before `awaitFontSettle` resolved. Cosmetic, not load-bearing. Resolves on next refit. | | ||
|
|
||
| ### Cursor drift / TUI breaks mid-stream | ||
|
|
||
| | Shape | Cause | | ||
| |---|---| | ||
| | Cursor lands in the wrong row after a CSI cursor-up | **TUI's expected row count differs from xterm's actual row count.** Resize wasn't propagated as SIGWINCH; TUI thinks it's 60 rows tall when it's actually 30. Fix: bounce SIGWINCH on initial attach. | | ||
| | Card appears at wrong location | Same root cause — absolute positioning landed at a stale row. | | ||
| | Card content sliced or partial | Predictive-echo write order race OR a `\e[K` after the cursor-up cleared too much OR alt-screen state leaked. | | ||
|
|
||
| ### Visible duplicates in chat | ||
|
|
||
| | Shape | Cause | | ||
| |---|---| | ||
| | User sees their own message twice within ~10s | **Optimistic-vs-canonical.** `addHumanMessage` appended the local record; `relay_inbound` then appended the canonical without realizing it was the echo. Fix: `local: true` flag scoping + canonical-of-optimistic match. | | ||
| | User sees their identical "ok" send disappear when they double-send | **Over-eager echo dedup.** The guard collapsed two distinct user messages thinking the second was a canonical echo. Fix: drop content-window check from `addHumanMessage`; only apply on the canonical reception side. | | ||
| | Two distinct agents in the same channel can't send identical content | **Agent dedup false-positive on `from` not strict enough.** The guard didn't differentiate sender. Fix: include `from` in the content-window predicate. | | ||
|
|
||
| ### Visible duplicates in terminal (NOT chunk-level) | ||
|
|
||
| | Shape | Cause | | ||
| |---|---| | ||
| | Same multi-line tool card stacked N times in scrollback | Cursor-positioning redraw failed (see "TUI breaks mid-stream"). Not a chunk-write bug — instrument to confirm: each chunk arrives once, each writeChunks fires once. Then look at the TUI's redraw pattern. | | ||
| | Streaming output renders twice per chunk | **Predictive echo write path doubled with direct write.** `writeChunks` should EITHER call `predictiveEcho.onServerOutput(chunk)` OR `liveTerm.write(chunk)`, never both. | | ||
|
|
||
| ### Blank canvas / black screen | ||
|
|
||
| | Shape | Cause | | ||
| |---|---| | ||
| | Terminal pane is blank on first attach | `term.open()` was called with a zero-size container. Defer init to `requestAnimationFrame` and retry until `hasLayout(container)` returns true. | | ||
| | Split-page terminal blank when initially hidden | Same — the page mounted with `display: none`, zero-size, init bailed, never retried. Schedule `initIfReady` rAF retry on no-layout. | | ||
| | Terminal blanks after WebGL context loss | The `onContextLoss` handler dropped WebGL but didn't fall back to DOM. Set `suggestedRenderer = 'dom'` and dispose the failed addon. | | ||
|
|
||
| ## How to use this table | ||
|
|
||
| 1. Read the symptom literally. "+1 card per tab switch back" not "duplication." | ||
| 2. Find the matching row above. | ||
| 3. Open the named file/code path BEFORE forming a fix hypothesis. | ||
| 4. If the symptom doesn't match any row → instrument before guessing (see `fix-discipline.md`). | ||
|
|
||
| ## When the symptom is reported vaguely | ||
|
|
||
| The single most useful question to ask the user: | ||
|
|
||
| > Does this happen on first attach, or only after a specific action (tab switch, cmd-tab, resize, theme change)? | ||
|
|
||
| This disambiguates 80% of the bug-class space in one question. | ||
|
|
||
| If still ambiguous, the second question: | ||
|
|
||
| > When you scroll back, are the duplicates exact byte-for-byte (suggesting chunk replay) or do they look like rendered cards that should have been redrawn in place (suggesting cursor positioning failure)? | ||
|
|
||
| Now you can pick the right file to open. | ||
|
|
||
| ## Companion reading | ||
|
|
||
| - `pty-broker-pipeline.md` — the chunk path | ||
| - `ansi-vt-sequences.md` — what cursor positioning sequences look like | ||
| - `fix-discipline.md` — what to do when the table doesn't match | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.