feat(ui): host-owned status line with inline prompts, exposed to extensions - #1095
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Greptile SummaryThis 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.
Confidence Score: 4/5The 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
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]
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); |
There was a problem hiding this comment.
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.| @@ -0,0 +1,331 @@ | |||
| # Status line as an extension point, and `/` as content search | |||
|
|
|||
| Status: proposal. Nothing here is implemented. | |||
There was a problem hiding this comment.
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.
| 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.
b754d95 to
e98f224
Compare
|
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. |
|
The status-line doc example is now self-contained and typechecks under |
Problem
The bottom status row is a filter-only, host-private surface.
hunk loghas a second, hand-rolled footer with its own hand-rolled line editor. Extensions that need aless-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'suseEffectbecause only panes holdactions.Approach
Make the status row one host-owned surface — the status line — that the host's own filter,
hunk logsearch, 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 oneStatusLinecomponent that owns the focused<input>and the badge click-to-exit.Appmounts it in place ofStatusBar. The file filter is the first prompt consumer (prefix: "filter:",onChange: review.setFilter); the residualfilter=fooand 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.LogAppmounts the same component;hunk.history.searchopens a/prompt.appendSearch/backspaceSearch/searchEditingand the second footer are deleted. The retained query stays visible on the row (/query) after a search son/Nhave a visible referent.ctx.statusLine.set/clear(commands, events, keyboard modes) andctx.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 throwingonChangewarns once.:line moves from a centereddialogs.input()modal toprompts.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 theStatusBarcases inui-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 logsearch case inlog-integration.test.ts; new prompt + overflow case and the updated vim-navigation case inextensions-integration.test.ts. Existing filter PTY coverage (filter-escape,key-routing,chrome) unchanged and passing.One existing test was adjusted:
AppHost.file-views.test.tsxpressed Tab, Escape, Tab inside oneact; the mock input parsesESC+Tabin 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 oneact, 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 onorigin/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 withbun test test/pty/lifecycle.test.tsonorigin/main. Focusedwatch,filter-escape,key-routing,chrome,log-integration, andextensions-integration -t "prompt"PTY runs all pass, including a new 50-column long-attribution/prefix test proving typed input remains visible beside the badge.TMPDIR=/private/tmpon macOS to avoid the/tmpsymlink affecting trust-state path assertions.bun run test:tty-smoke— skipped on macOS (needs util-linuxscript).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,/querystays on the row,nrepeats.Platforms: macOS only.
Follow-ups
/→ content search,n/N→ match stepping, bundledsearchextension, bundled composition for commands + line highlighters,selection.fileson the command context.