mini-coder 0.6.1 → 0.6.3
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/bun.lock +51 -47
- package/package.json +2 -2
- package/src/agent.ts +12 -2
- package/src/args.ts +1 -1
- package/src/git.ts +23 -0
- package/src/headless.ts +21 -3
- package/src/index.ts +3 -8
- package/src/oauth.ts +2 -2
- package/src/prompt.ts +60 -84
- package/src/session.ts +7 -5
- package/src/tool-bash.ts +4 -34
- package/src/tool-edit.ts +6 -18
- package/src/tool-read.ts +2 -2
- package/src/tui-components.ts +4 -4
- package/src/tui-conversation.ts +105 -215
- package/src/tui-overlay.ts +2 -2
- package/src/tui.ts +103 -43
- package/src/types.ts +19 -3
package/src/tui-conversation.ts
CHANGED
|
@@ -5,223 +5,13 @@ import type {
|
|
|
5
5
|
Message,
|
|
6
6
|
ToolResultMessage,
|
|
7
7
|
UserMessage,
|
|
8
|
-
} from "@
|
|
8
|
+
} from "@earendil-works/pi-ai";
|
|
9
9
|
import { estimateTokens, relativeTime } from "./shared";
|
|
10
10
|
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)];
|
|
11
|
+
import type { TUIMessage, TUIState, TUIToolCall } from "./types";
|
|
223
12
|
|
|
224
13
|
export function emptyState(): Node {
|
|
14
|
+
const randColor = theme.bgreen;
|
|
225
15
|
return HStack({ flex: 1, alignItems: "center" }, [
|
|
226
16
|
VStack({ flex: 1, alignItems: "center", gap: 1 }, [
|
|
227
17
|
HStack({ gap: 1 }, [
|
|
@@ -254,11 +44,111 @@ export function emptyState(): Node {
|
|
|
254
44
|
]);
|
|
255
45
|
}
|
|
256
46
|
|
|
47
|
+
function ConversationMessageToolCall(call: TUIToolCall) {
|
|
48
|
+
let outputNode: Node | null = null;
|
|
49
|
+
|
|
50
|
+
// Compress read and bash calls
|
|
51
|
+
if (call.tool === "read") {
|
|
52
|
+
outputNode = Text(`Read ~${estimateTokens(call.output)} tokens`, {
|
|
53
|
+
wrap: "word",
|
|
54
|
+
});
|
|
55
|
+
} else if (call.tool === "bash") {
|
|
56
|
+
const tail = call.output.trim().slice(-200);
|
|
57
|
+
const blocks: Node[] = [];
|
|
58
|
+
|
|
59
|
+
blocks.push(Text(tail, { fgColor: theme.white, wrap: "word" }));
|
|
60
|
+
|
|
61
|
+
if (tail.length !== call.output.trim().length) {
|
|
62
|
+
blocks.push(
|
|
63
|
+
Text("Showing the last 200 characters", {
|
|
64
|
+
fgColor: theme.bblack,
|
|
65
|
+
italic: true,
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
outputNode = VStack({ width: "100%" }, blocks);
|
|
71
|
+
} else if (call.tool === "edit") {
|
|
72
|
+
outputNode = SyntaxHighlight(call.output, "patch");
|
|
73
|
+
} else {
|
|
74
|
+
outputNode = Text(call.output, { wrap: "word" });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return VStack({}, [
|
|
78
|
+
TextPill(call.tool, theme.black, theme.bwhite),
|
|
79
|
+
|
|
80
|
+
...Object.entries(call.args).map(([key, value]) => {
|
|
81
|
+
let node: Node | null = Text(String(value));
|
|
82
|
+
|
|
83
|
+
// Syntax highlight bash args
|
|
84
|
+
if (call.tool === "bash") {
|
|
85
|
+
node = SyntaxHighlight(String(value), "bash");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return HStack({ gap: 1 }, [
|
|
89
|
+
Text(`${key}`, { italic: true, fgColor: theme.white }),
|
|
90
|
+
node,
|
|
91
|
+
]);
|
|
92
|
+
}),
|
|
93
|
+
|
|
94
|
+
outputNode,
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function ConversationMessage(message: TUIMessage) {
|
|
99
|
+
const blocks: Node[] = [];
|
|
100
|
+
|
|
101
|
+
if (message.thinking) {
|
|
102
|
+
// Compress thinking blocks
|
|
103
|
+
const estThinkingTok = estimateTokens(message.thinking);
|
|
104
|
+
blocks.push(
|
|
105
|
+
Text(`Thinking... ~${estThinkingTok} tokens`, {
|
|
106
|
+
wrap: "word",
|
|
107
|
+
fgColor: theme.bblack,
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (message.text.length) {
|
|
113
|
+
blocks.push(
|
|
114
|
+
VStack(
|
|
115
|
+
{
|
|
116
|
+
width: "100%",
|
|
117
|
+
bgColor: message.role === "user" ? theme.bblack : undefined,
|
|
118
|
+
padding: { y: 1, x: 1 },
|
|
119
|
+
},
|
|
120
|
+
[SyntaxHighlight(message.text, "markdown")],
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (message.toolCalls?.length) {
|
|
126
|
+
blocks.push(
|
|
127
|
+
VStack({ gap: 1 }, message.toolCalls.map(ConversationMessageToolCall)),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Msg footer:
|
|
132
|
+
blocks.push(
|
|
133
|
+
Text(`on ${message.timestamp} by ${message.role}`, {
|
|
134
|
+
fgColor: theme.bblack,
|
|
135
|
+
italic: true,
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return VStack(
|
|
140
|
+
{
|
|
141
|
+
gap: 1,
|
|
142
|
+
},
|
|
143
|
+
blocks,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
257
147
|
export function Conversation(state: TUIState) {
|
|
258
148
|
return VStack(
|
|
259
149
|
{
|
|
260
150
|
flex: 1,
|
|
261
|
-
gap:
|
|
151
|
+
gap: 2,
|
|
262
152
|
overflow: "scroll",
|
|
263
153
|
scrollOffset: state.stickToBottom ? Infinity : state.scrollOffset,
|
|
264
154
|
onScroll(offset, maxOffset) {
|
|
@@ -266,6 +156,6 @@ export function Conversation(state: TUIState) {
|
|
|
266
156
|
state.stickToBottom = offset >= maxOffset;
|
|
267
157
|
},
|
|
268
158
|
},
|
|
269
|
-
state.
|
|
159
|
+
state.tuiMessages.map(ConversationMessage),
|
|
270
160
|
);
|
|
271
161
|
}
|
package/src/tui-overlay.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HStack, Text, TextInput, VStack } from "@cel-tui/core";
|
|
2
|
-
import { getModels, type ThinkingLevel } from "@
|
|
3
|
-
import { getOAuthProviders } from "@
|
|
2
|
+
import { getModels, type ThinkingLevel } from "@earendil-works/pi-ai";
|
|
3
|
+
import { getOAuthProviders } from "@earendil-works/pi-ai/oauth";
|
|
4
4
|
import { saveSettings } from "./args";
|
|
5
5
|
import { getAvailableProviders } from "./oauth";
|
|
6
6
|
import { listSessionsForCwd } from "./session";
|
package/src/tui.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { cel, HStack, ProcessTerminal, VStack } from "@cel-tui/core";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import type {
|
|
3
|
+
AssistantMessage,
|
|
4
|
+
ToolResultMessage,
|
|
5
|
+
} from "@earendil-works/pi-ai";
|
|
6
|
+
import { streamAgent } from "./agent";
|
|
7
|
+
import { getBranchLabel } from "./git";
|
|
8
|
+
import {
|
|
9
|
+
buildSystemPrompt,
|
|
10
|
+
injectEnvReminder,
|
|
11
|
+
insertToolUsageReminder,
|
|
12
|
+
MAIN_PROMPT,
|
|
13
|
+
} from "./prompt";
|
|
5
14
|
import { updateSession } from "./session";
|
|
6
|
-
import { estimateTokens, secureRandomString } from "./shared";
|
|
15
|
+
import { estimateTokens, formatTimestamp, secureRandomString } from "./shared";
|
|
7
16
|
import { bash, runBashTool } from "./tool-bash";
|
|
8
17
|
import { edit, runEditTool } from "./tool-edit";
|
|
9
18
|
import { read, runReadTool } from "./tool-read";
|
|
@@ -19,10 +28,7 @@ import {
|
|
|
19
28
|
import { Conversation, emptyState } from "./tui-conversation";
|
|
20
29
|
import { Editor } from "./tui-editor";
|
|
21
30
|
import { mainMenu } from "./tui-overlay";
|
|
22
|
-
import type { AgentContex, ToolAndRunner, TUIState } from "./types";
|
|
23
|
-
|
|
24
|
-
// TODO: move all git things to `git.ts`
|
|
25
|
-
const git = simpleGit();
|
|
31
|
+
import type { AgentContex, ToolAndRunner, TUIMessage, TUIState } from "./types";
|
|
26
32
|
|
|
27
33
|
function clearOrAbort(state: TUIState) {
|
|
28
34
|
// Are we mid stream? Abort it.
|
|
@@ -154,6 +160,11 @@ async function streamAgentTUI(state: TUIState) {
|
|
|
154
160
|
content: userContent,
|
|
155
161
|
timestamp: Date.now(),
|
|
156
162
|
});
|
|
163
|
+
state.tuiMessages.push({
|
|
164
|
+
timestamp: formatTimestamp(Date.now()),
|
|
165
|
+
role: "user",
|
|
166
|
+
text: state.prompt,
|
|
167
|
+
});
|
|
157
168
|
state.prompt = "";
|
|
158
169
|
|
|
159
170
|
const systemPrompt = await buildSystemPrompt(MAIN_PROMPT);
|
|
@@ -165,41 +176,102 @@ async function streamAgentTUI(state: TUIState) {
|
|
|
165
176
|
signal: state.abortController?.signal,
|
|
166
177
|
};
|
|
167
178
|
|
|
168
|
-
|
|
169
|
-
|
|
179
|
+
const toTUIMessage = (partial: AssistantMessage) => {
|
|
180
|
+
const text = partial.content
|
|
181
|
+
.filter((c) => c.type === "text")
|
|
182
|
+
.map((c) => c.text)
|
|
183
|
+
.join("")
|
|
184
|
+
.trim();
|
|
185
|
+
const thinking = partial.content
|
|
186
|
+
.filter((c) => c.type === "thinking")
|
|
187
|
+
.map((c) => c.thinking)
|
|
188
|
+
.join("")
|
|
189
|
+
.trim();
|
|
190
|
+
const toolCalls = partial.content
|
|
191
|
+
.filter((c) => c.type === "toolCall")
|
|
192
|
+
.map((c) => {
|
|
193
|
+
return {
|
|
194
|
+
id: c.id,
|
|
195
|
+
tool: c.name,
|
|
196
|
+
args: c.arguments,
|
|
197
|
+
output: "",
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
timestamp: formatTimestamp(partial.timestamp),
|
|
203
|
+
role: "assistant" as const,
|
|
204
|
+
text,
|
|
205
|
+
thinking,
|
|
206
|
+
toolCalls,
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const updateToolCall = (
|
|
211
|
+
partial: ToolResultMessage,
|
|
212
|
+
tuiMessages: TUIMessage[],
|
|
213
|
+
) => {
|
|
214
|
+
tuiMessages.forEach((c) => {
|
|
215
|
+
const parentCall = c.toolCalls?.find((t) => t.id === partial.toolCallId);
|
|
216
|
+
if (parentCall) {
|
|
217
|
+
parentCall.output = partial.content
|
|
218
|
+
.filter((c) => c.type === "text")
|
|
219
|
+
.map((c) => c.text)
|
|
220
|
+
.join("")
|
|
221
|
+
.trim();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
|
|
170
226
|
const agent = streamAgent(ctx);
|
|
171
227
|
try {
|
|
172
228
|
for await (const ev of agent) {
|
|
173
229
|
switch (ev.type) {
|
|
174
230
|
case "message_start":
|
|
231
|
+
state.tuiMessages.push(toTUIMessage(ev.partial));
|
|
232
|
+
break;
|
|
175
233
|
case "message_update":
|
|
234
|
+
state.tuiMessages[state.tuiMessages.length - 1] = toTUIMessage(
|
|
235
|
+
ev.partial,
|
|
236
|
+
);
|
|
176
237
|
break;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
238
|
+
case "message_end": {
|
|
239
|
+
state.tuiMessages[state.tuiMessages.length - 1] = toTUIMessage(
|
|
240
|
+
ev.message,
|
|
241
|
+
);
|
|
242
|
+
const { systemPrompt, tools, messages } = ctx;
|
|
243
|
+
state.contextSize = estimateTokens(
|
|
244
|
+
JSON.stringify({ systemPrompt, tools, messages }),
|
|
245
|
+
);
|
|
180
246
|
break;
|
|
247
|
+
}
|
|
181
248
|
|
|
182
249
|
case "tool_message_start":
|
|
250
|
+
updateToolCall(ev.partial, state.tuiMessages);
|
|
251
|
+
break;
|
|
183
252
|
case "tool_message_update":
|
|
253
|
+
updateToolCall(ev.partial, state.tuiMessages);
|
|
184
254
|
break;
|
|
185
|
-
|
|
186
255
|
case "tool_message_end": {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
256
|
+
updateToolCall(ev.message, state.tuiMessages);
|
|
257
|
+
const withReminder = insertToolUsageReminder(
|
|
258
|
+
state.messages,
|
|
259
|
+
ev.message,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const idx = state.messages.findIndex(
|
|
263
|
+
(m) =>
|
|
264
|
+
m.role === "toolResult" &&
|
|
265
|
+
m.toolCallId === withReminder.toolCallId,
|
|
266
|
+
);
|
|
267
|
+
if (idx >= 0) {
|
|
268
|
+
state.messages[idx] = withReminder;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const { systemPrompt, tools, messages } = ctx;
|
|
272
|
+
state.contextSize = estimateTokens(
|
|
273
|
+
JSON.stringify({ systemPrompt, tools, messages }),
|
|
274
|
+
);
|
|
203
275
|
}
|
|
204
276
|
}
|
|
205
277
|
}
|
|
@@ -209,20 +281,8 @@ async function streamAgentTUI(state: TUIState) {
|
|
|
209
281
|
const id = secureRandomString(10);
|
|
210
282
|
state.sessionId = id;
|
|
211
283
|
}
|
|
212
|
-
// TODO: Should we make this delta only so compaction doesn;t affect saves?
|
|
213
|
-
// I'm not sure since it we do, there is no trace in logs about compaction
|
|
214
|
-
// and that would mean the logs don't repesent the truth. Confusing decision.
|
|
215
284
|
await updateSession(state.sessionId, state.messages);
|
|
216
|
-
|
|
217
|
-
// Compact after saving, if the next turn fails because of compaction, the session is recoverable.
|
|
218
|
-
// Compact at 80k tokens, the dumb zone threshold.
|
|
219
|
-
if (estimateTokens(JSON.stringify(state.messages)) > 80000)
|
|
220
|
-
compactContext(state.messages);
|
|
221
285
|
}
|
|
222
286
|
|
|
223
|
-
|
|
224
|
-
const gitStatus = (await git.status()).isClean() ? "" : "*";
|
|
225
|
-
const gitBranch = (await git.branch()).current;
|
|
226
|
-
state.gitBranch = `${gitBranch}${gitStatus}`;
|
|
227
|
-
} catch (_) {}
|
|
287
|
+
state.gitBranch = await getBranchLabel();
|
|
228
288
|
}
|
package/src/types.ts
CHANGED
|
@@ -8,8 +8,8 @@ import {
|
|
|
8
8
|
type Tool,
|
|
9
9
|
type ToolResultMessage,
|
|
10
10
|
Type,
|
|
11
|
-
} from "@
|
|
12
|
-
import type { OAuthCredentials } from "@
|
|
11
|
+
} from "@earendil-works/pi-ai";
|
|
12
|
+
import type { OAuthCredentials } from "@earendil-works/pi-ai/oauth";
|
|
13
13
|
|
|
14
14
|
const ThinkingLevelSchema = Type.Unsafe<ThinkingLevel>(
|
|
15
15
|
Type.Union([
|
|
@@ -76,10 +76,26 @@ export const SessionSchema = Type.Object({
|
|
|
76
76
|
export type Session = Static<typeof SessionSchema>;
|
|
77
77
|
export type Sessions = Session[];
|
|
78
78
|
|
|
79
|
+
export type TUIToolCall = {
|
|
80
|
+
id: string;
|
|
81
|
+
tool: string;
|
|
82
|
+
args: Record<string, any>;
|
|
83
|
+
output: string;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type TUIMessage = {
|
|
87
|
+
timestamp: string;
|
|
88
|
+
role: "user" | "assistant";
|
|
89
|
+
text: string;
|
|
90
|
+
thinking?: string;
|
|
91
|
+
toolCalls?: TUIToolCall[];
|
|
92
|
+
};
|
|
93
|
+
|
|
79
94
|
export type TUIState = {
|
|
80
95
|
options: CliOptions;
|
|
81
96
|
prompt: string;
|
|
82
|
-
messages: Message[];
|
|
97
|
+
messages: Message[]; // Context messages
|
|
98
|
+
tuiMessages: TUIMessage[];
|
|
83
99
|
contextSize?: number;
|
|
84
100
|
stickToBottom: boolean;
|
|
85
101
|
scrollOffset: number;
|