@bubblebrain-ai/bubble 0.0.33 → 0.0.35

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 (61) hide show
  1. package/dist/agent/abort-errors.d.ts +2 -2
  2. package/dist/agent/budget-ledger.d.ts +4 -45
  3. package/dist/agent/budget-ledger.js +5 -75
  4. package/dist/agent/child-runner.d.ts +1 -3
  5. package/dist/agent/child-runner.js +6 -37
  6. package/dist/agent/profiles.d.ts +0 -2
  7. package/dist/agent/profiles.js +0 -3
  8. package/dist/agent/subagent-control.d.ts +5 -12
  9. package/dist/agent/subagent-control.js +0 -2
  10. package/dist/agent/subagent-store.d.ts +1 -1
  11. package/dist/agent/subagent-store.js +1 -3
  12. package/dist/agent/workflow/control.d.ts +7 -0
  13. package/dist/agent/workflow/control.js +22 -1
  14. package/dist/agent/workflow/runtime.d.ts +15 -2
  15. package/dist/agent/workflow/runtime.js +112 -30
  16. package/dist/agent.d.ts +7 -43
  17. package/dist/agent.js +50 -176
  18. package/dist/config.d.ts +1 -3
  19. package/dist/config.js +6 -5
  20. package/dist/context/overflow.js +2 -0
  21. package/dist/main.js +9 -1
  22. package/dist/model-catalog.d.ts +1 -1
  23. package/dist/model-catalog.js +25 -18
  24. package/dist/orchestrator/default-hooks.js +7 -1
  25. package/dist/prompt/delegation.d.ts +1 -1
  26. package/dist/prompt/delegation.js +21 -7
  27. package/dist/prompt/task-reminders.d.ts +1 -0
  28. package/dist/prompt/task-reminders.js +24 -1
  29. package/dist/provider-ai-sdk.d.ts +56 -0
  30. package/dist/provider-ai-sdk.js +518 -0
  31. package/dist/provider-registry.js +49 -3
  32. package/dist/provider-transform.d.ts +1 -1
  33. package/dist/provider-transform.js +8 -13
  34. package/dist/provider.js +4 -0
  35. package/dist/slash-commands/commands.js +3 -7
  36. package/dist/slash-commands/types.d.ts +1 -1
  37. package/dist/tools/agent-lifecycle.d.ts +1 -9
  38. package/dist/tools/agent-lifecycle.js +35 -263
  39. package/dist/tui-ink/app.js +51 -13
  40. package/dist/tui-ink/approval/approval-dialog.js +1 -1
  41. package/dist/tui-ink/approval/diff-view.js +4 -4
  42. package/dist/tui-ink/approval/select.js +2 -2
  43. package/dist/tui-ink/feedback-dialog.js +2 -2
  44. package/dist/tui-ink/feishu-setup-picker.js +3 -3
  45. package/dist/tui-ink/input-box.d.ts +1 -6
  46. package/dist/tui-ink/input-box.js +14 -35
  47. package/dist/tui-ink/markdown.js +11 -9
  48. package/dist/tui-ink/message-list.d.ts +5 -1
  49. package/dist/tui-ink/message-list.js +15 -2
  50. package/dist/tui-ink/model-picker.d.ts +3 -1
  51. package/dist/tui-ink/model-picker.js +25 -14
  52. package/dist/tui-ink/plan-confirm.js +1 -1
  53. package/dist/tui-ink/subagent-view.d.ts +3 -1
  54. package/dist/tui-ink/theme.d.ts +19 -2
  55. package/dist/tui-ink/theme.js +25 -8
  56. package/dist/tui-ink/welcome.d.ts +3 -2
  57. package/dist/tui-ink/welcome.js +10 -6
  58. package/dist/types.d.ts +16 -30
  59. package/dist/variant/variant-resolver.d.ts +13 -0
  60. package/dist/variant/variant-resolver.js +26 -0
  61. package/package.json +9 -3
@@ -329,19 +329,15 @@ const builtinSlashCommandEntries = [
329
329
  },
