mini-coder 0.6.2 → 0.6.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mini-coder",
3
- "version": "0.6.2",
3
+ "version": "0.6.5",
4
4
  "type": "module",
5
5
  "packageManager": "bun@1.3.12",
6
6
  "bin": {
@@ -14,17 +14,17 @@
14
14
  "format": "bun run prettier --write *.md && bun run biome format --write . && bun run biome check --write ."
15
15
  },
16
16
  "devDependencies": {
17
- "@biomejs/biome": "^2.4.13",
18
- "@types/bun": "^1.3.13",
17
+ "@biomejs/biome": "^2.4.15",
18
+ "@types/bun": "^1.3.14",
19
19
  "prettier": "^3.8.3",
20
20
  "typescript": "^6.0.3"
21
21
  },
22
22
  "dependencies": {
23
23
  "@cel-tui/components": "^0.8.3",
24
24
  "@cel-tui/core": "^0.8.3",
25
- "@earendil-works/pi-ai": "^0.74.0",
25
+ "@earendil-works/pi-ai": "^0.75.5",
26
26
  "diff": "^9.0.0",
27
27
  "simple-git": "^3.36.0",
28
- "yaml": "^2.8.3"
28
+ "yaml": "^2.9.0"
29
29
  }
30
30
  }
package/src/agent.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  type ToolResultMessage,
10
10
  } from "@earendil-works/pi-ai";
11
11
  import { getApiKey } from "./oauth";
