@bubblebrain-ai/bubble 0.0.7 → 0.0.9

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.
Files changed (119) hide show
  1. package/dist/agent/categories.d.ts +34 -0
  2. package/dist/agent/categories.js +98 -0
  3. package/dist/agent/profiles.d.ts +4 -0
  4. package/dist/agent/profiles.js +2 -3
  5. package/dist/agent/subagent-control.d.ts +5 -0
  6. package/dist/agent/subagent-control.js +4 -0
  7. package/dist/agent/subagent-lifecycle-reminder.d.ts +3 -0
  8. package/dist/agent/subagent-lifecycle-reminder.js +102 -0
  9. package/dist/agent/subagent-route-format.d.ts +8 -0
  10. package/dist/agent/subagent-route-format.js +18 -0
  11. package/dist/agent/subtask-policy.d.ts +0 -1
  12. package/dist/agent/subtask-policy.js +0 -4
  13. package/dist/agent.d.ts +18 -0
  14. package/dist/agent.js +188 -16
  15. package/dist/config.d.ts +23 -3
  16. package/dist/config.js +59 -6
  17. package/dist/context/budget.d.ts +3 -2
  18. package/dist/context/budget.js +29 -15
  19. package/dist/context/compact.d.ts +23 -0
  20. package/dist/context/compact.js +129 -0
  21. package/dist/context/llm-compactor.d.ts +19 -0
  22. package/dist/context/llm-compactor.js +200 -0
  23. package/dist/context/projector.js +28 -12
  24. package/dist/context/token-estimator.d.ts +14 -0
  25. package/dist/context/token-estimator.js +106 -0
  26. package/dist/context/tool-output-truncate.d.ts +8 -0
  27. package/dist/context/tool-output-truncate.js +59 -0
  28. package/dist/context/usage.d.ts +34 -0
  29. package/dist/context/usage.js +213 -0
  30. package/dist/diff-stats.d.ts +5 -0
  31. package/dist/diff-stats.js +21 -0
  32. package/dist/main.js +68 -7
  33. package/dist/mcp/transports.d.ts +1 -0
  34. package/dist/mcp/transports.js +8 -0
  35. package/dist/model-catalog.d.ts +9 -0
  36. package/dist/model-catalog.js +17 -1
  37. package/dist/orchestrator/default-hooks.js +24 -18
  38. package/dist/prompt/compose.js +2 -1
  39. package/dist/prompt/provider-prompts/kimi.js +3 -1
  40. package/dist/provider-openai-codex.d.ts +13 -2
  41. package/dist/provider-openai-codex.js +81 -32
  42. package/dist/provider-registry.js +22 -6
  43. package/dist/provider-transform.d.ts +3 -1
  44. package/dist/provider-transform.js +15 -0
  45. package/dist/provider.d.ts +4 -1
  46. package/dist/provider.js +89 -4
  47. package/dist/reasoning-debug.d.ts +7 -0
  48. package/dist/reasoning-debug.js +30 -0
  49. package/dist/session-log.js +13 -2
  50. package/dist/session-types.d.ts +1 -1
  51. package/dist/slash-commands/commands.js +60 -2
  52. package/dist/slash-commands/types.d.ts +7 -0
  53. package/dist/tools/agent-lifecycle.js +22 -4
  54. package/dist/tools/edit.js +7 -2
  55. package/dist/tools/file-state.d.ts +19 -0
  56. package/dist/tools/file-state.js +15 -0
  57. package/dist/tools/glob.js +2 -1
  58. package/dist/tools/grep.js +2 -2
  59. package/dist/tools/lsp.js +2 -2
  60. package/dist/tools/path-utils.d.ts +2 -0
  61. package/dist/tools/path-utils.js +16 -0
  62. package/dist/tools/read.d.ts +1 -1
  63. package/dist/tools/read.js +207 -14
  64. package/dist/tools/write.js +3 -2
  65. package/dist/tui/escape-confirmation.d.ts +15 -0
  66. package/dist/tui/escape-confirmation.js +30 -0
  67. package/dist/tui/run.js +93 -23
  68. package/dist/tui-ink/app.d.ts +52 -0
  69. package/dist/tui-ink/app.js +1129 -0
  70. package/dist/tui-ink/approval/approval-dialog.d.ts +13 -0
  71. package/dist/tui-ink/approval/approval-dialog.js +132 -0
  72. package/dist/tui-ink/approval/diff-view.d.ts +7 -0
  73. package/dist/tui-ink/approval/diff-view.js +44 -0
  74. package/dist/tui-ink/approval/select.d.ts +35 -0
  75. package/dist/tui-ink/approval/select.js +88 -0
  76. package/dist/tui-ink/code-highlight.d.ts +8 -0
  77. package/dist/tui-ink/code-highlight.js +122 -0
  78. package/dist/tui-ink/detect-theme.d.ts +19 -0
  79. package/dist/tui-ink/detect-theme.js +123 -0
  80. package/dist/tui-ink/display-history.d.ts +38 -0
  81. package/dist/tui-ink/display-history.js +130 -0
  82. package/dist/tui-ink/edit-diff.d.ts +11 -0
  83. package/dist/tui-ink/edit-diff.js +52 -0
  84. package/dist/tui-ink/file-mentions.d.ts +29 -0
  85. package/dist/tui-ink/file-mentions.js +174 -0
  86. package/dist/tui-ink/footer.d.ts +19 -0
  87. package/dist/tui-ink/footer.js +45 -0
  88. package/dist/tui-ink/image-paste.d.ts +54 -0
  89. package/dist/tui-ink/image-paste.js +288 -0
  90. package/dist/tui-ink/input-box.d.ts +41 -0
  91. package/dist/tui-ink/input-box.js +694 -0
  92. package/dist/tui-ink/input-history.d.ts +16 -0
  93. package/dist/tui-ink/input-history.js +81 -0
  94. package/dist/tui-ink/markdown.d.ts +38 -0
  95. package/dist/tui-ink/markdown.js +394 -0
  96. package/dist/tui-ink/message-list.d.ts +33 -0
  97. package/dist/tui-ink/message-list.js +667 -0
  98. package/dist/tui-ink/model-picker.d.ts +43 -0
  99. package/dist/tui-ink/model-picker.js +331 -0
  100. package/dist/tui-ink/plan-confirm.d.ts +7 -0
  101. package/dist/tui-ink/plan-confirm.js +105 -0
  102. package/dist/tui-ink/question-dialog.d.ts +8 -0
  103. package/dist/tui-ink/question-dialog.js +99 -0
  104. package/dist/tui-ink/recent-activity.d.ts +8 -0
  105. package/dist/tui-ink/recent-activity.js +71 -0
  106. package/dist/tui-ink/run.d.ts +37 -0
  107. package/dist/tui-ink/run.js +53 -0
  108. package/dist/tui-ink/theme.d.ts +66 -0
  109. package/dist/tui-ink/theme.js +115 -0
  110. package/dist/tui-ink/todos.d.ts +7 -0
  111. package/dist/tui-ink/todos.js +46 -0
  112. package/dist/tui-ink/trace-groups.d.ts +27 -0
  113. package/dist/tui-ink/trace-groups.js +389 -0
  114. package/dist/tui-ink/use-terminal-size.d.ts +4 -0
  115. package/dist/tui-ink/use-terminal-size.js +21 -0
  116. package/dist/tui-ink/welcome.d.ts +18 -0
  117. package/dist/tui-ink/welcome.js +138 -0
  118. package/dist/types.d.ts +10 -0
  119. package/package.json +7 -1
