@bubblebrain-ai/bubble 0.0.8 → 0.0.9
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/categories.d.ts +34 -0
- package/dist/agent/categories.js +98 -0
- package/dist/agent/profiles.d.ts +4 -0
- package/dist/agent/profiles.js +2 -3
- package/dist/agent/subagent-control.d.ts +5 -0
- package/dist/agent/subagent-control.js +4 -0
- package/dist/agent/subagent-lifecycle-reminder.d.ts +3 -0
- package/dist/agent/subagent-lifecycle-reminder.js +102 -0
- package/dist/agent/subagent-route-format.d.ts +8 -0
- package/dist/agent/subagent-route-format.js +18 -0
- package/dist/agent/subtask-policy.d.ts +0 -1
- package/dist/agent/subtask-policy.js +0 -4
- package/dist/agent.d.ts +12 -0
- package/dist/agent.js +152 -13
- package/dist/config.d.ts +23 -3
- package/dist/config.js +59 -6
- package/dist/context/budget.d.ts +3 -3
- package/dist/context/budget.js +29 -15
- package/dist/context/compact.d.ts +23 -0
- package/dist/context/compact.js +129 -0
- package/dist/context/llm-compactor.d.ts +19 -0
- package/dist/context/llm-compactor.js +200 -0
- package/dist/context/projector.js +28 -12
- package/dist/context/token-estimator.d.ts +14 -0
- package/dist/context/token-estimator.js +106 -0
- package/dist/context/tool-output-truncate.d.ts +8 -0
- package/dist/context/tool-output-truncate.js +59 -0
- package/dist/context/usage.js +9 -9
- package/dist/main.js +43 -6
- package/dist/model-catalog.d.ts +9 -0
- package/dist/model-catalog.js +16 -0
- package/dist/orchestrator/default-hooks.js +18 -0
- package/dist/provider-openai-codex.d.ts +13 -2
- package/dist/provider-openai-codex.js +81 -32
- package/dist/provider-registry.js +20 -4
- package/dist/slash-commands/commands.js +24 -0
- package/dist/slash-commands/types.d.ts +7 -0
- package/dist/tools/agent-lifecycle.js +22 -4
- package/dist/tools/edit.js +2 -2
- package/dist/tools/glob.js +2 -1
- package/dist/tools/grep.js +2 -2
- package/dist/tools/lsp.js +2 -2
- package/dist/tools/path-utils.d.ts +2 -0
- package/dist/tools/path-utils.js +16 -0
- package/dist/tools/read.js +117 -5
- package/dist/tools/write.js +3 -2
- package/dist/tui-ink/app.d.ts +11 -2
- package/dist/tui-ink/app.js +191 -78
- package/dist/tui-ink/approval/approval-dialog.js +4 -1
- package/dist/tui-ink/approval/diff-view.js +2 -1
- package/dist/tui-ink/approval/select.js +2 -1
- package/dist/tui-ink/code-highlight.d.ts +2 -0
- package/dist/tui-ink/code-highlight.js +30 -2
- package/dist/tui-ink/detect-theme.d.ts +19 -0
- package/dist/tui-ink/detect-theme.js +123 -0
- package/dist/tui-ink/footer.js +4 -3
- package/dist/tui-ink/input-box.js +83 -26
- package/dist/tui-ink/input-history.d.ts +16 -0
- package/dist/tui-ink/input-history.js +81 -0
- package/dist/tui-ink/markdown.js +30 -20
- package/dist/tui-ink/message-list.js +112 -16
- package/dist/tui-ink/model-picker.js +6 -1
- package/dist/tui-ink/plan-confirm.js +2 -1
- package/dist/tui-ink/question-dialog.js +2 -1
- package/dist/tui-ink/run.d.ts +5 -1
- package/dist/tui-ink/run.js +30 -2
- package/dist/tui-ink/theme.d.ts +64 -35
- package/dist/tui-ink/theme.js +81 -8
- package/dist/tui-ink/todos.js +5 -3
- package/dist/tui-ink/trace-groups.d.ts +3 -1
- package/dist/tui-ink/trace-groups.js +93 -14
- package/dist/tui-ink/welcome.js +23 -4
- package/dist/types.d.ts +6 -0
- package/package.json +2 -1
package/dist/main.js
CHANGED
|
@@ -56,6 +56,20 @@ async function main() {
|
|
|
56
56
|
})
|
|
57
57
|
: createUnavailableProvider(unavailableProviderMessage);
|
|
58
58
|
const createProvider = (providerId, apiKey, baseURL) => createProviderInstance({ providerId, apiKey, baseURL, thinkingLevel: args.thinkingLevel });
|
|
59
|
+
const createProviderForRoute = async (route) => {
|
|
60
|
+
const providerId = route.providerId;
|
|
61
|
+
if (!providerId) {
|
|
62
|
+
throw new Error(`Subagent route for model "${route.model}" did not include a provider.`);
|
|
63
|
+
}
|
|
64
|
+
if (registry.supportsOAuth(providerId) && registry.getAuthStorage().has(providerId)) {
|
|
65
|
+
await registry.prepareProvider(providerId);
|
|
66
|
+
}
|
|
67
|
+
const target = registry.getConfigured().find((item) => item.id === providerId);
|
|
68
|
+
if (!target?.enabled || !target.apiKey) {
|
|
69
|
+
throw new Error(`Subagent route requires provider "${providerId}", but it is not configured or has no active credentials.`);
|
|
70
|
+
}
|
|
71
|
+
return createProvider(providerId, target.apiKey, target.baseURL);
|
|
72
|
+
};
|
|
59
73
|
let agentRef;
|
|
60
74
|
const todoStore = {
|
|
61
75
|
getTodos: () => agentRef?.getTodos() ?? [],
|
|
@@ -250,6 +264,8 @@ async function main() {
|
|
|
250
264
|
skills: skillSummaries,
|
|
251
265
|
memoryPrompt,
|
|
252
266
|
fileStateTracker,
|
|
267
|
+
agentCategories: userConfig.getAgentCategories(),
|
|
268
|
+
providerFactory: createProviderForRoute,
|
|
253
269
|
});
|
|
254
270
|
agentRef = agent;
|
|
255
271
|
if (sessionManager) {
|
|
@@ -335,10 +351,8 @@ async function main() {
|
|
|
335
351
|
return;
|
|
336
352
|
}
|
|
337
353
|
const tuiRuntime = process.env.BUBBLE_TUI === "opentui" ? "opentui" : "ink";
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
: await import("./tui-ink/run.js");
|
|
341
|
-
await runTui(agent, args, {
|
|
354
|
+
const themeConfig = userConfig.getTheme();
|
|
355
|
+
const commonOptions = {
|
|
342
356
|
sessionManager,
|
|
343
357
|
createProvider,
|
|
344
358
|
registry,
|
|
@@ -350,12 +364,35 @@ async function main() {
|
|
|
350
364
|
settingsManager,
|
|
351
365
|
lspService,
|
|
352
366
|
mcpManager,
|
|
353
|
-
theme: userConfig.getTheme(),
|
|
354
367
|
flushMemory,
|
|
355
368
|
runMemoryCompaction,
|
|
356
369
|
runMemorySummary,
|
|
357
370
|
runMemoryRefresh,
|
|
358
|
-
}
|
|
371
|
+
};
|
|
372
|
+
if (tuiRuntime === "opentui") {
|
|
373
|
+
const { runTui } = await import("./tui/run.js");
|
|
374
|
+
await runTui(agent, args, {
|
|
375
|
+
...commonOptions,
|
|
376
|
+
theme: themeConfig.overrides,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
// Probe the terminal background BEFORE Ink takes over stdin. OSC 11
|
|
381
|
+
// needs raw mode, and once Ink owns stdin the reply never reaches us.
|
|
382
|
+
let detectedTheme = "dark";
|
|
383
|
+
if (themeConfig.mode === "auto") {
|
|
384
|
+
const { detectTerminalTheme } = await import("./tui-ink/detect-theme.js");
|
|
385
|
+
detectedTheme = await detectTerminalTheme();
|
|
386
|
+
}
|
|
387
|
+
const { runTui } = await import("./tui-ink/run.js");
|
|
388
|
+
await runTui(agent, args, {
|
|
389
|
+
...commonOptions,
|
|
390
|
+
themeMode: themeConfig.mode,
|
|
391
|
+
themeOverrides: themeConfig.overrides,
|
|
392
|
+
detectedTheme,
|
|
393
|
+
onThemeModeChange: (mode) => userConfig.setThemeMode(mode),
|
|
394
|
+
});
|
|
395
|
+
}
|
|
359
396
|
}
|
|
360
397
|
finally {
|
|
361
398
|
await shutdownRuntime();
|
package/dist/model-catalog.d.ts
CHANGED
|
@@ -11,10 +11,19 @@ export interface BuiltinModelDefinition {
|
|
|
11
11
|
providerId: string;
|
|
12
12
|
reasoningLevels: ReasoningEffort[];
|
|
13
13
|
contextWindow?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Server-declared cap on per-tool-output tokens. When set, the agent must
|
|
16
|
+
* truncate each tool result to this token budget before adding it to history
|
|
17
|
+
* — otherwise the server's input window is exceeded by raw tool dumps.
|
|
18
|
+
* (For codex models this comes from the API's `truncation_policy.limit`.)
|
|
19
|
+
*/
|
|
20
|
+
toolOutputTokenLimit?: number;
|
|
14
21
|
}
|
|
15
22
|
export declare const BUILTIN_PROVIDERS: BuiltinProviderDefinition[];
|
|
16
23
|
export declare const BUILTIN_MODELS: BuiltinModelDefinition[];
|
|
17
24
|
export declare function listBuiltinModels(providerId: string): BuiltinModelDefinition[];
|
|
25
|
+
export declare function registerDynamicModelMetadata(model: BuiltinModelDefinition): void;
|
|
18
26
|
export declare function getBuiltinModel(providerId: string, modelId: string): BuiltinModelDefinition | undefined;
|
|
19
27
|
export declare function getBuiltinProvider(providerId: string): BuiltinProviderDefinition | undefined;
|
|
20
28
|
export declare function getModelContextWindow(providerId: string, modelId: string): number | undefined;
|
|
29
|
+
export declare function getToolOutputTokenLimit(providerId: string, modelId: string): number | undefined;
|
package/dist/model-catalog.js
CHANGED
|
@@ -85,7 +85,20 @@ export const BUILTIN_MODELS = [
|
|
|
85
85
|
export function listBuiltinModels(providerId) {
|
|
86
86
|
return BUILTIN_MODELS.filter((model) => model.providerId === providerId);
|
|
87
87
|
}
|
|
88
|
+
// Runtime overlay populated from provider-side discovery (e.g. ChatGPT codex /models).
|
|
89
|
+
// Looked up before the static catalog so newly-released models work without a code change.
|
|
90
|
+
const dynamicOverlay = new Map();
|
|
91
|
+
function overlayKey(providerId, modelId) {
|
|
92
|
+
return `${providerId}:${modelId}`;
|
|
93
|
+
}
|
|
94
|
+
export function registerDynamicModelMetadata(model) {
|
|
95
|
+
dynamicOverlay.set(overlayKey(model.providerId, model.id), model);
|
|
96
|
+
}
|
|
88
97
|
export function getBuiltinModel(providerId, modelId) {
|
|
98
|
+
const overlayHit = dynamicOverlay.get(overlayKey(providerId, modelId))
|
|
99
|
+
|| (providerId === "openai" ? dynamicOverlay.get(overlayKey("openai-codex", modelId)) : undefined);
|
|
100
|
+
if (overlayHit)
|
|
101
|
+
return overlayHit;
|
|
89
102
|
return BUILTIN_MODELS.find((model) => model.providerId === providerId && model.id === modelId)
|
|
90
103
|
|| (providerId === "openai"
|
|
91
104
|
? BUILTIN_MODELS.find((model) => model.providerId === "openai-codex" && model.id === modelId)
|
|
@@ -97,3 +110,6 @@ export function getBuiltinProvider(providerId) {
|
|
|
97
110
|
export function getModelContextWindow(providerId, modelId) {
|
|
98
111
|
return getBuiltinModel(providerId, modelId)?.contextWindow;
|
|
99
112
|
}
|
|
113
|
+
export function getToolOutputTokenLimit(providerId, modelId) {
|
|
114
|
+
return getBuiltinModel(providerId, modelId)?.toolOutputTokenLimit;
|
|
115
|
+
}
|
|
@@ -6,6 +6,7 @@ import { arbitrateToolCall } from "../agent/tool-arbiter.js";
|
|
|
6
6
|
import { buildEditRetryEscalationReminder, buildSmallTaskHint, buildTaskSummaryReminder, buildWorkflowPhaseReminder, } from "../prompt/reminders.js";
|
|
7
7
|
import { reminderForTaskType } from "../prompt/task-reminders.js";
|
|
8
8
|
import { formatCoverageSummary, resolveWorkflowPhase } from "./workflow.js";
|
|
9
|
+
import { buildSubagentLifecycleReminder } from "../agent/subagent-lifecycle-reminder.js";
|
|
9
10
|
export function createDefaultHooks() {
|
|
10
11
|
return [
|
|
11
12
|
{
|
|
@@ -127,6 +128,12 @@ export function createDefaultHooks() {
|
|
|
127
128
|
}
|
|
128
129
|
},
|
|
129
130
|
beforeContinuation(ctx) {
|
|
131
|
+
if (hasSubagentLifecycleActivity(ctx.toolCalls, ctx.toolResults)) {
|
|
132
|
+
const reminder = buildSubagentLifecycleReminder(ctx.agent.listSubAgents(), ctx.toolResults);
|
|
133
|
+
if (reminder) {
|
|
134
|
+
ctx.queueReminder(reminder);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
130
137
|
if (ctx.state.taskType === "security_investigation" && ctx.state.evidenceTracker?.isCoreCoverageComplete()) {
|
|
131
138
|
ctx.requestTextOnlyTurn("Core security investigation evidence has been collected. Summarize the findings instead of continuing with more tool calls.");
|
|
132
139
|
return;
|
|
@@ -149,6 +156,17 @@ function isCodeWriteResult(_toolCall, result) {
|
|
|
149
156
|
}
|
|
150
157
|
return result.metadata?.kind === "write" || result.metadata?.kind === "edit";
|
|
151
158
|
}
|
|
159
|
+
function hasSubagentLifecycleActivity(toolCalls, toolResults) {
|
|
160
|
+
return toolCalls.some((toolCall) => isSubagentLifecycleTool(toolCall.name))
|
|
161
|
+
|| toolResults.some((result) => result.metadata?.kind === "subagent");
|
|
162
|
+
}
|
|
163
|
+
function isSubagentLifecycleTool(name) {
|
|
164
|
+
return name === "spawn_agent"
|
|
165
|
+
|| name === "wait_agent"
|
|
166
|
+
|| name === "send_input"
|
|
167
|
+
|| name === "close_agent"
|
|
168
|
+
|| name === "task";
|
|
169
|
+
}
|
|
152
170
|
function hashEditCall(toolCall) {
|
|
153
171
|
// Cheap fingerprint that identifies "same edit/write call". JSON of the
|
|
154
172
|
// sorted parsed args is good enough — we only need stable equality between
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import type { Provider, ThinkingLevel } from "./types.js";
|
|
1
|
+
import type { Provider, ReasoningEffort, ThinkingLevel } from "./types.js";
|
|
2
|
+
export interface CodexModelDescriptor {
|
|
3
|
+
id: string;
|
|
4
|
+
displayName?: string;
|
|
5
|
+
contextWindow?: number;
|
|
6
|
+
reasoningLevels?: ReasoningEffort[];
|
|
7
|
+
visibility?: string;
|
|
8
|
+
minimalClientVersion?: string;
|
|
9
|
+
/** Server-declared per-tool-output token cap (truncation_policy.limit when mode=tokens). */
|
|
10
|
+
toolOutputTokenLimit?: number;
|
|
11
|
+
}
|
|
2
12
|
export declare function isOpenAICodexBaseUrl(baseURL: string): boolean;
|
|
3
13
|
export declare function getOpenAICodexFallbackModels(): string[];
|
|
4
14
|
export declare function extractChatGptAccountId(accessToken: string): string | undefined;
|
|
@@ -11,4 +21,5 @@ export declare function createOpenAICodexProvider(options: {
|
|
|
11
21
|
export declare function fetchOpenAICodexModels(options: {
|
|
12
22
|
baseURL: string;
|
|
13
23
|
accessToken: string;
|
|
14
|
-
}): Promise<
|
|
24
|
+
}): Promise<CodexModelDescriptor[]>;
|
|
25
|
+
export declare function sortCodexModelDescriptors(descriptors: CodexModelDescriptor[]): CodexModelDescriptor[];
|
|
@@ -2,7 +2,10 @@ import { listBuiltinModels } from "./model-catalog.js";
|
|
|
2
2
|
import { resolveProviderRequestConfig } from "./provider-transform.js";
|
|
3
3
|
const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
|
|
4
4
|
const OPENAI_BETA_RESPONSES = "responses=experimental";
|
|
5
|
-
|
|
5
|
+
// OpenAI gates new codex models server-side by client_version (each model carries a
|
|
6
|
+
// `minimal_client_version`). Track a recent real Codex CLI release; override via env
|
|
7
|
+
// when OpenAI lifts the gate again before we cut a new release.
|
|
8
|
+
const CODEX_CLIENT_VERSION = process.env.BUBBLE_CODEX_CLIENT_VERSION?.trim() || "0.150.0";
|
|
6
9
|
const MODEL_DISCOVERY_PATHS = [
|
|
7
10
|
`/codex/models?client_version=${CODEX_CLIENT_VERSION}`,
|
|
8
11
|
"/models",
|
|
@@ -205,9 +208,9 @@ export async function fetchOpenAICodexModels(options) {
|
|
|
205
208
|
if (!response?.ok)
|
|
206
209
|
continue;
|
|
207
210
|
const payload = await response.json().catch(() => undefined);
|
|
208
|
-
const
|
|
209
|
-
if (
|
|
210
|
-
return
|
|
211
|
+
const descriptors = extractCodexModelDescriptors(payload);
|
|
212
|
+
if (descriptors.length > 0) {
|
|
213
|
+
return sortCodexModelDescriptors(descriptors);
|
|
211
214
|
}
|
|
212
215
|
}
|
|
213
216
|
return [];
|
|
@@ -361,18 +364,54 @@ function resolveRelativeUrl(baseURL, path) {
|
|
|
361
364
|
const normalized = (baseURL.trim() || DEFAULT_CODEX_BASE_URL).replace(/\/+$/, "");
|
|
362
365
|
return `${normalized}${path}`;
|
|
363
366
|
}
|
|
364
|
-
|
|
365
|
-
|
|
367
|
+
const REASONING_EFFORTS = [
|
|
368
|
+
"off", "minimal", "low", "medium", "high", "xhigh", "max",
|
|
369
|
+
];
|
|
370
|
+
function extractCodexModelDescriptors(payload) {
|
|
371
|
+
const out = [];
|
|
366
372
|
const seen = new Set();
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
373
|
+
const isCodexId = (value) => typeof value === "string" && /^gpt-|^codex-/i.test(value);
|
|
374
|
+
const pickId = (record) => {
|
|
375
|
+
for (const key of ["slug", "id", "model_slug", "model"]) {
|
|
376
|
+
const v = record[key];
|
|
377
|
+
if (isCodexId(v))
|
|
378
|
+
return v;
|
|
379
|
+
}
|
|
380
|
+
return undefined;
|
|
381
|
+
};
|
|
382
|
+
const buildDescriptor = (record, id) => {
|
|
383
|
+
const desc = { id };
|
|
384
|
+
const displayName = record.display_name;
|
|
385
|
+
if (typeof displayName === "string" && displayName)
|
|
386
|
+
desc.displayName = displayName;
|
|
387
|
+
const ctx = record.context_window;
|
|
388
|
+
if (typeof ctx === "number" && ctx > 0)
|
|
389
|
+
desc.contextWindow = ctx;
|
|
390
|
+
const visibility = record.visibility;
|
|
391
|
+
if (typeof visibility === "string")
|
|
392
|
+
desc.visibility = visibility;
|
|
393
|
+
const minVer = record.minimal_client_version;
|
|
394
|
+
if (typeof minVer === "string")
|
|
395
|
+
desc.minimalClientVersion = minVer;
|
|
396
|
+
const levels = record.supported_reasoning_levels;
|
|
397
|
+
if (Array.isArray(levels)) {
|
|
398
|
+
const efforts = new Set(["off"]);
|
|
399
|
+
for (const level of levels) {
|
|
400
|
+
const effort = level?.effort;
|
|
401
|
+
if (typeof effort === "string" && REASONING_EFFORTS.includes(effort)) {
|
|
402
|
+
efforts.add(effort);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
desc.reasoningLevels = REASONING_EFFORTS.filter((e) => efforts.has(e));
|
|
406
|
+
}
|
|
407
|
+
const truncPolicy = record.truncation_policy;
|
|
408
|
+
if (truncPolicy && truncPolicy.mode === "tokens") {
|
|
409
|
+
const limit = truncPolicy.limit;
|
|
410
|
+
if (typeof limit === "number" && limit > 0) {
|
|
411
|
+
desc.toolOutputTokenLimit = limit;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return desc;
|
|
376
415
|
};
|
|
377
416
|
const visit = (value) => {
|
|
378
417
|
if (Array.isArray(value)) {
|
|
@@ -380,32 +419,42 @@ function extractModelIds(payload) {
|
|
|
380
419
|
visit(item);
|
|
381
420
|
return;
|
|
382
421
|
}
|
|
383
|
-
if (!value || typeof value !== "object")
|
|
384
|
-
maybeAdd(value);
|
|
422
|
+
if (!value || typeof value !== "object")
|
|
385
423
|
return;
|
|
386
|
-
}
|
|
387
424
|
const record = value;
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
425
|
+
const id = pickId(record);
|
|
426
|
+
if (id && !seen.has(id)) {
|
|
427
|
+
seen.add(id);
|
|
428
|
+
out.push(buildDescriptor(record, id));
|
|
429
|
+
}
|
|
392
430
|
for (const child of Object.values(record)) {
|
|
393
|
-
if (child
|
|
431
|
+
if (child && typeof child === "object")
|
|
394
432
|
visit(child);
|
|
395
|
-
}
|
|
396
433
|
}
|
|
397
434
|
};
|
|
398
435
|
visit(payload);
|
|
399
|
-
return
|
|
436
|
+
return out;
|
|
437
|
+
}
|
|
438
|
+
// Extracts the family version from a codex slug (e.g. "gpt-5.5-codex" → 5005).
|
|
439
|
+
// Used so models from a newer family float to the top even before the static
|
|
440
|
+
// catalog knows about them.
|
|
441
|
+
function parseCodexFamilyRank(id) {
|
|
442
|
+
const match = id.match(/(\d+)\.(\d+)/);
|
|
443
|
+
if (!match)
|
|
444
|
+
return 0;
|
|
445
|
+
return parseInt(match[1], 10) * 1000 + parseInt(match[2], 10);
|
|
400
446
|
}
|
|
401
|
-
function
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
|
|
447
|
+
export function sortCodexModelDescriptors(descriptors) {
|
|
448
|
+
const preferred = new Map(getOpenAICodexFallbackModels().map((id, index) => [id, index]));
|
|
449
|
+
return [...descriptors].sort((left, right) => {
|
|
450
|
+
const leftFamily = parseCodexFamilyRank(left.id);
|
|
451
|
+
const rightFamily = parseCodexFamilyRank(right.id);
|
|
452
|
+
if (leftFamily !== rightFamily)
|
|
453
|
+
return rightFamily - leftFamily;
|
|
454
|
+
const leftRank = preferred.get(left.id) ?? Number.MAX_SAFE_INTEGER;
|
|
455
|
+
const rightRank = preferred.get(right.id) ?? Number.MAX_SAFE_INTEGER;
|
|
407
456
|
if (leftRank !== rightRank)
|
|
408
457
|
return leftRank - rightRank;
|
|
409
|
-
return left.localeCompare(right);
|
|
458
|
+
return left.id.localeCompare(right.id);
|
|
410
459
|
});
|
|
411
460
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Supports OpenAI-compatible providers with dynamic or static model lists.
|
|
5
5
|
* Reads provider configuration from models.json first, then falls back to config.json.
|
|
6
6
|
*/
|
|
7
|
-
import { BUILTIN_PROVIDERS as CATALOG_PROVIDERS, getBuiltinModel, getBuiltinProvider, listBuiltinModels, } from "./model-catalog.js";
|
|
7
|
+
import { BUILTIN_PROVIDERS as CATALOG_PROVIDERS, getBuiltinModel, getBuiltinProvider, listBuiltinModels, registerDynamicModelMetadata, } from "./model-catalog.js";
|
|
8
8
|
import { ModelConfig } from "./model-config.js";
|
|
9
9
|
import { AuthStorage } from "./oauth/index.js";
|
|
10
10
|
import { fetchOpenAICodexModels } from "./provider-openai-codex.js";
|
|
@@ -194,12 +194,28 @@ export class ProviderRegistry {
|
|
|
194
194
|
}
|
|
195
195
|
if (provider.id === "openai" && provider.authType === "oauth" && provider.apiKey) {
|
|
196
196
|
try {
|
|
197
|
-
const
|
|
197
|
+
const descriptors = await fetchOpenAICodexModels({
|
|
198
198
|
baseURL: provider.baseURL,
|
|
199
199
|
accessToken: provider.apiKey,
|
|
200
200
|
});
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
const visible = descriptors.filter((d) => d.visibility !== "hide");
|
|
202
|
+
if (visible.length > 0) {
|
|
203
|
+
for (const d of visible) {
|
|
204
|
+
const catalogEntry = getBuiltinModel("openai-codex", d.id);
|
|
205
|
+
registerDynamicModelMetadata({
|
|
206
|
+
id: d.id,
|
|
207
|
+
name: d.displayName || catalogEntry?.name || d.id,
|
|
208
|
+
providerId: "openai-codex",
|
|
209
|
+
reasoningLevels: d.reasoningLevels ?? catalogEntry?.reasoningLevels ?? ["off"],
|
|
210
|
+
contextWindow: d.contextWindow ?? catalogEntry?.contextWindow,
|
|
211
|
+
toolOutputTokenLimit: d.toolOutputTokenLimit ?? catalogEntry?.toolOutputTokenLimit,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return visible.map((d) => ({
|
|
215
|
+
id: d.id,
|
|
216
|
+
name: d.displayName || d.id,
|
|
217
|
+
providerId: provider.id,
|
|
218
|
+
}));
|
|
203
219
|
}
|
|
204
220
|
}
|
|
205
221
|
catch {
|
|
@@ -308,6 +308,30 @@ const builtinSlashCommandEntries = [
|
|
|
308
308
|
ctx.exit();
|
|
309
309
|
},
|
|
310
310
|
},
|
|
311
|
+
{
|
|
312
|
+
name: "theme",
|
|
313
|
+
description: "Switch the color theme. Usage: /theme [auto|light|dark]",
|
|
314
|
+
async handler(args, ctx) {
|
|
315
|
+
if (!ctx.setThemeMode || !ctx.getThemeMode || !ctx.getResolvedTheme) {
|
|
316
|
+
return "Theme switching is only available inside the TUI.";
|
|
317
|
+
}
|
|
318
|
+
const arg = args.trim().toLowerCase();
|
|
319
|
+
if (!arg) {
|
|
320
|
+
const order = ["auto", "light", "dark"];
|
|
321
|
+
const current = ctx.getThemeMode();
|
|
322
|
+
const next = order[(order.indexOf(current) + 1) % order.length];
|
|
323
|
+
ctx.setThemeMode(next);
|
|
324
|
+
const resolved = next === "auto" ? ctx.getResolvedTheme() : next;
|
|
325
|
+
return `Theme: ${next}${next === "auto" ? ` (resolved to ${resolved})` : ""}`;
|
|
326
|
+
}
|
|
327
|
+
if (arg !== "auto" && arg !== "light" && arg !== "dark") {
|
|
328
|
+
return "Usage: /theme [auto|light|dark]";
|
|
329
|
+
}
|
|
330
|
+
ctx.setThemeMode(arg);
|
|
331
|
+
const resolved = arg === "auto" ? ctx.getResolvedTheme() : arg;
|
|
332
|
+
return `Theme set to ${arg}${arg === "auto" ? ` (resolved to ${resolved})` : ""}.`;
|
|
333
|
+
},
|
|
334
|
+
},
|
|
311
335
|
{
|
|
312
336
|
name: "clear",
|
|
313
337
|
description: "Clear the current conversation history",
|
|
@@ -8,6 +8,7 @@ import type { SettingsManager } from "../permissions/settings.js";
|
|
|
8
8
|
import type { McpManager } from "../mcp/manager.js";
|
|
9
9
|
import type { LspService } from "../lsp/index.js";
|
|
10
10
|
import type { MemoryScope } from "../memory/index.js";
|
|
11
|
+
import type { ThemeMode } from "../config.js";
|
|
11
12
|
export interface SlashCommandContext {
|
|
12
13
|
agent: Agent;
|
|
13
14
|
addMessage: (role: "user" | "assistant" | "error", content: string) => void;
|
|
@@ -27,6 +28,12 @@ export interface SlashCommandContext {
|
|
|
27
28
|
runMemoryCompaction?: () => Promise<string>;
|
|
28
29
|
runMemorySummary?: (scope?: MemoryScope) => Promise<string>;
|
|
29
30
|
runMemoryRefresh?: (scope?: MemoryScope) => Promise<string>;
|
|
31
|
+
/** Get the current theme mode (auto/light/dark) — undefined when running in non-TUI contexts. */
|
|
32
|
+
getThemeMode?: () => ThemeMode;
|
|
33
|
+
/** Get the resolved active theme (always light or dark) — undefined when running in non-TUI contexts. */
|
|
34
|
+
getResolvedTheme?: () => "light" | "dark";
|
|
35
|
+
/** Persist a new theme mode AND apply it to the running TUI. */
|
|
36
|
+
setThemeMode?: (mode: ThemeMode) => void;
|
|
30
37
|
}
|
|
31
38
|
/**
|
|
32
39
|
* Return types for a slash command handler:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { discoverAgentProfiles, findAgentProfile } from "../agent/profiles.js";
|
|
2
|
+
import { formatSubagentRoute } from "../agent/subagent-route-format.js";
|
|
2
3
|
export function createSpawnAgentTool() {
|
|
3
4
|
return {
|
|
4
5
|
name: "spawn_agent",
|
|
@@ -16,6 +17,7 @@ export function createSpawnAgentTool() {
|
|
|
16
17
|
properties: {
|
|
17
18
|
agent_type: { type: "string", description: "Subagent profile or role name. Defaults to default." },
|
|
18
19
|
agent: { type: "string", description: "Alias for agent_type." },
|
|
20
|
+
category: { type: "string", description: "Optional semantic category for model/thinking routing, such as quick, deep, explore, review, frontend, or writing." },
|
|
19
21
|
message: { type: "string", description: "Initial task for the subagent." },
|
|
20
22
|
task: { type: "string", description: "Alias for message." },
|
|
21
23
|
fork_context: { type: "boolean", description: "When true, copy recent parent conversation into the child thread." },
|
|
@@ -55,15 +57,18 @@ export function createSpawnAgentTool() {
|
|
|
55
57
|
const snapshot = await ctx.agent.spawnSubAgent(message, ctx.cwd, {
|
|
56
58
|
profile: resolved.profile,
|
|
57
59
|
parentToolCallId: ctx.toolCall?.id ?? snapshotFallbackId(),
|
|
60
|
+
category: stringArg(args.category),
|
|
58
61
|
approval: parseApproval(args.approval),
|
|
59
62
|
abortSignal: ctx.abortSignal,
|
|
60
63
|
forkContext: args.fork_context === true,
|
|
61
64
|
});
|
|
62
65
|
return formatLifecycleResult("spawn_agent", [snapshot], [
|
|
63
|
-
`Spawned ${snapshot.nickname} (${snapshot
|
|
66
|
+
`Spawned ${snapshot.nickname} (${formatSnapshotRole(snapshot)})`,
|
|
64
67
|
`agent_id: ${snapshot.agentId}`,
|
|
65
68
|
`status: ${snapshot.status}`,
|
|
66
|
-
|
|
69
|
+
...formatRouteLines(snapshot),
|
|
70
|
+
`next: call wait_agent for ${snapshot.agentId} before reporting this subagent's current status or final result`,
|
|
71
|
+
"counting: this spawn result creates one unique subagent; later wait_agent results for the same agent_id are updates, not additional subagents",
|
|
67
72
|
]);
|
|
68
73
|
}
|
|
69
74
|
catch (error) {
|
|
@@ -278,13 +283,17 @@ function isFinalSnapshotStatus(status) {
|
|
|
278
283
|
|| status === "closed";
|
|
279
284
|
}
|
|
280
285
|
function formatSnapshot(snapshot) {
|
|
281
|
-
const label = `${snapshot.nickname} (${snapshot
|
|
286
|
+
const label = `${snapshot.nickname} (${formatSnapshotRole(snapshot)})`;
|
|
282
287
|
const lines = [
|
|
283
288
|
`## ${label}`,
|
|
284
289
|
`agent_id: ${snapshot.agentId}`,
|
|
285
290
|
`status: ${snapshot.status}`,
|
|
286
|
-
`task: ${snapshot.task}`,
|
|
287
291
|
];
|
|
292
|
+
if (snapshot.category) {
|
|
293
|
+
lines.push(`category: ${snapshot.category}`);
|
|
294
|
+
}
|
|
295
|
+
lines.push(...formatRouteLines(snapshot));
|
|
296
|
+
lines.push(`task: ${snapshot.task}`);
|
|
288
297
|
if (snapshot.summary) {
|
|
289
298
|
lines.push("", "Summary:", snapshot.summary);
|
|
290
299
|
}
|
|
@@ -299,6 +308,13 @@ function formatSnapshot(snapshot) {
|
|
|
299
308
|
}
|
|
300
309
|
return lines;
|
|
301
310
|
}
|
|
311
|
+
function formatSnapshotRole(snapshot) {
|
|
312
|
+
return [snapshot.agentName, snapshot.category ? `/${snapshot.category}` : ""].join("") || "default";
|
|
313
|
+
}
|
|
314
|
+
function formatRouteLines(snapshot) {
|
|
315
|
+
const route = formatSubagentRoute(snapshot.route, { includeThinking: true });
|
|
316
|
+
return route ? [`route: ${route}`] : [];
|
|
317
|
+
}
|
|
302
318
|
function snapshotToMetadata(snapshot) {
|
|
303
319
|
return {
|
|
304
320
|
subAgentId: snapshot.agentId,
|
|
@@ -306,6 +322,8 @@ function snapshotToMetadata(snapshot) {
|
|
|
306
322
|
nickname: snapshot.nickname,
|
|
307
323
|
status: snapshot.status === "closed" ? "cancelled" : snapshot.status,
|
|
308
324
|
profileSource: snapshot.profileSource,
|
|
325
|
+
category: snapshot.category,
|
|
326
|
+
route: snapshot.route,
|
|
309
327
|
task: snapshot.task,
|
|
310
328
|
summary: snapshot.summary,
|
|
311
329
|
toolNotes: snapshot.toolNotes,
|
package/dist/tools/edit.js
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { constants } from "node:fs";
|
|
7
7
|
import { access, readFile, writeFile } from "node:fs/promises";
|
|
8
|
-
import { resolve } from "node:path";
|
|
9
8
|
import { createTwoFilesPatch } from "diff";
|
|
10
9
|
import { gateToolAction } from "../approval/tool-helper.js";
|
|
11
10
|
import { countUnifiedDiffChanges } from "../diff-stats.js";
|
|
@@ -13,6 +12,7 @@ import { formatDiagnosticBlocks } from "../lsp/index.js";
|
|
|
13
12
|
import { applyEditsToContent, EditApplyError, formatEditMatchNotes } from "./edit-apply.js";
|
|
14
13
|
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
|
15
14
|
import { isWithinWorkspace } from "./file-state.js";
|
|
15
|
+
import { resolveToolPath } from "./path-utils.js";
|
|
16
16
|
export function createEditTool(cwd, approval, lsp, fileState) {
|
|
17
17
|
return {
|
|
18
18
|
name: "edit",
|
|
@@ -39,7 +39,7 @@ export function createEditTool(cwd, approval, lsp, fileState) {
|
|
|
39
39
|
required: ["path", "edits"],
|
|
40
40
|
},
|
|
41
41
|
async execute(args) {
|
|
42
|
-
const filePath =
|
|
42
|
+
const filePath = resolveToolPath(cwd, args.path);
|
|
43
43
|
if (!isWithinWorkspace(cwd, filePath)) {
|
|
44
44
|
return {
|
|
45
45
|
content: `Error: Edit path is outside the workspace: ${filePath}`,
|
package/dist/tools/glob.js
CHANGED
|
@@ -5,6 +5,7 @@ import { readdir, stat } from "node:fs/promises";
|
|
|
5
5
|
import { relative, resolve } from "node:path";
|
|
6
6
|
import picomatch from "picomatch";
|
|
7
7
|
import { isSensitivePath } from "./sensitive-paths.js";
|
|
8
|
+
import { resolveToolPath } from "./path-utils.js";
|
|
8
9
|
const MAX_RESULTS = 100;
|
|
9
10
|
const DEFAULT_IGNORES = new Set([
|
|
10
11
|
".git",
|
|
@@ -31,7 +32,7 @@ export function createGlobTool(cwd) {
|
|
|
31
32
|
required: ["pattern"],
|
|
32
33
|
},
|
|
33
34
|
async execute(args, ctx) {
|
|
34
|
-
const root =
|
|
35
|
+
const root = resolveToolPath(cwd, typeof args.path === "string" && args.path.trim() ? args.path : ".");
|
|
35
36
|
const pattern = String(args.pattern || "").trim();
|
|
36
37
|
if (!pattern) {
|
|
37
38
|
return { content: "Error: glob pattern is required", isError: true, status: "command_error" };
|
package/dist/tools/grep.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Grep tool - search file contents using ripgrep.
|
|
3
3
|
*/
|
|
4
4
|
import { execFile } from "node:child_process";
|
|
5
|
-
import { resolve } from "node:path";
|
|
6
5
|
import { isSensitivePath } from "./sensitive-paths.js";
|
|
7
6
|
import { analyzeToolIntent } from "../agent/tool-intent.js";
|
|
7
|
+
import { resolveToolPath } from "./path-utils.js";
|
|
8
8
|
const MAX_MATCHES = 100;
|
|
9
9
|
export function createGrepTool(cwd) {
|
|
10
10
|
return {
|
|
@@ -22,7 +22,7 @@ export function createGrepTool(cwd) {
|
|
|
22
22
|
required: ["pattern"],
|
|
23
23
|
},
|
|
24
24
|
async execute(args) {
|
|
25
|
-
const searchPath = args.path ?
|
|
25
|
+
const searchPath = args.path ? resolveToolPath(cwd, args.path) : cwd;
|
|
26
26
|
const pattern = String(args.pattern);
|
|
27
27
|
const intent = analyzeToolIntent({
|
|
28
28
|
name: "grep",
|
package/dist/tools/lsp.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { access } from "node:fs/promises";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
3
|
import { gateToolAction } from "../approval/tool-helper.js";
|
|
5
4
|
import { getLspService } from "../lsp/index.js";
|
|
5
|
+
import { resolveToolPath } from "./path-utils.js";
|
|
6
6
|
const OPERATIONS = [
|
|
7
7
|
"goToDefinition",
|
|
8
8
|
"findReferences",
|
|
@@ -37,7 +37,7 @@ export function createLspTool(cwd, lsp = getLspService(cwd), approval) {
|
|
|
37
37
|
if (!OPERATIONS.includes(operation)) {
|
|
38
38
|
return { content: `Error: Unsupported LSP operation: ${args.operation}`, isError: true };
|
|
39
39
|
}
|
|
40
|
-
const file =
|
|
40
|
+
const file = resolveToolPath(cwd, args.filePath);
|
|
41
41
|
try {
|
|
42
42
|
await access(file, constants.R_OK);
|
|
43
43
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
export function expandHomePath(value) {
|
|
4
|
+
const text = String(value ?? "");
|
|
5
|
+
if (text === "~")
|
|
6
|
+
return homedir();
|
|
7
|
+
if (text.startsWith("~/") || text.startsWith("~\\")) {
|
|
8
|
+
return join(homedir(), text.slice(2));
|
|
9
|
+
}
|
|
10
|
+
return text;
|
|
11
|
+
}
|
|
12
|
+
export function resolveToolPath(cwd, value, fallback = ".") {
|
|
13
|
+
const text = String(value ?? "");
|
|
14
|
+
const path = text === "" ? fallback : text;
|
|
15
|
+
return resolve(cwd, expandHomePath(path));
|
|
16
|
+
}
|