@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.
- package/README.md +91 -0
- package/dist/index.js +53 -38
- package/dist/index.js.map +4 -4
- package/package.json +5 -3
- package/src/app.tsx +260 -39
- package/src/components/dialog-command.tsx +110 -107
- package/src/components/dialog-help.tsx +48 -124
- package/src/components/dialog-model.tsx +98 -49
- package/src/components/dialog-status.tsx +46 -84
- package/src/components/dialog-theme.tsx +197 -171
- package/src/components/input-area.tsx +74 -12
- package/src/components/message-list.tsx +250 -42
- package/src/components/permission-prompt.tsx +2 -1
- package/src/components/session-browser.tsx +71 -60
- package/src/components/session-footer.tsx +2 -2
- package/src/components/session-header.tsx +1 -1
- package/src/components/setup-wizard.tsx +149 -70
- package/src/components/sidebar.tsx +66 -13
- package/src/components/status-bar.tsx +2 -2
- package/src/context/theme.tsx +109 -1
- package/src/lib/autocomplete.ts +5 -1
- package/src/routes/home.tsx +2 -2
- package/src/routes/session.tsx +141 -18
- package/src/store/settings.ts +107 -0
|
@@ -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
|
|
26
|
+
// ── Tool Config ───────────────────────────────────────
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
|
326
|
-
|
|
327
|
-
<
|
|
328
|
-
|
|
329
|
-
|
|
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
|
|
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
|
-
<
|
|
123
|
-
{
|
|
124
|
-
|
|
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={
|
|
140
|
+
paddingY={1}
|
|
144
141
|
flexDirection="column"
|
|
145
142
|
flexGrow={1}
|
|
146
143
|
>
|
|
147
|
-
<
|
|
148
|
-
{
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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={
|
|
181
|
+
paddingY={1}
|
|
177
182
|
flexDirection="column"
|
|
178
183
|
flexGrow={1}
|
|
179
184
|
>
|
|
180
|
-
<
|
|
181
|
-
{
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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 = "^
|
|
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>
|