@bubblebrain-ai/bubble 0.0.13 → 0.0.15

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 (80) hide show
  1. package/dist/agent/execution-governor.js +1 -1
  2. package/dist/agent/tool-intent.js +1 -0
  3. package/dist/agent.d.ts +2 -0
  4. package/dist/agent.js +589 -316
  5. package/dist/approval/controller.d.ts +1 -0
  6. package/dist/approval/controller.js +20 -3
  7. package/dist/approval/tool-helper.js +2 -0
  8. package/dist/approval/types.d.ts +14 -1
  9. package/dist/cli.d.ts +3 -1
  10. package/dist/cli.js +12 -0
  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 +41 -0
  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 +12 -0
  33. package/dist/slash-commands/types.d.ts +2 -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/display-history.d.ts +1 -0
  55. package/dist/tui/display-history.js +5 -4
  56. package/dist/tui/edit-diff.js +6 -1
  57. package/dist/tui/model-picker-data.d.ts +10 -0
  58. package/dist/tui/model-picker-data.js +32 -0
  59. package/dist/tui/run.d.ts +2 -0
  60. package/dist/tui/run.js +717 -122
  61. package/dist/tui/tool-renderers/fallback.js +1 -1
  62. package/dist/tui/tool-renderers/write-preview.js +2 -0
  63. package/dist/tui/trace-groups.js +10 -3
  64. package/dist/tui-ink/app.js +1 -4
  65. package/dist/tui-ink/approval/approval-dialog.js +7 -1
  66. package/dist/tui-ink/display-history.d.ts +1 -0
  67. package/dist/tui-ink/display-history.js +5 -4
  68. package/dist/tui-ink/message-list.js +14 -8
  69. package/dist/tui-ink/trace-groups.js +1 -1
  70. package/dist/tui-opentui/app.js +2 -0
  71. package/dist/tui-opentui/approval/approval-dialog.js +7 -1
  72. package/dist/tui-opentui/display-history.d.ts +1 -0
  73. package/dist/tui-opentui/display-history.js +5 -4
  74. package/dist/tui-opentui/edit-diff.js +6 -1
  75. package/dist/tui-opentui/message-list.js +6 -3
  76. package/dist/tui-opentui/trace-groups.js +10 -3
  77. package/dist/types.d.ts +12 -2
  78. package/dist/update/index.d.ts +46 -0
  79. package/dist/update/index.js +240 -0
  80. package/package.json +1 -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",
@@ -47,6 +47,7 @@ export interface DisplayToolCall {
47
47
  streamingNewlineCount?: number;
48
48
  status?: "pending" | "running" | "completed" | "error";
49
49
  result?: string;
50
+ resultCollapsed?: boolean;
50
51
  isError?: boolean;
51
52
  metadata?: ToolResultMetadata;
52
53
  startedAt?: number;
@@ -42,7 +42,6 @@ const MAX_VISIBLE_MESSAGES = 80;
42
42
  const FULL_DETAIL_WINDOW = 24;
43
43
  const MAX_OLD_CONTENT_CHARS = 1200;
44
44
  const MAX_OLD_REASONING_CHARS = 600;
45
- const MAX_OLD_TOOL_RESULT_CHARS = 800;
46
45
  const COMPACTION_SUMMARY_ITEMS = 6;
47
46
  const COMPACTION_FILE_LIMIT = 8;
48
47
  const TOOL_PATH_KEYS = ["file", "path", "paths", "filePath"];
@@ -272,11 +271,13 @@ function compactDisplayPart(part) {
272
271
  };
273
272
  }
274
273
  function compactToolCall(toolCall) {
274
+ if (toolCall.result === undefined) {
275
+ return toolCall;
276
+ }
275
277
  return {
276
278
  ...toolCall,
277
- result: toolCall.result
278
- ? truncateText(toolCall.result, MAX_OLD_TOOL_RESULT_CHARS)
279
- : toolCall.result,
279
+ result: undefined,
280
+ resultCollapsed: true,
280
281
  };
281
282
  }
