@oh-my-pi/pi-coding-agent 11.5.1 → 11.6.0
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/CHANGELOG.md +24 -0
- package/package.json +7 -7
- package/src/config/model-registry.ts +129 -40
- package/src/extensibility/extensions/runner.ts +100 -25
- package/src/extensibility/extensions/wrapper.ts +3 -3
- package/src/extensibility/slash-commands.ts +1 -0
- package/src/modes/components/assistant-message.ts +9 -4
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/session/agent-session.ts +6 -0
- package/src/task/render.ts +13 -7
- package/src/tools/ask.ts +10 -41
- package/src/tools/todo-write.ts +14 -31
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [11.6.0] - 2026-02-07
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed task tool renderer not sanitizing tabs, causing visual holes in TUI output
|
|
10
|
+
- Fixed task tool expanded view showing redundant `<swarm_context>` block that is shared across all tasks
|
|
11
|
+
- Fixed assistant message spacer appearing before tool executions when no visible content follows thinking block
|
|
12
|
+
- Fixed extension runner `emit()` type safety with narrowed event/result types
|
|
13
|
+
- Fixed extension runner `tool_result` event chaining across multiple extensions via dedicated `emitToolResult()`
|
|
14
|
+
- Fixed queued messages not delivered after auto-compaction completes
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Added `/quit` slash command as alias for `/exit`
|
|
19
|
+
- Added per-model overrides (`modelOverrides`) in `models.json` for customizing built-in model properties
|
|
20
|
+
- Added `mergeCustomModels` to merge custom models with built-ins by provider+id instead of replacing
|
|
21
|
+
|
|
22
|
+
## [11.5.2] - 2026-02-07
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Fixed TUI crash when ask tool renders long user input exceeding terminal width by using Text component for word wrapping instead of raw line output
|
|
27
|
+
- Fixed TUI crash when todo_write tool renders long todo content exceeding terminal width by using Text component for word wrapping instead of truncation
|
|
28
|
+
|
|
5
29
|
## [11.5.0] - 2026-02-06
|
|
6
30
|
|
|
7
31
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.6.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -90,12 +90,12 @@
|
|
|
90
90
|
"@mozilla/readability": "0.6.0",
|
|
91
91
|
"@oclif/core": "^4.8.0",
|
|
92
92
|
"@oclif/plugin-autocomplete": "^3.2.40",
|
|
93
|
-
"@oh-my-pi/omp-stats": "11.
|
|
94
|
-
"@oh-my-pi/pi-agent-core": "11.
|
|
95
|
-
"@oh-my-pi/pi-ai": "11.
|
|
96
|
-
"@oh-my-pi/pi-natives": "11.
|
|
97
|
-
"@oh-my-pi/pi-tui": "11.
|
|
98
|
-
"@oh-my-pi/pi-utils": "11.
|
|
93
|
+
"@oh-my-pi/omp-stats": "11.6.0",
|
|
94
|
+
"@oh-my-pi/pi-agent-core": "11.6.0",
|
|
95
|
+
"@oh-my-pi/pi-ai": "11.6.0",
|
|
96
|
+
"@oh-my-pi/pi-natives": "11.6.0",
|
|
97
|
+
"@oh-my-pi/pi-tui": "11.6.0",
|
|
98
|
+
"@oh-my-pi/pi-utils": "11.6.0",
|
|
99
99
|
"@sinclair/typebox": "^0.34.48",
|
|
100
100
|
"ajv": "^8.17.1",
|
|
101
101
|
"chalk": "^5.6.2",
|
|
@@ -85,6 +85,27 @@ const ModelDefinitionSchema = Type.Object({
|
|
|
85
85
|
compat: Type.Optional(OpenAICompatSchema),
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
// Schema for per-model overrides (all fields optional, merged with built-in model)
|
|
89
|
+
const ModelOverrideSchema = Type.Object({
|
|
90
|
+
name: Type.Optional(Type.String({ minLength: 1 })),
|
|
91
|
+
reasoning: Type.Optional(Type.Boolean()),
|
|
92
|
+
input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))),
|
|
93
|
+
cost: Type.Optional(
|
|
94
|
+
Type.Object({
|
|
95
|
+
input: Type.Optional(Type.Number()),
|
|
96
|
+
output: Type.Optional(Type.Number()),
|
|
97
|
+
cacheRead: Type.Optional(Type.Number()),
|
|
98
|
+
cacheWrite: Type.Optional(Type.Number()),
|
|
99
|
+
}),
|
|
100
|
+
),
|
|
101
|
+
contextWindow: Type.Optional(Type.Number()),
|
|
102
|
+
maxTokens: Type.Optional(Type.Number()),
|
|
103
|
+
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
104
|
+
compat: Type.Optional(OpenAICompatSchema),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
type ModelOverride = Static<typeof ModelOverrideSchema>;
|
|
108
|
+
|
|
88
109
|
const ProviderConfigSchema = Type.Object({
|
|
89
110
|
baseUrl: Type.Optional(Type.String({ minLength: 1 })),
|
|
90
111
|
apiKey: Type.Optional(Type.String({ minLength: 1 })),
|
|
@@ -102,6 +123,7 @@ const ProviderConfigSchema = Type.Object({
|
|
|
102
123
|
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
103
124
|
authHeader: Type.Optional(Type.Boolean()),
|
|
104
125
|
models: Type.Optional(Type.Array(ModelDefinitionSchema)),
|
|
126
|
+
modelOverrides: Type.Optional(Type.Record(Type.String(), ModelOverrideSchema)),
|
|
105
127
|
});
|
|
106
128
|
|
|
107
129
|
const ModelsConfigSchema = Type.Object({
|
|
@@ -118,11 +140,11 @@ export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", ModelsCon
|
|
|
118
140
|
const models = providerConfig.models ?? [];
|
|
119
141
|
|
|
120
142
|
if (models.length === 0) {
|
|
121
|
-
// Override-only config:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
);
|
|
143
|
+
// Override-only config: needs baseUrl or modelOverrides
|
|
144
|
+
const hasModelOverrides =
|
|
145
|
+
providerConfig.modelOverrides && Object.keys(providerConfig.modelOverrides).length > 0;
|
|
146
|
+
if (!providerConfig.baseUrl && !hasModelOverrides) {
|
|
147
|
+
throw new Error(`Provider ${providerName}: must specify "baseUrl", "modelOverrides", or "models".`);
|
|
126
148
|
}
|
|
127
149
|
} else {
|
|
128
150
|
// Full replacement: needs baseUrl and apiKey
|
|
@@ -172,10 +194,8 @@ export interface SerializedModelRegistry {
|
|
|
172
194
|
/** Result of loading custom models from models.json */
|
|
173
195
|
interface CustomModelsResult {
|
|
174
196
|
models?: Model<Api>[];
|
|
175
|
-
/** Providers with custom models (full replacement) */
|
|
176
|
-
replacedProviders?: Set<string>;
|
|
177
|
-
/** Providers with only baseUrl/headers override (no custom models) */
|
|
178
197
|
overrides?: Map<string, ProviderOverride>;
|
|
198
|
+
modelOverrides?: Map<string, Map<string, ModelOverride>>;
|
|
179
199
|
error?: ConfigError;
|
|
180
200
|
found: boolean;
|
|
181
201
|
}
|
|
@@ -190,6 +210,45 @@ function resolveApiKeyConfig(keyConfig: string): string | undefined {
|
|
|
190
210
|
return keyConfig;
|
|
191
211
|
}
|
|
192
212
|
|
|
213
|
+
function mergeCompat(
|
|
214
|
+
baseCompat: Model<Api>["compat"],
|
|
215
|
+
overrideCompat: ModelOverride["compat"],
|
|
216
|
+
): Model<Api>["compat"] | undefined {
|
|
217
|
+
if (!overrideCompat) return baseCompat;
|
|
218
|
+
const base = baseCompat as any;
|
|
219
|
+
const override = overrideCompat as any;
|
|
220
|
+
const merged = { ...base, ...override };
|
|
221
|
+
if (base?.openRouterRouting || override.openRouterRouting) {
|
|
222
|
+
merged.openRouterRouting = { ...base?.openRouterRouting, ...override.openRouterRouting };
|
|
223
|
+
}
|
|
224
|
+
if (base?.vercelGatewayRouting || override.vercelGatewayRouting) {
|
|
225
|
+
merged.vercelGatewayRouting = { ...base?.vercelGatewayRouting, ...override.vercelGatewayRouting };
|
|
226
|
+
}
|
|
227
|
+
return merged;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<Api> {
|
|
231
|
+
const result = { ...model };
|
|
232
|
+
if (override.name !== undefined) result.name = override.name;
|
|
233
|
+
if (override.reasoning !== undefined) result.reasoning = override.reasoning;
|
|
234
|
+
if (override.input !== undefined) result.input = override.input as ("text" | "image")[];
|
|
235
|
+
if (override.contextWindow !== undefined) result.contextWindow = override.contextWindow;
|
|
236
|
+
if (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;
|
|
237
|
+
if (override.cost) {
|
|
238
|
+
result.cost = {
|
|
239
|
+
input: override.cost.input ?? model.cost.input,
|
|
240
|
+
output: override.cost.output ?? model.cost.output,
|
|
241
|
+
cacheRead: override.cost.cacheRead ?? model.cost.cacheRead,
|
|
242
|
+
cacheWrite: override.cost.cacheWrite ?? model.cost.cacheWrite,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (override.headers) {
|
|
246
|
+
result.headers = { ...model.headers, ...override.headers };
|
|
247
|
+
}
|
|
248
|
+
result.compat = mergeCompat(model.compat, override.compat);
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
|
|
193
252
|
/**
|
|
194
253
|
* Model registry - loads and manages models, resolves API keys via AuthStorage.
|
|
195
254
|
*/
|
|
@@ -272,17 +331,17 @@ export class ModelRegistry {
|
|
|
272
331
|
}
|
|
273
332
|
|
|
274
333
|
private loadModels() {
|
|
275
|
-
// Load custom models from models.json first (to know which providers to
|
|
334
|
+
// Load custom models from models.json first (to know which providers to override)
|
|
276
335
|
const {
|
|
277
336
|
models: customModels = [],
|
|
278
|
-
replacedProviders = new Set(),
|
|
279
337
|
overrides = new Map(),
|
|
338
|
+
modelOverrides = new Map(),
|
|
280
339
|
error: configError,
|
|
281
340
|
} = this.loadCustomModels();
|
|
282
341
|
this.configError = configError;
|
|
283
342
|
|
|
284
|
-
const builtInModels = this.loadBuiltInModels(
|
|
285
|
-
const combined =
|
|
343
|
+
const builtInModels = this.loadBuiltInModels(overrides, modelOverrides);
|
|
344
|
+
const combined = this.mergeCustomModels(builtInModels, customModels);
|
|
286
345
|
|
|
287
346
|
// Update github-copilot base URL based on OAuth credentials
|
|
288
347
|
const copilotCred = this.authStorage.getOAuthCredential("github-copilot");
|
|
@@ -297,56 +356,86 @@ export class ModelRegistry {
|
|
|
297
356
|
}
|
|
298
357
|
}
|
|
299
358
|
|
|
300
|
-
/** Load built-in models,
|
|
301
|
-
private loadBuiltInModels(
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
359
|
+
/** Load built-in models, applying provider and per-model overrides */
|
|
360
|
+
private loadBuiltInModels(
|
|
361
|
+
overrides: Map<string, ProviderOverride>,
|
|
362
|
+
modelOverrides: Map<string, Map<string, ModelOverride>>,
|
|
363
|
+
): Model<Api>[] {
|
|
364
|
+
return getProviders().flatMap(provider => {
|
|
365
|
+
const models = getModels(provider as any) as Model<Api>[];
|
|
366
|
+
const providerOverride = overrides.get(provider);
|
|
367
|
+
const perModelOverrides = modelOverrides.get(provider);
|
|
368
|
+
|
|
369
|
+
return models.map(m => {
|
|
370
|
+
let model = m;
|
|
371
|
+
if (providerOverride) {
|
|
372
|
+
model = {
|
|
373
|
+
...model,
|
|
374
|
+
baseUrl: providerOverride.baseUrl ?? model.baseUrl,
|
|
375
|
+
headers: providerOverride.headers ? { ...model.headers, ...providerOverride.headers } : model.headers,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
const modelOverride = perModelOverrides?.get(m.id);
|
|
379
|
+
if (modelOverride) {
|
|
380
|
+
model = applyModelOverride(model, modelOverride);
|
|
381
|
+
}
|
|
382
|
+
return model;
|
|
315
383
|
});
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Merge custom models with built-in, replacing by provider+id match */
|
|
388
|
+
private mergeCustomModels(builtInModels: Model<Api>[], customModels: Model<Api>[]): Model<Api>[] {
|
|
389
|
+
const merged = [...builtInModels];
|
|
390
|
+
for (const customModel of customModels) {
|
|
391
|
+
const existingIndex = merged.findIndex(m => m.provider === customModel.provider && m.id === customModel.id);
|
|
392
|
+
if (existingIndex >= 0) {
|
|
393
|
+
merged[existingIndex] = customModel;
|
|
394
|
+
} else {
|
|
395
|
+
merged.push(customModel);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return merged;
|
|
316
399
|
}
|
|
317
400
|
|
|
318
401
|
private loadCustomModels(): CustomModelsResult {
|
|
319
402
|
const { value, error, status } = this.modelsConfigFile.tryLoad();
|
|
320
403
|
|
|
321
404
|
if (status === "error") {
|
|
322
|
-
return { models: [],
|
|
405
|
+
return { models: [], overrides: new Map(), modelOverrides: new Map(), error, found: true };
|
|
323
406
|
} else if (status === "not-found") {
|
|
324
|
-
return { models: [],
|
|
407
|
+
return { models: [], overrides: new Map(), modelOverrides: new Map(), found: false };
|
|
325
408
|
}
|
|
326
409
|
|
|
327
|
-
// Separate providers into "full replacement" (has models) vs "override-only" (no models)
|
|
328
|
-
const replacedProviders = new Set<string>();
|
|
329
410
|
const overrides = new Map<string, ProviderOverride>();
|
|
411
|
+
const allModelOverrides = new Map<string, Map<string, ModelOverride>>();
|
|
330
412
|
|
|
331
413
|
for (const [providerName, providerConfig] of Object.entries(value.providers)) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
replacedProviders.add(providerName);
|
|
335
|
-
} else {
|
|
336
|
-
// No models -> just override baseUrl/headers on built-in
|
|
414
|
+
// Always set overrides when baseUrl/headers present
|
|
415
|
+
if (providerConfig.baseUrl || providerConfig.headers || providerConfig.apiKey) {
|
|
337
416
|
overrides.set(providerName, {
|
|
338
417
|
baseUrl: providerConfig.baseUrl,
|
|
339
418
|
headers: providerConfig.headers,
|
|
340
419
|
apiKey: providerConfig.apiKey,
|
|
341
420
|
});
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Always store API key for fallback resolver
|
|
424
|
+
if (providerConfig.apiKey) {
|
|
425
|
+
this.customProviderApiKeys.set(providerName, providerConfig.apiKey);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Parse per-model overrides
|
|
429
|
+
if (providerConfig.modelOverrides) {
|
|
430
|
+
const perModel = new Map<string, ModelOverride>();
|
|
431
|
+
for (const [modelId, override] of Object.entries(providerConfig.modelOverrides)) {
|
|
432
|
+
perModel.set(modelId, override);
|
|
345
433
|
}
|
|
434
|
+
allModelOverrides.set(providerName, perModel);
|
|
346
435
|
}
|
|
347
436
|
}
|
|
348
437
|
|
|
349
|
-
return { models: this.parseModels(value),
|
|
438
|
+
return { models: this.parseModels(value), overrides, modelOverrides: allModelOverrides, found: true };
|
|
350
439
|
}
|
|
351
440
|
|
|
352
441
|
private parseModels(config: ModelsConfig): Model<Api>[] {
|
|
@@ -34,11 +34,14 @@ import type {
|
|
|
34
34
|
RegisteredTool,
|
|
35
35
|
ResourcesDiscoverEvent,
|
|
36
36
|
ResourcesDiscoverResult,
|
|
37
|
+
SessionBeforeBranchResult,
|
|
37
38
|
SessionBeforeCompactResult,
|
|
39
|
+
SessionBeforeSwitchResult,
|
|
38
40
|
SessionBeforeTreeResult,
|
|
39
41
|
SessionCompactingResult,
|
|
40
42
|
ToolCallEvent,
|
|
41
43
|
ToolCallEventResult,
|
|
44
|
+
ToolResultEvent,
|
|
42
45
|
ToolResultEventResult,
|
|
43
46
|
UserBashEvent,
|
|
44
47
|
UserBashEventResult,
|
|
@@ -54,6 +57,44 @@ interface BeforeAgentStartCombinedResult {
|
|
|
54
57
|
|
|
55
58
|
export type ExtensionErrorListener = (error: ExtensionError) => void;
|
|
56
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Events handled by the generic emit() method.
|
|
62
|
+
* Events with dedicated emitXxx() methods are excluded for stronger type safety.
|
|
63
|
+
*/
|
|
64
|
+
type RunnerEmitEvent = Exclude<
|
|
65
|
+
ExtensionEvent,
|
|
66
|
+
| ToolCallEvent
|
|
67
|
+
| ToolResultEvent
|
|
68
|
+
| UserBashEvent
|
|
69
|
+
| ContextEvent
|
|
70
|
+
| BeforeAgentStartEvent
|
|
71
|
+
| ResourcesDiscoverEvent
|
|
72
|
+
| InputEvent
|
|
73
|
+
>;
|
|
74
|
+
|
|
75
|
+
type SessionBeforeEvent = Extract<
|
|
76
|
+
RunnerEmitEvent,
|
|
77
|
+
{ type: "session_before_switch" | "session_before_branch" | "session_before_compact" | "session_before_tree" }
|
|
78
|
+
>;
|
|
79
|
+
|
|
80
|
+
type SessionBeforeEventResult =
|
|
81
|
+
| SessionBeforeSwitchResult
|
|
82
|
+
| SessionBeforeBranchResult
|
|
83
|
+
| SessionBeforeCompactResult
|
|
84
|
+
| SessionBeforeTreeResult;
|
|
85
|
+
|
|
86
|
+
type RunnerEmitResult<TEvent extends RunnerEmitEvent> = TEvent extends { type: "session_before_switch" }
|
|
87
|
+
? SessionBeforeSwitchResult | undefined
|
|
88
|
+
: TEvent extends { type: "session_before_branch" }
|
|
89
|
+
? SessionBeforeBranchResult | undefined
|
|
90
|
+
: TEvent extends { type: "session_before_compact" }
|
|
91
|
+
? SessionBeforeCompactResult | undefined
|
|
92
|
+
: TEvent extends { type: "session_before_tree" }
|
|
93
|
+
? SessionBeforeTreeResult | undefined
|
|
94
|
+
: TEvent extends { type: "session.compacting" }
|
|
95
|
+
? SessionCompactingResult | undefined
|
|
96
|
+
: undefined;
|
|
97
|
+
|
|
57
98
|
export type NewSessionHandler = (options?: {
|
|
58
99
|
parentSession?: string;
|
|
59
100
|
setup?: (sessionManager: SessionManager) => Promise<void>;
|
|
@@ -368,29 +409,18 @@ export class ExtensionRunner {
|
|
|
368
409
|
};
|
|
369
410
|
}
|
|
370
411
|
|
|
371
|
-
private isSessionBeforeEvent(
|
|
372
|
-
type: string,
|
|
373
|
-
): type is "session_before_switch" | "session_before_branch" | "session_before_compact" | "session_before_tree" {
|
|
412
|
+
private isSessionBeforeEvent(event: RunnerEmitEvent): event is SessionBeforeEvent {
|
|
374
413
|
return (
|
|
375
|
-
type === "session_before_switch" ||
|
|
376
|
-
type === "session_before_branch" ||
|
|
377
|
-
type === "session_before_compact" ||
|
|
378
|
-
type === "session_before_tree"
|
|
414
|
+
event.type === "session_before_switch" ||
|
|
415
|
+
event.type === "session_before_branch" ||
|
|
416
|
+
event.type === "session_before_compact" ||
|
|
417
|
+
event.type === "session_before_tree"
|
|
379
418
|
);
|
|
380
419
|
}
|
|
381
420
|
|
|
382
|
-
async emit(
|
|
383
|
-
event: ExtensionEvent,
|
|
384
|
-
): Promise<
|
|
385
|
-
SessionBeforeCompactResult | SessionBeforeTreeResult | SessionCompactingResult | ToolResultEventResult | undefined
|
|
386
|
-
> {
|
|
421
|
+
async emit<TEvent extends RunnerEmitEvent>(event: TEvent): Promise<RunnerEmitResult<TEvent>> {
|
|
387
422
|
const ctx = this.createContext();
|
|
388
|
-
let result:
|
|
389
|
-
| SessionBeforeCompactResult
|
|
390
|
-
| SessionBeforeTreeResult
|
|
391
|
-
| SessionCompactingResult
|
|
392
|
-
| ToolResultEventResult
|
|
393
|
-
| undefined;
|
|
423
|
+
let result: SessionBeforeEventResult | SessionCompactingResult | undefined;
|
|
394
424
|
|
|
395
425
|
for (const ext of this.extensions) {
|
|
396
426
|
const handlers = ext.handlers.get(event.type);
|
|
@@ -400,16 +430,13 @@ export class ExtensionRunner {
|
|
|
400
430
|
try {
|
|
401
431
|
const handlerResult = await handler(event, ctx);
|
|
402
432
|
|
|
403
|
-
if (this.isSessionBeforeEvent(event
|
|
404
|
-
result = handlerResult as
|
|
433
|
+
if (this.isSessionBeforeEvent(event) && handlerResult) {
|
|
434
|
+
result = handlerResult as SessionBeforeEventResult;
|
|
405
435
|
if (result.cancel) {
|
|
406
|
-
return result
|
|
436
|
+
return result as RunnerEmitResult<TEvent>;
|
|
407
437
|
}
|
|
408
438
|
}
|
|
409
439
|
|
|
410
|
-
if (event.type === "tool_result" && handlerResult) {
|
|
411
|
-
result = handlerResult as ToolResultEventResult;
|
|
412
|
-
}
|
|
413
440
|
if (event.type === "session.compacting" && handlerResult) {
|
|
414
441
|
result = handlerResult as SessionCompactingResult;
|
|
415
442
|
}
|
|
@@ -426,7 +453,55 @@ export class ExtensionRunner {
|
|
|
426
453
|
}
|
|
427
454
|
}
|
|
428
455
|
|
|
429
|
-
return result
|
|
456
|
+
return result as RunnerEmitResult<TEvent>;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async emitToolResult(event: ToolResultEvent): Promise<ToolResultEventResult | undefined> {
|
|
460
|
+
const ctx = this.createContext();
|
|
461
|
+
const currentEvent: ToolResultEvent = { ...event };
|
|
462
|
+
let modified = false;
|
|
463
|
+
|
|
464
|
+
for (const ext of this.extensions) {
|
|
465
|
+
const handlers = ext.handlers.get("tool_result");
|
|
466
|
+
if (!handlers || handlers.length === 0) continue;
|
|
467
|
+
|
|
468
|
+
for (const handler of handlers) {
|
|
469
|
+
try {
|
|
470
|
+
const handlerResult = (await handler(currentEvent, ctx)) as ToolResultEventResult | undefined;
|
|
471
|
+
if (!handlerResult) continue;
|
|
472
|
+
|
|
473
|
+
if (handlerResult.content !== undefined) {
|
|
474
|
+
currentEvent.content = handlerResult.content;
|
|
475
|
+
modified = true;
|
|
476
|
+
}
|
|
477
|
+
if (handlerResult.details !== undefined) {
|
|
478
|
+
currentEvent.details = handlerResult.details;
|
|
479
|
+
modified = true;
|
|
480
|
+
}
|
|
481
|
+
if (handlerResult.isError !== undefined) {
|
|
482
|
+
currentEvent.isError = handlerResult.isError;
|
|
483
|
+
modified = true;
|
|
484
|
+
}
|
|
485
|
+
} catch (err) {
|
|
486
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
487
|
+
const stack = err instanceof Error ? err.stack : undefined;
|
|
488
|
+
this.emitError({
|
|
489
|
+
extensionPath: ext.path,
|
|
490
|
+
event: "tool_result",
|
|
491
|
+
error: message,
|
|
492
|
+
stack,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (!modified) return undefined;
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
content: currentEvent.content,
|
|
502
|
+
details: currentEvent.details,
|
|
503
|
+
isError: currentEvent.isError,
|
|
504
|
+
};
|
|
430
505
|
}
|
|
431
506
|
|
|
432
507
|
async emitToolCall(event: ToolCallEvent): Promise<ToolCallEventResult | undefined> {
|
|
@@ -7,7 +7,7 @@ import type { Static, TSchema } from "@sinclair/typebox";
|
|
|
7
7
|
import type { Theme } from "../../modes/theme/theme";
|
|
8
8
|
import { applyToolProxy } from "../tool-proxy";
|
|
9
9
|
import type { ExtensionRunner } from "./runner";
|
|
10
|
-
import type { RegisteredTool, ToolCallEventResult
|
|
10
|
+
import type { RegisteredTool, ToolCallEventResult } from "./types";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Adapts a RegisteredTool into an AgentTool.
|
|
@@ -137,7 +137,7 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
137
137
|
|
|
138
138
|
// Emit tool_result event - extensions can modify the result and error status
|
|
139
139
|
if (this.runner.hasHandlers("tool_result")) {
|
|
140
|
-
const resultResult =
|
|
140
|
+
const resultResult = await this.runner.emitToolResult({
|
|
141
141
|
type: "tool_result",
|
|
142
142
|
toolName: this.tool.name,
|
|
143
143
|
toolCallId,
|
|
@@ -145,7 +145,7 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
145
145
|
content: result.content,
|
|
146
146
|
details: result.details,
|
|
147
147
|
isError: !!executionError,
|
|
148
|
-
})
|
|
148
|
+
});
|
|
149
149
|
|
|
150
150
|
if (resultResult) {
|
|
151
151
|
const modifiedContent: (TextContent | ImageContent)[] = resultResult.content ?? result.content;
|
|
@@ -41,6 +41,7 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
|
|
|
41
41
|
{ name: "background", description: "Detach UI and continue running in background" },
|
|
42
42
|
{ name: "debug", description: "Write debug log (TUI state and messages)" },
|
|
43
43
|
{ name: "exit", description: "Exit the application" },
|
|
44
|
+
{ name: "quit", description: "Quit the application" },
|
|
44
45
|
];
|
|
45
46
|
|
|
46
47
|
import { slashCommandCapability } from "../capability/slash-command";
|
|
@@ -84,13 +84,16 @@ export class AssistantMessageComponent extends Container {
|
|
|
84
84
|
// Set paddingY=0 to avoid extra spacing before tool executions
|
|
85
85
|
this.contentContainer.addChild(new Markdown(content.text.trim(), 1, 0, getMarkdownTheme()));
|
|
86
86
|
} else if (content.type === "thinking" && content.thinking.trim()) {
|
|
87
|
-
//
|
|
88
|
-
|
|
87
|
+
// Add spacing only when another visible assistant content block follows.
|
|
88
|
+
// This avoids a superfluous blank line before separately-rendered tool execution blocks.
|
|
89
|
+
const hasVisibleContentAfter = message.content
|
|
90
|
+
.slice(i + 1)
|
|
91
|
+
.some(c => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()));
|
|
89
92
|
|
|
90
93
|
if (this.hideThinkingBlock) {
|
|
91
94
|
// Show static "Thinking..." label when hidden
|
|
92
95
|
this.contentContainer.addChild(new Text(theme.italic(theme.fg("thinkingText", "Thinking...")), 1, 0));
|
|
93
|
-
if (
|
|
96
|
+
if (hasVisibleContentAfter) {
|
|
94
97
|
this.contentContainer.addChild(new Spacer(1));
|
|
95
98
|
}
|
|
96
99
|
} else {
|
|
@@ -101,7 +104,9 @@ export class AssistantMessageComponent extends Container {
|
|
|
101
104
|
italic: true,
|
|
102
105
|
}),
|
|
103
106
|
);
|
|
104
|
-
|
|
107
|
+
if (hasVisibleContentAfter) {
|
|
108
|
+
this.contentContainer.addChild(new Spacer(1));
|
|
109
|
+
}
|
|
105
110
|
}
|
|
106
111
|
}
|
|
107
112
|
}
|
|
@@ -2884,6 +2884,12 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2884
2884
|
this.agent.replaceMessages(messages.slice(0, -1));
|
|
2885
2885
|
}
|
|
2886
2886
|
|
|
2887
|
+
setTimeout(() => {
|
|
2888
|
+
this.agent.continue().catch(() => {});
|
|
2889
|
+
}, 100);
|
|
2890
|
+
} else if (this.agent.hasQueuedMessages()) {
|
|
2891
|
+
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
|
2892
|
+
// Kick the loop so queued messages are actually delivered.
|
|
2887
2893
|
setTimeout(() => {
|
|
2888
2894
|
this.agent.continue().catch(() => {});
|
|
2889
2895
|
}, 100);
|
package/src/task/render.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
formatMoreItems,
|
|
16
16
|
formatStatusIcon,
|
|
17
17
|
formatTokens,
|
|
18
|
+
replaceTabs,
|
|
18
19
|
truncateToWidth,
|
|
19
20
|
} from "../tools/render-utils";
|
|
20
21
|
import {
|
|
@@ -300,7 +301,7 @@ function renderOutputSection(
|
|
|
300
301
|
const outputLines = output.trimEnd().split("\n");
|
|
301
302
|
const previewCount = expanded ? maxExpanded : maxCollapsed;
|
|
302
303
|
for (const line of outputLines.slice(0, previewCount)) {
|
|
303
|
-
lines.push(`${continuePrefix} ${theme.fg("dim", truncateToWidth(line, 70))}`);
|
|
304
|
+
lines.push(`${continuePrefix} ${theme.fg("dim", truncateToWidth(replaceTabs(line), 70))}`);
|
|
304
305
|
}
|
|
305
306
|
|
|
306
307
|
if (outputLines.length > previewCount) {
|
|
@@ -344,7 +345,7 @@ function renderOutputSection(
|
|
|
344
345
|
const outputLines = output.trimEnd().split("\n");
|
|
345
346
|
const previewCount = expanded ? maxExpanded : maxCollapsed;
|
|
346
347
|
for (const line of outputLines.slice(0, previewCount)) {
|
|
347
|
-
lines.push(`${continuePrefix} ${theme.fg("dim", truncateToWidth(line, 70))}`);
|
|
348
|
+
lines.push(`${continuePrefix} ${theme.fg("dim", truncateToWidth(replaceTabs(line), 70))}`);
|
|
348
349
|
}
|
|
349
350
|
|
|
350
351
|
if (outputLines.length > previewCount) {
|
|
@@ -365,10 +366,15 @@ function renderTaskSection(
|
|
|
365
366
|
const trimmed = task.trimEnd();
|
|
366
367
|
if (!expanded || !trimmed) return lines;
|
|
367
368
|
|
|
369
|
+
// Strip the shared <swarm_context>...</swarm_context> block — it's the same
|
|
370
|
+
// across all tasks and just adds noise when expanded.
|
|
371
|
+
const stripped = trimmed.replace(/<swarm_context>[\s\S]*?<\/swarm_context>\s*/, "").trimStart();
|
|
372
|
+
if (!stripped) return lines;
|
|
373
|
+
|
|
368
374
|
lines.push(`${continuePrefix}${theme.fg("dim", "Task")}`);
|
|
369
|
-
const taskLines =
|
|
375
|
+
const taskLines = stripped.split("\n");
|
|
370
376
|
for (const line of taskLines.slice(0, maxExpanded)) {
|
|
371
|
-
lines.push(`${continuePrefix} ${theme.fg("dim", truncateToWidth(line, 70))}`);
|
|
377
|
+
lines.push(`${continuePrefix} ${theme.fg("dim", truncateToWidth(replaceTabs(line), 70))}`);
|
|
372
378
|
}
|
|
373
379
|
if (taskLines.length > maxExpanded) {
|
|
374
380
|
lines.push(`${continuePrefix} ${theme.fg("dim", formatMoreItems(taskLines.length - maxExpanded, "line"))}`);
|
|
@@ -454,7 +460,7 @@ export function renderCall(args: TaskParams, theme: Theme): Component {
|
|
|
454
460
|
if (hasContext) {
|
|
455
461
|
lines.push(` ${branch} ${theme.fg("dim", "Context")}`);
|
|
456
462
|
for (const line of context.split("\n")) {
|
|
457
|
-
const content = line ? theme.fg("muted", line) : "";
|
|
463
|
+
const content = line ? theme.fg("muted", replaceTabs(line)) : "";
|
|
458
464
|
lines.push(` ${vertical} ${content}`);
|
|
459
465
|
}
|
|
460
466
|
const taskPrefix = showIsolated ? branch : last;
|
|
@@ -635,7 +641,7 @@ function renderReviewResult(
|
|
|
635
641
|
lines.push(`${continuePrefix}${theme.fg("dim", "Summary")}`);
|
|
636
642
|
const explanationLines = summary.explanation.split("\n");
|
|
637
643
|
for (const line of explanationLines) {
|
|
638
|
-
lines.push(`${continuePrefix} ${theme.fg("dim", line)}`);
|
|
644
|
+
lines.push(`${continuePrefix} ${theme.fg("dim", replaceTabs(line))}`);
|
|
639
645
|
}
|
|
640
646
|
} else {
|
|
641
647
|
// Preview: first sentence or ~100 chars
|
|
@@ -690,7 +696,7 @@ function renderFindings(
|
|
|
690
696
|
// Wrap body text
|
|
691
697
|
const bodyLines = finding.body.split("\n");
|
|
692
698
|
for (const bodyLine of bodyLines) {
|
|
693
|
-
lines.push(`${continuePrefix}${findingContinue}${theme.fg("dim", bodyLine)}`);
|
|
699
|
+
lines.push(`${continuePrefix}${findingContinue}${theme.fg("dim", replaceTabs(bodyLine))}`);
|
|
694
700
|
}
|
|
695
701
|
}
|
|
696
702
|
}
|
package/src/tools/ask.ts
CHANGED
|
@@ -454,18 +454,11 @@ export const askToolRenderer = {
|
|
|
454
454
|
const txt = result.content[0];
|
|
455
455
|
const fallback = txt?.type === "text" && txt.text ? txt.text : "";
|
|
456
456
|
const header = renderStatusLine({ icon: "warning", title: "Ask" }, uiTheme);
|
|
457
|
-
|
|
458
|
-
return {
|
|
459
|
-
render() {
|
|
460
|
-
return renderedLines;
|
|
461
|
-
},
|
|
462
|
-
invalidate() {},
|
|
463
|
-
};
|
|
457
|
+
return new Text(`${header}\n${uiTheme.fg("dim", fallback)}`, 0, 0);
|
|
464
458
|
}
|
|
465
459
|
|
|
466
460
|
// Multi-part results
|
|
467
461
|
if (details.results && details.results.length > 0) {
|
|
468
|
-
const lines: string[] = [];
|
|
469
462
|
const hasAnySelection = details.results.some(
|
|
470
463
|
r => r.customInput || (r.selectedOptions && r.selectedOptions.length > 0),
|
|
471
464
|
);
|
|
@@ -477,7 +470,7 @@ export const askToolRenderer = {
|
|
|
477
470
|
},
|
|
478
471
|
uiTheme,
|
|
479
472
|
);
|
|
480
|
-
|
|
473
|
+
let text = header;
|
|
481
474
|
|
|
482
475
|
for (let i = 0; i < details.results.length; i++) {
|
|
483
476
|
const r = details.results[i];
|
|
@@ -489,47 +482,29 @@ export const askToolRenderer = {
|
|
|
489
482
|
? uiTheme.styledSymbol("status.success", "success")
|
|
490
483
|
: uiTheme.styledSymbol("status.warning", "warning");
|
|
491
484
|
|
|
492
|
-
|
|
493
|
-
` ${uiTheme.fg("dim", branch)} ${statusIcon} ${uiTheme.fg("dim", `[${r.id}]`)} ${uiTheme.fg("accent", r.question)}`,
|
|
494
|
-
);
|
|
485
|
+
text += `\n ${uiTheme.fg("dim", branch)} ${statusIcon} ${uiTheme.fg("dim", `[${r.id}]`)} ${uiTheme.fg("accent", r.question)}`;
|
|
495
486
|
|
|
496
487
|
if (r.customInput) {
|
|
497
|
-
|
|
498
|
-
`${continuation}${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.success", "success")} ${uiTheme.fg("toolOutput", r.customInput)}`,
|
|
499
|
-
);
|
|
488
|
+
text += `\n${continuation}${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.success", "success")} ${uiTheme.fg("toolOutput", r.customInput)}`;
|
|
500
489
|
} else if (r.selectedOptions.length > 0) {
|
|
501
490
|
for (let j = 0; j < r.selectedOptions.length; j++) {
|
|
502
491
|
const isLast = j === r.selectedOptions.length - 1;
|
|
503
492
|
const optBranch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
504
|
-
|
|
505
|
-
`${continuation}${uiTheme.fg("dim", optBranch)} ${uiTheme.fg("success", uiTheme.checkbox.checked)} ${uiTheme.fg("toolOutput", r.selectedOptions[j])}`,
|
|
506
|
-
);
|
|
493
|
+
text += `\n${continuation}${uiTheme.fg("dim", optBranch)} ${uiTheme.fg("success", uiTheme.checkbox.checked)} ${uiTheme.fg("toolOutput", r.selectedOptions[j])}`;
|
|
507
494
|
}
|
|
508
495
|
} else {
|
|
509
|
-
|
|
510
|
-
`${continuation}${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`,
|
|
511
|
-
);
|
|
496
|
+
text += `\n${continuation}${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`;
|
|
512
497
|
}
|
|
513
498
|
}
|
|
514
499
|
|
|
515
|
-
return
|
|
516
|
-
render() {
|
|
517
|
-
return lines;
|
|
518
|
-
},
|
|
519
|
-
invalidate() {},
|
|
520
|
-
};
|
|
500
|
+
return new Text(text, 0, 0);
|
|
521
501
|
}
|
|
522
502
|
|
|
523
503
|
// Single question result
|
|
524
504
|
if (!details.question) {
|
|
525
505
|
const txt = result.content[0];
|
|
526
|
-
const
|
|
527
|
-
return
|
|
528
|
-
render() {
|
|
529
|
-
return renderedLines;
|
|
530
|
-
},
|
|
531
|
-
invalidate() {},
|
|
532
|
-
};
|
|
506
|
+
const fallback = txt?.type === "text" && txt.text ? txt.text : "";
|
|
507
|
+
return new Text(fallback, 0, 0);
|
|
533
508
|
}
|
|
534
509
|
|
|
535
510
|
const hasSelection = details.customInput || (details.selectedOptions && details.selectedOptions.length > 0);
|
|
@@ -552,12 +527,6 @@ export const askToolRenderer = {
|
|
|
552
527
|
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`;
|
|
553
528
|
}
|
|
554
529
|
|
|
555
|
-
|
|
556
|
-
return {
|
|
557
|
-
render() {
|
|
558
|
-
return renderedLines;
|
|
559
|
-
},
|
|
560
|
-
invalidate() {},
|
|
561
|
-
};
|
|
530
|
+
return new Text(text, 0, 0);
|
|
562
531
|
},
|
|
563
532
|
};
|
package/src/tools/todo-write.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
|
11
11
|
import type { Theme } from "../modes/theme/theme";
|
|
12
12
|
import todoWriteDescription from "../prompts/tools/todo-write.md" with { type: "text" };
|
|
13
13
|
import type { ToolSession } from "../sdk";
|
|
14
|
-
import {
|
|
14
|
+
import { renderStatusLine, renderTreeList } from "../tui";
|
|
15
15
|
import { PREVIEW_LIMITS } from "./render-utils";
|
|
16
16
|
|
|
17
17
|
const todoWriteSchema = Type.Object({
|
|
@@ -234,39 +234,22 @@ export const todoWriteToolRenderer = {
|
|
|
234
234
|
);
|
|
235
235
|
if (todos.length === 0) {
|
|
236
236
|
const fallback = result.content?.find(c => c.type === "text")?.text ?? "No todos";
|
|
237
|
-
|
|
238
|
-
return {
|
|
239
|
-
render() {
|
|
240
|
-
return renderedLines;
|
|
241
|
-
},
|
|
242
|
-
invalidate() {},
|
|
243
|
-
};
|
|
237
|
+
return new Text(`${header}\n${uiTheme.fg("dim", fallback)}`, 0, 0);
|
|
244
238
|
}
|
|
245
|
-
let cached: RenderCache | undefined;
|
|
246
239
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
expanded,
|
|
256
|
-
maxCollapsed: PREVIEW_LIMITS.COLLAPSED_ITEMS,
|
|
257
|
-
itemType: "todo",
|
|
258
|
-
renderItem: todo => formatTodoLine(todo, uiTheme, ""),
|
|
259
|
-
},
|
|
260
|
-
uiTheme,
|
|
261
|
-
);
|
|
262
|
-
const lines = [header, ...treeLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
263
|
-
cached = { key, lines };
|
|
264
|
-
return lines;
|
|
265
|
-
},
|
|
266
|
-
invalidate() {
|
|
267
|
-
cached = undefined;
|
|
240
|
+
const { expanded } = options;
|
|
241
|
+
const treeLines = renderTreeList(
|
|
242
|
+
{
|
|
243
|
+
items: todos,
|
|
244
|
+
expanded,
|
|
245
|
+
maxCollapsed: PREVIEW_LIMITS.COLLAPSED_ITEMS,
|
|
246
|
+
itemType: "todo",
|
|
247
|
+
renderItem: todo => formatTodoLine(todo, uiTheme, ""),
|
|
268
248
|
},
|
|
269
|
-
|
|
249
|
+
uiTheme,
|
|
250
|
+
);
|
|
251
|
+
const text = [header, ...treeLines].join("\n");
|
|
252
|
+
return new Text(text, 0, 0);
|
|
270
253
|
},
|
|
271
254
|
mergeCallAndResult: true,
|
|
272
255
|
};
|