@bubblebrain-ai/bubble 0.0.12 → 0.0.14

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 (180) hide show
  1. package/dist/agent/execution-governor.js +1 -1
  2. package/dist/agent/input-controller.d.ts +11 -0
  3. package/dist/agent/input-controller.js +30 -0
  4. package/dist/agent/tool-intent.js +1 -0
  5. package/dist/agent.d.ts +8 -4
  6. package/dist/agent.js +623 -312
  7. package/dist/approval/controller.d.ts +1 -0
  8. package/dist/approval/controller.js +20 -3
  9. package/dist/approval/tool-helper.js +2 -0
  10. package/dist/approval/types.d.ts +14 -1
  11. package/dist/context/compact.js +9 -3
  12. package/dist/context/projector.js +27 -12
  13. package/dist/debug-trace.d.ts +27 -0
  14. package/dist/debug-trace.js +385 -0
  15. package/dist/feishu/agent-host/approval-card.js +9 -0
  16. package/dist/feishu/serve.js +7 -1
  17. package/dist/main.js +86 -9
  18. package/dist/model-catalog.js +1 -0
  19. package/dist/orchestrator/default-hooks.js +19 -8
  20. package/dist/orchestrator/hooks.d.ts +1 -0
  21. package/dist/prompt/environment.js +2 -0
  22. package/dist/prompt/reminders.d.ts +5 -6
  23. package/dist/prompt/reminders.js +8 -9
  24. package/dist/prompt/runtime.js +2 -2
  25. package/dist/provider-openai-codex.d.ts +7 -0
  26. package/dist/provider-openai-codex.js +265 -124
  27. package/dist/provider-registry.d.ts +2 -0
  28. package/dist/provider-registry.js +58 -9
  29. package/dist/provider.d.ts +3 -0
  30. package/dist/provider.js +5 -1
  31. package/dist/session-log.js +13 -1
  32. package/dist/slash-commands/commands.js +39 -0
  33. package/dist/slash-commands/types.d.ts +12 -0
  34. package/dist/stats/usage.d.ts +52 -0
  35. package/dist/stats/usage.js +414 -0
  36. package/dist/tools/apply-patch.d.ts +9 -0
  37. package/dist/tools/apply-patch.js +330 -0
  38. package/dist/tools/bash.js +205 -44
  39. package/dist/tools/edit-apply.d.ts +5 -2
  40. package/dist/tools/edit-apply.js +221 -31
  41. package/dist/tools/edit.js +12 -3
  42. package/dist/tools/file-mutation-queue.d.ts +1 -0
  43. package/dist/tools/file-mutation-queue.js +12 -1
  44. package/dist/tools/index.d.ts +2 -0
  45. package/dist/tools/index.js +7 -1
  46. package/dist/tools/patch-apply.d.ts +41 -0
  47. package/dist/tools/patch-apply.js +312 -0
  48. package/dist/tools/server-manager.d.ts +36 -0
  49. package/dist/tools/server-manager.js +234 -0
  50. package/dist/tools/server.d.ts +6 -0
  51. package/dist/tools/server.js +245 -0
  52. package/dist/tools/write.d.ts +3 -6
  53. package/dist/tools/write.js +26 -46
  54. package/dist/tui/clipboard.d.ts +1 -0
  55. package/dist/tui/clipboard.js +53 -0
  56. package/dist/tui/detect-theme.d.ts +2 -0
  57. package/dist/tui/detect-theme.js +87 -0
  58. package/dist/tui/display-history.d.ts +63 -0
  59. package/dist/tui/display-history.js +306 -0
  60. package/dist/tui/edit-diff.d.ts +11 -0
  61. package/dist/tui/edit-diff.js +57 -0
  62. package/dist/tui/escape-confirmation.d.ts +15 -0
  63. package/dist/tui/escape-confirmation.js +30 -0
  64. package/dist/tui/file-mentions.d.ts +29 -0
  65. package/dist/tui/file-mentions.js +174 -0
  66. package/dist/tui/global-key-router.d.ts +3 -0
  67. package/dist/tui/global-key-router.js +87 -0
  68. package/dist/tui/image-paste.d.ts +95 -0
  69. package/dist/tui/image-paste.js +505 -0
  70. package/dist/tui/input-history.d.ts +16 -0
  71. package/dist/tui/input-history.js +79 -0
  72. package/dist/tui/markdown-inline.d.ts +22 -0
  73. package/dist/tui/markdown-inline.js +68 -0
  74. package/dist/tui/markdown-theme-rules.d.ts +23 -0
  75. package/dist/tui/markdown-theme-rules.js +164 -0
  76. package/dist/tui/markdown-theme.d.ts +5 -0
  77. package/dist/tui/markdown-theme.js +27 -0
  78. package/dist/tui/model-picker-data.d.ts +10 -0
  79. package/dist/tui/model-picker-data.js +32 -0
  80. package/dist/tui/opencode-spinner.d.ts +22 -0
  81. package/dist/tui/opencode-spinner.js +216 -0
  82. package/dist/tui/prompt-keybindings.d.ts +42 -0
  83. package/dist/tui/prompt-keybindings.js +35 -0
  84. package/dist/tui/recent-activity.d.ts +8 -0
  85. package/dist/tui/recent-activity.js +71 -0
  86. package/dist/tui/render-signature.d.ts +1 -0
  87. package/dist/tui/render-signature.js +7 -0
  88. package/dist/tui/run.d.ts +45 -0
  89. package/dist/tui/run.js +9359 -0
  90. package/dist/tui/session-display.d.ts +6 -0
  91. package/dist/tui/session-display.js +12 -0
  92. package/dist/tui/sidebar-mcp.d.ts +31 -0
  93. package/dist/tui/sidebar-mcp.js +62 -0
  94. package/dist/tui/sidebar-state.d.ts +12 -0
  95. package/dist/tui/sidebar-state.js +69 -0
  96. package/dist/tui/streaming-tool-args.d.ts +15 -0
  97. package/dist/tui/streaming-tool-args.js +30 -0
  98. package/dist/tui/tool-renderers/fallback.d.ts +2 -0
  99. package/dist/tui/tool-renderers/fallback.js +75 -0
  100. package/dist/tui/tool-renderers/registry.d.ts +3 -0
  101. package/dist/tui/tool-renderers/registry.js +11 -0
  102. package/dist/tui/tool-renderers/subagent.d.ts +2 -0
  103. package/dist/tui/tool-renderers/subagent.js +135 -0
  104. package/dist/tui/tool-renderers/types.d.ts +36 -0
  105. package/dist/tui/tool-renderers/types.js +1 -0
  106. package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
  107. package/dist/tui/tool-renderers/write-preview.js +32 -0
  108. package/dist/tui/tool-renderers/write.d.ts +6 -0
  109. package/dist/tui/tool-renderers/write.js +88 -0
  110. package/dist/tui/trace-groups.d.ts +27 -0
  111. package/dist/tui/trace-groups.js +419 -0
  112. package/dist/tui/wordmark.d.ts +15 -0
  113. package/dist/tui/wordmark.js +179 -0
  114. package/dist/tui-ink/app.js +45 -9
  115. package/dist/tui-ink/approval/approval-dialog.js +7 -1
  116. package/dist/tui-ink/display-history.d.ts +1 -0
  117. package/dist/tui-ink/display-history.js +5 -4
  118. package/dist/tui-ink/message-list.js +23 -9
  119. package/dist/tui-ink/theme.d.ts +3 -9
  120. package/dist/tui-ink/theme.js +39 -45
  121. package/dist/tui-ink/trace-groups.js +1 -1
  122. package/dist/tui-ink/welcome.js +22 -78
  123. package/dist/tui-opentui/app.d.ts +54 -0
  124. package/dist/tui-opentui/app.js +1365 -0
  125. package/dist/tui-opentui/approval/approval-dialog.d.ts +15 -0
  126. package/dist/tui-opentui/approval/approval-dialog.js +145 -0
  127. package/dist/tui-opentui/approval/diff-view.d.ts +9 -0
  128. package/dist/tui-opentui/approval/diff-view.js +43 -0
  129. package/dist/tui-opentui/approval/select.d.ts +37 -0
  130. package/dist/tui-opentui/approval/select.js +91 -0
  131. package/dist/tui-opentui/detect-theme.d.ts +2 -0
  132. package/dist/tui-opentui/detect-theme.js +87 -0
  133. package/dist/tui-opentui/display-history.d.ts +56 -0
  134. package/dist/tui-opentui/display-history.js +130 -0
  135. package/dist/tui-opentui/edit-diff.d.ts +11 -0
  136. package/dist/tui-opentui/edit-diff.js +57 -0
  137. package/dist/tui-opentui/feedback-dialog.d.ts +21 -0
  138. package/dist/tui-opentui/feedback-dialog.js +164 -0
  139. package/dist/tui-opentui/feishu-setup-picker.d.ts +7 -0
  140. package/dist/tui-opentui/feishu-setup-picker.js +272 -0
  141. package/dist/tui-opentui/file-mentions.d.ts +29 -0
  142. package/dist/tui-opentui/file-mentions.js +174 -0
  143. package/dist/tui-opentui/footer.d.ts +26 -0
  144. package/dist/tui-opentui/footer.js +40 -0
  145. package/dist/tui-opentui/image-paste.d.ts +54 -0
  146. package/dist/tui-opentui/image-paste.js +288 -0
  147. package/dist/tui-opentui/input-box.d.ts +34 -0
  148. package/dist/tui-opentui/input-box.js +471 -0
  149. package/dist/tui-opentui/input-history.d.ts +16 -0
  150. package/dist/tui-opentui/input-history.js +79 -0
  151. package/dist/tui-opentui/markdown.d.ts +66 -0
  152. package/dist/tui-opentui/markdown.js +127 -0
  153. package/dist/tui-opentui/message-list.d.ts +31 -0
  154. package/dist/tui-opentui/message-list.js +128 -0
  155. package/dist/tui-opentui/model-picker.d.ts +63 -0
  156. package/dist/tui-opentui/model-picker.js +450 -0
  157. package/dist/tui-opentui/plan-confirm.d.ts +9 -0
  158. package/dist/tui-opentui/plan-confirm.js +124 -0
  159. package/dist/tui-opentui/question-dialog.d.ts +10 -0
  160. package/dist/tui-opentui/question-dialog.js +110 -0
  161. package/dist/tui-opentui/recent-activity.d.ts +8 -0
  162. package/dist/tui-opentui/recent-activity.js +71 -0
  163. package/dist/tui-opentui/run-session-picker.d.ts +10 -0
  164. package/dist/tui-opentui/run-session-picker.js +28 -0
  165. package/dist/tui-opentui/run.d.ts +38 -0
  166. package/dist/tui-opentui/run.js +48 -0
  167. package/dist/tui-opentui/session-picker.d.ts +12 -0
  168. package/dist/tui-opentui/session-picker.js +120 -0
  169. package/dist/tui-opentui/theme.d.ts +89 -0
  170. package/dist/tui-opentui/theme.js +157 -0
  171. package/dist/tui-opentui/todos.d.ts +9 -0
  172. package/dist/tui-opentui/todos.js +45 -0
  173. package/dist/tui-opentui/trace-groups.d.ts +27 -0
  174. package/dist/tui-opentui/trace-groups.js +419 -0
  175. package/dist/tui-opentui/use-terminal-size.d.ts +4 -0
  176. package/dist/tui-opentui/use-terminal-size.js +5 -0
  177. package/dist/tui-opentui/welcome.d.ts +25 -0
  178. package/dist/tui-opentui/welcome.js +77 -0
  179. package/dist/types.d.ts +36 -2
  180. package/package.json +5 -1
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Managed development server tools.
3
+ */
4
+ import { gateToolAction } from "../approval/tool-helper.js";
5
+ import { getManagedServer, getManagedServerLogs, listManagedServers, startManagedServer, stopManagedServer, } from "./server-manager.js";
6
+ export function createManagedServerTools(cwd, approval) {
7
+ return [
8
+ {
9
+ name: "start_server",
10
+ effect: "unknown",
11
+ requiresApproval: true,
12
+ description: "Start a long-running development server as a managed service. Use this instead of bash for npm run dev, next dev, vite, webpack --watch, or similar commands. Pass a foreground server command without a trailing '&'. The tool waits for readiness and then returns a server_id; use server_status, server_logs, and stop_server to manage it.",
13
+ parameters: {
14
+ type: "object",
15
+ properties: {
16
+ command: { type: "string", description: "Foreground server command to run, for example npm run dev" },
17
+ port: { type: "number", description: "Expected localhost port, used for readiness checks and conflict detection" },
18
+ readinessUrl: { type: "string", description: "URL to poll until the server is ready. Defaults to http://localhost:<port> when port is provided." },
19
+ timeout: { type: "number", description: "Seconds to wait for readiness. Defaults to 30." },
20
+ purpose: { type: "string", enum: ["preview", "verification"], description: "Why the server is being started." },
21
+ lifecycle: { type: "string", enum: ["auto", "keep_alive"], description: "auto stops at the end of the agent run; keep_alive remains available until stop_server or process exit." },
22
+ },
23
+ required: ["command"],
24
+ additionalProperties: false,
25
+ },
26
+ async execute(args, ctx) {
27
+ const command = String(args.command ?? "").trim();
28
+ if (!command) {
29
+ return {
30
+ content: "Error: command is required",
31
+ isError: true,
32
+ status: "blocked",
33
+ metadata: { kind: "server", reason: "missing_command" },
34
+ };
35
+ }
36
+ if (command.endsWith("&")) {
37
+ return {
38
+ content: "Error: start_server expects a foreground command. Remove the trailing '&' so Bubble can own the server lifecycle.",
39
+ isError: true,
40
+ status: "blocked",
41
+ metadata: { kind: "server", reason: "background_command" },
42
+ };
43
+ }
44
+ const gate = await gateToolAction(approval, { type: "bash", command, cwd });
45
+ if (!gate.approved)
46
+ return gate.result;
47
+ const purpose = parsePurpose(args.purpose);
48
+ const lifecycle = parseLifecycle(args.lifecycle, purpose);
49
+ const timeoutSec = typeof args.timeout === "number" && args.timeout > 0 ? args.timeout : 30;
50
+ try {
51
+ const server = await startManagedServer({
52
+ command,
53
+ cwd: ctx.cwd || cwd,
54
+ port: parsePort(args.port),
55
+ readinessUrl: typeof args.readinessUrl === "string" && args.readinessUrl.trim()
56
+ ? args.readinessUrl.trim()
57
+ : undefined,
58
+ timeoutSec,
59
+ ownerSessionId: ctx.sessionID,
60
+ ownerRunId: ctx.toolCall?.id,
61
+ purpose,
62
+ lifecycle,
63
+ });
64
+ return {
65
+ content: formatServerStarted(server),
66
+ status: "success",
67
+ metadata: serverMetadata(server),
68
+ };
69
+ }
70
+ catch (error) {
71
+ return {
72
+ content: `Error: ${error.message ?? String(error)}`,
73
+ isError: true,
74
+ status: "command_error",
75
+ metadata: {
76
+ kind: "server",
77
+ reason: "start_failed",
78
+ command,
79
+ port: parsePort(args.port),
80
+ },
81
+ };
82
+ }
83
+ },
84
+ },
85
+ {
86
+ name: "server_status",
87
+ readOnly: true,
88
+ effect: "read",
89
+ description: "Show managed development server status. Pass serverId for one server, or omit it to list all managed servers.",
90
+ parameters: {
91
+ type: "object",
92
+ properties: {
93
+ serverId: { type: "string", description: "Managed server id returned by start_server" },
94
+ },
95
+ additionalProperties: false,
96
+ },
97
+ async execute(args) {
98
+ const serverId = typeof args.serverId === "string" ? args.serverId.trim() : "";
99
+ if (serverId) {
100
+ const server = getManagedServer(serverId);
101
+ if (!server)
102
+ return missingServer(serverId);
103
+ return {
104
+ content: formatServer(server),
105
+ status: "success",
106
+ metadata: serverMetadata(server),
107
+ };
108
+ }
109
+ const servers = listManagedServers();
110
+ if (servers.length === 0) {
111
+ return {
112
+ content: "No managed servers.",
113
+ status: "no_match",
114
+ metadata: { kind: "server", count: 0 },
115
+ };
116
+ }
117
+ return {
118
+ content: servers.map(formatServer).join("\n\n"),
119
+ status: "success",
120
+ metadata: { kind: "server", count: servers.length },
121
+ };
122
+ },
123
+ },
124
+ {
125
+ name: "server_logs",
126
+ readOnly: true,
127
+ effect: "read",
128
+ description: "Return recent logs for a managed development server.",
129
+ parameters: {
130
+ type: "object",
131
+ properties: {
132
+ serverId: { type: "string", description: "Managed server id returned by start_server" },
133
+ maxCharacters: { type: "number", description: "Maximum log characters to return. Defaults to 12000." },
134
+ },
135
+ required: ["serverId"],
136
+ additionalProperties: false,
137
+ },
138
+ async execute(args) {
139
+ const serverId = String(args.serverId ?? "").trim();
140
+ if (!serverId)
141
+ return missingServer(serverId);
142
+ const maxChars = typeof args.maxCharacters === "number" && args.maxCharacters > 0
143
+ ? Math.min(args.maxCharacters, 50000)
144
+ : 12000;
145
+ const logs = getManagedServerLogs(serverId, maxChars);
146
+ if (logs === undefined)
147
+ return missingServer(serverId);
148
+ return {
149
+ content: logs.trim() || "(no logs captured)",
150
+ status: "success",
151
+ metadata: { kind: "server", serverId, maxCharacters: maxChars },
152
+ };
153
+ },
154
+ },
155
+ {
156
+ name: "stop_server",
157
+ effect: "unknown",
158
+ description: "Stop a managed development server by serverId.",
159
+ parameters: {
160
+ type: "object",
161
+ properties: {
162
+ serverId: { type: "string", description: "Managed server id returned by start_server" },
163
+ },
164
+ required: ["serverId"],
165
+ additionalProperties: false,
166
+ },
167
+ async execute(args) {
168
+ const serverId = String(args.serverId ?? "").trim();
169
+ if (!serverId)
170
+ return missingServer(serverId);
171
+ const server = await stopManagedServer(serverId);
172
+ if (!server)
173
+ return missingServer(serverId);
174
+ return {
175
+ content: `Stopped ${server.id}.`,
176
+ status: "success",
177
+ metadata: serverMetadata(server),
178
+ };
179
+ },
180
+ },
181
+ ];
182
+ }
183
+ function parsePurpose(value) {
184
+ return value === "verification" ? "verification" : "preview";
185
+ }
186
+ function parseLifecycle(value, purpose) {
187
+ if (value === "auto" || value === "keep_alive")
188
+ return value;
189
+ return purpose === "verification" ? "auto" : "keep_alive";
190
+ }
191
+ function parsePort(value) {
192
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0 || value > 65535) {
193
+ return undefined;
194
+ }
195
+ return value;
196
+ }
197
+ function formatServerStarted(server) {
198
+ const lines = [`Started ${server.id}.`];
199
+ if (server.url)
200
+ lines.push(`URL: ${server.url}`);
201
+ if (server.port)
202
+ lines.push(`Port: ${server.port}`);
203
+ lines.push(`Status: ${server.status}`);
204
+ lines.push(`Lifecycle: ${server.lifecycle}`);
205
+ return lines.join("\n");
206
+ }
207
+ function formatServer(server) {
208
+ return [
209
+ `${server.id}: ${server.status}`,
210
+ `Command: ${server.command}`,
211
+ `CWD: ${server.cwd}`,
212
+ server.url ? `URL: ${server.url}` : undefined,
213
+ server.port ? `Port: ${server.port}` : undefined,
214
+ `Purpose: ${server.purpose}`,
215
+ `Lifecycle: ${server.lifecycle}`,
216
+ server.pid ? `PID: ${server.pid}` : undefined,
217
+ server.exitCode !== undefined ? `Exit code: ${server.exitCode}` : undefined,
218
+ ].filter(Boolean).join("\n");
219
+ }
220
+ function serverMetadata(server) {
221
+ return {
222
+ kind: "server",
223
+ serverId: server.id,
224
+ status: server.status,
225
+ command: server.command,
226
+ cwd: server.cwd,
227
+ port: server.port,
228
+ url: server.url,
229
+ purpose: server.purpose,
230
+ lifecycle: server.lifecycle,
231
+ pid: server.pid,
232
+ };
233
+ }
234
+ function missingServer(serverId) {
235
+ return {
236
+ content: serverId ? `Error: Managed server not found: ${serverId}` : "Error: serverId is required",
237
+ isError: true,
238
+ status: "no_match",
239
+ metadata: {
240
+ kind: "server",
241
+ serverId,
242
+ reason: "server_not_found",
243
+ },
244
+ };
245
+ }
@@ -1,12 +1,9 @@
1
1
  /**
2
- * Write tool - create files or safely replace full file contents.
2
+ * Write tool - create files or replace full file contents.
3
3
  */
