position-cursor runs every frame from main loop (after sleep 0.1),
drawing █ when cursor-visible-p returns T, space when NIL.
This creates a true 2Hz blink that toggles independently of keypresses.
Terminal cursor also set to blinking block as fallback.
Replaced software cursor (draw-text █ every frame) with native terminal
cursor (position-cursor using cursor-move + cursor-style). Terminal handles
blinking natively at 500ms — no redraw needed for cursor updates.
- position-cursor: computed input insertion point from state, calls
cursor-move + cursor-style (:block :blink t) + cursor-show.
- Called from main loop every frame after (sleep 0.1), outside
redraw's begin-sync/end-sync. No flicker.
view-input word-wraps input at prompt-w, expanding the grey panel
upward as needed. Uses software cursor (█) in :input-fg blinking
at 2Hz via get-internal-real-time.
view-chat max-lines adapts to variable panel height via input-panel-top.
Removed terminal cursor (position-cursor, cursor-show, cursor-style).
Dialog minibuffer top now computed from input-panel-top.
Clean implementation: spacer inserted in the rendering loop as an
(incf y) between message blocks, tracked in scroll-fitting loop
via spacer variable. No data structure changes.
Also: fixed premature let close in spacer binding, fixed view-input
closing paren count, and re-applied speaker alignment fixes lost in
revert.
- Theme: added :agent-border, :thinking-bg, :symbolic-border to all 13
presets with theme-load fallback for saved themes.
- Agent output now draws │ with :agent-border color (muted tan).
- Neuro-thinking (streaming): draw-rect at column 0 with :thinking-bg
(dark grey block) instead of a grey │ character. No border text.
- Gate traces: │ with :symbolic-border (was ╎ with :dim).
- Tool calls: │ with tool status color (was ╎).
- Removed > prompt prefix from input line.
- Added position-cursor function: blinking block cursor at insertion
point, called every frame from the main loop after sleep.
- redraw: always draws all three views (status/chat/input) when any
dirty flag is set. Dirty flags only gate frame rendering, not
which parts render. Fixes disappearing input/history.
- Added :bg-input to all 13 presets with #2e2e2e (dark) / #d4d4d4
(light-amber). theme-load fills missing keys from current preset
defaults for backward compatibility.
- Removed unused *sidebar-panels* defvar and obsolete contract docs.
- Renamed dim-bg → dim-fg (foreground color, not background).
- All draw-text calls in sidebar and dialog minibuffer now pass
explicit bg-panel, preventing background leaks.
- render-styled (markdown renderer) passes explicit (theme-color :bg).
- Fix h shadowing in view-chat scroll loop (h → mh).
- Theme: near-black (#0a0a0a) backgrounds, dark-grey panels (#141414),
warm amber (#fab283) accent only. New keys: :bg, :bg-panel, :bg-element,
:text-muted. All 13 presets updated.
- Messages: No background fills (sit on global black). User messages get
amber left border (│). Agent response has no border (invisible).
Streaming agent messages get grey left border. Gate traces and tool
calls use grey ╎ prefix. No label lines, no time separators.
- Sidebar: :sidebar-mode with :auto/:visible/:hidden. Auto-shows at >120
cols (opencode-style). Width 42 with version + connection dot footer.
- Input: 2-char hpad on each side. Grey panel (2 rows: separator +
prompt). Hint right-aligned at bottom on black.
- Status bar: empty (clean black line).
- cl-tty backend: draw-text, draw-rect, draw-link, draw-border now use
\e[22;23;24;25;27m (style-only reset) instead of \e[0m (full reset),
preserving foreground/background across draw calls.
- Fix: all sidebar text draws pass explicit bg-panel background.
- Fix: hint at h-1 passes explicit (theme-color :bg).
- Fix: sidebar bottom row uses draw-text (no \n) to prevent scroll at h-1.
- Remove 'code' variable binding (redundant with b). esc-seq now
starts with (and b (= b 27) ...) so when b is nil (timeout), the
and short-circuits before (= b 27) can error with 'NIL is not
of the type NUMBER'.
- Swank prints to *standard-output*, not *error-output*. Bind both
to string output streams to prevent ';; Swank started' leak.
- (= b2 91) errors when b2 is nil (read-raw-byte timeout). Add
(and b2 (= b2 91)) to guard against nil.
- Swank writes ';; Swark started at port:...' to *standard-output*,
not *error-output*. Bind *standard-output* to string stream too.
cl-tty.input:read-event has bugs (CSI parser timeout causes :escape
to be returned for arrow keys). Replace with direct read-raw-byte
calls that are proven to work for CSI sequences. The inline detection:
- Read first byte with 100ms timeout
- If ESC (27), read two more bytes with 150ms timeout each
- Map 65→:up, 66→:down, 67→:right, 68→:left, etc.
- Other bytes converted via the same cond chain as before
Also re-add resize check (was handled by read-event).
Use handler-case around the reader to prevent any reader errors
from crashing the TUI. Re-add Swank *error-output* redirect.
Replace the inline raw byte reader + CSI detection with
cl-tty.input:read-event which uses read-raw-byte (direct fd reads)
and properly parses CSI escape sequences, UTF-8, mouse events, etc.
Also fix: remove extra ) in (t nil) clause that was prematurely
closing the let* binding, causing the if form to receive 4 args.
- Swank: bind *error-output* to string stream to prevent 'Swank started
at port: 4006.' from leaking to terminal on exit
- CSI detection: wrap inner dotimes in (progn ... t) so the and form
doesn't short-circuit (dotimes returns nil, breaking the chain)
- Add debug add-msg for CSI detection results
The main loop's closing paren was missing — (sleep 0.1)) only closed
sleep and the minibuffer let, but NOT the loop itself. The next form
(progn (disconnect-daemon)) was INSIDE the loop body, called on every
iteration. On first call it added '* Disconnected *' and cleared the
daemon stream, making the TUI permanently disconnected.
Fix: add ) to close the loop. Also:
- Connect-daemon runs synchronously BEFORE with-terminal (3 ports, 6s
max). If daemon is already running, the TUI starts connected.
- If sync connect fails, background thread retries every 5 seconds.
- start-daemon in background (no blocking wait for daemon startup),
so TUI appears immediately.
set -e on line 2 causes the bash script to exit immediately when sbcl
returns non-zero, before the stty icanon echo ixon restore runs.
Add trap cleanup EXIT to guarantee terminal restore on any exit path.
- Remove handler-case + sb-ext:with-timeout 0.1 pattern entirely.
sb-ext:timeout is a condition class, not a recognized type in the
compilation environment, causing runtime 'undefined function' crash.
- Replace with dotimes 10 * (listen *standard-input*) + sleep 0.01
polling loop. Same 0.1s timeout, no condition type dependencies.
- Also fix handler-bind → handler-case in tui-load.lisp so the stack
unwinds properly (running with-terminal's shutdown-backend cleanup)
before the crash handler runs, restoring terminal to normal state.
- Fix paren imbalance (off by 1) in the new listen-based reader code.
Major bug: defun disconnect-daemon in channel-tui-main.org was missing
its closing paren. Every form after disconnect-daemon (tui-main, tests,
etc.) was inside the unclosed defun, causing 'end of file' compile errors.
Adding the missing ) fixed all compilation errors.
Also revert handler-case change: keep sb-ext:timeout condition type.
- start-daemon: handle ADDRESS-IN-USE-ERROR by trying ports 9105-9115
instead of crashing. Logs which port is used.
- Add *daemon-port* defvar to track actual listening port
- main: wrap start-daemon in handler-case so the daemon doesn't
crash if all ports are in use
- connect-daemon (TUI): try ports 9105-9115 with 2s timeout each
instead of retrying the same port 3 times
- Add debug messages for connection success and disconnection timestamp
- Add CSI escape sequence detection: when ESC (27) is received, poll
for up to 20ms for the next bytes to detect arrow/home/end keys
- Use listen+read-char polling (not nested with-timeout) to reliably
collect multi-byte sequences while keeping standalone ESC responsive
- Fix paren balance in main code block (2 extra opens from nested
esc-seq forms needed matching closes)
- Revert async connect-daemon thread (bt:make-thread unreliable — errors
in the thread cause silent failure, no connection, no error message)
- Restore blocking connect-daemon before with-terminal (original pattern
that was working)
- Revert /reconnect to synchronous call
- Remove stale async thread code and error messages
Cat subprocess (uiop:launch-program '("cat") :input :interactive) was
unreliable — the process would exit immediately in some environments,
breaking ALL keyboard input. Root cause: uiop's :input :interactive mode
opened /dev/tty which failed under specific process-group configurations.
Replace with direct read-char-no-hang on *standard-input*. The bash script
sets stty -icanon -echo -ixon before launching sbcl, so SBCL's stdin is
already in raw mode. No subprocess needed.
Also fixed pre-existing paren imbalance in tui-main (2 extra opens).
- Wrap cat with stdbuf -o0 so keystrokes aren't stuck in cat's 4096-byte
pipe buffer — text input was invisible until buffer filled
- Dialog filter: (characterp ch) rejects integer char codes from raw event
dispatch. Accept integerp in range 32-126 and convert via code-char
- Remove initial render (backend-clear + view calls) before main loop.
Dirty flags already trigger a full sync-wrapped redraw in the first
iteration, eliminating the pre-loop clear flash
Issue 1 — flickering during typing/updating:
- Wrap every frame render in DECICM sync (begin-sync/end-sync) so the
terminal defers rendering until the entire frame is written
- Replace backend-clear (ESC[2J full clear) with draw-rect background
fill — eliminates visible blank frame between redraws
- These two changes together eliminate all visible tearing/flicker
Issue 2 — bottom-anchored minibuffer (Emacs-style):
- Replace centered overlay dialog with bottom-anchored minibuffer
that expands upward from the input line
- Unified command menu: Ctrl+P and / both open the same menu with
all 35+ commands (slash + daemon), dispatch by value type
- Filter prompt at h-3 (same position as normal input),
options listed above, grows up to 15 lines
- No full-screen dim backdrop — just clear the minibuffer area
Issue 3 — color schemes:
- Add 5 new presets: catppuccin, tokyonight, dracula, gemini, mono
- Total: 13 presets (up from 8)
- Update /theme completion list and help text
Also fixed: pre-existing unbalanced paren in tui-main (missing close)
passepartout script:
- Add (uiop:quit 0) after tui-main so SBCL exits on Ctrl+Q
- Remove exec to allow stty restore after sbcl subprocess
- Restore icanon echo ixon after TUI exits (terminal stuck raw)
channel-tui-view.org:
- Remove unused fb/h vars from test-sidebar-not-shown-narrow
- Add (declare (ignore w)) to render-styled
- Qualify theme-color as passepartout.channel-tui:theme-color
(render-styled is in :passepartout package)
- Remove dead :url clause from pick in parse-markdown-spans
(URLs handled by dedicated branch, not via pick)
- Update literate prose for all changes
Replaced (let* ((cat-proc ...) (tty-in ...)) ...) with global
special variables *cat-proc* and *tty-in* with defvar declarations.
The let* caused 'unbound variable' errors on Ctrl+Q because the
lexical scope didn't extend to terminate-process. Global vars have
indefinite scope and work reliably regardless of paren nesting.
- Added -ixon to stty so Ctrl+Q (XON byte) isn't swallowed by the
terminal driver and reaches the TUI as :CTRL-Q
- view-input now truncates the prompt (> prefix + visible text) to
chat-w - 2 characters, and the hint to chat-w characters, so
neither extends into the sidebar area
Added chat-w = w - sidebar_width calculation in view-status and
view-input, matching view-chat's existing approach. Also added
chat-w for the separator line drawing in tui-main. This prevents
the prompt, separator, hint, and status bar from extending into
the sidebar area when it's visible.
uiop:run-program inside SBCL can't access the terminal, so stty
calls from within Lisp fail silently. By running 'stty -icanon -echo'
in the bash script before exec sbcl, the terminal is already in
character-at-a-time mode when the TUI starts, and Ctrl+P/B keys
arrive as individual bytes through the cat pipe.
- Sidebar threshold lowered from 120 to 60 so it works on 83-col
terminals
- Agent response text is now word-wrapped through cl-tty.box:word-wrap
after markdown rendering, preventing text from bleeding past the
terminal edge
- Added chat-w = w - sidebar-width in view-chat for all width
calculations (word-wrap, padding, borders) so text doesn't
bleed into the sidebar area
- Changed Ctrl+B dirty flags from (list t t nil) to (list t t t)
so input view also redraws, fixing the toggle-not-turning-off issue
The key event dispatch was lost during git restores, causing all
:key events (Ctrl+P, Ctrl+B, Enter, etc.) to fall through to
on-key which only handles :enter, :escape, and character insertion.
Added back the case ch with :CTRL-Q/:CTRL-P/:CTRL-B/:CTRL-L
branches and full dialog key routing.
Without -icanon, the terminal driver buffers all input until Enter,
so Ctrl+P/B never arrive as individual key events. With -icanon,
cat reads bytes immediately and pipes them to SBCL. SBCL reads from
the pipe, not fd 0, so there's no fd 0 read block issue.
SBCL strips COLUMNS and LINES from the process environment.
Use non-standard names that SBCL doesn't filter. The values
are captured from stty size before SBCL starts.
backend-size can return nil for height (especially when the /dev/tty
ioctl fallback hasn't been compiled in yet). view functions had nil
guards but the direct (- h 4) calls in tui-main's initial render
crashed before reaching them.
cat-proc and tty-in were defined by let* but the let* closed at
sleep's third ), putting them out of scope for terminate-process
and read-char. Restructured closing parens so the let* body wraps
the full loop + cleanup.
Every passepartout tui run now deletes stale cl-tty fasls,
ensuring the latest backend-size fixes (ioctl fd0, env vars,
stty size, tput) are always compiled in.
bash sets COLUMNS and LINES but doesn't export them to
subprocesses. Without export, SBCL's posix-getenv returns nil
and the terminal size fallback fails. Add explicit export
before the exec sbcl line.