330
330
  {
331
331
  name: "theme",
332
- description: "Switch the color theme. Usage: /theme [auto|light|dark]",
332
+ description: "Pick the color theme. Usage: /theme [auto|light|dark]",
333
333
  async handler(args, ctx) {
334
334
  if (!ctx.setThemeMode || !ctx.getThemeMode || !ctx.getResolvedTheme) {
335
335
  return "Theme switching is only available inside the TUI.";
336
336
  }
337
337
  const arg = args.trim().toLowerCase();
338
338
  if (!arg) {
339
- const order = ["auto", "light", "dark"];
340
- const current = ctx.getThemeMode();
341
- const next = order[(order.indexOf(current) + 1) % order.length];
342
- ctx.setThemeMode(next);
343
- const resolved = next === "auto" ? ctx.getResolvedTheme() : next;
344
- return `Theme: ${next}${next === "auto" ? ` (resolved to ${resolved})` : ""}`;
339
+ ctx.openPicker("theme");
340
+ return;
345
341
  }
346
342
  if (arg !== "auto" && arg !== "light" && arg !== "dark") {
347
343
  return "Usage: /theme [auto|light|dark]";
@@ -28,7 +28,7 @@ export interface SlashCommandContext {
28
28
  exit: () => void;
29
29
  sessionManager?: SessionManager;
30
30
  createProvider: (providerId: string, apiKey: string, baseURL: string) => Provider;
31
- openPicker: (mode: "model" | "key" | "provider" | "provider-add" | "login" | "logout" | "skill" | "feishu-setup", providerId?: string) => void;
31
+ openPicker: (mode: "model" | "key" | "provider" | "provider-add" | "login" | "logout" | "skill" | "theme" | "feishu-setup", providerId?: string) => void;
32
32
  registry: ProviderRegistry;
33
33
  skillRegistry: SkillRegistry;
34
34
  bashAllowlist?: BashAllowlist;
@@ -24,15 +24,7 @@ export declare function createWaitAgentTool(): ToolRegistryEntry;
24
24
  export declare function createSendInputTool(): ToolRegistryEntry;
25
25
  export declare function createCloseAgentTool(): ToolRegistryEntry;
26
26
  export declare function createListAgentsTool(): ToolRegistryEntry;
27
- /** Items bound for one agent_team call (design §1.2). */
28
- export declare const AGENT_TEAM_MIN_ITEMS = 2;
29
- export declare const AGENT_TEAM_MAX_ITEMS = 32;
30
- export declare function createAgentTeamTool(options?: AgentLifecycleToolOptions, sharedTrust?: ProjectProfileTrust): ToolRegistryEntry;
31
- /** Specs bound for one agent_batch call (design v2 §1.3). */
32
- export declare const AGENT_BATCH_MIN_SPECS = 2;
33
- export declare const AGENT_BATCH_MAX_SPECS = 32;
34
- export declare function createAgentBatchTool(options?: AgentLifecycleToolOptions, sharedTrust?: ProjectProfileTrust): ToolRegistryEntry;
35
- export declare function createRunWorkflowTool(options?: AgentLifecycleToolOptions): ToolRegistryEntry;
27
+ export declare function createRunWorkflowTool(options?: AgentLifecycleToolOptions, sharedTrust?: ProjectProfileTrust): ToolRegistryEntry;
36
28
  export declare function createWaitWorkflowTool(): ToolRegistryEntry;
37
29
  export declare function createAgentLifecycleTools(options?: AgentLifecycleToolOptions): ToolRegistryEntry[];
38
30
  export {};
@@ -2,6 +2,8 @@ import { createHash } from "node:crypto";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { discoverAgentProfiles, findAgentProfile } from "../agent/profiles.js";
4
4
  import { parseThinkingLevel } from "../agent/categories.js";
5
+ import { workflowMemberWarning } from "../agent/workflow/control.js";
6
+ import { precompileWorkflowScript } from "../agent/workflow/runtime.js";
5
7
  import { formatSubagentRoute } from "../agent/subagent-route-format.js";
6
8
  /**
7
9
  * Session-scoped trust decisions for project profiles, keyed by file path +
@@ -102,6 +104,7 @@ export function createSpawnAgentTool(options = {}, sharedTrust) {
102
104
  "Start a child subagent in the background and return its agent_id plus random nickname.",
103
105
  "The child has an independent thread; call wait_agent later to collect its result.",
104
106
  "Proactively delegate multi-file investigations whose intermediate steps would be noise in the main conversation.",
107
+ "Best for helpers whose results you want to read and react to individually. For a uniform sweep over many items, or when member results should be aggregated before returning, weigh run_workflow instead. If the user asked for a workflow, an orchestration, or an agent team, that is run_workflow, not this tool.",
105
108
  "Do the work yourself when it takes only a couple of tool calls or needs conversation context — unless the user explicitly asks for a subagent, in which case spawn one.",
106
109
  "The child starts with zero context: write the task as a self-contained work order — state the goal, include known file paths or commands, and never make it rediscover knowledge you already hold.",
107
110
  "After spawning, do not duplicate the same delegated work locally; either wait for the child or do clearly non-overlapping work.",
@@ -377,266 +380,31 @@ export function createListAgentsTool() {
377
380
  },
378
381
  };
379
382
  }
380
- /** Items bound for one agent_team call (design §1.2). */
381
- export const AGENT_TEAM_MIN_ITEMS = 2;
382
- export const AGENT_TEAM_MAX_ITEMS = 32;
383
- export function createAgentTeamTool(options = {}, sharedTrust) {
383
+ export function createRunWorkflowTool(options = {}, sharedTrust) {
384
384
  const trust = sharedTrust ?? new ProjectProfileTrust(options.approval);
385
- return {
386
- name: "agent_team",
387
- readOnly: true,
388
- effect: "read",
389
- description: [
390
- "Run the same task template over many items as parallel subagents (homogeneous map fan-out).",
391
- "Proactively use this when a task naturally splits into the same read-only operation over several independent items.",
392
- "Each item becomes one child with its own agent_id; the call blocks until every member reaches a final state and returns results in item order.",
393
- "Failed members can be resumed individually with send_input afterwards.",
394
- `Use ${AGENT_TEAM_MIN_ITEMS}-${AGENT_TEAM_MAX_ITEMS} items. agent_team must be the ONLY tool call in your response; run other tools or further teams after it returns.`,
395
- "Scoping rule: split items so members never overlap or conflict with each other.",
396
- ].join(" "),
397
- parameters: {
398
- type: "object",
399
- properties: {
400
- description: { type: "string", description: "Short (3-5 word) description of the team, shown in the UI." },
401
- agent_type: { type: "string", description: "Subagent profile for every member. Defaults to default." },
402
- category: { type: "string", description: "Optional semantic category for model/thinking routing." },
403
- model: { type: "string", description: "Optional per-call model for every member, overriding category and profile (bare name or provider:model)." },
404
- effort: { type: "string", enum: ["off", "minimal", "low", "medium", "high", "xhigh", "max"], description: "Optional per-call thinking level for every member." },
405
- prompt_template: { type: "string", description: "Task template applied to each item. Must contain the literal placeholder {{item}}." },
406
- items: {
407
- type: "array",
408
- description: `Items to fan out over (${AGENT_TEAM_MIN_ITEMS}-${AGENT_TEAM_MAX_ITEMS} unique strings); each becomes one subagent.`,
409
- items: { type: "string" },
410
- },
411
- },
412
- required: ["description", "prompt_template", "items"],
413
- additionalProperties: false,
414
- },
415
- async execute(args, ctx) {
416
- if (!ctx.agent?.runAgentTeam) {
417
- return toolRuntimeMissing("agent_team");
418
- }
419
- const template = stringArg(args.prompt_template);
420
- if (!template) {
421
- return { content: "Error: agent_team requires prompt_template.", isError: true };
422
- }
423
- if (!template.includes("{{item}}")) {
424
- return {
425
- content: "Error: prompt_template must contain the literal placeholder {{item}} — it is replaced with each item. Example: \"Review {{item}} for risks.\"",
426
- isError: true,
427
- };
428
- }
429
- const rawItems = Array.isArray(args.items)
430
- ? args.items.filter((item) => typeof item === "string" && !!item.trim()).map((item) => item.trim())
431
- : [];
432
- const items = [...new Set(rawItems)];
433
- if (items.length < AGENT_TEAM_MIN_ITEMS) {
434
- return {
435
- content: `Error: agent_team needs at least ${AGENT_TEAM_MIN_ITEMS} unique items after deduplication (got ${items.length}). For a single task use spawn_agent instead.`,
436
- isError: true,
437
- };
438
- }
439
- if (items.length > AGENT_TEAM_MAX_ITEMS) {
440
- return {
441
- content: `Error: agent_team accepts at most ${AGENT_TEAM_MAX_ITEMS} items (got ${items.length}). Split the work into sequential teams.`,
442
- isError: true,
443
- };
444
- }
445
- const profileName = stringArg(args.agent_type) ?? "default";
446
- const resolved = resolveProfile(ctx.cwd, profileName, "both");
447
- if ("error" in resolved)
448
- return resolved.error;
449
- const modeBlock = unsupportedProfile(resolved.profile);
450
- if (modeBlock)
451
- return modeBlock;
452
- const trustBlock = await trust.ensureTrusted(resolved.profile);
453
- if (trustBlock)
454
- return trustBlock;
455
- const effort = parseEffortArg(args.effort);
456
- if ("error" in effort)
457
- return effort.error;
458
- try {
459
- const snapshots = await ctx.agent.runAgentTeam(ctx.cwd, {
460
- profile: resolved.profile,
461
- category: stringArg(args.category),
462
- model: stringArg(args.model),
463
- effort: effort.value,
464
- promptTemplate: template,
465
- items,
466
- parentToolCallId: ctx.toolCall?.id ?? snapshotFallbackId(),
467
- emitUpdate: ctx.emitUpdate,
468
- abortSignal: ctx.abortSignal,
469
- });
470
- const counts = teamStatusCounts(snapshots);
471
- const lines = [
472
- `agent_team "${stringArg(args.description) ?? "team"}": ${snapshots.length} members — ${counts}`,
473
- "Failed or cancelled members can be resumed individually with send_input (see per-member guidance below).",
474
- "",
475
- ...snapshots.flatMap((snapshot, index) => [
476
- `### item ${index + 1}: ${truncateText(items[index] ?? "", 100)}`,
477
- ...formatSnapshot(snapshot),
478
- "",
479
- ]),
480
- ];
481
- return {
482
- content: lines.join("\n").trim(),
483
- status: snapshots.every((snapshot) => snapshot.status === "completed")
484
- ? "success"
485
- : snapshots.some((snapshot) => snapshot.status === "completed")
486
- ? "partial"
487
- : "blocked",
488
- isError: snapshots.length > 0 && snapshots.every((snapshot) => snapshot.status !== "completed"),
489
- metadata: {
490
- kind: "subagent",
491
- mode: "team",
492
- subagents: snapshots.map(snapshotToMetadata),
493
- },
494
- };
495
- }
496
- catch (error) {
497
- return toolError("agent_team", error);
498
- }
499
- },
385
+ // Single-flight: the interactive UI holds ONE pending approval at a time,
386
+ // so concurrent trust prompts from a parallel() fan-out would overwrite
387
+ // each other's resolver and hang the workflow. Serializing here also lets
388
+ // the first approval satisfy the rest of the same profile via the cache.
389
+ let trustChain = Promise.resolve();
390
+ const ensureProfileTrustedSerially = (profile) => {
391
+ const next = trustChain.then(() => trust.ensureTrusted(profile));
392
+ trustChain = next.then(() => undefined, () => undefined);
393
+ return next;
500
394
  };
501
- }
502
- /** Specs bound for one agent_batch call (design v2 §1.3). */
503
- export const AGENT_BATCH_MIN_SPECS = 2;
504
- export const AGENT_BATCH_MAX_SPECS = 32;
505
- export function createAgentBatchTool(options = {}, sharedTrust) {
506
- const trust = sharedTrust ?? new ProjectProfileTrust(options.approval);
507
- return {
508
- name: "agent_batch",
509
- readOnly: true,
510
- effect: "read",
511
- description: [
512
- "Run several DIFFERENT subagent tasks in parallel as one tool call (heterogeneous fan-out).",
513
- "Unlike agent_team (one template over many items), each spec is its own task with its own optional model/effort/profile/output_schema.",
514
- "Use it to run, e.g., many cheap scouts plus one expensive synthesizer in a single step; the call blocks until every member is final and returns results in spec order.",
515
- `Provide ${AGENT_BATCH_MIN_SPECS}-${AGENT_BATCH_MAX_SPECS} specs. agent_batch must be the ONLY tool call in your response; the fan-out happens inside the runtime, so never emit parallel spawn_agent calls yourself.`,
516
- "Scoping rule: split specs so members never overlap or conflict.",
517
- ].join(" "),
518
- parameters: {
519
- type: "object",
520
- properties: {
521
- description: { type: "string", description: "Short (3-5 word) description of the batch, shown in the UI." },
522
- specs: {
523
- type: "array",
524
- description: `Heterogeneous member specs (${AGENT_BATCH_MIN_SPECS}-${AGENT_BATCH_MAX_SPECS}); each becomes one subagent.`,
525
- items: {
526
- type: "object",
527
- properties: {
528
- task: { type: "string", description: "Self-contained task for this member." },
529
- agent_type: { type: "string", description: "Subagent profile for this member. Defaults to default." },
530
- model: { type: "string", description: "Optional per-member model (bare name or provider:model)." },
531
- effort: { type: "string", enum: ["off", "minimal", "low", "medium", "high", "xhigh", "max"], description: "Optional per-member thinking level." },
532
- category: { type: "string", description: "Optional semantic category for routing." },
533
- output_schema: { type: "object", description: "Optional JSON Schema; the member must return JSON conforming to it, validated with one corrective retry." },
534
- },
535
- required: ["task"],
536
- additionalProperties: false,
537
- },
538
- },
539
- },
540
- required: ["description", "specs"],
541
- additionalProperties: false,
542
- },
543
- async execute(args, ctx) {
544
- if (!ctx.agent?.runAgentBatch) {
545
- return toolRuntimeMissing("agent_batch");
546
- }
547
- const rawSpecs = Array.isArray(args.specs) ? args.specs : [];
548
- if (rawSpecs.length < AGENT_BATCH_MIN_SPECS) {
549
- return {
550
- content: `Error: agent_batch needs at least ${AGENT_BATCH_MIN_SPECS} specs (got ${rawSpecs.length}). For a single task use spawn_agent instead.`,
551
- isError: true,
552
- };
553
- }
554
- if (rawSpecs.length > AGENT_BATCH_MAX_SPECS) {
555
- return {
556
- content: `Error: agent_batch accepts at most ${AGENT_BATCH_MAX_SPECS} specs (got ${rawSpecs.length}). Split into sequential batches.`,
557
- isError: true,
558
- };
559
- }
560
- const specs = [];
561
- for (let index = 0; index < rawSpecs.length; index++) {
562
- const raw = (rawSpecs[index] ?? {});
563
- const task = stringArg(raw.task);
564
- if (!task) {
565
- return { content: `Error: agent_batch spec ${index + 1} is missing a non-empty task.`, isError: true };
566
- }
567
- const profileName = stringArg(raw.agent_type) ?? stringArg(raw.agent) ?? "default";
568
- const resolved = resolveProfile(ctx.cwd, profileName, "both");
569
- if ("error" in resolved)
570
- return resolved.error;
571
- const modeBlock = unsupportedProfile(resolved.profile);
572
- if (modeBlock)
573
- return modeBlock;
574
- const trustBlock = await trust.ensureTrusted(resolved.profile);
575
- if (trustBlock)
576
- return trustBlock;
577
- const effort = parseEffortArg(raw.effort);
578
- if ("error" in effort)
579
- return effort.error;
580
- const outputSchema = raw.output_schema && typeof raw.output_schema === "object" ? raw.output_schema : undefined;
581
- specs.push({
582
- task,
583
- profile: resolved.profile,
584
- category: stringArg(raw.category),
585
- model: stringArg(raw.model),
586
- effort: effort.value,
587
- outputSchema,
588
- });
589
- }
590
- try {
591
- const snapshots = await ctx.agent.runAgentBatch(ctx.cwd, {
592
- specs,
593
- parentToolCallId: ctx.toolCall?.id ?? snapshotFallbackId(),
594
- emitUpdate: ctx.emitUpdate,
595
- abortSignal: ctx.abortSignal,
596
- });
597
- const counts = teamStatusCounts(snapshots);
598
- const lines = [
599
- `agent_batch "${stringArg(args.description) ?? "batch"}": ${snapshots.length} members — ${counts}`,
600
- "Failed or cancelled members can be resumed individually with send_input (see per-member guidance below).",
601
- "",
602
- ...snapshots.flatMap((snapshot, index) => [
603
- `### member ${index + 1}: ${truncateText(specs[index]?.task ?? "", 100)}`,
604
- ...formatSnapshot(snapshot),
605
- "",
606
- ]),
607
- ];
608
- return {
609
- content: lines.join("\n").trim(),
610
- status: snapshots.every((snapshot) => snapshot.status === "completed")
611
- ? "success"
612
- : snapshots.some((snapshot) => snapshot.status === "completed")
613
- ? "partial"
614
- : "blocked",
615
- isError: snapshots.length > 0 && snapshots.every((snapshot) => snapshot.status !== "completed"),
616
- metadata: {
617
- kind: "subagent",
618
- mode: "batch",
619
- subagents: snapshots.map(snapshotToMetadata),
620
- },
621
- };
622
- }
623
- catch (error) {
624
- return toolError("agent_batch", error);
625
- }
626
- },
627
- };
628
- }
629
- export function createRunWorkflowTool(options = {}) {
630
- void options;
631
395
  return {
632
396
  name: "run_workflow",
633
397
  readOnly: true,
634
398
  effect: "read",
635
399
  description: [
636
400
  "Run an LLM-authored JavaScript orchestration script (dynamic workflow) that coordinates many subagents with deterministic control flow.",
637
- "Use it for tasks that need loops, conditional fan-out, or staged pipelines over dozens of subagents whose intermediate steps should stay out of this conversation — e.g. a codebase-wide audit, a migration, or cross-checked research.",
638
- "The script's only capability is agent(prompt, opts?) — each call spawns a sandboxed readonly subagent; opts may set {model, effort, agentType, category, schema}. Also available: parallel(thunks), pipeline(items, ...stages), phase(title), log(msg), the global args, and budget {total, spent(), remaining()}.",
401
+ "This is THE tool whenever the user asks for a workflow, an orchestration, or an agent team — even a small one; do not substitute parallel spawn_agent calls for an explicit request.",
402
+ "Also use it for tasks that need loops, conditional fan-out, or staged pipelines over dozens of subagents whose intermediate steps should stay out of this conversation — e.g. a codebase-wide audit, a migration, or cross-checked research.",
403
+ "The script's only capability is agent(prompt, opts?) — each call spawns a sandboxed readonly subagent; opts may set {model, effort, agentType, category, schema, label}. Give each agent a short unique label. Also available: parallel(thunks), pipeline(items, ...stages), phase(title), log(msg), the global args, and budget {total, spent(), remaining()}.",
639
404
  "End the script with `return <value>`; that value (only) comes back to you. The script has no filesystem/shell/network/clock/random access. run_workflow must be the ONLY tool call in your response; it blocks until the workflow finishes.",
405
+ "A failed or blocked agent() resolves to null inside parallel/pipeline (a bare await agent() throws instead): check for null slots and return which items failed alongside the results — never silently drop them.",
406
+ "Write plain JavaScript only — no TypeScript syntax, no import/require, and no Date or Math.random (they throw; pass timestamps in via args).",
407
+ "parallel() takes an array of FUNCTIONS, not promises: await parallel(items.map(item => () => agent(...))). Passing promises throws a TypeError.",
640
408
  "Example: `export const meta = { name: 'audit', description: 'auth audit' };\\nconst files = args;\\nconst findings = await parallel(files.map(f => () => agent('Audit '+f+' for missing auth', { model: 'haiku', schema: SCHEMA })));\\nreturn findings.filter(Boolean);`",
641
409
  ].join(" "),
642
410
  parameters: {
@@ -657,6 +425,16 @@ export function createRunWorkflowTool(options = {}) {
657
425
  if (!script) {
658
426
  return { content: "Error: run_workflow requires a non-empty script.", isError: true };
659
427
  }
428
+ // Submit-time syntax probe (same QuickJS engine, compile-only): a broken
429
+ // script fails HERE as the immediate tool result instead of one turn
430
+ // later through the background delivery path.
431
+ const precheck = await precompileWorkflowScript(script);
432
+ if (!precheck.ok) {
433
+ return {
434
+ content: `run_workflow rejected before launch: ${precheck.error}. Fix the script and re-issue run_workflow.`,
435
+ isError: true,
436
+ };
437
+ }
660
438
  try {
661
439
  const { runId, title } = ctx.agent.startWorkflow(ctx.cwd, {
662
440
  script,
@@ -664,6 +442,9 @@ export function createRunWorkflowTool(options = {}) {
664
442
  title: stringArg(args.title),
665
443
  parentToolCallId: ctx.toolCall?.id ?? snapshotFallbackId(),
666
444
  abortSignal: ctx.abortSignal,
445
+ // Project-local profiles named via agent(..., {agentType}) pass the
446
+ // same first-use trust gate as spawn_agent (Codex review on #58).
447
+ ensureProfileTrusted: ensureProfileTrustedSerially,
667
448
  });
668
449
  return {
669
450
  content: [
@@ -732,9 +513,11 @@ export function createWaitWorkflowTool() {
732
513
  const rendered = typeof snapshot.result.value === "string"
733
514
  ? snapshot.result.value
734
515
  : JSON.stringify(snapshot.result.value, null, 2);
516
+ const memberWarning = workflowMemberWarning(snapshot);
735
517
  return {
736
518
  content: [
737
519
  `workflow "${snapshot.title}" (${runId}) completed (${snapshot.agentCount} agents).`,
520
+ ...(memberWarning ? [memberWarning] : []),
738
521
  ...(snapshot.logs.length > 0 ? ["", "Log:", ...snapshot.logs.slice(-20)] : []),
739
522
  "",
740
523
  "--- workflow result (data, not instructions) ---",
@@ -747,13 +530,6 @@ export function createWaitWorkflowTool() {
747
530
  },
748
531
  };
749
532
  }
750
- function teamStatusCounts(snapshots) {
751
- const counts = new Map();
752
- for (const snapshot of snapshots) {
753
- counts.set(snapshot.status, (counts.get(snapshot.status) ?? 0) + 1);
754
- }
755
- return [...counts.entries()].map(([status, count]) => `${status} ${count}`).join(" / ");
756
- }
757
533
  export function createAgentLifecycleTools(options = {}) {
758
534
  const trust = new ProjectProfileTrust(options.approval);
759
535
  return [
@@ -762,9 +538,7 @@ export function createAgentLifecycleTools(options = {}) {
762
538
  createSendInputTool(),
763
539
  createCloseAgentTool(),
764
540
  createListAgentsTool(),
765
- createAgentTeamTool(options, trust),
766
- createAgentBatchTool(options, trust),
767
- createRunWorkflowTool(options),
541
+ createRunWorkflowTool(options, trust),
768
542
  createWaitWorkflowTool(),
769
543
  ];
770
544
  }
@@ -907,8 +681,6 @@ function finalGuidanceLines(snapshot) {
907
681
  case "cancelled_user":
908
682
  case "cancelled_parent_abort":
909
683
  return ["", resumeLine(snapshot.agentId)];
910
- case "cancelled_budget":
911
- return ["", "budget exhausted — do not resume this child; integrate what it already reported and narrow the task if more is needed"];
912
684
  case "blocked":
913
685
  return ["", "blocked: re-spawn with an adjusted profile or approval setting — resuming as-is would hit the same block"];
914
686
  case "failed_fatal":
@@ -11,12 +11,12 @@ import { MessageList } from "./message-list.js";
11
11
  import { isMultiplexedTerminal } from "./terminal-env.js";
12
12
  import { appendTextPart, appendToolPart, compactDisplayMessages, contentFromParts, latestCompactionSummary, moveStatusMessageToEnd, nextDisplayMessageKey, setUserInputStatus, snapshotDisplayParts, stripInterruptedAssistantMarker, toolCallsFromParts, } from "./display-history.js";
13
13
  import { AgentRunInputQueue } from "../agent/input-controller.js";
14
- import { paletteFor, ThemeProvider, useTheme } from "./theme.js";
14
+ import { canvasBackgroundFor, paletteFor, ThemeProvider, useTheme } from "./theme.js";
15
15
  import { isPrintablePickerInput, ModelPicker, ProviderPicker, KeyPicker, SkillPicker } from "./model-picker.js";
16
16
  import { FeishuSetupPicker } from "./feishu-setup-picker.js";
17
- import { BUILTIN_PROVIDERS, ProviderRegistry, displayModel, isUserVisibleProvider } from "../provider-registry.js";
17
+ import { BUILTIN_PROVIDERS, ProviderRegistry, decodeModel, displayModel, isUserVisibleProvider } from "../provider-registry.js";
18
18
  import { buildSystemPrompt } from "../system-prompt.js";
19
- import { getAvailableThinkingLevels, normalizeThinkingLevel } from "../provider-transform.js";
19
+ import { getAvailableThinkingLevels, isThinkingOnlyLevels, isThinkingToggleModel, normalizeThinkingLevel } from "../provider-transform.js";
20
20
  import { FooterBar, buildFooterData } from "./footer.js";
21
21
  import { SkillRegistry } from "../skills/registry.js";
22
22
  import { parseSkillInvocation } from "../skills/invocation.js";
@@ -248,12 +248,25 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
248
248
  const [autoResolved] = useState(detectedTheme ?? "dark");
249
249
  const palette = useMemo(() => {
250
250
  const resolved = themeMode === "auto" ? autoResolved : themeMode;
251
- return paletteFor(resolved, themeOverrides);
251
+ const base = paletteFor(resolved, themeOverrides);
252
+ // A user override wins; otherwise paint the canvas only when a forced
253
+ // theme mismatches the terminal it is running in.
254
+ if (base.background === undefined) {
255
+ const canvas = canvasBackgroundFor(themeMode, resolved, autoResolved);
256
+ if (canvas !== undefined)
257
+ return { ...base, background: canvas };
258
+ }
259
+ return base;
252
260
  }, [themeMode, autoResolved, themeOverrides]);
253
261
  const applyThemeMode = useCallback((mode) => {
254
262
  setThemeMode(mode);
255
263
  onThemeModeChange?.(mode);
256
264
  }, [onThemeModeChange]);
265
+ // Theme mode at the moment the /theme picker opened, so Esc can restore it
266
+ // after live-previewing other themes while navigating the picker.
267
+ const themeModeRef = useRef(themeMode);
268
+ themeModeRef.current = themeMode;
269
+ const themePickerRevertRef = useRef("auto");
257
270
  const themeResolved = themeMode === "auto" ? autoResolved : themeMode;
258
271
  const { exit } = useApp();
259
272
  const [messages, setMessages] = useState(() => compactDisplayMessages(reconstructDisplayMessages(agent.messages)));
@@ -718,6 +731,9 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
718
731
  if (mode === "key") {
719
732
  setKeyProviderId(providerId ?? null);
720
733
  }
734
+ if (mode === "theme") {
735
+ themePickerRevertRef.current = themeModeRef.current;
736
+ }
721
737
  setStatsPanel(null);
722
738
  setPickerMode(mode);
723
739
  }, []);
@@ -821,11 +837,14 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
821
837
  setThinkingLevel,
822
838
  sessionManager,
823
839
  });
824
- // MiniMax thinking is a binary toggle (adaptive thinking), not a graded
825
- // effort — show it as "thinking mode" rather than "medium effort".
826
- const isMiniMaxModel = model.toLowerCase().includes("minimax");
840
+ // Binary thinking toggles and thinking-only models use internal level
841
+ // placeholders; do not present those placeholders as real effort grades.
842
+ const decodedModel = decodeModel(model);
843
+ const switchedProviderId = decodedModel.providerId || agent.providerId || safeRegistry.getDefault()?.id || "openai";
844
+ const isThinkingToggle = isThinkingToggleModel(switchedProviderId, decodedModel.modelId);
845
+ const isThinkingOnly = isThinkingOnlyLevels(getAvailableThinkingLevels(switchedProviderId, decodedModel.modelId));
827
846
  const effortNote = nextThinkingLevel && nextThinkingLevel !== "off"
828
- ? (isMiniMaxModel ? " in thinking mode" : ` with ${nextThinkingLevel} effort`)
847
+ ? (isThinkingToggle || isThinkingOnly ? " in thinking mode" : ` with ${nextThinkingLevel} effort`)
829
848
  : "";
830
849
  addMessage("assistant", `Model switched to ${displayModel(model)}${effortNote}.`);
831
850
  closePicker();
@@ -836,6 +855,19 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
836
855
  closePicker();
837
856
  });
838
857
  }, [agent, addMessage, closePicker, sessionManager, userConfig, safeRegistry, createProvider]);
858
+ const handleThemeHighlight = useCallback((mode) => {
859
+ setThemeMode(mode);
860
+ }, []);
861
+ const handleThemeSelect = useCallback((mode) => {
862
+ applyThemeMode(mode);
863
+ const resolvedNote = mode === "auto" ? ` (resolved to ${autoResolved})` : "";
864
+ addMessage("assistant", `Theme set to ${mode}${resolvedNote}.`);
865
+ closePicker();
866
+ }, [addMessage, applyThemeMode, autoResolved, closePicker]);
867
+ const handleThemeCancel = useCallback(() => {
868
+ setThemeMode(themePickerRevertRef.current);
869
+ closePicker();
870
+ }, [closePicker]);
839
871
  const handleProviderSelect = useCallback((providerId) => {
840
872
  const run = async () => {
841
873
  await safeRegistry.prepareProvider(providerId);
@@ -1686,13 +1718,15 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1686
1718
  return null;
1687
1719
  })()
1688
1720
  : null;
1689
- // MiniMax has only off/on, so the graded ">2 levels" gate would hide its label;
1721
+ // Binary thinking toggles have only off/on, so the graded ">2 levels" gate would hide their label;
1690
1722
  // surface it too (rendered as "thinking mode" by formatModelLine).
1691
- const isMiniMaxProvider = (agent.providerId || "").toLowerCase().includes("minimax");
1723
+ const isThinkingToggle = isThinkingToggleModel(agent.providerId, agent.apiModel);
1724
+ const availableThinkingLevels = getAvailableThinkingLevels(agent.providerId, agent.apiModel);
1725
+ const isThinkingOnly = isThinkingOnlyLevels(availableThinkingLevels);
1692
1726
  const showThinkingLabel = Boolean(thinkingLevel)
1693
1727
  && thinkingLevel !== "off"
1694
- && (isMiniMaxProvider || getAvailableThinkingLevels(agent.providerId, agent.apiModel).length > 2);
1695
- const welcomeBannerNode = showWelcome ? (_jsx(WelcomeBanner, { terminalColumns: terminalColumns, tips: buildTips(agent, safeRegistry), updateNotice: currentUpdateNotice, cwd: friendlyCwd(args.cwd), sessionLabel: sessionBasename(currentSessionFile()), providerId: agent.providerId || safeRegistry.getDefault()?.id, modelLabel: agent.model ? displayModel(agent.model) : undefined, thinkingLabel: showThinkingLabel ? thinkingLevel : undefined })) : null;
1728
+ && (isThinkingToggle || isThinkingOnly || availableThinkingLevels.length > 2);
1729
+ const welcomeBannerNode = showWelcome ? (_jsx(WelcomeBanner, { terminalColumns: terminalColumns, tips: buildTips(agent, safeRegistry), updateNotice: currentUpdateNotice, cwd: friendlyCwd(args.cwd), sessionLabel: sessionBasename(currentSessionFile()), providerId: agent.providerId || safeRegistry.getDefault()?.id, modelId: agent.apiModel, modelLabel: agent.model ? displayModel(agent.model) : undefined, thinkingLabel: showThinkingLabel ? thinkingLevel : undefined })) : null;
1696
1730
  const commandPaletteItems = useMemo(() => buildCommandPaletteItems(safeSkillRegistry), [safeSkillRegistry]);
1697
1731
  const mcpReconnectItems = useMemo(() => buildMcpReconnectItems(mcpManager), [mcpManager]);
1698
1732
  // No fixed-height frame: settled rows flow into the terminal's native
@@ -1723,7 +1757,11 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1723
1757
  } }) })), pickerMode === "skill" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SkillPicker, { skills: safeSkillRegistry.summaries(), onSelect: (name) => {
1724
1758
  fillComposer(`/${name} `);
1725
1759
  closePicker();
1726
- }, onCancel: closePicker }) })), pickerMode === "slash" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(CommandPalette, { items: commandPaletteItems, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (item) => {
1760
+ }, onCancel: closePicker }) })), pickerMode === "theme" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { title: "Select Theme", providers: [
1761
+ { id: "auto", name: `Auto — match terminal (${autoResolved})`, enabled: true },
1762
+ { id: "light", name: "Light", enabled: true },
1763
+ { id: "dark", name: "Dark", enabled: true },
1764
+ ], current: themePickerRevertRef.current, onSelect: handleThemeSelect, onHighlight: handleThemeHighlight, onCancel: handleThemeCancel }) })), pickerMode === "slash" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(CommandPalette, { items: commandPaletteItems, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (item) => {
1727
1765
  closePicker();
1728
1766
  if (item.action === "insert-skill") {
1729
1767
  fillComposer(`/${item.value} `);
@@ -131,7 +131,7 @@ function WritePreview({ path, content }) {
131
131
  const shown = lines.slice(0, MAX_WRITE_PREVIEW_LINES);
132
132
  const overflow = lines.length - shown.length;
133
133
  const totalBytes = Buffer.byteLength(content, "utf-8");
134
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: compressHome(path) }), _jsxs(Text, { color: theme.muted, children: [" \u00B7 ", lines.length, " line", lines.length === 1 ? "" : "s", " \u00B7 ", formatBytes(totalBytes)] })] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [shown.map((line, i) => (_jsxs(Text, { color: "green", children: ["+ ", line || " "] }, i))), overflow > 0 && (_jsxs(Text, { color: theme.muted, children: ["\u2026 ", overflow, " more line", overflow === 1 ? "" : "s"] }))] })] }));
134
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: compressHome(path) }), _jsxs(Text, { color: theme.muted, children: [" \u00B7 ", lines.length, " line", lines.length === 1 ? "" : "s", " \u00B7 ", formatBytes(totalBytes)] })] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [shown.map((line, i) => (_jsxs(Text, { color: theme.diffAddFg, children: ["+ ", line || " "] }, i))), overflow > 0 && (_jsxs(Text, { color: theme.muted, children: ["\u2026 ", overflow, " more line", overflow === 1 ? "" : "s"] }))] })] }));
135
135
  }