4
4
  import type { ApprovalController } from "../approval/types.js";
5
5
  import type { ToolRegistryEntry } from "../types.js";
6
6
  import { type LspService } from "../lsp/index.js";
7
7
  import { type FileStateTracker } from "./file-state.js";
8
- export interface WriteToolOptions {
9
- /** If true, existing files require overwrite=true plus a fresh agent-observed version. */
10
- refuseOverwrite?: boolean;
11
- }
12
- export declare function createWriteTool(cwd: string, options?: WriteToolOptions, approval?: ApprovalController, lsp?: LspService, fileState?: FileStateTracker): ToolRegistryEntry;
8
+ export type WriteToolOptions = Record<string, never>;
9
+ export declare function createWriteTool(cwd: string, _options?: WriteToolOptions, approval?: ApprovalController, lsp?: LspService, fileState?: FileStateTracker): ToolRegistryEntry;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Write tool - create files or safely replace full file contents.
2
+ * Write tool - create files or replace full file contents.
3
3
  */
4
4
  import { mkdir, readFile, writeFile } from "node:fs/promises";
5
5
  import { dirname } from "node:path";
@@ -9,27 +9,22 @@ 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
- export function createWriteTool(cwd, options = {}, approval, lsp, fileState) {
12
+ export function createWriteTool(cwd, _options = {}, approval, lsp, fileState) {
13
13
  return {
14
14
  name: "write",
15
15
  effect: "write_direct",
16
16
  requiresApproval: true,
17
- description: "Write a file to disk. Creates parent directories if needed. For an existing file, use overwrite=true only for full-file replacement after the file has been read or modified in this session; use edit for small targeted changes.",
17
+ 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.",
18
18
  parameters: {
19
19
  type: "object",
20
20
  properties: {
21
21
  path: { type: "string", description: "Path to the file (relative or absolute)" },
22
22
  content: { type: "string", description: "File contents" },
23
- overwrite: {
24
- type: "boolean",
25
- description: "Set true only for full-file replacement of an existing file. Existing files must have been read or modified in this session.",
26
- },
27
23
  },
28
24
  required: ["path", "content"],
29
25
  },
30
26
  async execute(args) {
31
27
  const filePath = resolveToolPath(cwd, args.path);
32
- const overwrite = args.overwrite === true;
33
28
  if (!isWithinWorkspace(cwd, filePath)) {
34
29
  return {
35
30
  content: `Error: Write path is outside the workspace: ${filePath}`,
@@ -52,29 +47,6 @@ export function createWriteTool(cwd, options = {}, approval, lsp, fileState) {
52
47
  catch {
53
48
  // New file.
54
49
  }
55
- if (existed && options.refuseOverwrite && !overwrite) {
56
- return {
57
- content: `Error: File already exists: ${filePath}.\n\n`
58
- + "For small targeted changes, use edit.\n"
59
- + "For a full-file replacement, call write again with overwrite=true. Existing files must be read or modified in this session before they can be safely overwritten.\n"
60
- + "Do not delete and recreate the file just to overwrite it.",
61
- isError: true,
62
- };
63
- }
64
- if (existed && overwrite && options.refuseOverwrite) {
65
- if (!fileState) {
66
- return {
67
- content: `Error: Cannot safely overwrite ${filePath} because file-state tracking is unavailable. `
68
- + "Read the file first in this agent session, then retry the full-file replacement.",
69
- isError: true,
70
- status: "blocked",
71
- };
72
- }
73
- const freshness = await fileState.checkFresh(filePath);
74
- if (!freshness.ok) {
75
- return staleOverwriteResult(filePath, freshness.reason);
76
- }
77
- }
78
50
  const diff = createTwoFilesPatch(filePath, filePath, oldContent, args.content, "original", "modified", { context: 3 });
79
51
  const gate = await gateToolAction(approval, {
80
52
  type: "write",
@@ -85,11 +57,9 @@ export function createWriteTool(cwd, options = {}, approval, lsp, fileState) {
85
57
  });
86
58
  if (!gate.approved)
87
59
  return gate.result;
88
- if (existed && overwrite && options.refuseOverwrite && fileState) {
89
- const freshness = await fileState.checkFresh(filePath);
90
- if (!freshness.ok) {
91
- return staleOverwriteResult(filePath, freshness.reason);
92
- }
60
+ const changed = await changedSincePreview(filePath, existed, oldContent);
61
+ if (changed) {
62
+ return changedDuringApprovalResult(filePath, changed);
93
63
  }
94
64
  try {
95
65
  await mkdir(dirname(filePath), { recursive: true });
@@ -113,7 +83,7 @@ export function createWriteTool(cwd, options = {}, approval, lsp, fileState) {
113
83
  metadata: {
114
84
  kind: "write",
115
85
  path: filePath,
116
- overwrite,
86
+ fileExists: existed,
117
87
  },
118
88
  };
119
89
  }
@@ -124,12 +94,22 @@ export function createWriteTool(cwd, options = {}, approval, lsp, fileState) {
124
94
  },
125
95
  };
126
96
  }
127
- function staleOverwriteResult(filePath, reason) {
128
- if (reason === "unobserved") {
97
+ async function changedSincePreview(filePath, existed, oldContent) {
98
+ try {
99
+ const latest = await readFile(filePath, "utf-8");
100
+ if (!existed)
101
+ return "created";
102
+ return latest === oldContent ? undefined : "changed";
103
+ }
104
+ catch {
105
+ return existed ? "missing" : undefined;
106
+ }
107
+ }
108
+ function changedDuringApprovalResult(filePath, reason) {
109
+ if (reason === "changed") {
129
110
  return {
130
- content: `Error: Cannot safely overwrite existing file: ${filePath}.\n\n`
131
- + "This file has not been read or modified in this agent session. Read it first, then retry write with overwrite=true.\n"
132
- + "For small targeted changes, use edit. Do not delete and recreate the file just to overwrite it.",
111
+ content: `Error: Cannot safely write ${filePath} because it changed while approval was pending.\n\n`
112
+ + "Re-read the file and retry the write against the latest content.",
133
113
  isError: true,
134
114
  status: "blocked",
135
115
  metadata: {
@@ -139,10 +119,10 @@ function staleOverwriteResult(filePath, reason) {
139
119
  },
140
120
  };
141
121
  }
142
- if (reason === "changed") {
122
+ if (reason === "created") {
143
123
  return {
144
- content: `Error: Cannot safely overwrite ${filePath} because it changed since the last read/write/edit in this agent session.\n\n`
145
- + "Re-read the file to pick up the latest content, then retry write with overwrite=true if a full-file replacement is still intended.",
124
+ content: `Error: Cannot safely write ${filePath} because it was created while approval was pending.\n\n`
125
+ + "Re-read the file and retry the write against the latest content.",
146
126
  isError: true,
147
127
  status: "blocked",
148
128
  metadata: {
@@ -153,7 +133,7 @@ function staleOverwriteResult(filePath, reason) {
153
133
  };
154
134
  }
155
135
  return {
156
- content: `Error: Cannot safely overwrite ${filePath} because it is missing now.\n\n`
136
+ content: `Error: Cannot safely write ${filePath} because it is missing now.\n\n`
157
137
  + "Check the path before retrying.",
158
138
  isError: true,
159
139
  status: "blocked",
@@ -0,0 +1 @@
1
+ export declare function copyTextToClipboard(text: string): Promise<void>;
@@ -0,0 +1,53 @@
1
+ import { spawn } from "node:child_process";
2
+ export async function copyTextToClipboard(text) {
3
+ if (process.platform === "darwin") {
4
+ await writeToProcess("pbcopy", [], text);
5
+ return;
6
+ }
7
+ if (process.platform === "win32") {
8
+ await writeToProcess("powershell", [
9
+ "-NoProfile",
10
+ "-Command",
11
+ "Set-Clipboard -Value ([Console]::In.ReadToEnd())",
12
+ ], text);
13
+ return;
14
+ }
15
+ const candidates = [
16
+ ["wl-copy", []],
17
+ ["xclip", ["-selection", "clipboard"]],
18
+ ["xsel", ["--clipboard", "--input"]],
19
+ ];
20
+ let lastError;
21
+ for (const [command, args] of candidates) {
22
+ try {
23
+ await writeToProcess(command, args, text);
24
+ return;
25
+ }
26
+ catch (error) {
27
+ lastError = error;
28
+ }
29
+ }
30
+ throw lastError instanceof Error ? lastError : new Error("No clipboard command available");
31
+ }
32
+ function writeToProcess(command, args, input) {
33
+ return new Promise((resolve, reject) => {
34
+ const child = spawn(command, args, {
35
+ stdio: ["pipe", "ignore", "pipe"],
36
+ windowsHide: true,
37
+ });
38
+ let stderr = "";
39
+ child.stderr.setEncoding("utf8");
40
+ child.stderr.on("data", (chunk) => {
41
+ stderr += chunk;
42
+ });
43
+ child.on("error", reject);
44
+ child.on("close", (code) => {
45
+ if (code === 0) {
46
+ resolve();
47
+ return;
48
+ }
49
+ reject(new Error(stderr.trim() || `${command} exited with code ${code}`));
50
+ });
51
+ child.stdin.end(input);
52
+ });
53
+ }
@@ -0,0 +1,2 @@
1
+ export type ResolvedTheme = "light" | "dark";
2
+ export declare function detectTerminalTheme(timeoutMs?: number): Promise<ResolvedTheme>;
@@ -0,0 +1,87 @@
1
+ export async function detectTerminalTheme(timeoutMs = 150) {
2
+ const fromEnv = parseColorFgBg(process.env.COLORFGBG);
3
+ if (fromEnv)
4
+ return fromEnv;
5
+ if (process.stdout.isTTY && process.stdin.isTTY) {
6
+ const fromOsc = await queryOsc11(timeoutMs);
7
+ if (fromOsc)
8
+ return fromOsc;
9
+ }
10
+ return "dark";
11
+ }
12
+ function parseColorFgBg(value) {
13
+ if (!value)
14
+ return null;
15
+ const parts = value.split(";");
16
+ const last = parts[parts.length - 1];
17
+ if (!last)
18
+ return null;
19
+ const bg = parseInt(last, 10);
20
+ if (Number.isNaN(bg))
21
+ return null;
22
+ if (bg >= 0 && bg <= 6)
23
+ return "dark";
24
+ if (bg >= 7 && bg <= 15)
25
+ return "light";
26
+ return null;
27
+ }
28
+ function queryOsc11(timeoutMs) {
29
+ return new Promise((resolve) => {
30
+ const stdin = process.stdin;
31
+ const stdout = process.stdout;
32
+ let settled = false;
33
+ const originalRaw = stdin.isRaw;
34
+ let buffer = "";
35
+ const cleanup = () => {
36
+ stdin.removeListener("data", onData);
37
+ try {
38
+ stdin.setRawMode(originalRaw);
39
+ }
40
+ catch {
41
+ // ignore - terminal may have already restored
42
+ }
43
+ stdin.pause();
44
+ };
45
+ const finish = (result) => {
46
+ if (settled)
47
+ return;
48
+ settled = true;
49
+ clearTimeout(timer);
50
+ cleanup();
51
+ resolve(result);
52
+ };
53
+ const onData = (chunk) => {
54
+ buffer += chunk.toString("utf8");
55
+ const match = buffer.match(/\x1b\]11;rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)(?:\x07|\x1b\\)/);
56
+ if (!match)
57
+ return;
58
+ const [, r, g, b] = match;
59
+ const lum = relativeLuminance(parseHexChannel(r), parseHexChannel(g), parseHexChannel(b));
60
+ finish(lum > 0.5 ? "light" : "dark");
61
+ };
62
+ try {
63
+ stdin.setRawMode(true);
64
+ }
65
+ catch {
66
+ resolve(null);
67
+ return;
68
+ }
69
+ stdin.resume();
70
+ stdin.on("data", onData);
71
+ const timer = setTimeout(() => finish(null), timeoutMs);
72
+ try {
73
+ stdout.write("\x1b]11;?\x07");
74
+ }
75
+ catch {
76
+ finish(null);
77
+ }
78
+ });
79
+ }
80
+ function parseHexChannel(hex) {
81
+ const max = (1 << (hex.length * 4)) - 1;
82
+ return parseInt(hex, 16) / max;
83
+ }
84
+ function relativeLuminance(r, g, b) {
85
+ const channel = (c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
86
+ return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
87
+ }
@@ -0,0 +1,63 @@
1
+ import type { ToolResultMetadata, TokenUsage } from "../types.js";
2
+ export interface CompactionMeta {
3
+ turns: number;
4
+ messages: number;
5
+ tokensSaved: number;
6
+ summarySections: Array<{
7
+ label: string;
8
+ content: string;
9
+ }>;
10
+ contextWindow?: number;
11
+ compactedAt: number;
12
+ }
13
+ export interface DisplayMessage {
14
+ role: "user" | "assistant" | "error";
15
+ content: string;
16
+ clientId?: string;
17
+ queued?: boolean;
18
+ reasoning?: string;
19
+ toolCalls?: DisplayToolCall[];
20
+ parts?: DisplayMessagePart[];
21
+ status?: "thinking" | "responding";
22
+ streaming?: boolean;
23
+ syntheticKind?: "ui_compact_card";
24
+ hiddenCount?: number;
25
+ compactionMeta?: CompactionMeta;
26
+ turnStartedAt?: number;
27
+ turnCompletedAt?: number;
28
+ turnUsage?: TokenUsage;
29
+ taskElapsedMs?: number;
30
+ }
31
+ export type DisplayMessagePart = DisplayTextPart | DisplayToolsPart;
32
+ export interface DisplayTextPart {
33
+ type: "text";
34
+ content: string;
35
+ }
36
+ export interface DisplayToolsPart {
37
+ type: "tools";
38
+ toolCalls: DisplayToolCall[];
39
+ }
40
+ export interface DisplayToolCall {
41
+ id: string;
42
+ name: string;
43
+ args: Record<string, any>;
44
+ rawArguments?: string;
45
+ streamingArgs?: boolean;
46
+ /** During streaming, an approximate line count derived from `\n` escapes in rawArguments. */
47
+ streamingNewlineCount?: number;
48
+ status?: "pending" | "running" | "completed" | "error";
49
+ result?: string;
50
+ resultCollapsed?: boolean;
51
+ isError?: boolean;
52
+ metadata?: ToolResultMetadata;
53
+ startedAt?: number;
54
+ completedAt?: number;
55
+ }
56
+ export declare function appendTextPart(parts: DisplayMessagePart[], content: string): void;
57
+ export declare function appendToolPart(parts: DisplayMessagePart[], toolCall: DisplayToolCall): void;
58
+ export declare function snapshotDisplayParts(parts: DisplayMessagePart[]): DisplayMessagePart[];
59
+ export declare function contentFromParts(parts: DisplayMessagePart[]): string;
60
+ export declare function toolCallsFromParts(parts: DisplayMessagePart[]): DisplayToolCall[];
61
+ export declare function compactDisplayMessages(messages: DisplayMessage[]): DisplayMessage[];
62
+ export declare function truncateText(value: string, maxChars: number): string;
63
+ export declare function formatCompactNumber(n: number): string;