Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<img src="https://img.shields.io/badge/Claude_Code-2.1.288%2B-D97757" alt="Claude Code 2.1.288+">
<img src="https://img.shields.io/badge/Claude_Code-mod-555555" alt="Claude Code mod">
<img src="https://img.shields.io/badge/license-MIT-78997A" alt="License: MIT">
<img src="https://img.shields.io/badge/version-1.1.2-555555" alt="Version: 1.1.2">
<img src="https://img.shields.io/badge/version-1.1.3-555555" alt="Version: 1.1.3">
</p>

<p align="center"><strong>Progress and an ETA for background shells and Monitor watches, read from their real output or past runs, with no model request.</strong></p>
Expand Down Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
9 changes: 7 additions & 2 deletions hooks/drawing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ const PART_STYLES: Readonly<Record<BandPart['tone'], { color?: string; dimColor?
done: { dimColor: true },
}

/** The band: one line of counts and snippets, Open and Hide, and what other mods drew beneath. */
export function bandTree(ui: Ui, parts: readonly BandPart[], actions: { open: () => 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 (
<Box flexDirection="column">
Expand All @@ -23,6 +23,11 @@ export function bandTree(ui: Ui, parts: readonly BandPart[], actions: { open: ()
))}
</Text>
</Box>
{hint === undefined ? null : (
<Text key="hint" dimColor wrap="truncate-end">
{hint}
</Text>
)}
<Button key="open" label="Open" hotkey="j" onPress={actions.open} />
<Button key="hide" label="Hide" hotkey="h" dimColor onPress={actions.hide} />
</Box>
Expand Down
5 changes: 3 additions & 2 deletions hooks/register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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) => {
Expand Down
19 changes: 19 additions & 0 deletions hooks/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> = 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)
Expand Down