mini-coder 0.5.0 → 0.5.2
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 +2 -0
- package/bun.lock +39 -40
- package/package.json +10 -11
- package/src/agent.ts +49 -1
- package/src/errors.ts +15 -0
- package/src/git.ts +50 -17
- package/src/index.ts +151 -11
- package/src/prompt.ts +19 -9
- package/src/session.ts +211 -6
- package/src/settings.ts +72 -3
- package/src/skills.ts +11 -7
- package/src/submit.ts +4 -6
- package/src/tools.ts +3 -2
- package/src/ui/agent.ts +1 -4
- package/src/ui/commands.test.ts +3 -0
- package/src/ui/commands.ts +1 -4
- package/src/ui/conversation.test.ts +165 -1
- package/src/ui/conversation.ts +268 -36
- package/src/ui/help.test.ts +18 -0
- package/src/ui/help.ts +6 -0
- package/src/ui/input.test.ts +5 -1
- package/src/ui/input.ts +7 -1
- package/src/ui/status.test.ts +4 -0
- package/src/ui.ts +96 -11
- package/src/version.ts +48 -0
package/src/ui/conversation.ts
CHANGED
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
Markdown,
|
|
13
|
+
SyntaxHighlight,
|
|
14
|
+
type SyntaxHighlightTheme,
|
|
15
|
+
} from "@cel-tui/components";
|
|
12
16
|
import { HStack, measureContentHeight, Text, VStack } from "@cel-tui/core";
|
|
13
|
-
import type { Node } from "@cel-tui/types";
|
|
17
|
+
import type { Color, Node } from "@cel-tui/types";
|
|
14
18
|
import type {
|
|
15
19
|
AssistantMessage,
|
|
16
20
|
TextContent,
|
|
@@ -20,6 +24,7 @@ import type {
|
|
|
20
24
|
import type { AppState } from "../index.ts";
|
|
21
25
|
import type { UiMessage } from "../session.ts";
|
|
22
26
|
import type { Theme } from "../theme.ts";
|
|
27
|
+
import { APP_NAME, DEV_VERSION_LABEL } from "../version.ts";
|
|
23
28
|
|
|
24
29
|
/** Single blank-line gap used between conversation-level blocks. */
|
|
25
30
|
export const CONVERSATION_GAP = 1;
|
|
@@ -33,6 +38,26 @@ const DEFAULT_TOOL_PREVIEW_WIDTH = 80;
|
|
|
33
38
|
/** Horizontal columns consumed by tool-block padding and the left border. */
|
|
34
39
|
const TOOL_BLOCK_CHROME_WIDTH = 4;
|
|
35
40
|
|
|
41
|
+
/** ANSI16 fallback hex values for syntax-highlighter theme overrides. */
|
|
42
|
+
const ANSI_COLOR_HEX: Readonly<Record<Color, string>> = {
|
|
43
|
+
color00: "#000000",
|
|
44
|
+
color01: "#cd3131",
|
|
45
|
+
color02: "#0dbc79",
|
|
46
|
+
color03: "#e5e510",
|
|
47
|
+
color04: "#2472c8",
|
|
48
|
+
color05: "#bc3fbc",
|
|
49
|
+
color06: "#11a8cd",
|
|
50
|
+
color07: "#e5e5e5",
|
|
51
|
+
color08: "#666666",
|
|
52
|
+
color09: "#f14c4c",
|
|
53
|
+
color10: "#23d18b",
|
|
54
|
+
color11: "#f5f543",
|
|
55
|
+
color12: "#3b8eea",
|
|
56
|
+
color13: "#d670d6",
|
|
57
|
+
color14: "#29b8db",
|
|
58
|
+
color15: "#ffffff",
|
|
59
|
+
};
|
|
60
|
+
|
|
36
61
|
/** A pending tool result shown in the streaming tail. */
|
|
37
62
|
export interface PendingToolResult {
|
|
38
63
|
/** Tool call id from the assistant message. */
|
|
@@ -77,6 +102,13 @@ interface AssistantRenderState {
|
|
|
77
102
|
|
|
78
103
|
type ConversationMessage = AppState["messages"][number];
|
|
79
104
|
|
|
105
|
+
type ConversationLogState = Pick<
|
|
106
|
+
AppState,
|
|
107
|
+
"messages" | "showReasoning" | "verbose" | "theme"
|
|
108
|
+
> & {
|
|
109
|
+
versionLabel?: AppState["versionLabel"];
|
|
110
|
+
};
|
|
111
|
+
|
|
80
112
|
type ToolCallRenderInfo = {
|
|
81
113
|
name: string;
|
|
82
114
|
args: Record<string, unknown>;
|
|
@@ -110,6 +142,13 @@ type ToolRenderLineKind =
|
|
|
110
142
|
| "summary"
|
|
111
143
|
| "error";
|
|
112
144
|
|
|
145
|
+
interface HighlightedToolBodySpec {
|
|
146
|
+
/** Full raw source content to syntax-highlight. */
|
|
147
|
+
text: string;
|
|
148
|
+
/** Registered lextide language id. */
|
|
149
|
+
language: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
113
152
|
interface ToolBlockSpec {
|
|
114
153
|
/** Tool name shown in the header pill. */
|
|
115
154
|
toolName: string;
|
|
@@ -117,6 +156,8 @@ interface ToolBlockSpec {
|
|
|
117
156
|
direction: ToolRenderDirection;
|
|
118
157
|
/** Logical body lines for the tool block. */
|
|
119
158
|
bodyLines: readonly ToolRenderLine[];
|
|
159
|
+
/** Optional syntax-highlighted body rendered from the full unsplit source. */
|
|
160
|
+
highlightedBody?: HighlightedToolBodySpec;
|
|
120
161
|
/** Whether `/verbose` preview rules apply to the body. */
|
|
121
162
|
previewBody: boolean;
|
|
122
163
|
}
|
|
@@ -339,6 +380,159 @@ function getToolHeaderColor(toolName: string, theme: Theme) {
|
|
|
339
380
|
}
|
|
340
381
|
}
|
|
341
382
|
|
|
383
|
+
type SyntaxThemeRegistration = Exclude<SyntaxHighlightTheme, string>;
|
|
384
|
+
type SyntaxThemeTokenColor = NonNullable<
|
|
385
|
+
SyntaxThemeRegistration["tokenColors"]
|
|
386
|
+
>[number];
|
|
387
|
+
|
|
388
|
+
const graphemeSegmenter = new Intl.Segmenter(undefined, {
|
|
389
|
+
granularity: "grapheme",
|
|
390
|
+
});
|
|
391
|
+
const shellSyntaxThemeCache = new WeakMap<Theme, SyntaxThemeRegistration>();
|
|
392
|
+
|
|
393
|
+
function colorToHex(color: Color | undefined): string | undefined {
|
|
394
|
+
return color ? ANSI_COLOR_HEX[color] : undefined;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function pushSyntaxTokenColor(
|
|
398
|
+
tokenColors: SyntaxThemeTokenColor[],
|
|
399
|
+
scope: string | readonly string[],
|
|
400
|
+
foreground: Color | undefined,
|
|
401
|
+
fontStyle?: string,
|
|
402
|
+
): void {
|
|
403
|
+
const foregroundHex = colorToHex(foreground);
|
|
404
|
+
if (!foregroundHex && !fontStyle) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
tokenColors.push({
|
|
409
|
+
scope,
|
|
410
|
+
settings: {
|
|
411
|
+
...(foregroundHex ? { foreground: foregroundHex } : {}),
|
|
412
|
+
...(fontStyle ? { fontStyle } : {}),
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function getShellSyntaxTheme(theme: Theme): SyntaxThemeRegistration {
|
|
418
|
+
const cached = shellSyntaxThemeCache.get(theme);
|
|
419
|
+
if (cached) {
|
|
420
|
+
return cached;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const tokenColors: SyntaxThemeTokenColor[] = [];
|
|
424
|
+
pushSyntaxTokenColor(tokenColors, ["comment", "quote"], theme.mutedText);
|
|
425
|
+
pushSyntaxTokenColor(
|
|
426
|
+
tokenColors,
|
|
427
|
+
["keyword", "storage"],
|
|
428
|
+
theme.secondaryAccentText,
|
|
429
|
+
);
|
|
430
|
+
pushSyntaxTokenColor(
|
|
431
|
+
tokenColors,
|
|
432
|
+
[
|
|
433
|
+
"entity.name.function",
|
|
434
|
+
"function",
|
|
435
|
+
"meta.function-call",
|
|
436
|
+
"support.function",
|
|
437
|
+
"title",
|
|
438
|
+
],
|
|
439
|
+
theme.accentText,
|
|
440
|
+
);
|
|
441
|
+
pushSyntaxTokenColor(
|
|
442
|
+
tokenColors,
|
|
443
|
+
[
|
|
444
|
+
"built_in",
|
|
445
|
+
"class",
|
|
446
|
+
"entity.name.type",
|
|
447
|
+
"entity.other.inherited-class",
|
|
448
|
+
"support.class",
|
|
449
|
+
"support.type",
|
|
450
|
+
"type",
|
|
451
|
+
],
|
|
452
|
+
theme.accentText,
|
|
453
|
+
);
|
|
454
|
+
pushSyntaxTokenColor(
|
|
455
|
+
tokenColors,
|
|
456
|
+
["constant.language", "constant.numeric", "literal", "symbol"],
|
|
457
|
+
theme.secondaryAccentText ?? theme.accentText,
|
|
458
|
+
);
|
|
459
|
+
pushSyntaxTokenColor(
|
|
460
|
+
tokenColors,
|
|
461
|
+
["markup.inline", "string"],
|
|
462
|
+
theme.diffAdded,
|
|
463
|
+
);
|
|
464
|
+
pushSyntaxTokenColor(tokenColors, "regexp", theme.diffRemoved);
|
|
465
|
+
pushSyntaxTokenColor(
|
|
466
|
+
tokenColors,
|
|
467
|
+
["attr", "attribute", "entity.other.attribute-name", "property"],
|
|
468
|
+
theme.accentText,
|
|
469
|
+
);
|
|
470
|
+
pushSyntaxTokenColor(
|
|
471
|
+
tokenColors,
|
|
472
|
+
["entity.name.tag", "name", "tag"],
|
|
473
|
+
theme.accentText,
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
const syntaxTheme: SyntaxThemeRegistration = {
|
|
477
|
+
...(colorToHex(theme.toolText) ? { fg: colorToHex(theme.toolText) } : {}),
|
|
478
|
+
tokenColors,
|
|
479
|
+
};
|
|
480
|
+
shellSyntaxThemeCache.set(theme, syntaxTheme);
|
|
481
|
+
return syntaxTheme;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function splitHighlightedTextNode(
|
|
485
|
+
node: Extract<Node, { type: "text" }>,
|
|
486
|
+
): Node[] {
|
|
487
|
+
if (node.content === "") {
|
|
488
|
+
return [Text("", node.props)];
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const parts = node.content.match(/\s+|\S+/gu);
|
|
492
|
+
if (!parts) {
|
|
493
|
+
return [Text(node.content, node.props)];
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const children: Node[] = [];
|
|
497
|
+
for (const part of parts) {
|
|
498
|
+
if (/^\s+$/u.test(part)) {
|
|
499
|
+
children.push(Text(part, node.props));
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
for (const { segment } of graphemeSegmenter.segment(part)) {
|
|
504
|
+
children.push(Text(segment, node.props));
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return children;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function normalizeHighlightedToolLine(line: Node): Node {
|
|
512
|
+
if (line.type !== "hstack") {
|
|
513
|
+
return line;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const children = line.children.flatMap((child) => {
|
|
517
|
+
return child.type === "text" ? splitHighlightedTextNode(child) : [child];
|
|
518
|
+
});
|
|
519
|
+
return HStack(line.props, children);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function getHighlightedToolBodyLines(
|
|
523
|
+
spec: HighlightedToolBodySpec,
|
|
524
|
+
theme: Theme,
|
|
525
|
+
): Node[] {
|
|
526
|
+
if (spec.text === "") {
|
|
527
|
+
return [];
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const highlighted = SyntaxHighlight(spec.text, spec.language, {
|
|
531
|
+
theme: getShellSyntaxTheme(theme),
|
|
532
|
+
});
|
|
533
|
+
return highlighted.children.map((line) => normalizeHighlightedToolLine(line));
|
|
534
|
+
}
|
|
535
|
+
|
|
342
536
|
/** Render a single styled text node for a tool line. */
|
|
343
537
|
function renderToolLine(line: ToolRenderLine, theme: Theme): Node {
|
|
344
538
|
switch (line.kind) {
|
|
@@ -374,41 +568,28 @@ function renderToolLines(
|
|
|
374
568
|
return lines.map((line) => renderToolLine(line, theme));
|
|
375
569
|
}
|
|
376
570
|
|
|
377
|
-
function
|
|
378
|
-
lines: readonly ToolRenderLine[],
|
|
379
|
-
width: number,
|
|
380
|
-
theme: Theme,
|
|
381
|
-
): number {
|
|
571
|
+
function measureToolNodesHeight(lines: readonly Node[], width: number): number {
|
|
382
572
|
if (lines.length === 0) {
|
|
383
573
|
return 0;
|
|
384
574
|
}
|
|
385
575
|
|
|
386
|
-
return measureContentHeight(VStack({},
|
|
387
|
-
width,
|
|
388
|
-
});
|
|
576
|
+
return measureContentHeight(VStack({}, [...lines]), { width });
|
|
389
577
|
}
|
|
390
578
|
|
|
391
|
-
function
|
|
392
|
-
line
|
|
393
|
-
width: number,
|
|
394
|
-
theme: Theme,
|
|
395
|
-
): number {
|
|
396
|
-
return measureContentHeight(VStack({}, [renderToolLine(line, theme)]), {
|
|
397
|
-
width,
|
|
398
|
-
});
|
|
579
|
+
function measureToolNodeHeight(line: Node, width: number): number {
|
|
580
|
+
return measureContentHeight(VStack({}, [line]), { width });
|
|
399
581
|
}
|
|
400
582
|
|
|
401
|
-
function
|
|
402
|
-
lines: readonly
|
|
583
|
+
function countVisibleTailNodes(
|
|
584
|
+
lines: readonly Node[],
|
|
403
585
|
width: number,
|
|
404
|
-
theme: Theme,
|
|
405
586
|
maxRows: number,
|
|
406
587
|
): number {
|
|
407
588
|
let remainingRows = maxRows;
|
|
408
589
|
let visibleLineCount = 0;
|
|
409
590
|
|
|
410
591
|
for (let index = lines.length - 1; index >= 0; index--) {
|
|
411
|
-
const lineHeight =
|
|
592
|
+
const lineHeight = measureToolNodeHeight(lines[index]!, width);
|
|
412
593
|
if (lineHeight > remainingRows) {
|
|
413
594
|
return visibleLineCount > 0 ? visibleLineCount : 1;
|
|
414
595
|
}
|
|
@@ -432,8 +613,8 @@ function renderToolHeaderPill(
|
|
|
432
613
|
]);
|
|
433
614
|
}
|
|
434
615
|
|
|
435
|
-
function
|
|
436
|
-
lines: readonly
|
|
616
|
+
function renderToolBodyFromNodes(
|
|
617
|
+
lines: readonly Node[],
|
|
437
618
|
previewBody: boolean,
|
|
438
619
|
opts: Pick<ConversationRenderOpts, "previewWidth" | "theme" | "verbose">,
|
|
439
620
|
): { body: Node | null; summary?: ToolRenderLine } {
|
|
@@ -441,21 +622,19 @@ function renderToolBody(
|
|
|
441
622
|
return { body: null };
|
|
442
623
|
}
|
|
443
624
|
|
|
444
|
-
const renderedLines = renderToolLines(lines, opts.theme);
|
|
445
625
|
if (opts.verbose || !previewBody) {
|
|
446
|
-
return { body: VStack({},
|
|
626
|
+
return { body: VStack({}, [...lines]) };
|
|
447
627
|
}
|
|
448
628
|
|
|
449
629
|
const bodyWidth = getToolBodyWidth(opts.previewWidth);
|
|
450
|
-
const totalHeight =
|
|
630
|
+
const totalHeight = measureToolNodesHeight(lines, bodyWidth);
|
|
451
631
|
if (totalHeight <= UI_TOOL_PREVIEW_ROWS) {
|
|
452
|
-
return { body: VStack({},
|
|
632
|
+
return { body: VStack({}, [...lines]) };
|
|
453
633
|
}
|
|
454
634
|
|
|
455
|
-
const visibleLineCount =
|
|
635
|
+
const visibleLineCount = countVisibleTailNodes(
|
|
456
636
|
lines,
|
|
457
637
|
bodyWidth,
|
|
458
|
-
opts.theme,
|
|
459
638
|
UI_TOOL_PREVIEW_ROWS,
|
|
460
639
|
);
|
|
461
640
|
const previewLines = lines.slice(-visibleLineCount);
|
|
@@ -466,7 +645,7 @@ function renderToolBody(
|
|
|
466
645
|
{
|
|
467
646
|
height: UI_TOOL_PREVIEW_ROWS,
|
|
468
647
|
},
|
|
469
|
-
|
|
648
|
+
[...previewLines],
|
|
470
649
|
),
|
|
471
650
|
summary:
|
|
472
651
|
hiddenLineCount > 0
|
|
@@ -478,12 +657,31 @@ function renderToolBody(
|
|
|
478
657
|
};
|
|
479
658
|
}
|
|
480
659
|
|
|
660
|
+
function renderToolBody(
|
|
661
|
+
spec: ToolBlockSpec,
|
|
662
|
+
opts: Pick<ConversationRenderOpts, "previewWidth" | "theme" | "verbose">,
|
|
663
|
+
): { body: Node | null; summary?: ToolRenderLine } {
|
|
664
|
+
if (spec.highlightedBody) {
|
|
665
|
+
return renderToolBodyFromNodes(
|
|
666
|
+
getHighlightedToolBodyLines(spec.highlightedBody, opts.theme),
|
|
667
|
+
spec.previewBody,
|
|
668
|
+
opts,
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return renderToolBodyFromNodes(
|
|
673
|
+
renderToolLines(spec.bodyLines, opts.theme),
|
|
674
|
+
spec.previewBody,
|
|
675
|
+
opts,
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
481
679
|
/** Render a tool block with a left border and compact header pill. */
|
|
482
680
|
function renderToolBlock(
|
|
483
681
|
spec: ToolBlockSpec,
|
|
484
682
|
opts: Pick<ConversationRenderOpts, "previewWidth" | "theme" | "verbose">,
|
|
485
683
|
): Node {
|
|
486
|
-
const body = renderToolBody(spec
|
|
684
|
+
const body = renderToolBody(spec, opts);
|
|
487
685
|
const children: Node[] = [
|
|
488
686
|
HStack({}, [
|
|
489
687
|
renderToolHeaderPill(spec.toolName, spec.direction, opts.theme),
|
|
@@ -511,10 +709,18 @@ function getToolContentText(content: ToolResultMessage["content"]): string {
|
|
|
511
709
|
}
|
|
512
710
|
|
|
513
711
|
function buildShellToolCallSpec(args: Record<string, unknown>): ToolBlockSpec {
|
|
712
|
+
const command = getToolArgString(args, "command");
|
|
514
713
|
return {
|
|
515
714
|
toolName: "shell",
|
|
516
715
|
direction: "->",
|
|
517
|
-
bodyLines: splitToolTextLines(
|
|
716
|
+
bodyLines: splitToolTextLines(command, "command"),
|
|
717
|
+
highlightedBody:
|
|
718
|
+
command === ""
|
|
719
|
+
? undefined
|
|
720
|
+
: {
|
|
721
|
+
text: command,
|
|
722
|
+
language: "bash",
|
|
723
|
+
},
|
|
518
724
|
previewBody: true,
|
|
519
725
|
};
|
|
520
726
|
}
|
|
@@ -728,6 +934,23 @@ function renderUiMessage(msg: UiMessage, theme: Theme): Node {
|
|
|
728
934
|
]);
|
|
729
935
|
}
|
|
730
936
|
|
|
937
|
+
function renderEmptyConversationBanner(
|
|
938
|
+
theme: Theme,
|
|
939
|
+
versionLabel: string,
|
|
940
|
+
): Node {
|
|
941
|
+
return HStack({ justifyContent: "center" }, [
|
|
942
|
+
VStack({ padding: { x: 1 } }, [
|
|
943
|
+
Text(APP_NAME, {
|
|
944
|
+
bold: true,
|
|
945
|
+
fgColor: theme.accentText,
|
|
946
|
+
}),
|
|
947
|
+
Text(versionLabel, {
|
|
948
|
+
fgColor: theme.mutedText,
|
|
949
|
+
}),
|
|
950
|
+
]),
|
|
951
|
+
]);
|
|
952
|
+
}
|
|
953
|
+
|
|
731
954
|
function pushConversationNode(nodes: Node[], node: Node | null): void {
|
|
732
955
|
if (!node) {
|
|
733
956
|
return;
|
|
@@ -808,7 +1031,7 @@ function cacheToolCallArgs(messages: readonly ConversationMessage[]): void {
|
|
|
808
1031
|
}
|
|
809
1032
|
|
|
810
1033
|
function canReuseCommittedConversationCache(
|
|
811
|
-
state:
|
|
1034
|
+
state: ConversationLogState,
|
|
812
1035
|
startIndex: number,
|
|
813
1036
|
previewWidth: number,
|
|
814
1037
|
): boolean {
|
|
@@ -824,7 +1047,7 @@ function canReuseCommittedConversationCache(
|
|
|
824
1047
|
}
|
|
825
1048
|
|
|
826
1049
|
function cacheCommittedConversation(
|
|
827
|
-
state:
|
|
1050
|
+
state: ConversationLogState,
|
|
828
1051
|
renderOpts: ConversationRenderOpts,
|
|
829
1052
|
startIndex: number,
|
|
830
1053
|
): void {
|
|
@@ -879,7 +1102,7 @@ function hasStreamingTail(streaming: StreamingConversationState): boolean {
|
|
|
879
1102
|
* @returns The rendered conversation log nodes.
|
|
880
1103
|
*/
|
|
881
1104
|
export function buildConversationLogNodes(
|
|
882
|
-
state:
|
|
1105
|
+
state: ConversationLogState,
|
|
883
1106
|
streaming: StreamingConversationState,
|
|
884
1107
|
startIndex = 0,
|
|
885
1108
|
previewWidth = DEFAULT_TOOL_PREVIEW_WIDTH,
|
|
@@ -891,6 +1114,15 @@ export function buildConversationLogNodes(
|
|
|
891
1114
|
previewWidth,
|
|
892
1115
|
};
|
|
893
1116
|
|
|
1117
|
+
if (state.messages.length === 0 && !hasStreamingTail(streaming)) {
|
|
1118
|
+
return [
|
|
1119
|
+
renderEmptyConversationBanner(
|
|
1120
|
+
state.theme,
|
|
1121
|
+
state.versionLabel ?? DEV_VERSION_LABEL,
|
|
1122
|
+
),
|
|
1123
|
+
];
|
|
1124
|
+
}
|
|
1125
|
+
|
|
894
1126
|
cacheCommittedConversation(state, renderOpts, startIndex);
|
|
895
1127
|
|
|
896
1128
|
if (!hasStreamingTail(streaming)) {
|
package/src/ui/help.test.ts
CHANGED
|
@@ -23,4 +23,22 @@ describe("ui/help", () => {
|
|
|
23
23
|
"/verbose Toggle verbose tool rendering (currently off)",
|
|
24
24
|
);
|
|
25
25
|
});
|
|
26
|
+
|
|
27
|
+
test("buildHelpText includes the current Escape input-focus note", () => {
|
|
28
|
+
const helpState: HelpRenderState = {
|
|
29
|
+
providers: new Map(),
|
|
30
|
+
model: null,
|
|
31
|
+
agentsMd: [],
|
|
32
|
+
skills: [],
|
|
33
|
+
plugins: [],
|
|
34
|
+
showReasoning: DEFAULT_SHOW_REASONING,
|
|
35
|
+
verbose: false,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const text = buildHelpText(helpState);
|
|
39
|
+
|
|
40
|
+
expect(text).toContain("Escape blurs the input first");
|
|
41
|
+
expect(text).toContain("Tab re-focuses the input");
|
|
42
|
+
expect(text).toContain("Escape again interrupts the current turn");
|
|
43
|
+
});
|
|
26
44
|
});
|
package/src/ui/help.ts
CHANGED
|
@@ -77,6 +77,12 @@ export function buildHelpText(state: HelpRenderState): string {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
const providerNames = Array.from(state.providers.keys());
|
|
80
|
+
lines.push("");
|
|
81
|
+
lines.push("Note:");
|
|
82
|
+
lines.push(" While a turn is running, Escape blurs the input first.");
|
|
83
|
+
lines.push(" Tab re-focuses the input.");
|
|
84
|
+
lines.push(" Escape again interrupts the current turn.");
|
|
85
|
+
|
|
80
86
|
lines.push("");
|
|
81
87
|
lines.push(
|
|
82
88
|
providerNames.length > 0
|
package/src/ui/input.test.ts
CHANGED
|
@@ -53,7 +53,11 @@ describe("ui/input", () => {
|
|
|
53
53
|
if (!placeholder || placeholder.type !== "text") {
|
|
54
54
|
throw new Error("Expected a text placeholder");
|
|
55
55
|
}
|
|
56
|
-
expect(placeholder.content).toBe(
|
|
56
|
+
expect(placeholder.content).toBe(
|
|
57
|
+
"`Ctrl+R` for input history, `/` + `Tab` for interactive menu, or type a message…",
|
|
58
|
+
);
|
|
59
|
+
expect(placeholder.props.fgColor).toBe(DEFAULT_THEME.mutedText);
|
|
60
|
+
expect(placeholder.props.italic).toBe(true);
|
|
57
61
|
});
|
|
58
62
|
|
|
59
63
|
test("autocompleteInputPath completes the last file path token", () => {
|
package/src/ui/input.ts
CHANGED
|
@@ -10,6 +10,9 @@ import { Text, TextInput } from "@cel-tui/core";
|
|
|
10
10
|
import type { Node } from "@cel-tui/types";
|
|
11
11
|
import type { Theme } from "../theme.ts";
|
|
12
12
|
|
|
13
|
+
const INPUT_PLACEHOLDER =
|
|
14
|
+
"`Ctrl+R` for input history, `/` + `Tab` for interactive menu, or type a message…";
|
|
15
|
+
|
|
13
16
|
/** Stable callbacks for the controlled TextInput. */
|
|
14
17
|
export interface InputController {
|
|
15
18
|
/** Update the controlled input value and re-render. */
|
|
@@ -129,7 +132,10 @@ export function renderInputArea(
|
|
|
129
132
|
padding: { x: 1 },
|
|
130
133
|
value,
|
|
131
134
|
onChange: controller.onChange,
|
|
132
|
-
placeholder: Text(
|
|
135
|
+
placeholder: Text(INPUT_PLACEHOLDER, {
|
|
136
|
+
fgColor: theme.mutedText,
|
|
137
|
+
italic: true,
|
|
138
|
+
}),
|
|
133
139
|
focused,
|
|
134
140
|
onFocus: controller.onFocus,
|
|
135
141
|
onBlur: controller.onBlur,
|
package/src/ui/status.test.ts
CHANGED
|
@@ -103,6 +103,9 @@ function createTestState(): AppState {
|
|
|
103
103
|
activeTurnPromise: null,
|
|
104
104
|
showReasoning: DEFAULT_SHOW_REASONING,
|
|
105
105
|
verbose: DEFAULT_VERBOSE,
|
|
106
|
+
versionLabel: "dev",
|
|
107
|
+
customModels: [],
|
|
108
|
+
startupWarnings: [],
|
|
106
109
|
};
|
|
107
110
|
}
|
|
108
111
|
|
|
@@ -137,6 +140,7 @@ describe("ui/status", () => {
|
|
|
137
140
|
state.git = {
|
|
138
141
|
root: state.cwd,
|
|
139
142
|
branch: "main",
|
|
143
|
+
upstream: "origin/main",
|
|
140
144
|
staged: 1,
|
|
141
145
|
modified: 2,
|
|
142
146
|
untracked: 3,
|