| Commit message (Collapse) | Author | Age |
| ... | |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The counter increments once per user text block, so what it counts is
exactly a turn in the ordinary prompt -> response sense. It also matches
the vocabulary pi already uses internally (turnBudget, maxTurns), so the
label no longer invents a word for a thing that has one.
"round" was the other candidate and is worse: it implies a symmetric game
with fixed phases, and it collides with the rounded-border language used
throughout these widgets. "prompt #N" is the most literal reading of the
counter, but it undersells the box -- the spinner and timer measure the
whole agent run, not just the prompt.
No layout change was needed. The label is 7 columns narrower, and the top
border derives its dash fill from visibleWidth(leftLabel) rather than any
hardcoded width, so the frame absorbs it.
The cross-reference in slop-tool-boxes.ts ships in 36e422b, whose hunk it
shares.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Two changes that both come back to "what should an in-flight box look like",
plus the icon that was left out of it.
PULSE: single hue, brightness only. The old pulse interpolated a 4-color
synthwave ring (amber -> pink -> violet -> cyan) over 4s, which at that
period reads as a slow hue *drift*, not a pulse -- you notice the color has
changed, never that it is beating. Now it is one hue (green, i.e. "in
progress") modulated dim <-> bright: #0d4023 to #3bff7a, via a raised
cosine so there is no direction-reversal snap at the turn, then gamma ^1.7
to bias it toward the dim end. The border rests dark and spikes bright,
which is what makes it read as a heartbeat.
CYCLE_MS 4000 -> 800 (1.25Hz), and 800 is now the only pulse-speed knob.
There is a hard floor of ~400ms documented at the constant: a brightness
pulse IS a flicker, and 3-30Hz is the photosensitive-seizure band -- this
repo has already shipped one seizure-grade flicker bug (NOTES.md), so the
constant carries the warning rather than relying on memory. The upper bound
is render cadence, not taste: the pulse never forces a repaint, it samples
wall-clock time on renders that were happening anyway (~10fps while the
spinner runs), so 800ms is ~8 samples per beat. Much faster and consecutive
renders land on distant brightnesses -- the beat degrades into jitter.
TOP BORDER: freeze only when we actually must. 0d671ce stopped the
full-redraw flicker by freezing the call slot's top border for the whole
time the result streams. That was broader than the bug requires. The bug is
specifically "recolor a line that sits ABOVE the viewport": tui.js does
`if (firstChanged < prevViewportTop) fullRender(true)`, so an off-screen
recolor repaints everything, every tick. While the box still fits on
screen, firstChanged lands inside the viewport and tui diffs from there
down -- cheap. So canPulseTopBorder() compares callH + resultH against
rows - RESERVED_ROWS and only freezes past that, which gives short tool
calls their top and bottom borders beating in unison again.
resultH is one frame stale (the call slot renders before the result slot).
Harmless, because a box only grows: worst case is one extra full redraw on
the frame it crosses the threshold, and then it is frozen. RESERVED_ROWS
(14, for input box + footer + topbar) is deliberately generous in the same
direction -- over-estimating freezes a box that could have animated, which
nobody can see; under-estimating buys back the flicker.
A frozen border now holds HOLD_PAINT, a fixed mid-beat green (pulseRGB(0.5)),
instead of whatever phase the pulse happened to be at when content last
changed. Same number of writes, but it looks deliberately held rather than
randomly stuck. Resuming the pulse invalidates callActiveW so the frozen
lines cannot be served stale.
ICON: the pending ◉ takes the border's paint, so it beats with the frame
instead of sitting there as a static amber dot. This is why the label is
built inside build() now -- it has to be rebuilt per paint. It costs
nothing extra: the icon shares the top border's line, which is already
being rewritten.
Also picks up the sl0peration -> turn doc-comment rename in the header,
which is unified into the same hunk as the pulse comment above it.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Rhymes with the ❯ chip user-prompt-frame stamps on the message once it is
submitted, so what you type and what it becomes read as the same object:
❯ your input text
that wraps here
──────────────────────────────────────────────
The file's own comment warned that content lines must pass through
verbatim or the cursor desyncs. That is true but fixable: the TUI derives
the cursor column as visibleWidth(line.slice(0, markerIndex)) from the
FINAL rendered line, so a prefix inserted BEFORE the marker shifts the
reported column by exactly its own width -- which is where the text moved
to. Verified against native: the same text reports col 11 unpatched and
col 13 patched, and empty input parks the cursor at col 2, against the
glyph. Nothing may ever be inserted after the marker.
Layout now runs at (width - INDENT_W) instead of the full width, otherwise
the editor wraps two columns too late and the indent pushes every line past
the terminal edge. The rules it draws at that width are still discarded and
rebuilt at full width.
The glyph is suppressed when the input is scrolled: the first VISIBLE line
is then not the first line of the prompt, so the glyph would lie about
where the input begins. A scroll indicator in the top rule is exactly that
condition.
Glyph uses the instance's own borderColor, so it keeps reflecting bash-mode
and thinking-level like the rule does. The slash/autocomplete menu is left
at column 0 (INDENT_MENU): it draws full-width selection bars that would
otherwise sit offset against the rule above them.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The header was lost in 77f1d64: dropping the side rails also dropped
frameTop() from the return, leaving a bare closing rule under the message.
A rule *underneath* reads as an opener for the reply as much as a
terminator for the prompt, and it shared its glyph vocabulary with every
other box, so prompts were distinguished only by hue.
Replace it with a reverse-video chip and a left gutter:
█ ❯ PROMPT ▌──────────────────────────────────
▌ the prompt text
▌ wrapped continuation
The chip is the only filled element in the chrome and the rail the only
vertical (rails were removed everywhere else), so prompts are now
identifiable at a glance without adding glyphs to the rest of the UI.
The left cap is a FULL block, not ▐: the rail fills the left half of
column 0, so a right-half cap would sit in the opposite half of the same
cell and read as detached.
No closing rule; the reply terminates the block.
The native component wraps text in Box(pad, 1, bg), i.e. a blank padded
line above and below with OSC 133 markers on the first and last line.
paddingY is hardcoded and not configurable, so strip both pad lines and
re-home the markers: ZONE_START onto the header, ZONE_END+ZONE_FINAL
APPENDED to the last text line (prefixing would put the final line of the
prompt outside the semantic zone). Terminal prompt-jumping keeps working.
Note the OSC payload is not numeric-only -- pi emits 133;A / 133;B / 133;C
-- so the matcher runs to the terminator rather than a [0-9;] class. An
earlier numeric-only class silently matched nothing and left the pad lines
in place.
No timestamp: UserMessageComponent receives only `text`, and stamping at
first render would relabel every restored message with the restore time
under `pi -c`.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
The input box now renders as bare text: no top header (the "╭─ ❯ ───╮"
rule is dropped), no side rails, and the bottom is a plain edge-to-edge
rule rather than a ├──┤ divider, so nothing around the input reads as a
box. Content is laid out at the real full width and passed through
verbatim, which keeps the cursor/IME marker exactly where the editor put
it.
Scroll indicators ("↑ N more") still render, since they carry
information rather than chrome; set KEEP_SCROLL_INDICATOR to false to
drop those too. Removed the now-dead corner constants and railWrap, and
renamed buildBorder to buildRule to match what it emits.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
We run the agent inside a zellij fork that already draws a border around
the pane, so per-box verticals were redundant chrome. Body lines are now
indented into the box interior instead of railed, and the top/bottom
rules use straight end caps (a corner would curve toward a vertical that
is no longer there). Each file keeps the original glyphs documented so
the rounded box can be restored by flipping the cap constants back.
Backported from the installed set in ~/.pi/agent/extensions, which had
drifted ahead of this repo.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
An in-flight tool box taller than the viewport (top scrolled off-screen)
flickered violently the whole time it was active. Cause: the working spinner
(and pi's default spinner) drive ui.requestRender() at ~10-16fps, and the
active call slot recomputed a wall-clock pulse color every render, recoloring
the TOP border. tui.js falls back to a full-screen redraw when the first
changed line is above the viewport (firstChanged < prevViewportTop), so every
spinner tick = one full clear+repaint.
Fix: once the result slot is streaming (box can be tall), freeze the call
slot pulse color, caching built lines per (width, inner). The header/command
is static, so the top border is byte-identical between render ticks and never
triggers an above-viewport change. The pre-output phase (box still fully
visible) keeps the live pulse + ticking runtime; the result slot still pulses
its in-viewport bottom border. NOTES.md documents this as flicker #4.
Confirmed by repro: massive flicker with the fix reverted, calm with it in.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Replace pi's static "Working..." during streaming with a random loud
spinner per agent run and a rotating pool of goofy sl0p-swag status
messages ("warming up the h4x...", "computering extremely hard...",
"0day o'clock...", ...).
All via the official UI hooks: setWorkingIndicator (custom frames rendered
verbatim, colors baked in) + setWorkingMessage, both updating the live
indicator. Pick on agent_start, rotate the message (and sometimes swap the
spinner) on a ~2.2s timer, restore defaults on agent_settled/shutdown.
No-op without a TUI.
|
| |
|
|
|
| |
Drop the parallel audit-fanout subagent extension from the repo and the
global extensions dir.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Add sixel-images.ts (WIP): renders inline images as sixel via chafa by
forcing image capability on under tmux and patching pi-tui Image.render.
It works end-to-end (sixel draws inside the read box, width-guard bypassed
via an empty Kitty APC marker) but has rough edges (double-render, spacing),
so it is parked for now.
install.sh and dev.sh now carry an EXCLUDE list so this extension is neither
installed globally nor loaded in the isolated dev session. The slop-tool-boxes
read-image injection that paired with it was reverted separately.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Add sl0p-footer.ts: replace the built-in 2-3 line footer with one dense
single line via ctx.ui.setFooter — ↑/↓ tokens, context %/window (pressure
colored), cwd (+git branch), and model + thinking effort right-aligned.
Data via the live extension context (sessionManager/model/getContextUsage)
and footerData; repaints on branch/model/thinking changes.
Make the footer look attached to the input box:
- input-frame.ts now ends its box in a divider (├──┤) instead of a closing
corner, so the footer reads as a continuation.
- the footer wraps its line in matching side rails + the real closing bottom
(╰──╯), content inset to align with the input.
- footer frame is colored with theme.getThinkingBorderColor(level), tracked
via thinking_level_select (seeded from the branch/model default), so the
seam matches the input box border at every thinking level. Bash-mode border
color is the one unmirrored case (editor state not exposed to extensions).
Two stacked components styled to look like one box — no cross-component
theme/state sharing, and the autocomplete menu is left to render between them.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
pi's input area is just two full-width horizontal rules (no side rails)
drawn by the pi-tui Editor. Add input-frame.ts, which patches
CustomEditor.prototype.render (the only Editor subclass, and the class the
main input uses, so extension/selector editors keep native chrome) to lay
content out at the inner width and wrap it in a rounded, side-railed box:
╭─ ❯ ───────╮
│ input ... │
╰───────────╯
- content laid out at width-4 so word-wrap / lastWidth / cursor nav match
- cursor/IME marker stays aligned: the TUI derives the column from
visibleWidth(textBeforeMarker), and the uniform "│ " prefix shifts it by
the same 2 cols as the visible cursor; content lines are padded, never
truncated, so the marker is never dropped
- scroll indicators keep their "↑/↓ N more" text with rounded end caps
- autocomplete/slash menu (lines after the bottom rule) left as-is, indented
to align under the box interior
- reuses the instance borderColor, so bash-mode / thinking-level colors carry
- idempotent/reload-safe, feature-detected, falls back to native render on
any error, restores original on shutdown
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Replace the post-hoc applyDiffBackgrounds hack (which recolored the
native flat green/red diff lines) with a real diff renderer built from
the raw diff string (result.details.diff, and the live preview.diff on
the call component).
Each code line is syntax-highlighted via highlightCode/getLanguageFromPath,
drawn with a `± lineNum` gutter, and add/remove rows get a subtle dark
bg band across the full width. The band is re-asserted after fg resets so
syntax colors do not punch holes in it. Context rows are highlighted with
no band; skipped-context collapses to a dim ellipsis.
Applies to both slots: the live edit preview (renderCall) busts its width
cache on preview identity change, and the settled result (renderResult)
falls through to native output when there is no diff (e.g. errors).
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Track per-toolCallId execution timing via tool_execution_start/end and
render the elapsed duration into the bottom border of bash rows, aligned
to the right and kept one dash clear of the corner.
Respects the no-forced-repaint invariant: the live timer is
`end ?? Date.now()`, so it only advances on renders that already happen
(bash output streaming re-renders the result slot border); the final
duration is stamped once the row settles. Muted time text sits outside
the pulsing dash paint. Gated to bash via RUNTIME_TOOLS (extensible).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Fresh-context 'pi --print --mode json' child per audit task, run in parallel,
read-only (--exclude-tools edit,write), lean (-ne --no-session). Auditors
inherit the parent session model+thinking by default; override via model param.
Registers both an 'audit' tool (LLM-callable) and a /audit command.
Key fixes from live testing:
- stdio:['ignore','pipe','pipe'] — pi --print blocks on an open non-TTY stdin
pipe, which hung every child until timeout. Closing stdin fixes it.
- Incremental JSON-stream parsing surfaces real per-auditor activity
(reading X / grep Y / composing Nc) plus a 1fps elapsed heartbeat via
ctx.ui.setStatus, so a slow model never looks hung (the reason ESC got hit).
Raw reports written to .pi-audit/<ts>/; parent synthesizes.
|
| |
|
|
|
|
|
|
|
| |
Bring back body side rails with an active/settled distinction:
- in-flight: gray dotted (┊) rails + pulsing top/bottom borders
- finalized: solid status-colored (│) rails + solid borders
Rails are a STATIC per-render color (never carry the pulse), so streaming
output can't recolor existing body lines -> stays flicker-safe.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
flicker)
Third flicker bug: a taller-than-screen bash box flickered because the 10fps
timer forced a full updateDisplay() repaint on top of the stream's own
re-renders. Root fix: NEVER force a repaint.
- Pulse color is now a pure function of wall-clock time (Date.now() % CYCLE_MS);
no tick counter, no setInterval. Any render shows the current color.
- The pulse advances only on renders that already happen: streaming output
('the stream hydrates the pulse', irregular cadence) or start/end nudges.
- Silent long-running tools hold a static active color (flicker-free) rather
than animating.
- Also drop body side rails entirely (open-sided box); only top/bottom pulse.
Self-test: restored rows never pulse, active top border advances with time,
settles solid on end.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Two bugs from the pending-tool border pulse:
1. Streaming flicker: pulsing the side rails recolored every body line each
frame, forcing a full-box repaint per streamed token. Now only top/bottom
borders pulse; rails/body use a static status color.
2. Startup render storm (pi -c never accepts input): 'pending' was inferred
from the render-time default status, so every RESTORED tool row rendered as
pending -> all added to the 10fps animation set -> unbounded invalidate loop.
Animation is now driven strictly by live tool_execution_start/end events
keyed by toolCallId; restored rows fire no start event so they never animate.
Self-test covers: restored rows don't pulse, live rows pulse only between
start/end, rails static during a run.
|
| |
|
|
|
|
| |
Record the redraw-flicker-during-write report as a tracked safety issue.
Prime suspect: the 10fps pending-tool border pulse (slop-tool-boxes d3454b8)
driving full-frame repaints ~10Hz (photosensitive-seizure risk band).
|
| |
|
|
|
|
|
|
| |
-ne disables package extension discovery, so pi-subagents' subagent tool never
loaded under dev.sh. Add scripts/resolve-package-extensions.mjs to resolve each
settings.json package's extension entry (file/index-dir/discovery-dir, mirroring
pi's loader) and re-add them via -e. Package skills/prompts already load under
-ne. Opt out per package with PI_DEV_SKIP_PACKAGES.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
Apply the last-prompt palette pulse to tool rows while they're running:
- module-level 10fps timer re-paints only PENDING rows via context.invalidate()
(the tool render context's rerender trigger), and stops when none are pending
- render() reuses width+content-cached inner lines (persisted on the row state
so they survive the timer's updateDisplay rebuilds) and only re-frames cheap
strings with the pulse color; native tool render is NOT re-run per tick
- completed rows keep the existing width-memoized full-output cache untouched,
so no perf regression for finished tools
- frame builders take a Paint fn (solid theme color when idle, truecolor pulse
when pending); label/icon stay static, only the border rails pulse
- agent_settled/session_shutdown safety net guarantees the timer stops
|
| |
|
|
|
|
|
|
|
| |
The prior approach couldn't work: shortcut ctx has no reload(), and
sendUserMessage() uses prompt({expandPromptTemplates:false}) which skips
slash-command handling, so "/reload-runtime" went to the model as literal
text. No extension API can invoke reload directly. Best available: F5 drops
the built-in /reload into the editor (guarded against clobbering in-progress
text); user presses Enter. Documented the source-confirmed blockers.
|
| |
|
|
|
|
|
|
| |
registerShortcut handlers only get a plain ExtensionContext (no ctx.reload,
which is command-context only), and keybindings.json has no reload action.
So register a /reload-runtime command that owns ctx.reload() and have the
shortcut queue it as a follow-up message — pi's documented reload pattern.
Key configurable via RELOAD_KEY.
|
| |
|
|
|
|
|
| |
Replace the "you" label with a shell-prompt-style glyph in the frame corner:
- PROMPT_GLYPH (❯) + spaced CURSOR (▁) reads like a command prompt
- LABEL now optional (empty = glyph only); set a word for "❯ ▁ usr" style
- CURSOR/GLYPH/LABEL exposed as easy style knobs
|
| |
|
|
|
|
|
|
|
|
| |
Drop the sweeping scanline (looked janky) in favor of a smooth full-frame
border color pulse while running:
- interpolate truecolor RGB across a curated palette (amber/pink/violet/cyan),
~4s per cycle, since theme.fg() semantic tokens looked too steppy
- pulse only the border rails + prompt marker; the sl0peration label stays
static accent so it reads clearly against the moving frame
- settles to a static accent border when idle
|
| |
|
|
|
|
|
|
|
|
| |
The original regression wasn't the timer itself but full-frame re-renders
recomputing every tool box from scratch. That's now fixed at the source
(slop-tool-boxes width-memoization), so a bounded animation timer is safe:
- runs ONLY during active runs (piggybacks pi's own streaming renders)
- drives frames via captured tui.requestRender() instead of setWidget/tick
- 10fps live elapsed timer + spinner in the top border
- subtle scanline sweep along the bottom border while running
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Completes the two pieces deferred from the first workspace-hints pass.
`zellij run --purpose X` / `zellij action new-pane --purpose X` tag a pane at
spawn time, which is what an agent wants for panes it creates itself; the OSC
wrapper still works for programs that declare their own role. Not supported with
--in-place, which replaces another pane rather than being a pane of its own: clap
rejects the combination rather than accepting a flag that would do nothing.
`tab name="work" purpose="agent-work"` gives a whole tab a role, reported by
`list-tabs --json`. It rides the tab's ROOT layout node, reusing the pane field
because a tab's root is the tab container and never itself a pane -- asserted by a
test for the tightest case (a tab holding exactly one pane), since a leak there
would silently mislabel a pane.
The interesting part is why the flag first shipped broken. There are TWO protobuf
boundaries -- the plugin API and the client->server IPC contract -- and a CLI flag
must cross the second one. Defaulting the field in the conversion (what "no
protoc" usually forces) compiles cleanly and then silently drops the value in
transit, so --purpose parsed, printed a pane id, and did nothing. Fixed by adding
the field to both contracts by hand, in the .proto AND the committed generated
prost, kept in lockstep so a future regeneration reproduces it: protoc is only
needed to regenerate, not to build. Both boundaries now have a round-trip test,
and the IPC one was confirmed to fail when the wire read is removed.
Sanitising moved to Grid::set_authoritative_purpose, the single choke point every
out-of-band source passes through, after the CLI path was observed delivering
`bad purpose; rm -rf /` intact into list-panes --json.
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Implements the zellij half of sl0p/workspace-hints-proposal.md: a pane can carry
an opaque agent-facing hint for what it is FOR, so a project can ship a layout
that an agent is able to navigate instead of guessing.
zellij never interprets a purpose. The consuming agent owns the entire vocabulary,
which is what keeps this to one string threaded along the existing status_bar path
rather than a policy engine, and lets the vocabulary change without a rebuild.
Two ways in, because declared panes and agent-created panes are different:
layout: pane name="server" purpose="dev-server"
in-band: printf '\033]5577;1;purpose;agent:build\033\\'
The second exists because tagging at spawn time would mean threading a field
through 28 SpawnTerminal call sites, and a new action variant is impossible while
protobuf enums cannot be regenerated. Wrapping the command costs one printf.
A declared purpose outranks an in-band one and cannot be overridden or cleared by
it. OSC arrives from whatever the pane happens to be showing, so otherwise a
`cat hostile.txt` in the human's editor pane could relabel it as the agent's
scratch pane and invite an agent to run commands where the human is working. A
layout is written by a human in a file they can read; in-band data only fills a
blank.
Exposed on PaneListEntry (`list-panes --json`) rather than PaneInfo, which crosses
the plugin protobuf API we cannot regenerate; resolved in the screen thread where
panes live rather than via route.rs's per-pane PTY round-trips. Values are
sanitised to a bounded [A-Za-z0-9_.:/-] token at both entry points, because a
purpose is never rendered -- it is matched against and pasted into consumers'
shell and JSON pipelines.
Verified end to end rather than by inspection: purposes from a layout appear in
list-panes --json against the right pane ids, an agent-spawned pane tags itself,
a declared purpose survives an in-band override attempt, all of it comes back
after kill + resurrect, an unknown pane property is still a parse error, and a
layout with no purpose leaves dump-layout unchanged.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A design doc, not an implementation: lets a project ship a layout that also tells
an agent what each pane is for.
The load-bearing decision is the boundary -- zellij carries one opaque string per
pane and never interprets it, pi owns the whole vocabulary. That keeps the fork
change to a single field threaded along the existing status_bar path, and lets the
vocabulary change without rebuilding zellij.
Records two findings that were checked rather than assumed, since both would
otherwise be discovered the expensive way: pane names cannot carry metadata (there
is no stable name field at runtime, and title is clobbered by whatever the program
sets), and the runtime query surface already exists (list-panes --json), so the
work is only to get the field into it.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The option was only read when a client connected, so changing it needed a new
session -- inconsistent with gap/padding/title_alignment, which apply live, and
the kind of inconsistency that reads as "my config change did nothing".
Threaded through update_frame_layout, the existing vehicle for the fork's
pane_frames options: Tab -> tiled/floating containers -> each pane's Style,
including suppressed panes so a hidden stack member is styled correctly when it
swaps back into view.
Verified by editing config.kdl against a running session and watching
1:Approve flip to [Approve] and back, then confirming a click still resolves
afterwards -- the button widths change with the style, so a stale layout would
have silently mis-targeted clicks.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`status_bar_button_style "indexed"` renders `1:Approve 2:Reject` instead of
`[Approve] [Reject]`, so the bar doubles as a key legend for a harness that binds
alt+1..9 to the same actions.
A config option rather than a protocol field: whether the numbers mean anything
depends on what the USER has bound, not on what the program knows. It needs no
wire change and so no version bump, and the click report still carries the id --
never the index -- so a program is unaffected by which style is in force.
The style is threaded into layout_status_bar, the one function the renderer draws
from and the hit-test resolves against. That is what makes this safe: changing a
button's rendered width necessarily changes its clickable span too, verified by a
test asserting span width equals the drawn width and that every drawn column
resolves back, for both styles.
Also fixes the web client's Style construction to read the option from config
rather than defaulting it -- my first pass claimed that code had no pane_frames
config available, which was wrong; it uses it two lines further down.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A click target with no hover feedback gives you no way to tell what you are about
to hit, which matters more here than elsewhere because the buttons sit in a frame
row that is only one cell tall.
The hovered button is inverted rather than merely recoloured: swapping
fore/background reads regardless of which colours a theme uses, and makes "the
pointer is on THIS one" unmistakable next to its unhovered neighbours.
Tracked per client, because hover belongs to a pointer rather than to a pane --
with two users attached, one pointer must not light up the other's button. Stored
on the pane keyed by client id, which the frame renderer already has.
Updated before the action dispatch rather than in the hover action, because
motion over the ACTIVE pane is dispatched as SendToTerminal, not UpdateHover --
and the pane you are about to click a button in is usually the one you are
already in. Every other pane's hover for that client is cleared on each motion,
so moving between panes cannot leave a button lit behind.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Reference for programs wanting to drive the bar: the OSC 5577 verbs, escaping
rules, click reports, limits, rendering behaviour, persistence, and the security
model.
Both worked examples were run against a real session rather than written from
memory, which caught four bugs in my own samples:
- the Python reader kept a trailing buffer window, so a processed report was
re-matched and every click fired twice
- it used tty.setraw, which also disables signals, leaving the sample unkillable
with Ctrl-C
- it exited via traceback and left the bar behind; both samples now clear it on
exit
- the shell reader echoed the report to the screen, and an `stty -echo` did not
help because bash's `read -n` saves and restores terminal settings around each
call. `read -s` is the fix.
Also verified and documented two behaviours I would otherwise have guessed at: a
BEL terminator works as well as ST, and the bar is not drawn when pane frames are
off (it lives in the frame row), reappearing when they return.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Second slice of the status/action frame. A program declares buttons, and a click
is reported back to it:
printf '\033]5577;1;buttons;approve:Approve;reject:Reject\033\\'
-> click -> program's stdin receives OSC 5577 ; 1 ; click ; approve ST
The report is a FIXED format carrying only the id, never bytes the payload chose,
and ids are restricted to [A-Za-z0-9_-]. The sequence that creates a button is
in-band and untrusted -- `cat` of a hostile file emits it as readily as a
cooperating program -- so a click that could send arbitrary bytes would let
viewing a file inject into the stdin of whatever is running, a shell included.
Labels are '%'-escaped since the wire format is ';'-separated, and both the
button count and id length are capped because the list lives as long as the pane.
Positions come from one layout_status_bar function that the renderer draws from
and the hit-test resolves against. Computing that geometry twice is how the
stacked-pane picker once selected the wrong pane; a test asserts every drawn
column resolves back to the button drawn there, across 100 widths.
Two things the drive caught that unit tests could not:
- The first delivery path pushed onto grid.pending_messages_to_pty, which is
only drained when the PANE produces output -- so a click on an idle program sat
in the queue forever. The write now happens at the tab level, which owns the
senders.
- The hit-test derived the frame row from the pane's raw geometry, but this fork
draws the frame inset by gap_offset, so with `gap 1` every click missed by one
row. It now derives from the CONTENT rect, which is by definition the inside of
the drawn frame and therefore the same columns the renderer lays out.
Buttons serialise with the session like the status text, so a resurrected pane
comes back with its buttons intact.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The status bar survived detach/reattach (it lives on Grid) but not a
serialise/restore cycle, so a resurrected pane came back blank.
It now travels the whole per-pane serialisation pipeline, mirroring the fork's
existing default_fg/default_bg: pane -> PaneLayoutMetadata ->
PaneLayoutManifest -> TiledPaneLayout -> a status_bar property in the serialised
KDL, then back through the normal layout parser and applied by LayoutApplier
when the pane is recreated.
The sanitiser moved from grid.rs into zellij-utils so both entry points share
one definition. That matters: a layout file on disk is no more trustworthy than
the escape sequence that wrote it -- it may have been hand-edited, or copied from
somewhere else -- so restored text is stripped and length-capped on the way in
just like the OSC payload was.
Verified end to end rather than by unit test alone: set the bar, waited for a
serialisation pass, confirmed status_bar="..." in session-layout.kdl, killed the
session until list-sessions reported EXITED, then attached and watched it come
back from disk. The earlier attempt only proved detach/reattach because Quit had
left the session alive.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
First slice of the status/action frame: one row of program-supplied text in a
pane's bottom frame row.
printf '\033]5577;1;status;building 3/7\033\\'
printf '\033]5577;1;clear\033\\'
Rendered in the EXISTING frame row rather than in reserved rows. Reserving would
shrink the pane's content area and resize its PTY whenever the bar appeared or
disappeared, firing SIGWINCH at the program that asked for it -- the same churn
that made the stack-list picker use a fixed cap. The frame row is already a text
surface with a precedence chain, so this cost no geometry work at all.
State lives on Grid like the window title, so it survives re-renders, resizes
and detach/reattach with no draining. Session resurrection is NOT covered yet.
Versioned from the start, and unknown versions or verbs are ignored, so the
planned button verbs cannot be half-interpreted by an older build. Text is the
remaining params rejoined with ';', which means a single text field needs no
escaping -- the trick OSC 0/2 already uses for titles.
Treated as untrusted, because it is: an in-band sequence is emitted just as
readily by `cat` of a hostile file as by a cooperating program. Control
characters are stripped -- a raw ESC could otherwise repaint anything on screen,
a newline would corrupt the frame row -- and the length is capped, since the
string is retained for the life of the pane.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Upstream's mouse_hover_effects governs two unrelated things: the frame highlight
on hover, and the "Ctrl <MouseScroll> or <drag borders> to resize" hint. Its own
comment in default.kdl admits as much. So silencing a beginner reminder you have
outgrown also costs you a highlight that stays useful indefinitely.
mouse_hover_help_text (default true) gates only the hint. One line in
execute_send_to_terminal; the highlight is set in execute_update_hover and is
untouched.
Config-file only, deliberately. The option has no protobuf slot, so it does not
cross the client/server IPC contract -- the roundtrip test asserts exactly that,
with the fixture set to None and a comment explaining why. It still works from
the config file because the server parses the file itself and Options::merge
keeps the file value when the client sends None; a CLI flag would be silently
ignored.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
In a collapsed (mobile) layout every stack member except the visible one is
suppressed, and the finder skipped suppressed panes outright -- so it could only
ever find the one pane already on screen, which defeats the point of having it
there.
Suppressed terminals are now listed and marked "(hidden)". Suppressed plugins
still are not: zellij:link and friends are deliberately invisible
infrastructure, and focusing one shows an empty pane. Unfiltering everything
listed it, which is how that distinction surfaced.
Needed no server work. Screen::focus_pane_with_id already locates the owning tab
via has_pane_with_pid, which includes suppressed panes, and calls go_to_tab; and
Tab::focus_pane_with_id tries focus_hidden_stack_list_member first. So selecting
a hidden stack member swaps it into view and selecting a pane in another tab
switches tab, both already handled by the host.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Upstream's action focuses an already-running plugin, so a chord bound to it can
only ever open one -- there is no way to dismiss with the same key.
This cannot be fixed in a plugin. Keybindings are resolved into actions in
route.rs before a key reaches a pane, so the launching chord never arrives at
the plugin. intercept_key_presses does not help either: the intercept lives in
screen.rs and only sees keys that already became Action::Write, i.e. keys
destined for a pane. Confirmed empirically -- an intercepting build received
Ctrl-u but never Ctrl-;.
Closes only when the plugin is the focused pane in the client's ACTIVE tab, so
the chord cannot close whatever pane the user happens to be on. A plugin that
exists but is hidden behind a floating-layer toggle, or is focused in a
different tab, is revealed or brought over as before.
Defaults to false, so every existing binding is unaffected. Config-only, with no
protobuf representation: defaulted at both boundaries and dropped when an action
crosses the plugin API, so no prost regeneration is needed.
The 24 snapshot lines are the new field appearing in serialised keybind dumps.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Reaching the finder through a mode dance (leader, then session mode, then a
letter) is too many keys for something used constantly. Documents Ctrl-; as a
one-chord binding, and why: ';' has no control-code meaning, readline does not
bind it, and vim binds only bare ';' -- while plain ';' keeps typing normally.
Also records the tradeoff. Ctrl-; needs the kitty keyboard protocol to be
distinguishable from a plain ';', so it silently never fires on a terminal
without it. The combos that work anywhere are real control codes and are all
taken: Ctrl-\\ is SIGQUIT, Ctrl-] is vim's tag jump, Ctrl-_ is readline undo.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The finder always opened as a centred float with a frame. That costs its
margins (16% of columns, 28% of rows), a border, and on this fork the pane gap
as well -- on an 80x24 terminal leaving about 63x14 of the 80x24 available.
Rows are the scarce resource in a list, so most of the results were going to
decoration.
Below 100x30 it now takes the whole viewport and asks to be borderless, which
also drops the gap since this fork skips gap/padding for frameless panes. Above
that the centred float is kept, because it reads better when the space is
there.
The viewport is only knowable from TabInfo, so this re-evaluates on TabUpdate,
which is also how a terminal resize reaches a plugin -- switching modes live in
both directions. Re-applied only when the decision flips, so it does not fight
someone moving or resizing the pane by hand.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The pane finder was living in ~/dev with no version control at all, while
path-depending on this checkout's zellij-tile -- so the plugin and the binary
it must stay API-compatible with were maintained in two unrelated places, one
of them unversioned.
Its own workspace root rather than a member of zellij's: a non-root member has
its [profile] ignored, and opt-level="z" + lto + strip are what keep the wasm
small, which matters because the keybinding loads it with skip_plugin_cache and
recompiles on every launch. Staying independent also means carrying this crate
touches no upstream-tracked file, so it cannot conflict on a rebase.
install.sh no longer assumes ./target: this checkout's .cargo/config.toml sets
a shared build.target-dir, so the artifact lands in the repo's target dir
instead. It now asks cargo metadata where the build actually went, and fails
loudly rather than silently installing a stale wasm.
The zellij-tile path dep becomes relative, so the crate builds from any
checkout rather than only from /home/user/lol/zellij.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`hide_default_pane_names` is a frame-rendering option, but it was applied
inside current_title() -- which is also what fills PaneInfo.title. Every plugin
that lists panes therefore received an empty string for any pane whose program
had never set a title.
For the bundled session manager that is untidy. For a fuzzy pane finder it is
broken: the rows are identical AND unsearchable, so the plugin's whole purpose
stops working.
The blanking moves to the frame, which already had a blank_title concept, and
current_title() goes back to reporting a real title to everything that asks.
Frames still show nothing for an unnamed pane, named panes still show their
name, and PaneInfo reports "Pane #N" again.
Changes a value rather than adding a field, so it needs no protobuf
regeneration.
Only observable without a shell that sets a title: verified by disabling the
zsh hook, where the pane finder previously listed three identical blank rows
and now lists Pane #1/#2/#3. That masking is also why this went unnoticed --
and why I flagged it twice without confirming it was actually reachable.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Collapsing and expanding a tab from the default layout stranded the status bar
halfway down the screen, and left the collapsed stack three rows tall.
A synthesized layout describes only where the user's panes go. Applied bare, it
laid out EVERY pane in the tab -- including the status bar, which became an
ordinary grid cell. Real swap layouts do not hit this because their `tab` node
is expanded with the default tab template, which carries the pinned plugin
slots; a layout built at runtime has to take those from BASE instead.
Synthesis now builds into the tab's own structure: the first non-plugin leaf
becomes the stack or grid and inherits its size, further content leaves are
dropped since the replacement accounts for every pane itself, and plugin panes
keep their slots and sizes.
Falls back to the bare replacement when there is no content slot at all -- a
layout of nothing but plugin panes would otherwise be returned unchanged,
silently dropping every user pane. Found by the test, not by the drive.
Only reproducible with a layout that has plugin panes, which is why the
quaddie.kdl drives never showed it and the default layout did immediately.
|