diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json
index bca7ce4..4e5595b 100644
--- a/.claude-plugin/plugin.json
+++ b/.claude-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "downrange",
- "version": "1.1.2",
+ "version": "1.1.3",
"description": "A band, pane, and /downrange table: progress, ETA, and last output for Claude Code background shells and Monitor watches, tool activity for subagents, state for workflows, and the next /loop wakeup.",
"author": { "name": "dot-agi" },
"license": "MIT",
diff --git a/README.md b/README.md
index fba3a63..a7291c0 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-
+
Progress and an ETA for background shells and Monitor watches, read from their real output or past runs, with no model request.
@@ -139,7 +139,7 @@ Notes:
| `c` in the focused pane | Clears finished jobs |
| `x` in the focused pane | Closes the pane |
-Focus the band or the pane with Ctrl+X, then Tab, or a click. That chord is Claude Code's own `abovePrompt:focus` binding. Release Ctrl before you press Tab, because many terminals read Ctrl+Tab as next tab. In Emacs vterm, eat, or ansi-term, click the band, use `/downrange pane`, or rebind `abovePrompt:focus` with [`/keybindings`](https://code.claude.com/docs/en/keybindings#above-prompt-actions).
+Focus the band or the pane with Ctrl+X, then Tab, or a click. That chord is Claude Code's own `abovePrompt:focus` binding. Release Ctrl before you press Tab, because many terminals read Ctrl+Tab as next tab. On the focused band, Tab moves between **Open** and **Hide**, Enter presses one, and Esc returns to the prompt. When the band's row has free room, it shows the dim hint `ctrl+x tab to choose` just before **Open**. In Emacs vterm, eat, or ansi-term, click the band, use `/downrange pane`, or rebind `abovePrompt:focus` with [`/keybindings`](https://code.claude.com/docs/en/keybindings#above-prompt-actions).
## Options
diff --git a/docs/usage.md b/docs/usage.md
index 4d5380e..62e0fac 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -33,6 +33,8 @@ The band is one line above the prompt:
- Job snippets follow: recent failures, running jobs with progress, other running jobs (oldest first), then other recent ends. Snippets that do not fit the width are left out. The counts always show.
- **Open** (`j`) opens the pane. **Hide** (`h`) hides the band until a new job starts.
- Focus the band with Ctrl+X, then Tab, or a click before you use its keys. That chord is Claude Code's own `abovePrompt:focus` binding. Release Ctrl before you press Tab, because many terminals read Ctrl+Tab as next tab. In Emacs vterm, eat, or ansi-term, click the band, use `/downrange pane`, or rebind `abovePrompt:focus` with [`/keybindings`](https://code.claude.com/docs/en/keybindings#above-prompt-actions).
+- On the focused band, Tab moves between **Open** and **Hide**, Enter presses one, and Esc returns to the prompt.
+- When the row has free room after the counts and the snippets, the dim hint `ctrl+x tab to choose` shows just before **Open**. The hint never takes room from a snippet and never moves a button. A narrow or full band leaves it out. To decide, Downrange counts each character outside ASCII and its own band symbols as two cells, so a job title with accents, wide characters, or emoji can hide the hint a little earlier than needed.
- A survey takes the band's place while it shows.
- The option `band` turns the band off.
diff --git a/hooks/drawing.tsx b/hooks/drawing.tsx
index 293e36e..35d07fc 100644
--- a/hooks/drawing.tsx
+++ b/hooks/drawing.tsx
@@ -10,8 +10,8 @@ const PART_STYLES: Readonly void; hide: () => void }, below: RenderElement): RenderElement {
+/** The band: one line of counts and snippets, the key hint in the free room before Open and Hide when the row holds it, and what other mods drew beneath. */
+export function bandTree(ui: Ui, parts: readonly BandPart[], hint: string | undefined, actions: { open: () => void; hide: () => void }, below: RenderElement): RenderElement {
const { Box, Text, Button } = ui
return (
@@ -23,6 +23,11 @@ export function bandTree(ui: Ui, parts: readonly BandPart[], actions: { open: ()
))}
+ {hint === undefined ? null : (
+
+ {hint}
+
+ )}
diff --git a/hooks/register.tsx b/hooks/register.tsx
index 6b94584..64dd5e1 100644
--- a/hooks/register.tsx
+++ b/hooks/register.tsx
@@ -45,7 +45,7 @@ import { displayLinesOf, isOutputFileOf, jobBytesOf, jobLinesOf, oneLine, output
import { jobsStateOf, wakeupStateOf } from './state'
import { isStatusPrompt, normalizePrompt } from './prompt'
import { redirectFileOf, redirectTargetOf } from './redirect'
-import { bandOf, isBandHidden, paneModelOf, tableOf } from './view'
+import { bandHintOf, bandOf, isBandHidden, paneModelOf, tableOf } from './view'
/** The slash command, without its slash; declared here so that `claude plugin validate` resolves the matcher. */
const COMMAND = 'downrange'
@@ -715,7 +715,8 @@ export const register: Register = (on, options) => {
void pressed($, 'Hide')
},
}
- return bandTree($.ui.resolve(e), bandOf(jobs, wakeup, now, e.props.bodyColumns), actions, await next(e))
+ const parts = bandOf(jobs, wakeup, now, e.props.bodyColumns)
+ return bandTree($.ui.resolve(e), parts, bandHintOf(parts, e.props.bodyColumns), actions, await next(e))
}).catch(($, e, next) => passOn($, 'ui.render{component=AbovePrompt}', e, next))
on('ui.render', { component: 'Pane', requestId: PANE_ID }, async ($, e) => {
diff --git a/hooks/view.ts b/hooks/view.ts
index c96e77a..d0a6c05 100644
--- a/hooks/view.ts
+++ b/hooks/view.ts
@@ -248,6 +248,25 @@ export function bandOf(jobs: readonly Job[], wakeup: Wakeup | null, now: number,
return [...joined(counts, ' '), ...divider, ...joined(snippets, ' ยท ')]
}
+/** The band's sign of its keyboard path, drawn dim in the free room right before its first Button. */
+const BAND_KEY_HINT = 'ctrl+x tab to choose'
+
+/** The characters that Downrange itself draws in the band: glyphs, separators, and bar parts, each one cell wide. */
+const BAND_GLYPHS: ReadonlySet = new Set([...'\u00b7\u2014\u2026\u2265\u2501\u2502\u2578\u257a\u2588\u2589\u258a\u258b\u258c\u258d\u258e\u258f\u25a0\u25b0\u25b1\u25cc\u25cf\u25f7\u2713\u2717'])
+
+/** An upper bound of the display cells of text: one for ASCII and for the band's own glyphs, two for any other character, also a combining, joining, or format character. A wide character and an emoji sequence count at least as wide as a terminal draws them, so a room check never admits text that is wider than it measured. */
+export function cellsOf(text: string): number {
+ let cells = 0
+ for (const char of text) cells += (char.codePointAt(0) ?? 0) < 0x80 || BAND_GLYPHS.has(char) ? 1 : 2
+ return cells
+}
+
+/** The key hint when the row holds it after the band line, before the Buttons, with a one-cell gap; the band line keeps all its room, in display cells. */
+export function bandHintOf(parts: readonly BandPart[], columns: number): string | undefined {
+ const line = parts.reduce((sum, part) => sum + cellsOf(part.text), 0)
+ return line + 1 + cellsOf(BAND_KEY_HINT) + BAND_BUTTONS_COLUMNS <= columns ? BAND_KEY_HINT : undefined
+}
+
/** Hidden: the user pressed Hide, and no job started after that. */
export function isBandHidden(jobs: readonly Job[], hiddenAt: number | null): boolean {
return hiddenAt !== null && jobs.every(job => job.startedAt <= hiddenAt)