@bubblebrain-ai/bubble 0.0.17 → 0.0.18

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 (45) hide show
  1. package/dist/agent/tool-intent.js +0 -1
  2. package/dist/agent.d.ts +1 -0
  3. package/dist/agent.js +54 -21
  4. package/dist/context/prune.d.ts +1 -0
  5. package/dist/context/prune.js +32 -0
  6. package/dist/feishu/agent-host/run-driver.js +2 -2
  7. package/dist/feishu/card/run-state.js +1 -0
  8. package/dist/main.js +11 -9
  9. package/dist/model-pricing.js +2 -1
  10. package/dist/model-selection.d.ts +7 -0
  11. package/dist/model-selection.js +9 -0
  12. package/dist/network/chatgpt-transport.js +1 -0
  13. package/dist/orchestrator/default-hooks.js +1 -1
  14. package/dist/prompt/environment.js +1 -3
  15. package/dist/prompt/runtime.js +1 -1
  16. package/dist/provider-anthropic.d.ts +15 -3
  17. package/dist/provider-anthropic.js +55 -2
  18. package/dist/provider-openai-codex.js +3 -1
  19. package/dist/provider.js +1 -1
  20. package/dist/session-title.js +3 -6
  21. package/dist/slash-commands/commands.js +4 -0
  22. package/dist/stats/usage.d.ts +1 -0
  23. package/dist/stats/usage.js +28 -3
  24. package/dist/tools/edit.js +75 -1
  25. package/dist/tools/glob.js +77 -12
  26. package/dist/tools/index.d.ts +1 -1
  27. package/dist/tools/index.js +1 -3
  28. package/dist/tools/prompt-metadata.d.ts +3 -0
  29. package/dist/tools/prompt-metadata.js +17 -0
  30. package/dist/tools/write.js +14 -0
  31. package/dist/tui/paste-placeholder.d.ts +10 -0
  32. package/dist/tui/paste-placeholder.js +45 -0
  33. package/dist/tui/run.js +23 -0
  34. package/dist/tui-ink/app.js +2 -0
  35. package/dist/tui-ink/input-box.d.ts +1 -8
  36. package/dist/tui-ink/input-box.js +8 -38
  37. package/dist/tui-opentui/app.js +2 -0
  38. package/dist/tui-opentui/input-box.d.ts +1 -3
  39. package/dist/tui-opentui/input-box.js +17 -26
  40. package/dist/types.d.ts +9 -0
  41. package/package.json +7 -3
  42. package/dist/tools/apply-patch.d.ts +0 -9
  43. package/dist/tools/apply-patch.js +0 -330
  44. package/dist/tools/patch-apply.d.ts +0 -41
  45. package/dist/tools/patch-apply.js +0 -312
@@ -1,6 +1,5 @@
1
1
  import { normalizeSingleLine, truncateVisual } from "./text-display.js";
2
- const LONG_PASTE_CHAR_THRESHOLD = 1000;
3
- const LONG_PASTE_LINE_THRESHOLD = 20;
2
+ import { createPastedContentMarker, shouldCollapsePastedContent } from "./tui/paste-placeholder.js";
4
3
  const TITLE_INPUT_MAX_CHARS = 4000;
5
4
  const TITLE_MAX_WIDTH = 80;