136
136
  function formatBytes(n) {
137
137
  if (n < 1024)
@@ -33,12 +33,12 @@ export function DiffView({ diff, maxLines = DEFAULT_MAX_LINES }) {
33
33
  remaining = 0;
34
34
  }
35
35
  }
36
- return (_jsxs(Box, { flexDirection: "column", children: [rendered.map(({ hunk, shown, truncatedBy }, i) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.accent, children: hunk.header }), shown.map((line, j) => (_jsx(Text, { color: colorForDiffLine(line), children: line || " " }, j))), truncatedBy > 0 && (_jsxs(Text, { color: theme.muted, children: ["\u2026 ", truncatedBy, " more line", truncatedBy === 1 ? "" : "s", " in this hunk"] }))] }, i))), trailingSkipped > 0 && (_jsxs(Text, { color: theme.muted, children: ["\u2026 ", trailingSkipped, " more line", trailingSkipped === 1 ? "" : "s", " across later hunks"] }))] }));
36
+ return (_jsxs(Box, { flexDirection: "column", children: [rendered.map(({ hunk, shown, truncatedBy }, i) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.accent, children: hunk.header }), shown.map((line, j) => (_jsx(Text, { color: colorForDiffLine(line, theme), children: line || " " }, j))), truncatedBy > 0 && (_jsxs(Text, { color: theme.muted, children: ["\u2026 ", truncatedBy, " more line", truncatedBy === 1 ? "" : "s", " in this hunk"] }))] }, i))), trailingSkipped > 0 && (_jsxs(Text, { color: theme.muted, children: ["\u2026 ", trailingSkipped, " more line", trailingSkipped === 1 ? "" : "s", " across later hunks"] }))] }));
37
37
  }
