@bubblebrain-ai/bubble 0.0.2 → 0.0.4
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/README.md +8 -3
- package/dist/agent/execution-governor.d.ts +14 -0
- package/dist/agent/execution-governor.js +172 -14
- package/dist/agent/task-classifier.d.ts +1 -1
- package/dist/agent/task-classifier.js +60 -0
- package/dist/agent/tool-intent.d.ts +14 -0
- package/dist/agent/tool-intent.js +125 -1
- package/dist/agent.js +4 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +45 -0
- package/dist/orchestrator/default-hooks.js +53 -1
- package/dist/orchestrator/hooks.d.ts +5 -0
- package/dist/prompt/compose.js +12 -0
- package/dist/prompt/provider-prompts/deepseek.d.ts +1 -0
- package/dist/prompt/provider-prompts/deepseek.js +8 -0
- package/dist/prompt/provider-prompts/glm.d.ts +1 -0
- package/dist/prompt/provider-prompts/glm.js +7 -0
- package/dist/prompt/provider-prompts/kimi.d.ts +1 -0
- package/dist/prompt/provider-prompts/kimi.js +7 -0
- package/dist/prompt/reminders.d.ts +2 -0
- package/dist/prompt/reminders.js +28 -2
- package/dist/prompt/runtime.js +15 -2
- package/dist/prompt/task-reminders.d.ts +2 -0
- package/dist/prompt/task-reminders.js +56 -0
- package/dist/slash-commands/commands.js +2 -3
- package/dist/tools/bash.js +10 -7
- package/dist/tools/edit.js +5 -0
- package/dist/tools/write.js +8 -1
- package/dist/tui/image-paste.d.ts +41 -0
- package/dist/tui/image-paste.js +217 -0
- package/dist/tui/run.js +219 -11
- package/package.json +6 -3
package/dist/tui/image-paste.js
CHANGED
|
@@ -14,6 +14,7 @@ import path from "node:path";
|
|
|
14
14
|
import { promisify } from "node:util";
|
|
15
15
|
const execFileAsync = promisify(execFile);
|
|
16
16
|
const IMAGE_EXT = /\.(png|jpe?g|gif|webp|bmp)$/i;
|
|
17
|
+
const IMAGE_EXT_SOURCE = String.raw `(?:png|jpe?g|gif|webp|bmp)`;
|
|
17
18
|
// Anthropic/OpenAI image uploads cap at ~5MB base64. We target a bit below so
|
|
18
19
|
// the base64 inflation (4/3) doesn't push us over.
|
|
19
20
|
const MAX_BASE64_BYTES = 5 * 1024 * 1024;
|
|
@@ -30,6 +31,112 @@ export function isImageFilePath(raw) {
|
|
|
30
31
|
// be treated as a path.
|
|
31
32
|
return path.isAbsolute(s) || s.startsWith("~") || /^[A-Za-z]:\\/.test(s);
|
|
32
33
|
}
|
|
34
|
+
export function extractImagePathTokens(input) {
|
|
35
|
+
const pattern = new RegExp(String.raw `(^|\s)(?:"([^"]+\.${IMAGE_EXT_SOURCE})"|'([^']+\.${IMAGE_EXT_SOURCE})'|((?:~|\/|[A-Za-z]:\\)(?:\\ |[^\s"'<>])+\.${IMAGE_EXT_SOURCE}))(?=$|\s)`, "gi");
|
|
36
|
+
const tokens = [];
|
|
37
|
+
for (const match of input.matchAll(pattern)) {
|
|
38
|
+
const leading = match[1] ?? "";
|
|
39
|
+
const rawPath = match[2] ?? match[3] ?? match[4];
|
|
40
|
+
if (!rawPath || !isImageFilePath(rawPath))
|
|
41
|
+
continue;
|
|
42
|
+
const start = (match.index ?? 0) + leading.length;
|
|
43
|
+
const end = (match.index ?? 0) + match[0].length;
|
|
44
|
+
tokens.push({ rawPath, start, end });
|
|
45
|
+
}
|
|
46
|
+
return tokens;
|
|
47
|
+
}
|
|
48
|
+
export function removeImagePathTokens(input, tokens) {
|
|
49
|
+
if (tokens.length === 0)
|
|
50
|
+
return input.trim();
|
|
51
|
+
let out = "";
|
|
52
|
+
let cursor = 0;
|
|
53
|
+
for (const token of tokens) {
|
|
54
|
+
out += input.slice(cursor, token.start);
|
|
55
|
+
out += " ";
|
|
56
|
+
cursor = token.end;
|
|
57
|
+
}
|
|
58
|
+
out += input.slice(cursor);
|
|
59
|
+
return out
|
|
60
|
+
.replace(/[ \t]+/g, " ")
|
|
61
|
+
.replace(/ *\n */g, "\n")
|
|
62
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
63
|
+
.trim();
|
|
64
|
+
}
|
|
65
|
+
export function imageAttachmentLabel(att, index) {
|
|
66
|
+
return `image#${index}${imageExtension(att)}`;
|
|
67
|
+
}
|
|
68
|
+
export function imageAttachmentReference(att, index) {
|
|
69
|
+
return `[${imageAttachmentLabel(att, index)}]`;
|
|
70
|
+
}
|
|
71
|
+
export function imageAttachmentLabelPattern() {
|
|
72
|
+
return /\[image#(\d+)\.[^\]\s]+\]/g;
|
|
73
|
+
}
|
|
74
|
+
function defaultImagePrompt(count) {
|
|
75
|
+
return count === 1
|
|
76
|
+
? "Please analyze the attached image."
|
|
77
|
+
: "Please analyze the attached images.";
|
|
78
|
+
}
|
|
79
|
+
function imageExtension(att) {
|
|
80
|
+
const fromPath = path.extname(att.filename ?? att.sourcePath ?? "").toLowerCase();
|
|
81
|
+
if (fromPath)
|
|
82
|
+
return fromPath;
|
|
83
|
+
if (att.mediaType === "image/jpeg")
|
|
84
|
+
return ".jpg";
|
|
85
|
+
if (att.mediaType === "image/webp")
|
|
86
|
+
return ".webp";
|
|
87
|
+
if (att.mediaType === "image/gif")
|
|
88
|
+
return ".gif";
|
|
89
|
+
if (att.mediaType === "image/bmp")
|
|
90
|
+
return ".bmp";
|
|
91
|
+
return ".png";
|
|
92
|
+
}
|
|
93
|
+
export function buildImageContentParts(promptText, attachments) {
|
|
94
|
+
const text = promptText.trim() || defaultImagePrompt(attachments.length);
|
|
95
|
+
return [
|
|
96
|
+
{ type: "text", text },
|
|
97
|
+
...attachments.map((attachment) => ({
|
|
98
|
+
type: "image_url",
|
|
99
|
+
image_url: { url: attachment.dataUrl },
|
|
100
|
+
})),
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
export function formatImageDisplayInput(promptText, attachments, labelStart = 1) {
|
|
104
|
+
const text = promptText.trim() || defaultImagePrompt(attachments.length);
|
|
105
|
+
const imageLines = attachments.map((attachment, index) => imageAttachmentReference(attachment, labelStart + index));
|
|
106
|
+
return `${text}\n${imageLines.join("\n")}`;
|
|
107
|
+
}
|
|
108
|
+
export function buildImageContentPartsFromLabels(input, attachmentsByLabel) {
|
|
109
|
+
const matches = Array.from(input.matchAll(imageAttachmentLabelPattern()));
|
|
110
|
+
const usedLabels = [];
|
|
111
|
+
const parts = [];
|
|
112
|
+
let cursor = 0;
|
|
113
|
+
for (const match of matches) {
|
|
114
|
+
const label = match[0].slice(1, -1);
|
|
115
|
+
const attachment = attachmentsByLabel.get(label);
|
|
116
|
+
if (!attachment)
|
|
117
|
+
continue;
|
|
118
|
+
const start = match.index ?? 0;
|
|
119
|
+
const before = input.slice(cursor, start).trim();
|
|
120
|
+
if (before)
|
|
121
|
+
parts.push({ type: "text", text: before });
|
|
122
|
+
parts.push({ type: "image_url", image_url: { url: attachment.dataUrl } });
|
|
123
|
+
usedLabels.push(label);
|
|
124
|
+
cursor = start + match[0].length;
|
|
125
|
+
}
|
|
126
|
+
if (usedLabels.length === 0)
|
|
127
|
+
return { displayInput: input, usedLabels: [] };
|
|
128
|
+
const rest = input.slice(cursor).trim();
|
|
129
|
+
if (rest)
|
|
130
|
+
parts.push({ type: "text", text: rest });
|
|
131
|
+
if (!parts.some((part) => part.type === "text")) {
|
|
132
|
+
parts.unshift({ type: "text", text: defaultImagePrompt(usedLabels.length) });
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
actualInput: parts,
|
|
136
|
+
displayInput: input.trim() || usedLabels.map((label) => `[${label}]`).join("\n"),
|
|
137
|
+
usedLabels,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
33
140
|
/**
|
|
34
141
|
* Split a pasted blob into candidate path tokens.
|
|
35
142
|
*
|
|
@@ -286,3 +393,113 @@ export async function ingestClipboardImage() {
|
|
|
286
393
|
return { error: validation.reason };
|
|
287
394
|
return { attachment: sized };
|
|
288
395
|
}
|
|
396
|
+
export async function resolveImageInput(input, options = {}) {
|
|
397
|
+
const tokens = extractImagePathTokens(input);
|
|
398
|
+
if (tokens.length === 0) {
|
|
399
|
+
return {
|
|
400
|
+
actualInput: input,
|
|
401
|
+
displayInput: input,
|
|
402
|
+
errors: [],
|
|
403
|
+
attachments: [],
|
|
404
|
+
imagePathCount: 0,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
const attachments = [];
|
|
408
|
+
const errors = [];
|
|
409
|
+
const attachmentsByToken = new Map();
|
|
410
|
+
let nextLabelIndex = options.labelStart ?? 1;
|
|
411
|
+
for (const token of tokens) {
|
|
412
|
+
const result = await ingestImagePath(token.rawPath);
|
|
413
|
+
if (result.attachment) {
|
|
414
|
+
attachments.push(result.attachment);
|
|
415
|
+
attachmentsByToken.set(token, {
|
|
416
|
+
attachment: result.attachment,
|
|
417
|
+
label: imageAttachmentLabel(result.attachment, nextLabelIndex++),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
errors.push(`${token.rawPath}: ${result.error ?? "could not attach image"}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (attachments.length === 0) {
|
|
425
|
+
return {
|
|
426
|
+
actualInput: input,
|
|
427
|
+
displayInput: input,
|
|
428
|
+
errors,
|
|
429
|
+
attachments: [],
|
|
430
|
+
imagePathCount: tokens.length,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const parts = [];
|
|
434
|
+
let displayInput = "";
|
|
435
|
+
let cursor = 0;
|
|
436
|
+
for (const token of tokens) {
|
|
437
|
+
const entry = attachmentsByToken.get(token);
|
|
438
|
+
if (!entry)
|
|
439
|
+
continue;
|
|
440
|
+
const before = input.slice(cursor, token.start);
|
|
441
|
+
displayInput += before;
|
|
442
|
+
const text = before.trim();
|
|
443
|
+
if (text)
|
|
444
|
+
parts.push({ type: "text", text });
|
|
445
|
+
parts.push({ type: "image_url", image_url: { url: entry.attachment.dataUrl } });
|
|
446
|
+
displayInput += `[${entry.label}]`;
|
|
447
|
+
cursor = token.end;
|
|
448
|
+
}
|
|
449
|
+
const rest = input.slice(cursor);
|
|
450
|
+
displayInput += rest;
|
|
451
|
+
const restText = rest.trim();
|
|
452
|
+
if (restText)
|
|
453
|
+
parts.push({ type: "text", text: restText });
|
|
454
|
+
if (!parts.some((part) => part.type === "text")) {
|
|
455
|
+
parts.unshift({ type: "text", text: defaultImagePrompt(attachments.length) });
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
actualInput: parts,
|
|
459
|
+
displayInput: displayInput.trim(),
|
|
460
|
+
errors,
|
|
461
|
+
attachments,
|
|
462
|
+
imagePathCount: tokens.length,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
export async function resolveComposerImagePaths(input, options = {}) {
|
|
466
|
+
const tokens = extractImagePathTokens(input);
|
|
467
|
+
let nextLabelIndex = options.labelStart ?? 1;
|
|
468
|
+
if (tokens.length === 0) {
|
|
469
|
+
return {
|
|
470
|
+
text: input,
|
|
471
|
+
attachments: [],
|
|
472
|
+
errors: [],
|
|
473
|
+
imagePathCount: 0,
|
|
474
|
+
nextLabelIndex,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
const errors = [];
|
|
478
|
+
const attachments = [];
|
|
479
|
+
const replacements = new Map();
|
|
480
|
+
for (const token of tokens) {
|
|
481
|
+
const result = await ingestImagePath(token.rawPath);
|
|
482
|
+
if (!result.attachment) {
|
|
483
|
+
errors.push(`${token.rawPath}: ${result.error ?? "could not attach image"}`);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const label = imageAttachmentLabel(result.attachment, nextLabelIndex++);
|
|
487
|
+
attachments.push({ ...result.attachment, label });
|
|
488
|
+
replacements.set(token, `[${label}]`);
|
|
489
|
+
}
|
|
490
|
+
let text = "";
|
|
491
|
+
let cursor = 0;
|
|
492
|
+
for (const token of tokens) {
|
|
493
|
+
text += input.slice(cursor, token.start);
|
|
494
|
+
text += replacements.get(token) ?? input.slice(token.start, token.end);
|
|
495
|
+
cursor = token.end;
|
|
496
|
+
}
|
|
497
|
+
text += input.slice(cursor);
|
|
498
|
+
return {
|
|
499
|
+
text,
|
|
500
|
+
attachments,
|
|
501
|
+
errors,
|
|
502
|
+
imagePathCount: tokens.length,
|
|
503
|
+
nextLabelIndex,
|
|
504
|
+
};
|
|
505
|
+
}
|
package/dist/tui/run.js
CHANGED
|
@@ -20,6 +20,7 @@ import { inferBashPrefix } from "../approval/session-cache.js";
|
|
|
20
20
|
import { createFrames } from "./opencode-spinner.js";
|
|
21
21
|
import { copyTextToClipboard } from "./clipboard.js";
|
|
22
22
|
import { readGitSidebarState } from "./sidebar-state.js";
|
|
23
|
+
import { buildImageContentPartsFromLabels, imageAttachmentLabelPattern, resolveComposerImagePaths, resolveImageInput, } from "./image-paste.js";
|
|
23
24
|
import { isModeCycleKeyEvent, isModeCycleSequence, isModifiedEnterSequence, PROMPT_TEXTAREA_KEYBINDINGS, } from "./prompt-keybindings.js";
|
|
24
25
|
import { keyNameFromEvent, keyNameFromSequence } from "./global-key-router.js";
|
|
25
26
|
const treeSitterClient = getTreeSitterClient();
|
|
@@ -263,6 +264,10 @@ function OpenTuiApp(props) {
|
|
|
263
264
|
.filter((message) => message.role === "user" && message.content !== "(multimedia)")
|
|
264
265
|
.map((message) => message.content)
|
|
265
266
|
.slice(-PROMPT_HISTORY_LIMIT);
|
|
267
|
+
let nextImageAttachmentIndex = nextImageLabelIndex(displayMessages);
|
|
268
|
+
const pendingImageAttachments = new Map();
|
|
269
|
+
let composerImageResolutionSeq = 0;
|
|
270
|
+
let applyingComposerImageReplacement = false;
|
|
266
271
|
let promptHistoryIndex;
|
|
267
272
|
let promptHistoryDraft = "";
|
|
268
273
|
const [isRunning, setIsRunning] = createSignal(false);
|
|
@@ -387,6 +392,10 @@ function OpenTuiApp(props) {
|
|
|
387
392
|
const sidebarLspRows = [];
|
|
388
393
|
const sidebarLspMarkers = [];
|
|
389
394
|
const sidebarLspLabels = [];
|
|
395
|
+
let sidebarTodoSection;
|
|
396
|
+
const sidebarTodoRows = [];
|
|
397
|
+
const sidebarTodoMarkers = [];
|
|
398
|
+
const sidebarTodoLabels = [];
|
|
390
399
|
const sidebarFileRows = [];
|
|
391
400
|
const sidebarFileLabels = [];
|
|
392
401
|
const sidebarFileAdditions = [];
|
|
@@ -621,6 +630,12 @@ function OpenTuiApp(props) {
|
|
|
621
630
|
setSidebarTick((value) => value + 1);
|
|
622
631
|
syncSidebarContext();
|
|
623
632
|
};
|
|
633
|
+
const syncTodosFromAgent = () => {
|
|
634
|
+
const nextTodos = props.agent.getTodos();
|
|
635
|
+
setTodos(nextTodos);
|
|
636
|
+
syncSidebarTodos(nextTodos);
|
|
637
|
+
bumpSidebar();
|
|
638
|
+
};
|
|
624
639
|
function refreshGitSidebar() {
|
|
625
640
|
setGitState(readGitSidebarState(props.args.cwd));
|
|
626
641
|
syncSidebarFiles();
|
|
@@ -721,6 +736,35 @@ function OpenTuiApp(props) {
|
|
|
721
736
|
}
|
|
722
737
|
sidebarShell?.requestRender();
|
|
723
738
|
}
|
|
739
|
+
function syncSidebarTodos(nextTodos = todos()) {
|
|
740
|
+
const visible = nextTodos.slice(0, 8);
|
|
741
|
+
if (sidebarTodoSection) {
|
|
742
|
+
sidebarTodoSection.visible = visible.length > 0;
|
|
743
|
+
}
|
|
744
|
+
for (let index = 0; index < 8; index++) {
|
|
745
|
+
const row = sidebarTodoRows[index];
|
|
746
|
+
const marker = sidebarTodoMarkers[index];
|
|
747
|
+
const label = sidebarTodoLabels[index];
|
|
748
|
+
const todo = visible[index];
|
|
749
|
+
if (!row || !marker || !label)
|
|
750
|
+
continue;
|
|
751
|
+
row.visible = !!todo;
|
|
752
|
+
if (!todo) {
|
|
753
|
+
safeRequestRender(row);
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
const completed = todo.status === "completed";
|
|
757
|
+
const inProgress = todo.status === "in_progress";
|
|
758
|
+
const labelText = inProgress ? (todo.activeForm || todo.content) : todo.content;
|
|
759
|
+
marker.content = completed ? "✓" : inProgress ? "◉" : "○";
|
|
760
|
+
marker.fg = completed ? theme.success : inProgress ? theme.warning : theme.textMuted;
|
|
761
|
+
label.content = labelText;
|
|
762
|
+
label.fg = completed ? theme.success : inProgress ? theme.warning : theme.textMuted;
|
|
763
|
+
safeRequestRender(row);
|
|
764
|
+
}
|
|
765
|
+
sidebarShell?.requestRender();
|
|
766
|
+
rootBox?.requestRender();
|
|
767
|
+
}
|
|
724
768
|
function showSidebarLspRows(statuses) {
|
|
725
769
|
for (let index = 0; index < sidebarLspRows.length; index++) {
|
|
726
770
|
const row = sidebarLspRows[index];
|
|
@@ -2689,6 +2733,9 @@ function OpenTuiApp(props) {
|
|
|
2689
2733
|
function onPromptContentChange(value) {
|
|
2690
2734
|
const nextValue = typeof value === "string" ? value : readPromptText();
|
|
2691
2735
|
promptText = nextValue;
|
|
2736
|
+
if (!applyingComposerImageReplacement) {
|
|
2737
|
+
void applyComposerImagePathReplacement(nextValue);
|
|
2738
|
+
}
|
|
2692
2739
|
if (promptHistoryIndex !== undefined
|
|
2693
2740
|
&& nextValue !== (promptHistory[promptHistoryIndex] ?? "")) {
|
|
2694
2741
|
resetPromptHistoryBrowse();
|
|
@@ -2956,8 +3003,62 @@ function OpenTuiApp(props) {
|
|
|
2956
3003
|
setPromptText(`/${skillName} `);
|
|
2957
3004
|
redrawDock();
|
|
2958
3005
|
}
|
|
3006
|
+
async function applyComposerImagePathReplacement(snapshot) {
|
|
3007
|
+
const seq = ++composerImageResolutionSeq;
|
|
3008
|
+
const result = await resolveComposerImagePaths(snapshot, { labelStart: nextImageAttachmentIndex });
|
|
3009
|
+
if (seq !== composerImageResolutionSeq)
|
|
3010
|
+
return;
|
|
3011
|
+
if (result.attachments.length === 0)
|
|
3012
|
+
return;
|
|
3013
|
+
if ((readPromptText() || promptText) !== snapshot)
|
|
3014
|
+
return;
|
|
3015
|
+
for (const attachment of result.attachments) {
|
|
3016
|
+
pendingImageAttachments.set(attachment.label, attachment);
|
|
3017
|
+
}
|
|
3018
|
+
nextImageAttachmentIndex = Math.max(nextImageAttachmentIndex, result.nextLabelIndex);
|
|
3019
|
+
applyingComposerImageReplacement = true;
|
|
3020
|
+
try {
|
|
3021
|
+
setPromptText(result.text);
|
|
3022
|
+
}
|
|
3023
|
+
finally {
|
|
3024
|
+
applyingComposerImageReplacement = false;
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
async function expandTextParts(parts) {
|
|
3028
|
+
const expandedParts = [];
|
|
3029
|
+
for (const part of parts) {
|
|
3030
|
+
if (part.type !== "text") {
|
|
3031
|
+
expandedParts.push(part);
|
|
3032
|
+
continue;
|
|
3033
|
+
}
|
|
3034
|
+
const expansion = await expandAtMentions(part.text, props.args.cwd);
|
|
3035
|
+
if (expansion.missing.length)
|
|
3036
|
+
addMessage("error", `Could not resolve @mention: ${expansion.missing.join(", ")}`);
|
|
3037
|
+
for (const skipped of expansion.skipped)
|
|
3038
|
+
addMessage("error", `Skipped @${skipped.path}: ${skipped.reason}`);
|
|
3039
|
+
expandedParts.push({ type: "text", text: expansion.text });
|
|
3040
|
+
}
|
|
3041
|
+
return expandedParts;
|
|
3042
|
+
}
|
|
2959
3043
|
async function handleInput(input) {
|
|
2960
3044
|
setNotice("");
|
|
3045
|
+
const labeledInput = buildImageContentPartsFromLabels(input, pendingImageAttachments);
|
|
3046
|
+
if (labeledInput.actualInput) {
|
|
3047
|
+
await runAgentInput(await expandTextParts(labeledInput.actualInput), labeledInput.displayInput);
|
|
3048
|
+
for (const label of labeledInput.usedLabels)
|
|
3049
|
+
pendingImageAttachments.delete(label);
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
const imageInput = await resolveImageInput(input, { labelStart: nextImageAttachmentIndex });
|
|
3053
|
+
for (const error of imageInput.errors)
|
|
3054
|
+
addMessage("error", `Skipped image: ${error}`);
|
|
3055
|
+
if (imageInput.attachments.length > 0) {
|
|
3056
|
+
await runAgentInput(await expandTextParts(imageInput.actualInput), imageInput.displayInput);
|
|
3057
|
+
nextImageAttachmentIndex += imageInput.attachments.length;
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
3060
|
+
if (imageInput.imagePathCount > 0)
|
|
3061
|
+
return;
|
|
2961
3062
|
if (input.startsWith("/")) {
|
|
2962
3063
|
const skillInvocation = parseSkillInvocation(input, skills);
|
|
2963
3064
|
if (skillInvocation) {
|
|
@@ -3016,6 +3117,7 @@ function OpenTuiApp(props) {
|
|
|
3016
3117
|
return true;
|
|
3017
3118
|
if (props.agent.mode !== mode())
|
|
3018
3119
|
setMode(props.agent.mode);
|
|
3120
|
+
syncTodosFromAgent();
|
|
3019
3121
|
syncModelChrome();
|
|
3020
3122
|
syncModeChrome();
|
|
3021
3123
|
if (uiDisposed)
|
|
@@ -3031,7 +3133,9 @@ function OpenTuiApp(props) {
|
|
|
3031
3133
|
const isCompactResult = result.startsWith("✓ Compaction complete");
|
|
3032
3134
|
if (isCompactResult) {
|
|
3033
3135
|
setNotice(result);
|
|
3034
|
-
|
|
3136
|
+
displayMessages = reconstructDisplayMessages(props.agent.messages);
|
|
3137
|
+
streamingDisplay = undefined;
|
|
3138
|
+
redrawTranscript(undefined, displayMessages);
|
|
3035
3139
|
setTimeout(() => setNotice(""), 4000);
|
|
3036
3140
|
}
|
|
3037
3141
|
else {
|
|
@@ -3494,6 +3598,7 @@ function OpenTuiApp(props) {
|
|
|
3494
3598
|
}
|
|
3495
3599
|
else if (event.type === "todos_updated") {
|
|
3496
3600
|
setTodos(event.todos);
|
|
3601
|
+
syncSidebarTodos(event.todos);
|
|
3497
3602
|
bumpSidebar();
|
|
3498
3603
|
}
|
|
3499
3604
|
else if (event.type === "mode_changed") {
|
|
@@ -4068,7 +4173,6 @@ function OpenTuiApp(props) {
|
|
|
4068
4173
|
const context = sidebarContextState();
|
|
4069
4174
|
const mcpStates = sidebarMcpStates();
|
|
4070
4175
|
const files = gitState().files;
|
|
4071
|
-
const activeTodos = todos().filter((todo) => todo.status !== "completed");
|
|
4072
4176
|
return h("box", {
|
|
4073
4177
|
ref: (ref) => {
|
|
4074
4178
|
sidebarShell = ref;
|
|
@@ -4154,7 +4258,7 @@ function OpenTuiApp(props) {
|
|
|
4154
4258
|
? renderSidebarSection("Compactions", [
|
|
4155
4259
|
h("text", { fg: theme.info, wrapMode: "word" }, `${currentTranscriptMessages().filter((m) => m.syntheticKind === "ui_compact_card").length} in this session`),
|
|
4156
4260
|
])
|
|
4157
|
-
: null, renderSidebarMcp(mcpStates), renderSidebarLsp(),
|
|
4261
|
+
: null, renderSidebarMcp(mcpStates), renderSidebarLsp(), renderSidebarTodos(todos()), renderSidebarFiles(files))),
|
|
4158
4262
|
renderSidebarFooter(),
|
|
4159
4263
|
]);
|
|
4160
4264
|
}
|
|
@@ -4239,11 +4343,37 @@ function OpenTuiApp(props) {
|
|
|
4239
4343
|
}),
|
|
4240
4344
|
]);
|
|
4241
4345
|
}
|
|
4242
|
-
function renderSidebarTodos(
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4346
|
+
function renderSidebarTodos(todos) {
|
|
4347
|
+
const visible = todos.slice(0, 8);
|
|
4348
|
+
return h("box", {
|
|
4349
|
+
flexDirection: "column",
|
|
4350
|
+
flexShrink: 0,
|
|
4351
|
+
visible: visible.length > 0,
|
|
4352
|
+
ref: (ref) => {
|
|
4353
|
+
sidebarTodoSection = ref;
|
|
4354
|
+
syncSidebarTodos();
|
|
4355
|
+
},
|
|
4356
|
+
}, h("text", { fg: theme.text }, "Todo"), ...Array.from({ length: 8 }, (_, index) => {
|
|
4357
|
+
const todo = visible[index];
|
|
4358
|
+
const completed = todo?.status === "completed";
|
|
4359
|
+
const inProgress = todo?.status === "in_progress";
|
|
4360
|
+
const labelText = todo
|
|
4361
|
+
? (inProgress ? (todo.activeForm || todo.content) : todo.content)
|
|
4362
|
+
: "";
|
|
4363
|
+
return h("box", {
|
|
4364
|
+
flexDirection: "row",
|
|
4365
|
+
gap: 1,
|
|
4366
|
+
visible: !!todo,
|
|
4367
|
+
ref: (ref) => { sidebarTodoRows[index] = ref; },
|
|
4368
|
+
}, h("text", {
|
|
4369
|
+
fg: completed ? theme.success : inProgress ? theme.warning : theme.textMuted,
|
|
4370
|
+
flexShrink: 0,
|
|
4371
|
+
ref: (ref) => { sidebarTodoMarkers[index] = ref; },
|
|
4372
|
+
}, completed ? "✓" : inProgress ? "◉" : "○"), h("text", {
|
|
4373
|
+
fg: completed ? theme.success : inProgress ? theme.warning : theme.textMuted,
|
|
4374
|
+
wrapMode: "word",
|
|
4375
|
+
ref: (ref) => { sidebarTodoLabels[index] = ref; },
|
|
4376
|
+
}, labelText));
|
|
4247
4377
|
}));
|
|
4248
4378
|
}
|
|
4249
4379
|
function renderSidebarFiles(files) {
|
|
@@ -5249,10 +5379,50 @@ function createModelSwitchEntry(ctx, model, key, signature) {
|
|
|
5249
5379
|
]);
|
|
5250
5380
|
return { key, signature, node, refs: {} };
|
|
5251
5381
|
}
|
|
5382
|
+
function createTodoWriteRenderable(ctx, tool) {
|
|
5383
|
+
const todos = tool.args.todos || [];
|
|
5384
|
+
const summary = tool.result || "";
|
|
5385
|
+
if (!isToolFinished(tool)) {
|
|
5386
|
+
return createBox(ctx, {
|
|
5387
|
+
paddingLeft: 3,
|
|
5388
|
+
marginTop: 1,
|
|
5389
|
+
flexDirection: "column",
|
|
5390
|
+
flexShrink: 0,
|
|
5391
|
+
}, [
|
|
5392
|
+
createText(ctx, `~ → Planning tasks...`, { fg: toolColor(tool) }),
|
|
5393
|
+
]);
|
|
5394
|
+
}
|
|
5395
|
+
return createBox(ctx, {
|
|
5396
|
+
border: ["left"],
|
|
5397
|
+
borderColor: theme.borderSubtle,
|
|
5398
|
+
backgroundColor: theme.backgroundPanel,
|
|
5399
|
+
marginTop: 1,
|
|
5400
|
+
paddingTop: 1,
|
|
5401
|
+
paddingBottom: 1,
|
|
5402
|
+
paddingLeft: 2,
|
|
5403
|
+
flexDirection: "column",
|
|
5404
|
+
flexShrink: 0,
|
|
5405
|
+
}, [
|
|
5406
|
+
createText(ctx, `# Todo ${summary ? `— ${summary}` : ""}`, { fg: theme.textMuted }),
|
|
5407
|
+
...todos.map((todo, index) => {
|
|
5408
|
+
const completed = todo.status === "completed";
|
|
5409
|
+
const inProgress = todo.status === "in_progress";
|
|
5410
|
+
const marker = completed ? "✓" : inProgress ? "◉" : "○";
|
|
5411
|
+
const fg = completed ? theme.success : inProgress ? theme.warning : theme.textMuted;
|
|
5412
|
+
return createText(ctx, ` ${marker} ${todo.content}`, {
|
|
5413
|
+
fg,
|
|
5414
|
+
marginTop: index === 0 ? 1 : 0,
|
|
5415
|
+
});
|
|
5416
|
+
}),
|
|
5417
|
+
]);
|
|
5418
|
+
}
|
|
5252
5419
|
function createToolRenderable(ctx, tool, syntaxStyle, width = 80) {
|
|
5253
5420
|
if (tool.name === "question") {
|
|
5254
5421
|
return createQuestionToolRenderable(ctx, tool);
|
|
5255
5422
|
}
|
|
5423
|
+
if (tool.name === "todo_write") {
|
|
5424
|
+
return createTodoWriteRenderable(ctx, tool);
|
|
5425
|
+
}
|
|
5256
5426
|
const icon = tool.name === "bash" ? "$" : tool.name === "edit" || tool.name === "write" ? "✎" : "●";
|
|
5257
5427
|
const color = toolColor(tool);
|
|
5258
5428
|
const header = toolHeader(tool);
|
|
@@ -5638,6 +5808,37 @@ function toSelectOption(item) {
|
|
|
5638
5808
|
value: item.value,
|
|
5639
5809
|
};
|
|
5640
5810
|
}
|
|
5811
|
+
function nextImageLabelIndex(messages) {
|
|
5812
|
+
let max = 0;
|
|
5813
|
+
for (const message of messages) {
|
|
5814
|
+
for (const match of message.content.matchAll(imageAttachmentLabelPattern())) {
|
|
5815
|
+
max = Math.max(max, Number(match[1] ?? 0));
|
|
5816
|
+
}
|
|
5817
|
+
}
|
|
5818
|
+
return max + 1;
|
|
5819
|
+
}
|
|
5820
|
+
function imageExtensionFromUrl(url) {
|
|
5821
|
+
const mediaMatch = url.match(/^data:image\/([^;,]+)/i);
|
|
5822
|
+
const media = mediaMatch?.[1]?.toLowerCase();
|
|
5823
|
+
if (media === "jpeg")
|
|
5824
|
+
return "jpg";
|
|
5825
|
+
if (media === "png" || media === "webp" || media === "gif" || media === "bmp")
|
|
5826
|
+
return media;
|
|
5827
|
+
const pathMatch = url.match(/\.([a-z0-9]+)(?:[?#].*)?$/i);
|
|
5828
|
+
return pathMatch?.[1]?.toLowerCase() || "png";
|
|
5829
|
+
}
|
|
5830
|
+
function formatDisplayContentParts(content, labelStart) {
|
|
5831
|
+
const text = content
|
|
5832
|
+
.filter((part) => part.type === "text")
|
|
5833
|
+
.map((part) => part.text)
|
|
5834
|
+
.join("\n")
|
|
5835
|
+
.trim();
|
|
5836
|
+
let imageIndex = labelStart;
|
|
5837
|
+
const imageLines = content
|
|
5838
|
+
.filter((part) => part.type === "image_url")
|
|
5839
|
+
.map((part) => `[image#${imageIndex++}.${imageExtensionFromUrl(part.image_url.url)}]`);
|
|
5840
|
+
return [text, ...imageLines].filter(Boolean).join("\n") || "(multimedia)";
|
|
5841
|
+
}
|
|
5641
5842
|
function reconstructDisplayMessages(agentMessages) {
|
|
5642
5843
|
const result = [];
|
|
5643
5844
|
for (const message of agentMessages) {
|
|
@@ -5646,7 +5847,12 @@ function reconstructDisplayMessages(agentMessages) {
|
|
|
5646
5847
|
if (message.role === "user") {
|
|
5647
5848
|
if (message.isMeta)
|
|
5648
5849
|
continue;
|
|
5649
|
-
result.push({
|
|
5850
|
+
result.push({
|
|
5851
|
+
role: "user",
|
|
5852
|
+
content: typeof message.content === "string"
|
|
5853
|
+
? message.content
|
|
5854
|
+
: formatDisplayContentParts(message.content, nextImageLabelIndex(result)),
|
|
5855
|
+
});
|
|
5650
5856
|
continue;
|
|
5651
5857
|
}
|
|
5652
5858
|
const toolCalls = [];
|
|
@@ -5988,8 +6194,10 @@ function extractToolDiff(tool) {
|
|
|
5988
6194
|
const index = tool.result.indexOf(marker);
|
|
5989
6195
|
if (index === -1)
|
|
5990
6196
|
return undefined;
|
|
5991
|
-
const
|
|
5992
|
-
|
|
6197
|
+
const rawDiff = tool.result.slice(index + marker.length);
|
|
6198
|
+
const diagnosticsIndex = rawDiff.search(/\n\nLSP diagnostics in /);
|
|
6199
|
+
const diff = diagnosticsIndex === -1 ? rawDiff : rawDiff.slice(0, diagnosticsIndex);
|
|
6200
|
+
return diff.trim().length > 0 ? diff : undefined;
|
|
5993
6201
|
}
|
|
5994
6202
|
function diffViewMode(width = 80) {
|
|
5995
6203
|
return width > 120 ? "split" : "unified";
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bubblebrain-ai/bubble",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "A terminal coding agent",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20.0.0"
|
|
8
|
+
},
|
|
6
9
|
"bin": {
|
|
7
|
-
"bubble": "dist/
|
|
10
|
+
"bubble": "dist/bin.js"
|
|
8
11
|
},
|
|
9
12
|
"files": [
|
|
10
13
|
"dist",
|
|
@@ -13,7 +16,7 @@
|
|
|
13
16
|
"!dist/**/*.test.d.ts"
|
|
14
17
|
],
|
|
15
18
|
"scripts": {
|
|
16
|
-
"build": "rm -rf dist && tsc && chmod +x dist/main.js",
|
|
19
|
+
"build": "rm -rf dist && tsc && chmod +x dist/bin.js dist/main.js",
|
|
17
20
|
"dev": "tsc && bun dist/main.js",
|
|
18
21
|
"prepack": "npm run build",
|
|
19
22
|
"start": "bun dist/main.js",
|