Skip to content

feat(ui): host-owned status line with inline prompts, exposed to extensions - #1095

Merged
elucid merged 14 commits into
mainfrom
status-line-primitive
Sep 11, 2026
Merged

feat(ui): host-owned status line with inline prompts, exposed to extensions#1095
elucid merged 14 commits into
mainfrom
status-line-primitive

Conversation

@elucid

@elucid elucid commented Sep 11, 2026

Copy link
Copy Markdown
Member

Problem

The bottom status row is a filter-only, host-private surface. hunk log has a second, hand-rolled footer with its own hand-rolled line editor. Extensions that need a less-style prompt (e.g. elucid/hunk-less-search) have to fake one: a permanently open bottom pane costing a row for the whole session, a keyboard mode re-implementing a line editor with standing in for a cursor, and a store token deferring navigation to the pane's useEffect because only panes hold actions.

Approach

Make the status row one host-owned surface — the status line — that the host's own filter, hunk log search, and extensions all drive through the same primitive. Host-internal first, then exposed.

  • packages/hunk/src/ui/statusLine/: a renderer-free store (insertion-ordered items + a FIFO prompt queue that settles like the dialog queue), a deterministic, theme-free width/priority layout, and one StatusLine component that owns the focused <input> and the badge click-to-exit.
  • App mounts it in place of StatusBar. The file filter is the first prompt consumer (prefix: "filter:", onChange: review.setFilter); the residual filter=foo and the notice channel are host items derived from state. focusArea === "filter" is now derived from "the host filter prompt is current", so focus and the visible input cannot disagree.
  • LogApp mounts the same component; hunk.history.search opens a / prompt. appendSearch / backspaceSearch / searchEditing and the second footer are deleted. The retained query stays visible on the row (/query) after a search so n/N have a visible referent.
  • Extension API 26: ctx.statusLine.set/clear (commands, events, keyboard modes) and ctx.prompts.line() (commands). No factory-scope writes; items namespace under the extension, clear on registry replacement/teardown, survive content reloads; prompts cancel on any reload. Attribution follows the dialog policy. Malformed items throw, malformed prompt options reject, a throwing onChange warns once.
  • The vim-navigation example's : line moves from a centered dialogs.input() modal to prompts.line({ prefix: ":" }), the shape it always wanted.

Non-goals: no history extension surface; no prompt onKey/history recall; menu entries remain non-contributable.

Why core

A prompt-shaped interaction and the row it lives on are terminal chrome every review surface shares; the extension system needed the general capability rather than a feature-specific hook. Two host consumers (filter, log search) prove the primitive before any extension uses it.

Tests

Unit (colocated): statusLine/layout.test.ts, store.test.ts, extensionControls.test.ts, StatusLine.test.tsx (replaces the StatusBar cases in ui-components.test.tsx), log/controller.test.ts.
App-level: new AppHost.status-line.test.tsx (items from commands/events/modes, registry-replacement clearing, prompt typing/submit/two-step Escape/queueing/reload cancellation/coexistence with the filter).
PTY: new hunk log search case in log-integration.test.ts; new prompt + overflow case and the updated vim-navigation case in extensions-integration.test.ts. Existing filter PTY coverage (filter-escape, key-routing, chrome) unchanged and passing.

One existing test was adjusted: AppHost.file-views.test.tsx pressed Tab, Escape, Tab inside one act; the mock input parses ESC + Tab in one chunk as a single alt-chord, and with a store-driven prompt the keys now actually reach the input (the old state-driven bar never rendered between keys inside one act, so the typed text never landed and the test passed by accident). It now presses them in separate acts and asserts each state.

Commands run

  • bun run typecheck, bun run lint, bun run deps:check — clean.
  • bun run test — 4236 pass, 52 skip; only the Jujutsu "logs unexpected source failures" test fails, reproduced alone on origin/main. The watch observer-debounce test passes on main and was a real PR regression, now fixed: the host filter opts into surviving reloads with its value and focus; extension prompts still cancel.
  • bun run test:integration — 174 pass; only lifecycle SIGHUP/SIGPIPE/SIGQUIT fail, all three reproduced with bun test test/pty/lifecycle.test.ts on origin/main. Focused watch, filter-escape, key-routing, chrome, log-integration, and extensions-integration -t "prompt" PTY runs all pass, including a new 50-column long-attribution/prefix test proving typed input remains visible beside the badge.
  • Latest validation uses Bun 1.4.2 and TMPDIR=/private/tmp on macOS to avoid the /tmp symlink affecting trust-state path assertions.
  • bun run test:tty-smoke — skipped on macOS (needs util-linux script).
  • Real TTY (tmux, isolated XDG_CONFIG_HOME): hunk diff HEAD~3 --no-extensions/ opens the prompt with a cursor, typing narrows live, Escape clears then closes, Tab in/out, filter=… residual. hunk log --no-extensions/ prompt, Enter jumps to the match, /query stays on the row, n repeats.