282
283
  export function truncateText(value, maxChars) {
@@ -1,7 +1,7 @@
1
1
  import { countUnifiedDiffChanges } from "../diff-stats.js";
2
2
  export const EDIT_COLLAPSED_DIFF_LINES = 20;
3
3
  export function getEditDiffDetails(tool) {
4
- if (tool.name !== "edit" || tool.isError)
4
+ if ((tool.name !== "edit" && tool.name !== "apply_patch") || tool.isError)
5
5
  return null;
6
6
  const metadata = tool.metadata;
7
7
  const metadataDiff = readMetadataString(metadata, "diff");
@@ -12,9 +12,14 @@ export function getEditDiffDetails(tool) {
12
12
  const added = readMetadataNumber(metadata, "addedLines") ?? counted.added;
13
13
  const removed = readMetadataNumber(metadata, "removedLines") ?? counted.removed;
14
14
  const path = readMetadataString(metadata, "path")
15
+ ?? readFirstMetadataPath(metadata)
15
16
  ?? (typeof tool.args.path === "string" ? tool.args.path : undefined);
16
17
  return { diff, added, removed, path };
17
18
  }
19
+ function readFirstMetadataPath(metadata) {
20
+ const value = metadata?.paths;
21
+ return Array.isArray(value) && typeof value[0] === "string" ? value[0] : undefined;
22
+ }
18
23
  export function formatEditSuccessSummary(details) {
19
24
  const stats = details ? formatEditStats(details.added, details.removed) : "";
20
25
  return `Succeeded. File edited.${stats ? ` ${stats}` : ""}`;
@@ -0,0 +1,10 @@
1
+ import { type ModelInfo, type ProviderProfile, type ProviderRegistry } from "../provider-registry.js";
2
+ type ModelPickerRegistry = Pick<ProviderRegistry, "getEnabled" | "getModelConfig" | "listModels">;
3
+ export type ModelProviderGroup = {
4
+ provider: ProviderProfile;
5
+ models: ModelInfo[];
6
+ };
7
+ export declare function localModelsForProvider(registry: Pick<ProviderRegistry, "getModelConfig">, provider: ProviderProfile): ModelInfo[];
8
+ export declare function getVisibleModelProviders(registry: Pick<ProviderRegistry, "getEnabled">, providerId?: string): ProviderProfile[];
9
+ export declare function discoverModelProviderGroups(registry: ModelPickerRegistry, providerId?: string): Promise<ModelProviderGroup[]>;
10
+ export {};
@@ -0,0 +1,32 @@
1
+ import { listBuiltinModels } from "../model-catalog.js";
2
+ import { isUserVisibleProvider, } from "../provider-registry.js";
3
+ export function localModelsForProvider(registry, provider) {
4
+ const customModels = registry.getModelConfig().getCustomModels(provider.id);
5
+ if (customModels.length > 0)
6
+ return customModels;
7
+ const builtinProviderId = provider.id === "openai" && provider.authType === "oauth"
8
+ ? "openai-codex"
9
+ : provider.id;
10
+ return listBuiltinModels(builtinProviderId).map((model) => ({
11
+ id: model.id,
12
+ name: model.name,
13
+ providerId: provider.id,
14
+ }));
15
+ }
16
+ export function getVisibleModelProviders(registry, providerId) {
17
+ return registry
18
+ .getEnabled()
19
+ .filter((provider) => isUserVisibleProvider(provider.id))
20
+ .filter((provider) => !providerId || provider.id === providerId);
21
+ }
22
+ export async function discoverModelProviderGroups(registry, providerId) {
23
+ const providers = getVisibleModelProviders(registry, providerId);
24
+ return Promise.all(providers.map(async (provider) => {
25
+ try {
26
+ return { provider, models: await registry.listModels(provider) };
27
+ }
28
+ catch {
29
+ return { provider, models: localModelsForProvider(registry, provider) };
30
+ }
31
+ }));
32
+ }
package/dist/tui/run.d.ts CHANGED
@@ -41,5 +41,7 @@ export interface RunTuiOptions {
41
41
  runMemoryCompaction?: () => Promise<string>;
42
42
  runMemorySummary?: (scope?: MemoryScope) => Promise<string>;
43
43
  runMemoryRefresh?: (scope?: MemoryScope) => Promise<string>;
44
+ /** One-line "update available" notice shown on the home screen, if any. */
45
+ updateNotice?: string;
44
46
  }
45
47
  export declare function runTui(agent: Agent, args: CliArgs, options?: RunTuiOptions): Promise<void>;