@cdoing/opentuicli 0.1.2 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,18 +18,41 @@ export interface Message {
18
18
  content: string;
19
19
  toolName?: string;
20
20
  toolStatus?: "running" | "done" | "error";
21
+ toolInput?: Record<string, any>;
21
22
  isError?: boolean;
22
23
  timestamp: number;
23
24
  }
24
25
 
25
- // ── Tool Icons ─────────────────────────────────────────
26
+ // ── Tool Config ───────────────────────────────────────
26
27
 
27
- const TOOL_ICONS: Record<string, string> = {
28
- file_read: "📖", file_write: "✏️", file_edit: "🔧", multi_edit: "🔧",
29
- shell_exec: "💻", file_run: "▶", glob_search: "🔍", grep_search: "🔎",
30
- codebase_search: "🔎", web_fetch: "🌐", web_search: "🔮", sub_agent: "🤖",
31
- todo: "📋", list_dir: "📁", view_diff: "📊", view_repo_map: "🗺️",
32
- code_verify: "✅", system_info: "ℹ️", ast_edit: "🌳", notebook_edit: "📓",
28
+ interface ToolConfig {
29
+ label: string;
30
+ icon: string;
31
+ verb: string;
32
+ }
33
+
34
+ const TOOL_CONFIG: Record<string, ToolConfig> = {
35
+ file_read: { label: "Read", icon: "◇", verb: "Reading" },
36
+ file_write: { label: "Write", icon: "◈", verb: "Writing" },
37
+ file_edit: { label: "Edit", icon: "◈", verb: "Editing" },
38
+ multi_edit: { label: "MultiEdit", icon: "◈", verb: "Editing" },
39
+ apply_patch: { label: "Patch", icon: "◈", verb: "Patching" },
40
+ shell_exec: { label: "Bash", icon: "$", verb: "Running" },
41
+ file_run: { label: "Run", icon: "▶", verb: "Running" },
42
+ glob_search: { label: "Search files", icon: "◎", verb: "Searching" },
43
+ grep_search: { label: "Search", icon: "◎", verb: "Searching" },
44
+ codebase_search: { label: "Codebase", icon: "◎", verb: "Searching" },
45
+ web_fetch: { label: "Fetch", icon: "◌", verb: "Fetching" },
46
+ web_search: { label: "Web Search", icon: "◌", verb: "Searching" },
47
+ sub_agent: { label: "Agent", icon: "◆", verb: "Running" },
48
+ todo: { label: "Todo", icon: "☐", verb: "Updating" },
49
+ list_dir: { label: "List Dir", icon: "├", verb: "Listing" },
50
+ view_diff: { label: "Diff", icon: "±", verb: "Viewing" },
51
+ view_repo_map: { label: "Repo Map", icon: "⊞", verb: "Mapping" },
52
+ code_verify: { label: "Verify", icon: "✓", verb: "Verifying" },
53
+ system_info: { label: "System Info", icon: "i", verb: "Checking" },
54
+ ast_edit: { label: "AST Edit", icon: "⌥", verb: "Editing" },
55
+ notebook_edit: { label: "Notebook", icon: "⊡", verb: "Editing" },
33
56
  };
34
57
 
35
58
  // ── Inline Markdown Helpers ──────────────────────────────
@@ -258,6 +281,7 @@ export function MessageList(props: {
258
281
  name={msg.toolName || "unknown"}
259
282
  content={msg.content}
260
283
  status={msg.toolStatus || (msg.isError ? "error" : "done")}
284
+ input={msg.toolInput}
261
285
  />
262
286
  );
263
287
  }
@@ -285,54 +309,238 @@ export function MessageList(props: {
285
309
  );
286
310
  }
287
311
 
312
+ // ── Tool Helpers ──────────────────────────────────────
313
+
314
+ function trimText(s: string, max: number): string {
315
+ const first = s.split("\n")[0] || "";
316
+ return first.length > max ? first.substring(0, max) + "…" : first;
317
+ }
318
+
319
+ function shortPath(p: string): string {
320
+ const home = process.env.HOME || "";
321
+ let s = (home && p.startsWith(home)) ? "~" + p.slice(home.length) : p;
322
+ // Show last 2 segments if too long
323
+ if (s.length > 50) {
324
+ const parts = s.split("/");
325
+ s = "…/" + parts.slice(-2).join("/");
326
+ }
327
+ return s;
328
+ }
329
+
330
+ function countLines(s: string): number {
331
+ return s ? s.split("\n").length : 0;
332
+ }
333
+
334
+ /** Extract a short description from tool input, per tool type */
335
+ function getToolDescription(name: string, input?: Record<string, any>): string {
336
+ if (!input) return "";
337
+ switch (name) {
338
+ case "file_read":
339
+ return input.file_path ? shortPath(input.file_path) : "";
340
+ case "file_write":
341
+ return input.file_path ? shortPath(input.file_path) : "";
342
+ case "file_edit":
343
+ case "multi_edit":
344
+ case "apply_patch":
345
+ return input.file_path ? shortPath(input.file_path) : "";
346
+ case "shell_exec":
347
+ return input.command ? trimText(input.command, 55) : "";
348
+ case "file_run":
349
+ return input.file_path ? shortPath(input.file_path) : "";
350
+ case "glob_search":
351
+ return input.pattern ? `"${trimText(input.pattern, 40)}"` : "";
352
+ case "grep_search":
353
+ return input.pattern ? `"${trimText(input.pattern, 30)}"${input.path ? ` in ${shortPath(input.path)}` : ""}` : "";
354
+ case "codebase_search":
355
+ return input.query ? `"${trimText(input.query, 40)}"` : "";
356
+ case "web_fetch":
357
+ return input.url ? trimText(input.url, 50) : "";
358
+ case "web_search":
359
+ return input.query ? `"${trimText(input.query, 40)}"` : "";
360
+ case "sub_agent":
361
+ return input.description ? trimText(input.description, 50) : "";
362
+ case "list_dir":
363
+ return input.path ? shortPath(input.path) : "";
364
+ default:
365
+ return input.description || "";
366
+ }
367
+ }
368
+
369
+ /** Get a summary of the output for the collapsed view */
370
+ function getOutputSummary(name: string, output: string, isError?: boolean): string {
371
+ if (!output) return "";
372
+ if (isError) return trimText(output, 50);
373
+
374
+ const lines = countLines(output);
375
+ switch (name) {
376
+ case "file_read":
377
+ return `${lines} lines`;
378
+ case "shell_exec":
379
+ case "file_run":
380
+ return lines > 1 ? `${lines} lines` : trimText(output, 40);
381
+ case "grep_search":
382
+ case "glob_search":
383
+ case "codebase_search": {
384
+ const results = output.split("\n").filter((l) => l.trim()).length;
385
+ return `${results} result${results !== 1 ? "s" : ""}`;
386
+ }
387
+ case "file_write":
388
+ case "file_edit":
389
+ case "multi_edit":
390
+ case "apply_patch":
391
+ return output.includes("+") || output.includes("-") ? trimText(output, 40) : "done";
392
+ default:
393
+ return lines > 1 ? `${lines} lines` : trimText(output, 40);
394
+ }
395
+ }
396
+
288
397
  // ── Tool Call Row ──────────────────────────────────────
289
398
 
290
399
  function ToolCallRow(props: {
291
400
  name: string;
292
401
  content: string;
293
402
  status: "running" | "done" | "error";
403
+ input?: Record<string, any>;
294
404
  }) {
295
405
  const { theme } = useTheme();
296
406
  const t = theme;
297
407
 
298
- const icon = TOOL_ICONS[props.name] || "⚙️";
299
- const statusIcon = (() => {
300
- switch (props.status) {
301
- case "running": return "⏳";
302
- case "done": return "✓";
303
- case "error": return "✗";
304
- }
305
- })();
306
- const statusColor = (() => {
307
- switch (props.status) {
308
- case "running": return t.toolRunning;
309
- case "done": return t.toolDone;
310
- case "error": return t.toolError;
311
- }
312
- })();
313
-
314
- const shortName = (() => {
315
- const names: Record<string, string> = {
316
- file_read: "Read", file_write: "Write", file_edit: "Edit",
317
- multi_edit: "MultiEdit", shell_exec: "Bash", glob_search: "Search files",
318
- grep_search: "Search code", web_fetch: "Fetch", sub_agent: "Agent",
319
- list_dir: "List dir", codebase_search: "Codebase search",
320
- };
321
- return names[props.name] || props.name.replace(/_/g, " ");
322
- })();
408
+ const config = TOOL_CONFIG[props.name] || { label: props.name.replace(/_/g, " "), icon: "⚙", verb: "Running" };
409
+ const isRunning = props.status === "running";
410
+ const isError = props.status === "error";
411
+
412
+ // Status indicator
413
+ const statusIcon = isRunning ? "" : isError ? "✗" : "✓";
414
+ const statusColor = isRunning ? t.toolRunning : isError ? t.toolError : t.toolDone;
415
+
416
+ // Description from input args
417
+ const description = getToolDescription(props.name, props.input);
418
+
419
+ // Output summary (shown after → arrow)
420
+ const outputSummary = !isRunning ? getOutputSummary(props.name, props.content, isError) : "";
421
+
422
+ // For shell commands, show the command inline
423
+ const isShell = props.name === "shell_exec" || props.name === "file_run";
424
+ const shellCmd = isShell && props.input?.command ? trimText(props.input.command, 55) : "";
425
+
426
+ // For file ops, show the path
427
+ const isFileOp = ["file_read", "file_write", "file_edit", "multi_edit", "apply_patch"].includes(props.name);
428
+ const filePath = isFileOp && props.input?.file_path ? shortPath(props.input.file_path) : "";
429
+
430
+ // For search ops, show the pattern
431
+ const isSearch = ["grep_search", "glob_search", "codebase_search"].includes(props.name);
432
+ const searchPattern = isSearch && (props.input?.pattern || props.input?.query) ?
433
+ trimText(props.input?.pattern || props.input?.query, 35) : "";
434
+
435
+ // Diff preview for edits
436
+ const hasEditDiff = (props.name === "file_edit" || props.name === "multi_edit") &&
437
+ props.input?.old_string && props.input?.new_string;
438
+ const oldStr = hasEditDiff ? trimText(props.input!.old_string, 50) : "";
439
+ const newStr = hasEditDiff ? trimText(props.input!.new_string, 50) : "";
440
+
441
+ // Output content lines (for expanded view)
442
+ const outputLines = props.content ? props.content.split("\n") : [];
443
+ const maxPreviewLines = 6;
444
+ const previewLines = outputLines.slice(0, maxPreviewLines);
445
+ const hasMoreLines = outputLines.length > maxPreviewLines;
323
446
 
324
447
  return (
325
- <box paddingX={2}>
326
- <text fg={statusColor}>{`${statusIcon} `}</text>
327
- <text fg={t.toolText}>{`${icon} ${shortName}`}</text>
328
- {props.content && (
329
- <text fg={t.textDim}>{` — ${trimText(props.content, 60)}`}</text>
448
+ <box flexDirection="column" paddingLeft={2} paddingRight={1}>
449
+ {/* ── Header row: status + icon + label + description + output summary ── */}
450
+ <box flexDirection="row" height={1}>
451
+ <text fg={statusColor} attributes={isRunning ? TextAttributes.BOLD : undefined}>
452
+ {`${statusIcon} `}
453
+ </text>
454
+ <text fg={t.textMuted}>{`${config.icon} `}</text>
455
+ <text fg={isRunning ? t.toolRunning : t.text} attributes={TextAttributes.BOLD}>
456
+ {isRunning ? `${config.verb}...` : config.label}
457
+ </text>
458
+
459
+ {/* Inline detail: file path, command, or search pattern */}
460
+ {filePath && !isRunning ? (
461
+ <text fg={t.info}>{` ${filePath}`}</text>
462
+ ) : shellCmd ? (
463
+ <text fg={isRunning ? t.textDim : t.textMuted}>{` $ ${shellCmd}`}</text>
464
+ ) : searchPattern ? (
465
+ <text fg={t.warning}>{` "${searchPattern}"`}</text>
466
+ ) : description && !filePath && !shellCmd ? (
467
+ <text fg={t.textDim}>{` ${description}`}</text>
468
+ ) : null}
469
+
470
+ {/* Output summary after arrow */}
471
+ {outputSummary && !isRunning ? (
472
+ <>
473
+ <text fg={t.textDim}>{" → "}</text>
474
+ <text fg={isError ? t.error : t.toolDone}>{outputSummary}</text>
475
+ </>
476
+ ) : null}
477
+ </box>
478
+
479
+ {/* ── Edit diff preview (for file_edit) ── */}
480
+ {hasEditDiff && !isRunning && (
481
+ <box flexDirection="column" paddingLeft={3}>
482
+ <box flexDirection="row" height={1}>
483
+ <text fg={t.diffRemove}>{` - ${oldStr}`}</text>
484
+ </box>
485
+ <box flexDirection="row" height={1}>
486
+ <text fg={t.diffAdd}>{` + ${newStr}`}</text>
487
+ </box>
488
+ </box>
489
+ )}
490
+
491
+ {/* ── Shell output preview (for bash/run) ── */}
492
+ {isShell && !isRunning && previewLines.length > 0 && (
493
+ <box flexDirection="column" paddingLeft={3}>
494
+ <box height={1}>
495
+ <text fg={t.border}>{" ┌" + "─".repeat(40)}</text>
496
+ </box>
497
+ {previewLines.map((line, i) => (
498
+ <box key={i} height={1}>
499
+ <text fg={t.textDim}>{` │ ${trimText(line, 55)}`}</text>
500
+ </box>
501
+ ))}
502
+ {hasMoreLines && (
503
+ <box height={1}>
504
+ <text fg={t.textDim}>{` │ … ${outputLines.length - maxPreviewLines} more lines`}</text>
505
+ </box>
506
+ )}
507
+ <box height={1}>
508
+ <text fg={t.border}>{" └" + "─".repeat(40)}</text>
509
+ </box>
510
+ </box>
511
+ )}
512
+
513
+ {/* ── Search results preview ── */}
514
+ {isSearch && !isRunning && previewLines.length > 0 && (
515
+ <box flexDirection="column" paddingLeft={3}>
516
+ {previewLines.slice(0, 4).map((line, i) => (
517
+ <box key={i} height={1}>
518
+ <text fg={t.textDim}>{` ${trimText(line, 60)}`}</text>
519
+ </box>
520
+ ))}
521
+ {outputLines.length > 4 && (
522
+ <box height={1}>
523
+ <text fg={t.textDim}>{` … ${outputLines.length - 4} more`}</text>
524
+ </box>
525
+ )}
526
+ </box>
527
+ )}
528
+
529
+ {/* ── Error output ── */}
530
+ {isError && props.content && (
531
+ <box flexDirection="column" paddingLeft={3}>
532
+ <box height={1}>
533
+ <text fg={t.error}>{` ✗ ${trimText(props.content, 70)}`}</text>
534
+ </box>
535
+ </box>
536
+ )}
537
+
538
+ {/* ── Running indicator for shell commands ── */}
539
+ {isShell && isRunning && (
540
+ <box paddingLeft={3} height={1}>
541
+ <text fg={t.toolRunning}>{" ▊ Executing command..."}</text>
542
+ </box>
330
543
  )}
331
544
  </box>
332
545
  );
333
546
  }
334
-
335
- function trimText(s: string, max: number): string {
336
- const first = s.split("\n")[0] || "";
337
- return first.length > max ? first.substring(0, max) + "…" : first;
338
- }
@@ -20,7 +20,7 @@ const OPTIONS = [
20
20
  ];
21
21
 
22
22
  export function PermissionPrompt(props: PermissionPromptProps) {
23
- const { theme } = useTheme();
23
+ const { theme, customBg } = useTheme();
24
24
  const t = theme;
25
25
  const [selected, setSelected] = useState(0);
26
26
 
@@ -44,6 +44,7 @@ export function PermissionPrompt(props: PermissionPromptProps) {
44
44
  <box
45
45
  borderStyle="single"
46
46
  borderColor={t.warning}
47
+ backgroundColor={customBg || t.bg}
47
48
  paddingX={1}
48
49
  paddingY={0}
49
50
  flexDirection="column"
@@ -6,10 +6,11 @@
6
6
  * - Enter to resume a conversation
7
7
  * - d to delete, f to fork, v to view
8
8
  * - Escape to close
9
+ * - Native <scrollbox> for smooth scrolling
9
10
  */
10
11
 
11
12
  import { TextAttributes } from "@opentui/core";
12
- import { useState, useMemo } from "react";
13
+ import { useState } from "react";
13
14
  import { useKeyboard, useTerminalDimensions } from "@opentui/react";
14
15
  import { useTheme } from "../context/theme";
15
16
  import {
@@ -26,7 +27,7 @@ export interface SessionBrowserProps {
26
27
  }
27
28
 
28
29
  export function SessionBrowser(props: SessionBrowserProps) {
29
- const { theme } = useTheme();
30
+ const { theme, customBg } = useTheme();
30
31
  const t = theme;
31
32
  const dims = useTerminalDimensions();
32
33
 
@@ -38,13 +39,6 @@ export function SessionBrowser(props: SessionBrowserProps) {
38
39
 
39
40
  const maxVisible = Math.max(5, Math.floor((dims.height || 20) - 10));
40
41
 
41
- const scrollOffset = useMemo(() => {
42
- if (selected < maxVisible) return 0;
43
- return selected - maxVisible + 1;
44
- }, [selected, maxVisible]);
45
-
46
- const visibleConvs = conversations.slice(scrollOffset, scrollOffset + maxVisible);
47
-
48
42
  useKeyboard((key: any) => {
49
43
  if (viewMode) {
50
44
  // View mode controls
@@ -114,16 +108,19 @@ export function SessionBrowser(props: SessionBrowserProps) {
114
108
  <box
115
109
  borderStyle="single"
116
110
  borderColor={t.primary}
111
+ backgroundColor={customBg || t.bg}
117
112
  paddingX={2}
118
113
  paddingY={1}
119
114
  flexDirection="column"
120
115
  flexGrow={1}
121
116
  >
122
- <text fg={t.primary} attributes={TextAttributes.BOLD}>
123
- {"Sessions"}
124
- </text>
117
+ <box flexDirection="row" flexShrink={0}>
118
+ <text fg={t.primary} attributes={TextAttributes.BOLD} flexGrow={1}>
119
+ {"Sessions"}
120
+ </text>
121
+ <text fg={t.textDim}>{"esc"}</text>
122
+ </box>
125
123
  <text fg={t.textDim}>{"\nNo saved conversations.\n"}</text>
126
- <text fg={t.textMuted}>{"Esc Close"}</text>
127
124
  </box>
128
125
  );
129
126
  }
@@ -140,29 +137,36 @@ export function SessionBrowser(props: SessionBrowserProps) {
140
137
  borderStyle="single"
141
138
  borderColor={t.primary}
142
139
  paddingX={1}
143
- paddingY={0}
140
+ paddingY={1}
144
141
  flexDirection="column"
145
142
  flexGrow={1}
146
143
  >
147
- <text fg={t.primary} attributes={TextAttributes.BOLD}>
148
- {`Viewing: ${conv?.title || "Untitled"}`}
149
- </text>
150
- <text fg={t.textDim}>
144
+ <box flexDirection="row" flexShrink={0}>
145
+ <text fg={t.primary} attributes={TextAttributes.BOLD} flexGrow={1}>
146
+ {`Viewing: ${conv?.title || "Untitled"}`}
147
+ </text>
148
+ <text fg={t.textDim}>{"esc"}</text>
149
+ </box>
150
+ <text fg={t.textDim} flexShrink={0}>
151
151
  {`${viewScroll + 1}–${Math.min(viewScroll + maxVisible, total)} of ${total} messages`}
152
152
  </text>
153
- <text>{""}</text>
154
- {visibleMsgs.map((m, i) => {
155
- const prefix = m.role === "user" ? "❯" : "◆";
156
- const color = m.role === "user" ? t.success : t.text;
157
- const content = m.content.length > 120 ? m.content.substring(0, 117) + "..." : m.content;
158
- return (
159
- <text key={`view-${viewScroll + i}`} fg={color}>
160
- {` ${prefix} ${content.replace(/\n/g, " ")}`}
161
- </text>
162
- );
163
- })}
164
- <text>{""}</text>
165
- <text fg={t.textMuted}>{" ↑↓ Scroll Enter Resume Esc Back"}</text>
153
+ <text flexShrink={0}>{""}</text>
154
+ <scrollbox flexGrow={1}>
155
+ <box flexShrink={0}>
156
+ {visibleMsgs.map((m, i) => {
157
+ const prefix = m.role === "user" ? "" : "◆";
158
+ const color = m.role === "user" ? t.success : t.text;
159
+ const content = m.content.length > 120 ? m.content.substring(0, 117) + "..." : m.content;
160
+ return (
161
+ <text key={`view-${viewScroll + i}`} fg={color}>
162
+ {` ${prefix} ${content.replace(/\n/g, " ")}`}
163
+ </text>
164
+ );
165
+ })}
166
+ </box>
167
+ </scrollbox>
168
+ <text flexShrink={0}>{""}</text>
169
+ <text fg={t.textMuted} flexShrink={0}>{" ↑↓ Scroll Enter Resume Esc Back"}</text>
166
170
  </box>
167
171
  );
168
172
  }
@@ -172,49 +176,56 @@ export function SessionBrowser(props: SessionBrowserProps) {
172
176
  <box
173
177
  borderStyle="single"
174
178
  borderColor={t.primary}
179
+ backgroundColor={customBg || t.bg}
175
180
  paddingX={1}
176
- paddingY={0}
181
+ paddingY={1}
177
182
  flexDirection="column"
178
183
  flexGrow={1}
179
184
  >
180
- <text fg={t.primary} attributes={TextAttributes.BOLD}>
181
- {"Sessions"}
182
- </text>
183
- <text fg={t.textDim}>
185
+ <box flexDirection="row" flexShrink={0}>
186
+ <text fg={t.primary} attributes={TextAttributes.BOLD} flexGrow={1}>
187
+ {"Sessions"}
188
+ </text>
189
+ <text fg={t.textDim}>{"esc"}</text>
190
+ </box>
191
+ <text fg={t.textDim} flexShrink={0}>
184
192
  {`${conversations.length} conversation${conversations.length !== 1 ? "s" : ""}`}
185
193
  </text>
186
- <text>{""}</text>
187
- {visibleConvs.map((conv, i) => {
188
- const idx = scrollOffset + i;
189
- const isSelected = idx === selected;
190
- const date = formatRelativeDate(conv.updatedAt);
191
- const msgCount = conv.messages.filter((m) => m.role === "user").length;
192
- const title = conv.title.length > 40 ? conv.title.substring(0, 37) + "..." : conv.title;
194
+ <text flexShrink={0}>{""}</text>
195
+ <scrollbox flexGrow={1}>
196
+ <box flexShrink={0}>
197
+ {conversations.map((conv, idx) => {
198
+ const isSelected = idx === selected;
199
+ const date = formatRelativeDate(conv.updatedAt);
200
+ const msgCount = conv.messages.filter((m) => m.role === "user").length;
201
+ const title = conv.title.length > 40 ? conv.title.substring(0, 37) + "..." : conv.title;
193
202
 
194
- return (
195
- <box key={conv.id} flexDirection="row">
196
- <text
197
- fg={isSelected ? t.primary : t.textMuted}
198
- attributes={isSelected ? TextAttributes.BOLD : undefined}
199
- >
200
- {` ${isSelected ? "❯" : " "} ${title}`}
201
- </text>
202
- <text fg={t.textDim}>
203
- {` ${date} (${msgCount} msgs)`}
204
- </text>
205
- </box>
206
- );
207
- })}
203
+ return (
204
+ <box key={conv.id} flexDirection="row">
205
+ <text
206
+ fg={isSelected ? t.primary : t.textMuted}
207
+ attributes={isSelected ? TextAttributes.BOLD : undefined}
208
+ >
209
+ {` ${isSelected ? "❯" : " "} ${title}`}
210
+ </text>
211
+ <text fg={t.textDim}>
212
+ {` ${date} (${msgCount} msgs)`}
213
+ </text>
214
+ </box>
215
+ );
216
+ })}
217
+ </box>
218
+ </scrollbox>
208
219
  {confirmDelete && (
209
- <box>
220
+ <box flexShrink={0}>
210
221
  <text>{""}</text>
211
222
  <text fg={t.warning} attributes={TextAttributes.BOLD}>
212
223
  {` Delete "${conversations[selected]?.title}"? (y/n)`}
213
224
  </text>
214
225
  </box>
215
226
  )}
216
- <text>{""}</text>
217
- <text fg={t.textMuted}>{" ↑↓ Navigate Enter Resume v View d Delete f Fork Esc Close"}</text>
227
+ <text flexShrink={0}>{""}</text>
228
+ <text fg={t.textMuted} flexShrink={0}>{" ↑↓ Navigate Enter Resume v View d Delete f Fork Esc Close"}</text>
218
229
  </box>
219
230
  );
220
231
  }
@@ -18,10 +18,10 @@ export function SessionFooter(props: SessionFooterProps) {
18
18
  ? "~" + props.workingDir.slice(home.length)
19
19
  : props.workingDir;
20
20
 
21
- const shortcuts = "^N:New ^P:Model ^T:Theme ^S:Sessions ^B:Sidebar";
21
+ const shortcuts = "^P:Commands ^O:Model ^T:Theme ^S:Sessions ^B:Sidebar";
22
22
 
23
23
  return (
24
- <box height={1} flexDirection="row">
24
+ <box height={1} flexDirection="row" backgroundColor={t.bgSubtle}>
25
25
  <text fg={t.textDim}>{` ${shortDir}`}</text>
26
26
  <box flexGrow={1} />
27
27
  <text fg={t.textMuted}>{`${shortcuts} `}</text>
@@ -28,7 +28,7 @@ export function SessionHeader(props: SessionHeaderProps) {
28
28
  const left = ` ◆ ${props.title || "Session"} │ ${props.provider}/${props.model} │ ${inTok}→${outTok} tokens`;
29
29
 
30
30
  return (
31
- <box height={1} flexDirection="row">
31
+ <box height={1} flexDirection="row" backgroundColor={t.bgSubtle}>
32
32
  <text fg={t.primary} attributes={TextAttributes.BOLD}>{left}</text>
33
33
  <text fg={t.border}>{" │ "}</text>
34
34
  <text fg={pctColor}>{`${pct}%`}</text>