Platforms: macOS only.

Follow-ups

  • PR 2: / → content search, n/N → match stepping, bundled search extension, bundled composition for commands + line highlighters, selection.files on the command context.

Introduce `ui/statusLine/` with the host-internal item and prompt shapes
and a deterministic, theme-free layout that fits persistent items, one
inline prompt, and the keyboard-mode badge into a single terminal row.

Policy encoded here, and covered by unit tests:

- Left items paint in set order; right items sit beside the badge.
- An active prompt takes the whole left region and keeps right items
  only when they fit whole beside a four-cell minimum input.
- On overflow the lowest-priority item is dropped whole (newest first
  among equal priorities); the last survivor is truncated with an
  ellipsis across its spans while keeping their tones.
- The badge is never dropped and is capped at half the row, matching
  what StatusBar already does for the keyboard-mode badge.

Nothing consumes this yet. It is the first step of replacing the
filter-only StatusBar and the hand-rolled `hunk log` footer with one
host-owned surface that extensions can later write to.
Add `createStatusLineStore`, the renderer-free state behind the status
row: an insertion-ordered item map and a FIFO prompt queue, exposed as
a stable snapshot for `useSyncExternalStore`.

Prompts follow the `ctx.dialogs` lifetime rules so a consumer awaiting
one is never left hanging: one on screen at a time, later requests
queue in call order, `cancelAllPrompts` drains everything on reload
while keeping the store open, and `shutdown` settles what is pending
and answers later requests with `null` immediately. A request whose
owner is no longer live cancels without appearing, and a submit from an
owner that expired while the prompt was open also resolves `null`.

`updatePromptValue` keeps the input controlled and forwards each edit
to the consumer's optional `onChange`; a throwing `onChange` is
reported once and does not end the prompt. Consumer text is sanitized
like every other terminal-bound string.
Replace the filter-only StatusBar with the host status line. `App`
owns one status-line store, and the file filter becomes its first
prompt consumer: focusing the filter is
`openPrompt({ prefix: "filter:", initial, onChange: review.setFilter })`,
and the residual `filter=foo` and the one notice channel are host
items derived from state on every render, so they can never go stale.

Focus is derived rather than stored: `focusArea === "filter"` now means
"the host filter prompt is the current status-line prompt", computed
from the store snapshot and the prompt id the host opened. Storing it
separately let a render observe the input without the focus flag (or
the reverse) between two keys of one input chunk, which routed Tab
into the input instead of out of it.

Behavior preserved: Escape clears a non-empty filter first and leaves
it second; Tab still toggles between files and filter through
`hunk.app.toggleFocusArea`; selecting a file from a pane or starting a
note draft submits an open filter prompt; the keyboard-mode badge is
still clickable. Any other status-line prompt owns typing through the
same focused-input tier, ahead of file-view and session modes.

The symbolic span painting (`tone`/`attributes` → theme) moves out of
FileView into `ui/lib/symbolicSpans.ts` so the status line and file
views share one mapping. StatusBar's component tests move to
`statusLine/StatusLine.test.tsx`. The file-views test that pressed
Tab, Escape, Tab inside one act now presses them separately: the mock
input parses an Escape followed by a Tab in one chunk as a single
alt-chord, and with a store-driven prompt the keys really reach the
input, which the old state-driven bar never let happen inside one act.
… alignments

Right-aligned items were fitted before left ones, which let a
low-priority hint outrank a high-priority status purely because of
where it sits. Overflow now drops the lowest-priority item across the
whole row, newest first among equals, exactly as the design states;
alignment only decides placement.
`LogApp` now mounts the same status-line component as the review. The
`provider · N commits` text, the selection count, notices, and the
responsive key hint become host items; `hunk.history.search` opens a
real inline prompt with a `/` prefix, and Enter runs
`controller.search(query)`, which stores the repeatable query and
selects the next match.

The hand-rolled line editor goes away: `appendSearch`,
`backspaceSearch`, `beginSearch`, `cancelSearch`, `finishSearch`, and
the `searchEditing` snapshot flag are deleted along with the inline
`/query` branch in the keyboard handler and the second footer
implementation. While a prompt is open the focused input owns typing,
Enter, and the two-step Escape; the keyboard handler keeps only the
Ctrl-C escape hatch so a search can never trap the terminal.

The retained query stays visible on the row after a search, the way
the review keeps `filter=foo`, so `n`/`N` have a visible referent.

PTY coverage drives the whole flow: placeholder, in-place typing, a
bound key (`q`) typed as text, Enter selecting the match and opening
it, reopening with the retained query, and the two-step Escape.
…ions

Extension API 26. The host status line becomes an extension surface
through two small capabilities:

