@bubblebrain-ai/bubble 0.0.21 → 0.0.22
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.
- package/dist/agent/abort-errors.d.ts +14 -0
- package/dist/agent/abort-errors.js +21 -0
- package/dist/agent/budget-ledger.d.ts +41 -0
- package/dist/agent/budget-ledger.js +64 -0
- package/dist/agent/child-runner.d.ts +55 -0
- package/dist/agent/child-runner.js +312 -0
- package/dist/agent/profiles.d.ts +8 -0
- package/dist/agent/profiles.js +27 -5
- package/dist/agent/result-integrator.d.ts +22 -0
- package/dist/agent/result-integrator.js +50 -0
- package/dist/agent/subagent-control.d.ts +31 -0
- package/dist/agent/subagent-control.js +27 -0
- package/dist/agent/subagent-lifecycle-reminder.js +11 -2
- package/dist/agent/subagent-scheduler.d.ts +95 -0
- package/dist/agent/subagent-scheduler.js +256 -0
- package/dist/agent/subagent-store.d.ts +41 -0
- package/dist/agent/subagent-store.js +149 -0
- package/dist/agent/subagent-summary.d.ts +30 -0
- package/dist/agent/subagent-summary.js +74 -0
- package/dist/agent/worktree.d.ts +29 -0
- package/dist/agent/worktree.js +73 -0
- package/dist/agent.d.ts +63 -5
- package/dist/agent.js +360 -287
- package/dist/approval/controller.js +9 -1
- package/dist/approval/tool-helper.js +2 -0
- package/dist/approval/types.d.ts +17 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.js +17 -0
- package/dist/feishu/agent-host/approval-card.js +9 -0
- package/dist/feishu/agent-host/run-driver.js +1 -0
- package/dist/main.js +34 -0
- package/dist/network/errors.d.ts +28 -0
- package/dist/network/errors.js +24 -0
- package/dist/orchestrator/default-hooks.js +5 -1
- package/dist/prompt/compose.js +3 -0
- package/dist/prompt/delegation.d.ts +14 -0
- package/dist/prompt/delegation.js +64 -0
- package/dist/prompt/task-reminders.d.ts +5 -1
- package/dist/prompt/task-reminders.js +10 -2
- package/dist/provider-anthropic.js +23 -0
- package/dist/provider.js +23 -3
- package/dist/slash-commands/commands.js +29 -2
- package/dist/slash-commands/types.d.ts +2 -0
- package/dist/tools/agent-lifecycle.d.ts +29 -3
- package/dist/tools/agent-lifecycle.js +394 -40
- package/dist/tools/child-tools.d.ts +31 -0
- package/dist/tools/child-tools.js +106 -0
- package/dist/tools/index.js +1 -1
- package/dist/tui/run.d.ts +11 -1
- package/dist/tui/run.js +92 -4
- package/dist/tui/session-picker-data.d.ts +18 -0
- package/dist/tui/session-picker-data.js +21 -0
- package/dist/tui/wordmark.d.ts +2 -0
- package/dist/tui/wordmark.js +31 -4
- package/dist/tui-ink/approval/approval-dialog.js +10 -0
- package/dist/tui-opentui/approval/approval-dialog.js +10 -0
- package/dist/types.d.ts +17 -0
- package/package.json +1 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-child tool factory for write_worktree subagents (design doc §8).
|
|
3
|
+
*
|
|
4
|
+
* Parent tools close over the parent cwd at creation, so a write child needs
|
|
5
|
+
* fresh instances bound to its worktree — with their own FileStateTracker —
|
|
6
|
+
* plus a worktree-scoped approval policy: file operations are runtime-checked
|
|
7
|
+
* to stay under the worktree root (the tools' own workspace fence does this
|
|
8
|
+
* structurally), bash auto-approves inside the worktree when the command
|
|
9
|
+
* passes a deny-list of escaping operations, and everything else fails fast.
|
|
10
|
+
*/
|
|
11
|
+
import { isAbsolute, resolve, sep } from "node:path";
|
|
12
|
+
import { createBashTool } from "./bash.js";
|
|
13
|
+
import { createEditTool } from "./edit.js";
|
|
14
|
+
import { createGlobTool } from "./glob.js";
|
|
15
|
+
import { createGrepTool } from "./grep.js";
|
|
16
|
+
import { createReadTool } from "./read.js";
|
|
17
|
+
import { createWriteTool } from "./write.js";
|
|
18
|
+
import { FileStateTracker } from "./file-state.js";
|
|
19
|
+
/** Operations a worktree child may never run, regardless of cwd. */
|
|
20
|
+
const WORKTREE_BASH_DENY_PATTERNS = [
|
|
21
|
+
{ pattern: /\bgit\s+push\b/, reason: "pushing from a subagent worktree is not allowed; the parent reviews and applies changes." },
|
|
22
|
+
{ pattern: /\bgit\s+remote\b/, reason: "remote configuration is not allowed inside a subagent worktree." },
|
|
23
|
+
{ pattern: /\bgit\s+worktree\b/, reason: "managing worktrees from inside a subagent worktree is not allowed." },
|
|
24
|
+
{ pattern: /\bsudo\b/, reason: "privileged commands are not allowed inside a subagent worktree." },
|
|
25
|
+
];
|
|
26
|
+
export function isPathInsideWorktree(worktreeRoot, candidate) {
|
|
27
|
+
const resolved = isAbsolute(candidate) ? resolve(candidate) : resolve(worktreeRoot, candidate);
|
|
28
|
+
const root = resolve(worktreeRoot);
|
|
29
|
+
return resolved === root || resolved.startsWith(root + sep);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Approval policy for a worktree child: containment is enforced by code
|
|
33
|
+
* (path checks, deny-list), never by prompt text. There is no interactive
|
|
34
|
+
* fallback — anything outside the policy fails fast (design §11).
|
|
35
|
+
*/
|
|
36
|
+
export class WorktreeApprovalController {
|
|
37
|
+
worktreeRoot;
|
|
38
|
+
constructor(worktreeRoot) {
|
|
39
|
+
this.worktreeRoot = worktreeRoot;
|
|
40
|
+
}
|
|
41
|
+
async request(req) {
|
|
42
|
+
switch (req.type) {
|
|
43
|
+
case "bash": {
|
|
44
|
+
for (const { pattern, reason } of WORKTREE_BASH_DENY_PATTERNS) {
|
|
45
|
+
if (pattern.test(req.command)) {
|
|
46
|
+
return { action: "reject", feedback: `Blocked by worktree policy: ${reason}` };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!isPathInsideWorktree(this.worktreeRoot, req.cwd)) {
|
|
50
|
+
return { action: "reject", feedback: "Blocked by worktree policy: commands must run inside the subagent worktree." };
|
|
51
|
+
}
|
|
52
|
+
// Absolute paths reaching outside the worktree are an escape attempt.
|
|
53
|
+
const absolutePaths = req.command.match(/(?<=^|[\s"'=])\/[^\s"';|&]+/g) ?? [];
|
|
54
|
+
for (const path of absolutePaths) {
|
|
55
|
+
if (path.startsWith("/dev/") || path.startsWith("/tmp/") || path.startsWith("/usr/") || path.startsWith("/bin/") || path.startsWith("/opt/") || path.startsWith("/etc/"))
|
|
56
|
+
continue;
|
|
57
|
+
if (!isPathInsideWorktree(this.worktreeRoot, path)) {
|
|
58
|
+
return {
|
|
59
|
+
action: "reject",
|
|
60
|
+
feedback: `Blocked by worktree policy: the command references a path outside the worktree (${path}).`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { action: "approve" };
|
|
65
|
+
}
|
|
66
|
+
case "edit":
|
|
67
|
+
case "write":
|
|
68
|
+
return isPathInsideWorktree(this.worktreeRoot, req.path)
|
|
69
|
+
? { action: "approve" }
|
|
70
|
+
: { action: "reject", feedback: `Blocked by worktree policy: ${req.path} is outside the subagent worktree.` };
|
|
71
|
+
case "patch":
|
|
72
|
+
return req.paths.every((path) => isPathInsideWorktree(this.worktreeRoot, path))
|
|
73
|
+
? { action: "approve" }
|
|
74
|
+
: { action: "reject", feedback: "Blocked by worktree policy: the patch touches paths outside the subagent worktree." };
|
|
75
|
+
case "lsp":
|
|
76
|
+
return { action: "approve" };
|
|
77
|
+
case "agent_profile":
|
|
78
|
+
return { action: "reject", feedback: "Subagents cannot approve agent profiles." };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
checkRules() {
|
|
82
|
+
return { decision: "ask" };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const WORKTREE_TOOL_NAMES = new Set(["read", "glob", "grep", "edit", "write", "bash"]);
|
|
86
|
+
/**
|
|
87
|
+
* Builds the write child's toolset bound to its worktree: fresh instances
|
|
88
|
+
* with their own FileStateTracker and the worktree approval policy. A
|
|
89
|
+
* profile's tools list can narrow the set but never widen it.
|
|
90
|
+
*/
|
|
91
|
+
export function createWorktreeChildTools(worktreeCwd, include) {
|
|
92
|
+
const approval = new WorktreeApprovalController(worktreeCwd);
|
|
93
|
+
const fileState = new FileStateTracker(worktreeCwd);
|
|
94
|
+
const tools = [
|
|
95
|
+
createReadTool(worktreeCwd, approval, undefined, fileState),
|
|
96
|
+
createGlobTool(worktreeCwd),
|
|
97
|
+
createGrepTool(worktreeCwd),
|
|
98
|
+
createEditTool(worktreeCwd, approval, undefined, fileState),
|
|
99
|
+
createWriteTool(worktreeCwd, {}, approval, undefined, fileState),
|
|
100
|
+
createBashTool(worktreeCwd, approval, fileState),
|
|
101
|
+
];
|
|
102
|
+
if (!include || include.length === 0)
|
|
103
|
+
return tools;
|
|
104
|
+
const requested = new Set(include.filter((name) => WORKTREE_TOOL_NAMES.has(name)));
|
|
105
|
+
return requested.size > 0 ? tools.filter((tool) => requested.has(tool.name)) : tools;
|
|
106
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -57,7 +57,7 @@ export function createAllTools(cwd, skillRegistry, options = {}) {
|
|
|
57
57
|
createWebFetchTool(approval),
|
|
58
58
|
createMemorySearchTool(cwd),
|
|
59
59
|
createMemoryReadSummaryTool(cwd),
|
|
60
|
-
...createAgentLifecycleTools(),
|
|
60
|
+
...createAgentLifecycleTools({ cwd, approval }),
|
|
61
61
|
...(options.questionController ? [createQuestionTool(options.questionController)] : []),
|
|
62
62
|
...(skillRegistry ? [createSkillSearchTool(skillRegistry), createSkillTool(skillRegistry)] : []),
|
|
63
63
|
...(options.todoStore ? [createTodoTool(options.todoStore)] : []),
|
package/dist/tui/run.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type Agent } from "../agent.js";
|
|
|
2
2
|
import type { CliArgs } from "../cli.js";
|
|
3
3
|
import type { ThemeMode } from "../config.js";
|
|
4
4
|
import type { ExternalHookController } from "../hooks/controller.js";
|
|
5
|
-
import
|
|
5
|
+
import { SessionManager } from "../session.js";
|
|
6
6
|
import type { PlanDecision, Provider } from "../types.js";
|
|
7
7
|
import type { ProviderRegistry } from "../provider-registry.js";
|
|
8
8
|
import type { SkillRegistry } from "../skills/registry.js";
|
|
@@ -45,5 +45,15 @@ export interface RunTuiOptions {
|
|
|
45
45
|
runMemoryRefresh?: (scope?: MemoryScope) => Promise<string>;
|
|
46
46
|
/** One-line "update available" notice shown on the home screen, if any. */
|
|
47
47
|
updateNotice?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Swap the active session in place (driven by the /session picker).
|
|
50
|
+
* Rebinds persistence to the picked session file and replaces the agent's
|
|
51
|
+
* message history; the TUI rebuilds its transcript from the result.
|
|
52
|
+
*/
|
|
53
|
+
switchSession?: (sessionFile: string) => {
|
|
54
|
+
manager: SessionManager;
|
|
55
|
+
} | {
|
|
56
|
+
error: string;
|
|
57
|
+
};
|
|
48
58
|
}
|
|
49
59
|
export declare function runTui(agent: Agent, args: CliArgs, options?: RunTuiOptions): Promise<void>;
|
package/dist/tui/run.js
CHANGED
|
@@ -12,6 +12,8 @@ import { debugReasoningStream, summarizeDebugText } from "../reasoning-debug.js"
|
|
|
12
12
|
import { isHiddenToolMetadata } from "../agent/discovery-barrier.js";
|
|
13
13
|
import { createStreamingInternalReminderSanitizer, sanitizeInternalReasoningText, sanitizeInternalReminderBlocks, } from "../agent/internal-reminder-sanitizer.js";
|
|
14
14
|
import { summarizeAgentEventForTrace, summarizeTraceError, summarizeTraceValue, traceEvent, } from "../debug-trace.js";
|
|
15
|
+
import { SessionManager } from "../session.js";
|
|
16
|
+
import { buildSessionPickerEntries, preferredSessionPickerIndex } from "./session-picker-data.js";
|
|
15
17
|
import { BUILTIN_PROVIDERS, decodeModel, displayModel, isUserVisibleProvider } from "../provider-registry.js";
|
|
16
18
|
import { calculateUsageCost } from "../model-pricing.js";
|
|
17
19
|
import { getAvailableThinkingLevels } from "../provider-transform.js";
|
|
@@ -1177,7 +1179,13 @@ function OpenTuiApp(props) {
|
|
|
1177
1179
|
promptModelLabels.delete(ref);
|
|
1178
1180
|
};
|
|
1179
1181
|
const cycleMode = () => {
|
|
1180
|
-
|
|
1182
|
+
// Mode switching is intentionally allowed while the agent is running:
|
|
1183
|
+
// Agent.setMode() is safe mid-run and the approval controller reads the
|
|
1184
|
+
// live mode on every request, so flipping to bypass (or into plan) takes
|
|
1185
|
+
// effect from the very next tool call — no need to wait for the turn to
|
|
1186
|
+
// finish. Only pickers and the plan-approval dialog still block it,
|
|
1187
|
+
// because those surfaces own the keyboard.
|
|
1188
|
+
if (picker || pendingPlan())
|
|
1181
1189
|
return false;
|
|
1182
1190
|
const next = getNextPermissionMode(props.agent.mode);
|
|
1183
1191
|
props.agent.setMode(next);
|
|
@@ -3042,7 +3050,10 @@ function OpenTuiApp(props) {
|
|
|
3042
3050
|
// "(current)" sits at the bottom of the rewind list and is the safe default.
|
|
3043
3051
|
: step === "rewind"
|
|
3044
3052
|
? Math.max(0, items.length - 1)
|
|
3045
|
-
:
|
|
3053
|
+
// Sessions: start on the most recent conversation that is not the active one.
|
|
3054
|
+
: step === "sessions"
|
|
3055
|
+
? preferredSessionPickerIndex(items)
|
|
3056
|
+
: 0,
|
|
3046
3057
|
apiKey: "",
|
|
3047
3058
|
};
|
|
3048
3059
|
activePrompt()?.clear();
|
|
@@ -3077,6 +3088,8 @@ function OpenTuiApp(props) {
|
|
|
3077
3088
|
return buildRewindPickerItems();
|
|
3078
3089
|
if (step === "rewind-action")
|
|
3079
3090
|
return buildRewindActionItems(providerId);
|
|
3091
|
+
if (step === "sessions")
|
|
3092
|
+
return buildSessionPickerItems();
|
|
3080
3093
|
if (step === "models") {
|
|
3081
3094
|
if (providerDialogModelItems?.key === modelPickerCacheKey(providerId)) {
|
|
3082
3095
|
return providerDialogModelItems.items;
|
|
@@ -3319,6 +3332,8 @@ function OpenTuiApp(props) {
|
|
|
3319
3332
|
return "Rewind — restore to the point before…";
|
|
3320
3333
|
if (state.step === "rewind-action")
|
|
3321
3334
|
return "Rewind — what to restore?";
|
|
3335
|
+
if (state.step === "sessions")
|
|
3336
|
+
return "Resume a session";
|
|
3322
3337
|
const provider = providerDisplayName(state.providerId);
|
|
3323
3338
|
if (state.step === "auth")
|
|
3324
3339
|
return `${provider} auth method`;
|
|
@@ -3343,6 +3358,8 @@ function OpenTuiApp(props) {
|
|
|
3343
3358
|
return `↑/↓ move · enter continue · esc cancel${count}`;
|
|
3344
3359
|
if (state.step === "rewind-action")
|
|
3345
3360
|
return "↑/↓ move · enter confirm · esc back";
|
|
3361
|
+
if (state.step === "sessions")
|
|
3362
|
+
return `↑/↓ move · enter resume · esc close${count}`;
|
|
3346
3363
|
const escLabel = state.step === "providers" ? "esc close" : "esc back";
|
|
3347
3364
|
return `↑/↓ move · enter select · ${escLabel}${count}`;
|
|
3348
3365
|
}
|
|
@@ -3360,7 +3377,7 @@ function OpenTuiApp(props) {
|
|
|
3360
3377
|
}
|
|
3361
3378
|
function providerDialogColumnWidths(state, panelWidth) {
|
|
3362
3379
|
const contentWidth = Math.max(24, panelWidth - PROVIDER_DIALOG_ROW_RESERVED_WIDTH);
|
|
3363
|
-
const footer = state.step === "skills" ? 10 : state.step === "providers" ? 9 : 8;
|
|
3380
|
+
const footer = state.step === "skills" || state.step === "sessions" ? 10 : state.step === "providers" ? 9 : 8;
|
|
3364
3381
|
const minLabel = state.step === "skills" ? 18 : 24;
|
|
3365
3382
|
const desiredDetail = state.step === "skills"
|
|
3366
3383
|
? 30
|
|
@@ -3370,7 +3387,9 @@ function OpenTuiApp(props) {
|
|
|
3370
3387
|
? 40
|
|
3371
3388
|
: state.step === "rewind"
|
|
3372
3389
|
? 18
|
|
3373
|
-
:
|
|
3390
|
+
: state.step === "sessions"
|
|
3391
|
+
? 14
|
|
3392
|
+
: 16;
|
|
3374
3393
|
const detail = Math.max(8, Math.min(desiredDetail, contentWidth - footer - minLabel));
|
|
3375
3394
|
const label = Math.max(8, contentWidth - detail - footer);
|
|
3376
3395
|
return { label, detail, footer };
|
|
@@ -3579,6 +3598,15 @@ function OpenTuiApp(props) {
|
|
|
3579
3598
|
openProviderDialog("rewind-action", item.value);
|
|
3580
3599
|
return;
|
|
3581
3600
|
}
|
|
3601
|
+
if (state.step === "sessions") {
|
|
3602
|
+
closeProviderDialog();
|
|
3603
|
+
if (!item.value || item.value === props.options.sessionManager?.getSessionFile()) {
|
|
3604
|
+
// Selecting the active session keeps everything as is.
|
|
3605
|
+
return;
|
|
3606
|
+
}
|
|
3607
|
+
await switchToSession(item.value);
|
|
3608
|
+
return;
|
|
3609
|
+
}
|
|
3582
3610
|
if (state.step === "rewind-action") {
|
|
3583
3611
|
closeProviderDialog();
|
|
3584
3612
|
await executeSlash(item.command);
|
|
@@ -4909,6 +4937,9 @@ function OpenTuiApp(props) {
|
|
|
4909
4937
|
openRewindPicker: () => {
|
|
4910
4938
|
openProviderDialog("rewind");
|
|
4911
4939
|
},
|
|
4940
|
+
openSessionPicker: () => {
|
|
4941
|
+
openProviderDialog("sessions");
|
|
4942
|
+
},
|
|
4912
4943
|
fillComposer: (text) => {
|
|
4913
4944
|
resetPromptHistoryBrowse();
|
|
4914
4945
|
setPromptText(text);
|
|
@@ -5256,6 +5287,44 @@ function OpenTuiApp(props) {
|
|
|
5256
5287
|
items.push({ label: "(current)", value: "", command: "" });
|
|
5257
5288
|
return items;
|
|
5258
5289
|
}
|
|
5290
|
+
function buildSessionPickerItems() {
|
|
5291
|
+
const activeFile = props.options.sessionManager?.getSessionFile();
|
|
5292
|
+
const summaries = SessionManager.summarizeSessionsForCwd(props.args.cwd);
|
|
5293
|
+
return buildSessionPickerEntries(summaries, activeFile).map((entry) => ({
|
|
5294
|
+
label: entry.label,
|
|
5295
|
+
detail: entry.detail,
|
|
5296
|
+
value: entry.value,
|
|
5297
|
+
command: "",
|
|
5298
|
+
footer: entry.footer,
|
|
5299
|
+
gutter: entry.gutter,
|
|
5300
|
+
}));
|
|
5301
|
+
}
|
|
5302
|
+
async function switchToSession(sessionFile) {
|
|
5303
|
+
const switchSession = props.options.switchSession;
|
|
5304
|
+
if (!switchSession) {
|
|
5305
|
+
addMessage("error", "Session switching is not available in this mode.");
|
|
5306
|
+
return;
|
|
5307
|
+
}
|
|
5308
|
+
if (isRunning()) {
|
|
5309
|
+
setNotice("Stop the current run before switching sessions.");
|
|
5310
|
+
return;
|
|
5311
|
+
}
|
|
5312
|
+
const result = switchSession(sessionFile);
|
|
5313
|
+
if ("error" in result) {
|
|
5314
|
+
addMessage("error", `Failed to switch session: ${result.error}`);
|
|
5315
|
+
return;
|
|
5316
|
+
}
|
|
5317
|
+
props.options.sessionManager = result.manager;
|
|
5318
|
+
// Same rebuild path as /rewind: the agent history was replaced wholesale,
|
|
5319
|
+
// so reconstruct the transcript from it instead of patching the display.
|
|
5320
|
+
displayMessages = reconstructDisplayMessages(props.agent.messages);
|
|
5321
|
+
streamingDisplay = undefined;
|
|
5322
|
+
redrawTranscript(undefined, displayMessages);
|
|
5323
|
+
syncTodosFromAgent();
|
|
5324
|
+
bumpSidebar();
|
|
5325
|
+
syncPromptSurfaces(true);
|
|
5326
|
+
addMessage("assistant", `⤷ Resumed session: ${sessionDisplayName(result.manager)}`);
|
|
5327
|
+
}
|
|
5259
5328
|
function buildRewindActionItems(turnNumber) {
|
|
5260
5329
|
if (!turnNumber)
|
|
5261
5330
|
return [];
|
|
@@ -9340,6 +9409,17 @@ function getApprovalPanelMeta(request) {
|
|
|
9340
9409
|
path: request.path,
|
|
9341
9410
|
};
|
|
9342
9411
|
}
|
|
9412
|
+
if (request.type === "agent_profile") {
|
|
9413
|
+
return {
|
|
9414
|
+
icon: "@",
|
|
9415
|
+
title: `Trust project agent profile "${request.name}"`,
|
|
9416
|
+
subtitle: "from .bubble/agents — its prompt will drive a subagent",
|
|
9417
|
+
preview: `${shortCwd(request.path)}\n${request.promptPreview}`,
|
|
9418
|
+
previewHeight: 8,
|
|
9419
|
+
previewColor: theme.toolText,
|
|
9420
|
+
path: request.path,
|
|
9421
|
+
};
|
|
9422
|
+
}
|
|
9343
9423
|
const path = shortCwd(request.path);
|
|
9344
9424
|
if (request.type === "edit") {
|
|
9345
9425
|
return {
|
|
@@ -9454,6 +9534,8 @@ function displayToolName(name) {
|
|
|
9454
9534
|
wait_agent: "WaitAgent",
|
|
9455
9535
|
send_input: "SendInput",
|
|
9456
9536
|
close_agent: "CloseAgent",
|
|
9537
|
+
list_agents: "ListAgents",
|
|
9538
|
+
agent_team: "AgentTeam",
|
|
9457
9539
|
task: "Task",
|
|
9458
9540
|
todo: "Todo",
|
|
9459
9541
|
question: "Questions",
|
|
@@ -9476,6 +9558,12 @@ function toolHeader(tool) {
|
|
|
9476
9558
|
const agentId = args.agent_id ?? (Array.isArray(args.agent_ids) ? `${args.agent_ids.length} agents` : undefined);
|
|
9477
9559
|
return agentId ? `(${truncate(String(agentId), 64)})` : "";
|
|
9478
9560
|
}
|
|
9561
|
+
if (tool.name === "agent_team") {
|
|
9562
|
+
const items = Array.isArray(args.items) ? `${args.items.length} items` : "";
|
|
9563
|
+
const description = typeof args.description === "string" ? args.description : "";
|
|
9564
|
+
const label = [description, items].filter(Boolean).join(", ");
|
|
9565
|
+
return label ? `(${truncate(label, 64)})` : "";
|
|
9566
|
+
}
|
|
9479
9567
|
const value = args.path ?? args.command ?? args.pattern ?? args.url ?? args.query ?? toolPath(tool);
|
|
9480
9568
|
return value ? `(${truncate(String(value).replace(/\n/g, " "), 64)})` : "";
|
|
9481
9569
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SessionSummary } from "../session.js";
|
|
2
|
+
export interface SessionPickerEntry {
|
|
3
|
+
/** Session title (or first-message preview when untitled). */
|
|
4
|
+
label: string;
|
|
5
|
+
/** Message count, e.g. "12 messages". */
|
|
6
|
+
detail: string;
|
|
7
|
+
/** Absolute path to the session .jsonl file. */
|
|
8
|
+
value: string;
|
|
9
|
+
/** "current" for the active session, otherwise a relative timestamp. */
|
|
10
|
+
footer: string;
|
|
11
|
+
/** "●" marks the active session. */
|
|
12
|
+
gutter?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function buildSessionPickerEntries(summaries: SessionSummary[], activeFile: string | undefined, now?: number): SessionPickerEntry[];
|
|
15
|
+
/** Default selection: the most recent session that is not the active one. */
|
|
16
|
+
export declare function preferredSessionPickerIndex(entries: Array<{
|
|
17
|
+
gutter?: string;
|
|
18
|
+
}>): number;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { normalizeSingleLine, truncateVisual } from "../text-display.js";
|
|
2
|
+
import { formatRelativeTime } from "./recent-activity.js";
|
|
3
|
+
const SESSION_PICKER_LABEL_MAX_WIDTH = 72;
|
|
4
|
+
export function buildSessionPickerEntries(summaries, activeFile, now = Date.now()) {
|
|
5
|
+
return summaries.map((summary) => {
|
|
6
|
+
const isCurrent = summary.file === activeFile;
|
|
7
|
+
const label = truncateVisual(normalizeSingleLine(summary.title || summary.preview || summary.name), SESSION_PICKER_LABEL_MAX_WIDTH) || summary.name;
|
|
8
|
+
return {
|
|
9
|
+
label,
|
|
10
|
+
detail: `${summary.messageCount} message${summary.messageCount === 1 ? "" : "s"}`,
|
|
11
|
+
value: summary.file,
|
|
12
|
+
footer: isCurrent ? "current" : formatRelativeTime(summary.mtime, now),
|
|
13
|
+
gutter: isCurrent ? "●" : undefined,
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/** Default selection: the most recent session that is not the active one. */
|
|
18
|
+
export function preferredSessionPickerIndex(entries) {
|
|
19
|
+
const firstOther = entries.findIndex((entry) => entry.gutter !== "●");
|
|
20
|
+
return firstOther >= 0 ? firstOther : 0;
|
|
21
|
+
}
|
package/dist/tui/wordmark.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface BubbleWordmarkLine {
|
|
|
8
8
|
tone?: BubbleWordmarkTone;
|
|
9
9
|
segments?: BubbleWordmarkSegment[];
|
|
10
10
|
}
|
|
11
|
+
export declare const BUBBLE_CAT: BubbleWordmarkLine[];
|
|
12
|
+
export declare const BUBBLE_CAT_LARGE: BubbleWordmarkLine[];
|
|
11
13
|
export declare const BUBBLE_WORDMARK: BubbleWordmarkLine[];
|
|
12
14
|
export declare const BUBBLE_WORDMARK_LARGE: BubbleWordmarkLine[];
|
|
13
15
|
export declare const BUBBLE_COMPACT_WORDMARK: BubbleWordmarkLine[];
|
package/dist/tui/wordmark.js
CHANGED
|
@@ -22,6 +22,27 @@ const LOWER_B = {
|
|
|
22
22
|
" ",
|
|
23
23
|
],
|
|
24
24
|
};
|
|
25
|
+
// Pixel cat mascot, drawn on the same half-block pixel grid as the letters:
|
|
26
|
+
// pointy ears, 2x2-pixel eyes, tiny mouth, round chin (10x14 pixels). It is
|
|
27
|
+
// stacked above the wordmark (icon-over-name lockup) rather than inlined, so
|
|
28
|
+
// its solid fill doesn't compete with the thin letter strokes.
|
|
29
|
+
const CAT_LINES = [
|
|
30
|
+
" █▄ ▄█ ",
|
|
31
|
+
" ███▄▄███ ",
|
|
32
|
+
"██████████",
|
|
33
|
+
"█ ████ █",
|
|
34
|
+
"████▀▀████",
|
|
35
|
+
"██████████",
|
|
36
|
+
" ▀██████▀ ",
|
|
37
|
+
];
|
|
38
|
+
export const BUBBLE_CAT = CAT_LINES.map((text) => ({
|
|
39
|
+
text,
|
|
40
|
+
tone: "brand",
|
|
41
|
+
}));
|
|
42
|
+
export const BUBBLE_CAT_LARGE = CAT_LINES.map((text) => ({
|
|
43
|
+
text: text.split("").map((ch) => ch + ch).join(""),
|
|
44
|
+
tone: "brand",
|
|
45
|
+
}));
|
|
25
46
|
const GLYPHS = {
|
|
26
47
|
u: {
|
|
27
48
|
tone: "ink",
|
|
@@ -172,10 +193,16 @@ export function bubbleWordmarkLineText(line) {
|
|
|
172
193
|
export function bubbleWordmarkMaxWidth(lines = BUBBLE_WORDMARK) {
|
|
173
194
|
return Math.max(...lines.map((line) => bubbleWordmarkLineText(line).length));
|
|
174
195
|
}
|
|
196
|
+
const LOGO_GAP = { text: "", tone: "caption" };
|
|
197
|
+
// Icon-over-name lockup: pixel cat centered above the wordmark. Both render
|
|
198
|
+
// sites center every line independently, which is what stacks the cat over
|
|
199
|
+
// the text without any per-line padding here.
|
|
175
200
|
export function bubbleWordmarkForWidth(width) {
|
|
176
|
-
if (width >= bubbleWordmarkMaxWidth(BUBBLE_WORDMARK_LARGE) + 4)
|
|
177
|
-
return BUBBLE_WORDMARK_LARGE;
|
|
178
|
-
|
|
179
|
-
|
|
201
|
+
if (width >= bubbleWordmarkMaxWidth(BUBBLE_WORDMARK_LARGE) + 4) {
|
|
202
|
+
return [...BUBBLE_CAT_LARGE, LOGO_GAP, ...BUBBLE_WORDMARK_LARGE];
|
|
203
|
+
}
|
|
204
|
+
if (width >= bubbleWordmarkMaxWidth() + 4) {
|
|
205
|
+
return [...BUBBLE_CAT, LOGO_GAP, ...BUBBLE_WORDMARK];
|
|
206
|
+
}
|
|
180
207
|
return BUBBLE_COMPACT_WORDMARK;
|
|
181
208
|
}
|
|
@@ -77,6 +77,8 @@ function dialogTitle(req) {
|
|
|
77
77
|
return "Bash command";
|
|
78
78
|
case "lsp":
|
|
79
79
|
return "Language server operation";
|
|
80
|
+
case "agent_profile":
|
|
81
|
+
return "Project agent profile";
|
|
80
82
|
}
|
|
81
83
|
}
|
|
82
84
|
function dialogQuestion(req) {
|
|
@@ -91,6 +93,8 @@ function dialogQuestion(req) {
|
|
|
91
93
|
return "Do you want to proceed?";
|
|
92
94
|
case "lsp":
|
|
93
95
|
return `Do you want to run ${req.operation} on ${basename(req.path)}?`;
|
|
96
|
+
case "agent_profile":
|
|
97
|
+
return `Trust the repository profile "${req.name}" to drive a subagent? It is remembered for this session until the file changes.`;
|
|
94
98
|
}
|
|
95
99
|
}
|
|
96
100
|
function basename(p) {
|
|
@@ -107,8 +111,14 @@ function RequestPreview({ request }) {
|
|
|
107
111
|
return _jsx(DiffView, { diff: request.diff });
|
|
108
112
|
case "write":
|
|
109
113
|
return _jsx(WritePreview, { path: request.path, content: request.content });
|
|
114
|
+
case "agent_profile":
|
|
115
|
+
return _jsx(AgentProfilePreview, { path: request.path, promptPreview: request.promptPreview });
|
|
110
116
|
}
|
|
111
117
|
}
|
|
118
|
+
function AgentProfilePreview({ path, promptPreview }) {
|
|
119
|
+
const theme = useTheme();
|
|
120
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.muted, children: compressHome(path) }), _jsx(Text, { children: promptPreview }), _jsx(Text, { color: theme.warning, children: "This prompt comes from the repository's .bubble/agents and will drive a subagent." })] }));
|
|
121
|
+
}
|
|
112
122
|
function BashPreview({ command, cwd }) {
|
|
113
123
|
const theme = useTheme();
|
|
114
124
|
const danger = classifyBashDanger(command);
|
|
@@ -84,6 +84,8 @@ function dialogTitle(req) {
|
|
|
84
84
|
return "Bash command";
|
|
85
85
|
case "lsp":
|
|
86
86
|
return "Language server operation";
|
|
87
|
+
case "agent_profile":
|
|
88
|
+
return "Project agent profile";
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
function dialogQuestion(req) {
|
|
@@ -98,6 +100,8 @@ function dialogQuestion(req) {
|
|
|
98
100
|
return "Do you want to proceed?";
|
|
99
101
|
case "lsp":
|
|
100
102
|
return `Do you want to run ${req.operation} on ${basename(req.path)}?`;
|
|
103
|
+
case "agent_profile":
|
|
104
|
+
return `Trust the repository profile "${req.name}" to drive a subagent? It is remembered for this session until the file changes.`;
|
|
101
105
|
}
|
|
102
106
|
}
|
|
103
107
|
function basename(p) {
|
|
@@ -114,8 +118,14 @@ function RequestPreview({ request }) {
|
|
|
114
118
|
return _jsx(DiffView, { diff: request.diff });
|
|
115
119
|
case "write":
|
|
116
120
|
return _jsx(WritePreview, { path: request.path, content: request.content });
|
|
121
|
+
case "agent_profile":
|
|
122
|
+
return _jsx(AgentProfilePreview, { path: request.path, promptPreview: request.promptPreview });
|
|
117
123
|
}
|
|
118
124
|
}
|
|
125
|
+
function AgentProfilePreview({ path, promptPreview }) {
|
|
126
|
+
const theme = useTheme();
|
|
127
|
+
return (_jsxs("box", { style: { flexDirection: "column" }, children: [_jsx("text", { fg: theme.muted, children: compressHome(path) }), _jsx("text", { children: promptPreview }), _jsx("box", { style: { marginTop: 1 }, children: _jsx("text", { fg: theme.warning, children: "This prompt comes from the repository's .bubble/agents and will drive a subagent." }) })] }));
|
|
128
|
+
}
|
|
119
129
|
function BashPreview({ command, cwd }) {
|
|
120
130
|
const theme = useTheme();
|
|
121
131
|
const danger = classifyBashDanger(command);
|
package/dist/types.d.ts
CHANGED
|
@@ -199,6 +199,16 @@ export interface ToolContext {
|
|
|
199
199
|
}) => Promise<import("./agent/subagent-control.js").SubagentThreadSnapshot>;
|
|
200
200
|
closeSubAgent?: (agentId: string) => Promise<import("./agent/subagent-control.js").SubagentThreadSnapshot>;
|
|
201
201
|
listSubAgents?: () => import("./agent/subagent-control.js").SubagentThreadSnapshot[];
|
|
202
|
+
runAgentTeam?: (cwd: string, options: {
|
|
203
|
+
profile: import("./agent/profiles.js").AgentProfile;
|
|
204
|
+
category?: string;
|
|
205
|
+
promptTemplate: string;
|
|
206
|
+
items: string[];
|
|
207
|
+
parentToolCallId: string;
|
|
208
|
+
emitUpdate?: (update: ToolUpdate) => void;
|
|
209
|
+
abortSignal?: AbortSignal;
|
|
210
|
+
approval?: "fail" | "disabled";
|
|
211
|
+
}) => Promise<import("./agent/subagent-control.js").SubagentThreadSnapshot[]>;
|
|
202
212
|
};
|
|
203
213
|
emitUpdate?: (update: ToolUpdate) => void;
|
|
204
214
|
}
|
|
@@ -291,6 +301,13 @@ export interface Provider {
|
|
|
291
301
|
temperature?: number;
|
|
292
302
|
thinkingLevel?: ThinkingLevel;
|
|
293
303
|
abortSignal?: AbortSignal;
|
|
304
|
+
/**
|
|
305
|
+
* How the transport treats HTTP 429 (design doc §4.5). "handle"
|
|
306
|
+
* (default): retry inside the transport. "defer": throw a typed
|
|
307
|
+
* RateLimitError immediately so the caller owns the backoff — used by
|
|
308
|
+
* subagent routes where the scheduler is the single 429 backoff layer.
|
|
309
|
+
*/
|
|
310
|
+
rateLimitPolicy?: import("./network/errors.js").RateLimitPolicy;
|
|
294
311
|
}): AsyncIterable<StreamChunk>;
|
|
295
312
|
complete(messages: ProviderMessage[], options?: {
|
|
296
313
|
model?: string;
|