@oh-my-pi/pi-coding-agent 5.5.0 → 5.6.70
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 +105 -0
- package/docs/python-repl.md +77 -0
- package/examples/hooks/snake.ts +7 -7
- package/package.json +5 -5
- package/src/bun-imports.d.ts +6 -0
- package/src/cli/args.ts +7 -0
- package/src/cli/setup-cli.ts +231 -0
- package/src/cli.ts +2 -0
- package/src/core/agent-session.ts +118 -15
- package/src/core/bash-executor.ts +3 -84
- package/src/core/compaction/compaction.ts +10 -5
- package/src/core/extensions/index.ts +2 -0
- package/src/core/extensions/loader.ts +13 -1
- package/src/core/extensions/runner.ts +50 -2
- package/src/core/extensions/types.ts +67 -2
- package/src/core/keybindings.ts +51 -1
- package/src/core/prompt-templates.ts +15 -0
- package/src/core/python-executor-display.test.ts +42 -0
- package/src/core/python-executor-lifecycle.test.ts +99 -0
- package/src/core/python-executor-mapping.test.ts +41 -0
- package/src/core/python-executor-per-call.test.ts +49 -0
- package/src/core/python-executor-session.test.ts +103 -0
- package/src/core/python-executor-streaming.test.ts +77 -0
- package/src/core/python-executor-timeout.test.ts +35 -0
- package/src/core/python-executor.lifecycle.test.ts +139 -0
- package/src/core/python-executor.result.test.ts +49 -0
- package/src/core/python-executor.test.ts +180 -0
- package/src/core/python-executor.ts +313 -0
- package/src/core/python-gateway-coordinator.ts +832 -0
- package/src/core/python-kernel-display.test.ts +54 -0
- package/src/core/python-kernel-env.test.ts +138 -0
- package/src/core/python-kernel-session.test.ts +87 -0
- package/src/core/python-kernel-ws.test.ts +104 -0
- package/src/core/python-kernel.lifecycle.test.ts +249 -0
- package/src/core/python-kernel.test.ts +461 -0
- package/src/core/python-kernel.ts +1182 -0
- package/src/core/python-modules.test.ts +102 -0
- package/src/core/python-modules.ts +110 -0
- package/src/core/python-prelude.py +889 -0
- package/src/core/python-prelude.test.ts +140 -0
- package/src/core/python-prelude.ts +3 -0
- package/src/core/sdk.ts +24 -6
- package/src/core/session-manager.ts +174 -82
- package/src/core/settings-manager-python.test.ts +23 -0
- package/src/core/settings-manager.ts +202 -0
- package/src/core/streaming-output.test.ts +26 -0
- package/src/core/streaming-output.ts +100 -0
- package/src/core/system-prompt.python.test.ts +17 -0
- package/src/core/system-prompt.ts +3 -1
- package/src/core/timings.ts +1 -1
- package/src/core/tools/bash.ts +13 -2
- package/src/core/tools/edit-diff.ts +9 -1
- package/src/core/tools/index.test.ts +50 -23
- package/src/core/tools/index.ts +83 -1
- package/src/core/tools/python-execution.test.ts +68 -0
- package/src/core/tools/python-fallback.test.ts +72 -0
- package/src/core/tools/python-renderer.test.ts +36 -0
- package/src/core/tools/python-tool-mode.test.ts +43 -0
- package/src/core/tools/python.test.ts +121 -0
- package/src/core/tools/python.ts +760 -0
- package/src/core/tools/renderers.ts +2 -0
- package/src/core/tools/schema-validation.test.ts +1 -0
- package/src/core/tools/task/executor.ts +146 -3
- package/src/core/tools/task/worker-protocol.ts +32 -2
- package/src/core/tools/task/worker.ts +182 -15
- package/src/index.ts +6 -0
- package/src/main.ts +136 -40
- package/src/modes/interactive/components/custom-editor.ts +16 -31
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +5 -16
- package/src/modes/interactive/components/extensions/extension-list.ts +5 -13
- package/src/modes/interactive/components/history-search.ts +5 -8
- package/src/modes/interactive/components/hook-editor.ts +3 -4
- package/src/modes/interactive/components/hook-input.ts +3 -3
- package/src/modes/interactive/components/hook-selector.ts +5 -15
- package/src/modes/interactive/components/index.ts +1 -0
- package/src/modes/interactive/components/keybinding-hints.ts +66 -0
- package/src/modes/interactive/components/model-selector.ts +53 -66
- package/src/modes/interactive/components/oauth-selector.ts +5 -5
- package/src/modes/interactive/components/session-selector.ts +29 -23
- package/src/modes/interactive/components/settings-defs.ts +404 -196
- package/src/modes/interactive/components/settings-selector.ts +14 -10
- package/src/modes/interactive/components/status-line-segment-editor.ts +7 -7
- package/src/modes/interactive/components/tool-execution.ts +8 -0
- package/src/modes/interactive/components/tree-selector.ts +29 -23
- package/src/modes/interactive/components/user-message-selector.ts +6 -17
- package/src/modes/interactive/controllers/command-controller.ts +86 -37
- package/src/modes/interactive/controllers/event-controller.ts +8 -0
- package/src/modes/interactive/controllers/extension-ui-controller.ts +51 -0
- package/src/modes/interactive/controllers/input-controller.ts +42 -6
- package/src/modes/interactive/interactive-mode.ts +56 -30
- package/src/modes/interactive/theme/theme-schema.json +2 -2
- package/src/modes/interactive/types.ts +6 -1
- package/src/modes/interactive/utils/ui-helpers.ts +2 -1
- package/src/modes/print-mode.ts +23 -0
- package/src/modes/rpc/rpc-mode.ts +21 -0
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/system/system-prompt.md +32 -1
- package/src/prompts/tools/python.md +91 -0
|
@@ -46,6 +46,10 @@ export interface TerminalSettings {
|
|
|
46
46
|
showImages?: boolean; // default: true (only relevant if terminal supports images)
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export interface StartupSettings {
|
|
50
|
+
quiet?: boolean; // default: false - suppress welcome screen and startup info
|
|
51
|
+
}
|
|
52
|
+
|
|
49
53
|
export interface ImageSettings {
|
|
50
54
|
autoResize?: boolean; // default: true (resize images to 2000x2000 max for better model compatibility)
|
|
51
55
|
blockImages?: boolean; // default: false - when true, prevents all images from being sent to LLM providers
|
|
@@ -108,6 +112,15 @@ export interface LspSettings {
|
|
|
108
112
|
diagnosticsOnEdit?: boolean; // default: false (return LSP diagnostics after edit tool edits code files)
|
|
109
113
|
}
|
|
110
114
|
|
|
115
|
+
export type PythonToolMode = "ipy-only" | "bash-only" | "both";
|
|
116
|
+
export type PythonKernelMode = "session" | "per-call";
|
|
117
|
+
|
|
118
|
+
export interface PythonSettings {
|
|
119
|
+
toolMode?: PythonToolMode;
|
|
120
|
+
kernelMode?: PythonKernelMode;
|
|
121
|
+
sharedGateway?: boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
111
124
|
export interface EditSettings {
|
|
112
125
|
fuzzyMatch?: boolean; // default: true (accept high-confidence fuzzy matches for whitespace/indentation)
|
|
113
126
|
}
|
|
@@ -194,6 +207,7 @@ export interface Settings {
|
|
|
194
207
|
hideThinkingBlock?: boolean;
|
|
195
208
|
shellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)
|
|
196
209
|
collapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full)
|
|
210
|
+
startup?: StartupSettings;
|
|
197
211
|
doubleEscapeAction?: "branch" | "tree"; // Action for double-escape with empty editor (default: "tree")
|
|
198
212
|
thinkingBudgets?: ThinkingBudgetsSettings; // Custom token budgets for thinking levels
|
|
199
213
|
/** Environment variables to set automatically on startup */
|
|
@@ -210,6 +224,7 @@ export interface Settings {
|
|
|
210
224
|
git?: GitSettings;
|
|
211
225
|
mcp?: MCPSettings;
|
|
212
226
|
lsp?: LspSettings;
|
|
227
|
+
python?: PythonSettings;
|
|
213
228
|
edit?: EditSettings;
|
|
214
229
|
ttsr?: TtsrSettings;
|
|
215
230
|
todoCompletion?: TodoCompletionSettings;
|
|
@@ -218,6 +233,7 @@ export interface Settings {
|
|
|
218
233
|
disabledProviders?: string[]; // Discovery provider IDs that are disabled
|
|
219
234
|
disabledExtensions?: string[]; // Individual extension IDs that are disabled (e.g., "skill:commit")
|
|
220
235
|
statusLine?: StatusLineSettings; // Status line configuration
|
|
236
|
+
showHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME
|
|
221
237
|
}
|
|
222
238
|
|
|
223
239
|
export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [
|
|
@@ -301,6 +317,7 @@ const DEFAULT_SETTINGS: Settings = {
|
|
|
301
317
|
git: { enabled: false },
|
|
302
318
|
mcp: { enableProjectConfig: true },
|
|
303
319
|
lsp: { formatOnWrite: false, diagnosticsOnWrite: true, diagnosticsOnEdit: false },
|
|
320
|
+
python: { toolMode: "both", kernelMode: "session", sharedGateway: true },
|
|
304
321
|
edit: { fuzzyMatch: true },
|
|
305
322
|
ttsr: { enabled: true, contextMode: "discard", repeatMode: "once", repeatGap: 10 },
|
|
306
323
|
voice: {
|
|
@@ -377,6 +394,25 @@ function normalizeSettings(settings: Settings): Settings {
|
|
|
377
394
|
...merged,
|
|
378
395
|
symbolPreset,
|
|
379
396
|
bashInterceptor: normalizeBashInterceptorSettings(merged.bashInterceptor),
|
|
397
|
+
python: normalizePythonSettings(merged.python),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function normalizePythonSettings(settings: PythonSettings | undefined): PythonSettings {
|
|
402
|
+
const toolMode = settings?.toolMode;
|
|
403
|
+
const kernelMode = settings?.kernelMode;
|
|
404
|
+
const sharedGateway = settings?.sharedGateway;
|
|
405
|
+
return {
|
|
406
|
+
toolMode:
|
|
407
|
+
toolMode === "ipy-only" || toolMode === "bash-only" || toolMode === "both"
|
|
408
|
+
? toolMode
|
|
409
|
+
: (DEFAULT_SETTINGS.python?.toolMode ?? "both"),
|
|
410
|
+
kernelMode:
|
|
411
|
+
kernelMode === "session" || kernelMode === "per-call"
|
|
412
|
+
? kernelMode
|
|
413
|
+
: (DEFAULT_SETTINGS.python?.kernelMode ?? "session"),
|
|
414
|
+
sharedGateway:
|
|
415
|
+
typeof sharedGateway === "boolean" ? sharedGateway : (DEFAULT_SETTINGS.python?.sharedGateway ?? true),
|
|
380
416
|
};
|
|
381
417
|
}
|
|
382
418
|
|
|
@@ -781,6 +817,30 @@ export class SettingsManager {
|
|
|
781
817
|
};
|
|
782
818
|
}
|
|
783
819
|
|
|
820
|
+
getRetryMaxRetries(): number {
|
|
821
|
+
return this.settings.retry?.maxRetries ?? 3;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
async setRetryMaxRetries(maxRetries: number): Promise<void> {
|
|
825
|
+
if (!this.globalSettings.retry) {
|
|
826
|
+
this.globalSettings.retry = {};
|
|
827
|
+
}
|
|
828
|
+
this.globalSettings.retry.maxRetries = maxRetries;
|
|
829
|
+
await this.save();
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
getRetryBaseDelayMs(): number {
|
|
833
|
+
return this.settings.retry?.baseDelayMs ?? 2000;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
async setRetryBaseDelayMs(baseDelayMs: number): Promise<void> {
|
|
837
|
+
if (!this.globalSettings.retry) {
|
|
838
|
+
this.globalSettings.retry = {};
|
|
839
|
+
}
|
|
840
|
+
this.globalSettings.retry.baseDelayMs = baseDelayMs;
|
|
841
|
+
await this.save();
|
|
842
|
+
}
|
|
843
|
+
|
|
784
844
|
getTodoCompletionSettings(): { enabled: boolean; maxReminders: number } {
|
|
785
845
|
return {
|
|
786
846
|
enabled: this.settings.todoCompletion?.enabled ?? false,
|
|
@@ -843,6 +903,18 @@ export class SettingsManager {
|
|
|
843
903
|
await this.save();
|
|
844
904
|
}
|
|
845
905
|
|
|
906
|
+
getStartupQuiet(): boolean {
|
|
907
|
+
return this.settings.startup?.quiet ?? false;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
async setStartupQuiet(quiet: boolean): Promise<void> {
|
|
911
|
+
if (!this.globalSettings.startup) {
|
|
912
|
+
this.globalSettings.startup = {};
|
|
913
|
+
}
|
|
914
|
+
this.globalSettings.startup.quiet = quiet;
|
|
915
|
+
await this.save();
|
|
916
|
+
}
|
|
917
|
+
|
|
846
918
|
getExtensionPaths(): string[] {
|
|
847
919
|
return [...(this.settings.extensions ?? [])];
|
|
848
920
|
}
|
|
@@ -883,6 +955,14 @@ export class SettingsManager {
|
|
|
883
955
|
return this.settings.skills?.enableSkillCommands ?? true;
|
|
884
956
|
}
|
|
885
957
|
|
|
958
|
+
async setEnableSkillCommands(enabled: boolean): Promise<void> {
|
|
959
|
+
if (!this.globalSettings.skills) {
|
|
960
|
+
this.globalSettings.skills = {};
|
|
961
|
+
}
|
|
962
|
+
this.globalSettings.skills.enableSkillCommands = enabled;
|
|
963
|
+
await this.save();
|
|
964
|
+
}
|
|
965
|
+
|
|
886
966
|
getCommandsSettings(): Required<CommandsSettings> {
|
|
887
967
|
return {
|
|
888
968
|
enableClaudeUser: this.settings.commands?.enableClaudeUser ?? true,
|
|
@@ -890,6 +970,30 @@ export class SettingsManager {
|
|
|
890
970
|
};
|
|
891
971
|
}
|
|
892
972
|
|
|
973
|
+
getCommandsEnableClaudeUser(): boolean {
|
|
974
|
+
return this.settings.commands?.enableClaudeUser ?? true;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
async setCommandsEnableClaudeUser(enabled: boolean): Promise<void> {
|
|
978
|
+
if (!this.globalSettings.commands) {
|
|
979
|
+
this.globalSettings.commands = {};
|
|
980
|
+
}
|
|
981
|
+
this.globalSettings.commands.enableClaudeUser = enabled;
|
|
982
|
+
await this.save();
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
getCommandsEnableClaudeProject(): boolean {
|
|
986
|
+
return this.settings.commands?.enableClaudeProject ?? true;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
async setCommandsEnableClaudeProject(enabled: boolean): Promise<void> {
|
|
990
|
+
if (!this.globalSettings.commands) {
|
|
991
|
+
this.globalSettings.commands = {};
|
|
992
|
+
}
|
|
993
|
+
this.globalSettings.commands.enableClaudeProject = enabled;
|
|
994
|
+
await this.save();
|
|
995
|
+
}
|
|
996
|
+
|
|
893
997
|
getShowImages(): boolean {
|
|
894
998
|
return this.settings.terminal?.showImages ?? true;
|
|
895
999
|
}
|
|
@@ -1046,10 +1150,54 @@ export class SettingsManager {
|
|
|
1046
1150
|
await this.save();
|
|
1047
1151
|
}
|
|
1048
1152
|
|
|
1153
|
+
async setBashInterceptorSimpleLsEnabled(enabled: boolean): Promise<void> {
|
|
1154
|
+
if (!this.globalSettings.bashInterceptor) {
|
|
1155
|
+
this.globalSettings.bashInterceptor = {};
|
|
1156
|
+
}
|
|
1157
|
+
this.globalSettings.bashInterceptor.simpleLs = enabled;
|
|
1158
|
+
await this.save();
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1049
1161
|
getGitToolEnabled(): boolean {
|
|
1050
1162
|
return this.settings.git?.enabled ?? false;
|
|
1051
1163
|
}
|
|
1052
1164
|
|
|
1165
|
+
getPythonToolMode(): PythonToolMode {
|
|
1166
|
+
return this.settings.python?.toolMode ?? "both";
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
async setPythonToolMode(mode: PythonToolMode): Promise<void> {
|
|
1170
|
+
if (!this.globalSettings.python) {
|
|
1171
|
+
this.globalSettings.python = {};
|
|
1172
|
+
}
|
|
1173
|
+
this.globalSettings.python.toolMode = mode;
|
|
1174
|
+
await this.save();
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
getPythonKernelMode(): PythonKernelMode {
|
|
1178
|
+
return this.settings.python?.kernelMode ?? "session";
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
async setPythonKernelMode(mode: PythonKernelMode): Promise<void> {
|
|
1182
|
+
if (!this.globalSettings.python) {
|
|
1183
|
+
this.globalSettings.python = {};
|
|
1184
|
+
}
|
|
1185
|
+
this.globalSettings.python.kernelMode = mode;
|
|
1186
|
+
await this.save();
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
getPythonSharedGateway(): boolean {
|
|
1190
|
+
return this.settings.python?.sharedGateway ?? true;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
async setPythonSharedGateway(enabled: boolean): Promise<void> {
|
|
1194
|
+
if (!this.globalSettings.python) {
|
|
1195
|
+
this.globalSettings.python = {};
|
|
1196
|
+
}
|
|
1197
|
+
this.globalSettings.python.sharedGateway = enabled;
|
|
1198
|
+
await this.save();
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1053
1201
|
async setGitToolEnabled(enabled: boolean): Promise<void> {
|
|
1054
1202
|
if (!this.globalSettings.git) {
|
|
1055
1203
|
this.globalSettings.git = {};
|
|
@@ -1244,6 +1392,42 @@ export class SettingsManager {
|
|
|
1244
1392
|
await this.save();
|
|
1245
1393
|
}
|
|
1246
1394
|
|
|
1395
|
+
getVoiceTtsModel(): string {
|
|
1396
|
+
return this.settings.voice?.ttsModel ?? "gpt-4o-mini-tts";
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
async setVoiceTtsModel(model: string): Promise<void> {
|
|
1400
|
+
if (!this.globalSettings.voice) {
|
|
1401
|
+
this.globalSettings.voice = {};
|
|
1402
|
+
}
|
|
1403
|
+
this.globalSettings.voice.ttsModel = model;
|
|
1404
|
+
await this.save();
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
getVoiceTtsVoice(): string {
|
|
1408
|
+
return this.settings.voice?.ttsVoice ?? "alloy";
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
async setVoiceTtsVoice(voice: string): Promise<void> {
|
|
1412
|
+
if (!this.globalSettings.voice) {
|
|
1413
|
+
this.globalSettings.voice = {};
|
|
1414
|
+
}
|
|
1415
|
+
this.globalSettings.voice.ttsVoice = voice;
|
|
1416
|
+
await this.save();
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
getVoiceTtsFormat(): "wav" | "mp3" | "opus" | "aac" | "flac" {
|
|
1420
|
+
return this.settings.voice?.ttsFormat ?? "wav";
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
async setVoiceTtsFormat(format: "wav" | "mp3" | "opus" | "aac" | "flac"): Promise<void> {
|
|
1424
|
+
if (!this.globalSettings.voice) {
|
|
1425
|
+
this.globalSettings.voice = {};
|
|
1426
|
+
}
|
|
1427
|
+
this.globalSettings.voice.ttsFormat = format;
|
|
1428
|
+
await this.save();
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1247
1431
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1248
1432
|
// Status Line Settings
|
|
1249
1433
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -1374,6 +1558,24 @@ export class SettingsManager {
|
|
|
1374
1558
|
await this.save();
|
|
1375
1559
|
}
|
|
1376
1560
|
|
|
1561
|
+
getShowHardwareCursor(): boolean {
|
|
1562
|
+
// Check settings first
|
|
1563
|
+
if (this.settings.showHardwareCursor !== undefined) {
|
|
1564
|
+
return this.settings.showHardwareCursor;
|
|
1565
|
+
}
|
|
1566
|
+
// Check env var override
|
|
1567
|
+
const envVar = process.env.OMP_HARDWARE_CURSOR?.toLowerCase();
|
|
1568
|
+
if (envVar === "0" || envVar === "false") return false;
|
|
1569
|
+
if (envVar === "1" || envVar === "true") return true;
|
|
1570
|
+
// Default to true on Linux/macOS for IME support
|
|
1571
|
+
return process.platform === "linux" || process.platform === "darwin";
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
async setShowHardwareCursor(show: boolean): Promise<void> {
|
|
1575
|
+
this.globalSettings.showHardwareCursor = show;
|
|
1576
|
+
await this.save();
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1377
1579
|
/**
|
|
1378
1580
|
* Get environment variables from settings
|
|
1379
1581
|
*/
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { createOutputSink } from "./streaming-output";
|
|
3
|
+
|
|
4
|
+
function makeLargeOutput(size: number): string {
|
|
5
|
+
return "x".repeat(size);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
describe("createOutputSink", () => {
|
|
9
|
+
it("spills to disk and truncates large output", async () => {
|
|
10
|
+
const largeOutput = makeLargeOutput(60_000);
|
|
11
|
+
const sink = createOutputSink(10, 70_000);
|
|
12
|
+
const writer = sink.getWriter();
|
|
13
|
+
|
|
14
|
+
await writer.write(largeOutput);
|
|
15
|
+
await writer.close();
|
|
16
|
+
|
|
17
|
+
const result = sink.dump();
|
|
18
|
+
|
|
19
|
+
expect(result.truncated).toBe(true);
|
|
20
|
+
expect(result.fullOutputPath).toBeDefined();
|
|
21
|
+
expect(result.output.length).toBeLessThan(largeOutput.length);
|
|
22
|
+
|
|
23
|
+
const fullOutput = await Bun.file(result.fullOutputPath!).text();
|
|
24
|
+
expect(fullOutput).toBe(largeOutput);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { tmpdir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { nanoid } from "nanoid";
|
|
4
|
+
import stripAnsi from "strip-ansi";
|
|
5
|
+
import { sanitizeBinaryOutput } from "../utils/shell";
|
|
6
|
+
import { truncateTail } from "./tools/truncate";
|
|
7
|
+
|
|
8
|
+
interface OutputFileSink {
|
|
9
|
+
write(data: string): number | Promise<number>;
|
|
10
|
+
end(): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createSanitizer(): TransformStream<Uint8Array, string> {
|
|
14
|
+
const decoder = new TextDecoder();
|
|
15
|
+
const sanitizeText = (text: string) => sanitizeBinaryOutput(stripAnsi(text)).replace(/\r/g, "");
|
|
16
|
+
return new TransformStream({
|
|
17
|
+
transform(chunk, controller) {
|
|
18
|
+
const text = sanitizeText(decoder.decode(chunk, { stream: true }));
|
|
19
|
+
if (text) {
|
|
20
|
+
controller.enqueue(text);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
flush(controller) {
|
|
24
|
+
const text = sanitizeText(decoder.decode());
|
|
25
|
+
if (text) {
|
|
26
|
+
controller.enqueue(text);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function pumpStream(readable: ReadableStream<Uint8Array>, writer: WritableStreamDefaultWriter<string>) {
|
|
33
|
+
const reader = readable.pipeThrough(createSanitizer()).getReader();
|
|
34
|
+
try {
|
|
35
|
+
while (true) {
|
|
36
|
+
const { done, value } = await reader.read();
|
|
37
|
+
if (done) break;
|
|
38
|
+
await writer.write(value);
|
|
39
|
+
}
|
|
40
|
+
} finally {
|
|
41
|
+
reader.releaseLock();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createOutputSink(
|
|
46
|
+
spillThreshold: number,
|
|
47
|
+
maxBuffer: number,
|
|
48
|
+
onChunk?: (text: string) => void,
|
|
49
|
+
): WritableStream<string> & {
|
|
50
|
+
dump: (annotation?: string) => { output: string; truncated: boolean; fullOutputPath?: string };
|
|
51
|
+
} {
|
|
52
|
+
const chunks: Array<{ text: string; bytes: number }> = [];
|
|
53
|
+
let chunkBytes = 0;
|
|
54
|
+
let totalBytes = 0;
|
|
55
|
+
let fullOutputPath: string | undefined;
|
|
56
|
+
let fullOutputStream: OutputFileSink | undefined;
|
|
57
|
+
|
|
58
|
+
const sink = new WritableStream<string>({
|
|
59
|
+
write(text) {
|
|
60
|
+
const bytes = Buffer.byteLength(text, "utf-8");
|
|
61
|
+
totalBytes += bytes;
|
|
62
|
+
|
|
63
|
+
if (totalBytes > spillThreshold && !fullOutputPath) {
|
|
64
|
+
fullOutputPath = join(tmpdir(), `omp-${nanoid()}.buffer`);
|
|
65
|
+
const stream = Bun.file(fullOutputPath).writer();
|
|
66
|
+
for (const chunk of chunks) {
|
|
67
|
+
stream.write(chunk.text);
|
|
68
|
+
}
|
|
69
|
+
fullOutputStream = stream;
|
|
70
|
+
}
|
|
71
|
+
fullOutputStream?.write(text);
|
|
72
|
+
|
|
73
|
+
chunks.push({ text, bytes });
|
|
74
|
+
chunkBytes += bytes;
|
|
75
|
+
while (chunkBytes > maxBuffer && chunks.length > 1) {
|
|
76
|
+
const removed = chunks.shift();
|
|
77
|
+
if (removed) {
|
|
78
|
+
chunkBytes -= removed.bytes;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
onChunk?.(text);
|
|
83
|
+
},
|
|
84
|
+
close() {
|
|
85
|
+
fullOutputStream?.end();
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return Object.assign(sink, {
|
|
90
|
+
dump(annotation?: string) {
|
|
91
|
+
if (annotation) {
|
|
92
|
+
const text = `\n\n${annotation}`;
|
|
93
|
+
chunks.push({ text, bytes: Buffer.byteLength(text, "utf-8") });
|
|
94
|
+
}
|
|
95
|
+
const full = chunks.map((chunk) => chunk.text).join("");
|
|
96
|
+
const { content, truncated } = truncateTail(full);
|
|
97
|
+
return { output: truncated ? content : full, truncated, fullOutputPath };
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { buildSystemPrompt } from "./system-prompt";
|
|
3
|
+
|
|
4
|
+
describe("buildSystemPrompt", () => {
|
|
5
|
+
it("includes python tool details when enabled", async () => {
|
|
6
|
+
const prompt = await buildSystemPrompt({
|
|
7
|
+
cwd: "/tmp",
|
|
8
|
+
toolNames: ["python"],
|
|
9
|
+
contextFiles: [],
|
|
10
|
+
skills: [],
|
|
11
|
+
rules: [],
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
expect(prompt).toContain("python: Execute Python code via a session-backed IPython kernel");
|
|
15
|
+
expect(prompt).toContain("What python IS for");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -76,6 +76,7 @@ const toolDescriptions: Record<ToolName, string> = {
|
|
|
76
76
|
ask: "Ask user for input or clarification",
|
|
77
77
|
read: "Read file contents",
|
|
78
78
|
bash: "Execute bash commands (npm, docker, etc.)",
|
|
79
|
+
python: "Execute Python code via a session-backed IPython kernel",
|
|
79
80
|
calc: "{ calculations: array of { expression: string, prefix: string, suffix: string } } Basic calculations.",
|
|
80
81
|
ssh: "Execute commands on remote hosts via SSH",
|
|
81
82
|
edit: "Make surgical edits to files (find exact text and replace)",
|
|
@@ -672,7 +673,8 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
672
673
|
|
|
673
674
|
// Build tool descriptions array
|
|
674
675
|
// Priority: toolNames (explicit list) > tools (Map) > defaults
|
|
675
|
-
|
|
676
|
+
// Default includes both bash and python; actual availability determined by settings in createTools
|
|
677
|
+
const defaultToolNames: ToolName[] = ["read", "bash", "python", "edit", "write"];
|
|
676
678
|
let toolNamesArray: string[];
|
|
677
679
|
if (toolNames !== undefined) {
|
|
678
680
|
// Explicit toolNames list provided (could be empty)
|
package/src/core/timings.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Enable with OMP_TIMING=1 or PI_TIMING=1 environment variable.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const ENABLED = process.env.OMP_TIMING === "1"
|
|
6
|
+
const ENABLED = process.env.OMP_TIMING === "1";
|
|
7
7
|
const timings: Array<{ label: string; ms: number }> = [];
|
|
8
8
|
let lastTime = Date.now();
|
|
9
9
|
|
package/src/core/tools/bash.ts
CHANGED
|
@@ -178,6 +178,8 @@ interface BashRenderContext {
|
|
|
178
178
|
expanded?: boolean;
|
|
179
179
|
/** Number of preview lines when collapsed */
|
|
180
180
|
previewLines?: number;
|
|
181
|
+
/** Timeout in seconds */
|
|
182
|
+
timeout?: number;
|
|
181
183
|
}
|
|
182
184
|
|
|
183
185
|
// Preview line limit when not expanded (matches tool-execution behavior)
|
|
@@ -236,6 +238,11 @@ export const bashToolRenderer = {
|
|
|
236
238
|
// Build truncation warning lines (static, doesn't depend on width)
|
|
237
239
|
const truncation = details?.truncation;
|
|
238
240
|
const fullOutputPath = details?.fullOutputPath;
|
|
241
|
+
const timeoutSeconds = renderContext?.timeout;
|
|
242
|
+
const timeoutLine =
|
|
243
|
+
typeof timeoutSeconds === "number"
|
|
244
|
+
? uiTheme.fg("dim", ui.wrapBrackets(`Timeout: ${timeoutSeconds}s`))
|
|
245
|
+
: undefined;
|
|
239
246
|
let warningLine: string | undefined;
|
|
240
247
|
if (fullOutputPath || (truncation?.truncated && !showingFullOutput)) {
|
|
241
248
|
const warnings: string[] = [];
|
|
@@ -258,7 +265,8 @@ export const bashToolRenderer = {
|
|
|
258
265
|
|
|
259
266
|
if (!displayOutput) {
|
|
260
267
|
// No output - just show warning if any
|
|
261
|
-
|
|
268
|
+
const lines = [timeoutLine, warningLine].filter(Boolean) as string[];
|
|
269
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
262
270
|
}
|
|
263
271
|
|
|
264
272
|
if (expanded) {
|
|
@@ -267,7 +275,7 @@ export const bashToolRenderer = {
|
|
|
267
275
|
.split("\n")
|
|
268
276
|
.map((line) => uiTheme.fg("toolOutput", line))
|
|
269
277
|
.join("\n");
|
|
270
|
-
const lines =
|
|
278
|
+
const lines = [styledOutput, timeoutLine, warningLine].filter(Boolean) as string[];
|
|
271
279
|
return new Text(lines.join("\n"), 0, 0);
|
|
272
280
|
}
|
|
273
281
|
|
|
@@ -300,6 +308,9 @@ export const bashToolRenderer = {
|
|
|
300
308
|
outputLines.push(truncateToWidth(skippedLine, width, uiTheme.fg("dim", uiTheme.format.ellipsis)));
|
|
301
309
|
}
|
|
302
310
|
outputLines.push(...cachedLines);
|
|
311
|
+
if (timeoutLine) {
|
|
312
|
+
outputLines.push(truncateToWidth(timeoutLine, width, uiTheme.fg("dim", uiTheme.format.ellipsis)));
|
|
313
|
+
}
|
|
303
314
|
if (warningLine) {
|
|
304
315
|
outputLines.push(truncateToWidth(warningLine, width, uiTheme.fg("warning", uiTheme.format.ellipsis)));
|
|
305
316
|
}
|
|
@@ -80,6 +80,13 @@ function computeRelativeIndentDepths(lines: string[]): number[] {
|
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
function normalizeFuzzyText(text: string): string {
|
|
84
|
+
return text
|
|
85
|
+
.replace(/[“”„‟«»]/g, '"')
|
|
86
|
+
.replace(/[‘’‚‛`´]/g, "'")
|
|
87
|
+
.replace(/[‐‑‒–—−]/g, "-");
|
|
88
|
+
}
|
|
89
|
+
|
|
83
90
|
function normalizeLinesForMatch(lines: string[], includeDepth = true): string[] {
|
|
84
91
|
const indentDepths = includeDepth ? computeRelativeIndentDepths(lines) : null;
|
|
85
92
|
return lines.map((line, index) => {
|
|
@@ -88,7 +95,8 @@ function normalizeLinesForMatch(lines: string[], includeDepth = true): string[]
|
|
|
88
95
|
if (trimmed.length === 0) {
|
|
89
96
|
return prefix;
|
|
90
97
|
}
|
|
91
|
-
const
|
|
98
|
+
const normalized = normalizeFuzzyText(trimmed);
|
|
99
|
+
const collapsed = normalized.replace(/[ \t]+/g, " ");
|
|
92
100
|
return `${prefix}${collapsed}`;
|
|
93
101
|
});
|
|
94
102
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { BUILTIN_TOOLS, createTools, HIDDEN_TOOLS, type ToolSession } from "./index";
|
|
3
3
|
|
|
4
|
+
process.env.OMP_PYTHON_SKIP_CHECK = "1";
|
|
5
|
+
|
|
4
6
|
function createTestSession(overrides: Partial<ToolSession> = {}): ToolSession {
|
|
5
7
|
return {
|
|
6
8
|
cwd: "/tmp/test",
|
|
@@ -11,6 +13,21 @@ function createTestSession(overrides: Partial<ToolSession> = {}): ToolSession {
|
|
|
11
13
|
};
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
function createBaseSettings(overrides: Partial<NonNullable<ToolSession["settings"]>> = {}) {
|
|
17
|
+
return {
|
|
18
|
+
getImageAutoResize: () => true,
|
|
19
|
+
getLspFormatOnWrite: () => true,
|
|
20
|
+
getLspDiagnosticsOnWrite: () => true,
|
|
21
|
+
getLspDiagnosticsOnEdit: () => false,
|
|
22
|
+
getEditFuzzyMatch: () => true,
|
|
23
|
+
getGitToolEnabled: () => true,
|
|
24
|
+
getBashInterceptorEnabled: () => true,
|
|
25
|
+
getBashInterceptorSimpleLsEnabled: () => true,
|
|
26
|
+
getBashInterceptorRules: () => [],
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
describe("createTools", () => {
|
|
15
32
|
it("creates all builtin tools by default", async () => {
|
|
16
33
|
const session = createTestSession();
|
|
@@ -18,7 +35,8 @@ describe("createTools", () => {
|
|
|
18
35
|
const names = tools.map((t) => t.name);
|
|
19
36
|
|
|
20
37
|
// Core tools should always be present
|
|
21
|
-
expect(names).toContain("
|
|
38
|
+
expect(names).toContain("python");
|
|
39
|
+
expect(names).not.toContain("bash");
|
|
22
40
|
expect(names).toContain("calc");
|
|
23
41
|
expect(names).toContain("read");
|
|
24
42
|
expect(names).toContain("edit");
|
|
@@ -35,6 +53,34 @@ describe("createTools", () => {
|
|
|
35
53
|
expect(names).toContain("web_search");
|
|
36
54
|
});
|
|
37
55
|
|
|
56
|
+
it("includes bash and python when python mode is both", async () => {
|
|
57
|
+
const session = createTestSession({
|
|
58
|
+
settings: createBaseSettings({
|
|
59
|
+
getPythonToolMode: () => "both",
|
|
60
|
+
getPythonKernelMode: () => "session",
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
const tools = await createTools(session);
|
|
64
|
+
const names = tools.map((t) => t.name);
|
|
65
|
+
|
|
66
|
+
expect(names).toContain("bash");
|
|
67
|
+
expect(names).toContain("python");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("includes bash only when python mode is bash-only", async () => {
|
|
71
|
+
const session = createTestSession({
|
|
72
|
+
settings: createBaseSettings({
|
|
73
|
+
getPythonToolMode: () => "bash-only",
|
|
74
|
+
getPythonKernelMode: () => "session",
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
const tools = await createTools(session);
|
|
78
|
+
const names = tools.map((t) => t.name);
|
|
79
|
+
|
|
80
|
+
expect(names).toContain("bash");
|
|
81
|
+
expect(names).not.toContain("python");
|
|
82
|
+
});
|
|
83
|
+
|
|
38
84
|
it("excludes lsp tool when session disables LSP", async () => {
|
|
39
85
|
const session = createTestSession({ enableLsp: false });
|
|
40
86
|
const tools = await createTools(session, ["read", "lsp", "write"]);
|
|
@@ -93,17 +139,7 @@ describe("createTools", () => {
|
|
|
93
139
|
|
|
94
140
|
it("excludes git tool when disabled in settings", async () => {
|
|
95
141
|
const session = createTestSession({
|
|
96
|
-
settings: {
|
|
97
|
-
getImageAutoResize: () => true,
|
|
98
|
-
getLspFormatOnWrite: () => true,
|
|
99
|
-
getLspDiagnosticsOnWrite: () => true,
|
|
100
|
-
getLspDiagnosticsOnEdit: () => false,
|
|
101
|
-
getEditFuzzyMatch: () => true,
|
|
102
|
-
getGitToolEnabled: () => false,
|
|
103
|
-
getBashInterceptorEnabled: () => true,
|
|
104
|
-
getBashInterceptorSimpleLsEnabled: () => true,
|
|
105
|
-
getBashInterceptorRules: () => [],
|
|
106
|
-
},
|
|
142
|
+
settings: createBaseSettings({ getGitToolEnabled: () => false }),
|
|
107
143
|
});
|
|
108
144
|
const tools = await createTools(session);
|
|
109
145
|
const names = tools.map((t) => t.name);
|
|
@@ -113,17 +149,7 @@ describe("createTools", () => {
|
|
|
113
149
|
|
|
114
150
|
it("includes git tool when enabled in settings", async () => {
|
|
115
151
|
const session = createTestSession({
|
|
116
|
-
settings: {
|
|
117
|
-
getImageAutoResize: () => true,
|
|
118
|
-
getLspFormatOnWrite: () => true,
|
|
119
|
-
getLspDiagnosticsOnWrite: () => true,
|
|
120
|
-
getLspDiagnosticsOnEdit: () => false,
|
|
121
|
-
getEditFuzzyMatch: () => true,
|
|
122
|
-
getGitToolEnabled: () => true,
|
|
123
|
-
getBashInterceptorEnabled: () => true,
|
|
124
|
-
getBashInterceptorSimpleLsEnabled: () => true,
|
|
125
|
-
getBashInterceptorRules: () => [],
|
|
126
|
-
},
|
|
152
|
+
settings: createBaseSettings({ getGitToolEnabled: () => true }),
|
|
127
153
|
});
|
|
128
154
|
const tools = await createTools(session);
|
|
129
155
|
const names = tools.map((t) => t.name);
|
|
@@ -153,6 +179,7 @@ describe("createTools", () => {
|
|
|
153
179
|
const expectedTools = [
|
|
154
180
|
"ask",
|
|
155
181
|
"bash",
|
|
182
|
+
"python",
|
|
156
183
|
"calc",
|
|
157
184
|
"ssh",
|
|
158
185
|
"edit",
|