- `ctx.statusLine.set(item)` / `clear(id)` write persistent text items
  (`ExtensionStatusItem`: spans in the file-view span vocabulary,
  `alignment`, `priority`). Ids are namespaced `ext:<extensionId>:<id>`
  so extensions cannot collide with each other or with host items.
- `ctx.prompts.line(options)` opens a real inline input with a cursor on
  the status row and resolves the submitted text, or `null` on Escape,
  reload, or teardown. Attribution follows the dialog policy: installed
  extensions carry the `ext <id>` marker, bundled ones do not.

Where they live follows the design: commands get `statusLine` and
`prompts`; event handlers and keyboard modes get `statusLine` only,
since a prompt-shaped interaction is now a command plus
`prompts.line` rather than a keyboard mode. The factory object gets
neither, so every write belongs to a context whose lifetime the host
can scope. The pre-mount event context reports both as unavailable
like the other controls it stands in for.

Lifetimes: items survive ordinary content reloads and clear when the
extension registry is replaced or the review unmounts; prompts cancel
on any reload through the store's generation hook. Malformed items and
options are programming errors: `set` throws and `line` rejects, and a
throwing `onChange` warns once, attributed, without ending the prompt.
…ted app

Drive `ctx.statusLine` and `ctx.prompts` through AppHost with fixture
extensions: a command sets and clears a right-aligned item; an event
handler's item survives a daemon content reload; a keyboard mode's
buffer item shares the row with the host badge and clears on exit; a
malformed item throws from `set`; and granting repo trust replaces the
registry and clears the retired registry's items.

Prompts: attributed inline rendering with placeholder, a bound key
typed as text instead of quitting, Enter resolving the text, the
two-step Escape, FIFO queueing with live `onChange` edits, daemon
reload cancellation, and coexistence with the host filter's residual
item.
A fixture extension asks for a line on the status row and reports the
answer as an item. The PTY run checks the attributed prefix and
placeholder, in-place typing including a bound key (`q`) as text,
Enter resolving the text, the two-step Escape resolving `null`, and an
overflow case where the lowest-priority item is dropped whole while a
higher-priority left item and a right-aligned item keep their places.

The fixture loads through `--extension` from outside the repo so the
same file is not also discovered as an untrusted repo extension; that
double load produces a startup notice that would otherwise share the
row under test.
@vercel

vercel Bot commented Sep 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
hunk-web Ignored Ignored Preview Sep 11, 2026 2:15pm UTC

Request Review

@greptile-apps

greptile-apps Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces the private status bar and history footer with a shared host-owned status-line store and renderer, migrates file filtering and history search to queued inline prompts, and exposes status items and prompts through extension API 26.

  • Adds insertion-ordered status items, prompt lifecycle management, width-aware layout, and symbolic-span rendering.
  • Wires status controls into command, event, and keyboard-mode contexts with registry and review-generation scoping.
  • Updates the vim-navigation example, extension documentation, and unit/app/PTY coverage.
  • The prompt layout still needs to preserve a visible input when the lead-in cannot fit.

Confidence Score: 4/5

The PR should not merge until attributed prompts retain a visible input on narrow status rows; the stale design-document status should also be corrected.

The new prompt layout can allocate more width than the row provides, clipping the focused input while it still captures keyboard input. The remaining implementation and lifecycle paths are well covered, with one additional documentation inconsistency.

Files Needing Attention: packages/hunk/src/ui/statusLine/layout.ts, packages/hunk/src/ui/statusLine/StatusLine.tsx, docs/status-line-extension-point.md

Important Files Changed

Filename Overview
packages/hunk/src/ui/statusLine/store.ts Introduces the observable status-item store and FIFO prompt queue with reload and teardown cancellation.
packages/hunk/src/ui/statusLine/layout.ts Implements deterministic priority-based layout, but can over-allocate an attributed prompt and clip its input.
packages/hunk/src/ui/statusLine/StatusLine.tsx Renders symbolic items, a focused inline input, and the keyboard-mode badge from the computed layout.
packages/hunk/src/ui/statusLine/extensionControls.ts Adds validated, namespaced, lease-scoped status-item and prompt controls for extensions.
packages/hunk/src/ui/App.tsx Replaces StatusBar with the shared status line and migrates filter focus and extension context wiring.
packages/hunk/src/ui/log/LogApp.tsx Migrates history search and footer content to the shared status-line primitive.
packages/hunk/src/extension-api/types.ts Publishes API version 26 types for status items and inline prompts.
docs/status-line-extension-point.md Records the two-PR design but incorrectly labels the now-implemented first phase as entirely unimplemented.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  H[Host filter or log search] --> S[Status-line store]
  E[Extension command] --> C[Scoped status and prompt controls]
  C --> S
  S --> Q[FIFO prompt queue]
  S --> I[Persistent status items]
  Q --> L[Width-aware layout]
  I --> L
  B[Keyboard-mode badge] --> L
  L --> R[StatusLine renderer]
  R --> U[Focused terminal input and status row]