@@ -0,0 +1,130 @@
1
+ let __displayMessageCounter = 0;
2
+ export function nextDisplayMessageKey(prefix = "msg") {
3
+ __displayMessageCounter += 1;
4
+ return `${prefix}-${__displayMessageCounter}`;
5
+ }
6
+ export function appendTextPart(parts, content) {
7
+ if (!content)
8
+ return;
9
+ const last = parts[parts.length - 1];
10
+ if (last?.type === "text") {
11
+ last.content += content;
12
+ }
13
+ else {
14
+ parts.push({ type: "text", content });
15
+ }
16
+ }
17
+ export function appendToolPart(parts, toolCall) {
18
+ const last = parts[parts.length - 1];
19
+ if (last?.type === "tools") {
20
+ last.toolCalls.push(toolCall);
21
+ }
22
+ else {
23
+ parts.push({ type: "tools", toolCalls: [toolCall] });
24
+ }
25
+ }
26
+ export function snapshotDisplayParts(parts) {
27
+ return parts.map((part) => {
28
+ if (part.type === "text") {
29
+ return { ...part };
30
+ }
31
+ return {
32
+ type: "tools",
33
+ toolCalls: part.toolCalls.map(cloneToolCall),
34
+ };
35
+ });
36
+ }
37
+ export function contentFromParts(parts) {
38
+ return parts
39
+ .filter((part) => part.type === "text")
40
+ .map((part) => part.content)
41
+ .join("");
42
+ }
43
+ export function toolCallsFromParts(parts) {
44
+ return parts.flatMap((part) => part.type === "tools" ? part.toolCalls : []);
45
+ }
46
+ const MAX_VISIBLE_MESSAGES = 80;
47
+ const FULL_DETAIL_WINDOW = 24;
48
+ const MAX_OLD_CONTENT_CHARS = 1200;
49
+ const MAX_OLD_REASONING_CHARS = 600;
50
+ const MAX_OLD_TOOL_RESULT_CHARS = 800;
51
+ export function compactDisplayMessages(messages) {
52
+ if (messages.length === 0) {
53
+ return messages;
54
+ }
55
+ let hiddenCount = 0;
56
+ const withoutSynthetic = messages.filter((message) => {
57
+ if (message.syntheticKind !== "ui_summary") {
58
+ return true;
59
+ }
60
+ hiddenCount += message.hiddenCount ?? 0;
61
+ return false;
62
+ });
63
+ const overflow = Math.max(0, withoutSynthetic.length - MAX_VISIBLE_MESSAGES);
64
+ hiddenCount += overflow;
65
+ const visible = overflow > 0 ? withoutSynthetic.slice(overflow) : withoutSynthetic;
66
+ const detailStart = Math.max(0, visible.length - FULL_DETAIL_WINDOW);
67
+ const compacted = visible.map((message, index) => (index < detailStart ? compactDisplayMessage(message) : message));
68
+ if (hiddenCount === 0) {
69
+ return compacted;
70
+ }
71
+ return [buildUiSummary(hiddenCount), ...compacted];
72
+ }
73
+ function compactDisplayMessage(message) {
74
+ if (message.syntheticKind === "ui_summary") {
75
+ return message;
76
+ }
77
+ return {
78
+ ...message,
79
+ content: truncateText(message.content, MAX_OLD_CONTENT_CHARS),
80
+ reasoning: message.reasoning
81
+ ? truncateText(message.reasoning, MAX_OLD_REASONING_CHARS)
82
+ : message.reasoning,
83
+ toolCalls: message.toolCalls?.map(compactToolCall),
84
+ parts: message.parts?.map(compactDisplayPart),
85
+ };
86
+ }
87
+ function buildUiSummary(hiddenCount) {
88
+ return {
89
+ key: "synthetic-ui-summary",
90
+ role: "assistant",
91
+ content: `[Earlier UI history compacted to control memory: ${hiddenCount} message${hiddenCount === 1 ? "" : "s"} hidden]`,
92
+ syntheticKind: "ui_summary",
93
+ hiddenCount,
94
+ };
95
+ }
96
+ function truncateText(value, maxChars) {
97
+ if (value.length <= maxChars) {
98
+ return value;
99
+ }
100
+ const head = Math.max(1, Math.floor(maxChars * 0.7));
101
+ const tail = Math.max(1, maxChars - head - 32);
102
+ const omitted = value.length - head - tail;
103
+ return `${value.slice(0, head)}\n...[${omitted} chars omitted for UI]...\n${value.slice(-tail)}`;
104
+ }
105
+ function cloneToolCall(toolCall) {
106
+ return {
107
+ ...toolCall,
108
+ args: { ...toolCall.args },
109
+ };
110
+ }
111
+ function compactDisplayPart(part) {
112
+ if (part.type === "text") {
113
+ return {
114
+ ...part,
115
+ content: truncateText(part.content, MAX_OLD_CONTENT_CHARS),
116
+ };
117
+ }
118
+ return {
119
+ type: "tools",
120
+ toolCalls: part.toolCalls.map(compactToolCall),
121
+ };
122
+ }
123
+ function compactToolCall(toolCall) {
124
+ return {
125
+ ...toolCall,
126
+ result: toolCall.result
127
+ ? truncateText(toolCall.result, MAX_OLD_TOOL_RESULT_CHARS)
128
+ : toolCall.result,
129
+ };
130
+ }
@@ -0,0 +1,11 @@
1
+ import type { DisplayToolCall } from "./display-history.js";
2
+ export declare const EDIT_COLLAPSED_DIFF_LINES = 20;
3
+ export interface EditDiffDetails {
4
+ diff: string;
5
+ added: number;
6
+ removed: number;
7
+ path?: string;
8
+ }
9
+ export declare function getEditDiffDetails(tool: DisplayToolCall): EditDiffDetails | null;
10
+ export declare function formatEditSuccessSummary(details: EditDiffDetails | null): string;
11
+ export declare function formatEditStats(added: number, removed: number): string;
@@ -0,0 +1,52 @@
1
+ import { countUnifiedDiffChanges } from "../diff-stats.js";
2
+ export const EDIT_COLLAPSED_DIFF_LINES = 20;
3
+ export function getEditDiffDetails(tool) {
4
+ if (tool.name !== "edit" || tool.isError)
5
+ return null;
6
+ const metadata = tool.metadata;
7
+ const metadataDiff = readMetadataString(metadata, "diff");
8
+ const diff = metadataDiff ?? extractDiffFromResult(tool.result);
9
+ if (!diff)
10
+ return null;
11
+ const counted = countUnifiedDiffChanges(diff);
12
+ const added = readMetadataNumber(metadata, "addedLines") ?? counted.added;
13
+ const removed = readMetadataNumber(metadata, "removedLines") ?? counted.removed;
14
+ const path = readMetadataString(metadata, "path")
15
+ ?? (typeof tool.args.path === "string" ? tool.args.path : undefined);
16
+ return { diff, added, removed, path };
17
+ }
18
+ export function formatEditSuccessSummary(details) {
19
+ const stats = details ? formatEditStats(details.added, details.removed) : "";
20
+ return `Succeeded. File edited.${stats ? ` ${stats}` : ""}`;
21
+ }
22
+ export function formatEditStats(added, removed) {
23
+ const parts = [];
24
+ if (added > 0)
25
+ parts.push(`+${added} added`);
26
+ if (removed > 0)
27
+ parts.push(`-${removed} removed`);
28
+ if (parts.length === 0)
29
+ return "";
30
+ return `(${parts.join(", ")})`;
31
+ }
32
+ function extractDiffFromResult(result) {
33
+ if (!result)
34
+ return null;
35
+ const normalized = result.replace(/\r\n/g, "\n");
36
+ const marker = "\nDiff:\n";
37
+ const index = normalized.indexOf(marker);
38
+ if (index === -1)
39
+ return null;
40
+ const rawDiff = normalized.slice(index + marker.length);
41
+ const diagnosticsIndex = rawDiff.search(/\n\nLSP diagnostics in /);
42
+ const diff = diagnosticsIndex === -1 ? rawDiff : rawDiff.slice(0, diagnosticsIndex);
43
+ return diff.trim().length > 0 ? diff : null;
44
+ }
45
+ function readMetadataString(metadata, key) {
46
+ const value = metadata?.[key];
47
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
48
+ }
49
+ function readMetadataNumber(metadata, key) {
50
+ const value = metadata?.[key];
51
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
52
+ }
@@ -0,0 +1,29 @@
1
+ export interface AtContext {
2
+ start: number;
3
+ end: number;
4
+ query: string;
5
+ }
6
+ export interface FileSuggestion {
7
+ path: string;
8
+ score: number;
9
+ }
10
+ export interface ExpandedMention {
11
+ path: string;
12
+ bytes: number;
13
+ truncated: boolean;
14
+ }
15
+ export interface ExpandResult {
16
+ text: string;
17
+ expanded: ExpandedMention[];
18
+ missing: string[];
19
+ skipped: Array<{
20
+ path: string;
21
+ reason: string;
22
+ bytes?: number;
23
+ }>;
24
+ }
25
+ export declare function findAtContext(text: string, cursor: number): AtContext | null;
26
+ export declare function filterFileSuggestions(files: string[], query: string, limit?: number): FileSuggestion[];
27
+ export declare function listProjectFiles(cwd: string): Promise<string[]>;
28
+ export declare function invalidateFileListCache(cwd?: string): void;
29
+ export declare function expandAtMentions(text: string, cwd: string): Promise<ExpandResult>;
@@ -0,0 +1,174 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promises as fs } from "node:fs";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ const execFileAsync = promisify(execFile);
6
+ const MAX_INLINE_BYTES = 200 * 1024;
7
+ const IGNORED_DIRS = new Set([".git", "node_modules", "dist", "build", ".next", ".turbo", ".cache"]);
8
+ const fileListCache = new Map();
9
+ export function findAtContext(text, cursor) {
10
+ const before = text.slice(0, cursor);
11
+ const at = before.lastIndexOf("@");
12
+ if (at === -1)
13
+ return null;
14
+ const prev = at === 0 ? "" : before[at - 1];
15
+ if (prev !== "" && !/\s/.test(prev))
16
+ return null;
17
+ const query = before.slice(at + 1);
18
+ if (/\s/.test(query))
19
+ return null;
20
+ return { start: at, end: cursor, query };
21
+ }
22
+ export function filterFileSuggestions(files, query, limit = 20) {
23
+ const q = query.toLowerCase();
24
+ if (q.length === 0) {
25
+ return files.slice(0, limit).map((p) => ({ path: p, score: 1 }));
26
+ }
27
+ const scored = [];
28
+ for (const file of files) {
29
+ const lower = file.toLowerCase();
30
+ const base = path.basename(lower);
31
+ let score = 0;
32
+ if (base.startsWith(q))
33
+ score = 100;
34
+ else if (lower.startsWith(q))
35
+ score = 80;
36
+ else if (base.includes(q))
37
+ score = 60;
38
+ else if (lower.includes(q))
39
+ score = 40;
40
+ if (score > 0)
41
+ scored.push({ path: file, score });
42
+ }
43
+ scored.sort((a, b) => (b.score - a.score) || (a.path.length - b.path.length) || a.path.localeCompare(b.path));
44
+ return scored.slice(0, limit);
45
+ }
46
+ export async function listProjectFiles(cwd) {
47
+ const cached = fileListCache.get(cwd);
48
+ if (cached)
49
+ return cached;
50
+ const files = await discoverFiles(cwd);
51
+ fileListCache.set(cwd, files);
52
+ return files;
53
+ }
54
+ export function invalidateFileListCache(cwd) {
55
+ if (cwd)
56
+ fileListCache.delete(cwd);
57
+ else
58
+ fileListCache.clear();
59
+ }
60
+ async function discoverFiles(cwd) {
61
+ try {
62
+ const { stdout } = await execFileAsync("git", ["ls-files", "-co", "--exclude-standard"], {
63
+ cwd,
64
+ maxBuffer: 32 * 1024 * 1024,
65
+ });
66
+ const files = stdout.split("\n").map((s) => s.trim()).filter(Boolean);
67
+ if (files.length > 0)
68
+ return files;
69
+ }
70
+ catch {
71
+ // Not a git repo or git unavailable — fall through to filesystem walk.
72
+ }
73
+ return walkFilesystem(cwd);
74
+ }
75
+ async function walkFilesystem(root) {
76
+ const results = [];
77
+ async function visit(dir, rel) {
78
+ let entries;
79
+ try {
80
+ entries = (await fs.readdir(dir, { withFileTypes: true }));
81
+ }
82
+ catch {
83
+ return;
84
+ }
85
+ for (const entry of entries) {
86
+ if (entry.name.startsWith(".") && entry.name !== ".env" && entry.name !== ".gitignore")
87
+ continue;
88
+ if (IGNORED_DIRS.has(entry.name))
89
+ continue;
90
+ const abs = path.join(dir, entry.name);
91
+ const relPath = rel ? `${rel}/${entry.name}` : entry.name;
92
+ if (entry.isDirectory()) {
93
+ await visit(abs, relPath);
94
+ }
95
+ else if (entry.isFile()) {
96
+ results.push(relPath);
97
+ }
98
+ }
99
+ }
100
+ await visit(root, "");
101
+ return results;
102
+ }
103
+ const MENTION_REGEX = /(^|\s)@([^\s]+)/g;
104
+ export async function expandAtMentions(text, cwd) {
105
+ const result = { text, expanded: [], missing: [], skipped: [] };
106
+ const mentions = Array.from(text.matchAll(MENTION_REGEX));
107
+ if (mentions.length === 0)
108
+ return result;
109
+ const blocks = [];
110
+ const seen = new Set();
111
+ for (const match of mentions) {
112
+ const token = match[2];
113
+ if (seen.has(token))
114
+ continue;
115
+ seen.add(token);
116
+ const abs = path.resolve(cwd, token);
117
+ if (!abs.startsWith(path.resolve(cwd))) {
118
+ result.skipped.push({ path: token, reason: "outside project" });
119
+ continue;
120
+ }
121
+ let stat;
122
+ try {
123
+ stat = await fs.stat(abs);
124
+ }
125
+ catch {
126
+ result.missing.push(token);
127
+ continue;
128
+ }
129
+ if (!stat.isFile()) {
130
+ result.skipped.push({ path: token, reason: "not a file" });
131
+ continue;
132
+ }
133
+ if (stat.size > MAX_INLINE_BYTES) {
134
+ result.skipped.push({ path: token, reason: "too large", bytes: stat.size });
135
+ blocks.push(`### @${token}\n(${formatBytes(stat.size)}, exceeds inline limit of ${formatBytes(MAX_INLINE_BYTES)} — use the Read tool to access)`);
136
+ continue;
137
+ }
138
+ let contents;
139
+ try {
140
+ contents = await fs.readFile(abs, "utf8");
141
+ }
142
+ catch (err) {
143
+ result.skipped.push({ path: token, reason: `read failed: ${err.message || String(err)}` });
144
+ continue;
145
+ }
146
+ result.expanded.push({ path: token, bytes: stat.size, truncated: false });
147
+ const lang = guessLanguage(token);
148
+ blocks.push(`### @${token}\n\`\`\`${lang}\n${contents}\n\`\`\``);
149
+ }
150
+ if (blocks.length === 0)
151
+ return result;
152
+ result.text = `${text}\n\n---\nReferenced files:\n\n${blocks.join("\n\n")}`;
153
+ return result;
154
+ }
155
+ function formatBytes(n) {
156
+ if (n < 1024)
157
+ return `${n}B`;
158
+ if (n < 1024 * 1024)
159
+ return `${(n / 1024).toFixed(1)}KB`;
160
+ return `${(n / 1024 / 1024).toFixed(1)}MB`;
161
+ }
162
+ function guessLanguage(filePath) {
163
+ const ext = path.extname(filePath).slice(1).toLowerCase();
164
+ const map = {
165
+ ts: "ts", tsx: "tsx", js: "js", jsx: "jsx",
166
+ py: "python", rb: "ruby", go: "go", rs: "rust",
167
+ java: "java", kt: "kotlin", swift: "swift",
168
+ c: "c", h: "c", cpp: "cpp", cc: "cpp", hpp: "cpp",
169
+ cs: "csharp", php: "php", sh: "bash", zsh: "bash", bash: "bash",
170
+ json: "json", yaml: "yaml", yml: "yaml", toml: "toml", xml: "xml",
171
+ html: "html", css: "css", scss: "scss", sql: "sql", md: "markdown",
172
+ };
173
+ return map[ext] ?? "";
174
+ }
@@ -0,0 +1,19 @@
1
+ import type { PermissionMode } from "../types.js";
2
+ export interface FooterUsageTotals {
3
+ prompt: number;
4
+ completion: number;
5
+ }
6
+ export interface FooterData {
7
+ cwd: string;
8
+ providerId: string;
9
+ model: string;
10
+ thinkingLevel: string;
11
+ showThinking: boolean;
12
+ mode?: PermissionMode;
13
+ usageTotals: FooterUsageTotals;
14
+ verboseTrace?: boolean;
15
+ }
16
+ export declare function FooterBar({ data }: {
17
+ data: FooterData;
18
+ }): import("react/jsx-runtime").JSX.Element;
19
+ export declare function buildFooterData(input: FooterData): FooterData;
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { homedir } from "node:os";
4
+ import { useTheme } from "./theme.js";
5
+ import { PERMISSION_MODE_INFO } from "../permission/mode.js";
6
+ export function FooterBar({ data }) {
7
+ const theme = useTheme();
8
+ const usageText = data.usageTotals.prompt || data.usageTotals.completion
9
+ ? `↑${formatTokens(data.usageTotals.prompt)} ↓${formatTokens(data.usageTotals.completion)}`
10
+ : "";
11
+ const thinkingText = data.showThinking
12
+ ? data.thinkingLevel && data.thinkingLevel !== "off"
13
+ ? ` • ⌃R ${data.thinkingLevel}`
14
+ : " • ⌃R off"
15
+ : "";
16
+ return (_jsxs(Box, { paddingX: 1, flexShrink: 0, children: [_jsx(Text, { color: theme.muted, children: formatCwd(data.cwd) }), usageText && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: " " }), _jsx(Text, { color: theme.muted, dimColor: true, children: usageText })] })), _jsx(ModeBadge, { mode: data.mode }), _jsx(Box, { flexGrow: 1 }), _jsx(Text, { color: theme.muted, children: data.providerId }), _jsx(Text, { color: theme.muted, children: " \u2022 " }), _jsx(Text, { color: theme.toolName, children: data.model }), _jsx(Text, { color: theme.muted, dimColor: true, children: thinkingText })] }));
17
+ }
18
+ function ModeBadge({ mode }) {
19
+ const theme = useTheme();
20
+ if (!mode || mode === "default")
21
+ return null;
22
+ const info = PERMISSION_MODE_INFO[mode];
23
+ const color = theme[info.color] ?? theme.muted;
24
+ const symbol = info.symbol ? `${info.symbol} ` : "";
25
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: " " }), _jsxs(Text, { color: color, bold: true, children: [symbol, info.shortTitle, " on"] }), _jsx(Text, { color: theme.muted, children: " \u21E7\u21E5" })] }));
26
+ }
27
+ export function buildFooterData(input) {
28
+ return input;
29
+ }
30
+ function formatTokens(count) {
31
+ if (count < 1000)
32
+ return String(count);
33
+ if (count < 10000)
34
+ return `${(count / 1000).toFixed(1)}k`;
35
+ if (count < 1000000)
36
+ return `${Math.round(count / 1000)}k`;
37
+ return `${(count / 1000000).toFixed(1)}M`;
38
+ }
39
+ function formatCwd(cwd) {
40
+ const home = homedir();
41
+ if (cwd.startsWith(home)) {
42
+ return `~${cwd.slice(home.length)}`;
43
+ }
44
+ return cwd;
45
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Image paste utilities: path detection, file reading, clipboard access, and size-capping.
3
+ *
4
+ * Terminals don't forward image bytes to stdin. Paths arrive as text when users
5
+ * drag files in; Cmd+V of an image produces an empty paste (we probe the
6
+ * clipboard). macOS screenshot shortcut (Cmd+Shift+Ctrl+4) writes to both a
7
+ * TemporaryItems path and the clipboard — the path often gets cleaned up before
8
+ * we can read it, so we fall back to the clipboard.
9
+ */
10
+ export interface ImageAttachment {
11
+ base64: string;
12
+ mediaType: string;
13
+ /** Raw byte size of the decoded image (not base64). */
14
+ bytes: number;
15
+ /** data:<mediaType>;base64,<...> — ready to send as image_url.url. */
16
+ dataUrl: string;
17
+ filename?: string;
18
+ sourcePath?: string;
19
+ }
20
+ export declare function isImageFilePath(raw: string): boolean;
21
+ /**
22
+ * Split a pasted blob into candidate path tokens.
23
+ *
24
+ * Multi-drag from Finder delivers a mix of newline- and space-separated
25
+ * absolute paths. Spaces inside a single path are escaped (`\ `) — we split
26
+ * only on a space that is followed by the start of a new absolute path.
27
+ */
28
+ export declare function splitPastedPaths(pasted: string): string[];
29
+ export declare function readImageFromPath(rawPath: string): Promise<ImageAttachment | null>;
30
+ /** macOS screenshot shortcut writes to these paths and they may be auto-cleaned. */
31
+ export declare function isScreenshotTempPath(s: string): boolean;
32
+ export declare function getImageFromClipboard(): Promise<ImageAttachment | null>;
33
+ /**
34
+ * If the image is close to the API size cap, try to downscale it in place.
35
+ * Uses the OS-native tools that are typically available:
36
+ * - macOS: `sips` (always present)
37
+ * - linux: ImageMagick `convert` (if installed)
38
+ * Returns the original attachment if resize isn't needed or can't run.
39
+ */
40
+ export declare function maybeResizeImage(att: ImageAttachment): Promise<ImageAttachment>;
41
+ export interface ValidationResult {
42
+ ok: boolean;
43
+ reason?: string;
44
+ }
45
+ export declare function validateImageSize(att: ImageAttachment): ValidationResult;
46
+ /** End-to-end: given a file path, read -> resize-if-needed -> validate. */
47
+ export declare function ingestImagePath(p: string): Promise<{
48
+ attachment?: ImageAttachment;
49
+ error?: string;
50
+ }>;
51
+ export declare function ingestClipboardImage(): Promise<{
52
+ attachment?: ImageAttachment;
53
+ error?: string;
54
+ }>;