Skip to content

Commit efe6f12

Browse files
authored
ui: show the ctrl+c quit hint on the meta line for 3s, truncate the meta line to the pane (#120)
1 parent 9e57105 commit efe6f12

2 files changed

Lines changed: 22 additions & 11 deletions

File tree

src/ui/ConnectApp.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -815,14 +815,14 @@ export function ConnectApp(props: ConnectAppProps): React.ReactElement {
815815
if (working && canSend) submit('/stop')
816816
},
817817
)
818-
// The notice bar doubles as the ctrl+c prompt: armed, it says what a second
819-
// press does, so the quit is never a surprise.
818+
// Armed, the meta line below the composer becomes the ctrl+c prompt (it
819+
// says what a second press does), and the arm expires after a few seconds.
820820
// The browser is for reading, not sending: it drops the composer for the rows
821821
// (a whole screen of conversation is the point of opening it) and says so on
822822
// the notice line, which is where the app's one line of transient guidance
823823
// already lives.
824824
const browserNotice = '↑↓ scroll · → open · ← close · ctrl+r expand all · esc back to the chat'
825-
const shownNotice = windowed ? browserNotice : ctrlCArmed ? CTRL_C_QUIT_HINT : notice
825+
const shownNotice = windowed ? browserNotice : notice
826826
const { viewBudget, padRows, composerRows, noticeRows, menuRows } = useMemo(() => {
827827
// Both wrapping parts of the footer are measured as the rows they will
828828
// actually OCCUPY, not as the newlines they contain: a notice ("stream
@@ -1479,9 +1479,10 @@ export function ConnectApp(props: ConnectAppProps): React.ReactElement {
14791479
const totalStr = `$${(serverCostUsd ?? cost.total ?? 0).toFixed(2)}`
14801480
// Sized to fit by construction: Ink measures the OSC-8 hyperlink's
14811481
// invisible URL bytes as width, so a linked id only ships when the whole
1482-
// line — escape bytes included — fits the pane; otherwise the id renders
1483-
// as plain text, shortened if even that overflows. Never rely on Ink
1484-
// wrapping/truncating this line: it's budgeted at exactly one row.
1482+
// line — escape bytes included — fits the pane; otherwise the line renders
1483+
// as plain text, cut to the pane with a … if even that overflows. Never
1484+
// rely on Ink wrapping/truncating this line: it's budgeted at exactly one
1485+
// row.
14851486
const metaParts = (id: string): string[] => [
14861487
`${statusWord} · ${totalStr} total`,
14871488
...(props.model ? [props.model] : []),
@@ -1491,12 +1492,13 @@ export function ConnectApp(props: ConnectAppProps): React.ReactElement {
14911492
]
14921493
const linked = metaParts(hyperlink(props.sessionUrl, sessionId)).join(' · ')
14931494
const plain = metaParts(sessionId).join(' · ')
1494-
const metaLine =
1495-
linked.length < cols
1495+
const metaLine = ctrlCArmed
1496+
? CTRL_C_QUIT_HINT
1497+
: linked.length < cols
14961498
? linked
14971499
: plain.length < cols
14981500
? plain
1499-
: metaParts(`${sessionId.slice(0, 20)}…`).join(' · ')
1501+
: `${plain.slice(0, Math.max(0, cols - 2))}…`
15001502
return (
15011503
// Hosted panes pin BOTH dimensions: without the width the root sizes to
15021504
// its widest child (the unwrapped meta line) and smears rows across the

src/ui/ctrlC.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from 'react'
1+
import { useEffect, useState } from 'react'
22
import { useApp, useInput } from 'ink'
33

44
// ctrl+c, everywhere in the UI: the first press interrupts (whatever the pane
@@ -10,10 +10,19 @@ import { useApp, useInput } from 'ink'
1010
// Every pane that owns the keyboard mounts this, and only the pane with focus
1111
// is active — so the armed flag is per-pane, and one press can't arm a handler
1212
// that a later press won't reach. Returns whether the quit is armed, for the
13-
// pane to prompt with.
13+
// pane to prompt with. The arm expires on its own after a few seconds, so the
14+
// prompt never lingers and a much-later ctrl+c interrupts again instead of
15+
// quitting.
16+
const ARM_MS = 3000
17+
1418
export function useCtrlCQuit(active: boolean, onInterrupt?: () => void): boolean {
1519
const { exit } = useApp()
1620
const [armed, setArmed] = useState(false)
21+
useEffect(() => {
22+
if (!armed) return
23+
const t = setTimeout(() => setArmed(false), ARM_MS)
24+
return () => clearTimeout(t)
25+
}, [armed])
1726
useInput(
1827
(ch, key) => {
1928
if (key.ctrl && ch === 'c') {

0 commit comments

Comments
 (0)