@bubblebrain-ai/bubble 0.0.19 → 0.0.20
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/dist/agent/internal-reminder-sanitizer.d.ts +1 -0
- package/dist/agent/internal-reminder-sanitizer.js +46 -0
- package/dist/agent.d.ts +9 -0
- package/dist/agent.js +305 -17
- package/dist/approval/controller.d.ts +6 -0
- package/dist/approval/controller.js +104 -11
- package/dist/debug-trace.js +4 -0
- package/dist/feishu/agent-host/run-driver.js +28 -0
- package/dist/hooks/config.d.ts +9 -0
- package/dist/hooks/config.js +278 -0
- package/dist/hooks/controller.d.ts +24 -0
- package/dist/hooks/controller.js +254 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/log.d.ts +14 -0
- package/dist/hooks/log.js +54 -0
- package/dist/hooks/runner.d.ts +5 -0
- package/dist/hooks/runner.js +225 -0
- package/dist/hooks/trust.d.ts +37 -0
- package/dist/hooks/trust.js +143 -0
- package/dist/hooks/types.d.ts +173 -0
- package/dist/hooks/types.js +46 -0
- package/dist/main.js +32 -0
- package/dist/memory/prompts.js +3 -1
- package/dist/model-catalog.js +2 -0
- package/dist/model-pricing.js +8 -0
- package/dist/network/chatgpt-transport.d.ts +0 -1
- package/dist/network/chatgpt-transport.js +40 -121
- package/dist/network/provider-transport.d.ts +32 -0
- package/dist/network/provider-transport.js +265 -0
- package/dist/network/retry.d.ts +29 -0
- package/dist/network/retry.js +88 -0
- package/dist/network/system-proxy.d.ts +18 -0
- package/dist/network/system-proxy.js +175 -0
- package/dist/provider-anthropic.d.ts +1 -0
- package/dist/provider-anthropic.js +127 -52
- package/dist/provider-openai-codex.js +19 -29
- package/dist/session-log.js +3 -3
- package/dist/slash-commands/commands.js +84 -0
- package/dist/slash-commands/types.d.ts +2 -0
- package/dist/tools/edit-apply.js +63 -3
- package/dist/tools/edit.js +4 -4
- package/dist/tui/display-history.d.ts +4 -3
- package/dist/tui/display-history.js +34 -57
- package/dist/tui/display-sanitizer.d.ts +3 -0
- package/dist/tui/display-sanitizer.js +38 -0
- package/dist/tui/paste-placeholder.d.ts +1 -0
- package/dist/tui/paste-placeholder.js +7 -0
- package/dist/tui/run.d.ts +2 -0
- package/dist/tui/run.js +260 -155
- package/dist/tui/trace-groups.js +40 -4
- package/dist/tui/wordmark.d.ts +1 -0
- package/dist/tui/wordmark.js +56 -54
- package/dist/tui-ink/app.js +2 -1
- package/dist/tui-ink/trace-groups.js +40 -4
- package/dist/tui-opentui/app.js +2 -1
- package/dist/tui-opentui/trace-groups.js +40 -4
- package/dist/types.d.ts +27 -0
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ import type { McpManager } from "../mcp/manager.js";
|
|
|
9
9
|
import type { LspService } from "../lsp/index.js";
|
|
10
10
|
import type { MemoryScope } from "../memory/index.js";
|
|
11
11
|
import type { ThemeMode } from "../config.js";
|
|
12
|
+
import type { ExternalHookController } from "../hooks/controller.js";
|
|
12
13
|
export type SidebarMode = "auto" | "expanded" | "collapsed";
|
|
13
14
|
export interface SidebarCommandState {
|
|
14
15
|
mode: SidebarMode;
|
|
@@ -28,6 +29,7 @@ export interface SlashCommandContext {
|
|
|
28
29
|
skillRegistry: SkillRegistry;
|
|
29
30
|
bashAllowlist?: BashAllowlist;
|
|
30
31
|
settingsManager?: SettingsManager;
|
|
32
|
+
hookController?: ExternalHookController;
|
|
31
33
|
mcpManager?: McpManager;
|
|
32
34
|
lspService?: LspService;
|
|
33
35
|
flushMemory?: () => Promise<void>;
|
package/dist/tools/edit-apply.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSensitivePath } from "./sensitive-paths.js";
|
|
1
2
|
export class EditApplyError extends Error {
|
|
2
3
|
status;
|
|
3
4
|
constructor(message, status = "no_match") {
|
|
@@ -6,6 +7,9 @@ export class EditApplyError extends Error {
|
|
|
6
7
|
this.name = "EditApplyError";
|
|
7
8
|
}
|
|
8
9
|
}
|
|
10
|
+
const CANDIDATE_EXCERPT_CONTEXT_LINES = 3;
|
|
11
|
+
const CANDIDATE_EXCERPT_MAX_LINES = 8;
|
|
12
|
+
const CANDIDATE_EXCERPT_MAX_CHARS = 1200;
|
|
9
13
|
function detectLineEnding(content) {
|
|
10
14
|
const crlf = content.indexOf("\r\n");
|
|
11
15
|
const lf = content.indexOf("\n");
|
|
@@ -234,19 +238,75 @@ function findBestLineHint(content, oldText) {
|
|
|
234
238
|
return undefined;
|
|
235
239
|
const contentLines = nonBlankLines(splitLines(content));
|
|
236
240
|
let best;
|
|
241
|
+
let tieCount = 0;
|
|
237
242
|
for (let i = 0; i < contentLines.length; i++) {
|
|
238
243
|
let score = 0;
|
|
239
244
|
for (let j = 0; j < oldLines.length && i + j < contentLines.length; j++) {
|
|
240
245
|
if (contentLines[i + j].normalized === oldLines[j])
|
|
241
246
|
score++;
|
|
242
247
|
}
|
|
243
|
-
if (!best || score > best.score)
|
|
248
|
+
if (!best || score > best.score) {
|
|
244
249
|
best = { index: i, score };
|
|
250
|
+
tieCount = 1;
|
|
251
|
+
}
|
|
252
|
+
else if (score === best.score) {
|
|
253
|
+
tieCount++;
|
|
254
|
+
}
|
|
245
255
|
}
|
|
246
256
|
if (!best || best.score === 0)
|
|
247
257
|
return undefined;
|
|
248
258
|
const startLine = contentLines[best.index].lineIndex + 1;
|
|
249
|
-
return
|
|
259
|
+
return {
|
|
260
|
+
startLine,
|
|
261
|
+
score: best.score,
|
|
262
|
+
total: oldLines.length,
|
|
263
|
+
lineIndex: contentLines[best.index].lineIndex,
|
|
264
|
+
tieCount,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function isHighConfidenceLineHint(hint) {
|
|
268
|
+
return hint.score >= 2 && hint.score / hint.total >= 0.5 && hint.tieCount === 1;
|
|
269
|
+
}
|
|
270
|
+
function formatLineHint(hint) {
|
|
271
|
+
if (hint.tieCount > 1) {
|
|
272
|
+
return `Closest ambiguous line-based candidate starts near line ${hint.startLine} and matched ${hint.score}/${hint.total} non-blank lines, but ${hint.tieCount} candidates tied. Current bytes were not included because the candidate may be unrelated.`;
|
|
273
|
+
}
|
|
274
|
+
if (!isHighConfidenceLineHint(hint)) {
|
|
275
|
+
return `Closest low-confidence line-based candidate starts near line ${hint.startLine} and matched ${hint.score}/${hint.total} non-blank lines. Current bytes were not included because the candidate may be unrelated.`;
|
|
276
|
+
}
|
|
277
|
+
return `Closest line-based candidate starts near line ${hint.startLine} and matched ${hint.score}/${hint.total} non-blank lines.`;
|
|
278
|
+
}
|
|
279
|
+
function formatFence(content) {
|
|
280
|
+
let fence = "```";
|
|
281
|
+
while (content.includes(fence))
|
|
282
|
+
fence += "`";
|
|
283
|
+
return `${fence}\n${content}\n${fence}`;
|
|
284
|
+
}
|
|
285
|
+
function truncateExcerpt(excerpt) {
|
|
286
|
+
if (excerpt.length <= CANDIDATE_EXCERPT_MAX_CHARS)
|
|
287
|
+
return excerpt;
|
|
288
|
+
const marker = "\n...[truncated current candidate excerpt]";
|
|
289
|
+
return excerpt.slice(0, Math.max(0, CANDIDATE_EXCERPT_MAX_CHARS - marker.length)) + marker;
|
|
290
|
+
}
|
|
291
|
+
function formatCandidateExcerpt(content, hint) {
|
|
292
|
+
const lines = splitLines(content);
|
|
293
|
+
const startLineIndex = Math.max(0, hint.lineIndex - CANDIDATE_EXCERPT_CONTEXT_LINES);
|
|
294
|
+
const requestedEnd = Math.min(lines.length, hint.lineIndex + CANDIDATE_EXCERPT_CONTEXT_LINES + 1);
|
|
295
|
+
const endLineIndex = Math.min(requestedEnd, startLineIndex + CANDIDATE_EXCERPT_MAX_LINES);
|
|
296
|
+
const excerpt = truncateExcerpt(lines.slice(startLineIndex, endLineIndex).map((line) => line.text).join("\n"));
|
|
297
|
+
return [
|
|
298
|
+
`Current candidate excerpt (high confidence, current file lines ${startLineIndex + 1}-${endLineIndex}, not guaranteed target):`,
|
|
299
|
+
formatFence(excerpt),
|
|
300
|
+
].join("\n");
|
|
301
|
+
}
|
|
302
|
+
function formatBestLineHint(content, hint, options) {
|
|
303
|
+
const lineHint = formatLineHint(hint);
|
|
304
|
+
if (!isHighConfidenceLineHint(hint))
|
|
305
|
+
return lineHint;
|
|
306
|
+
if (options?.path && isSensitivePath(options.path)) {
|
|
307
|
+
return `${lineHint}\nCurrent bytes were not included because this path is blocked by the sensitive-path read policy.`;
|
|
308
|
+
}
|
|
309
|
+
return `${lineHint}\n\n${formatCandidateExcerpt(content, hint)}`;
|
|
250
310
|
}
|
|
251
311
|
function matchEdit(content, edit, index, total, options) {
|
|
252
312
|
if (edit.oldText.length === 0) {
|
|
@@ -350,7 +410,7 @@ function matchEdit(content, edit, index, total, options) {
|
|
|
350
410
|
}
|
|
351
411
|
}
|
|
352
412
|
const hint = findBestLineHint(content, oldText);
|
|
353
|
-
const hintSuffix = hint ? `\n${hint}` : "";
|
|
413
|
+
const hintSuffix = hint ? `\n${formatBestLineHint(content, hint, options)}` : "";
|
|
354
414
|
const recovery = [
|
|
355
415
|
"",
|
|
356
416
|
"How to recover:",
|
package/dist/tools/edit.js
CHANGED
|
@@ -65,12 +65,12 @@ export function createEditTool(cwd, approval, lsp, fileState) {
|
|
|
65
65
|
name: "edit",
|
|
66
66
|
effect: "write_direct",
|
|
67
67
|
requiresApproval: true,
|
|
68
|
-
description: "Edit a single file using targeted text replacements. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes
|
|
68
|
+
description: "Edit a single file using targeted text replacements. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes overlap or one replacement is nested inside another, merge them into one edit. Do not include large unchanged regions just to connect distant changes.",
|
|
69
69
|
promptSnippet: "Make precise file edits with exact text replacement, including multiple disjoint edits in one call",
|
|
70
70
|
promptGuidelines: [
|
|
71
|
-
"Use edit for precise changes; edits[].oldText
|
|
72
|
-
"When changing multiple
|
|
73
|
-
"Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits
|
|
71
|
+
"Use edit for precise changes; each edits[].oldText must be copied verbatim from a fresh read of the current exact target block and must identify a unique target. Do not reconstruct oldText from memory, stale reads, or similar code elsewhere.",
|
|
72
|
+
"When changing multiple small, clearly disjoint locations copied from the same fresh read, you may use one edit call with multiple entries in edits[]. Use separate smaller edit calls after re-reading when anchors are uncertain, stale, or likely to drift.",
|
|
73
|
+
"Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits; merge only truly overlapping targets.",
|
|
74
74
|
"Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.",
|
|
75
75
|
],
|
|
76
76
|
parameters: {
|
|
@@ -2,7 +2,6 @@ import type { ToolResultMetadata, TokenUsage } from "../types.js";
|
|
|
2
2
|
export interface CompactionMeta {
|
|
3
3
|
turns: number;
|
|
4
4
|
messages: number;
|
|
5
|
-
tokensSaved: number;
|
|
6
5
|
summarySections: Array<{
|
|
7
6
|
label: string;
|
|
8
7
|
content: string;
|
|
@@ -10,11 +9,12 @@ export interface CompactionMeta {
|
|
|
10
9
|
contextWindow?: number;
|
|
11
10
|
compactedAt: number;
|
|
12
11
|
}
|
|
12
|
+
export type UserInputStatus = "queued" | "pending_steer";
|
|
13
13
|
export interface DisplayMessage {
|
|
14
14
|
role: "user" | "assistant" | "error";
|
|
15
15
|
content: string;
|
|
16
16
|
clientId?: string;
|
|
17
|
-
|
|
17
|
+
inputStatus?: UserInputStatus;
|
|
18
18
|
reasoning?: string;
|
|
19
19
|
toolCalls?: DisplayToolCall[];
|
|
20
20
|
parts?: DisplayMessagePart[];
|
|
@@ -53,11 +53,12 @@ export interface DisplayToolCall {
|
|
|
53
53
|
startedAt?: number;
|
|
54
54
|
completedAt?: number;
|
|
55
55
|
}
|
|
56
|
+
export declare function userInputStatusBadgeLabel(status?: UserInputStatus): string | undefined;
|
|
57
|
+
export declare function setUserInputStatus(message: DisplayMessage, inputStatus?: UserInputStatus): DisplayMessage;
|
|
56
58
|
export declare function appendTextPart(parts: DisplayMessagePart[], content: string): void;
|
|
57
59
|
export declare function appendToolPart(parts: DisplayMessagePart[], toolCall: DisplayToolCall): void;
|
|
58
60
|
export declare function snapshotDisplayParts(parts: DisplayMessagePart[]): DisplayMessagePart[];
|
|
59
61
|
export declare function contentFromParts(parts: DisplayMessagePart[]): string;
|
|
60
62
|
export declare function toolCallsFromParts(parts: DisplayMessagePart[]): DisplayToolCall[];
|
|
61
63
|
export declare function compactDisplayMessages(messages: DisplayMessage[]): DisplayMessage[];
|
|
62
|
-
export declare function truncateText(value: string, maxChars: number): string;
|
|
63
64
|
export declare function formatCompactNumber(n: number): string;
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
export function userInputStatusBadgeLabel(status) {
|
|
2
|
+
switch (status) {
|
|
3
|
+
case "queued":
|
|
4
|
+
return "QUEUED";
|
|
5
|
+
case "pending_steer":
|
|
6
|
+
return "STEER";
|
|
7
|
+
default:
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function setUserInputStatus(message, inputStatus) {
|
|
12
|
+
if (inputStatus)
|
|
13
|
+
return { ...message, inputStatus };
|
|
14
|
+
const { inputStatus: _inputStatus, ...rest } = message;
|
|
15
|
+
return rest;
|
|
16
|
+
}
|
|
1
17
|
export function appendTextPart(parts, content) {
|
|
2
18
|
if (!content)
|
|
3
19
|
return;
|
|
@@ -40,18 +56,21 @@ export function toolCallsFromParts(parts) {
|
|
|
40
56
|
}
|
|
41
57
|
const MAX_VISIBLE_MESSAGES = 80;
|
|
42
58
|
const FULL_DETAIL_WINDOW = 24;
|
|
43
|
-
const MAX_OLD_CONTENT_CHARS = 1200;
|
|
44
|
-
const MAX_OLD_REASONING_CHARS = 600;
|
|
45
59
|
const COMPACTION_SUMMARY_ITEMS = 6;
|
|
46
60
|
const COMPACTION_FILE_LIMIT = 8;
|
|
47
61
|
const TOOL_PATH_KEYS = ["file", "path", "paths", "filePath"];
|
|
62
|
+
// Display-history folding policy: message text is NEVER rewritten or truncated.
|
|
63
|
+
// Visible messages keep their content verbatim (older ones only collapse bulky
|
|
64
|
+
// tool-result bodies, which the UI can re-expand). When history exceeds
|
|
65
|
+
// MAX_VISIBLE_MESSAGES, the entire older span is folded behind a single summary
|
|
66
|
+
// card — mirroring how mainstream coding agents present compacted history —
|
|
67
|
+
// instead of clipping individual messages mid-sentence.
|
|
48
68
|
export function compactDisplayMessages(messages) {
|
|
49
69
|
if (messages.length === 0) {
|
|
50
70
|
return messages;
|
|
51
71
|
}
|
|
52
72
|
let hiddenCount = 0;
|
|
53
73
|
let accumulatedTurns = 0;
|
|
54
|
-
let accumulatedTokens = 0;
|
|
55
74
|
const summarySections = [];
|
|
56
75
|
const withoutSynthetic = messages.filter((message) => {
|
|
57
76
|
if (message.syntheticKind !== "ui_compact_card") {
|
|
@@ -60,7 +79,6 @@ export function compactDisplayMessages(messages) {
|
|
|
60
79
|
hiddenCount += message.hiddenCount ?? 0;
|
|
61
80
|
if (message.compactionMeta) {
|
|
62
81
|
accumulatedTurns += message.compactionMeta.turns;
|
|
63
|
-
accumulatedTokens += message.compactionMeta.tokensSaved;
|
|
64
82
|
for (const section of message.compactionMeta.summarySections) {
|
|
65
83
|
summarySections.push(section);
|
|
66
84
|
}
|
|
@@ -69,36 +87,31 @@ export function compactDisplayMessages(messages) {
|
|
|
69
87
|
});
|
|
70
88
|
const overflow = Math.max(0, withoutSynthetic.length - MAX_VISIBLE_MESSAGES);
|
|
71
89
|
hiddenCount += overflow;
|
|
90
|
+
const hiddenMessages = overflow > 0 ? withoutSynthetic.slice(0, overflow) : [];
|
|
72
91
|
const visible = overflow > 0 ? withoutSynthetic.slice(overflow) : withoutSynthetic;
|
|
73
92
|
const detailStart = Math.max(0, visible.length - FULL_DETAIL_WINDOW);
|
|
74
93
|
const compacted = visible.map((message, index) => {
|
|
75
94
|
if (message.syntheticKind === "ui_compact_card") {
|
|
76
95
|
return message;
|
|
77
96
|
}
|
|
78
|
-
return index < detailStart ?
|
|
97
|
+
return index < detailStart ? collapseToolResults(message) : message;
|
|
79
98
|
});
|
|
80
99
|
if (hiddenCount === 0) {
|
|
81
100
|
return compacted;
|
|
82
101
|
}
|
|
83
|
-
const
|
|
84
|
-
const extractedMeta = extractCompactionMeta(truncatedMessages, hiddenCount, accumulatedTurns, accumulatedTokens, summarySections);
|
|
102
|
+
const extractedMeta = extractCompactionMeta(hiddenMessages, hiddenCount, accumulatedTurns, summarySections);
|
|
85
103
|
return [buildCompactCard(extractedMeta), ...compacted];
|
|
86
104
|
}
|
|
87
|
-
function extractCompactionMeta(
|
|
88
|
-
const turnsInBatch = countUserTurns(
|
|
105
|
+
function extractCompactionMeta(hiddenMessages, hiddenCount, previousTurns, previousSections) {
|
|
106
|
+
const turnsInBatch = countUserTurns(hiddenMessages);
|
|
89
107
|
const totalTurns = previousTurns + turnsInBatch;
|
|
90
|
-
const messagesInBatch = truncatedMessages.length;
|
|
91
|
-
const totalMessages = hiddenCount;
|
|
92
|
-
const estimatedTokens = estimateTokenSavings(truncatedMessages);
|
|
93
|
-
const totalTokens = previousTokens + estimatedTokens;
|
|
94
108
|
const sections = [
|
|
95
109
|
...previousSections,
|
|
96
|
-
...extractSummarySections(
|
|
110
|
+
...extractSummarySections(hiddenMessages),
|
|
97
111
|
];
|
|
98
112
|
return {
|
|
99
113
|
turns: totalTurns,
|
|
100
|
-
messages:
|
|
101
|
-
tokensSaved: totalTokens > 0 ? totalTokens : estimatedTokens,
|
|
114
|
+
messages: hiddenCount,
|
|
102
115
|
summarySections: mergeSummarySections(sections, COMPACTION_SUMMARY_ITEMS),
|
|
103
116
|
compactedAt: Date.now(),
|
|
104
117
|
};
|
|
@@ -106,18 +119,6 @@ function extractCompactionMeta(truncatedMessages, hiddenCount, previousTurns, pr
|
|
|
106
119
|
function countUserTurns(messages) {
|
|
107
120
|
return messages.filter((message) => message.role === "user").length;
|
|
108
121
|
}
|
|
109
|
-
function estimateTokenSavings(messages) {
|
|
110
|
-
let chars = 0;
|
|
111
|
-
for (const message of messages) {
|
|
112
|
-
chars += message.content.length;
|
|
113
|
-
chars += (message.reasoning?.length ?? 0);
|
|
114
|
-
for (const tool of message.toolCalls ?? []) {
|
|
115
|
-
chars += (tool.result?.length ?? 0);
|
|
116
|
-
chars += JSON.stringify(tool.args).length;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return Math.ceil(chars / 4);
|
|
120
|
-
}
|
|
121
122
|
function extractSummarySections(messages) {
|
|
122
123
|
const sections = [];
|
|
123
124
|
const userMessages = messages
|
|
@@ -206,13 +207,6 @@ function mergeSummarySections(sections, maxItems) {
|
|
|
206
207
|
.slice(0, maxItems);
|
|
207
208
|
}
|
|
208
209
|
function buildCompactCard(meta) {
|
|
209
|
-
const formatNum = (n) => {
|
|
210
|
-
if (n >= 1_000_000)
|
|
211
|
-
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
212
|
-
if (n >= 1_000)
|
|
213
|
-
return `${(n / 1_000).toFixed(1)}K`;
|
|
214
|
-
return String(n);
|
|
215
|
-
};
|
|
216
210
|
const parts = [];
|
|
217
211
|
if (meta.turns > 0) {
|
|
218
212
|
parts.push(`${meta.turns} turn${meta.turns === 1 ? "" : "s"}`);
|
|
@@ -220,9 +214,6 @@ function buildCompactCard(meta) {
|
|
|
220
214
|
if (meta.messages > 0) {
|
|
221
215
|
parts.push(`${meta.messages} message${meta.messages === 1 ? "" : "s"}`);
|
|
222
216
|
}
|
|
223
|
-
if (meta.tokensSaved > 0) {
|
|
224
|
-
parts.push(`~${formatNum(meta.tokensSaved)} tokens`);
|
|
225
|
-
}
|
|
226
217
|
const statsLine = parts.length > 0 ? `┃ ${parts.join(" · ")}` : "";
|
|
227
218
|
const sectionLines = [];
|
|
228
219
|
for (const section of meta.summarySections) {
|
|
@@ -238,16 +229,15 @@ function buildCompactCard(meta) {
|
|
|
238
229
|
status: "responding",
|
|
239
230
|
};
|
|
240
231
|
}
|
|
241
|
-
|
|
232
|
+
// Collapses bulky tool-result bodies on older messages while leaving the
|
|
233
|
+
// message text (content, reasoning) verbatim — never truncate what the user
|
|
234
|
+
// or the assistant actually said.
|
|
235
|
+
function collapseToolResults(message) {
|
|
242
236
|
if (message.syntheticKind === "ui_compact_card") {
|
|
243
237
|
return message;
|
|
244
238
|
}
|
|
245
239
|
return {
|
|
246
240
|
...message,
|
|
247
|
-
content: truncateText(message.content, MAX_OLD_CONTENT_CHARS),
|
|
248
|
-
reasoning: message.reasoning
|
|
249
|
-
? truncateText(message.reasoning, MAX_OLD_REASONING_CHARS)
|
|
250
|
-
: message.reasoning,
|
|
251
241
|
toolCalls: message.toolCalls?.map(compactToolCall),
|
|
252
242
|
parts: message.parts?.map(compactDisplayPart),
|
|
253
243
|
};
|
|
@@ -260,10 +250,7 @@ function cloneToolCall(toolCall) {
|
|
|
260
250
|
}
|
|
261
251
|
function compactDisplayPart(part) {
|
|
262
252
|
if (part.type === "text") {
|
|
263
|
-
return
|
|
264
|
-
...part,
|
|
265
|
-
content: truncateText(part.content, MAX_OLD_CONTENT_CHARS),
|
|
266
|
-
};
|
|
253
|
+
return part;
|
|
267
254
|
}
|
|
268
255
|
return {
|
|
269
256
|
type: "tools",
|
|
@@ -280,16 +267,6 @@ function compactToolCall(toolCall) {
|
|
|
280
267
|
resultCollapsed: true,
|
|
281
268
|
};
|
|
282
269
|
}
|
|
283
|
-
export function truncateText(value, maxChars) {
|
|
284
|
-
if (value.length <= maxChars) {
|
|
285
|
-
return value;
|
|
286
|
-
}
|
|
287
|
-
const head = Math.max(1, Math.floor(maxChars * 0.7));
|
|
288
|
-
const tail = Math.max(1, maxChars - head - 32);
|
|
289
|
-
const omitted = value.length - head - tail;
|
|
290
|
-
const separator = "─".repeat(12);
|
|
291
|
-
return `${value.slice(0, head)}\n${separator} ✂ ${omitted} chars truncated ${separator}\n${value.slice(-tail)}`;
|
|
292
|
-
}
|
|
293
270
|
function shorten(text, maxChars) {
|
|
294
271
|
const normalized = text.replace(/\s+/g, " ").trim();
|
|
295
272
|
if (normalized.length <= maxChars) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { sanitizeInternalReasoningText, sanitizeInternalReminderBlocks, } from "../agent/internal-reminder-sanitizer.js";
|
|
2
|
+
export function sanitizeDisplayMessages(messages) {
|
|
3
|
+
return messages.map(sanitizeDisplayMessage);
|
|
4
|
+
}
|
|
5
|
+
export function sanitizeDisplayMessage(message) {
|
|
6
|
+
if (message.role !== "assistant")
|
|
7
|
+
return message;
|
|
8
|
+
const content = sanitizeInternalReminderBlocks(message.content);
|
|
9
|
+
const reasoning = message.reasoning !== undefined
|
|
10
|
+
? sanitizeInternalReasoningText(message.reasoning)
|
|
11
|
+
: undefined;
|
|
12
|
+
const sanitizedParts = message.parts
|
|
13
|
+
?.map(sanitizeDisplayPart)
|
|
14
|
+
.filter(isRenderableDisplayPart);
|
|
15
|
+
const parts = sanitizedParts && sanitizedParts.length > 0 ? sanitizedParts : undefined;
|
|
16
|
+
if (content === message.content
|
|
17
|
+
&& reasoning === message.reasoning
|
|
18
|
+
&& parts === message.parts) {
|
|
19
|
+
return message;
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
...message,
|
|
23
|
+
content,
|
|
24
|
+
reasoning,
|
|
25
|
+
parts,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function sanitizeDisplayPart(part) {
|
|
29
|
+
if (part.type !== "text")
|
|
30
|
+
return part;
|
|
31
|
+
const content = sanitizeInternalReminderBlocks(part.content);
|
|
32
|
+
return content === part.content ? part : { ...part, content };
|
|
33
|
+
}
|
|
34
|
+
function isRenderableDisplayPart(part) {
|
|
35
|
+
if (part.type === "text")
|
|
36
|
+
return !!part.content.trim();
|
|
37
|
+
return part.toolCalls.length > 0;
|
|
38
|
+
}
|
|
@@ -7,4 +7,5 @@ export interface PastedContentReference {
|
|
|
7
7
|
export declare function countTextLines(text: string): number;
|
|
8
8
|
export declare function shouldCollapsePastedContent(text: string): boolean;
|
|
9
9
|
export declare function createPastedContentMarker(content: string, index?: number): string;
|
|
10
|
+
export declare function decodePastedBytes(bytes: unknown): string;
|
|
10
11
|
export declare function expandPastedContentMarkers(displayText: string, references: PastedContentReference[]): string;
|
|
@@ -16,6 +16,13 @@ export function createPastedContentMarker(content, index = 1) {
|
|
|
16
16
|
: `${content.length} ${content.length === 1 ? "char" : "chars"}`;
|
|
17
17
|
return `[Pasted text #${safeIndex} +${size}]`;
|
|
18
18
|
}
|
|
19
|
+
export function decodePastedBytes(bytes) {
|
|
20
|
+
if (typeof bytes === "string")
|
|
21
|
+
return bytes;
|
|
22
|
+
if (bytes instanceof Uint8Array)
|
|
23
|
+
return new TextDecoder().decode(bytes);
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
19
26
|
export function expandPastedContentMarkers(displayText, references) {
|
|
20
27
|
if (references.length === 0 || displayText.length === 0)
|
|
21
28
|
return displayText;
|
package/dist/tui/run.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Agent } from "../agent.js";
|
|
2
2
|
import type { CliArgs } from "../cli.js";
|
|
3
3
|
import type { ThemeMode } from "../config.js";
|
|
4
|
+
import type { ExternalHookController } from "../hooks/controller.js";
|
|
4
5
|
import type { SessionManager } from "../session.js";
|
|
5
6
|
import type { PlanDecision, Provider } from "../types.js";
|
|
6
7
|
import type { ProviderRegistry } from "../provider-registry.js";
|
|
@@ -29,6 +30,7 @@ export interface RunTuiOptions {
|
|
|
29
30
|
questionController?: QuestionController;
|
|
30
31
|
bashAllowlist?: BashAllowlist;
|
|
31
32
|
settingsManager?: SettingsManager;
|
|
33
|
+
hookController?: ExternalHookController;
|
|
32
34
|
lspService?: LspService;
|
|
33
35
|
mcpManager?: McpManager;
|
|
34
36
|
themeMode?: ThemeMode;
|