@bubblebrain-ai/bubble 0.0.50 → 0.0.52

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.
@@ -12,7 +12,6 @@ const READONLY_PRESET = [
12
12
  "web_fetch",
13
13
  "memory",
14
14
  "skill",
15
- "todo_write",
16
15
  ];
17
16
  const SUBAGENT_DENY_TOOLS = new Set([
18
17
  "subagent",
@@ -124,12 +123,12 @@ export function builtinAgentProfiles() {
124
123
  "You are an explorer subagent for codebase reconnaissance.",
125
124
  "Answer the specific question by inspecting the repository directly. Prefer precise file paths and line-level evidence.",
126
125
  "Keep the answer compact and avoid broad refactors or implementation plans unless asked.",
127
- ].join("\n"), ["read", "glob", "grep", "lsp", "memory", "skill", "todo_write"]),
126
+ ].join("\n"), ["read", "glob", "grep", "lsp", "memory", "skill"]),
128
127
  roleProfile("worker", "Implementation-planning worker subagent", [
129
128
  "You are a worker subagent. In this Phase 1 runtime you are read-only, so you must not modify files.",
130
129
  "Analyze the assigned implementation slice, identify exact files to change, and return a concrete patch plan or findings.",
131
130
  "If write-capable worker mode is needed, say so explicitly.",
132
- ].join("\n"), ["read", "glob", "grep", "lsp", "memory", "skill", "todo_write"]),
131
+ ].join("\n"), ["read", "glob", "grep", "lsp", "memory", "skill"]),
133
132
  toProfile("search"),
134
133
  toProfile("security_investigation"),
135
134
  toProfile("evidence_correlation"),