6
5
  const TITLE_SYSTEM_PROMPT = [
@@ -81,10 +80,8 @@ export function deterministicTitleFromUserContent(content) {
81
80
  const text = userContentText(content);
82
81
  if (!text)
83
82
  return "User message";
84
- const charCount = text.length;
85
- const lineCount = text.split(/\r?\n/).length;
86
- if (charCount > LONG_PASTE_CHAR_THRESHOLD || lineCount > LONG_PASTE_LINE_THRESHOLD) {
87
- return `[Pasted Content ${charCount} chars]`;
83
+ if (shouldCollapsePastedContent(text)) {
84
+ return createPastedContentMarker(text);
88
85
  }
89
86
  return truncateVisual(normalizeSingleLine(text), TITLE_MAX_WIDTH) || "User message";
90
87
  }
@@ -61,6 +61,9 @@ function persistSelectedModel(model, ctx) {
61
61
  }
62
62
  function syncSystemPrompt(ctx, model) {
63
63
  const { providerId, modelId } = decodeModel(model);
64
+ const toolPromptOptions = typeof ctx.agent.getSystemPromptToolOptions === "function"
65
+ ? ctx.agent.getSystemPromptToolOptions()
66
+ : {};
64
67
  ctx.agent.setSystemPrompt(buildSystemPrompt({
65
68
  agentName: "Bubble",
66
69
  configuredProvider: providerId,
@@ -68,6 +71,7 @@ function syncSystemPrompt(ctx, model) {
68
71
  configuredModelId: model,
69
72
  thinkingLevel: ctx.agent.thinking,
70
73
  workingDir: ctx.cwd,
74
+ ...toolPromptOptions,
71
75
  memoryPrompt: buildMemoryPrompt(ctx.cwd),
72
76
  }));
73
77
  }
@@ -20,6 +20,7 @@ export interface ModelUsageStats {
20
20
  completionTokens: number;
21
21
  promptCacheHitTokens: number;
22
22
  promptCacheMissTokens: number;
23
+ cacheCreationTokens: number;
23
24
  reasoningTokens: number;
24
25
  totalTokens: number;
25
26
  cost?: number;
@@ -187,6 +187,7 @@ function addModelUsage(accumulator, model, message, usage) {
187
187
  completionTokens: 0,
188
188
  promptCacheHitTokens: 0,
189
189
  promptCacheMissTokens: 0,
190
+ cacheCreationTokens: 0,
190
191
  reasoningTokens: 0,
191
192
  totalTokens: 0,
192
193
  };
@@ -195,6 +196,7 @@ function addModelUsage(accumulator, model, message, usage) {
195
196
  existing.completionTokens += usage.completionTokens;
196
197
  existing.promptCacheHitTokens += usage.promptCacheHitTokens ?? 0;
197
198
  existing.promptCacheMissTokens += usage.promptCacheMissTokens ?? 0;
199
+ existing.cacheCreationTokens += usage.cacheCreationTokens ?? 0;
198
200
  existing.reasoningTokens += usage.reasoningTokens ?? 0;
199
201
  existing.totalTokens += tokenTotal(usage);
200
202
  if (providerId && modelId) {
@@ -284,6 +286,9 @@ function formatSummaryLines(stats, width) {
284
286
  if (favorite) {
285
287
  lines.push(` Favorite model ${truncate(favorite, Math.max(12, width - 17))}`);
286
288
  }
289
+ const cacheSummary = formatCacheSummary(stats.models);
290
+ if (cacheSummary)
291
+ lines.push(` ${cacheSummary}`);
287
292
  const trackedCostText = formatTrackedCosts(stats);
288
293
  if (trackedCostText)
289
294
  lines.push(` Tracked cost ${trackedCostText}`);
@@ -293,6 +298,17 @@ function formatSummaryLines(stats, width) {
293
298
  }
294
299
  return lines;
295
300
  }
301
+ function formatCacheSummary(models) {
302
+ const read = models.reduce((sum, model) => sum + model.promptCacheHitTokens, 0);
303
+ const create = models.reduce((sum, model) => sum + model.cacheCreationTokens, 0);
304
+ const missWithCreate = models.reduce((sum, model) => sum + model.promptCacheMissTokens, 0);
305
+ const miss = Math.max(0, missWithCreate - create);
306
+ const observed = read + create + miss;
307
+ if (observed === 0)
308
+ return undefined;
309
+ const hitRate = Math.round((read / observed) * 100);
310
+ return `Prompt cache ${formatCompactNumber(read)} read · ${formatCompactNumber(create)} create · ${formatCompactNumber(miss)} miss · ${hitRate}% hit`;
311
+ }
296
312
  function aggregateCosts(models) {
297
313
  const totals = {};
298
314
  for (const model of models) {
@@ -355,15 +371,24 @@ function normalizeUsage(raw) {
355
371
  if (!raw || typeof raw !== "object")
356
372
  return undefined;
357
373
  const value = raw;
358
- const promptTokens = numberValue(value.promptTokens) ?? numberValue(value.input_tokens);
374
+ const rawInputTokens = numberValue(value.input_tokens);
375
+ const cacheReadTokens = numberValue(value.promptCacheHitTokens) ?? numberValue(value.cache_read_input_tokens);
376
+ const cacheCreationTokens = numberValue(value.cacheCreationTokens) ?? numberValue(value.cache_creation_input_tokens);
377
+ const promptTokens = numberValue(value.promptTokens)
378
+ ?? (rawInputTokens !== undefined
379
+ ? rawInputTokens + (cacheReadTokens ?? 0) + (cacheCreationTokens ?? 0)
380
+ : undefined);
359
381
  const completionTokens = numberValue(value.completionTokens) ?? numberValue(value.output_tokens);
360
382
  if (promptTokens === undefined || completionTokens === undefined)
361
383
  return undefined;
362
384
  return {
363
385
  promptTokens,
364
386
  completionTokens,
365
- promptCacheHitTokens: numberValue(value.promptCacheHitTokens) ?? numberValue(value.cache_read_input_tokens),
366
- promptCacheMissTokens: numberValue(value.promptCacheMissTokens) ?? numberValue(value.cache_creation_input_tokens),
387
+ promptCacheHitTokens: cacheReadTokens,
388
+ promptCacheMissTokens: numberValue(value.promptCacheMissTokens)
389
+ ?? numberValue(value.cache_miss_input_tokens)
390
+ ?? (rawInputTokens !== undefined ? rawInputTokens + (cacheCreationTokens ?? 0) : undefined),
391
+ cacheCreationTokens,
367
392
  reasoningTokens: numberValue(value.reasoningTokens),
368
393
  totalTokens: numberValue(value.totalTokens) ?? numberValue(value.total_tokens),
369
394
  };
@@ -13,12 +13,66 @@ import { applyEditsToContent, EditApplyError, formatEditMatchNotes } from "./edi
13
13
  import { withFileMutationQueue } from "./file-mutation-queue.js";
14
14
  import { isWithinWorkspace } from "./file-state.js";
15
15
  import { resolveToolPath } from "./path-utils.js";
16
+ function prepareEditArguments(input) {
17
+ if (!input || typeof input !== "object")
18
+ return input;
19
+ const args = { ...input };
20
+ if (typeof args.file_path === "string" && typeof args.path !== "string") {
21
+ args.path = args.file_path;
22
+ }
23
+ if (typeof args.edits === "string") {
24
+ try {
25
+ const parsed = JSON.parse(args.edits);
26
+ if (Array.isArray(parsed))
27
+ args.edits = parsed;
28
+ }
29
+ catch {
30
+ // Keep the original value so validation surfaces the problem.
31
+ }
32
+ }
33
+ if (Array.isArray(args.edits)) {
34
+ args.edits = args.edits.map((edit) => {
35
+ if (!edit || typeof edit !== "object")
36
+ return edit;
37
+ const normalized = { ...edit };
38
+ if (typeof normalized.oldText !== "string") {
39
+ normalized.oldText = firstString(edit.old_text, edit.oldString, edit.old_string);
40
+ }
41
+ if (typeof normalized.newText !== "string") {
42
+ normalized.newText = firstString(edit.new_text, edit.newString, edit.new_string);
43
+ }
44
+ return normalized;
45
+ });
46
+ }
47
+ if (!Array.isArray(args.edits)) {
48
+ const oldText = firstString(args.oldText, args.old_text, args.oldString, args.old_string);
49
+ const newText = firstString(args.newText, args.new_text, args.newString, args.new_string);
50
+ if (typeof oldText === "string" && typeof newText === "string") {
51
+ args.edits = [{ oldText, newText }];
52
+ }
53
+ }
54
+ return args;
55
+ }
56
+ function firstString(...values) {
57
+ for (const value of values) {
58
+ if (typeof value === "string")
59
+ return value;
60
+ }
61
+ return undefined;
62
+ }
16
63
  export function createEditTool(cwd, approval, lsp, fileState) {
17
64
  return {
18
65
  name: "edit",
19
66
  effect: "write_direct",
20
67
  requiresApproval: true,
21
- description: "Apply targeted string replacements to a file. Prefer exact oldText copied from a recent read. The tool can tolerate common AI formatting mistakes such as extra leading/trailing whitespace, over-escaped sequences, line ending differences, indentation differences, trailing whitespace, Unicode punctuation/space, and blank-line differences when the target is unique.",
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 affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.",
69
+ promptSnippet: "Make precise file edits with exact text replacement, including multiple disjoint edits in one call",
70
+ promptGuidelines: [
71
+ "Use edit for precise changes; edits[].oldText should be copied from a recent read and must identify a unique target.",
72
+ "When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls.",
73
+ "Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.",
74
+ "Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.",
75
+ ],
22
76
  parameters: {
23
77
  type: "object",
24
78
  properties: {
@@ -38,7 +92,27 @@ export function createEditTool(cwd, approval, lsp, fileState) {
38
92
  },
39
93
  required: ["path", "edits"],
40
94
  },
95
+ prepareArguments: prepareEditArguments,
41
96
  async execute(args) {
97
+ if (!Array.isArray(args.edits)) {
98
+ return {
99
+ content: "Error: edit requires edits to be an array of { oldText, newText } replacements.",
100
+ isError: true,
101
+ status: "blocked",
102
+ metadata: { kind: "edit", reason: "invalid_args" },
103
+ };
104
+ }
105
+ for (let index = 0; index < args.edits.length; index++) {
106
+ const edit = args.edits[index];
107
+ if (!edit || typeof edit !== "object" || typeof edit.oldText !== "string" || typeof edit.newText !== "string") {
108
+ return {
109
+ content: `Error: edit requires edits[${index}] to contain string oldText and newText fields.`,
110
+ isError: true,
111
+ status: "blocked",
112
+ metadata: { kind: "edit", reason: "invalid_args", index },
113
+ };
114
+ }
115
+ }
42
116
  const filePath = resolveToolPath(cwd, args.path);
43
117
  if (!isWithinWorkspace(cwd, filePath)) {
44
118
  return {
@@ -2,10 +2,10 @@
2
2
  * Glob tool - discover files by path pattern without shell access.
3
3
  */
4
4
  import { readdir, stat } from "node:fs/promises";
5
- import { relative, resolve } from "node:path";
5
+ import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
6
6
  import picomatch from "picomatch";
7
7
  import { isSensitivePath } from "./sensitive-paths.js";
8
- import { resolveToolPath } from "./path-utils.js";
8
+ import { expandHomePath, resolveToolPath } from "./path-utils.js";
9
9
  const MAX_RESULTS = 100;
10
10
  const DEFAULT_IGNORES = new Set([
11
11
  ".git",
@@ -32,11 +32,16 @@ export function createGlobTool(cwd) {
32
32
  required: ["pattern"],
33
33
  },
34
34
  async execute(args, ctx) {
35
- const root = resolveToolPath(cwd, typeof args.path === "string" && args.path.trim() ? args.path : ".");
36
- const pattern = String(args.pattern || "").trim();
37
- if (!pattern) {
35
+ const requestedRoot = resolveToolPath(cwd, typeof args.path === "string" && args.path.trim() ? args.path : ".");
36
+ const originalPattern = String(args.pattern || "").trim();
37
+ if (!originalPattern) {
38
38
  return { content: "Error: glob pattern is required", isError: true, status: "command_error" };
39
39
  }
40
+ const normalized = normalizeGlobSearch(requestedRoot, originalPattern);
41
+ if (normalized.error) {
42
+ return normalized.error;
43
+ }
44
+ const { root, pattern, normalizedPattern } = normalized;
40
45
  if (isSensitivePath(root)) {
41
46
  return {
42
47
  content: `Error: Glob blocked for sensitive credential storage: ${root}`,
@@ -45,7 +50,9 @@ export function createGlobTool(cwd) {
45
50
  metadata: {
46
51
  kind: "security",
47
52
  path: root,
48
- pattern,
53
+ pattern: normalizedPattern,
54
+ originalPattern,
55
+ normalizedPattern,
49
56
  reason: "Sensitive credential storage is not searchable from general-purpose tasks.",
50
57
  },
51
58
  };
@@ -74,11 +81,13 @@ export function createGlobTool(cwd) {
74
81
  metadata: {
75
82
  kind: "search",
76
83
  path: root,
77
- pattern,
84
+ pattern: normalizedPattern,
85
+ originalPattern,
86
+ normalizedPattern,
78
87
  matches: 0,
79
88
  truncated: false,
80
- searchSignature: `glob:${root}:${pattern}`,
81
- searchFamily: `glob:${pattern}`,
89
+ searchSignature: `glob:${root}:${normalizedPattern}`,
90
+ searchFamily: `glob:${normalizedPattern}`,
82
91
  paths: [],
83
92
  },
84
93
  };
@@ -89,17 +98,70 @@ export function createGlobTool(cwd) {
89
98
  metadata: {
90
99
  kind: "search",
91
100
  path: root,
92
- pattern,
101
+ pattern: normalizedPattern,
102
+ originalPattern,
103
+ normalizedPattern,
93
104
  matches: matches.length,
94
105
  truncated: wasTruncated,
95
- searchSignature: `glob:${root}:${pattern}`,
96
- searchFamily: `glob:${pattern}`,
106
+ searchSignature: `glob:${root}:${normalizedPattern}`,
107
+ searchFamily: `glob:${normalizedPattern}`,
97
108
  paths: absoluteMatches,
98
109
  },
99
110
  };
100
111
  },
101
112
  };
102
113
  }
114
+ function normalizeGlobSearch(requestedRoot, originalPattern) {
115
+ const expandedPattern = expandGlobPatternHome(originalPattern);
116
+ const scan = picomatch.scan(expandedPattern);
117
+ const prefix = scan.prefix ?? "";
118
+ if (!isAbsolute(scan.base)) {
119
+ if (escapesSearchRoot(scan.base)) {
120
+ return {
121
+ error: {
122
+ content: `Error: Glob pattern must stay within the search path: ${originalPattern}`,
123
+ isError: true,
124
+ status: "command_error",
125
+ metadata: {
126
+ kind: "search",
127
+ path: requestedRoot,
128
+ pattern: originalPattern,
129
+ originalPattern,
130
+ normalizedPattern: originalPattern,
131
+ reason: "pattern_outside_search_path",
132
+ },
133
+ },
134
+ };
135
+ }
136
+ return { root: requestedRoot, pattern: originalPattern, normalizedPattern: originalPattern };
137
+ }
138
+ const absoluteBase = resolve(scan.base);
139
+ const patternRoot = scan.isGlob ? absoluteBase : dirname(absoluteBase);
140
+ const patternBody = scan.isGlob ? scan.glob : basename(absoluteBase);
141
+ const normalizedRoot = isWithinSearchRoot(requestedRoot, patternRoot) ? requestedRoot : patternRoot;
142
+ const relativeBase = toPosix(relative(normalizedRoot, patternRoot));
143
+ const normalizedBody = [relativeBase, patternBody].filter(Boolean).join("/");
144
+ const normalizedPattern = `${prefix}${normalizedBody}`;
145
+ return {
146
+ root: normalizedRoot,
147
+ pattern: normalizedPattern,
148
+ normalizedPattern,
149
+ };
150
+ }
151
+ function expandGlobPatternHome(pattern) {
152
+ if (pattern.startsWith("!")) {
153
+ return `!${expandHomePath(pattern.slice(1))}`;
154
+ }
155
+ return expandHomePath(pattern);
156
+ }
157
+ function escapesSearchRoot(base) {
158
+ const normalized = toPosix(base);
159
+ return normalized === ".." || normalized.startsWith("../");
160
+ }
161
+ function isWithinSearchRoot(root, target) {
162
+ const rel = relative(root, target);
163
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
164
+ }
103
165
  async function walk(root, dir, matcher, files, truncated, abortSignal) {
104
166
  if (abortSignal?.aborted || files.length >= MAX_RESULTS) {
105
167
  truncated.value = true;
@@ -115,6 +177,9 @@ async function walk(root, dir, matcher, files, truncated, abortSignal) {
115
177
  continue;
116
178
  }
117
179
  const absolute = resolve(dir, entry.name);
180
+ if (isSensitivePath(absolute)) {
181
+ continue;
182
+ }
118
183
  const rel = toPosix(relative(root, absolute));
119
184
  if (entry.isDirectory()) {
120
185
  await walk(root, absolute, matcher, files, truncated, abortSignal);
@@ -6,7 +6,7 @@ export { createBashTool } from "./bash.js";
6
6
  export { createManagedServerTools } from "./server.js";
7
7
  export { createWriteTool } from "./write.js";
8
8
  export { createEditTool } from "./edit.js";
9
- export { createApplyPatchTool } from "./apply-patch.js";
9
+ export { buildToolPromptOptions } from "./prompt-metadata.js";
10
10
  export { createGlobTool } from "./glob.js";
11
11
  export { createGrepTool } from "./grep.js";
12
12
  export { createLspTool } from "./lsp.js";
@@ -6,7 +6,7 @@ export { createBashTool } from "./bash.js";
6
6
  export { createManagedServerTools } from "./server.js";
7
7
  export { createWriteTool } from "./write.js";
8
8
  export { createEditTool } from "./edit.js";
9
- export { createApplyPatchTool } from "./apply-patch.js";
9
+ export { buildToolPromptOptions } from "./prompt-metadata.js";
10
10
  export { createGlobTool } from "./glob.js";
11
11
  export { createGrepTool } from "./grep.js";
12
12
  export { createLspTool } from "./lsp.js";
@@ -23,7 +23,6 @@ export { createMemoryReadSummaryTool, createMemorySearchTool } from "./memory.js
23
23
  import { createBashTool } from "./bash.js";
24
24
  import { createManagedServerTools } from "./server.js";
25
25
  import { createEditTool } from "./edit.js";
26
- import { createApplyPatchTool } from "./apply-patch.js";
27
26
  import { createExitPlanModeTool } from "./exit-plan-mode.js";
28
27
  import { createGlobTool } from "./glob.js";
29
28
  import { createGrepTool } from "./grep.js";
@@ -51,7 +50,6 @@ export function createAllTools(cwd, skillRegistry, options = {}) {
51
50
  ...createManagedServerTools(cwd, approval),
52
51
  createWriteTool(cwd, {}, approval, lsp, fileState),
53
52
  createEditTool(cwd, approval, lsp, fileState),
54
- createApplyPatchTool(cwd, approval, lsp, fileState),
55
53
  createGlobTool(cwd),
56
54
  createGrepTool(cwd),
57
55
  createLspTool(cwd, lsp, approval),
@@ -0,0 +1,3 @@
1
+ import type { SystemPromptOptions } from "../system-prompt.js";
2
+ import type { ToolRegistryEntry } from "../types.js";
3
+ export declare function buildToolPromptOptions(tools: ToolRegistryEntry[]): Pick<SystemPromptOptions, "tools" | "toolSnippets" | "guidelines">;
@@ -0,0 +1,17 @@
1
+ export function buildToolPromptOptions(tools) {
2
+ const toolSnippets = {};
3
+ const guidelines = [];
4
+ for (const tool of tools) {
5
+ if (tool.promptSnippet) {
6
+ toolSnippets[tool.name] = tool.promptSnippet;
7
+ }
8
+ for (const guideline of tool.promptGuidelines ?? []) {
9
+ guidelines.push(guideline);
10
+ }
11
+ }
12
+ return {
13
+ tools: tools.map((tool) => tool.name),
14
+ toolSnippets,
15
+ guidelines,
16
+ };
17
+ }
@@ -9,12 +9,25 @@ import { formatDiagnosticBlocks } from "../lsp/index.js";
9
9
  import { isWithinWorkspace } from "./file-state.js";
10
10
  import { withFileMutationQueue } from "./file-mutation-queue.js";
11
11
  import { resolveToolPath } from "./path-utils.js";
12
+ function prepareWriteArguments(input) {
13
+ if (!input || typeof input !== "object")
14
+ return input;
15
+ const args = { ...input };
16
+ if (typeof args.file_path === "string" && typeof args.path !== "string") {
17
+ args.path = args.file_path;
18
+ }
19
+ return args;
20
+ }
12
21
  export function createWriteTool(cwd, _options = {}, approval, lsp, fileState) {
13
22
  return {
14
23
  name: "write",
15
24
  effect: "write_direct",
16
25
  requiresApproval: true,
17
26
  description: "Write content to a file. Creates parent directories as needed. If the file already exists, this replaces the full file; use edit for small targeted changes.",
27
+ promptSnippet: "Create new files or intentionally rewrite complete files",
28
+ promptGuidelines: [
29
+ "Use write only for new files, generated files, or intentional complete rewrites. Use edit for targeted changes to existing files.",
30
+ ],
18
31
  parameters: {
19
32
  type: "object",
20
33
  properties: {
@@ -23,6 +36,7 @@ export function createWriteTool(cwd, _options = {}, approval, lsp, fileState) {
23
36
  },
24
37
  required: ["path", "content"],
25
38
  },
39
+ prepareArguments: prepareWriteArguments,
26
40
  async execute(args) {
27
41
  const filePath = resolveToolPath(cwd, args.path);
28
42
  if (!isWithinWorkspace(cwd, filePath)) {
@@ -0,0 +1,10 @@
1
+ export declare const LONG_PASTE_CHAR_THRESHOLD = 1000;
2
+ export declare const LONG_PASTE_LINE_THRESHOLD = 20;
3
+ export interface PastedContentReference {
4
+ marker: string;
5
+ content: string;
6
+ }
7
+ export declare function countTextLines(text: string): number;
8
+ export declare function shouldCollapsePastedContent(text: string): boolean;
9
+ export declare function createPastedContentMarker(content: string, index?: number): string;
10
+ export declare function expandPastedContentMarkers(displayText: string, references: PastedContentReference[]): string;
@@ -0,0 +1,45 @@
1
+ export const LONG_PASTE_CHAR_THRESHOLD = 1000;
2
+ export const LONG_PASTE_LINE_THRESHOLD = 20;
3
+ export function countTextLines(text) {
4
+ return text.length === 0 ? 0 : text.split(/\r?\n/).length;
5
+ }
6
+ export function shouldCollapsePastedContent(text) {
7
+ if (text.length >= LONG_PASTE_CHAR_THRESHOLD)
8
+ return true;
9
+ return countTextLines(text) >= LONG_PASTE_LINE_THRESHOLD;
10
+ }
11
+ export function createPastedContentMarker(content, index = 1) {
12
+ const safeIndex = Math.max(1, Math.floor(index));
13
+ const lineCount = countTextLines(content);
14
+ const size = lineCount > 1
15
+ ? `${lineCount} ${lineCount === 1 ? "line" : "lines"}`
16
+ : `${content.length} ${content.length === 1 ? "char" : "chars"}`;
17
+ return `[Pasted text #${safeIndex} +${size}]`;
18
+ }
19
+ export function expandPastedContentMarkers(displayText, references) {
20
+ if (references.length === 0 || displayText.length === 0)
21
+ return displayText;
22
+ let expanded = "";
23
+ let index = 0;
24
+ const used = new Set();
25
+ while (index < displayText.length) {
26
+ let matched = -1;
27
+ for (let i = 0; i < references.length; i++) {
28
+ const ref = references[i];
29
+ if (!used.has(i) && displayText.startsWith(ref.marker, index)) {
30
+ matched = i;
31
+ break;
32
+ }
33
+ }
34
+ if (matched >= 0) {
35
+ const ref = references[matched];
36
+ expanded += ref.content;
37
+ index += ref.marker.length;
38
+ used.add(matched);
39
+ continue;
40
+ }
41
+ expanded += displayText[index];
42
+ index += 1;
43
+ }
44
+ return expanded;
45
+ }
package/dist/tui/run.js CHANGED
@@ -480,6 +480,7 @@ function OpenTuiApp(props) {
480
480
  completionTokens: 0,
481
481
  promptCacheHitTokens: 0,
482
482
  promptCacheMissTokens: 0,
483
+ cacheCreationTokens: 0,
483
484
  reasoningTokens: 0,
484
485
  turns: 0,
485
486
  });
@@ -602,6 +603,7 @@ function OpenTuiApp(props) {
602
603
  let sidebarGaugeText;
603
604
  let sidebarGaugeLabelText;
604
605
  let sidebarUsageText;
606
+ let sidebarCacheText;
605
607
  let sidebarReasoningText;
606
608
  let sidebarCostText;
607
609
  let sidebarLspSummaryText;
@@ -956,6 +958,7 @@ function OpenTuiApp(props) {
956
958
  setSidebarText(sidebarUsageText, context.turns > 0
957
959
  ? `${formatCompactNumber(context.promptTokens)} in · ${formatCompactNumber(context.completionTokens)} out`
958
960
  : "usage pending");
961
+ setSidebarText(sidebarCacheText, context.cacheText);
959
962
  setSidebarText(sidebarReasoningText, context.reasoningTokens > 0
960
963
  ? `${formatCompactNumber(context.reasoningTokens)} reasoning`
961
964
  : "");
@@ -5341,6 +5344,7 @@ function OpenTuiApp(props) {
5341
5344
  promptCacheHitTokens: current.promptCacheHitTokens + (event.usage.promptCacheHitTokens ?? 0),
5342
5345
  promptCacheMissTokens: current.promptCacheMissTokens + (event.usage.promptCacheMissTokens
5343
5346
  ?? (event.usage.promptCacheHitTokens === undefined ? event.usage.promptTokens : 0)),
5347
+ cacheCreationTokens: current.cacheCreationTokens + (event.usage.cacheCreationTokens ?? 0),
5344
5348
  reasoningTokens: current.reasoningTokens + (event.usage.reasoningTokens ?? 0),
5345
5349
  turns: current.turns + 1,
5346
5350
  }));
@@ -6368,6 +6372,13 @@ function OpenTuiApp(props) {
6368
6372
  : "usage pending";
6369
6373
  },
6370
6374
  }),
6375
+ h("text", {
6376
+ fg: theme.textMuted,
6377
+ ref: (ref) => {
6378
+ sidebarCacheText = ref;
6379
+ ref.content = context.cacheText;
6380
+ },
6381
+ }),
6371
6382
  h("text", {
6372
6383
  fg: theme.textMuted,
6373
6384
  ref: (ref) => {
@@ -6573,16 +6584,28 @@ function OpenTuiApp(props) {
6573
6584
  completionTokens: usage.completionTokens,
6574
6585
  promptCacheHitTokens: usage.promptCacheHitTokens,
6575
6586
  promptCacheMissTokens: usage.promptCacheMissTokens,
6587
+ cacheCreationTokens: usage.cacheCreationTokens,
6576
6588
  reasoningTokens: usage.reasoningTokens,
6577
6589
  totalTokens: usage.promptTokens + usage.completionTokens,
6578
6590
  };
6579
6591
  const cost = providerId && modelId ? calculateUsageCost(providerId, modelId, tokenUsage) : undefined;
6592
+ const cacheReadTokens = usage.promptCacheHitTokens;
6593
+ const cacheCreateTokens = usage.cacheCreationTokens;
6594
+ const cacheMissTokens = Math.max(0, usage.promptCacheMissTokens - cacheCreateTokens);
6595
+ const cacheObservedTokens = cacheReadTokens + cacheCreateTokens + cacheMissTokens;
6596
+ const cacheHitRate = cacheObservedTokens > 0
6597
+ ? Math.round((cacheReadTokens / cacheObservedTokens) * 100)
6598
+ : 0;
6599
+ const cacheText = cacheObservedTokens > 0
6600
+ ? `cache ${formatCompactNumber(cacheReadTokens)} read · ${formatCompactNumber(cacheCreateTokens)} create · ${formatCompactNumber(cacheMissTokens)} miss · ${cacheHitRate}% hit`
6601
+ : "";
6580
6602
  return {
6581
6603
  tokens: contextTokens,
6582
6604
  percent: contextPercent,
6583
6605
  remainingTokens,
6584
6606
  promptTokens: usage.promptTokens,
6585
6607
  completionTokens: usage.completionTokens,
6608
+ cacheText,
6586
6609
  reasoningTokens: usage.reasoningTokens,
6587
6610
  turns: usage.turns,
6588
6611
  costText: cost ? `${formatCurrency(cost.cost, cost.currency)} spent${cost.estimated ? " est." : ""}` : "cost unavailable",
@@ -501,6 +501,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
501
501
  thinkingLevel: overrides?.thinkingLevel ?? agent.thinking,
502
502
  mode: overrides?.mode ?? agent.mode,
503
503
  workingDir: args.cwd,
504
+ ...agent.getSystemPromptToolOptions(),
504
505
  }));
505
506
  }, [agent, args.cwd, safeRegistry, safeSkillRegistry]);
506
507
  useInput((input, key) => {
@@ -614,6 +615,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
614
615
  configuredModelId: model,
615
616
  thinkingLevel: agent.thinking,
616
617
  workingDir: args.cwd,
618
+ ...agent.getSystemPromptToolOptions(),
617
619
  }));
618
620
  userConfig.pushRecentModel(model);
619
621
  setThinkingLevel(agent.thinking);
@@ -1,5 +1,6 @@
1
1
  import type { SkillRegistry } from "../skills/registry.js";
2
2
  import { type ImageAttachment } from "./image-paste.js";
3
+ export { createPastedContentMarker, expandPastedContentMarkers, shouldCollapsePastedContent, type PastedContentReference, } from "../tui/paste-placeholder.js";
3
4
  export interface SubmitPayload {
4
5
  /** Fully-expanded text sent to the agent. */
5
6
  text: string;
@@ -19,10 +20,6 @@ interface InputBoxProps {
19
20
  terminalColumns: number;
20
21
  cwd: string;
21
22
  }
22
- export interface PastedContentReference {
23
- marker: string;
24
- content: string;
25
- }
26
23
  export declare function needsCursorRowCompensation(nextOutputHeight: number, viewportRows: number, previousOutputHeight: number | null): boolean;
27
24
  export declare function resolveCursorRowCompensation(input: {
28
25
  sameRenderedFrame: boolean;
@@ -58,8 +55,4 @@ export declare function insertNewlineAtCursor(text: string, cursor: number): {
58
55
  text: string;
59
56
  cursor: number;
60
57
  };
61
- export declare function shouldCollapsePastedContent(text: string): boolean;
62
- export declare function createPastedContentMarker(content: string): string;
63
- export declare function expandPastedContentMarkers(displayText: string, references: PastedContentReference[]): string;
64
58
  export declare function InputBox({ onSubmit, onPasteNotice, disabled, cursorResetEpoch, draftText, draftEpoch, onDraftApplied, skillRegistry, terminalColumns, cwd, }: InputBoxProps): import("react/jsx-runtime").JSX.Element;
65
- export {};