mini-coder 0.5.12 → 0.5.14
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/BENCHMARK.md +15 -316
- package/PROGRESS.md +3 -3
- package/README.md +54 -21
- package/benchmark-baseline.sh +15 -0
- package/bun.lock +265 -90
- package/package.json +8 -7
- package/skills-lock.json +15 -0
- package/src/agent.ts +526 -13
- package/src/assistant-output.ts +73 -0
- package/src/cli.ts +2 -1
- package/src/delegation.ts +238 -0
- package/src/headless.ts +90 -44
- package/src/index.ts +267 -102
- package/src/input.ts +13 -1
- package/src/mcp.ts +609 -0
- package/src/prompt.ts +11 -13
- package/src/session-message.ts +57 -65
- package/src/session.ts +389 -42
- package/src/settings.ts +199 -7
- package/src/skills.ts +12 -3
- package/src/submit.ts +24 -2
- package/src/theme.ts +186 -3
- package/src/tool-common.ts +2 -0
- package/src/tool-delegate.ts +125 -0
- package/src/tool-shell.ts +190 -8
- package/src/tools.ts +335 -6
- package/src/ui/agent.ts +10 -0
- package/src/ui/commands.test.ts +525 -10
- package/src/ui/commands.ts +224 -8
- package/src/ui/conversation.test.ts +252 -27
- package/src/ui/conversation.ts +468 -390
- package/src/ui/help.ts +27 -11
- package/src/ui.ts +230 -75
- package/src/plugins.ts +0 -183
package/src/ui/help.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { AppState } from "../index.ts";
|
|
8
|
-
import { COMMANDS } from "../input.ts";
|
|
8
|
+
import { COMMANDS, SKILL_COMMAND } from "../input.ts";
|
|
9
9
|
import { abbreviatePath } from "./status.ts";
|
|
10
10
|
|
|
11
11
|
/** Help text inputs derived from application state. */
|
|
@@ -18,12 +18,12 @@ export interface HelpRenderState {
|
|
|
18
18
|
agentsMd: AppState["agentsMd"];
|
|
19
19
|
/** Discovered skills. */
|
|
20
20
|
skills: AppState["skills"];
|
|
21
|
-
/** Active plugins. */
|
|
22
|
-
plugins: AppState["plugins"];
|
|
23
21
|
/** Whether reasoning blocks are shown in the log. */
|
|
24
22
|
showReasoning: AppState["showReasoning"];
|
|
25
23
|
/** Whether verbose tool rendering is enabled in the log. */
|
|
26
24
|
verbose: AppState["verbose"];
|
|
25
|
+
/** Configured MCP servers and their current on/off state. */
|
|
26
|
+
mcpServers: AppState["mcpServers"];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/** Command descriptions for `/help` and command autocomplete. */
|
|
@@ -36,12 +36,20 @@ export const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
36
36
|
undo: "Undo last turn",
|
|
37
37
|
reasoning: "Toggle thinking display",
|
|
38
38
|
verbose: "Toggle verbose tool rendering",
|
|
39
|
+
mcp: "Toggle configured MCP servers",
|
|
39
40
|
todo: "Show the current todo list",
|
|
40
41
|
login: "OAuth login",
|
|
41
42
|
logout: "OAuth logout",
|
|
42
43
|
help: "Show help",
|
|
43
44
|
};
|
|
44
45
|
|
|
46
|
+
/** Slash-skill label shown in `/help` and command autocomplete. */
|
|
47
|
+
export const SKILL_REFERENCE_LABEL = `/${SKILL_COMMAND}:name`;
|
|
48
|
+
|
|
49
|
+
/** Description for the slash-skill input helper. */
|
|
50
|
+
export const SKILL_REFERENCE_DESCRIPTION =
|
|
51
|
+
"Insert a discovered skill into the next message";
|
|
52
|
+
|
|
45
53
|
/**
|
|
46
54
|
* Get the `/help` description for a command, including current state when relevant.
|
|
47
55
|
*
|
|
@@ -71,6 +79,12 @@ function formatInlineCodeList(items: readonly string[]): string {
|
|
|
71
79
|
return items.map((item) => formatInlineCode(item)).join(", ");
|
|
72
80
|
}
|
|
73
81
|
|
|
82
|
+
function formatMcpServerState(
|
|
83
|
+
server: HelpRenderState["mcpServers"][number],
|
|
84
|
+
): string {
|
|
85
|
+
return `${formatInlineCode(server.name)} (${server.enabled ? "on" : "off"})`;
|
|
86
|
+
}
|
|
87
|
+
|
|
74
88
|
/**
|
|
75
89
|
* Build the `/help` text shown in the conversation log.
|
|
76
90
|
*
|
|
@@ -85,15 +99,21 @@ export function buildHelpText(state: HelpRenderState): string {
|
|
|
85
99
|
`- ${formatInlineCode(`/${command}`)} — ${getHelpCommandDescription(command, state)}`,
|
|
86
100
|
);
|
|
87
101
|
}
|
|
102
|
+
lines.push(
|
|
103
|
+
`- ${formatInlineCode(SKILL_REFERENCE_LABEL)} — ${SKILL_REFERENCE_DESCRIPTION}; submit ${formatInlineCode(`/${SKILL_COMMAND}`)} to open the skill picker.`,
|
|
104
|
+
);
|
|
88
105
|
|
|
89
106
|
const providerNames = Array.from(state.providers.keys());
|
|
107
|
+
const mcpServerStates = state.mcpServers.map((server) =>
|
|
108
|
+
formatMcpServerState(server),
|
|
109
|
+
);
|
|
90
110
|
lines.push(
|
|
91
111
|
"",
|
|
92
112
|
"## Keyboard",
|
|
93
113
|
"",
|
|
94
114
|
"- `Enter` submits the current draft.",
|
|
95
115
|
"- `Shift+Enter` inserts a newline.",
|
|
96
|
-
"- `Tab` opens command autocomplete when the draft starts with
|
|
116
|
+
"- `Tab` opens command autocomplete when the draft starts with `/`, preserving the current draft.",
|
|
97
117
|
"- Otherwise, `Tab` autocompletes file paths and can open a path picker when there are multiple matches.",
|
|
98
118
|
"- `Ctrl+R` opens global input history search.",
|
|
99
119
|
"- `Escape` closes the current overlay and returns focus to the input.",
|
|
@@ -111,6 +131,9 @@ export function buildHelpText(state: HelpRenderState): string {
|
|
|
111
131
|
state.model
|
|
112
132
|
? `- Model: ${formatInlineCode(`${state.model.provider}/${state.model.id}`)}`
|
|
113
133
|
: "- Model: none — use `/model`",
|
|
134
|
+
mcpServerStates.length > 0
|
|
135
|
+
? `- MCP servers: ${mcpServerStates.join(", ")}`
|
|
136
|
+
: "- MCP servers: none — configure them in `settings.json`",
|
|
114
137
|
);
|
|
115
138
|
|
|
116
139
|
if (state.agentsMd.length > 0) {
|
|
@@ -131,12 +154,5 @@ export function buildHelpText(state: HelpRenderState): string {
|
|
|
131
154
|
}
|
|
132
155
|
}
|
|
133
156
|
|
|
134
|
-
if (state.plugins.length > 0) {
|
|
135
|
-
lines.push("", "## Plugins", "");
|
|
136
|
-
for (const plugin of state.plugins) {
|
|
137
|
-
lines.push(`- ${formatInlineCode(plugin.entry.name)}`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
157
|
return lines.join("\n");
|
|
142
158
|
}
|
package/src/ui.ts
CHANGED
|
@@ -27,7 +27,7 @@ import type { Theme } from "./theme.ts";
|
|
|
27
27
|
import { createUiAgentController } from "./ui/agent.ts";
|
|
28
28
|
import { createCommandController } from "./ui/commands.ts";
|
|
29
29
|
import {
|
|
30
|
-
|
|
30
|
+
buildConversationLayoutSnapshot,
|
|
31
31
|
CONVERSATION_GAP,
|
|
32
32
|
resetConversationRenderCache,
|
|
33
33
|
} from "./ui/conversation.ts";
|
|
@@ -74,8 +74,8 @@ const TERMINAL_TITLE_FRAMES = [
|
|
|
74
74
|
"[o=---]",
|
|
75
75
|
] as const;
|
|
76
76
|
|
|
77
|
-
/**
|
|
78
|
-
const
|
|
77
|
+
/** Overscan applied above and below the viewport for virtualized log rendering. */
|
|
78
|
+
const CONVERSATION_OVERSCAN_ROWS = 6;
|
|
79
79
|
|
|
80
80
|
/** Minimum delay between coalesced streaming renders. */
|
|
81
81
|
const STREAM_RENDER_MIN_INTERVAL_MS = 33;
|
|
@@ -97,6 +97,14 @@ const QUIT_RULES: Readonly<{
|
|
|
97
97
|
keysWhenEmptyInput: new Set(["ctrl+d"]),
|
|
98
98
|
};
|
|
99
99
|
|
|
100
|
+
/** Keypresses that still bubble while the queued steering draft is readonly. */
|
|
101
|
+
const READONLY_INPUT_BUBBLE_KEYS = new Set([
|
|
102
|
+
"ctrl+c",
|
|
103
|
+
"ctrl+d",
|
|
104
|
+
"ctrl+z",
|
|
105
|
+
"escape",
|
|
106
|
+
]);
|
|
107
|
+
|
|
100
108
|
// ---------------------------------------------------------------------------
|
|
101
109
|
// UI state (module-scoped, not in AppState)
|
|
102
110
|
// ---------------------------------------------------------------------------
|
|
@@ -107,12 +115,12 @@ let scrollOffset = 0;
|
|
|
107
115
|
/** Whether the log auto-scrolls to the bottom. */
|
|
108
116
|
let stickToBottom = true;
|
|
109
117
|
|
|
110
|
-
/** First visible committed message when older history is chunked. */
|
|
111
|
-
let visibleConversationStart = 0;
|
|
112
|
-
|
|
113
118
|
/** Current text in the input area. */
|
|
114
119
|
let inputValue = "";
|
|
115
120
|
|
|
121
|
+
/** Whether the visible input draft is temporarily readonly. */
|
|
122
|
+
let inputReadOnly = false;
|
|
123
|
+
|
|
116
124
|
/** Whether the text input is focused. */
|
|
117
125
|
let inputFocused = true;
|
|
118
126
|
|
|
@@ -166,8 +174,8 @@ export function isQuitKey(key: string, input: string): boolean {
|
|
|
166
174
|
export function resetUiState(): void {
|
|
167
175
|
scrollOffset = 0;
|
|
168
176
|
stickToBottom = true;
|
|
169
|
-
visibleConversationStart = 0;
|
|
170
177
|
inputValue = "";
|
|
178
|
+
inputReadOnly = false;
|
|
171
179
|
inputFocused = true;
|
|
172
180
|
dividerTick = 0;
|
|
173
181
|
stopDividerAnimation();
|
|
@@ -516,62 +524,151 @@ function renderDivider(state: AppState, width: number): Node {
|
|
|
516
524
|
// Conversation log
|
|
517
525
|
// ---------------------------------------------------------------------------
|
|
518
526
|
|
|
519
|
-
function
|
|
520
|
-
|
|
527
|
+
function measureConversationSliceHeight(
|
|
528
|
+
prefixHeights: readonly number[],
|
|
529
|
+
startIndex: number,
|
|
530
|
+
endIndex: number,
|
|
531
|
+
): number {
|
|
532
|
+
const count = endIndex - startIndex;
|
|
533
|
+
if (count <= 0) {
|
|
534
|
+
return 0;
|
|
535
|
+
}
|
|
536
|
+
return (
|
|
537
|
+
prefixHeights[endIndex]! -
|
|
538
|
+
prefixHeights[startIndex]! +
|
|
539
|
+
CONVERSATION_GAP * Math.max(0, count - 1)
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function getConversationItemTop(
|
|
544
|
+
prefixHeights: readonly number[],
|
|
545
|
+
index: number,
|
|
546
|
+
): number {
|
|
547
|
+
return prefixHeights[index]! + CONVERSATION_GAP * index;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function findConversationStartIndex(
|
|
551
|
+
itemHeights: readonly { height: number }[],
|
|
552
|
+
prefixHeights: readonly number[],
|
|
553
|
+
offset: number,
|
|
554
|
+
): number {
|
|
555
|
+
let low = 0;
|
|
556
|
+
let high = itemHeights.length;
|
|
557
|
+
|
|
558
|
+
while (low < high) {
|
|
559
|
+
const mid = Math.floor((low + high) / 2);
|
|
560
|
+
const bottom =
|
|
561
|
+
getConversationItemTop(prefixHeights, mid) + itemHeights[mid]!.height;
|
|
562
|
+
if (bottom > offset) {
|
|
563
|
+
high = mid;
|
|
564
|
+
} else {
|
|
565
|
+
low = mid + 1;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return Math.min(low, Math.max(0, itemHeights.length - 1));
|
|
521
570
|
}
|
|
522
571
|
|
|
523
|
-
function
|
|
524
|
-
|
|
525
|
-
|
|
572
|
+
function findConversationEndIndex(
|
|
573
|
+
items: readonly { height: number }[],
|
|
574
|
+
prefixHeights: readonly number[],
|
|
575
|
+
offset: number,
|
|
576
|
+
): number {
|
|
577
|
+
let low = 0;
|
|
578
|
+
let high = items.length;
|
|
579
|
+
|
|
580
|
+
while (low < high) {
|
|
581
|
+
const mid = Math.floor((low + high) / 2);
|
|
582
|
+
if (getConversationItemTop(prefixHeights, mid) < offset) {
|
|
583
|
+
low = mid + 1;
|
|
584
|
+
} else {
|
|
585
|
+
high = mid;
|
|
586
|
+
}
|
|
526
587
|
}
|
|
527
588
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
589
|
+
return low;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function renderConversationSpacer(height: number): Node | null {
|
|
593
|
+
const rows = Math.max(0, Math.floor(height));
|
|
594
|
+
if (rows === 0) {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const paddingY = Math.floor(rows / 2);
|
|
599
|
+
if (rows % 2 === 0) {
|
|
600
|
+
return VStack({ padding: { y: paddingY } }, []);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return VStack({ padding: { y: paddingY } }, [Text("")]);
|
|
533
604
|
}
|
|
534
605
|
|
|
535
606
|
/** Build the full conversation log as an array of nodes. */
|
|
536
607
|
export function buildConversationLog(
|
|
537
608
|
state: AppState,
|
|
538
609
|
width = Number.POSITIVE_INFINITY,
|
|
610
|
+
viewportHeight = Number.POSITIVE_INFINITY,
|
|
539
611
|
): Node[] {
|
|
540
|
-
|
|
612
|
+
const layout = buildConversationLayoutSnapshot(
|
|
541
613
|
state,
|
|
542
614
|
agentController.getStreamingConversationState(),
|
|
543
|
-
getVisibleConversationStart(state.messages.length),
|
|
544
615
|
width,
|
|
545
616
|
);
|
|
546
|
-
}
|
|
617
|
+
const { items, prefixHeights } = layout;
|
|
618
|
+
if (!Number.isFinite(viewportHeight) || items.length <= 1) {
|
|
619
|
+
return items.map((item) => item.node);
|
|
620
|
+
}
|
|
547
621
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
)
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
622
|
+
const totalHeight = measureConversationSliceHeight(
|
|
623
|
+
prefixHeights,
|
|
624
|
+
0,
|
|
625
|
+
items.length,
|
|
626
|
+
);
|
|
627
|
+
const visibleHeight = Math.max(1, Math.floor(viewportHeight));
|
|
628
|
+
const maxOffset = Math.max(0, totalHeight - visibleHeight);
|
|
629
|
+
const effectiveScrollOffset = stickToBottom
|
|
630
|
+
? maxOffset
|
|
631
|
+
: Math.max(0, Math.min(scrollOffset, maxOffset));
|
|
632
|
+
const paddedTop = Math.max(
|
|
633
|
+
0,
|
|
634
|
+
effectiveScrollOffset - CONVERSATION_OVERSCAN_ROWS,
|
|
635
|
+
);
|
|
636
|
+
const paddedBottom = Math.min(
|
|
637
|
+
totalHeight,
|
|
638
|
+
effectiveScrollOffset + visibleHeight + CONVERSATION_OVERSCAN_ROWS,
|
|
564
639
|
);
|
|
565
|
-
}
|
|
566
640
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
641
|
+
let startIndex = findConversationStartIndex(items, prefixHeights, paddedTop);
|
|
642
|
+
let endIndex = findConversationEndIndex(items, prefixHeights, paddedBottom);
|
|
643
|
+
if (endIndex <= startIndex) {
|
|
644
|
+
endIndex = Math.min(items.length, startIndex + 1);
|
|
645
|
+
}
|
|
646
|
+
startIndex = Math.min(startIndex, Math.max(0, endIndex - 1));
|
|
572
647
|
|
|
573
|
-
|
|
574
|
-
|
|
648
|
+
const topSpacerHeight = measureConversationSliceHeight(
|
|
649
|
+
prefixHeights,
|
|
650
|
+
0,
|
|
651
|
+
startIndex,
|
|
652
|
+
);
|
|
653
|
+
const bottomSpacerHeight = measureConversationSliceHeight(
|
|
654
|
+
prefixHeights,
|
|
655
|
+
endIndex,
|
|
656
|
+
items.length,
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
const nodes: Node[] = [];
|
|
660
|
+
const topSpacer = renderConversationSpacer(topSpacerHeight);
|
|
661
|
+
if (topSpacer) {
|
|
662
|
+
nodes.push(topSpacer);
|
|
663
|
+
}
|
|
664
|
+
for (let index = startIndex; index < endIndex; index++) {
|
|
665
|
+
nodes.push(items[index]!.node);
|
|
666
|
+
}
|
|
667
|
+
const bottomSpacer = renderConversationSpacer(bottomSpacerHeight);
|
|
668
|
+
if (bottomSpacer) {
|
|
669
|
+
nodes.push(bottomSpacer);
|
|
670
|
+
}
|
|
671
|
+
return nodes;
|
|
575
672
|
}
|
|
576
673
|
|
|
577
674
|
// ---------------------------------------------------------------------------
|
|
@@ -610,6 +707,13 @@ function openPathAutocompleteOverlay(state: AppState): void {
|
|
|
610
707
|
inputValue = value;
|
|
611
708
|
dismissOverlay();
|
|
612
709
|
},
|
|
710
|
+
onKeyPress: (key) => {
|
|
711
|
+
if (key === "escape") {
|
|
712
|
+
dismissOverlay();
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
return false;
|
|
716
|
+
},
|
|
613
717
|
onBlur: dismissOverlay,
|
|
614
718
|
});
|
|
615
719
|
|
|
@@ -663,7 +767,7 @@ export function createInputController(state: AppState): InputController {
|
|
|
663
767
|
|
|
664
768
|
return {
|
|
665
769
|
onChange: (value) => {
|
|
666
|
-
if (inputValue === value) {
|
|
770
|
+
if (inputReadOnly || inputValue === value) {
|
|
667
771
|
return;
|
|
668
772
|
}
|
|
669
773
|
inputValue = value;
|
|
@@ -675,6 +779,13 @@ export function createInputController(state: AppState): InputController {
|
|
|
675
779
|
inputFocused = false;
|
|
676
780
|
},
|
|
677
781
|
onKeyPress: (key) => {
|
|
782
|
+
if (inputReadOnly) {
|
|
783
|
+
if (READONLY_INPUT_BUBBLE_KEYS.has(key)) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
return false;
|
|
787
|
+
}
|
|
788
|
+
|
|
678
789
|
if (key === "enter") {
|
|
679
790
|
const raw = inputValue;
|
|
680
791
|
|
|
@@ -684,8 +795,13 @@ export function createInputController(state: AppState): InputController {
|
|
|
684
795
|
return false;
|
|
685
796
|
}
|
|
686
797
|
|
|
687
|
-
|
|
798
|
+
const queuedInputCount = state.queuedUserMessages.length;
|
|
688
799
|
handleInput(raw, state);
|
|
800
|
+
if (state.queuedUserMessages.length > queuedInputCount) {
|
|
801
|
+
inputReadOnly = true;
|
|
802
|
+
} else {
|
|
803
|
+
inputValue = "";
|
|
804
|
+
}
|
|
689
805
|
return false;
|
|
690
806
|
}
|
|
691
807
|
if (key === "tab") {
|
|
@@ -773,7 +889,7 @@ const commandController = createCommandController({
|
|
|
773
889
|
openOverlay,
|
|
774
890
|
dismissOverlay,
|
|
775
891
|
setInputValue: (value) => {
|
|
776
|
-
if (inputValue === value) {
|
|
892
|
+
if (inputReadOnly || inputValue === value) {
|
|
777
893
|
return;
|
|
778
894
|
}
|
|
779
895
|
inputValue = value;
|
|
@@ -797,6 +913,14 @@ const agentController = createUiAgentController({
|
|
|
797
913
|
commandController.handleCommand(command, state),
|
|
798
914
|
requestRender,
|
|
799
915
|
scrollConversationToBottom,
|
|
916
|
+
clearQueuedInputDraft: () => {
|
|
917
|
+
if (!inputReadOnly) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
inputReadOnly = false;
|
|
921
|
+
inputValue = "";
|
|
922
|
+
inputFocused = true;
|
|
923
|
+
},
|
|
800
924
|
startDividerAnimation,
|
|
801
925
|
stopDividerAnimation,
|
|
802
926
|
});
|
|
@@ -891,7 +1015,30 @@ export function suspendToBackground(
|
|
|
891
1015
|
// Main
|
|
892
1016
|
// ---------------------------------------------------------------------------
|
|
893
1017
|
|
|
894
|
-
function
|
|
1018
|
+
function getConversationViewportHeight(
|
|
1019
|
+
cols: number,
|
|
1020
|
+
rows: number,
|
|
1021
|
+
divider: Node,
|
|
1022
|
+
input: Node,
|
|
1023
|
+
statusBar: Node,
|
|
1024
|
+
): number {
|
|
1025
|
+
if (!Number.isFinite(rows)) {
|
|
1026
|
+
return Number.POSITIVE_INFINITY;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const width = Math.max(1, Math.floor(cols));
|
|
1030
|
+
const reservedHeight =
|
|
1031
|
+
measureContentHeight(divider, { width }) +
|
|
1032
|
+
measureContentHeight(input, { width }) +
|
|
1033
|
+
measureContentHeight(statusBar, { width });
|
|
1034
|
+
return Math.max(1, Math.floor(rows) - reservedHeight);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function renderConversationLog(
|
|
1038
|
+
state: AppState,
|
|
1039
|
+
width: number,
|
|
1040
|
+
viewportHeight: number,
|
|
1041
|
+
): Node {
|
|
895
1042
|
return VStack(
|
|
896
1043
|
{
|
|
897
1044
|
flex: 1,
|
|
@@ -900,23 +1047,11 @@ function renderConversationLog(state: AppState, width: number): Node {
|
|
|
900
1047
|
justifyContent: state.messages.length === 0 ? "center" : undefined,
|
|
901
1048
|
scrollOffset: stickToBottom ? Infinity : scrollOffset,
|
|
902
1049
|
onScroll: (offset, maxOffset) => {
|
|
903
|
-
const wasStickToBottom = stickToBottom;
|
|
904
1050
|
scrollOffset = offset;
|
|
905
|
-
|
|
906
|
-
if (wasStickToBottom && offset < maxOffset) {
|
|
907
|
-
visibleConversationStart = getLatestConversationChunkStart(
|
|
908
|
-
state.messages.length,
|
|
909
|
-
);
|
|
910
|
-
}
|
|
911
|
-
|
|
912
1051
|
stickToBottom = offset >= maxOffset;
|
|
913
|
-
|
|
914
|
-
if (!stickToBottom && offset === 0 && visibleConversationStart > 0) {
|
|
915
|
-
prependConversationChunk(state, width);
|
|
916
|
-
}
|
|
917
1052
|
},
|
|
918
1053
|
},
|
|
919
|
-
buildConversationLog(state, width),
|
|
1054
|
+
buildConversationLog(state, width, viewportHeight),
|
|
920
1055
|
);
|
|
921
1056
|
}
|
|
922
1057
|
|
|
@@ -929,6 +1064,8 @@ function renderConversationLog(state: AppState, width: number): Node {
|
|
|
929
1064
|
* @param state - Application state.
|
|
930
1065
|
* @param cols - Current terminal width in columns.
|
|
931
1066
|
* @param inputController - Stable callbacks for the controlled TextInput.
|
|
1067
|
+
* @param onSuspend - Optional suspend handler for Ctrl+Z.
|
|
1068
|
+
* @param rows - Current terminal height in rows.
|
|
932
1069
|
* @returns The base layout node.
|
|
933
1070
|
*/
|
|
934
1071
|
export function renderBaseLayout(
|
|
@@ -936,6 +1073,7 @@ export function renderBaseLayout(
|
|
|
936
1073
|
cols: number,
|
|
937
1074
|
inputController: InputController,
|
|
938
1075
|
onSuspend?: () => void,
|
|
1076
|
+
rows = Number.POSITIVE_INFINITY,
|
|
939
1077
|
): Node {
|
|
940
1078
|
titleState = state;
|
|
941
1079
|
titleViewportActive = true;
|
|
@@ -943,6 +1081,17 @@ export function renderBaseLayout(
|
|
|
943
1081
|
syncTerminalTitle(state);
|
|
944
1082
|
}
|
|
945
1083
|
|
|
1084
|
+
const divider = renderDivider(state, cols);
|
|
1085
|
+
const input = renderInputArea(state.theme, inputController);
|
|
1086
|
+
const statusBar = renderStatusBar(state, cols);
|
|
1087
|
+
const conversationViewportHeight = getConversationViewportHeight(
|
|
1088
|
+
cols,
|
|
1089
|
+
rows,
|
|
1090
|
+
divider,
|
|
1091
|
+
input,
|
|
1092
|
+
statusBar,
|
|
1093
|
+
);
|
|
1094
|
+
|
|
946
1095
|
return VStack(
|
|
947
1096
|
{
|
|
948
1097
|
height: "100%",
|
|
@@ -973,16 +1122,16 @@ export function renderBaseLayout(
|
|
|
973
1122
|
},
|
|
974
1123
|
[
|
|
975
1124
|
// ── Conversation log ──
|
|
976
|
-
renderConversationLog(state, cols),
|
|
1125
|
+
renderConversationLog(state, cols, conversationViewportHeight),
|
|
977
1126
|
|
|
978
1127
|
// ── Animated divider (pulse when agent is working) ──
|
|
979
|
-
|
|
1128
|
+
divider,
|
|
980
1129
|
|
|
981
1130
|
// ── Input area ──
|
|
982
|
-
|
|
1131
|
+
input,
|
|
983
1132
|
|
|
984
1133
|
// ── Status bar (1 line) ──
|
|
985
|
-
|
|
1134
|
+
statusBar,
|
|
986
1135
|
],
|
|
987
1136
|
);
|
|
988
1137
|
}
|
|
@@ -1005,16 +1154,22 @@ export function startUI(state: AppState): void {
|
|
|
1005
1154
|
|
|
1006
1155
|
cel.viewport(() => {
|
|
1007
1156
|
const cols = terminal.columns;
|
|
1008
|
-
const base = renderBaseLayout(
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1157
|
+
const base = renderBaseLayout(
|
|
1158
|
+
state,
|
|
1159
|
+
cols,
|
|
1160
|
+
inputController,
|
|
1161
|
+
() => {
|
|
1162
|
+
suspendToBackground(() => {
|
|
1163
|
+
resumeTerminalUi();
|
|
1164
|
+
cel._getBuffer()?.clear();
|
|
1165
|
+
if (state.running) {
|
|
1166
|
+
startDividerAnimation();
|
|
1167
|
+
}
|
|
1168
|
+
requestRender("immediate");
|
|
1169
|
+
});
|
|
1170
|
+
},
|
|
1171
|
+
terminal.rows,
|
|
1172
|
+
);
|
|
1018
1173
|
const overlay = renderActiveOverlay(state);
|
|
1019
1174
|
|
|
1020
1175
|
if (overlay) {
|