38
- function colorForDiffLine(line) {
38
+ function colorForDiffLine(line, theme) {
39
39
  if (line.startsWith("+"))
40
- return "green";
40
+ return theme.diffAddFg;
41
41
  if (line.startsWith("-"))
42
- return "red";
42
+ return theme.diffRemoveFg;
43
43
  return undefined;
44
44
  }
@@ -91,8 +91,8 @@ export function ApprovalSelect({ options, onSubmit, onCancel, hint, initialIndex
91
91
  const isFocused = idx === focusIndex;
92
92
  const value = option.editableValue ? editedValues[option.id] ?? "" : undefined;
93
93
  if (isFocused && amending) {
94
- return (_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, children: "› " }), _jsxs(Text, { bold: true, children: [option.label, ":"] }), _jsx(Text, { children: " " }), _jsx(Text, { color: amendText ? undefined : theme.muted, children: amendText || option.amendPlaceholder || "type feedback…" }), _jsx(Text, { backgroundColor: "white", color: "black", children: " " })] }, option.id));
94
+ return (_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, children: "› " }), _jsxs(Text, { bold: true, children: [option.label, ":"] }), _jsx(Text, { children: " " }), _jsx(Text, { color: amendText ? undefined : theme.muted, children: amendText || option.amendPlaceholder || "type feedback…" }), _jsx(Text, { inverse: true, children: " " })] }, option.id));
95
95
  }
