@bubblebrain-ai/bubble 0.0.1 → 0.0.2
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.d.ts +8 -1
- package/dist/agent.js +45 -6
- package/dist/approval/controller.d.ts +3 -3
- package/dist/approval/controller.js +5 -5
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +2 -3
- package/dist/main.js +1 -3
- package/dist/permission/mode.d.ts +3 -7
- package/dist/permission/mode.js +7 -11
- package/dist/permissions/settings.js +3 -4
- package/dist/prompt/reminders.d.ts +1 -0
- package/dist/prompt/reminders.js +10 -16
- package/dist/provider-openai-codex.js +2 -0
- package/dist/provider.js +6 -2
- package/dist/slash-commands/commands.js +2 -23
- package/dist/slash-commands/types.d.ts +1 -1
- package/dist/tools/bash.js +30 -3
- package/dist/tui/clipboard.d.ts +1 -0
- package/dist/tui/clipboard.js +53 -0
- package/dist/tui/global-key-router.d.ts +3 -0
- package/dist/tui/global-key-router.js +87 -0
- package/dist/tui/prompt-keybindings.d.ts +1 -0
- package/dist/tui/prompt-keybindings.js +7 -0
- package/dist/tui/run.d.ts +1 -2
- package/dist/tui/run.js +679 -194
- package/dist/types.d.ts +5 -5
- package/package.json +2 -2
package/dist/agent.d.ts
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { AgentEvent, ContentPart, PermissionMode, Message, Provider, ThinkingLevel, Todo, ToolResult, ToolRegistryEntry } from "./types.js";
|
|
6
6
|
import { type TurnHooks } from "./orchestrator/hooks.js";
|
|
7
|
+
export declare class AgentAbortError extends Error {
|
|
8
|
+
constructor(message?: string);
|
|
9
|
+
}
|
|
7
10
|
export interface AgentOptions {
|
|
8
11
|
provider: Provider;
|
|
9
12
|
sessionID?: string;
|
|
@@ -57,6 +60,7 @@ export declare class Agent {
|
|
|
57
60
|
/** Whether a given tool is deferred and not yet unlocked. */
|
|
58
61
|
isDeferredAndLocked(name: string): boolean;
|
|
59
62
|
injectSystemReminder(content: string): void;
|
|
63
|
+
injectModeReminder(): void;
|
|
60
64
|
get model(): string;
|
|
61
65
|
set model(value: string);
|
|
62
66
|
get providerId(): string;
|
|
@@ -67,6 +71,7 @@ export declare class Agent {
|
|
|
67
71
|
model?: string;
|
|
68
72
|
temperature?: number;
|
|
69
73
|
thinkingLevel?: ThinkingLevel;
|
|
74
|
+
abortSignal?: AbortSignal;
|
|
70
75
|
}): Promise<string>;
|
|
71
76
|
get thinking(): ThinkingLevel;
|
|
72
77
|
set thinking(value: ThinkingLevel);
|
|
@@ -82,7 +87,9 @@ export declare class Agent {
|
|
|
82
87
|
/** Internal: snapshot counter that bumps on every setTodos. Used by run loop to detect mutations. */
|
|
83
88
|
get todosVersion(): number;
|
|
84
89
|
setSystemPrompt(prompt: string): void;
|
|
85
|
-
run(userInput: string | ContentPart[], cwd: string
|
|
90
|
+
run(userInput: string | ContentPart[], cwd: string, options?: {
|
|
91
|
+
abortSignal?: AbortSignal;
|
|
92
|
+
}): AsyncIterable<AgentEvent>;
|
|
86
93
|
private recoverFromOverflow;
|
|
87
94
|
compactResidentHistory(): void;
|
|
88
95
|
runSubtask(input: string | ContentPart[], cwd: string, options?: {
|
package/dist/agent.js
CHANGED
|
@@ -8,7 +8,7 @@ import { getContextBudget } from "./context/budget.js";
|
|
|
8
8
|
import { isContextOverflowError } from "./context/overflow.js";
|
|
9
9
|
import { projectMessages } from "./context/projector.js";
|
|
10
10
|
import { aggressivePruneMessages } from "./context/prune.js";
|
|
11
|
-
import { buildDeferredToolsReminder, buildToolFreezeReminder, reminderForMode } from "./prompt/reminders.js";
|
|
11
|
+
import { buildDeferredToolsReminder, buildToolFreezeReminder, isPermissionModeReminder, reminderForMode } from "./prompt/reminders.js";
|
|
12
12
|
import { HookBus } from "./orchestrator/hooks.js";
|
|
13
13
|
import { createDefaultHooks } from "./orchestrator/default-hooks.js";
|
|
14
14
|
import { filterToolsForSubtask, getSubtaskPolicy } from "./agent/subtask-policy.js";
|
|
@@ -19,6 +19,12 @@ const RESIDENT_HISTORY_CHAR_SOFT_LIMIT = 256 * 1024;
|
|
|
19
19
|
const RESIDENT_HISTORY_CHAR_HARD_LIMIT = 512 * 1024;
|
|
20
20
|
const RESIDENT_HISTORY_HEAP_SOFT_LIMIT = 512 * 1024 * 1024;
|
|
21
21
|
const RESIDENT_HISTORY_HEAP_HARD_LIMIT = 768 * 1024 * 1024;
|
|
22
|
+
export class AgentAbortError extends Error {
|
|
23
|
+
constructor(message = "Agent run cancelled.") {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "AgentAbortError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
22
28
|
export class Agent {
|
|
23
29
|
messages = [];
|
|
24
30
|
provider;
|
|
@@ -67,7 +73,7 @@ export class Agent {
|
|
|
67
73
|
// If the agent boots in a non-default mode, inject the corresponding reminder so the
|
|
68
74
|
// model sees the active rules on its very first turn. Default mode needs no reminder.
|
|
69
75
|
if (this._mode !== "default") {
|
|
70
|
-
this.
|
|
76
|
+
this.injectModeReminder();
|
|
71
77
|
}
|
|
72
78
|
// Advertise any deferred tools so the model knows they exist and how to
|
|
73
79
|
// reach them. Keeps the per-turn tool list small; schemas load on demand.
|
|
@@ -97,6 +103,12 @@ export class Agent {
|
|
|
97
103
|
injectSystemReminder(content) {
|
|
98
104
|
this.appendMessage({ role: "user", content, isMeta: true });
|
|
99
105
|
}
|
|
106
|
+
injectModeReminder() {
|
|
107
|
+
this.messages = this.messages.filter((message) => !(message.role === "user"
|
|
108
|
+
&& message.isMeta
|
|
109
|
+
&& isPermissionModeReminder(message.content)));
|
|
110
|
+
this.injectSystemReminder(reminderForMode(this._mode));
|
|
111
|
+
}
|
|
100
112
|
get model() {
|
|
101
113
|
return this._model;
|
|
102
114
|
}
|
|
@@ -123,6 +135,7 @@ export class Agent {
|
|
|
123
135
|
model: options?.model ?? this.apiModel,
|
|
124
136
|
temperature: options?.temperature ?? this.temperature,
|
|
125
137
|
thinkingLevel: options?.thinkingLevel ?? this.thinkingLevel,
|
|
138
|
+
abortSignal: options?.abortSignal,
|
|
126
139
|
});
|
|
127
140
|
}
|
|
128
141
|
get thinking() {
|
|
@@ -148,7 +161,7 @@ export class Agent {
|
|
|
148
161
|
return;
|
|
149
162
|
this._mode = value;
|
|
150
163
|
this._modeVersion += 1;
|
|
151
|
-
this.
|
|
164
|
+
this.injectModeReminder();
|
|
152
165
|
this.onModeUpdate?.(value);
|
|
153
166
|
}
|
|
154
167
|
/** Internal: snapshot counter that bumps on every mode change. Used by run loop. */
|
|
@@ -175,7 +188,9 @@ export class Agent {
|
|
|
175
188
|
}
|
|
176
189
|
this.messages.unshift(systemMessage);
|
|
177
190
|
}
|
|
178
|
-
async *run(userInput, cwd) {
|
|
191
|
+
async *run(userInput, cwd, options = {}) {
|
|
192
|
+
const abortSignal = options.abortSignal;
|
|
193
|
+
throwIfAborted(abortSignal);
|
|
179
194
|
const hookBus = new HookBus();
|
|
180
195
|
for (const hooks of createDefaultHooks()) {
|
|
181
196
|
hookBus.register(hooks);
|
|
@@ -210,6 +225,7 @@ export class Agent {
|
|
|
210
225
|
let consecutiveOverflowRecoveries = 0;
|
|
211
226
|
let step = 0;
|
|
212
227
|
while (true) {
|
|
228
|
+
throwIfAborted(abortSignal);
|
|
213
229
|
flushGovernorReminders();
|
|
214
230
|
yield { type: "turn_start" };
|
|
215
231
|
step += 1;
|
|
@@ -253,6 +269,9 @@ export class Agent {
|
|
|
253
269
|
};
|
|
254
270
|
await hookBus.runBeforeModelCall(beforeModelCallCtx);
|
|
255
271
|
toolEntries = beforeModelCallCtx.toolEntries;
|
|
272
|
+
if (this._mode !== "plan") {
|
|
273
|
+
toolEntries = toolEntries.filter((t) => t.name !== "exit_plan_mode");
|
|
274
|
+
}
|
|
256
275
|
flushGovernorReminders();
|
|
257
276
|
const toolDefinitions = ((hookState.forceTextOnlyReason ? [] : toolEntries))
|
|
258
277
|
.map((t) => ({
|
|
@@ -273,8 +292,10 @@ export class Agent {
|
|
|
273
292
|
tools: toolDefinitions,
|
|
274
293
|
temperature: this.temperature,
|
|
275
294
|
thinkingLevel: this.thinkingLevel,
|
|
295
|
+
abortSignal,
|
|
276
296
|
});
|
|
277
297
|
for await (const chunk of stream) {
|
|
298
|
+
throwIfAborted(abortSignal);
|
|
278
299
|
switch (chunk.type) {
|
|
279
300
|
case "text":
|
|
280
301
|
assistantMsg.content += chunk.content;
|
|
@@ -346,6 +367,7 @@ export class Agent {
|
|
|
346
367
|
}
|
|
347
368
|
const executedResults = [];
|
|
348
369
|
for (let index = 0; index < parsedCalls.length; index++) {
|
|
370
|
+
throwIfAborted(abortSignal);
|
|
349
371
|
let tc = parsedCalls[index];
|
|
350
372
|
let blockedResult;
|
|
351
373
|
await hookBus.runBeforeToolCall({
|
|
@@ -373,7 +395,8 @@ export class Agent {
|
|
|
373
395
|
yield { type: "tool_start", id: tc.id, name: tc.name, args: tc.parsedArgs };
|
|
374
396
|
const todosVersionBefore = this._todosVersion;
|
|
375
397
|
const modeVersionBefore = this._modeVersion;
|
|
376
|
-
let result = blockedResult ?? await this.executeTool(tc, cwd);
|
|
398
|
+
let result = blockedResult ?? await this.executeTool(tc, cwd, abortSignal);
|
|
399
|
+
throwIfAborted(abortSignal);
|
|
377
400
|
await hookBus.runAfterToolCall({
|
|
378
401
|
agent: this,
|
|
379
402
|
cwd,
|
|
@@ -581,7 +604,14 @@ export class Agent {
|
|
|
581
604
|
this.messages.push(message);
|
|
582
605
|
this.onMessageAppend?.(message);
|
|
583
606
|
}
|
|
584
|
-
async executeTool(toolCall, cwd) {
|
|
607
|
+
async executeTool(toolCall, cwd, abortSignal) {
|
|
608
|
+
throwIfAborted(abortSignal);
|
|
609
|
+
if (toolCall.name === "exit_plan_mode" && this._mode !== "plan") {
|
|
610
|
+
return {
|
|
611
|
+
content: "Ignored exit_plan_mode because plan mode is not active. " +
|
|
612
|
+
"Continue with the user's request directly using the regular tools.",
|
|
613
|
+
};
|
|
614
|
+
}
|
|
585
615
|
const tool = this.tools.get(toolCall.name);
|
|
586
616
|
if (!tool) {
|
|
587
617
|
return {
|
|
@@ -608,6 +638,7 @@ export class Agent {
|
|
|
608
638
|
return await tool.execute(toolCall.parsedArgs, {
|
|
609
639
|
cwd,
|
|
610
640
|
sessionID: this.sessionID,
|
|
641
|
+
abortSignal,
|
|
611
642
|
toolCall: { id: toolCall.id, name: toolCall.name },
|
|
612
643
|
agent: this,
|
|
613
644
|
});
|
|
@@ -651,6 +682,14 @@ function estimateResidentChars(messages) {
|
|
|
651
682
|
}
|
|
652
683
|
return total;
|
|
653
684
|
}
|
|
685
|
+
function throwIfAborted(signal) {
|
|
686
|
+
if (!signal?.aborted)
|
|
687
|
+
return;
|
|
688
|
+
const reason = signal.reason;
|
|
689
|
+
if (reason instanceof Error)
|
|
690
|
+
throw reason;
|
|
691
|
+
throw new AgentAbortError(typeof reason === "string" ? reason : undefined);
|
|
692
|
+
}
|
|
654
693
|
function estimateToolPayloadChars(messages) {
|
|
655
694
|
return messages.reduce((sum, message) => {
|
|
656
695
|
if (message.role !== "tool") {
|
|
@@ -28,12 +28,12 @@ export interface ApprovalControllerOptions {
|
|
|
28
28
|
* Default ApprovalController. Decision tree:
|
|
29
29
|
*
|
|
30
30
|
* deny rule match → reject (applies even under bypassPermissions)
|
|
31
|
-
* bypassPermissions
|
|
32
|
-
*
|
|
31
|
+
* bypassPermissions → auto-approve, no prompt
|
|
32
|
+
* default + edit|write → auto-approve
|
|
33
33
|
* plan → reject with instructions to use exit_plan_mode
|
|
34
34
|
* allow rule match → auto-approve
|
|
35
35
|
* bash in session allowlist → auto-approve
|
|
36
|
-
*
|
|
36
|
+
* bash / other → delegate to UI; if no UI, reject
|
|
37
37
|
*
|
|
38
38
|
* Deny rules sit at the top as a hard ceiling: bypassPermissions is a trust
|
|
39
39
|
* escalation, not a policy override. Users who want to permit a currently-
|
|
@@ -3,12 +3,12 @@ import { checkPermission } from "../permissions/rule.js";
|
|
|
3
3
|
* Default ApprovalController. Decision tree:
|
|
4
4
|
*
|
|
5
5
|
* deny rule match → reject (applies even under bypassPermissions)
|
|
6
|
-
* bypassPermissions
|
|
7
|
-
*
|
|
6
|
+
* bypassPermissions → auto-approve, no prompt
|
|
7
|
+
* default + edit|write → auto-approve
|
|
8
8
|
* plan → reject with instructions to use exit_plan_mode
|
|
9
9
|
* allow rule match → auto-approve
|
|
10
10
|
* bash in session allowlist → auto-approve
|
|
11
|
-
*
|
|
11
|
+
* bash / other → delegate to UI; if no UI, reject
|
|
12
12
|
*
|
|
13
13
|
* Deny rules sit at the top as a hard ceiling: bypassPermissions is a trust
|
|
14
14
|
* escalation, not a policy override. Users who want to permit a currently-
|
|
@@ -35,10 +35,10 @@ export class PermissionAwareApprovalController {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
const mode = this.options.getMode();
|
|
38
|
-
if (mode === "bypassPermissions"
|
|
38
|
+
if (mode === "bypassPermissions") {
|
|
39
39
|
return { action: "approve" };
|
|
40
40
|
}
|
|
41
|
-
if (mode === "
|
|
41
|
+
if (mode === "default" && (req.type === "edit" || req.type === "write")) {
|
|
42
42
|
return { action: "approve" };
|
|
43
43
|
}
|
|
44
44
|
if (mode === "plan") {
|
package/dist/cli.d.ts
CHANGED
|
@@ -12,8 +12,6 @@ export interface CliArgs {
|
|
|
12
12
|
prompt?: string;
|
|
13
13
|
thinkingLevel?: ThinkingLevel;
|
|
14
14
|
mode?: PermissionMode;
|
|
15
|
-
/** When true, --dangerously-skip-permissions was passed; bypassPermissions is reachable via the mode cycle and auto-approves every tool. */
|
|
16
|
-
bypassEnabled?: boolean;
|
|
17
15
|
}
|
|
18
16
|
export declare function parseArgs(argv: string[]): CliArgs;
|
|
19
17
|
export declare function printHelp(): void;
|
package/dist/cli.js
CHANGED
|
@@ -45,10 +45,10 @@ export function parseArgs(argv) {
|
|
|
45
45
|
args.mode = "plan";
|
|
46
46
|
break;
|
|
47
47
|
case "--accept-edits":
|
|
48
|
-
|
|
48
|
+
// Backward-compatible no-op: Build mode now includes edit/write auto-approval.
|
|
49
|
+
args.mode = "default";
|
|
49
50
|
break;
|
|
50
51
|
case "--dangerously-skip-permissions":
|
|
51
|
-
args.bypassEnabled = true;
|
|
52
52
|
args.mode = "bypassPermissions";
|
|
53
53
|
break;
|
|
54
54
|
default:
|
|
@@ -73,7 +73,6 @@ Options:
|
|
|
73
73
|
--reasoning Enable reasoning mode at medium effort
|
|
74
74
|
--reasoning-effort <l> Set reasoning effort: off|minimal|low|medium|high|xhigh|max
|
|
75
75
|
--plan Start in plan mode (read-only investigation; propose before executing)
|
|
76
|
-
--accept-edits Start with edits/writes auto-approved (bash still prompts)
|
|
77
76
|
--dangerously-skip-permissions
|
|
78
77
|
Enable bypass mode (auto-approve EVERY tool; disables all safety prompts)
|
|
79
78
|
-p, --print Non-interactive mode (single prompt)
|
package/dist/main.js
CHANGED
|
@@ -284,8 +284,7 @@ async function main() {
|
|
|
284
284
|
// Reassigning agent.messages drops any <system-reminder> we injected during
|
|
285
285
|
// construction. Re-inject if the agent is starting in plan mode.
|
|
286
286
|
if (agent.mode === "plan") {
|
|
287
|
-
|
|
288
|
-
agent.injectSystemReminder(PLAN_MODE_ENTER_REMINDER);
|
|
287
|
+
agent.injectModeReminder();
|
|
289
288
|
}
|
|
290
289
|
console.log(chalk.dim(`Resumed session: ${sessionManager.getSessionFile()}`));
|
|
291
290
|
}
|
|
@@ -326,7 +325,6 @@ async function main() {
|
|
|
326
325
|
settingsManager,
|
|
327
326
|
lspService,
|
|
328
327
|
mcpManager,
|
|
329
|
-
bypassEnabled: args.bypassEnabled,
|
|
330
328
|
theme: userConfig.getTheme(),
|
|
331
329
|
flushMemory,
|
|
332
330
|
runMemoryCompaction,
|
|
@@ -13,11 +13,7 @@ export interface PermissionModeInfo {
|
|
|
13
13
|
}
|
|
14
14
|
export declare const PERMISSION_MODE_INFO: Record<PermissionMode, PermissionModeInfo>;
|
|
15
15
|
/**
|
|
16
|
-
* Cycle order for the interactive mode keybind.
|
|
17
|
-
*
|
|
18
|
-
* Permission presets like acceptEdits/bypassPermissions remain opt-in through
|
|
19
|
-
* flags or commands and are never reached accidentally from Tab.
|
|
16
|
+
* Cycle order for the interactive mode keybind. The visible TUI loop keeps the
|
|
17
|
+
* mental model simple: Build -> Plan -> Bypass -> Build.
|
|
20
18
|
*/
|
|
21
|
-
export declare function getNextPermissionMode(current: PermissionMode
|
|
22
|
-
bypassEnabled?: boolean;
|
|
23
|
-
}): PermissionMode;
|
|
19
|
+
export declare function getNextPermissionMode(current: PermissionMode): PermissionMode;
|
package/dist/permission/mode.js
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
export const PERMISSION_MODE_INFO = {
|
|
2
2
|
default: { title: "Default", shortTitle: "default", symbol: "", color: "muted" },
|
|
3
|
-
acceptEdits: { title: "Accept edits", shortTitle: "accept edits", symbol: "⏵⏵", color: "success" },
|
|
4
3
|
plan: { title: "Plan", shortTitle: "plan", symbol: "⏸", color: "accent" },
|
|
5
4
|
bypassPermissions: { title: "Bypass permissions", shortTitle: "bypass", symbol: "⏵⏵", color: "error" },
|
|
6
|
-
dontAsk: { title: "Do not ask", shortTitle: "silent", symbol: "·", color: "warning" },
|
|
7
5
|
};
|
|
8
6
|
/**
|
|
9
|
-
* Cycle order for the interactive mode keybind.
|
|
10
|
-
*
|
|
11
|
-
* Permission presets like acceptEdits/bypassPermissions remain opt-in through
|
|
12
|
-
* flags or commands and are never reached accidentally from Tab.
|
|
7
|
+
* Cycle order for the interactive mode keybind. The visible TUI loop keeps the
|
|
8
|
+
* mental model simple: Build -> Plan -> Bypass -> Build.
|
|
13
9
|
*/
|
|
14
|
-
export function getNextPermissionMode(current
|
|
10
|
+
export function getNextPermissionMode(current) {
|
|
11
|
+
if (current === "default")
|
|
12
|
+
return "plan";
|
|
15
13
|
if (current === "plan")
|
|
16
|
-
return "
|
|
17
|
-
|
|
18
|
-
return "default";
|
|
19
|
-
return "plan";
|
|
14
|
+
return "bypassPermissions";
|
|
15
|
+
return "default";
|
|
20
16
|
}
|
|
@@ -21,10 +21,8 @@ import { normalizeLspConfig } from "../lsp/config.js";
|
|
|
21
21
|
import { parseRules } from "./rule.js";
|
|
22
22
|
const KNOWN_MODES = new Set([
|
|
23
23
|
"default",
|
|
24
|
-
"acceptEdits",
|
|
25
24
|
"plan",
|
|
26
25
|
"bypassPermissions",
|
|
27
|
-
"dontAsk",
|
|
28
26
|
]);
|
|
29
27
|
export class SettingsManager {
|
|
30
28
|
cwd;
|
|
@@ -83,8 +81,9 @@ export class SettingsManager {
|
|
|
83
81
|
continue;
|
|
84
82
|
const perms = data.permissions;
|
|
85
83
|
if (typeof perms.defaultMode === "string") {
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
const rawMode = perms.defaultMode === "acceptEdits" ? "default" : perms.defaultMode;
|
|
85
|
+
if (KNOWN_MODES.has(rawMode)) {
|
|
86
|
+
defaultMode = rawMode;
|
|
88
87
|
}
|
|
89
88
|
else {
|
|
90
89
|
diagnostics.push({
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { PermissionMode } from "../types.js";
|
|
10
10
|
export declare function wrapInSystemReminder(content: string): string;
|
|
11
|
+
export declare function isPermissionModeReminder(content: unknown): boolean;
|
|
11
12
|
/** Picks the correct reminder text for a transition TO a given mode. */
|
|
12
13
|
export declare function reminderForMode(mode: PermissionMode): string;
|
|
13
14
|
export declare const PLAN_MODE_ENTER_REMINDER: string;
|
package/dist/prompt/reminders.js
CHANGED
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
export function wrapInSystemReminder(content) {
|
|
10
10
|
return `<system-reminder>\n${content.trim()}\n</system-reminder>`;
|
|
11
11
|
}
|
|
12
|
+
export function isPermissionModeReminder(content) {
|
|
13
|
+
if (typeof content !== "string")
|
|
14
|
+
return false;
|
|
15
|
+
return content.includes("Plan mode is now ACTIVE")
|
|
16
|
+
|| content.includes("Permission mode is now: bypassPermissions")
|
|
17
|
+
|| content.includes("Permission mode is now: default Build mode");
|
|
18
|
+
}
|
|
12
19
|
const PLAN_MODE_ENTER = `
|
|
13
20
|
Plan mode is now ACTIVE.
|
|
14
21
|
|
|
@@ -21,12 +28,6 @@ Rules while in plan mode:
|
|
|
21
28
|
- The user will approve, edit, or reject your plan. On approval the harness switches back to default mode and you may execute.
|
|
22
29
|
- On rejection, remain in plan mode and iterate.
|
|
23
30
|
`;
|
|
24
|
-
const ACCEPT_EDITS_ENTER = `
|
|
25
|
-
Permission mode is now: acceptEdits.
|
|
26
|
-
|
|
27
|
-
The user has granted blanket approval for file edits and writes in this session.
|
|
28
|
-
Bash commands still require explicit approval. Other tool safety rules are unchanged.
|
|
29
|
-
`;
|
|
30
31
|
const BYPASS_ENTER = `
|
|
31
32
|
Permission mode is now: bypassPermissions.
|
|
32
33
|
|
|
@@ -34,25 +35,18 @@ ALL tool calls auto-approve with no user confirmation. The user has explicitly o
|
|
|
34
35
|
Proceed with extra care — explain risky actions in the chat BEFORE performing them, and
|
|
35
36
|
prefer reversible operations when possible.
|
|
36
37
|
`;
|
|
37
|
-
const DONT_ASK_ENTER = `
|
|
38
|
-
Permission mode is now: dontAsk.
|
|
39
|
-
|
|
40
|
-
All tool calls auto-approve silently. Minimise narration; execute and report results tersely.
|
|
41
|
-
`;
|
|
42
38
|
const DEFAULT_ENTER = `
|
|
43
|
-
Permission mode is now: default
|
|
39
|
+
Permission mode is now: default Build mode.
|
|
40
|
+
|
|
41
|
+
File edits and writes auto-approve. Bash commands and other destructive tools still require explicit approval unless allowed by rules.
|
|
44
42
|
`;
|
|
45
43
|
/** Picks the correct reminder text for a transition TO a given mode. */
|
|
46
44
|
export function reminderForMode(mode) {
|
|
47
45
|
switch (mode) {
|
|
48
46
|
case "plan":
|
|
49
47
|
return wrapInSystemReminder(PLAN_MODE_ENTER);
|
|
50
|
-
case "acceptEdits":
|
|
51
|
-
return wrapInSystemReminder(ACCEPT_EDITS_ENTER);
|
|
52
48
|
case "bypassPermissions":
|
|
53
49
|
return wrapInSystemReminder(BYPASS_ENTER);
|
|
54
|
-
case "dontAsk":
|
|
55
|
-
return wrapInSystemReminder(DONT_ASK_ENTER);
|
|
56
50
|
case "default":
|
|
57
51
|
default:
|
|
58
52
|
return wrapInSystemReminder(DEFAULT_ENTER);
|
|
@@ -44,6 +44,7 @@ export function createOpenAICodexProvider(options) {
|
|
|
44
44
|
const response = await fetch(resolveCodexUrl(options.baseURL), {
|
|
45
45
|
method: "POST",
|
|
46
46
|
headers: buildSseHeaders(options.apiKey, accountId, sessionId),
|
|
47
|
+
signal: chatOptions.abortSignal,
|
|
47
48
|
body: JSON.stringify(buildRequestBody(messages, {
|
|
48
49
|
model: chatOptions.model,
|
|
49
50
|
tools: chatOptions.tools,
|
|
@@ -181,6 +182,7 @@ export function createOpenAICodexProvider(options) {
|
|
|
181
182
|
model: chatOptions?.model ?? "gpt-5.4",
|
|
182
183
|
temperature: chatOptions?.temperature,
|
|
183
184
|
thinkingLevel: chatOptions?.thinkingLevel,
|
|
185
|
+
abortSignal: chatOptions?.abortSignal,
|
|
184
186
|
})) {
|
|
185
187
|
if (chunk.type === "text") {
|
|
186
188
|
content += chunk.content;
|
package/dist/provider.js
CHANGED
|
@@ -87,7 +87,9 @@ export function createProviderInstance(options) {
|
|
|
87
87
|
if (requestConfig.reasoningEffort && requestConfig.reasoningEffort !== "off") {
|
|
88
88
|
body.reasoning = { enabled: true };
|
|
89
89
|
}
|
|
90
|
-
const stream = (await client.chat.completions.create(body
|
|
90
|
+
const stream = (await client.chat.completions.create(body, {
|
|
91
|
+
signal: chatOptions.abortSignal,
|
|
92
|
+
}));
|
|
91
93
|
yield* translateOpenAIStream(stream);
|
|
92
94
|
yield { type: "done" };
|
|
93
95
|
}
|
|
@@ -108,7 +110,9 @@ export function createProviderInstance(options) {
|
|
|
108
110
|
if (requestConfig.reasoningEffort && requestConfig.reasoningEffort !== "off") {
|
|
109
111
|
body.reasoning = { enabled: true };
|
|
110
112
|
}
|
|
111
|
-
const response = await client.chat.completions.create(body
|
|
113
|
+
const response = await client.chat.completions.create(body, {
|
|
114
|
+
signal: chatOptions?.abortSignal,
|
|
115
|
+
});
|
|
112
116
|
return response.choices[0]?.message?.content ?? "";
|
|
113
117
|
}
|
|
114
118
|
return { streamChat, complete };
|
|
@@ -227,30 +227,9 @@ function parseKeyArgs(args, ctx) {
|
|
|
227
227
|
const builtinSlashCommandEntries = [
|
|
228
228
|
{
|
|
229
229
|
name: "skills",
|
|
230
|
-
description: "
|
|
230
|
+
description: "Open the searchable skills picker",
|
|
231
231
|
async handler(args, ctx) {
|
|
232
|
-
|
|
233
|
-
const diagnostics = ctx.skillRegistry.getDiagnostics();
|
|
234
|
-
const lines = [];
|
|
235
|
-
if (skills.length === 0) {
|
|
236
|
-
lines.push("No skills available.");
|
|
237
|
-
}
|
|
238
|
-
else {
|
|
239
|
-
lines.push("Available skills:");
|
|
240
|
-
for (const skill of skills) {
|
|
241
|
-
const tagSuffix = skill.tags && skill.tags.length > 0 ? ` [tags: ${skill.tags.join(", ")}]` : "";
|
|
242
|
-
lines.push(`- ${skill.name}: ${skill.description}${tagSuffix}`);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
if (diagnostics.length > 0) {
|
|
246
|
-
lines.push("", "Skill diagnostics:");
|
|
247
|
-
for (const diagnostic of diagnostics) {
|
|
248
|
-
const prefix = diagnostic.level === "error" ? "ERROR" : "WARN";
|
|
249
|
-
const target = diagnostic.skillName ?? diagnostic.filePath ?? "skills";
|
|
250
|
-
lines.push(`- ${prefix} ${target}: ${diagnostic.message}`);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
return lines.join("\n");
|
|
232
|
+
ctx.openPicker("skill");
|
|
254
233
|
},
|
|
255
234
|
},
|
|
256
235
|
{
|
|
@@ -16,7 +16,7 @@ export interface SlashCommandContext {
|
|
|
16
16
|
exit: () => void;
|
|
17
17
|
sessionManager?: SessionManager;
|
|
18
18
|
createProvider: (providerId: string, apiKey: string, baseURL: string) => Provider;
|
|
19
|
-
openPicker: (mode: "model" | "key" | "provider" | "provider-add" | "login" | "logout", providerId?: string) => void;
|
|
19
|
+
openPicker: (mode: "model" | "key" | "provider" | "provider-add" | "login" | "logout" | "skill", providerId?: string) => void;
|
|
20
20
|
registry: ProviderRegistry;
|
|
21
21
|
skillRegistry: SkillRegistry;
|
|
22
22
|
bashAllowlist?: BashAllowlist;
|
package/dist/tools/bash.js
CHANGED
|
@@ -20,7 +20,7 @@ export function createBashTool(cwd, approval) {
|
|
|
20
20
|
},
|
|
21
21
|
required: ["command"],
|
|
22
22
|
},
|
|
23
|
-
async execute(args) {
|
|
23
|
+
async execute(args, ctx) {
|
|
24
24
|
if (!existsSync(cwd)) {
|
|
25
25
|
return { content: `Error: Working directory does not exist: ${cwd}`, isError: true };
|
|
26
26
|
}
|
|
@@ -52,11 +52,21 @@ export function createBashTool(cwd, approval) {
|
|
|
52
52
|
let stdout = "";
|
|
53
53
|
let stderr = "";
|
|
54
54
|
let timedOut = false;
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
let aborted = false;
|
|
56
|
+
const abortChild = () => {
|
|
57
|
+
if (aborted)
|
|
58
|
+
return;
|
|
59
|
+
aborted = true;
|
|
57
60
|
child.kill("SIGTERM");
|
|
58
61
|
setTimeout(() => child.kill("SIGKILL"), 5000);
|
|
62
|
+
};
|
|
63
|
+
const timeoutHandle = setTimeout(() => {
|
|
64
|
+
timedOut = true;
|
|
65
|
+
abortChild();
|
|
59
66
|
}, timeoutSec * 1000);
|
|
67
|
+
if (ctx.abortSignal?.aborted)
|
|
68
|
+
abortChild();
|
|
69
|
+
ctx.abortSignal?.addEventListener("abort", abortChild, { once: true });
|
|
60
70
|
child.stdout?.on("data", (data) => {
|
|
61
71
|
stdout += data.toString();
|
|
62
72
|
});
|
|
@@ -65,10 +75,12 @@ export function createBashTool(cwd, approval) {
|
|
|
65
75
|
});
|
|
66
76
|
child.on("error", (err) => {
|
|
67
77
|
clearTimeout(timeoutHandle);
|
|
78
|
+
ctx.abortSignal?.removeEventListener("abort", abortChild);
|
|
68
79
|
resolve({ content: `Error: ${err.message}`, isError: true });
|
|
69
80
|
});
|
|
70
81
|
child.on("close", (code) => {
|
|
71
82
|
clearTimeout(timeoutHandle);
|
|
83
|
+
ctx.abortSignal?.removeEventListener("abort", abortChild);
|
|
72
84
|
let output = "";
|
|
73
85
|
if (stdout)
|
|
74
86
|
output += `stdout:\n${stdout}\n`;
|
|
@@ -90,6 +102,21 @@ export function createBashTool(cwd, approval) {
|
|
|
90
102
|
});
|
|
91
103
|
return;
|
|
92
104
|
}
|
|
105
|
+
if (aborted || ctx.abortSignal?.aborted) {
|
|
106
|
+
output += "[Command cancelled]";
|
|
107
|
+
resolve({
|
|
108
|
+
content: output.trim(),
|
|
109
|
+
isError: true,
|
|
110
|
+
status: "blocked",
|
|
111
|
+
metadata: {
|
|
112
|
+
kind: parsedSearch ? "search" : "shell",
|
|
113
|
+
pattern: parsedSearch?.pattern,
|
|
114
|
+
path: parsedSearch?.path,
|
|
115
|
+
reason: "cancelled",
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
93
120
|
if (Buffer.byteLength(output, "utf-8") > MAX_OUTPUT) {
|
|
94
121
|
output = Buffer.from(output, "utf-8").subarray(0, MAX_OUTPUT).toString("utf-8");
|
|
95
122
|
output += "\n[Output truncated]";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function copyTextToClipboard(text: string): Promise<void>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export async function copyTextToClipboard(text) {
|
|
3
|
+
if (process.platform === "darwin") {
|
|
4
|
+
await writeToProcess("pbcopy", [], text);
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
if (process.platform === "win32") {
|
|
8
|
+
await writeToProcess("powershell", [
|
|
9
|
+
"-NoProfile",
|
|
10
|
+
"-Command",
|
|
11
|
+
"Set-Clipboard -Value ([Console]::In.ReadToEnd())",
|
|
12
|
+
], text);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const candidates = [
|
|
16
|
+
["wl-copy", []],
|
|
17
|
+
["xclip", ["-selection", "clipboard"]],
|
|
18
|
+
["xsel", ["--clipboard", "--input"]],
|
|
19
|
+
];
|
|
20
|
+
let lastError;
|
|
21
|
+
for (const [command, args] of candidates) {
|
|
22
|
+
try {
|
|
23
|
+
await writeToProcess(command, args, text);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
lastError = error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
throw lastError instanceof Error ? lastError : new Error("No clipboard command available");
|
|
31
|
+
}
|
|
32
|
+
function writeToProcess(command, args, input) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const child = spawn(command, args, {
|
|
35
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
36
|
+
windowsHide: true,
|
|
37
|
+
});
|
|
38
|
+
let stderr = "";
|
|
39
|
+
child.stderr.setEncoding("utf8");
|
|
40
|
+
child.stderr.on("data", (chunk) => {
|
|
41
|
+
stderr += chunk;
|
|
42
|
+
});
|
|
43
|
+
child.on("error", reject);
|
|
44
|
+
child.on("close", (code) => {
|
|
45
|
+
if (code === 0) {
|
|
46
|
+
resolve();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
reject(new Error(stderr.trim() || `${command} exited with code ${code}`));
|
|
50
|
+
});
|
|
51
|
+
child.stdin.end(input);
|
|
52
|
+
});
|
|
53
|
+
}
|