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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ is migrated on first use — your existing login becomes a host named for its AP
base.

The config file also carries UI preferences. `"sessionBar"` scopes the session
list, which the interactive UI opens as a full screen with `ctrl+j`:
list, which the interactive UI opens as a full screen with `esc`:

```json
{
Expand Down
709 changes: 107 additions & 602 deletions src/ui/ConnectApp.tsx

Large diffs are not rendered by default.

25 changes: 10 additions & 15 deletions src/ui/SessionsApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,14 @@ import { ConnectApp } from './ConnectApp'
// the trackpad and select/copy are the terminal's own — which is the reason
// for the screen split. Nothing may be pinned above or below it: rows
// scrolling past would run straight through any such band.
// * the session picker (ctrl+j, or esc / ↓ out of the chat) takes over the
// * the session picker (esc or ↓ out of the chat) takes over the
// ALTERNATE screen: the full session list, with the chat left untouched on
// the primary buffer behind it. enter opens a session and returns.
// * the transcript browser (ctrl+r, owned by ConnectApp) is the other
// alternate-screen view: the windowed transcript with folding and ↑/↓ nav,
// which the scrollback view cannot do.
// * the new-session composer and the loading placeholder do own their frame,
// so they keep the header band and the frame inset.
//
// Focus is modal and esc steps outward: inside the chat esc closes the browser,
// then transcript navigation, then opens the picker. Exactly one useInput
// handler is active at a time.
// Focus is modal: esc in the chat opens the picker, esc in the picker goes back.
// Exactly one useInput handler is active at a time.
//
// Liveness: ONE WebSocket — the focused session's, owned by its ConnectApp —
// plus a 5s REST poll of the session list for the nav. Transcript stores are
Expand Down Expand Up @@ -130,7 +126,7 @@ export interface SessionsAppProps {
initialConfigName?: string
initialNotice?: string
// Which sessions reach the picker. `hidden` drops it entirely — focus then
// never leaves the chat, so esc, ↓ at the bottom edge and ctrl+j do nothing.
// never leaves the chat, so esc and ↓ at the bottom edge do nothing.
// Set under "sessionBar" in the config file.
sessionBar: ResolvedSessionBar
// Builds the start request for a composer-spawned session (the entry point
Expand Down Expand Up @@ -255,8 +251,8 @@ export function SessionsApp(props: SessionsAppProps): React.ReactElement {
// Both openings start with the main pane focused: a connect lands you in
// the conversation, a bare `agent` lands you in the new-session composer.
// 'nav' = the session picker owns the keyboard, which now means it is OPEN:
// the session list is a screen of its own on the alternate buffer (ctrl+j, or
// ↓/esc out of the chat) rather than a band pinned under every frame.
// the session list is a screen of its own on the alternate buffer ( or esc
// out of the chat) rather than a band pinned under every frame.
//
// It moved there because the chat gave up its viewport: the transcript is
// printed into the terminal's real scrollback now, and scrollback is
Expand Down Expand Up @@ -609,11 +605,10 @@ export function SessionsApp(props: SessionsAppProps): React.ReactElement {
shownChats.current.add(mainPane.sessionId)
main = (
// The chat, owning the terminal rather than sitting in a pane: no
// width/height box around it and no pane props, which is what puts
// ConnectApp in its scrollback view (settled transcript printed into the
// terminal's own scrollback, native wheel and select/copy, and ctrl+r for
// the full-screen browser). It renders its own meta line again, since
// there is no header band above it to carry one.
// width/height box around it and no pane props, so its settled transcript
// prints into the terminal's own scrollback and the wheel and select/copy
// are the terminal's. It renders its own meta line again, since there is
// no header band above it to carry one.
<ConnectApp
key={mainPane.sessionId}
scrollbackBreak={needsBreak}
Expand Down
6 changes: 3 additions & 3 deletions src/ui/altScreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { useApp, useStdout } from 'ink'
// `alternateScreen` only as a render()-time option and this app has to hop
// buffers at runtime under ONE live ink instance: the default view lives in the
// primary buffer with its settled transcript flushed to real scrollback, and
// the full-frame screens (picker, composer, browser) take over the alt screen
// and hand the primary buffer back untouched on exit.
// the full-frame screens (picker, composer) take over the alt screen and hand
// the primary buffer back untouched on exit.
//
// The switch goes through ink's suspendTerminal, which is the only way to keep
// ink's picture of the screen honest across it: suspend ERASES the current
Expand All @@ -27,7 +27,7 @@ export function useAltScreen(active: boolean): boolean {
const { suspendTerminal } = useApp()
const { stdout } = useStdout()
// Transitions are serialized: suspendTerminal throws if the terminal is
// already suspended, and two fast toggles (ctrl+r then esc) would otherwise
// already suspended, and two fast toggles (esc then enter) would otherwise
// overlap. Errors are swallowed — failing to hop buffers must not take the
// session down.
const queue = useRef<Promise<void>>(Promise.resolve())
Expand Down
9 changes: 4 additions & 5 deletions src/ui/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// forwarded. Silently sending "/stpo" to the agent as prose is the failure mode
// that rule exists to prevent.

export type CommandId = 'stop' | 'transcript' | 'sessions' | 'exit'
export type CommandId = 'stop' | 'sessions' | 'exit'

export type SlashCommand = {
id: CommandId
Expand All @@ -18,12 +18,11 @@ export type SlashCommand = {
detail: string
}

// Every command, in menu order: the two that act on the session first, then the
// screens, then the way out.
// Every command, in menu order: the one that acts on the session first, then the
// screen, then the way out.
export const SLASH_COMMANDS: readonly SlashCommand[] = [
{ id: 'stop', name: 'stop', detail: 'interrupt the agent, keeping the conversation' },
{ id: 'transcript', name: 'transcript', detail: 'browse the full transcript (ctrl+r)' },
{ id: 'sessions', name: 'sessions', detail: 'switch sessions (ctrl+j)' },
{ id: 'sessions', name: 'sessions', detail: 'switch sessions (esc)' },
{ id: 'exit', name: 'exit', aliases: ['quit'], detail: 'leave the CLI; the session keeps running' },
]

Expand Down
Loading