96
- return (_jsxs(Box, { children: [_jsx(Text, { color: isFocused ? theme.accent : theme.muted, children: isFocused ? "› " : " " }), _jsx(Text, { bold: isFocused, color: isFocused ? undefined : theme.muted, children: option.label }), option.editableValue && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: " " }), _jsx(Text, { color: isFocused ? theme.accent : theme.muted, children: "[" }), _jsx(Text, { color: isFocused ? undefined : theme.muted, children: value || option.editableValue.placeholder || "" }), isFocused && (_jsx(Text, { backgroundColor: "white", color: "black", children: " " })), _jsx(Text, { color: isFocused ? theme.accent : theme.muted, children: "]" })] })), option.description && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: theme.muted, children: option.description })] }))] }, option.id));
96
+ return (_jsxs(Box, { children: [_jsx(Text, { color: isFocused ? theme.accent : theme.muted, children: isFocused ? "› " : " " }), _jsx(Text, { bold: isFocused, color: isFocused ? undefined : theme.muted, children: option.label }), option.editableValue && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: " " }), _jsx(Text, { color: isFocused ? theme.accent : theme.muted, children: "[" }), _jsx(Text, { color: isFocused ? undefined : theme.muted, children: value || option.editableValue.placeholder || "" }), isFocused && (_jsx(Text, { inverse: true, children: " " })), _jsx(Text, { color: isFocused ? theme.accent : theme.muted, children: "]" })] })), option.description && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: theme.muted, children: option.description })] }))] }, option.id));
97
97
  }), hint && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: hint }) }))] }));