Loading
Prompt To Fix All With AI
### Issue 1
packages/hunk/src/ui/statusLine/layout.ts:197
**Prompt Input Can Disappear**

On a narrow terminal or with a long extension attribution, the prefix and non-droppable badge can exhaust the row, but the prompt still receives a minimum width of four cells. The input is then clipped inside the overflow-hidden container while it continues capturing every key, leaving the prompt unusable until the user cancels it. Please truncate the lead-in or otherwise reserve visible space for the input within the actual available width.

### Issue 2
docs/status-line-extension-point.md:3
**Proposal Status Is Stale**

This document says that nothing is implemented, but this PR implements its entire PR 1 status-line scope. That makes it harder for readers to distinguish the shipped status-line API from the still-proposed PR 2 search work.

```suggestion
Status: PR 1 implemented; PR 2 proposed.
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "chore(changeset): keep the status-line s..." | Re-trigger Greptile

).right
: [];
const rightWidth = right.length > 0 ? itemsWidth(right) + ITEM_GAP : 0;
const inputWidth = Math.max(MIN_PROMPT_INPUT_WIDTH, available - leadWidth - rightWidth);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Prompt Input Can Disappear

On a narrow terminal or with a long extension attribution, the prefix and non-droppable badge can exhaust the row, but the prompt still receives a minimum width of four cells. The input is then clipped inside the overflow-hidden container while it continues capturing every key, leaving the prompt unusable until the user cancels it. Please truncate the lead-in or otherwise reserve visible space for the input within the actual available width.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/hunk/src/ui/statusLine/layout.ts
Line: 197

Comment:
**Prompt Input Can Disappear**

On a narrow terminal or with a long extension attribution, the prefix and non-droppable badge can exhaust the row, but the prompt still receives a minimum width of four cells. The input is then clipped inside the overflow-hidden container while it continues capturing every key, leaving the prompt unusable until the user cancels it. Please truncate the lead-in or otherwise reserve visible space for the input within the actual available width.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread docs/status-line-extension-point.md Outdated
@@ -0,0 +1,331 @@
# Status line as an extension point, and `/` as content search

Status: proposal. Nothing here is implemented.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Proposal Status Is Stale

This document says that nothing is implemented, but this PR implements its entire PR 1 status-line scope. That makes it harder for readers to distinguish the shipped status-line API from the still-proposed PR 2 search work.

Suggested change
Status: proposal. Nothing here is implemented.
Status: PR 1 implemented; PR 2 proposed.
Prompt To Fix With AI
This is a comment left during a code review.
Path: docs/status-line-extension-point.md
Line: 3

Comment:
**Proposal Status Is Stale**

This document says that nothing is implemented, but this PR implements its entire PR 1 status-line scope. That makes it harder for readers to distinguish the shipped status-line API from the still-proposed PR 2 search work.

```suggestion
Status: PR 1 implemented; PR 2 proposed.
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

…onto prompts

Add a "Status line" section to docs/extensions.md covering items (span
vocabulary, alignment, priority-based overflow, lifetimes) and prompts
(prefix/placeholder/initial/onChange, two-step Escape, key routing tier,
queueing, reload cancellation, attribution), plus where each control
appears. Note API 26 in the version history, the keyboard-mode context,
and the hunk-extensions skill's capability map.

The vim-navigation example's `:` command line was always prompt-shaped;
it now uses `ctx.prompts.line({ prefix: ":" })` instead of a centered
`ctx.dialogs.input()` modal, declares API 26, and its PTY test follows
the inline prompt and the two-step Escape. Describe the status line's
place in docs/extension-architecture.md and add the minor Changeset.
@elucid
elucid force-pushed the status-line-primitive branch from b754d95 to e98f224 Compare September 11, 2026 13:45
@elucid

elucid commented Sep 11, 2026

Copy link
Copy Markdown
Member Author

Force-pushed PR 1: removed the local-only design doc from branch history; restored the host filter’s value/focus across content reloads while extension prompts still cancel; fixed Greptile P1 by truncating long prompt lead-ins before they hide the input. Added store/app/layout and real-PTY regression coverage. Typecheck, lint, dependency checks, and all requested focused PTY suites pass. Full suites only retain main-reproduced Jujutsu source-error and lifecycle signal failures; validation details updated in the PR body.

@elucid

elucid commented Sep 11, 2026

Copy link
Copy Markdown
Member Author

The status-line doc example is now self-contained and typechecks under bun run check:pack; the Windows jj 5000 ms timeout is an unrelated runner-speed flake.

@elucid
elucid merged commit 515188e into main Sep 11, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant