@bubblebrain-ai/bubble 0.0.19 → 0.0.21
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 +10 -0
- package/dist/agent.js +310 -18
- package/dist/approval/controller.d.ts +6 -0
- package/dist/approval/controller.js +104 -11
- package/dist/checkpoints.d.ts +57 -0
- package/dist/checkpoints.js +0 -0
- package/dist/debug-trace.js +4 -0
- package/dist/feishu/agent-host/run-driver.js +29 -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 +86 -13
- 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/session.d.ts +31 -0
- package/dist/session.js +69 -0
- package/dist/slash-commands/commands.js +164 -0
- package/dist/slash-commands/types.d.ts +6 -0
- package/dist/tools/bash.js +4 -0
- package/dist/tools/edit-apply.js +63 -3
- package/dist/tools/edit.d.ts +2 -1
- package/dist/tools/edit.js +6 -5
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.js +2 -2
- package/dist/tools/write.d.ts +2 -1
- package/dist/tools/write.js +2 -1
- 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/image-paste.d.ts +18 -0
- package/dist/tui/image-paste.js +60 -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 +568 -223
- package/dist/tui/trace-groups.d.ts +16 -0
- package/dist/tui/trace-groups.js +82 -5
- package/dist/tui/transcript-scroll.d.ts +25 -0
- package/dist/tui/transcript-scroll.js +20 -0
- package/dist/tui/wordmark.d.ts +1 -0
- package/dist/tui/wordmark.js +56 -54
- package/dist/tui-ink/app.d.ts +4 -1
- package/dist/tui-ink/app.js +303 -248
- package/dist/tui-ink/display-history.d.ts +16 -1
- package/dist/tui-ink/display-history.js +50 -21
- package/dist/tui-ink/footer.d.ts +6 -12
- package/dist/tui-ink/footer.js +10 -29
- package/dist/tui-ink/image-paste.d.ts +59 -0
- package/dist/tui-ink/image-paste.js +277 -0
- package/dist/tui-ink/input-box.d.ts +26 -1
- package/dist/tui-ink/input-box.js +171 -41
- package/dist/tui-ink/message-list.d.ts +1 -1
- package/dist/tui-ink/message-list.js +46 -29
- package/dist/tui-ink/run.d.ts +7 -2
- package/dist/tui-ink/run.js +73 -23
- package/dist/tui-ink/terminal-mouse.d.ts +1 -0
- package/dist/tui-ink/terminal-mouse.js +4 -0
- package/dist/tui-ink/trace-groups.d.ts +16 -0
- package/dist/tui-ink/trace-groups.js +90 -6
- package/dist/tui-ink/transcript-viewport-math.d.ts +11 -0
- package/dist/tui-ink/transcript-viewport-math.js +17 -0
- package/dist/tui-ink/transcript-viewport.d.ts +24 -0
- package/dist/tui-ink/transcript-viewport.js +83 -0
- package/dist/tui-ink/welcome.d.ts +9 -7
- package/dist/tui-ink/welcome.js +7 -33
- 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
|
@@ -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
|
+
}
|
|
@@ -44,6 +44,12 @@ export declare function isImageFilePath(raw: string): boolean;
|
|
|
44
44
|
export declare function extractImagePathTokens(input: string): ImagePathToken[];
|
|
45
45
|
export declare function removeImagePathTokens(input: string, tokens: ImagePathToken[]): string;
|
|
46
46
|
export declare function imageAttachmentLabel(att: ImageAttachment, index: number): string;
|
|
47
|
+
/**
|
|
48
|
+
* Label for an image path before ingestion runs. Matches what
|
|
49
|
+
* imageAttachmentLabel produces for the same file, so a label inserted at
|
|
50
|
+
* paste time stays a valid key once the attachment is registered.
|
|
51
|
+
*/
|
|
52
|
+
export declare function imageLabelForPath(rawPath: string, index: number): string;
|
|
47
53
|
export declare function imageAttachmentReference(att: ImageAttachment, index: number): string;
|
|
48
54
|
export declare function imageAttachmentLabelPattern(): RegExp;
|
|
49
55
|
export declare function buildImageContentParts(promptText: string, attachments: ImageAttachment[]): ContentPart[];
|
|
@@ -61,6 +67,18 @@ export declare function buildImageContentPartsFromLabels(input: string, attachme
|
|
|
61
67
|
* only on a space that is followed by the start of a new absolute path.
|
|
62
68
|
*/
|
|
63
69
|
export declare function splitPastedPaths(pasted: string): string[];
|
|
70
|
+
/**
|
|
71
|
+
* True when a pasted blob consists solely of image file paths (drag from
|
|
72
|
+
* Finder, or a terminal that converts clipboard images to temp-file paths).
|
|
73
|
+
*/
|
|
74
|
+
export declare function isImagePathPaste(pasted: string): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Bare image filename with no directory, e.g. "Screenshot ... AM.png".
|
|
77
|
+
* Copying an image file in Finder puts only the file's NAME in the
|
|
78
|
+
* clipboard's plain-text flavor — the actual bits arrive as a file-url or
|
|
79
|
+
* image flavor that must be read from the clipboard separately.
|
|
80
|
+
*/
|
|
81
|
+
export declare function bareImageFilenameFromPaste(pasted: string): string | null;
|
|
64
82
|
export declare function readImageFromPath(rawPath: string): Promise<ImageAttachment | null>;
|
|
65
83
|
/** macOS screenshot shortcut writes to these paths and they may be auto-cleaned. */
|
|
66
84
|
export declare function isScreenshotTempPath(s: string): boolean;
|
package/dist/tui/image-paste.js
CHANGED
|
@@ -65,6 +65,15 @@ export function removeImagePathTokens(input, tokens) {
|
|
|
65
65
|
export function imageAttachmentLabel(att, index) {
|
|
66
66
|
return `image#${index}${imageExtension(att)}`;
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Label for an image path before ingestion runs. Matches what
|
|
70
|
+
* imageAttachmentLabel produces for the same file, so a label inserted at
|
|
71
|
+
* paste time stays a valid key once the attachment is registered.
|
|
72
|
+
*/
|
|
73
|
+
export function imageLabelForPath(rawPath, index) {
|
|
74
|
+
const ext = path.extname(unescapeShell(rawPath.trim())).toLowerCase() || ".png";
|
|
75
|
+
return `image#${index}${ext}`;
|
|
76
|
+
}
|
|
68
77
|
export function imageAttachmentReference(att, index) {
|
|
69
78
|
return `[${imageAttachmentLabel(att, index)}]`;
|
|
70
79
|
}
|
|
@@ -155,6 +164,30 @@ export function splitPastedPaths(pasted) {
|
|
|
155
164
|
}
|
|
156
165
|
return out;
|
|
157
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* True when a pasted blob consists solely of image file paths (drag from
|
|
169
|
+
* Finder, or a terminal that converts clipboard images to temp-file paths).
|
|
170
|
+
*/
|
|
171
|
+
export function isImagePathPaste(pasted) {
|
|
172
|
+
const pieces = splitPastedPaths(pasted);
|
|
173
|
+
return pieces.length > 0 && pieces.every((piece) => isImageFilePath(piece));
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Bare image filename with no directory, e.g. "Screenshot ... AM.png".
|
|
177
|
+
* Copying an image file in Finder puts only the file's NAME in the
|
|
178
|
+
* clipboard's plain-text flavor — the actual bits arrive as a file-url or
|
|
179
|
+
* image flavor that must be read from the clipboard separately.
|
|
180
|
+
*/
|
|
181
|
+
export function bareImageFilenameFromPaste(pasted) {
|
|
182
|
+
const s = pasted.trim();
|
|
183
|
+
if (!s || s.length > 255)
|
|
184
|
+
return null;
|
|
185
|
+
if (/[\n\r/\\]/.test(s))
|
|
186
|
+
return null;
|
|
187
|
+
if (!IMAGE_EXT.test(s))
|
|
188
|
+
return null;
|
|
189
|
+
return s;
|
|
190
|
+
}
|
|
158
191
|
function mediaTypeFromExt(p) {
|
|
159
192
|
const ext = path.extname(p).toLowerCase();
|
|
160
193
|
if (ext === ".jpg" || ext === ".jpeg")
|
|
@@ -384,6 +417,14 @@ export async function ingestImagePath(p) {
|
|
|
384
417
|
return { attachment: sized };
|
|
385
418
|
}
|
|
386
419
|
export async function ingestClipboardImage() {
|
|
420
|
+
// A file reference wins over bitmap flavors: for a copied FILE, coercing
|
|
421
|
+
// the clipboard to PNGf yields the file's generic ICON, not the image.
|
|
422
|
+
const filePath = await getClipboardFilePath();
|
|
423
|
+
if (filePath) {
|
|
424
|
+
if (isImageFilePath(filePath))
|
|
425
|
+
return ingestImagePath(filePath);
|
|
426
|
+
return { error: `clipboard file is not an image: ${filePath}` };
|
|
427
|
+
}
|
|
387
428
|
const raw = await getImageFromClipboard();
|
|
388
429
|
if (!raw)
|
|
389
430
|
return { error: "clipboard has no image" };
|
|
@@ -393,6 +434,25 @@ export async function ingestClipboardImage() {
|
|
|
393
434
|
return { error: validation.reason };
|
|
394
435
|
return { attachment: sized };
|
|
395
436
|
}
|
|
437
|
+
async function getClipboardFilePath() {
|
|
438
|
+
if (process.platform !== "darwin")
|
|
439
|
+
return null;
|
|
440
|
+
try {
|
|
441
|
+
// Probe first — AppleScript happily coerces plain TEXT into a file URL,
|
|
442
|
+
// so only trust «class furl» when the clipboard really carries one.
|
|
443
|
+
const probe = await execFileAsync("osascript", ["-e", "clipboard info for «class furl»"], {
|
|
444
|
+
timeout: 5000,
|
|
445
|
+
});
|
|
446
|
+
if (!String(probe.stdout).includes("furl"))
|
|
447
|
+
return null;
|
|
448
|
+
const result = await execFileAsync("osascript", ["-e", "POSIX path of (the clipboard as «class furl»)"], { timeout: 5000 });
|
|
449
|
+
const p = String(result.stdout).trim();
|
|
450
|
+
return p || null;
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
396
456
|
export async function resolveImageInput(input, options = {}) {
|
|
397
457
|
const tokens = extractImagePathTokens(input);
|
|
398
458
|
if (tokens.length === 0) {
|
|
@@ -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;
|