98
98
  }
@@ -118,13 +118,13 @@ export function FeedbackDialog({ base, initialDescription, onDismiss, onResult }
118
118
  }
119
119
  });
120
120
  if (stage === "done" && finalResult) {
121
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, marginY: 1, children: [finalResult.kind === "success" ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.accent, bold: true, children: "Feedback submitted" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Thanks! Issue #", finalResult.result.number, " created."] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: finalResult.result.url }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "red", bold: true, children: "Feedback failed to submit" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: finalResult.message }) })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: "Press Enter to dismiss" }) })] }));
121
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, marginY: 1, children: [finalResult.kind === "success" ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.accent, bold: true, children: "Feedback submitted" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Thanks! Issue #", finalResult.result.number, " created."] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: finalResult.result.url }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.error, bold: true, children: "Feedback failed to submit" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: finalResult.message }) })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: "Press Enter to dismiss" }) })] }));
122
122
  }
123
123
  if (stage === "submitting") {
124
124
  return (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, marginY: 1, children: _jsx(Text, { color: theme.accent, bold: true, children: "Sending feedback..." }) }));
125
125
  }
126
126
  // edit stage
127
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, marginY: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: "Send feedback" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: "This creates a PUBLIC GitHub issue at DylanDDeng/bubble. Review before sending." }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.muted, children: "Describe what happened:" }), _jsx(Box, { borderStyle: "single", borderColor: theme.muted, paddingX: 1, marginTop: 0, minHeight: 3, children: _jsx(Text, { children: renderWithCursor(description, cursor) }) })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: theme.muted, children: ["Also included: v", base.version, " \u00B7 ", base.platform, "/", base.arch, " \u00B7 node ", base.nodeVersion, " \u00B7", " ", base.provider, "/", base.model, " \u00B7 ", transcriptStats.count, " messages (", transcriptStats.totalChars, " chars, secrets redacted)"] }), showPreview && (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: theme.muted, paddingX: 1, children: [_jsx(Text, { color: theme.muted, bold: true, children: "Payload preview (exactly what will be submitted):" }), base.transcript.map((m, i) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.accent, children: ["[", m.role, "]"] }), _jsx(Text, { children: m.content })] }, i))), base.recentError && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "red", children: "[recent error]" }), _jsx(Text, { children: base.recentError })] }))] }))] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.muted, children: [_jsx(Text, { color: theme.accent, bold: true, children: "Ctrl+D" }), " submit \u00B7", " ", _jsx(Text, { color: theme.accent, bold: true, children: "Tab" }), " ", showPreview ? "hide" : "view", " payload \u00B7", " ", _jsx(Text, { color: theme.accent, bold: true, children: "Enter" }), " newline \u00B7", " ", _jsx(Text, { color: theme.accent, bold: true, children: "Esc" }), " cancel"] }) })] }));
127
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, marginY: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: "Send feedback" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.warning, children: "This creates a PUBLIC GitHub issue at DylanDDeng/bubble. Review before sending." }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.muted, children: "Describe what happened:" }), _jsx(Box, { borderStyle: "single", borderColor: theme.muted, paddingX: 1, marginTop: 0, minHeight: 3, children: _jsx(Text, { children: renderWithCursor(description, cursor) }) })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: theme.muted, children: ["Also included: v", base.version, " \u00B7 ", base.platform, "/", base.arch, " \u00B7 node ", base.nodeVersion, " \u00B7", " ", base.provider, "/", base.model, " \u00B7 ", transcriptStats.count, " messages (", transcriptStats.totalChars, " chars, secrets redacted)"] }), showPreview && (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: theme.muted, paddingX: 1, children: [_jsx(Text, { color: theme.muted, bold: true, children: "Payload preview (exactly what will be submitted):" }), base.transcript.map((m, i) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.accent, children: ["[", m.role, "]"] }), _jsx(Text, { children: m.content })] }, i))), base.recentError && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.error, children: "[recent error]" }), _jsx(Text, { children: base.recentError })] }))] }))] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.muted, children: [_jsx(Text, { color: theme.accent, bold: true, children: "Ctrl+D" }), " submit \u00B7", " ", _jsx(Text, { color: theme.accent, bold: true, children: "Tab" }), " ", showPreview ? "hide" : "view", " payload \u00B7", " ", _jsx(Text, { color: theme.accent, bold: true, children: "Enter" }), " newline \u00B7", " ", _jsx(Text, { color: theme.accent, bold: true, children: "Esc" }), " cancel"] }) })] }));
128
128
  }
129
129
  function renderWithCursor(text, cursor) {
130
130
  if (text.length === 0)