@@ -146,7 +145,7 @@ export function builtinAgentProfiles() {
146
145
  model: "inherit",
147
146
  tools: {
148
147
  preset: "explicit",
149
- include: ["read", "glob", "grep", "edit", "write", "bash", "lsp", "todo_write"],
148
+ include: ["read", "glob", "grep", "edit", "write", "bash", "lsp"],
150
149
  exclude: [],
151
150
  },
152
151
  approval: "fail",
@@ -1,7 +1,7 @@
1
1
  const POLICY_MAP = {
2
2
  search: {
3
3
  type: "search",
4
- allowedTools: ["read", "glob", "grep", "web_search", "web_fetch", "skill", "todo_write"],
4
+ allowedTools: ["read", "glob", "grep", "web_search", "web_fetch", "skill"],
5
5
  reminder: [
6
6
  "Subtask policy: search",
7
7
  "- Focus on locating relevant files, symbols, and evidence quickly.",
@@ -13,7 +13,7 @@ const POLICY_MAP = {
13
13
  },
14
14
  security_investigation: {
15
15
  type: "security_investigation",
16
- allowedTools: ["read", "glob", "grep", "web_search", "web_fetch", "skill", "todo_write"],
16
+ allowedTools: ["read", "glob", "grep", "web_search", "web_fetch", "skill"],
17
17
  reminder: [
18
18
  "Subtask policy: security_investigation",
19
19
  "- Investigate only in read-only mode.",
@@ -25,7 +25,7 @@ const POLICY_MAP = {
25
25
  },
26
26
  evidence_correlation: {
27
27
  type: "evidence_correlation",
28
- allowedTools: ["read", "skill", "todo_write"],
28
+ allowedTools: ["read", "skill"],
29
29
  reminder: [
30
30
  "Subtask policy: evidence_correlation",
31
31
  "- Correlate evidence already discovered.",
@@ -37,7 +37,7 @@ const POLICY_MAP = {
37
37
  },
38
38
  general_readonly: {
39
39
  type: "general_readonly",
40
- allowedTools: ["read", "glob", "grep", "web_search", "web_fetch", "skill", "todo_write"],
40
+ allowedTools: ["read", "glob", "grep", "web_search", "web_fetch", "skill"],
41
41
  reminder: [
42
42
  "Subtask policy: general_readonly",
43
43
  "- Stay in read-only mode.",
package/dist/agent.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * It maintains message state, calls the LLM, executes tools, and auto-continues.
4
4
  */
5
5
  import { type ContextUsageSnapshot } from "./context/usage.js";
6
- import type { AgentEvent, AgentInputController, ContentPart, PermissionMode, Message, Provider, ThinkingLevel, Todo, ToolResult, ToolRegistryEntry, ToolUpdate } from "./types.js";
6
+ import type { AgentEvent, AgentInputController, ContentPart, PermissionMode, Message, Provider, ThinkingLevel, ToolResult, ToolRegistryEntry, ToolUpdate } from "./types.js";
7
7
  import { type TurnHooks } from "./orchestrator/hooks.js";
8
8
  import type { ExternalHookController } from "./hooks/controller.js";
9
9
  import type { AgentCategoriesConfig, ResolvedSubagentRoute } from "./agent/categories.js";
@@ -49,11 +49,9 @@ export interface AgentOptions {
49
49
  taskBudget?: {
50
50
  total: number;
51
51
  };
52
- todos?: Todo[];
53
52
  systemPrompt?: string;
54
53
  onMessageAppend?: (message: Message) => void;
55
54
  onToolResult?: (toolName: string, result: ToolResult) => void;
56
- onTodosUpdate?: (todos: Todo[]) => void;
57
55
  onModeUpdate?: (mode: PermissionMode) => void;
58
56
  /**
59
57
  * Fired when MULTI-TURN compaction rewrites resident history, with the
@@ -152,9 +150,6 @@ export declare class Agent {
152
150
  private _modeVersion;
153
151
  private onModeUpdate?;
154
152
  private onCompactionApplied?;
155
- private _todos;
156
- private _todosVersion;
157
- private onTodosUpdate?;
158
153
  private onMessageAppend?;
159
154
  private onToolResult?;
160
155
  private hookDefinitions;
@@ -291,10 +286,6 @@ export declare class Agent {
291
286
  setMode(value: PermissionMode): void;
292
287
  /** Internal: snapshot counter that bumps on every mode change. Used by run loop. */
293
288
  get modeVersion(): number;
294
- getTodos(): Todo[];
295
- setTodos(next: Todo[]): void;
296
- /** Internal: snapshot counter that bumps on every setTodos. Used by run loop to detect mutations. */
297
- get todosVersion(): number;
298
289
  setSystemPrompt(prompt: string): void;
299
290
  run(userInput: string | ContentPart[], cwd: string, options?: AgentRunOptions): AsyncIterable<AgentEvent>;
300
291
  private recoverFromOverflow;
@@ -440,6 +431,5 @@ export declare class Agent {
440
431
  private persistCompactionSummary;
441
432
  private appendMessage;
442
433
  private appendInterruptedAssistantBoundary;
443
- private clearTodosAfterInterruptedRun;
444
434
  private executeTool;
445
435
  }
package/dist/agent.js CHANGED
@@ -120,9 +120,6 @@ export class Agent {
120
120
  _modeVersion = 0;
121
121
  onModeUpdate;
122
122
  onCompactionApplied;
123
- _todos;
124
- _todosVersion = 0;
125
- onTodosUpdate;
126
123
  onMessageAppend;
127
124
  onToolResult;
128
125
  hookDefinitions;
@@ -170,10 +167,8 @@ export class Agent {
170
167
  this.temperature = options.temperature ?? 0.2;
171
168
  this.thinkingLevel = options.thinkingLevel ?? "off";
172
169
  this._mode = options.mode ?? "default";
173
- this._todos = options.todos ? [...options.todos] : [];
174
170
  this.onMessageAppend = options.onMessageAppend;
175
171
  this.onToolResult = options.onToolResult;
176
- this.onTodosUpdate = options.onTodosUpdate;
177
172
  this.onModeUpdate = options.onModeUpdate;
178
173
  this.onCompactionApplied = options.onCompactionApplied;
179
174
  this.hookDefinitions = options.hooks ?? [];
@@ -556,18 +551,6 @@ export class Agent {
556
551
  get modeVersion() {
557
552
  return this._modeVersion;
558
553
  }
559
- getTodos() {
560
- return this._todos.map((todo) => ({ ...todo }));
561
- }
562
- setTodos(next) {
563
- this._todos = next.map((todo) => ({ ...todo }));
564
- this._todosVersion += 1;
565
- this.onTodosUpdate?.(this.getTodos());
566
- }
567
- /** Internal: snapshot counter that bumps on every setTodos. Used by run loop to detect mutations. */
568
- get todosVersion() {
569
- return this._todosVersion;
570
- }
571
554
  setSystemPrompt(prompt) {
572
555
  const systemMessage = { role: "system", content: prompt };
573
556
  if (this.messages[0]?.role === "system") {
@@ -686,10 +669,6 @@ export class Agent {
686
669
  this.injectSystemReminder(reminder);
687
670
  }
688
671
  };
689
- if (this._todos.length > 0 && this._todos.every((t) => t.status === "completed")) {
690
- this.setTodos([]);
691
- yield emit({ type: "todos_updated", todos: [] });
692
- }
693
672
  if (!options.resumeWithoutInput) {
694
673
  const promptHook = await this.runExternalHook({
695
674
  eventName: "UserPromptSubmit",
@@ -1256,7 +1235,6 @@ export class Agent {
1256
1235
  argsCorrupt: tc.argsCorrupt,
1257
1236
  }, traceContext);
1258
1237
  yield emit({ type: "tool_start", id: tc.id, name: tc.name, args: tc.parsedArgs });
1259
- const todosVersionBefore = this._todosVersion;
1260
1238
  const modeVersionBefore = this._modeVersion;
1261
1239
  const updateQueue = createUpdateQueue();
1262
1240
  let result;
@@ -1382,9 +1360,6 @@ export class Agent {
1382
1360
  yield emit({ type: "tool_end", id: tc.id, name: tc.name, result });
1383
1361
  for (const update of this.subagents.drainToolUpdates())
1384
1362
  yield emit(update);
1385
- if (this._todosVersion !== todosVersionBefore) {
1386
- yield emit({ type: "todos_updated", todos: this.getTodos() });
1387
- }
1388
1363
  if (this._modeVersion !== modeVersionBefore) {
1389
1364
  yield emit({ type: "mode_changed", mode: this._mode });
1390
1365
  }
@@ -1456,15 +1431,10 @@ export class Agent {
1456
1431
  catch (error) {
1457
1432
  if (isAbortError(error, abortSignal)) {
1458
1433
  const appendedBoundary = this.appendInterruptedAssistantBoundary(currentAssistantMsg, currentAssistantAppended);
1459
- const clearedTodos = this.clearTodosAfterInterruptedRun();
1460
1434
  traceEvent("agent_run_interrupted", {
1461
1435
  appendedBoundary,
1462
- clearedTodos,
1463
1436
  messageCount: this.messages.length,
1464
1437
  }, traceContext);
1465
- if (clearedTodos) {
1466
- yield emit({ type: "todos_updated", todos: this.getTodos() });
1467
- }
1468
1438
  }
1469
1439
  else {
1470
1440
  const stopFailureHook = await this.runExternalHook({
@@ -1930,12 +1900,6 @@ export class Agent {
1930
1900
  });
1931
1901
  return true;
1932
1902
  }
1933
- clearTodosAfterInterruptedRun() {
1934
- if (this._todos.length === 0)
1935
- return false;
1936
- this.setTodos([]);
1937
- return true;
1938
- }
1939
1903
  async executeTool(toolCall, cwd, abortSignal, emitUpdate) {
1940
1904
  throwIfAborted(abortSignal);
1941
1905
  if (toolCall.name === "exit_plan_mode" && this._mode !== "plan") {
@@ -1954,7 +1918,7 @@ export class Agent {
1954
1918
  if (this._mode === "plan" && !tool.readOnly) {
1955
1919
  return {
1956
1920
  content: `Error: Tool "${toolCall.name}" is not allowed in plan mode. ` +
1957
- `In plan mode you may only use read-only tools (read, glob, grep, web_search, web_fetch, spawn_agent, wait_agent, send_input, skill_search, skill, todo_write, tool_search, question, exit_plan_mode). ` +
1921
+ `In plan mode you may only use read-only tools (read, glob, grep, web_search, web_fetch, spawn_agent, wait_agent, send_input, skill_search, skill, tool_search, question, exit_plan_mode). ` +
1958
1922
  `To modify files or run commands, present your proposal and call exit_plan_mode so the user can review and approve it.`,
1959
1923
  isError: true,
1960
1924
  };
@@ -235,8 +235,6 @@ export function summarizeAgentEventForTrace(event) {
235
235
  ...event,
236
236
  content: summarizeTraceText(event.content),
237
237
  };
238
- case "todos_updated":
239
- return { type: event.type, count: event.todos.length };
240
238
  default:
241
239
  return { ...event };
242
240
  }
@@ -70,12 +70,7 @@ export class RunDriver {
70
70
  ({ action: "reject", reason: "Plan approval over Feishu not implemented; use /mode default." }),
71
71
  setMode: (mode) => agentForPlan?.setMode(mode),
72
72
  };
73
- const todoStore = {
74
- getTodos: () => agentRef?.getTodos() ?? [],
75
- setTodos: (todos) => agentRef?.setTodos(todos),
76
- };
77
73
  const tools = createAllTools(session.cwd, this.opts.deps.skillRegistry, {
78
- todoStore,
79
74
  planController,
80
75
  approvalController,
81
76
  lspService,
@@ -125,7 +120,6 @@ export class RunDriver {
125
120
  temperature: 0.2,
126
121
  thinkingLevel,
127
122
  mode: initialMode,
128
- todos: session.manager.getTodos(),
129
123
  onMessageAppend: (message) => {
130
124
  if (message.role === "system" || message.role === "meta")
131
125
  return;
@@ -145,7 +139,6 @@ export class RunDriver {
145
139
  if (match?.[1])
146
140
  session.manager.appendMarker("skill_activated", match[1].trim());
147
141
  },
148
- onTodosUpdate: (todos) => session.manager.appendTodosSnapshot(todos),
149
142
  onModeUpdate: (mode) => {
150
143
  session.manager.appendMarker("mode_switch", mode);
151
144
  this.opts.binder.setMode(req.scopeKey, mode);
@@ -97,9 +97,6 @@ export function reduceRunState(state, event) {
97
97
  case "mode_changed":
98
98
  state.mode = event.mode;
99
99
  return state;
100
- case "todos_updated":
101
- // Todos render is deferred (would need a todos block kind). v1 ignores.
102
- return state;
103
100
  case "context_recovered":
104
101
  // Internal recovery — silent.
105
102
  return state;
package/dist/main.js CHANGED
@@ -128,10 +128,6 @@ async function main() {
128
128
  return createProvider(providerId, target.apiKey, target.baseURL);
129
129
  };
130
130
  let agentRef;
131
- const todoStore = {
132
- getTodos: () => agentRef?.getTodos() ?? [],
133
- setTodos: (todos) => agentRef?.setTodos(todos),
134
- };
135
131
  const planHandlerRef = {};
136
132
  const planController = {
137
133
  getMode: () => agentRef?.mode ?? "default",
@@ -181,7 +177,6 @@ async function main() {
181
177
  const allowBackgroundTasks = !printMode;
182
178
  const promotionChannel = new PromotionChannel();
183
179
  const tools = createAllTools(args.cwd, skillRegistry, {
184
- todoStore,
185
180
  planController,
186
181
  approvalController,
187
182
  questionController: printMode ? undefined : questionController,
@@ -338,7 +333,6 @@ async function main() {
338
333
  ? normalizeThinkingLevel(configuredThinkingLevel, getAvailableThinkingLevels(activeProviderId, effectiveModelId))
339
334
  : getDefaultThinkingLevel(activeProviderId, effectiveModelId)
340
335
  : (args.thinkingLevel ?? sessionThinkingLevel ?? configuredThinkingLevel ?? "off");
341
- const restoredTodos = sessionManager?.getTodos() ?? [];
342
336
  const initialMode = args.mode ?? "default";
343
337
  const skillSummaries = skillRegistry.summaries();
344
338
  // This synchronous provenance gate must run before buildMemoryPrompt: the
@@ -401,7 +395,6 @@ async function main() {
401
395
  temperature: 0.2,
402
396
  thinkingLevel: initialThinkingLevel,
403
397
  mode: initialMode,
404
- todos: restoredTodos,
405
398
  onMessageAppend: (message) => {
406
399
  if (!sessionManager)
407
400
  return;
@@ -435,9 +428,6 @@ async function main() {
435
428
  sessionManager.appendMarker("skill_activated", match[1].trim());
436
429
  }
437
430
  },
438
- onTodosUpdate: (todos) => {
439
- sessionManager?.appendTodosSnapshot(todos);
440
- },
441
431
  onModeUpdate: (mode) => {
442
432
  sessionManager?.appendMarker("mode_switch", mode);
443
433
  },
@@ -703,7 +693,6 @@ async function main() {
703
693
  // mirroring the /rewind history-replacement pattern.
704
694
  const head = agent.messages.filter((m) => m.role === "system" || m.role === "meta");
705
695
  const nextMessages = [...head, ...history];
706
- const nextTodos = next.getTodos();
707
696
  // Commit only after every file read/write and reconstruction step has
708
697
  // succeeded. Callers can safely prepare candidate sessions without a
709
698
  // failed switch rebinding persistence or replacing the live history.
@@ -714,10 +703,6 @@ async function main() {
714
703
  // These update only live in-memory/UI mirrors after the persistence
715
704
  // commit. A best-effort callback failure must not report the switch as
716
705
  // rolled back after the outer session binding has already changed.
717
- try {
718
- agent.setTodos(nextTodos);
719
- }
720
- catch { /* candidate already committed */ }
721
706
  try {
722
707
  agent.resetContextUsageAnchor();
723
708
  }
@@ -160,8 +160,6 @@ function serializeSessionEntry(entry) {
160
160
  return `[tool_call:${entry.toolCall.name}] ${truncate(entry.toolCall.arguments, 1_500)}`;
161
161
  case "tool_result":
162
162
  return `[tool_result${entry.message.isError ? " error=true" : ""}] ${truncate(entry.message.content, 2_000)}`;
163
- case "todos_snapshot":
164
- return `[todos] ${truncate(JSON.stringify(entry.todos), 2_000)}`;
165
163
  }
166
164
  }
167
165
  function countMeaningfulEntries(entries) {
@@ -86,9 +86,6 @@ function buildGuidelines(tools, extraGuidelines) {
86
86
  if (tools.includes("skill_search") && tools.includes("skill")) {
87
87
  add("Skills may provide specialized workflows. When a task appears to match a specialized workflow, call skill_search to find relevant skills, then call skill with the exact name to load the selected skill before applying it");
88
88
  }
89
- if (tools.includes("todo_write")) {
90
- add("Default to just doing the work. Reach for todo_write only when tracking progress would genuinely help — multi-phase work with real dependencies, a long task spanning several areas, an explicit user request, or steps you discover mid-task. Don't make a list to pad a task you could just do; when in doubt, skip it. Mark each item completed as soon as it is done; do not batch updates");
91
- }
92
89
  for (const item of extraGuidelines) {
93
90
  add(item);
94
91
  }
@@ -14,7 +14,6 @@ export const defaultToolSnippets = {
14
14
  question: "Ask the user structured questions when clarification or preference choices would materially improve the work",
15
15
  skill_search: "Search available skills by name, description, tags, and source",
16
16
  skill: "Load a named skill with specialized instructions and bundled resources",
17
- todo_write: "Plan and track multi-step work. Mark each task completed as soon as it is done — do not batch.",
18
17
  };
19
18
  export const defaultToolNames = [
20
19
  "read",
@@ -31,7 +30,6 @@ export const defaultToolNames = [
31
30
  "question",
32
31
  "skill_search",
33
32
  "skill",
34
- "todo_write",
35
33
  ];
36
34
  export function buildEnvironmentPrompt(options = {}) {
37
35
  const configuredProvider = options.configuredProvider ?? "unknown";
@@ -29,7 +29,7 @@ const PLAN_MODE_ENTER = `
29
29
  Plan mode is now ACTIVE.
30
30
 
31
31
  Rules while in plan mode:
32
- - Only read-only tools are allowed, including read, glob, grep, web_search, web_fetch, spawn_agent, wait_agent, send_input, skill_search, skill, todo_write, tool_search, question, and exit_plan_mode.
32
+ - Only read-only tools are allowed, including read, glob, grep, web_search, web_fetch, spawn_agent, wait_agent, send_input, skill_search, skill, tool_search, question, and exit_plan_mode.
33
33
  - Writes, edits, and shell commands WILL be rejected by the harness; do not try them.
34
34
  - Do not edit files or claim implementation is complete while plan mode is active.
35
35
  - Investigate the codebase, then use the question tool to clarify important ambiguities, tradeoffs, requirements, or preference choices that would materially change the plan.
@@ -357,7 +357,13 @@ export class ProviderRegistry {
357
357
  else {
358
358
  // 2. Fall back to config.json providers (interactive TUI style)
359
359
  providers = this.config.getProviders().map((provider) => {
360
- const baseURL = upgradeLegacyBaseURL(provider.id, provider.baseURL, provider.protocol);
360
+ // config.json entries may omit baseURL (a bare {id, apiKey} written
361
+ // by minimal setups / benchmark harnesses); merge the builtin default
362
+ // the same way the models.json path above does, instead of propagating
363
+ // undefined into provider construction.
364
+ const builtinBase = getBuiltinProvider(provider.id)?.baseURL ?? "";
365
+ const rawBaseURL = provider.baseURL || builtinBase;
366
+ const baseURL = upgradeLegacyBaseURL(provider.id, rawBaseURL, provider.protocol);
361
367
  return {
362
368
  ...provider,
363
369
  baseURL,
package/dist/provider.js CHANGED
@@ -201,9 +201,9 @@ export function createProviderInstance(options) {
201
201
  // paths already wrap — this generic chat-completions path did not).
202
202
  try {
203
203
  yield* translateOpenAIStream(stream, {
204
- toolArgsMergeMode: resolveToolArgsMergeMode(options.providerId || "", options.baseURL),
205
- reasoningMergeMode: resolveReasoningMergeMode(options.providerId || "", options.baseURL),
206
- textMergeMode: resolveTextMergeMode(options.providerId || "", options.baseURL),
204
+ toolArgsMergeMode: resolveToolArgsMergeMode(options.providerId || "", options.baseURL || ""),
205
+ reasoningMergeMode: resolveReasoningMergeMode(options.providerId || "", options.baseURL || ""),
206
+ textMergeMode: resolveTextMergeMode(options.providerId || "", options.baseURL || ""),
207
207
  debugProviderId: options.providerId || "",
208
208
  debugModelId: chatOptions.model,
209
209
  });
@@ -246,7 +246,7 @@ function resolveProviderProtocol(options) {
246
246
  if (options.protocol)
247
247
  return options.protocol;
248
248
  const providerId = (options.providerId || "").toLowerCase();
249
- const baseURL = options.baseURL.toLowerCase();
249
+ const baseURL = (options.baseURL || "").toLowerCase();
250
250
  if (providerId === "doubao"
251
251
  && baseURL.replace(/\/+$/, "") === "https://ark.cn-beijing.volces.com/api/v3") {
252
252
  return "ark-responses";
@@ -260,7 +260,7 @@ function resolveProviderProtocol(options) {
260
260
  }
261
261
  function isMiniMaxOpenAICompatible(options) {
262
262
  const providerId = (options.providerId || "").toLowerCase();
263
- const baseURL = options.baseURL.toLowerCase();
263
+ const baseURL = (options.baseURL || "").toLowerCase();
264
264
  return providerId === "minimax-openai"
265
265
  || (providerId === "minimax" && !baseURL.includes("/anthropic"))
266
266
  || baseURL.includes("api.minimaxi.com/v1")
@@ -17,10 +17,11 @@ import { type SessionSummary } from "../session.js";
17
17
  import type { ApprovalDecision, ApprovalRequest } from "../approval/types.js";
18
18
  import { UserConfig } from "../config.js";
19
19
  import { ProviderRegistry } from "../provider-registry.js";
20
+ import type { ResolvedSubagentRoute } from "../agent/categories.js";
20
21
  import { type QuestionAnswer, type QuestionRequest } from "../question/controller.js";
21
22
  import { SkillRegistry } from "../skills/registry.js";
22
23
  import type { SkillSummary } from "../skills/types.js";
23
- import type { AgentEvent, ContentPart, Message, PermissionMode, ThinkingLevel } from "../types.js";
24
+ import type { AgentEvent, ContentPart, Message, PermissionMode, Provider, ThinkingLevel } from "../types.js";
24
25
  export interface BubbleSdkOptions {
25
26
  /** Fallback working directory for sessions created without an explicit cwd. */
26
27
  defaultCwd?: string;
@@ -66,6 +67,14 @@ export interface RunTurnOptions extends TurnHandlers {
66
67
  * harness) A/B prompt variants without forking the prompt builder.
67
68
  */
68
69
  appendSystemPrompt?: string;
70
+ /**
71
+ * Creates the provider instance for a subagent route whose provider differs
72
+ * from the turn's provider (e.g. spawn_agent with `model: "provider:model"`).
73
+ * When omitted the SDK builds a default factory over its own registry —
74
+ * same semantics as the TUI's createProviderForRoute (OAuth prepare, then
75
+ * configured+enabled+keyed profile lookup). Pass your own to override.
76
+ */
77
+ providerFactory?: (route: ResolvedSubagentRoute) => Provider | Promise<Provider>;
69
78
  signal?: AbortSignal;
70
79
  }
71
80
  export interface SdkSessionRef {
@@ -106,6 +115,12 @@ export declare class BubbleSdk {
106
115
  };
107
116
  runTurn(sessionId: string, options: RunTurnOptions): AsyncGenerator<AgentEvent>;
108
117
  private bashAllowlistFor;
118
+ /**
119
+ * Default cross-provider subagent factory over this SDK instance's own
120
+ * registry — the same semantics as the TUI's createProviderForRoute. The
121
+ * registry is reloaded first so a key added after construction works.
122
+ */
123
+ private defaultProviderFactory;
109
124
  /** MCP servers are started lazily, once per cwd (McpManager has no stop). */
110
125
  private mcpToolsFor;
111
126
  private resolveSession;
@@ -149,4 +164,4 @@ export { ExternalHookController } from "../hooks/controller.js";
149
164
  export { calculateUsageCost, getModelPricing, type ModelPricing, type PricingCurrency, } from "../model-pricing.js";
150
165
  export { registry as slashCommandRegistry } from "../slash-commands/index.js";
151
166
  export type { SlashCommandContext } from "../slash-commands/types.js";
152
- export type { AgentEvent, ContentPart, Message, PermissionMode, Provider, ThinkingLevel, Todo, TokenUsage, UsageCost, ToolRegistryEntry, ToolResult, ToolUpdate, } from "../types.js";
167
+ export type { AgentEvent, ContentPart, Message, PermissionMode, Provider, ThinkingLevel, TokenUsage, UsageCost, ToolRegistryEntry, ToolResult, ToolUpdate, } from "../types.js";
package/dist/sdk/index.js CHANGED
@@ -171,10 +171,6 @@ export class BubbleSdk {
171
171
  approvalController,
172
172
  fileStateTracker,
173
173
  planController,
174
- todoStore: {
175
- getTodos: () => agentRef?.getTodos() ?? [],
176
- setTodos: (todos) => agentRef?.setTodos(todos),
177
- },
178
174
  questionController,
179
175
  goalStore: new GoalStore(),
180
176
  checkpoints: () => session.getCheckpoints(),
@@ -214,12 +210,17 @@ export class BubbleSdk {
214
210
  temperature: 0.2,
215
211
  thinkingLevel,
216
212
  mode,
217
- todos: session.getTodos(),
218
213
  budgetLedger: new BudgetLedger(),
219
214
  fileStateTracker,
220
215
  skills: skillRegistry.summaries(),
221
216
  memoryPrompt,
222
217
  externalHooks: hookController,
218
+ // Cross-provider subagent routes (spawn_agent with "provider:model")
219
+ // need a factory; without one the child blocks with
220
+ // "no provider factory is configured". Default mirrors the TUI's
221
+ // createProviderForRoute (OAuth prepare + configured+enabled+keyed
222
+ // profile lookup); hosts may override per turn.
223
+ providerFactory: options.providerFactory ?? this.defaultProviderFactory(),
223
224
  onMessageAppend: (message) => {
224
225
  if (message.role === "system" || message.role === "meta")
225
226
  return;
@@ -230,7 +231,6 @@ export class BubbleSdk {
230
231
  onCompactionApplied: (summary) => {
231
232
  session.applyLLMCompaction(summary);
232
233
  },
233
- onTodosUpdate: (todos) => session.appendTodosSnapshot(todos),
234
234
  onModeUpdate: (m) => session.appendMarker("mode_switch", m),
235
235
  });
236
236
  agentRef = agent;
@@ -276,6 +276,37 @@ export class BubbleSdk {
276
276
  }
277
277
  return allowlist;
278
278
  }
279
+ /**
280
+ * Default cross-provider subagent factory over this SDK instance's own
281
+ * registry — the same semantics as the TUI's createProviderForRoute. The
282
+ * registry is reloaded first so a key added after construction works.
283
+ */
284
+ defaultProviderFactory() {
285
+ return async (route) => {
286
+ const providerId = route.providerId;
287
+ if (!providerId) {
288
+ throw new Error(`Subagent route for model "${route.model}" did not include a provider.`);
289
+ }
290
+ // getConfigured() reads config.json from disk every call, so keys
291
+ // added after SDK construction are picked up without a reload call.
292
+ if (this.registry.supportsOAuth(providerId) && this.registry.getAuthStorage().has(providerId)) {
293
+ await this.registry.prepareProvider(providerId);
294
+ }
295
+ const target = this.registry.getConfigured().find((item) => item.id === providerId);
296
+ if (!target?.enabled || !target.apiKey) {
297
+ throw new Error(`Subagent route requires provider "${providerId}", but it is not configured or has no active credentials.`);
298
+ }
299
+ return createProviderInstance({
300
+ providerId,
301
+ apiKey: target.apiKey,
302
+ baseURL: target.baseURL,
303
+ protocol: target.protocol,
304
+ headers: target.headers,
305
+ openAICodexAuth: this.registry.createOpenAICodexAuthAdapter(providerId),
306
+ grokAuth: this.registry.createGrokAuthAdapter(providerId),
307
+ });
308
+ };
309
+ }
279
310
  /** MCP servers are started lazily, once per cwd (McpManager has no stop). */
280
311
  mcpToolsFor(cwd) {
281
312
  if (!this.mcpEnabled)
@@ -1,5 +1,5 @@
1
- import type { Message, Todo } from "./types.js";
2
- import type { SessionLogEntry, SessionMarkerKind, SessionMetadata, SessionSummaryEntry, SessionTodosSnapshotEntry } from "./session-types.js";
1
+ import type { Message } from "./types.js";
2
+ import type { SessionLogEntry, SessionMarkerKind, SessionMetadata, SessionSummaryEntry } from "./session-types.js";
3
3
  export declare class SessionLog {
4
4
  private entries;
5
5
  load(lines: string[]): void;
@@ -9,8 +9,6 @@ export declare class SessionLog {
9
9
  setMetadata(metadata: SessionMetadata): SessionLogEntry[];
10
10
  appendMessage(message: Message): SessionLogEntry[];
11
11
  appendSummary(summary: string): SessionSummaryEntry;
12
- appendTodosSnapshot(todos: Todo[]): SessionTodosSnapshotEntry;
13
- getTodos(): Todo[];
14
12
  appendMarker(kind: SessionMarkerKind, value: string): SessionLogEntry;
15
13
  toMessages(): Message[];
16
14
  }
@@ -60,28 +60,6 @@ export class SessionLog {
60
60
  this.entries.push(entry);
61
61
  return entry;
62
62
  }
63
- appendTodosSnapshot(todos) {
64
- const entry = {
65
- id: nextEntryId(this.entries),
66
- type: "todos_snapshot",
67
- todos: todos.map((todo) => ({ ...todo })),
68
- timestamp: Date.now(),
69
- };
70
- this.entries.push(entry);
71
- return entry;
72
- }
73
- getTodos() {
74
- for (let i = this.entries.length - 1; i >= 0; i--) {
75
- const entry = this.entries[i];
76
- if (entry.type === "marker" && entry.kind === "conversation_clear") {
77
- return [];
78
- }
79
- if (entry.type === "todos_snapshot") {
80
- return entry.todos.map((todo) => ({ ...todo }));
81
- }
82
- }
83
- return [];
84
- }
85
63
  appendMarker(kind, value) {
86
64
  const entry = {
87
65
  id: nextEntryId(this.entries),
@@ -239,7 +217,6 @@ function isSessionLogEntry(entry) {
239
217
  "assistant_message",
240
218
  "tool_call",
241
219
  "tool_result",
242
- "todos_snapshot",
243
220
  ].includes(entry.type);
244
221
  }
245
222
  function nextEntryId(entries) {
@@ -1,4 +1,4 @@
1
- import type { AssistantMessage, Message, ThinkingLevel, Todo, ToolCall, ToolMessage, UserMessage } from "./types.js";
1
+ import type { AssistantMessage, Message, ThinkingLevel, ToolCall, ToolMessage, UserMessage } from "./types.js";
2
2
  import type { GoalState } from "./goal/store.js";
3
3
  export interface ExternalRuntimeSessionMetadata {
4
4
  /** External agent runtime that owns the conversational state. */
@@ -65,11 +65,7 @@ export interface SessionToolResultEntry extends BaseSessionLogEntry {
65
65
  type: "tool_result";
66
66
  message: ToolMessage;
67
67
  }
68
- export interface SessionTodosSnapshotEntry extends BaseSessionLogEntry {
69
- type: "todos_snapshot";
70
- todos: Todo[];
71
- }
72
- export type SessionLogEntry = SessionMetadataEntry | SessionSummaryEntry | SessionMarkerEntry | SessionUserMessageEntry | SessionAssistantMessageEntry | SessionToolCallEntry | SessionToolResultEntry | SessionTodosSnapshotEntry;
68
+ export type SessionLogEntry = SessionMetadataEntry | SessionSummaryEntry | SessionMarkerEntry | SessionUserMessageEntry | SessionAssistantMessageEntry | SessionToolCallEntry | SessionToolResultEntry;
73
69
  export interface LegacySessionEntry {
74
70
  id: string;
75
71
  type: "metadata" | "message" | "compaction";
package/dist/session.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { CheckpointStore } from "./checkpoints.js";
5
5
  import { type CompactOptions, type CompactResult } from "./context/compact.js";
6
- import type { Message, Todo } from "./types.js";
6
+ import type { Message } from "./types.js";
7
7
  import type { SessionLogEntry, SessionMarkerKind, SessionMetadata } from "./session-types.js";
8
8
  export interface SessionSummary {
9
9
  file: string;
@@ -55,8 +55,6 @@ export declare class SessionManager {
55
55
  appendMessage(message: Message): void;
56
56
  appendCompaction(summary: string): void;
57
57
  appendMarker(kind: SessionMarkerKind, value: string): void;
58
- appendTodosSnapshot(todos: Todo[]): void;
59
- getTodos(): Todo[];
60
58
  compact(options?: CompactOptions): CompactResult;
61
59
  /**
62
60
  * Inspect whether the session is large enough to compact and, if so, return
package/dist/session.js CHANGED
@@ -152,14 +152,6 @@ export class SessionManager {
152
152
  const entry = this.log.appendMarker(kind, value);
153
153
  this.persist(entry);
154
154
  }
155
- appendTodosSnapshot(todos) {
156
- const entry = this.log.appendTodosSnapshot(todos);
157
- this.persist(entry);
158
- this.maybeAutoCompact();
159
- }
160
- getTodos() {
161
- return this.log.getTodos();
162
- }
163
155
  compact(options) {
164
156
  const result = compactSessionEntries(this.log.list(), options);
165
157
  if (result.compacted && result.entries) {
@@ -490,9 +490,6 @@ const builtinSlashCommandEntries = [
490
490
  ctx.agent.resetContextUsageAnchor();
491
491
  ctx.sessionManager?.appendMarker("conversation_clear", "");
492
492
  ctx.sessionManager?.clearTitleMetadata?.();
493
- if (ctx.agent.getTodos().length > 0) {
494
- ctx.agent.setTodos([]);
495
- }
496
493
  ctx.clearMessages();
497
494
  },
498
495
  },
@@ -563,7 +560,6 @@ const builtinSlashCommandEntries = [
563
560
  session.rewindToEntry(target.id);
564
561
  const head = ctx.agent.messages.filter((m) => m.role === "system" || m.role === "meta");
565
562
  ctx.agent.messages = [...head, ...session.getMessages()];
566
- ctx.agent.setTodos(session.getTodos());
567
563
  ctx.agent.resetContextUsageAnchor();
568
564
  if (ctx.fillComposer) {
569
565
  // Put the rewound message back into the input box for re-editing.
@@ -33,7 +33,7 @@ export declare class WorktreeApprovalController implements ApprovalController {
33
33
  * exposes a cloneForChild hook (the standard `read`, which carries a
34
34
  * FileStateTracker) is rebuilt as a fresh per-child instance, so concurrent
35
35
  * members of a fan-out never share mutable tool state. Stateless tools
36
- * (glob/grep, web/memory/skill/todo) and custom/mock tools without the hook are
36
+ * (glob/grep, web/memory/skill) and custom/mock tools without the hook are
37
37
  * passed through unchanged. Write children get full isolation via
38
38
  * createWorktreeChildTools instead.
39
39
  */
@@ -97,7 +97,7 @@ const WORKTREE_TOOL_NAMES = new Set(["read", "glob", "grep", "edit", "write", "b
97
97
  * exposes a cloneForChild hook (the standard `read`, which carries a
98
98
  * FileStateTracker) is rebuilt as a fresh per-child instance, so concurrent
99
99
  * members of a fan-out never share mutable tool state. Stateless tools
100
- * (glob/grep, web/memory/skill/todo) and custom/mock tools without the hook are
100
+ * (glob/grep, web/memory/skill) and custom/mock tools without the hook are
101
101
  * passed through unchanged. Write children get full isolation via
102
102
  * createWorktreeChildTools instead.
103
103
  */
@@ -15,7 +15,6 @@ export { createWebSearchTool } from "./web-search.js";
15
15
  export { createSkillTool } from "./skill.js";
16
16
  export { createSkillSearchTool } from "./skill-search.js";
17
17
  export { createAgentLifecycleTools, createCloseAgentTool, createSendInputTool, createSpawnAgentTool, createWaitAgentTool } from "./agent-lifecycle.js";
18
- export { createTodoTool, type TodoStore } from "./todo.js";
19
18
  export { createExitPlanModeTool, type PlanController } from "./exit-plan-mode.js";
20
19
  export { createToolSearchTool, type ToolSearchController } from "./tool-search.js";
21
20
  export { createQuestionTool } from "./question.js";
@@ -25,7 +24,6 @@ import type { ApprovalController } from "../approval/types.js";
25
24
  import type { SkillRegistry } from "../skills/registry.js";
26
25
  import { type PlanController } from "./exit-plan-mode.js";
27
26
  import { type LspService } from "../lsp/index.js";
28
- import { type TodoStore } from "./todo.js";
29
27
  import { type ToolSearchController } from "./tool-search.js";
30
28
  import type { QuestionController } from "../question/index.js";
31
29
  import type { CheckpointStore } from "../checkpoints.js";
@@ -34,7 +32,6 @@ import type { GoalStore } from "../goal/store.js";
34
32
  import type { ProcessManager } from "../tasks/manager.js";
35
33
  import type { PromotionChannel } from "../tasks/promotion.js";
36
34
  export interface CreateAllToolsOptions {
37
- todoStore?: TodoStore;
38
35
  planController?: PlanController;
39
36
  approvalController?: ApprovalController;
40
37
  questionController?: QuestionController;
@@ -15,7 +15,6 @@ export { createWebSearchTool } from "./web-search.js";
15
15
  export { createSkillTool } from "./skill.js";
16
16
  export { createSkillSearchTool } from "./skill-search.js";
17
17
  export { createAgentLifecycleTools, createCloseAgentTool, createSendInputTool, createSpawnAgentTool, createWaitAgentTool } from "./agent-lifecycle.js";
18
- export { createTodoTool } from "./todo.js";
19
18
  export { createExitPlanModeTool } from "./exit-plan-mode.js";
20
19
  export { createToolSearchTool } from "./tool-search.js";
21
20
  export { createQuestionTool } from "./question.js";
@@ -32,7 +31,6 @@ import { createReadTool } from "./read.js";
32
31
  import { createSkillTool } from "./skill.js";
33
32
  import { createSkillSearchTool } from "./skill-search.js";
34
33
  import { createAgentLifecycleTools } from "./agent-lifecycle.js";
35
- import { createTodoTool } from "./todo.js";
36
34
  import { createToolSearchTool } from "./tool-search.js";
37
35
  import { createWebFetchTool } from "./web-fetch.js";
38
36
  import { createWebSearchTool } from "./web-search.js";
@@ -71,7 +69,6 @@ export function createAllTools(cwd, skillRegistry, options = {}) {
71
69
  createToolSearchTool(options.toolSearchController),
72
70
  ...(options.questionController ? [createQuestionTool(options.questionController)] : []),
73
71
  ...(skillRegistry ? [createSkillSearchTool(skillRegistry), createSkillTool(skillRegistry)] : []),
74
- ...(options.todoStore ? [createTodoTool(options.todoStore)] : []),
75
72
  ...(options.planController ? [createExitPlanModeTool(options.planController)] : []),
76
73
  ...(options.goalStore ? createGoalTools(options.goalStore) : []),
77
74
  ];
@@ -23,7 +23,6 @@ import { useTerminalSize } from "./use-terminal-size.js";
23
23
  import { WelcomeBanner, shouldShowWelcomeBanner } from "./welcome.js";
24
24
  import { BubbleCodeWordmark } from "./wordmark.js";
25
25
  import { expandAtMentions } from "./file-mentions.js";
26
- import { TodosPanel } from "./todos.js";
27
26
  import { CompactionProgressCard } from "./compaction-progress.js";
28
27
  import { PlanConfirm } from "./plan-confirm.js";
29
28
  import { ApprovalDialog } from "./approval/approval-dialog.js";
@@ -146,7 +145,6 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
146
145
  // model ladder.
147
146
  const [thinkingLevel, setThinkingLevel] = useState(agent.thinking);
148
147
  const [permissionMode, setPermissionMode] = useState(agent.mode);
149
- const [todos, setTodos] = useState(() => agent.getTodos());
150
148
  const [goalLine, setGoalLine] = useState("");
151
149
  const [branch, setBranch] = useState(undefined);
152
150
  const [contextUsage, setContextUsage] = useState("");
@@ -644,10 +642,6 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
644
642
  // <Static>), so clearing React state is not enough — resetTranscript wipes
645
643
  // the screen + scrollback and re-prints the (now empty) transcript.
646
644
  resetTranscript(() => []);
647
- // The todos panel renders off React state, not the transcript, so wiping
648
- // messages alone leaves a stale To-Do list on screen. /clear already reset
649
- // the agent's todos; mirror that into the UI (same as session switch).
650
- setTodos(agent.getTodos());
651
645
  clearLiveSubagentTools();
652
646
  }, [resetTranscript, agent, clearLiveSubagentTools]);
653
647
  // Render a placeholder user row for input waiting to enter the run.
@@ -805,7 +799,6 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
805
799
  const nextExternalBinding = result.manager.getMetadata().externalRuntime;
806
800
  externalRuntimeBindingRef.current = nextExternalBinding;
807
801
  setExternalRuntimeBinding(nextExternalBinding);
808
- setTodos(agent.getTodos());
809
802
  clearLiveSubagentTools();
810
803
  resetTranscript(() => [
811
804
  ...reconstructDisplayMessages(agent.messages).filter((message) => !queuedDisplayKeys.has(message.key ?? "")),
@@ -1513,10 +1506,6 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1513
1506
  }
1514
1507
  break;
1515
1508
  }
1516
- case "todos_updated": {
1517
- setTodos(event.todos);
1518
- break;
1519
- }
1520
1509
  case "mode_changed": {
1521
1510
  // Marker already persisted by onModeUpdate at setMode time.
1522
1511
  setPermissionMode(event.mode);
@@ -2318,7 +2307,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
2318
2307
  }, onCancel: () => {
2319
2308
  closePicker();
2320
2309
  addMessage("assistant", "已取消 Feishu setup。");
2321
- } }) })), statsPanel && !pickerMode && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(StatsPanel, { panel: statsPanel, terminalColumns: mainWidth, terminalRows: terminalRows, onRangeChange: (range) => setStatsPanel((current) => current ? { ...current, range } : current), onCancel: closeStatsPanel }) })), todos.length > 0 && !pickerMode && !statsPanel && !pendingPlan && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(TodosPanel, { todos: todos, terminalColumns: terminalColumns }) })), pendingPlan && !pickerMode && !statsPanel && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(PlanConfirm, { initialPlan: pendingPlan.plan, onApprove: (finalPlan) => {
2310
+ } }) })), statsPanel && !pickerMode && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(StatsPanel, { panel: statsPanel, terminalColumns: mainWidth, terminalRows: terminalRows, onRangeChange: (range) => setStatsPanel((current) => current ? { ...current, range } : current), onCancel: closeStatsPanel }) })), pendingPlan && !pickerMode && !statsPanel && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(PlanConfirm, { initialPlan: pendingPlan.plan, onApprove: (finalPlan) => {
2322
2311
  const resolve = pendingPlan.resolve;
2323
2312
  setPendingPlan(null);
2324
2313
  resolve({ action: "approve", plan: finalPlan });
@@ -442,7 +442,6 @@ const TOOL_GLYPHS = {
442
442
  web_fetch: "⇲",
443
443
  web_search: "⌕",
444
444
  task: "↳",
445
- todo: "✓",
446
445
  skill: "★",
447
446
  };
448
447
  function displayToolName(name) {
package/dist/types.d.ts CHANGED
@@ -284,12 +284,6 @@ export type PlanDecision = {
284
284
  action: "reject";
285
285
  reason?: string;
286
286
  };
287
- export type TodoStatus = "pending" | "in_progress" | "completed";
288
- export interface Todo {
289
- content: string;
290
- status: TodoStatus;
291
- activeForm: string;
292
- }
293
287
  export type StreamChunk = {
294
288
  type: "text";
295
289
  content: string;
@@ -463,9 +457,6 @@ export type AgentEvent = {
463
457
  } | {
464
458
  type: "mode_changed";
465
459
  mode: PermissionMode;
466
- } | {
467
- type: "todos_updated";
468
- todos: Todo[];
469
460
  } | {
470
461
  type: "agent_end";
471
462
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bubblebrain-ai/bubble",
3
- "version": "0.0.50",
3
+ "version": "0.0.52",
4
4
  "description": "A terminal coding agent",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,12 +0,0 @@
1
- /**
2
- * Todo tool - manage an agent-visible task list for multi-step work.
3
- *
4
- * The tool overwrites the entire list on each call. The model should re-send the full
5
- * array with updated statuses rather than trying to patch individual items.
6
- */
7
- import type { Todo, ToolRegistryEntry } from "../types.js";
8
- export interface TodoStore {
9
- getTodos: () => Todo[];
10
- setTodos: (todos: Todo[]) => void;
11
- }
12
- export declare function createTodoTool(store: TodoStore): ToolRegistryEntry;
@@ -1,136 +0,0 @@
1
- /**
2
- * Todo tool - manage an agent-visible task list for multi-step work.
3
- *
4
- * The tool overwrites the entire list on each call. The model should re-send the full
5
- * array with updated statuses rather than trying to patch individual items.
6
- */
7
- export function createTodoTool(store) {
8
- return {
9
- name: "todo_write",
10
- readOnly: true,
11
- effect: "read",
12
- description: `Create or update the task list for the current work. Send the COMPLETE list each call; this overwrites the prior list entirely.
13
-
14
- ## When to use
15
-
16
- Default to just doing the work. Reach for a list only when actively tracking progress would genuinely help you or the user follow it — never to pad simple work with filler steps or to state the obvious. When in doubt, skip the list and do the task; a list you never meaningfully update is just noise.
17
-
18
- A list earns its place when:
19
- - The task is non-trivial and spans many actions across several areas of the codebase
20
- - There are non-obvious phases or dependencies you must hold in mind to avoid losing track (a plain read → edit → test sequence does not count)
21
- - The work is ambiguous and benefits from outlining the goals up front
22
- - The user asked for several distinct things in one prompt, or gave a numbered/bulleted list
23
- - The user explicitly asked for a todo list (aka TODOs)
24
- - You discover extra steps mid-task and intend to finish them before yielding
25
-
26
- ## Quality bar
27
-
28
- If you do make a list, make a good one: meaningful, logically ordered steps that are easy to verify as you go.
29
-
30
- Good — distinct, verifiable steps for genuinely multi-part work:
31
- 1. Add CSS variables for the color palette
32
- 2. Add the toggle with localStorage state
33
- 3. Refactor components to use the variables
34
- 4. Verify every view for readability
35
-
36
- Good — scope a search uncovers makes the list worth it:
37
- "Rename getCwd across the project" → grep finds 15 call sites in 8 files → one item per file so none are missed.
38
-
39
- Bad — padding a task you could just do; do NOT create a list for this:
40
- "Fix the typo in the README title" → 1. Find typo 2. Open file 3. Fix it 4. Save. That is one edit — just make it.
41
-
42
- Bad — vague, unverifiable filler: "Make it work", "Improve the styling", "Clean things up".
43
-
44
- ## Task states
45
-
46
- - pending: not yet started
47
- - in_progress: currently working on — exactly ONE at a time
48
- - completed: finished successfully
49
-
50
- Each item needs:
51
- - content: imperative form (e.g. "Run tests")
52
- - activeForm: present continuous, shown while in progress (e.g. "Running tests")
53
-
54
- ## Rules
55
-
56
- - Update status in real time; mark completed IMMEDIATELY on finishing.
57
- - Never mark completed if tests are failing, implementation is partial, errors are unresolved, or needed files are missing — keep as in_progress.
58
- - When blocked, add a new task describing what must be resolved.
59
- - Remove items that are no longer relevant; don't leave stale entries.
60
- - Do not re-send the list when nothing meaningful has changed since the last call; update only after real progress.`,
61
- parameters: {
62
- type: "object",
63
- properties: {
64
- todos: {
65
- type: "array",
66
- description: "The complete list of todos. Replaces any existing list.",
67
- items: {
68
- type: "object",
69
- properties: {
70
- content: {
71
- type: "string",
72
- description: "Imperative form describing the task (e.g., 'Add unit tests for foo')",
73
- },
74
- activeForm: {
75
- type: "string",
76
- description: "Present continuous form shown while in progress (e.g., 'Adding unit tests for foo')",
77
- },
78
- status: {
79
- type: "string",
80
- enum: ["pending", "in_progress", "completed"],
81
- description: "Current status of the task",
82
- },
83
- },
84
- required: ["content", "activeForm", "status"],
85
- },
86
- },
87
- },
88
- required: ["todos"],
89
- },
90
- async execute(args) {
91
- if (!Array.isArray(args.todos)) {
92
- return { content: "Error: 'todos' must be an array", isError: true };
93
- }
94
- const normalized = [];
95
- for (let i = 0; i < args.todos.length; i++) {
96
- const raw = args.todos[i];
97
- if (!raw || typeof raw !== "object") {
98
- return { content: `Error: todo at index ${i} is not an object`, isError: true };
99
- }
100
- const content = typeof raw.content === "string" ? raw.content.trim() : "";
101
- const activeForm = typeof raw.activeForm === "string" ? raw.activeForm.trim() : "";
102
- const status = raw.status;
103
- if (!content) {
104
- return { content: `Error: todo at index ${i} has empty content`, isError: true };
105
- }
106
- if (!activeForm) {
107
- return { content: `Error: todo at index ${i} has empty activeForm`, isError: true };
108
- }
109
- if (status !== "pending" && status !== "in_progress" && status !== "completed") {
110
- return {
111
- content: `Error: todo at index ${i} has invalid status "${status}". Must be pending|in_progress|completed`,
112
- isError: true,
113
- };
114
- }
115
- normalized.push({ content, activeForm, status });
116
- }
117
- const inProgressCount = normalized.filter((t) => t.status === "in_progress").length;
118
- if (inProgressCount > 1) {
119
- return {
120
- content: `Error: at most one todo may be in_progress at a time, found ${inProgressCount}`,
121
- isError: true,
122
- };
123
- }
124
- store.setTodos(normalized);
125
- const counts = {
126
- pending: normalized.filter((t) => t.status === "pending").length,
127
- in_progress: inProgressCount,
128
- completed: normalized.filter((t) => t.status === "completed").length,
129
- };
130
- return {
131
- content: `Todo list updated: ${normalized.length} item${normalized.length === 1 ? "" : "s"} ` +
132
- `(${counts.completed} completed, ${counts.in_progress} in progress, ${counts.pending} pending).`,
133
- };
134
- },
135
- };
136
- }
@@ -1,7 +0,0 @@
1
- import type { Todo } from "../types.js";
2
- interface TodosPanelProps {
3
- todos: Todo[];
4
- terminalColumns: number;
5
- }
6
- export declare function TodosPanel({ todos, terminalColumns }: TodosPanelProps): import("react/jsx-runtime").JSX.Element | null;
7
- export {};
@@ -1,46 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from "ink";
3
- import { useTheme } from "./theme.js";
4
- const MAX_ROWS = 8;
5
- export function TodosPanel({ todos, terminalColumns }) {
6
- const theme = useTheme();
7
- if (todos.length === 0) {
8
- return null;
9
- }
10
- const rows = selectVisibleRows(todos);
11
- const hiddenCount = todos.length - rows.length;
12
- const contentWidth = Math.max(20, terminalColumns - 4);
13
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: "\u25CF Todos" }), rows.map((todo, index) => (_jsx(TodoRow, { todo: todo, maxWidth: contentWidth }, index))), hiddenCount > 0 && (_jsxs(Text, { color: theme.muted, children: ["... ", hiddenCount, " more item", hiddenCount === 1 ? "" : "s", " hidden"] }))] }));
14
- }
15
- function selectVisibleRows(todos) {
16
- if (todos.length <= MAX_ROWS) {
17
- return todos;
18
- }
19
- // Prefer to show: all in_progress, the last N completed just before current, and upcoming pending.
20
- const inProgressIdx = todos.findIndex((t) => t.status === "in_progress");
21
- const anchor = inProgressIdx >= 0 ? inProgressIdx : todos.findIndex((t) => t.status === "pending");
22
- const pivot = anchor >= 0 ? anchor : 0;
23
- const half = Math.floor(MAX_ROWS / 2);
24
- let start = Math.max(0, pivot - half);
25
- let end = Math.min(todos.length, start + MAX_ROWS);
26
- if (end - start < MAX_ROWS) {
27
- start = Math.max(0, end - MAX_ROWS);
28
- }
29
- return todos.slice(start, end);
30
- }
31
- function TodoRow({ todo, maxWidth }) {
32
- const theme = useTheme();
33
- const { glyph, color, dim, label } = statusStyle(todo, theme);
34
- const text = label || todo.content;
35
- const trimmed = text.length > maxWidth - 4 ? text.slice(0, maxWidth - 5) + "…" : text;
36
- return (_jsx(Box, { height: 1, children: _jsxs(Text, { color: color, dimColor: dim, children: [glyph, " ", trimmed] }) }));
37
- }
38
- function statusStyle(todo, theme) {
39
- if (todo.status === "completed") {
40
- return { glyph: "✔", color: theme.muted, dim: true, label: todo.content };
41
- }
42
- if (todo.status === "in_progress") {
43
- return { glyph: "▶", color: theme.accent, dim: false, label: todo.activeForm || todo.content };
44
- }
45
- return { glyph: "○", color: theme.muted, dim: false, label: todo.content };
46
- }