mini-coder 0.5.11 → 0.5.13
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 +107 -0
- package/PROGRESS.md +4 -0
- package/README.md +66 -21
- package/assets/mc-claude-smart.png +0 -0
- package/assets/mc-gpt-smart.png +0 -0
- package/benchmark-baseline.sh +15 -0
- package/benchmark-loop.sh +19 -0
- package/bun.lock +265 -90
- package/package.json +8 -7
- package/skills-lock.json +15 -0
- package/src/agent.ts +20 -0
- package/src/cli.ts +2 -1
- package/src/headless.ts +152 -38
- package/src/index.ts +107 -143
- package/src/input.ts +13 -1
- package/src/mcp.ts +609 -0
- package/src/prompt.ts +10 -12
- package/src/session-message.ts +393 -0
- package/src/session.ts +102 -396
- package/src/settings.ts +218 -22
- package/src/shared.ts +39 -0
- package/src/skills.ts +12 -3
- package/src/submit.ts +20 -25
- package/src/text.ts +71 -0
- package/src/theme.ts +186 -3
- package/src/tool-common.ts +93 -0
- package/src/tool-grep.ts +606 -0
- package/src/tool-read.ts +313 -0
- package/src/tool-shell.ts +1001 -0
- package/src/tools.ts +190 -995
- package/src/ui/agent.ts +206 -110
- package/src/ui/commands.test.ts +489 -17
- package/src/ui/commands.ts +231 -55
- package/src/ui/conversation.test.ts +585 -0
- package/src/ui/conversation.ts +878 -455
- package/src/ui/help.ts +27 -11
- package/src/ui/input.test.ts +1 -43
- package/src/ui/runtime.ts +69 -0
- package/src/ui.ts +422 -185
- package/src/plugins.ts +0 -183
package/src/settings.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* User settings persistence and startup resolution.
|
|
3
3
|
*
|
|
4
4
|
* Stores global defaults such as model, effort, reasoning visibility,
|
|
5
|
-
*
|
|
5
|
+
* verbose tool output, custom providers, and MCP server settings in a JSON
|
|
6
|
+
* file under the app data directory.
|
|
6
7
|
*
|
|
7
8
|
* @module
|
|
8
9
|
*/
|
|
@@ -11,6 +12,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
11
12
|
import { dirname } from "node:path";
|
|
12
13
|
import type { ThinkingLevel } from "@mariozechner/pi-ai";
|
|
13
14
|
import { getErrorMessage } from "./errors.ts";
|
|
15
|
+
import { readBoolean, readString, toRecord } from "./shared.ts";
|
|
14
16
|
|
|
15
17
|
/** A user-configured OpenAI-compatible provider endpoint. */
|
|
16
18
|
export interface CustomProvider {
|
|
@@ -22,6 +24,22 @@ export interface CustomProvider {
|
|
|
22
24
|
apiKey?: string;
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
/** A single configured MCP server endpoint. */
|
|
28
|
+
export interface McpServerConfig {
|
|
29
|
+
/** Stable server identifier. Used as the imported tool-name prefix. */
|
|
30
|
+
name: string;
|
|
31
|
+
/** Absolute Streamable HTTP MCP endpoint URL. */
|
|
32
|
+
url: string;
|
|
33
|
+
/** Whether the server should start enabled. */
|
|
34
|
+
enabled: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** MCP-related user settings. */
|
|
38
|
+
export interface McpSettings {
|
|
39
|
+
/** MCP servers to connect to at startup. */
|
|
40
|
+
servers?: McpServerConfig[];
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
/** Default reasoning effort when no saved setting exists. */
|
|
26
44
|
const DEFAULT_EFFORT: ThinkingLevel = "medium";
|
|
27
45
|
|
|
@@ -43,6 +61,8 @@ export interface UserSettings {
|
|
|
43
61
|
verbose?: boolean;
|
|
44
62
|
/** Custom OpenAI-compatible provider endpoints. */
|
|
45
63
|
customProviders?: CustomProvider[];
|
|
64
|
+
/** MCP server definitions to manage and connect when enabled. */
|
|
65
|
+
mcp?: McpSettings;
|
|
46
66
|
}
|
|
47
67
|
|
|
48
68
|
/** Resolved startup settings after applying defaults and availability checks. */
|
|
@@ -64,6 +84,8 @@ const THINKING_LEVELS = new Set<ThinkingLevel>([
|
|
|
64
84
|
"xhigh",
|
|
65
85
|
]);
|
|
66
86
|
|
|
87
|
+
const MCP_SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
88
|
+
|
|
67
89
|
/**
|
|
68
90
|
* Load and validate user settings from disk.
|
|
69
91
|
*
|
|
@@ -79,13 +101,81 @@ export function loadSettings(path: string): UserSettings {
|
|
|
79
101
|
}
|
|
80
102
|
|
|
81
103
|
try {
|
|
82
|
-
|
|
83
|
-
|
|
104
|
+
return parseSettingsFile(path);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw createSettingsReadError(path, error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Load settings for startup without aborting on invalid JSON.
|
|
112
|
+
*
|
|
113
|
+
* Missing files and invalid JSON content are treated as empty settings so
|
|
114
|
+
* startup behaves like there are no saved settings. Other filesystem errors
|
|
115
|
+
* still fail with the same descriptive read error as {@link loadSettings}.
|
|
116
|
+
*
|
|
117
|
+
* @param path - Absolute path to `settings.json`.
|
|
118
|
+
* @returns The validated settings object, or `{}` when startup should ignore invalid JSON.
|
|
119
|
+
*/
|
|
120
|
+
export function loadStartupSettings(path: string): UserSettings {
|
|
121
|
+
if (!existsSync(path)) {
|
|
122
|
+
return {};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
return parseSettingsFile(path);
|
|
84
127
|
} catch (error) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
128
|
+
if (error instanceof SyntaxError) {
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
throw createSettingsReadError(path, error);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Merge two settings objects using startup overlay semantics.
|
|
137
|
+
*
|
|
138
|
+
* Scalar fields use override-wins. `customProviders` merges by provider name,
|
|
139
|
+
* and `mcp.servers` merges by server name. Same-name override entries replace
|
|
140
|
+
* base entries while keeping the base ordering stable; new override entries are
|
|
141
|
+
* appended in override order.
|
|
142
|
+
*
|
|
143
|
+
* @param base - Base settings, usually the global settings file.
|
|
144
|
+
* @param override - Higher-priority settings, usually a repo-local overlay.
|
|
145
|
+
* @returns The merged effective settings.
|
|
146
|
+
*/
|
|
147
|
+
export function mergeUserSettings(
|
|
148
|
+
base: UserSettings,
|
|
149
|
+
override: UserSettings,
|
|
150
|
+
): UserSettings {
|
|
151
|
+
const sanitizedBase = sanitizeSettings(base);
|
|
152
|
+
const sanitizedOverride = sanitizeSettings(override);
|
|
153
|
+
const merged: UserSettings = {
|
|
154
|
+
...sanitizedBase,
|
|
155
|
+
...sanitizedOverride,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const customProviders = mergeNamedEntries(
|
|
159
|
+
sanitizedBase.customProviders,
|
|
160
|
+
sanitizedOverride.customProviders,
|
|
161
|
+
);
|
|
162
|
+
if (customProviders) {
|
|
163
|
+
merged.customProviders = customProviders;
|
|
164
|
+
} else {
|
|
165
|
+
delete merged.customProviders;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const servers = mergeNamedEntries(
|
|
169
|
+
sanitizedBase.mcp?.servers,
|
|
170
|
+
sanitizedOverride.mcp?.servers,
|
|
171
|
+
);
|
|
172
|
+
if (servers) {
|
|
173
|
+
merged.mcp = { servers };
|
|
174
|
+
} else {
|
|
175
|
+
delete merged.mcp;
|
|
88
176
|
}
|
|
177
|
+
|
|
178
|
+
return merged;
|
|
89
179
|
}
|
|
90
180
|
|
|
91
181
|
/**
|
|
@@ -122,7 +212,7 @@ export function updateSettings(
|
|
|
122
212
|
update: Partial<UserSettings>,
|
|
123
213
|
): UserSettings {
|
|
124
214
|
const current = loadSettings(path);
|
|
125
|
-
const merged =
|
|
215
|
+
const merged = mergeUserSettings(current, sanitizeSettings(update));
|
|
126
216
|
return saveSettings(path, merged);
|
|
127
217
|
}
|
|
128
218
|
|
|
@@ -155,6 +245,46 @@ export function resolveStartupSettings(
|
|
|
155
245
|
};
|
|
156
246
|
}
|
|
157
247
|
|
|
248
|
+
function mergeNamedEntries<T extends { name: string }>(
|
|
249
|
+
base: readonly T[] | undefined,
|
|
250
|
+
override: readonly T[] | undefined,
|
|
251
|
+
): T[] | undefined {
|
|
252
|
+
if (!base?.length && !override?.length) {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const merged = [...(base ?? [])];
|
|
257
|
+
const indexes = new Map<string, number>();
|
|
258
|
+
|
|
259
|
+
for (const [index, entry] of merged.entries()) {
|
|
260
|
+
indexes.set(entry.name, index);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (const entry of override ?? []) {
|
|
264
|
+
const existingIndex = indexes.get(entry.name);
|
|
265
|
+
if (existingIndex === undefined) {
|
|
266
|
+
indexes.set(entry.name, merged.length);
|
|
267
|
+
merged.push(entry);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
merged[existingIndex] = entry;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return merged.length > 0 ? merged : undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function parseSettingsFile(path: string): UserSettings {
|
|
278
|
+
const raw = JSON.parse(readFileSync(path, "utf-8")) as unknown;
|
|
279
|
+
return sanitizeSettings(raw);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function createSettingsReadError(path: string, error: unknown): Error {
|
|
283
|
+
return new Error(
|
|
284
|
+
`Failed to read settings ${path}: ${getErrorMessage(error)}`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
158
288
|
/**
|
|
159
289
|
* Validate and normalize a parsed settings object.
|
|
160
290
|
*
|
|
@@ -164,24 +294,27 @@ export function resolveStartupSettings(
|
|
|
164
294
|
* @returns Sanitized settings.
|
|
165
295
|
*/
|
|
166
296
|
function sanitizeSettings(value: unknown): UserSettings {
|
|
167
|
-
|
|
297
|
+
const candidate = toRecord(value);
|
|
298
|
+
if (!candidate) {
|
|
168
299
|
return {};
|
|
169
300
|
}
|
|
170
301
|
|
|
171
|
-
const candidate = value as Record<string, unknown>;
|
|
172
302
|
const settings: UserSettings = {};
|
|
303
|
+
const defaultModel = readString(candidate, "defaultModel");
|
|
304
|
+
const showReasoning = readBoolean(candidate, "showReasoning");
|
|
305
|
+
const verbose = readBoolean(candidate, "verbose");
|
|
173
306
|
|
|
174
|
-
if (
|
|
175
|
-
settings.defaultModel =
|
|
307
|
+
if (defaultModel !== null) {
|
|
308
|
+
settings.defaultModel = defaultModel;
|
|
176
309
|
}
|
|
177
310
|
if (isThinkingLevel(candidate.defaultEffort)) {
|
|
178
311
|
settings.defaultEffort = candidate.defaultEffort;
|
|
179
312
|
}
|
|
180
|
-
if (
|
|
181
|
-
settings.showReasoning =
|
|
313
|
+
if (showReasoning !== null) {
|
|
314
|
+
settings.showReasoning = showReasoning;
|
|
182
315
|
}
|
|
183
|
-
if (
|
|
184
|
-
settings.verbose =
|
|
316
|
+
if (verbose !== null) {
|
|
317
|
+
settings.verbose = verbose;
|
|
185
318
|
}
|
|
186
319
|
|
|
187
320
|
const customProviders = sanitizeCustomProviders(candidate.customProviders);
|
|
@@ -189,27 +322,32 @@ function sanitizeSettings(value: unknown): UserSettings {
|
|
|
189
322
|
settings.customProviders = customProviders;
|
|
190
323
|
}
|
|
191
324
|
|
|
325
|
+
const mcp = sanitizeMcpSettings(candidate.mcp);
|
|
326
|
+
if (mcp) {
|
|
327
|
+
settings.mcp = mcp;
|
|
328
|
+
}
|
|
329
|
+
|
|
192
330
|
return settings;
|
|
193
331
|
}
|
|
194
332
|
|
|
195
333
|
/** Try to parse a single custom provider entry, returning null on failure. */
|
|
196
334
|
function parseCustomProvider(item: unknown): CustomProvider | null {
|
|
197
|
-
|
|
335
|
+
const candidate = toRecord(item);
|
|
336
|
+
if (!candidate) {
|
|
198
337
|
return null;
|
|
199
338
|
}
|
|
200
339
|
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
const baseUrl =
|
|
204
|
-
typeof candidate.baseUrl === "string" ? candidate.baseUrl.trim() : "";
|
|
340
|
+
const name = readString(candidate, "name")?.trim() ?? "";
|
|
341
|
+
const baseUrl = readString(candidate, "baseUrl")?.trim() ?? "";
|
|
205
342
|
|
|
206
343
|
if (!name || !baseUrl) {
|
|
207
344
|
return null;
|
|
208
345
|
}
|
|
209
346
|
|
|
210
347
|
const entry: CustomProvider = { name, baseUrl };
|
|
211
|
-
|
|
212
|
-
|
|
348
|
+
const apiKey = readString(candidate, "apiKey");
|
|
349
|
+
if (apiKey !== null) {
|
|
350
|
+
entry.apiKey = apiKey;
|
|
213
351
|
}
|
|
214
352
|
return entry;
|
|
215
353
|
}
|
|
@@ -240,6 +378,64 @@ function sanitizeCustomProviders(value: unknown): CustomProvider[] | undefined {
|
|
|
240
378
|
return result.length > 0 ? result : undefined;
|
|
241
379
|
}
|
|
242
380
|
|
|
381
|
+
function sanitizeMcpSettings(value: unknown): McpSettings | undefined {
|
|
382
|
+
const candidate = toRecord(value);
|
|
383
|
+
if (!candidate) {
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const servers = sanitizeMcpServers(candidate.servers);
|
|
388
|
+
if (!servers) {
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return { servers };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Try to parse a single MCP server entry, returning null on failure. */
|
|
396
|
+
function parseMcpServer(item: unknown): McpServerConfig | null {
|
|
397
|
+
const candidate = toRecord(item);
|
|
398
|
+
if (!candidate) {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const name = readString(candidate, "name")?.trim() ?? "";
|
|
403
|
+
const url = readString(candidate, "url")?.trim() ?? "";
|
|
404
|
+
const enabled = readBoolean(candidate, "enabled") ?? true;
|
|
405
|
+
|
|
406
|
+
if (!name || !url || !MCP_SERVER_NAME_PATTERN.test(name)) {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return { name, url, enabled };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Validate and normalize configured MCP servers.
|
|
415
|
+
*
|
|
416
|
+
* Drops entries with missing/invalid names or URLs, and deduplicates by name
|
|
417
|
+
* (first entry wins).
|
|
418
|
+
*/
|
|
419
|
+
function sanitizeMcpServers(value: unknown): McpServerConfig[] | undefined {
|
|
420
|
+
if (!Array.isArray(value)) {
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const result: McpServerConfig[] = [];
|
|
425
|
+
const seen = new Set<string>();
|
|
426
|
+
|
|
427
|
+
for (const item of value) {
|
|
428
|
+
const entry = parseMcpServer(item);
|
|
429
|
+
if (!entry || seen.has(entry.name)) {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
seen.add(entry.name);
|
|
433
|
+
result.push(entry);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return result.length > 0 ? result : undefined;
|
|
437
|
+
}
|
|
438
|
+
|
|
243
439
|
/**
|
|
244
440
|
* Check whether a value is a valid thinking level.
|
|
245
441
|
*
|
package/src/shared.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared runtime record/primitive readers used across the app.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Convert an unknown value into a plain record, rejecting arrays and null. */
|
|
8
|
+
export function toRecord(value: unknown): Record<string, unknown> | null {
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
10
|
+
? (value as Record<string, unknown>)
|
|
11
|
+
: null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Read a string field from a record, returning null for missing or invalid values. */
|
|
15
|
+
export function readString(
|
|
16
|
+
record: Record<string, unknown>,
|
|
17
|
+
key: string,
|
|
18
|
+
): string | null {
|
|
19
|
+
const value = record[key];
|
|
20
|
+
return typeof value === "string" ? value : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Read a boolean field from a record, returning null for missing or invalid values. */
|
|
24
|
+
export function readBoolean(
|
|
25
|
+
record: Record<string, unknown>,
|
|
26
|
+
key: string,
|
|
27
|
+
): boolean | null {
|
|
28
|
+
const value = record[key];
|
|
29
|
+
return typeof value === "boolean" ? value : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Read a finite numeric field from a record, returning null for missing or invalid values. */
|
|
33
|
+
export function readFiniteNumber(
|
|
34
|
+
record: Record<string, unknown>,
|
|
35
|
+
key: string,
|
|
36
|
+
): number | null {
|
|
37
|
+
const value = record[key];
|
|
38
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
39
|
+
}
|
package/src/skills.ts
CHANGED
|
@@ -226,6 +226,15 @@ export function discoverSkills(scanPaths: string[]): Skill[] {
|
|
|
226
226
|
// Catalog generation
|
|
227
227
|
// ---------------------------------------------------------------------------
|
|
228
228
|
|
|
229
|
+
function escapeXml(value: string): string {
|
|
230
|
+
return value
|
|
231
|
+
.replaceAll("&", "&")
|
|
232
|
+
.replaceAll("<", "<")
|
|
233
|
+
.replaceAll(">", ">")
|
|
234
|
+
.replaceAll('"', """)
|
|
235
|
+
.replaceAll("'", "'");
|
|
236
|
+
}
|
|
237
|
+
|
|
229
238
|
/**
|
|
230
239
|
* Build the XML skill catalog for the system prompt.
|
|
231
240
|
*
|
|
@@ -243,9 +252,9 @@ export function buildSkillCatalog(skills: Skill[]): string {
|
|
|
243
252
|
.map(
|
|
244
253
|
(s) =>
|
|
245
254
|
` <skill>\n` +
|
|
246
|
-
` <name>${s.name}</name>\n` +
|
|
247
|
-
` <description>${s.description}</description>\n` +
|
|
248
|
-
` <location>${s.path}</location>\n` +
|
|
255
|
+
` <name>${escapeXml(s.name)}</name>\n` +
|
|
256
|
+
` <description>${escapeXml(s.description)}</description>\n` +
|
|
257
|
+
` <location>${escapeXml(s.path)}</location>\n` +
|
|
249
258
|
` </skill>`,
|
|
250
259
|
)
|
|
251
260
|
.join("\n");
|
package/src/submit.ts
CHANGED
|
@@ -18,8 +18,7 @@ import {
|
|
|
18
18
|
} from "./index.ts";
|
|
19
19
|
import { parseInput } from "./input.ts";
|
|
20
20
|
import {
|
|
21
|
-
|
|
22
|
-
addMessageToStats,
|
|
21
|
+
appendConversationMessage,
|
|
23
22
|
appendMessage,
|
|
24
23
|
appendPromptHistory,
|
|
25
24
|
filterModelMessages,
|
|
@@ -216,6 +215,20 @@ function recordRawPromptHistory(
|
|
|
216
215
|
truncatePromptHistory(state.db, MAX_PROMPT_HISTORY);
|
|
217
216
|
}
|
|
218
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Drop any resolved steering messages still queued on the active app state.
|
|
220
|
+
*
|
|
221
|
+
* Raw prompt-history rows are intentionally left untouched because queue resets
|
|
222
|
+
* must not rewrite global input-history behavior.
|
|
223
|
+
*
|
|
224
|
+
* @param state - Mutable application state containing the queued steering list.
|
|
225
|
+
*/
|
|
226
|
+
export function clearQueuedUserMessages(
|
|
227
|
+
state: Pick<AppState, "queuedUserMessages">,
|
|
228
|
+
): void {
|
|
229
|
+
state.queuedUserMessages.length = 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
219
232
|
/**
|
|
220
233
|
* Queue resolved user content for the next model-request boundary of an active run.
|
|
221
234
|
*
|
|
@@ -254,26 +267,9 @@ export function queueResolvedInput(
|
|
|
254
267
|
function handleAgentEvent(event: AgentEvent, state: AppState): void {
|
|
255
268
|
switch (event.type) {
|
|
256
269
|
case "user_message":
|
|
257
|
-
state.messages.push(event.message);
|
|
258
|
-
state.contextTokens = addMessageToContextTokens(
|
|
259
|
-
state.contextTokens,
|
|
260
|
-
event.message,
|
|
261
|
-
);
|
|
262
|
-
break;
|
|
263
270
|
case "assistant_message":
|
|
264
|
-
state.messages.push(event.message);
|
|
265
|
-
state.stats = addMessageToStats(state.stats, event.message);
|
|
266
|
-
state.contextTokens = addMessageToContextTokens(
|
|
267
|
-
state.contextTokens,
|
|
268
|
-
event.message,
|
|
269
|
-
);
|
|
270
|
-
break;
|
|
271
271
|
case "tool_result":
|
|
272
|
-
state
|
|
273
|
-
state.contextTokens = addMessageToContextTokens(
|
|
274
|
-
state.contextTokens,
|
|
275
|
-
event.message,
|
|
276
|
-
);
|
|
272
|
+
appendConversationMessage(state, event.message);
|
|
277
273
|
break;
|
|
278
274
|
case "text_delta":
|
|
279
275
|
case "thinking_delta":
|
|
@@ -319,6 +315,8 @@ export async function submitResolvedInput(
|
|
|
319
315
|
throw new Error("Cannot submit empty input.");
|
|
320
316
|
}
|
|
321
317
|
|
|
318
|
+
clearQueuedUserMessages(state);
|
|
319
|
+
|
|
322
320
|
const session = ensureSession(state);
|
|
323
321
|
recordRawPromptHistory(rawInput, state, session.id);
|
|
324
322
|
|
|
@@ -329,11 +327,7 @@ export async function submitResolvedInput(
|
|
|
329
327
|
} satisfies UserMessage;
|
|
330
328
|
|
|
331
329
|
const turn = appendMessage(state.db, session.id, userMessage);
|
|
332
|
-
state
|
|
333
|
-
state.contextTokens = addMessageToContextTokens(
|
|
334
|
-
state.contextTokens,
|
|
335
|
-
userMessage,
|
|
336
|
-
);
|
|
330
|
+
appendConversationMessage(state, userMessage);
|
|
337
331
|
hooks?.onUserMessage?.(state);
|
|
338
332
|
|
|
339
333
|
const systemPrompt = buildPrompt(state);
|
|
@@ -369,6 +363,7 @@ export async function submitResolvedInput(
|
|
|
369
363
|
stopReason = result.stopReason;
|
|
370
364
|
return result.stopReason;
|
|
371
365
|
} finally {
|
|
366
|
+
clearQueuedUserMessages(state);
|
|
372
367
|
state.running = false;
|
|
373
368
|
state.abortController = null;
|
|
374
369
|
hooks?.onTurnEnd?.(state, stopReason);
|
package/src/text.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared text-shaping helpers.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Collapse whitespace runs to single spaces and trim the ends.
|
|
9
|
+
*
|
|
10
|
+
* @param text - Raw text to normalize.
|
|
11
|
+
* @returns The collapsed single-line text.
|
|
12
|
+
*/
|
|
13
|
+
export function collapseWhitespace(text: string): string {
|
|
14
|
+
return text.replace(/\s+/g, " ").trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Collapse whitespace into a single line, returning `null` when nothing remains.
|
|
19
|
+
*
|
|
20
|
+
* @param text - Raw text to normalize.
|
|
21
|
+
* @returns Collapsed text, or `null` when the result is empty.
|
|
22
|
+
*/
|
|
23
|
+
export function collapseWhitespaceToNull(text: string): string | null {
|
|
24
|
+
const collapsed = collapseWhitespace(text);
|
|
25
|
+
return collapsed.length > 0 ? collapsed : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Join only `text` blocks from multipart content into one space-separated string.
|
|
30
|
+
*
|
|
31
|
+
* @param content - Multipart content blocks.
|
|
32
|
+
* @returns Concatenated text-block content.
|
|
33
|
+
*/
|
|
34
|
+
export function joinTextBlocks<T extends { type: string }>(
|
|
35
|
+
content: readonly T[],
|
|
36
|
+
): string {
|
|
37
|
+
return content
|
|
38
|
+
.flatMap((block) => {
|
|
39
|
+
return block.type === "text" &&
|
|
40
|
+
"text" in block &&
|
|
41
|
+
typeof block.text === "string"
|
|
42
|
+
? [block.text]
|
|
43
|
+
: [];
|
|
44
|
+
})
|
|
45
|
+
.join(" ");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Truncate text with an ellipsis from the start or end.
|
|
50
|
+
*
|
|
51
|
+
* @param text - Text to truncate.
|
|
52
|
+
* @param maxChars - Maximum visible characters including the ellipsis.
|
|
53
|
+
* @param side - Which side to truncate from.
|
|
54
|
+
* @returns Truncated text when needed.
|
|
55
|
+
*/
|
|
56
|
+
export function truncateText(
|
|
57
|
+
text: string,
|
|
58
|
+
maxChars: number,
|
|
59
|
+
side: "start" | "end" = "end",
|
|
60
|
+
): string {
|
|
61
|
+
if (text.length <= maxChars) {
|
|
62
|
+
return text;
|
|
63
|
+
}
|
|
64
|
+
if (maxChars <= 1) {
|
|
65
|
+
return "…";
|
|
66
|
+
}
|
|
67
|
+
if (side === "start") {
|
|
68
|
+
return `…${text.slice(text.length - (maxChars - 1))}`;
|
|
69
|
+
}
|
|
70
|
+
return `${text.slice(0, maxChars - 1)}…`;
|
|
71
|
+
}
|