12
+ import { estimateTokens } from "./shared";
12
13
  import type {
13
14
  AgentContex,
14
15
  AgentEvent,
@@ -77,6 +78,10 @@ export async function* streamAgent(
77
78
 
78
79
  // Main agent loop, continues until llm sends a response other than toolCall or has no tool calls.
79
80
  while (true) {
81
+ let estimate = estimateTokens(JSON.stringify(llmCtx));
82
+ // 80k is the agreed uppon threshold to the DUMB ZONE
83
+ if (estimate > 80000) compactContext(llmCtx.messages);
84
+
80
85
  const s = streamSimple(agentCtx.options.model, llmCtx, {
81
86
  reasoning: agentCtx.options.effort,
82
87
  signal: agentCtx.signal,
@@ -103,13 +108,18 @@ export async function* streamAgent(
103
108
  case "thinking_end":
104
109
  case "toolcall_start":
105
110
  case "toolcall_delta":
106
- case "toolcall_end":
111
+ case "toolcall_end": {
107
112
  if (partial) {
108
113
  partial = e.partial;
109
114
  llmCtx.messages[llmCtx.messages.length - 1] = partial;
110
115
  yield { type: "message_update", partial };
111
116
  }
117
+
118
+ estimate = estimateTokens(JSON.stringify(llmCtx));
119
+ // 80k is the agreed uppon threshold to the DUMB ZONE
120
+ if (estimate > 80000) compactContext(llmCtx.messages);
112
121
  break;
122
+ }
113
123
 
114
124
  case "error": {
115
125
  const finalMessage = await s.result();
package/src/git.ts ADDED
@@ -0,0 +1,23 @@
1
+ import simpleGit from "simple-git";
2
+
3
+ const git = simpleGit();
4
+
5
+ export async function getGitStatus() {
6
+ try {
7
+ const status = await git.status();
8
+ return status;
9
+ } catch (_) {
10
+ return { nogit: "No git repo in this folder." };
11
+ } // No git
12
+ }
13
+
14
+ export async function getBranchLabel() {
15
+ try {
16
+ const status = await git.status();
17
+ const isClean = status.isClean();
18
+
19
+ return `${status.current}${isClean ? "" : "*"}`;
20
+ } catch (_) {
21
+ return "No git";
22
+ } // No git
23
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { basename } from "node:path";
2
- import simpleGit from "simple-git";
3
2
  import { handleArgv } from "./args.ts";
3
+ import { getBranchLabel } from "./git.ts";
4
4
  import { streamHeadless } from "./headless.ts";
5
5
  import { initTUI } from "./tui.ts";
6
6
  import type { TUIState } from "./types.ts";
@@ -23,18 +23,13 @@ export async function main(): Promise<void> {
23
23
  options,
24
24
  prompt: "",
25
25
  messages: [],
26
+ tuiMessages: [],
26
27
  streaming: false,
27
28
  stickToBottom: true,
28
29
  scrollOffset: 0,
29
30
  cwd,
31
+ gitBranch: await getBranchLabel(),
30
32
  };
31
33
 
32
- const git = simpleGit();
33
- try {
34
- const gitStatus = (await git.status()).isClean() ? "" : "*";
35
- const gitBranch = (await git.branch()).current;
36
- state.gitBranch = `${gitBranch}${gitStatus}`;
37
- } catch (_) {} // No git
38
-
39
34
  initTUI(state, leave);
40
35
  }
package/src/oauth.ts CHANGED
@@ -5,11 +5,55 @@ import {
5
5
  getOAuthApiKey,
6
6
  getOAuthProvider,
7
7
  getOAuthProviders,
8
+ type OAuthLoginCallbacks,
9
+ type OAuthPrompt,
8
10
  type OAuthProviderId,
11
+ type OAuthSelectPrompt,
9
12
  } from "@earendil-works/pi-ai/oauth";
10
13
  import { AUTH_PATH as AUTH_FILE } from "./shared";
11
14
  import type { CliOptions, SavedOAuthCreds } from "./types";
12
15
 
16
+ type ReadlineInterface = ReturnType<typeof readline.createInterface>;
17
+
18
+ function ask(rl: ReadlineInterface, question: string): Promise<string> {
19
+ return new Promise((resolve) => rl.question(question, resolve));
20
+ }
21
+
22
+ function formatPrompt(prompt: OAuthPrompt): string {
23
+ const placeholder = prompt.placeholder ? ` (${prompt.placeholder})` : "";
24
+ return `${prompt.message}${placeholder}: `;
25
+ }
26
+
27
+ async function selectOption(
28
+ rl: ReadlineInterface,
29
+ prompt: OAuthSelectPrompt,
30
+ ): Promise<string | undefined> {
31
+ console.log(prompt.message);
32
+ for (let i = 0; i < prompt.options.length; i++) {
33
+ console.log(`${i + 1}. ${prompt.options[i]?.label}`);
34
+ }
35
+
36
+ const choice = await ask(rl, `Enter number (1-${prompt.options.length}): `);
37
+ const index = Number.parseInt(choice, 10) - 1;
38
+ return prompt.options[index]?.id;
39
+ }
40
+
41
+ function createLoginCallbacks(rl: ReadlineInterface): OAuthLoginCallbacks {
42
+ return {
43
+ onAuth: ({ url, instructions }) => {
44
+ console.log(`Open: ${url}`);
45
+ if (instructions) console.log(instructions);
46
+ },
47
+ onDeviceCode: ({ userCode, verificationUri }) => {
48
+ console.log(`Open: ${verificationUri}`);
49
+ console.log(`Enter code: ${userCode}`);
50
+ },
51
+ onPrompt: (prompt) => ask(rl, formatPrompt(prompt)),
52
+ onProgress: (message) => console.log(message),
53
+ onSelect: (prompt) => selectOption(rl, prompt),
54
+ };
55
+ }
56
+
13
57
  export function isOAuthProvider(provider: string): boolean {
14
58
  return getOAuthProviders().some(
15
59
  (oauthProvider) => oauthProvider.id === provider,
@@ -47,18 +91,7 @@ export async function loginOAuth(provider: OAuthProviderId) {
47
91
  });
48
92
 
49
93
  try {
50
- const creds = await oauthProvider.login({
51
- onAuth: ({ url, instructions }) => {
52
- console.log(`Open: ${url}`);
53
- if (instructions) console.log(instructions);
54
- },
55
- onPrompt: async (prompt) => {
56
- let answer: string = "";
57
- await rl.question(prompt.message, (a) => (answer = a));
58
- return answer;
59
- },
60
- onProgress: (message) => console.log(message),
61
- });
94
+ const creds = await oauthProvider.login(createLoginCallbacks(rl));
62
95
 
63
96
  await writeCreds({ [provider]: { type: "oauth", ...creds } });
64
97
  } finally {
package/src/prompt.ts CHANGED
@@ -3,7 +3,7 @@ import { readdir } from "node:fs/promises";
3
3
  import { homedir, platform } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import type { Message, ToolResultMessage } from "@earendil-works/pi-ai";
6
- import simpleGit, { type StatusResult } from "simple-git";
6
+ import { getGitStatus } from "./git";
7
7
  import { parseSkillFrontmatter } from "./shared";
8
8
 
9
9
  export const MAIN_PROMPT = `You are a coding agent interacting with users via the mini-coder harness. You help users by reading files, executing commands, and editting code.
@@ -36,12 +36,7 @@ async function getDir() {
36
36
 
37
37
  async function getEnvPrompt() {
38
38
  // TODO: What else do the agents always check before answering every time?
39
- let gitStatus: StatusResult | { nogit: string };
40
- try {
41
- gitStatus = await simpleGit().status();
42
- } catch (_) {
43
- gitStatus = { nogit: "No git repo in this folder." };
44
- }
39
+ const gitStatus = await getGitStatus();
45
40
  const envKeys = ["PATH", "USER", "LANG", "HOME", "SHELL", "BUN_INSTALL"];
46
41
  const env: Record<string, string> = {};
47
42
  for (const key of envKeys) {
@@ -216,7 +211,7 @@ export function insertToolUsageReminder(
216
211
  for (const c of calls) {
217
212
  const args = JSON.stringify(c.arguments);
218
213
  if (seenArgs.has(args)) sameToolCount++;
219
- seenArgs.add(args)
214
+ seenArgs.add(args);
220
215
  }
221
216
  }
222
217
  }
package/src/session.ts CHANGED
@@ -5,8 +5,6 @@ import { Value } from "typebox/value";
5
5
  import { SESSIONS_DIR } from "./shared";
6
6
  import { type Session, SessionSchema } from "./types";
7
7
 
8
- // TODO: sessions are json files in SESSIONS_DIR inside of DATA_DIR, use a 10 length `secureRandomString()` for the ids.
9
-
10
8
  export async function ensureSessionsDir(): Promise<void> {
11
9
  await mkdir(SESSIONS_DIR, { recursive: true });
12
10
  }
@@ -71,8 +69,12 @@ export async function saveSession(s: Session) {
71
69
  export async function updateSession(id: string, messages: Message[]) {
72
70
  const existing = await getSession(id);
73
71
  if (existing) {
74
- existing.messages = messages;
75
- await saveSession(existing);
72
+ // Only append new messages, so we don't save compacted messages.
73
+ if (existing.messages.length < messages.length) {
74
+ const newMessages = messages.slice(existing.messages.length);
75
+ existing.messages = [...existing.messages, ...newMessages];
76
+ await saveSession(existing);
77
+ }
76
78
  return;
77
79
  }
78
80
 
package/src/tool-bash.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  import { type Tool, Type } from "@earendil-works/pi-ai";
2
- import { secureRandomString } from "./shared";
3
2
  import type { ToolRunnerEvent } from "./types";
4
3
 
5
- const OUTPUT_THRESHOLD = 16000;
6
4
  const description = `Bash CLI tool
7
5
 
8
6
  Execute shell commands on the user's environment.
@@ -68,25 +66,9 @@ export async function* runBashTool(
68
66
 
69
67
  const exitCode = await proc.exited;
70
68
 
71
- let result = `# EXIT CODE: ${exitCode}`;
69
+ let result = `${output.length ? output : "(no ouput)"}\n\nExit code: ${exitCode}`;
72
70
  if (output.length) {
73
- result += `
74
- # OUTPUT:
75
-
76
- ${output}`;
77
- }
78
-
79
- // If `out` is too big, more than ~XXKB, write it to a temp file
80
- // And add that to the truncation label for the agent to be able
81
- // to continue the read with scans. This is to protect context,
82
- // not a general read guard. The hint is for the agent, not the TUI
83
- if (result.length > OUTPUT_THRESHOLD) {
84
- const key = `${Date.now()}-${secureRandomString(4)}`;
85
- const pathname = `/tmp/bash_result_${key}.txt`;
86
- await Bun.write(pathname, result);
87
- result = `${result.substring(0, OUTPUT_THRESHOLD)}
88
-
89
- Truncated at ~${OUTPUT_THRESHOLD / 1000}KB. Full output at ${pathname}`;
71
+ result += `${output}`;
90
72
  }
91
73
 
92
74
  yield {
@@ -88,7 +88,7 @@ export function Spinner() {
88
88
  }
89
89
 
90
90
  export function ActivityPill(state: TUIState, spinnerFrame: string) {
91
- let label = "idle";
91
+ let label = "";
92
92
  const frame = state.streaming ? spinnerFrame : "";
93
93
 
94
94
  if (frame) {
@@ -1,227 +1,11 @@
1
1
  import { SyntaxHighlight } from "@cel-tui/components";
2
2
  import { HStack, type Node, Text, VStack } from "@cel-tui/core";
3
- import type {
4
- AssistantMessage,
5
- Message,
6
- ToolResultMessage,
7
- UserMessage,
8
- } from "@earendil-works/pi-ai";
9
- import { estimateTokens, relativeTime } from "./shared";
3
+ import { estimateTokens } from "./shared";
10
4
  import { TextPill, theme } from "./tui-components";
11
- import type { TUIState } from "./types";
12
-
13
- function agentMessageNode(msg: AssistantMessage): Node {
14
- let thinking = "";
15
- let text = "";
16
- const toolCalls: Node[] = [];
17
-
18
- for (const block of msg.content) {
19
- if (block.type === "thinking" && block.thinking.length > 0) {
20
- thinking += block.thinking;
21
- }
22
-
23
- if (block.type === "text" && block.text.length > 0) {
24
- text += block.text;
25
- }
26
-
27
- if (block.type === "toolCall" && block.arguments && block.name) {
28
- let text = "";
29
- let node: Node | undefined;
30
- if ("path" in block.arguments) {
31
- text = block.arguments.path;
32
- node = Text(text);
33
- } else if ("command" in block.arguments) {
34
- text = block.arguments.command;
35
- node = SyntaxHighlight(text, "bash");
36
- } else {
37
- text = JSON.stringify(block.arguments);
38
- node = Text(text);
39
- }
40
-
41
- toolCalls.push(
42
- VStack({ padding: { x: 4 }, gap: 1 }, [
43
- TextPill(block.name, theme.white, theme.bblack),
44
- node,
45
- ]),
46
- );
47
- }
48
- }
49
- const textBlocks: Node[] = [];
50
- if (thinking.length > 0) {
51
- const tokens = estimateTokens(thinking);
52
-
53
- textBlocks.push(
54
- Text(`Thinking... (~${tokens} tokens)`, {
55
- fgColor: theme.bblack,
56
- italic: true,
57
- }),
58
- );
59
- }
60
- if (text.length > 0) {
61
- textBlocks.push(SyntaxHighlight(text, "markdown"));
62
- }
63
- if (toolCalls.length > 0) {
64
- textBlocks.push(...toolCalls);
65
- }
66
- const error =
67
- ((msg.stopReason === "error" || msg.stopReason === "aborted") &&
68
- msg.errorMessage) ??
69
- "Unknown error.";
70
- if (error) {
71
- textBlocks.push(
72
- VStack({ padding: { x: 4 }, gap: 1 }, [
73
- TextPill(msg.stopReason, theme.white, theme.bblack),
74
- Text(error),
75
- ]),
76
- );
77
- }
78
-
79
- if (textBlocks.length === 0) {
80
- textBlocks.push(
81
- Text("Loading...", {
82
- fgColor: theme.bblack,
83
- italic: true,
84
- }),
85
- );
86
- }
87
-
88
- return VStack({ gap: 1 }, textBlocks);
89
- }
90
-
91
- function userMessageNode(msg: UserMessage): Node {
92
- let text = "";
93
- if (typeof msg.content === "string") {
94
- text = msg.content;
95
- } else {
96
- text = msg.content
97
- .filter((b) => b.type === "text")
98
- .map((b) => b.text)
99
- .join("");
100
- }
101
-
102
- // Remove any reminders we might have attached before render
103
- // Keep this fast, it runs on the render cycle.
104
- text = text
105
- .replaceAll(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "")
106
- .trimStart();
107
-
108
- return VStack(
109
- {
110
- padding: { x: 1, y: 1 },
111
- bgColor: theme.bblack,
112
- fgColor: theme.white,
113
- },
114
- [SyntaxHighlight(text, "markdown")],
115
- );
116
- }
117
-
118
- function toolMessageNode(msg: ToolResultMessage): Node {
119
- // TODO: read tool image output
120
- // Output only shows last 10 lines of scroll.
121
- const text = msg.content
122
- .filter((c) => c.type === "text")
123
- .map((c) => c.text)
124
- .join("")
125
- .trim();
126
-
127
- return VStack({ padding: { x: 4 }, gap: 1 }, [
128
- Text(`~${estimateTokens(text)} tokens, ${text.split("\n").length} lines.`, {
129
- fgColor: theme.bblack,
130
- }),
131
- VStack(
132
- {
133
- flex: 1,
134
- maxHeight: msg.toolName === "edit" ? 20 : 10,
135
- overflow: "scroll",
136
- scrollOffset: Infinity,
137
- onScroll: () => false,
138
- },
139
- [
140
- msg.toolName === "edit"
141
- ? SyntaxHighlight(text, "diff")
142
- : Text(text, { wrap: "word", fgColor: theme.white }),
143
- ],
144
- ),
145
- ]);
146
- }
147
-
148
- const messageBodyCache = new WeakMap<Message, { key: number; node: Node }>();
149
-
150
- function messageCacheKey(msg: Message): number {
151
- if (msg.role === "assistant") {
152
- let key = 0;
153
- for (const block of msg.content) {
154
- if (block.type === "thinking") key += block.thinking.length;
155
- if (block.type === "text") key += block.text.length;
156
- if (block.type === "toolCall" && block.arguments)
157
- key += JSON.stringify(block.arguments).length;
158
- }
159
- if (msg.stopReason) key += msg.stopReason.length;
160
- if (msg.errorMessage) key += msg.errorMessage.length;
161
- return key;
162
- }
163
- if (msg.role === "user") {
164
- if (typeof msg.content === "string") return msg.content.length;
165
- return msg.content.reduce(
166
- (sum, b) => (b.type === "text" ? sum + b.text.length : sum),
167
- 0,
168
- );
169
- }
170
- if (msg.role === "toolResult") {
171
- return msg.content.reduce(
172
- (sum, b) => (b.type === "text" ? sum + b.text.length : sum),
173
- 0,
174
- );
175
- }
176
- return 0;
177
- }
178
-
179
- function cachedMessageBody(msg: Message): Node {
180
- const key = messageCacheKey(msg);
181
- const cached = messageBodyCache.get(msg);
182
- if (cached && cached.key === key) {
183
- return cached.node;
184
- }
185
- const node =
186
- msg.role === "assistant"
187
- ? agentMessageNode(msg)
188
- : msg.role === "user"
189
- ? userMessageNode(msg)
190
- : msg.role === "toolResult"
191
- ? toolMessageNode(msg)
192
- : Text("Unknown message?", {
193
- wrap: "word",
194
- fgColor: theme.bwhite,
195
- });
196
- messageBodyCache.set(msg, { key, node });
197
- return node;
198
- }
199
-
200
- function conversationMessageNode(msg: Message): Node {
201
- const label = msg.role === "toolResult" ? `${msg.toolName} result` : msg.role;
202
- return VStack({ gap: 1 }, [
203
- cachedMessageBody(msg),
204
- HStack({ gap: 1, justifyContent: "end" }, [
205
- Text(`${relativeTime(msg.timestamp)} ago.`, {
206
- fgColor: theme.bblack,
207
- italic: true,
208
- }),
209
- TextPill(label, theme.bwhite, theme.bblack),
210
- ]),
211
- ]);
212
- }
213
-
214
- const colors = [
215
- theme.bgreen,
216
- theme.byellow,
217
- theme.bblue,
218
- theme.bcyan,
219
- theme.bmagenta,
220
- theme.bred,
221
- ];
222
- const randColor = colors[Math.floor(Math.random() * colors.length)];
5
+ import type { TUIMessage, TUIState, TUIToolCall } from "./types";
223
6
 
224
7
  export function emptyState(): Node {
8
+ const randColor = theme.bgreen;
225
9
  return HStack({ flex: 1, alignItems: "center" }, [
226
10
  VStack({ flex: 1, alignItems: "center", gap: 1 }, [
227
11
  HStack({ gap: 1 }, [
@@ -254,11 +38,129 @@ export function emptyState(): Node {
254
38
  ]);
255
39
  }
256
40
 
41
+ function ConversationMessageToolCall(call: TUIToolCall) {
42
+ let outputNode: Node | null = null;
43
+
44
+ // Compress read and bash calls
45
+ if (call.tool === "read") {
46
+ outputNode = Text(`Read ~${estimateTokens(call.output)} tokens`, {
47
+ wrap: "word",
48
+ });
49
+ } else if (call.tool === "bash") {
50
+ const tail = call.output.trim().slice(-200);
51
+ const blocks: Node[] = [];
52
+
53
+ blocks.push(Text(tail, { fgColor: theme.white, wrap: "word" }));
54
+
55
+ if (tail.length !== call.output.trim().length) {
56
+ blocks.push(
57
+ Text("Showing the last 200 characters", {
58
+ fgColor: theme.bblack,
59
+ italic: true,
60
+ }),
61
+ );
62
+ }
63
+
64
+ outputNode = VStack({ width: "100%" }, blocks);
65
+ } else if (call.tool === "edit") {
66
+ outputNode = SyntaxHighlight(call.output, "patch");
67
+ } else {
68
+ outputNode = Text(call.output, { wrap: "word" });
69
+ }
70
+
71
+ let argumentNodes: Node[] = [];
72
+
73
+ if (call.tool === "edit") {
74
+ argumentNodes = [
75
+ Text(`Writing... ~${estimateTokens(JSON.stringify(call.args))} tokens`),
76
+ ];
77
+ } else {
78
+ argumentNodes = Object.entries(call.args).map(([key, value]) => {
79
+ let node: Node | null = Text(String(value));
80
+
81
+ // Syntax highlight bash args
82
+ if (call.tool === "bash") {
83
+ node = SyntaxHighlight(String(value), "bash");
84
+ return node;
85
+ }
86
+
87
+ return HStack({ gap: 1 }, [
88
+ Text(`${key}`, { italic: true, fgColor: theme.white }),
89
+ node,
90
+ ]);
91
+ });
92
+ }
93
+
94
+ return VStack({}, [
95
+ TextPill(call.tool, theme.black, theme.bwhite),
96
+ ...argumentNodes,
97
+ outputNode,
98
+ ]);
99
+ }
100
+
101
+ function ConversationMessage(message: TUIMessage) {
102
+ const blocks: Node[] = [];
103
+
104
+ if (message.thinking) {
105
+ // Compress thinking blocks
106
+ const estThinkingTok = estimateTokens(message.thinking);
107
+ blocks.push(
108
+ Text(`Thinking... ~${estThinkingTok} tokens`, {
109
+ wrap: "word",
110
+ fgColor: theme.bblack,
111
+ }),
112
+ );
113
+ }
114
+
115
+ if (message.text.length) {
116
+ blocks.push(
117
+ VStack(
118
+ {
119
+ width: "100%",
120
+ bgColor: message.role === "user" ? theme.bblack : undefined,
121
+ padding: { y: 1, x: message.role === "user" ? 1 : 0 },
122
+ },
123
+ [SyntaxHighlight(message.text, "markdown")],
124
+ ),
125
+ );
126
+ }
127
+
128
+ if (message.toolCalls?.length) {
129
+ blocks.push(
130
+ VStack({ gap: 1 }, message.toolCalls.map(ConversationMessageToolCall)),
131
+ );
132
+ }
133
+
134
+ if (!message.thinking && !message.text.length && !message.toolCalls?.length) {
135
+ blocks.push(
136
+ Text(`Loading...`, {
137
+ wrap: "word",
138
+ fgColor: theme.bblack,
139
+ }),
140
+ );
141
+ }
142
+
143
+ // Msg footer:
144
+ blocks.push(
145
+ Text(`on ${message.timestamp} by ${message.role}`, {
146
+ fgColor: theme.bblack,
147
+ italic: true,
148
+ }),
149
+ );
150
+
151
+ return VStack(
152
+ {
153
+ gap: 1,
154
+ },
155
+ blocks,
156
+ );
157
+ }
158
+
257
159
  export function Conversation(state: TUIState) {
258
160
  return VStack(
259
161
  {
260
162
  flex: 1,
261
- gap: 3,
163
+ gap: 2,
262
164
  overflow: "scroll",
263
165
  scrollOffset: state.stickToBottom ? Infinity : state.scrollOffset,
264
166
  onScroll(offset, maxOffset) {
@@ -266,6 +168,6 @@ export function Conversation(state: TUIState) {
266
168
  state.stickToBottom = offset >= maxOffset;
267
169
  },
268
170
  },
269
- state.messages.map(conversationMessageNode),
171
+ state.tuiMessages.map(ConversationMessage),
270
172
  );
271
173
  }