@bubblebrain-ai/bubble 0.0.28 → 0.0.30
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/README.md +23 -3
- package/dist/agent/categories.d.ts +2 -0
- package/dist/agent/categories.js +4 -0
- package/dist/agent/child-runner.d.ts +5 -1
- package/dist/agent/child-runner.js +35 -2
- package/dist/agent/profiles.js +3 -0
- package/dist/agent/structured-output.d.ts +37 -0
- package/dist/agent/structured-output.js +193 -0
- package/dist/agent/subagent-control.d.ts +3 -0
- package/dist/agent/subagent-scheduler.d.ts +10 -0
- package/dist/agent/subagent-scheduler.js +31 -0
- package/dist/agent/workflow/control.d.ts +37 -0
- package/dist/agent/workflow/control.js +20 -0
- package/dist/agent/workflow/errors.d.ts +16 -0
- package/dist/agent/workflow/errors.js +24 -0
- package/dist/agent/workflow/runtime.d.ts +75 -0
- package/dist/agent/workflow/runtime.js +237 -0
- package/dist/agent.d.ts +105 -0
- package/dist/agent.js +425 -17
- package/dist/context/compact-llm.d.ts +10 -1
- package/dist/context/compact-llm.js +13 -5
- package/dist/context/compact.d.ts +30 -0
- package/dist/context/compact.js +34 -17
- package/dist/goal/format.d.ts +1 -1
- package/dist/goal/format.js +1 -1
- package/dist/network/provider-transport.d.ts +9 -0
- package/dist/network/provider-transport.js +19 -1
- package/dist/provider.d.ts +14 -0
- package/dist/provider.js +24 -0
- package/dist/session.d.ts +16 -0
- package/dist/session.js +33 -1
- package/dist/slash-commands/commands.js +41 -113
- package/dist/slash-commands/types.d.ts +14 -9
- package/dist/tools/agent-lifecycle.d.ts +6 -0
- package/dist/tools/agent-lifecycle.js +285 -0
- package/dist/tools/child-tools.d.ts +10 -0
- package/dist/tools/child-tools.js +12 -0
- package/dist/tools/read.d.ts +1 -1
- package/dist/tools/read.js +9 -0
- package/dist/tui/image-display.d.ts +6 -0
- package/dist/tui/image-display.js +26 -1
- package/dist/tui-ink/app.d.ts +0 -18
- package/dist/tui-ink/app.js +168 -230
- package/dist/tui-ink/compaction-progress.d.ts +19 -0
- package/dist/tui-ink/compaction-progress.js +74 -0
- package/dist/tui-ink/input-box.d.ts +10 -1
- package/dist/tui-ink/input-box.js +56 -16
- package/dist/tui-ink/markdown.d.ts +18 -0
- package/dist/tui-ink/markdown.js +172 -16
- package/dist/tui-ink/message-list.d.ts +1 -2
- package/dist/tui-ink/message-list.js +50 -107
- package/dist/tui-ink/run.js +5 -0
- package/dist/tui-ink/subagent-inspector.d.ts +17 -0
- package/dist/tui-ink/subagent-inspector.js +189 -0
- package/dist/tui-ink/subagent-view.d.ts +47 -0
- package/dist/tui-ink/subagent-view.js +163 -0
- package/dist/tui-ink/terminal-env.d.ts +15 -0
- package/dist/tui-ink/terminal-env.js +22 -0
- package/dist/tui-ink/use-terminal-size.js +33 -6
- package/dist/tui-ink/width.d.ts +18 -0
- package/dist/tui-ink/width.js +130 -0
- package/dist/types.d.ts +35 -0
- package/package.json +2 -1
package/dist/context/compact.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
export function
|
|
1
|
+
export function planSessionCompaction(entries, options = {}) {
|
|
2
2
|
const keepRecentTurns = options.keepRecentTurns ?? 2;
|
|
3
|
-
const maxSummaryItems = options.maxSummaryItems ?? 4;
|
|
4
3
|
const metadataEntries = entries.filter((entry) => entry.type === "metadata");
|
|
5
4
|
const nonMetadataEntries = entries.filter((entry) => entry.type !== "metadata");
|
|
6
5
|
const latestSummaryIndex = findLatestSummaryIndex(nonMetadataEntries);
|
|
@@ -10,34 +9,52 @@ export function compactSessionEntries(entries, options = {}) {
|
|
|
10
9
|
.map((entry, index) => (entry.type === "user_message" ? index : -1))
|
|
11
10
|
.filter((index) => index >= 0);
|
|
12
11
|
if (turnStartIndexes.length <= keepRecentTurns) {
|
|
13
|
-
return {
|
|
12
|
+
return { compactable: false };
|
|
14
13
|
}
|
|
15
14
|
const keepStartIndex = turnStartIndexes[Math.max(0, turnStartIndexes.length - keepRecentTurns)];
|
|
16
15
|
if (keepStartIndex <= 0) {
|
|
17
|
-
return {
|
|
18
|
-
}
|
|
19
|
-
const oldEntries = activeEntries.slice(0, keepStartIndex);
|
|
20
|
-
const keptEntries = activeEntries.slice(keepStartIndex);
|
|
21
|
-
const summary = buildCompactionSummary(oldEntries, maxSummaryItems);
|
|
22
|
-
if (!summary) {
|
|
23
|
-
return { compacted: false };
|
|
16
|
+
return { compactable: false };
|
|
24
17
|
}
|
|
18
|
+
return {
|
|
19
|
+
compactable: true,
|
|
20
|
+
metadataEntries,
|
|
21
|
+
oldEntries: activeEntries.slice(0, keepStartIndex),
|
|
22
|
+
keptEntries: activeEntries.slice(keepStartIndex),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Assemble the post-compaction entry list from a plan and a (possibly
|
|
27
|
+
* LLM-generated) summary string. The summary entry is keyed off the full
|
|
28
|
+
* original `entries` so its id never collides with a prior summary.
|
|
29
|
+
*/
|
|
30
|
+
export function buildCompactedEntries(entries, plan, summary) {
|
|
25
31
|
const summaryEntry = {
|
|
26
32
|
id: nextSummaryId(entries),
|
|
27
33
|
type: "summary",
|
|
28
34
|
summary,
|
|
29
35
|
timestamp: Date.now(),
|
|
30
36
|
};
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
return [...plan.metadataEntries, summaryEntry, ...plan.keptEntries];
|
|
38
|
+
}
|
|
39
|
+
/** Flatten a plan's old entries into messages for an external summarizer. */
|
|
40
|
+
export function planOldMessages(plan) {
|
|
41
|
+
return entriesToMessages(plan.oldEntries);
|
|
42
|
+
}
|
|
43
|
+
export function compactSessionEntries(entries, options = {}) {
|
|
44
|
+
const maxSummaryItems = options.maxSummaryItems ?? 4;
|
|
45
|
+
const plan = planSessionCompaction(entries, options);
|
|
46
|
+
if (!plan.compactable) {
|
|
47
|
+
return { compacted: false };
|
|
48
|
+
}
|
|
49
|
+
const summary = buildCompactionSummary(plan.oldEntries, maxSummaryItems);
|
|
50
|
+
if (!summary) {
|
|
51
|
+
return { compacted: false };
|
|
52
|
+
}
|
|
36
53
|
return {
|
|
37
54
|
compacted: true,
|
|
38
55
|
summary,
|
|
39
|
-
entries:
|
|
40
|
-
droppedEntries: oldEntries.length,
|
|
56
|
+
entries: buildCompactedEntries(entries, plan, summary),
|
|
57
|
+
droppedEntries: plan.oldEntries.length,
|
|
41
58
|
};
|
|
42
59
|
}
|
|
43
60
|
export function compactMessages(messages, options = {}) {
|
package/dist/goal/format.d.ts
CHANGED
|
@@ -14,5 +14,5 @@ export declare function goalSummaryText(goal: GoalState): string;
|
|
|
14
14
|
* update_goal tool can't report this — see goal/tools.ts).
|
|
15
15
|
*/
|
|
16
16
|
export declare function goalCompleteNotice(goal: GoalState): string;
|
|
17
|
-
/** Compact single-line indicator for
|
|
17
|
+
/** Compact single-line indicator for status surfaces. */
|
|
18
18
|
export declare function goalIndicatorLine(goal: GoalState, maxObjective?: number): string;
|
package/dist/goal/format.js
CHANGED
|
@@ -95,7 +95,7 @@ function completionTokenUsagePhrase(goal) {
|
|
|
95
95
|
? `${formatTokensCompact(goal.tokensUsed)}/${formatTokensCompact(goal.tokenBudget)} tok used`
|
|
96
96
|
: `${formatTokensCompact(goal.tokensUsed)} tok used`;
|
|
97
97
|
}
|
|
98
|
-
/** Compact single-line indicator for
|
|
98
|
+
/** Compact single-line indicator for status surfaces. */
|
|
99
99
|
export function goalIndicatorLine(goal, maxObjective = 48) {
|
|
100
100
|
const segments = [`goal: ${goalStatusLabel(goal.status)}`, `${goal.turnsSpent} turns`];
|
|
101
101
|
const tokens = tokensPart(goal);
|
|
@@ -28,5 +28,14 @@ export declare function normalizeProviderNetworkError(error: unknown, options: {
|
|
|
28
28
|
env?: NodeJS.ProcessEnv;
|
|
29
29
|
}): Error;
|
|
30
30
|
export declare function isProviderTransportError(error: unknown): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Request/response timeouts surface as prose rather than errno tokens — e.g.
|
|
33
|
+
* Bun fetch throws a DOMException named "TimeoutError" with message
|
|
34
|
+
* "The operation timed out.", and openai-node raises APIConnectionTimeoutError.
|
|
35
|
+
* These are kept OUT of isProviderNetworkErrorText on purpose: that predicate
|
|
36
|
+
* drives normalizeProviderNetworkError's proxy/TLS/CA advice, and a plain
|
|
37
|
+
* timeout must not be rewrapped into a misleading "check your proxy" message.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isProviderTimeoutErrorText(text: string): boolean;
|
|
31
40
|
export declare function shouldEnableFetchVerbose(env?: NodeJS.ProcessEnv, providerVerboseEnvVar?: string): boolean;
|
|
32
41
|
export {};
|
|
@@ -99,7 +99,25 @@ export function normalizeProviderNetworkError(error, options) {
|
|
|
99
99
|
return new Error(message, { cause: error });
|
|
100
100
|
}
|
|
101
101
|
export function isProviderTransportError(error) {
|
|
102
|
-
|
|
102
|
+
const text = errorMessageChain(error).join("\n");
|
|
103
|
+
return isProviderNetworkErrorText(text) || isProviderTimeoutErrorText(text);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Request/response timeouts surface as prose rather than errno tokens — e.g.
|
|
107
|
+
* Bun fetch throws a DOMException named "TimeoutError" with message
|
|
108
|
+
* "The operation timed out.", and openai-node raises APIConnectionTimeoutError.
|
|
109
|
+
* These are kept OUT of isProviderNetworkErrorText on purpose: that predicate
|
|
110
|
+
* drives normalizeProviderNetworkError's proxy/TLS/CA advice, and a plain
|
|
111
|
+
* timeout must not be rewrapped into a misleading "check your proxy" message.
|
|
112
|
+
*/
|
|
113
|
+
export function isProviderTimeoutErrorText(text) {
|
|
114
|
+
return [
|
|
115
|
+
/operation timed out/i,
|
|
116
|
+
/request timed out/i,
|
|
117
|
+
/\bTimeoutError\b/i,
|
|
118
|
+
/\bAPIConnectionTimeoutError\b/i,
|
|
119
|
+
/\bESOCKETTIMEDOUT\b/i,
|
|
120
|
+
].some((pattern) => pattern.test(text));
|
|
103
121
|
}
|
|
104
122
|
export function shouldEnableFetchVerbose(env = process.env, providerVerboseEnvVar) {
|
|
105
123
|
const providerValue = providerVerboseEnvVar ? env[providerVerboseEnvVar] : undefined;
|
package/dist/provider.d.ts
CHANGED
|
@@ -54,4 +54,18 @@ export declare function translateOpenAIFullResponse(response: any): AsyncIterabl
|
|
|
54
54
|
* in index order to keep multi-call turns deterministic.
|
|
55
55
|
*/
|
|
56
56
|
export declare function translateOpenAIStream(stream: AsyncIterable<any>, options?: TranslateOpenAIStreamOptions): AsyncIterable<StreamChunk>;
|
|
57
|
+
/** Largest value Node's 32-bit timers accept; ~24.8 days. */
|
|
58
|
+
export declare const MAX_TIMER_MS = 2147483647;
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the provider request timeout (ms) from the operator override.
|
|
61
|
+
*
|
|
62
|
+
* Default is effectively NO TIMEOUT — safe for streaming APIs where the model
|
|
63
|
+
* sends chunks continuously. But Node's timers are 32-bit: a duration above
|
|
64
|
+
* 2**31-1 ms overflows, which makes Node print a TimeoutOverflowWarning to
|
|
65
|
+
* stderr (corrupting the Ink TUI) AND silently clamp the timeout to 1ms,
|
|
66
|
+
* aborting the request almost immediately. So we use the largest SAFE timer
|
|
67
|
+
* value as the "no timeout" sentinel — never Number.MAX_SAFE_INTEGER — and
|
|
68
|
+
* clamp any operator-supplied value into range too.
|
|
69
|
+
*/
|
|
70
|
+
export declare function resolveRequestTimeoutMs(raw: string | undefined): number;
|
|
57
71
|
export {};
|
package/dist/provider.js
CHANGED
|
@@ -94,6 +94,7 @@ export function createProviderInstance(options) {
|
|
|
94
94
|
const client = new OpenAI({
|
|
95
95
|
apiKey: options.apiKey,
|
|
96
96
|
baseURL: options.baseURL,
|
|
97
|
+
timeout: resolveRequestTimeoutMs(process.env.BUBBLE_PROVIDER_REQUEST_TIMEOUT_MS),
|
|
97
98
|
});
|
|
98
99
|
const fallbackModel = "gpt-4o";
|
|
99
100
|
async function* streamChat(messages, chatOptions) {
|
|
@@ -704,6 +705,29 @@ function mergeToolArgumentDelta(current, incoming, mode) {
|
|
|
704
705
|
debugToolArgs({ stage: "merge", branch: mode === "delta" ? "delta-append" : "snapshot-fallback-concat", current, incoming, args: current + incoming, delta: incoming });
|
|
705
706
|
return { args: current + incoming, delta: incoming };
|
|
706
707
|
}
|
|
708
|
+
function parsePositiveInt(raw) {
|
|
709
|
+
if (!raw?.trim())
|
|
710
|
+
return undefined;
|
|
711
|
+
const value = Number(raw);
|
|
712
|
+
return Number.isInteger(value) && value > 0 ? value : undefined;
|
|
713
|
+
}
|
|
714
|
+
/** Largest value Node's 32-bit timers accept; ~24.8 days. */
|
|
715
|
+
export const MAX_TIMER_MS = 2_147_483_647; // 2**31 - 1
|
|
716
|
+
/**
|
|
717
|
+
* Resolve the provider request timeout (ms) from the operator override.
|
|
718
|
+
*
|
|
719
|
+
* Default is effectively NO TIMEOUT — safe for streaming APIs where the model
|
|
720
|
+
* sends chunks continuously. But Node's timers are 32-bit: a duration above
|
|
721
|
+
* 2**31-1 ms overflows, which makes Node print a TimeoutOverflowWarning to
|
|
722
|
+
* stderr (corrupting the Ink TUI) AND silently clamp the timeout to 1ms,
|
|
723
|
+
* aborting the request almost immediately. So we use the largest SAFE timer
|
|
724
|
+
* value as the "no timeout" sentinel — never Number.MAX_SAFE_INTEGER — and
|
|
725
|
+
* clamp any operator-supplied value into range too.
|
|
726
|
+
*/
|
|
727
|
+
export function resolveRequestTimeoutMs(raw) {
|
|
728
|
+
const requested = parsePositiveInt(raw);
|
|
729
|
+
return Math.min(requested ?? MAX_TIMER_MS, MAX_TIMER_MS);
|
|
730
|
+
}
|
|
707
731
|
function mergeStreamingText(current, incoming, mode) {
|
|
708
732
|
if (!current)
|
|
709
733
|
return { args: incoming, delta: incoming };
|
package/dist/session.d.ts
CHANGED
|
@@ -57,6 +57,22 @@ export declare class SessionManager {
|
|
|
57
57
|
appendTodosSnapshot(todos: Todo[]): void;
|
|
58
58
|
getTodos(): Todo[];
|
|
59
59
|
compact(options?: CompactOptions): CompactResult;
|
|
60
|
+
/**
|
|
61
|
+
* Inspect whether the session is large enough to compact and, if so, return
|
|
62
|
+
* the older messages an external summarizer should condense. Returns null
|
|
63
|
+
* when there isn't enough history past the last summary to bother — the
|
|
64
|
+
* caller should then report "already compact enough" without calling a model.
|
|
65
|
+
*/
|
|
66
|
+
getCompactionPlan(options?: CompactOptions): {
|
|
67
|
+
oldMessages: Message[];
|
|
68
|
+
} | null;
|
|
69
|
+
/**
|
|
70
|
+
* Apply a precomputed (typically LLM-generated) summary as the compaction
|
|
71
|
+
* checkpoint, rewriting the log to [metadata, summary, kept turns]. Mirrors
|
|
72
|
+
* `compact()` but skips the built-in heuristic summarizer. Returns
|
|
73
|
+
* `{ compacted: false }` if the session is no longer compactable.
|
|
74
|
+
*/
|
|
75
|
+
applyLLMCompaction(summary: string, options?: CompactOptions): CompactResult;
|
|
60
76
|
getMessages(): Message[];
|
|
61
77
|
/**
|
|
62
78
|
* Pre-edit file snapshot store for this session, used by /rewind.
|
package/dist/session.js
CHANGED
|
@@ -6,7 +6,7 @@ import { mkdirSync, appendFileSync, existsSync, readFileSync, readdirSync, statS
|
|
|
6
6
|
import { basename, dirname, join } from "node:path";
|
|
7
7
|
import { getBubbleHome } from "./bubble-home.js";
|
|
8
8
|
import { CheckpointStore } from "./checkpoints.js";
|
|
9
|
-
import { compactSessionEntries } from "./context/compact.js";
|
|
9
|
+
import { buildCompactedEntries, compactSessionEntries, planOldMessages, planSessionCompaction, } from "./context/compact.js";
|
|
10
10
|
import { SessionLog } from "./session-log.js";
|
|
11
11
|
import { normalizeSingleLine, truncateVisual } from "./text-display.js";
|
|
12
12
|
import { deterministicTitleFromUserContent } from "./session-title.js";
|
|
@@ -162,6 +162,38 @@ export class SessionManager {
|
|
|
162
162
|
}
|
|
163
163
|
return result;
|
|
164
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Inspect whether the session is large enough to compact and, if so, return
|
|
167
|
+
* the older messages an external summarizer should condense. Returns null
|
|
168
|
+
* when there isn't enough history past the last summary to bother — the
|
|
169
|
+
* caller should then report "already compact enough" without calling a model.
|
|
170
|
+
*/
|
|
171
|
+
getCompactionPlan(options) {
|
|
172
|
+
const plan = planSessionCompaction(this.log.list(), options);
|
|
173
|
+
if (!plan.compactable)
|
|
174
|
+
return null;
|
|
175
|
+
return { oldMessages: planOldMessages(plan) };
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Apply a precomputed (typically LLM-generated) summary as the compaction
|
|
179
|
+
* checkpoint, rewriting the log to [metadata, summary, kept turns]. Mirrors
|
|
180
|
+
* `compact()` but skips the built-in heuristic summarizer. Returns
|
|
181
|
+
* `{ compacted: false }` if the session is no longer compactable.
|
|
182
|
+
*/
|
|
183
|
+
applyLLMCompaction(summary, options) {
|
|
184
|
+
const entries = this.log.list();
|
|
185
|
+
const plan = planSessionCompaction(entries, options);
|
|
186
|
+
if (!plan.compactable)
|
|
187
|
+
return { compacted: false };
|
|
188
|
+
const nextEntries = buildCompactedEntries(entries, plan, summary);
|
|
189
|
+
this.rewrite(nextEntries);
|
|
190
|
+
return {
|
|
191
|
+
compacted: true,
|
|
192
|
+
summary,
|
|
193
|
+
entries: nextEntries,
|
|
194
|
+
droppedEntries: plan.oldEntries.length,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
165
197
|
getMessages() {
|
|
166
198
|
return this.log.toMessages();
|
|
167
199
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { UserConfig
|
|
1
|
+
import { UserConfig } from "../config.js";
|
|
2
2
|
import { formatContextUsage } from "../context/usage.js";
|
|
3
3
|
import { formatDiagnostics } from "../lsp/index.js";
|
|
4
4
|
import { normalizeNameForMCP } from "../mcp/name.js";
|
|
@@ -8,7 +8,6 @@ import { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingL
|
|
|
8
8
|
import { SessionManager } from "../session.js";
|
|
9
9
|
import { buildSystemPrompt } from "../system-prompt.js";
|
|
10
10
|
import { normalizeSingleLine } from "../text-display.js";
|
|
11
|
-
import { copyToClipboard } from "../clipboard.js";
|
|
12
11
|
import { formatRelativeTime } from "../tui/recent-activity.js";
|
|
13
12
|
import { HOOK_EVENT_NAMES, isHookEventName } from "../hooks/index.js";
|
|
14
13
|
import { isThinkingLevel } from "../variant/thinking-level.js";
|
|
@@ -287,17 +286,6 @@ async function handleMemoryCommand(args, ctx) {
|
|
|
287
286
|
}
|
|
288
287
|
return "Usage: /memory [status|search|compact|summarize|refresh|reset]";
|
|
289
288
|
}
|
|
290
|
-
function parseKeyArgs(args, ctx) {
|
|
291
|
-
const trimmed = args.trim();
|
|
292
|
-
const [first, ...rest] = trimmed.split(/\s+/);
|
|
293
|
-
const explicitProvider = first
|
|
294
|
-
? ctx.registry.getConfigured().find((provider) => provider.id === first)
|
|
295
|
-
: undefined;
|
|
296
|
-
if (explicitProvider) {
|
|
297
|
-
return { provider: explicitProvider, apiKey: rest.join(" ") };
|
|
298
|
-
}
|
|
299
|
-
return { provider: ctx.registry.getDefault(), apiKey: trimmed };
|
|
300
|
-
}
|
|
301
289
|
const builtinSlashCommandEntries = [
|
|
302
290
|
{
|
|
303
291
|
name: "skills",
|
|
@@ -363,33 +351,6 @@ const builtinSlashCommandEntries = [
|
|
|
363
351
|
return `Theme set to ${arg}${arg === "auto" ? ` (resolved to ${resolved})` : ""}.`;
|
|
364
352
|
},
|
|
365
353
|
},
|
|
366
|
-
{
|
|
367
|
-
name: "sidebar",
|
|
368
|
-
description: "Toggle the right sidebar. Usage: /sidebar [open|close|auto]",
|
|
369
|
-
async handler(args, ctx) {
|
|
370
|
-
if (!ctx.toggleSidebar || !ctx.setSidebarMode) {
|
|
371
|
-
return "Sidebar control is only available inside the TUI.";
|
|
372
|
-
}
|
|
373
|
-
const arg = args.trim().toLowerCase();
|
|
374
|
-
if (!arg) {
|
|
375
|
-
ctx.toggleSidebar();
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
if (["open", "show", "expand", "expanded", "on"].includes(arg)) {
|
|
379
|
-
ctx.setSidebarMode("expanded");
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
if (["close", "hide", "collapse", "collapsed", "off"].includes(arg)) {
|
|
383
|
-
ctx.setSidebarMode("collapsed");
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
if (arg === "auto") {
|
|
387
|
-
ctx.setSidebarMode("auto");
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
return "Usage: /sidebar [open|close|auto]";
|
|
391
|
-
},
|
|
392
|
-
},
|
|
393
354
|
{
|
|
394
355
|
name: "clear",
|
|
395
356
|
description: "Clear the current conversation history",
|
|
@@ -403,27 +364,6 @@ const builtinSlashCommandEntries = [
|
|
|
403
364
|
ctx.clearMessages();
|
|
404
365
|
},
|
|
405
366
|
},
|
|
406
|
-
{
|
|
407
|
-
name: "copy",
|
|
408
|
-
description: "Copy the last assistant message to the system clipboard",
|
|
409
|
-
async handler(args, ctx) {
|
|
410
|
-
const lastAssistant = [...ctx.agent.messages]
|
|
411
|
-
.reverse()
|
|
412
|
-
.find((m) => m.role === "assistant" && typeof m.content === "string" && m.content.trim().length > 0);
|
|
413
|
-
if (!lastAssistant || typeof lastAssistant.content !== "string") {
|
|
414
|
-
return "No assistant message to copy yet.";
|
|
415
|
-
}
|
|
416
|
-
const text = lastAssistant.content;
|
|
417
|
-
try {
|
|
418
|
-
await copyToClipboard(text);
|
|
419
|
-
}
|
|
420
|
-
catch (err) {
|
|
421
|
-
return `Failed to copy to clipboard: ${err?.message || String(err)}`;
|
|
422
|
-
}
|
|
423
|
-
const chars = text.length;
|
|
424
|
-
return `Copied last assistant message to clipboard (${chars} character${chars === 1 ? "" : "s"}).`;
|
|
425
|
-
},
|
|
426
|
-
},
|
|
427
367
|
{
|
|
428
368
|
name: "rewind",
|
|
429
369
|
description: "Rewind conversation and/or file edits to before an earlier message. Usage: /rewind [n] [--code|--chat]",
|
|
@@ -667,31 +607,6 @@ const builtinSlashCommandEntries = [
|
|
|
667
607
|
return `Model switched to ${displaySelectedModel(next, ctx.agent.thinking)}.`;
|
|
668
608
|
},
|
|
669
609
|
},
|
|
670
|
-
{
|
|
671
|
-
name: "key",
|
|
672
|
-
description: "Set API key for the current or a specific provider. Usage: /key [provider-id] <key>",
|
|
673
|
-
async handler(args, ctx) {
|
|
674
|
-
if (!args) {
|
|
675
|
-
ctx.openPicker("key");
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
const { provider, apiKey } = parseKeyArgs(args, ctx);
|
|
679
|
-
if (!provider) {
|
|
680
|
-
return "No provider configured. Use /provider --add <id> first.";
|
|
681
|
-
}
|
|
682
|
-
if (!apiKey) {
|
|
683
|
-
return `Usage: /key ${provider.id} <key>`;
|
|
684
|
-
}
|
|
685
|
-
if (ctx.registry.getModelConfig().hasProvider(provider.id)) {
|
|
686
|
-
return `API key for ${provider.name} is managed in ~/.bubble/models.json. Please edit that file directly.`;
|
|
687
|
-
}
|
|
688
|
-
ctx.registry.updateProviderKey(provider.id, apiKey);
|
|
689
|
-
ctx.registry.setDefault(provider.id);
|
|
690
|
-
ctx.agent.setProvider(ctx.createProvider(provider.id, apiKey, provider.baseURL));
|
|
691
|
-
ctx.agent.providerId = provider.id;
|
|
692
|
-
return `API key updated for ${provider.name} to ${maskKey(apiKey)}.`;
|
|
693
|
-
},
|
|
694
|
-
},
|
|
695
610
|
{
|
|
696
611
|
name: "logout",
|
|
697
612
|
description: "Remove OAuth credentials for a provider. Usage: /logout [openai]",
|
|
@@ -732,32 +647,6 @@ const builtinSlashCommandEntries = [
|
|
|
732
647
|
: "Exited plan mode.";
|
|
733
648
|
},
|
|
734
649
|
},
|
|
735
|
-
{
|
|
736
|
-
name: "todos",
|
|
737
|
-
description: "Show the current todo list. Use /todos clear to reset it.",
|
|
738
|
-
async handler(args, ctx) {
|
|
739
|
-
const sub = args.trim();
|
|
740
|
-
if (sub === "clear") {
|
|
741
|
-
const previous = ctx.agent.getTodos().length;
|
|
742
|
-
if (previous === 0) {
|
|
743
|
-
return "Todo list is already empty.";
|
|
744
|
-
}
|
|
745
|
-
ctx.agent.setTodos([]);
|
|
746
|
-
return `Cleared ${previous} todo item${previous === 1 ? "" : "s"}.`;
|
|
747
|
-
}
|
|
748
|
-
const todos = ctx.agent.getTodos();
|
|
749
|
-
if (todos.length === 0) {
|
|
750
|
-
return "No todos yet. The assistant will create some when working on multi-step tasks.";
|
|
751
|
-
}
|
|
752
|
-
const glyph = (status) => status === "completed" ? "✔" : status === "in_progress" ? "▶" : "○";
|
|
753
|
-
const lines = ["Todos:"];
|
|
754
|
-
for (const todo of todos) {
|
|
755
|
-
const label = todo.status === "in_progress" ? (todo.activeForm || todo.content) : todo.content;
|
|
756
|
-
lines.push(` ${glyph(todo.status)} ${label}`);
|
|
757
|
-
}
|
|
758
|
-
return lines.join("\n");
|
|
759
|
-
},
|
|
760
|
-
},
|
|
761
650
|
{
|
|
762
651
|
name: "permissions",
|
|
763
652
|
description: "Inspect or edit allow/deny rules. Subcommands: add <scope> <list> <rule>, remove <scope> <list> <rule>, clear (session allowlist), reload.",
|
|
@@ -978,7 +867,46 @@ const builtinSlashCommandEntries = [
|
|
|
978
867
|
if (preHook?.decision === "deny") {
|
|
979
868
|
return preHook.reason ?? `Compaction blocked by hook ${preHook.sourceHookId ?? "<unknown>"}.`;
|
|
980
869
|
}
|
|
981
|
-
|
|
870
|
+
// Plan first so we can report "already compact" without spending a model
|
|
871
|
+
// call, and so the LLM summarizer gets the exact set of evicted messages.
|
|
872
|
+
const plan = ctx.sessionManager.getCompactionPlan();
|
|
873
|
+
if (!plan) {
|
|
874
|
+
await ctx.hookController?.runEvent({
|
|
875
|
+
eventName: "PostCompact",
|
|
876
|
+
cwd: ctx.cwd,
|
|
877
|
+
sessionId: ctx.sessionManager.getSessionFile(),
|
|
878
|
+
agentRole: "driver",
|
|
879
|
+
target: "manual",
|
|
880
|
+
payload: { kind: "manual", compacted: false },
|
|
881
|
+
});
|
|
882
|
+
return "Session is already compact enough.";
|
|
883
|
+
}
|
|
884
|
+
// Stream an LLM summary for high fidelity, reporting progress to the TUI.
|
|
885
|
+
// On any failure (or empty output) fall back to the instant heuristic
|
|
886
|
+
// compaction so /compact always makes progress.
|
|
887
|
+
let result;
|
|
888
|
+
try {
|
|
889
|
+
ctx.compactionProgress?.({ phase: "collecting", streamedChars: 0 });
|
|
890
|
+
let summary = "";
|
|
891
|
+
try {
|
|
892
|
+
summary = await ctx.agent.summarizeForCompaction(plan.oldMessages, (full) => {
|
|
893
|
+
ctx.compactionProgress?.({ phase: "summarizing", streamedChars: full.length });
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
catch {
|
|
897
|
+
summary = "";
|
|
898
|
+
}
|
|
899
|
+
if (summary) {
|
|
900
|
+
ctx.compactionProgress?.({ phase: "applying", streamedChars: summary.length });
|
|
901
|
+
result = ctx.sessionManager.applyLLMCompaction(summary);
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
result = ctx.sessionManager.compact();
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
finally {
|
|
908
|
+
ctx.compactionProgress?.(null);
|
|
909
|
+
}
|
|
982
910
|
if (!result.compacted) {
|
|
983
911
|
await ctx.hookController?.runEvent({
|
|
984
912
|
eventName: "PostCompact",
|
|
@@ -10,11 +10,15 @@ import type { LspService } from "../lsp/index.js";
|
|
|
10
10
|
import type { MemoryScope } from "../memory/index.js";
|
|
11
11
|
import type { ThemeMode } from "../config.js";
|
|
12
12
|
import type { ExternalHookController } from "../hooks/controller.js";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Live progress for a manual `/compact` run, pushed to the TUI so it can render
|
|
15
|
+
* a progress bar. `phase` advances collecting → summarizing → applying;
|
|
16
|
+
* `streamedChars` is the running length of the streamed summary (drives the
|
|
17
|
+
* bar's fill within the summarizing phase). Hosts without a UI omit the sink.
|
|
18
|
+
*/
|
|
19
|
+
export interface CompactionProgress {
|
|
20
|
+
phase: "collecting" | "summarizing" | "applying";
|
|
21
|
+
streamedChars: number;
|
|
18
22
|
}
|
|
19
23
|
export interface SlashCommandContext {
|
|
20
24
|
agent: Agent;
|
|
@@ -42,10 +46,6 @@ export interface SlashCommandContext {
|
|
|
42
46
|
getResolvedTheme?: () => "light" | "dark";
|
|
43
47
|
/** Persist a new theme mode AND apply it to the running TUI. */
|
|
44
48
|
setThemeMode?: (mode: ThemeMode) => void;
|
|
45
|
-
/** Toggle the right session sidebar in the running TUI. */
|
|
46
|
-
toggleSidebar?: () => SidebarCommandState;
|
|
47
|
-
/** Set the right session sidebar mode in the running TUI. */
|
|
48
|
-
setSidebarMode?: (mode: SidebarMode) => SidebarCommandState;
|
|
49
49
|
/** Open the feedback dialog. `initialDescription` prefills the description field. */
|
|
50
50
|
openFeedback?: (initialDescription: string) => void;
|
|
51
51
|
/** Open the interactive rewind picker. When absent, /rewind falls back to a text listing. */
|
|
@@ -56,6 +56,11 @@ export interface SlashCommandContext {
|
|
|
56
56
|
fillComposer?: (text: string) => void;
|
|
57
57
|
/** Open the interactive usage stats panel. */
|
|
58
58
|
openStats?: () => void;
|
|
59
|
+
/**
|
|
60
|
+
* Push live compaction progress to the running TUI. Pass a progress object
|
|
61
|
+
* while compacting and `null` to clear the indicator. Absent in non-TUI hosts.
|
|
62
|
+
*/
|
|
63
|
+
compactionProgress?: (progress: CompactionProgress | null) => void;
|
|
59
64
|
}
|
|
60
65
|
/**
|
|
61
66
|
* Return types for a slash command handler:
|
|
@@ -28,5 +28,11 @@ export declare function createListAgentsTool(): ToolRegistryEntry;
|
|
|
28
28
|
export declare const AGENT_TEAM_MIN_ITEMS = 2;
|
|
29
29
|
export declare const AGENT_TEAM_MAX_ITEMS = 32;
|
|
30
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;
|
|
36
|
+
export declare function createWaitWorkflowTool(): ToolRegistryEntry;
|
|
31
37
|
export declare function createAgentLifecycleTools(options?: AgentLifecycleToolOptions): ToolRegistryEntry[];
|
|
32
38
|
export {};
|