Skip to content

Commit 2bc9dd1

Browse files
committed
feat: type-ahead queue, expanded verbs, VS Code tab title
- Mid-turn typing: pressing Enter while the agent is busy now queues the prompt instead of dropping it. Drains automatically (one at a time, in order) the moment the agent goes idle. Status pane shows "↩ queued (N): <preview>" on enqueue. Ctrl-C while busy clears the queue along with aborting the turn. Slash commands and `!cmd` shell escapes still run immediately — they don't talk to the agent. - THINKING_VERBS expanded from 12 to 34 engineering-themed verbs. Mix of standard build/compile/index/parse + dev-humor (yak-shaving, reticulating, hoisting, refactoring). All clearly "machine doing work." - VS Code / Cursor terminal tabs showed "node" despite our OSC 0 write because their default tab-title template reads ${process} (the OS process name), not ${sequence}. Setting process.title alongside the OSC escape closes the gap — both channels updated. Implementation note on the queue: kept it simple, single React state array with a useEffect that drains on status === "idle". CC's queue is priority-aware and accepts items from multiple sources (notifications, permissions); we have one source (user input), so we don't need the elaboration.
1 parent e9e5cd1 commit 2bc9dd1

4 files changed

Lines changed: 86 additions & 12 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codebase-cli",
3-
"version": "2.0.0-pre.49",
3+
"version": "2.0.0-pre.50",
44
"description": "Codebase CLI — a TypeScript coding agent on the pi-mono runtime. OAuth-aware, any LLM provider, single install.",
55
"keywords": [
66
"ai",

src/ui/App.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ function ChatApp({ initialBundle, onExit }: ChatAppProps) {
107107
const [tasks, setTasks] = useState<readonly Task[]>(() => bundle.toolContext.tasks.list());
108108
const inputRef = useRef<InputHandle | null>(null);
109109
const [modelPickerOpen, setModelPickerOpen] = useState(false);
110+
// Prompts typed while the agent is busy. The bottom of the queue is
111+
// dispatched automatically when the agent goes idle. Lets the user
112+
// stack the next thing while the current turn is still running, the
113+
// CC-style "type ahead" pattern.
114+
const [queuedPrompts, setQueuedPrompts] = useState<readonly string[]>([]);
110115

111116
const registry = useMemo(() => {
112117
const reg = new CommandRegistry();
@@ -159,7 +164,23 @@ function ChatApp({ initialBundle, onExit }: ChatAppProps) {
159164

160165
const { suggestion, dismiss: dismissSuggestion } = usePromptSuggestion(bundle, state.status, state.messages.length);
161166

167+
// Hold the freshest handleSubmit in a ref so the drain effect below
168+
// can call into it without subscribing to every dependency change. The
169+
// effect only re-runs when status or queue length flips — calling
170+
// .current at fire time gives it the latest closure.
171+
const handleSubmitRef = useRef<((text: string) => Promise<void>) | null>(null);
172+
162173
const handleSubmit = async (text: string) => {
174+
// Mid-turn typing: if the agent is mid-run, queue the prompt instead
175+
// of dropping or interrupting. Slash commands and `!cmd` shell
176+
// escapes bypass the queue — they don't talk to the agent, so they
177+
// can run alongside whatever the agent is doing.
178+
if (busy && !text.startsWith("/") && !text.startsWith("!")) {
179+
setQueuedPrompts((q) => [...q, text]);
180+
const preview = text.length > 60 ? `${text.slice(0, 60)}…` : text;
181+
appendStatus(`↩ queued (${queuedPrompts.length + 1}): ${preview}`);
182+
return;
183+
}
163184
// `!cmd` runs a shell command directly without involving the LLM —
164185
// "I just want to check something real quick." We bypass the agent
165186
// loop entirely and inject the output as a synthetic user /
@@ -242,6 +263,19 @@ function ChatApp({ initialBundle, onExit }: ChatAppProps) {
242263
});
243264
};
244265

266+
handleSubmitRef.current = handleSubmit;
267+
268+
// Drain queued prompts one at a time when the agent goes idle. The
269+
// effect runs on every status / queue-length change, but the early
270+
// return keeps the body cheap until both conditions are met.
271+
useEffect(() => {
272+
if (state.status !== "idle") return;
273+
if (queuedPrompts.length === 0) return;
274+
const [next, ...rest] = queuedPrompts;
275+
setQueuedPrompts(rest);
276+
void handleSubmitRef.current?.(next);
277+
}, [state.status, queuedPrompts]);
278+
245279
// Ctrl-C semantics, in priority order:
246280
// 1. Open overlay (Permission, UserQuery) → dismiss it. Never trap
247281
// the user behind a prompt with no escape.
@@ -288,6 +322,12 @@ function ChatApp({ initialBundle, onExit }: ChatAppProps) {
288322
if (busy) {
289323
bundle.agent.abort();
290324
dispatch({ type: "abort" });
325+
// Cancel anything the user queued for after this turn — they
326+
// just hit Ctrl-C, the queue is no longer what they want.
327+
if (queuedPrompts.length > 0) {
328+
setQueuedPrompts([]);
329+
appendStatus(`(dropped ${queuedPrompts.length} queued prompt${queuedPrompts.length === 1 ? "" : "s"})`);
330+
}
291331
// No hint here — the abort itself is the feedback. The
292332
// exit window is set silently so a quick second tap still
293333
// gets the user out without confirmation theater.
@@ -402,7 +442,6 @@ function ChatApp({ initialBundle, onExit }: ChatAppProps) {
402442
) : (
403443
<Input
404444
ref={inputRef}
405-
disabled={busy}
406445
onSubmit={handleSubmit}
407446
onAbort={handleAbort}
408447
commands={commandSuggestions}

src/ui/Status.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,29 @@ const THINKING_VERBS = [
4646
"Linking",
4747
"Optimizing",
4848
"Stashing",
49+
"Bisecting",
50+
"Memoizing",
51+
"Tokenizing",
52+
"Hoisting",
53+
"Reticulating",
54+
"Refactoring",
55+
"Bundling",
56+
"Pruning",
57+
"Spawning",
58+
"Crunching",
59+
"Marshaling",
60+
"Currying",
61+
"Folding",
62+
"Reducing",
63+
"Lexing",
64+
"Yak-shaving",
65+
"Caching",
66+
"Threading",
67+
"Vendoring",
68+
"Inlining",
69+
"Hashing",
70+
"Allocating",
71+
"Branching",
4972
];
5073

5174
const STATUS_LABEL: Record<ChatState["status"], string> = {

src/ui/terminal-title.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
11
/**
2-
* Set the terminal window/tab title via OSC 0. Honored by all modern
3-
* terminals (xterm, iTerm2, Ghostty, kitty) plus the integrated
4-
* terminals in VS Code, Cursor, JetBrains IDEs — so "node" tabs become
5-
* "codebase". TMUX/screen forward this through to the host emulator.
2+
* Set the terminal window/tab title via two independent channels:
63
*
7-
* No-op on non-TTY stdouts (piped output, CI logs) so we don't pollute
8-
* captured output with stray escape bytes.
4+
* 1. OSC 0 escape — read by xterm, iTerm2, Ghostty, kitty, alacritty,
5+
* gnome-terminal, konsole, and tmux/screen forward it through. This
6+
* is the standard.
7+
*
8+
* 2. process.title — VS Code and Cursor's integrated terminals show
9+
* "node" by default because their tab-title template defaults to
10+
* `${process}`, which reads the OS process name rather than the OSC
11+
* sequence. Setting process.title changes what the OS reports, so
12+
* VS Code's default template picks up our chosen name.
13+
*
14+
* Both writes are no-ops on non-TTY stdouts (piped output, CI logs)
15+
* so we don't pollute captured output with stray escape bytes.
916
*
1017
* We intentionally don't try to "restore" the prior title on exit:
11-
* there's no portable way to read the current title, and on shell exit
12-
* the parent shell's PROMPT_COMMAND / precmd hook will set it back
13-
* within milliseconds. Setting it to empty would be worse than leaving
14-
* our title in place during the brief gap.
18+
* there's no portable way to read the current title, and the parent
19+
* shell's prompt hook resets it within milliseconds anyway.
1520
*/
1621
export function setTerminalTitle(title: string): void {
1722
if (!process.stdout.isTTY) return;
1823
process.stdout.write(`\x1b]0;${title}\x07`);
24+
try {
25+
process.title = title;
26+
} catch {
27+
// On some platforms / sandboxed environments process.title is
28+
// read-only or restricted. The OSC write is the main path; this
29+
// is just additional coverage for editor terminals.
30+
}
1931
}

0 commit comments

Comments
 (0)