@oh-my-pi/pi-coding-agent 3.37.1 → 4.0.1
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 +103 -0
- package/README.md +44 -3
- package/docs/extensions.md +29 -4
- package/docs/sdk.md +3 -3
- package/package.json +5 -5
- package/src/cli/args.ts +8 -0
- package/src/config.ts +5 -15
- package/src/core/agent-session.ts +217 -51
- package/src/core/auth-storage.ts +456 -47
- package/src/core/bash-executor.ts +79 -14
- package/src/core/custom-commands/types.ts +1 -1
- package/src/core/custom-tools/types.ts +1 -1
- package/src/core/export-html/index.ts +33 -1
- package/src/core/export-html/template.css +99 -0
- package/src/core/export-html/template.generated.ts +1 -1
- package/src/core/export-html/template.js +133 -8
- package/src/core/extensions/index.ts +22 -4
- package/src/core/extensions/loader.ts +152 -214
- package/src/core/extensions/runner.ts +139 -79
- package/src/core/extensions/types.ts +143 -19
- package/src/core/extensions/wrapper.ts +5 -8
- package/src/core/hooks/types.ts +1 -1
- package/src/core/index.ts +2 -1
- package/src/core/keybindings.ts +4 -1
- package/src/core/model-registry.ts +4 -4
- package/src/core/model-resolver.ts +35 -26
- package/src/core/sdk.ts +96 -76
- package/src/core/settings-manager.ts +45 -14
- package/src/core/system-prompt.ts +5 -15
- package/src/core/tools/bash.ts +115 -54
- package/src/core/tools/find.ts +86 -7
- package/src/core/tools/grep.ts +27 -6
- package/src/core/tools/index.ts +15 -6
- package/src/core/tools/ls.ts +49 -18
- package/src/core/tools/render-utils.ts +2 -1
- package/src/core/tools/task/worker.ts +35 -12
- package/src/core/tools/web-search/auth.ts +37 -32
- package/src/core/tools/web-search/providers/anthropic.ts +35 -22
- package/src/index.ts +101 -9
- package/src/main.ts +60 -20
- package/src/migrations.ts +47 -2
- package/src/modes/index.ts +2 -2
- package/src/modes/interactive/components/assistant-message.ts +25 -7
- package/src/modes/interactive/components/bash-execution.ts +5 -0
- package/src/modes/interactive/components/branch-summary-message.ts +5 -0
- package/src/modes/interactive/components/compaction-summary-message.ts +5 -0
- package/src/modes/interactive/components/countdown-timer.ts +38 -0
- package/src/modes/interactive/components/custom-editor.ts +8 -0
- package/src/modes/interactive/components/custom-message.ts +5 -0
- package/src/modes/interactive/components/footer.ts +2 -5
- package/src/modes/interactive/components/hook-input.ts +29 -20
- package/src/modes/interactive/components/hook-selector.ts +52 -38
- package/src/modes/interactive/components/index.ts +39 -0
- package/src/modes/interactive/components/login-dialog.ts +160 -0
- package/src/modes/interactive/components/model-selector.ts +10 -2
- package/src/modes/interactive/components/session-selector.ts +5 -1
- package/src/modes/interactive/components/settings-defs.ts +9 -0
- package/src/modes/interactive/components/status-line/segments.ts +3 -3
- package/src/modes/interactive/components/tool-execution.ts +9 -16
- package/src/modes/interactive/components/tree-selector.ts +1 -6
- package/src/modes/interactive/interactive-mode.ts +466 -215
- package/src/modes/interactive/theme/theme.ts +50 -2
- package/src/modes/print-mode.ts +78 -31
- package/src/modes/rpc/rpc-mode.ts +186 -78
- package/src/modes/rpc/rpc-types.ts +10 -3
- package/src/prompts/system-prompt.md +36 -28
- package/src/utils/clipboard.ts +90 -50
- package/src/utils/image-convert.ts +1 -1
- package/src/utils/image-resize.ts +1 -1
- package/src/utils/tools-manager.ts +2 -2
package/src/core/sdk.ts
CHANGED
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import { join } from "node:path";
|
|
30
|
-
import { Agent, type AgentTool, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
31
|
-
import type { Model } from "@oh-my-pi/pi-ai";
|
|
30
|
+
import { Agent, type AgentMessage, type AgentTool, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
31
|
+
import type { Message, Model } from "@oh-my-pi/pi-ai";
|
|
32
32
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
33
33
|
import chalk from "chalk";
|
|
34
34
|
// Import discovery to register all providers on startup
|
|
@@ -51,9 +51,10 @@ import {
|
|
|
51
51
|
type ExtensionContext,
|
|
52
52
|
type ExtensionFactory,
|
|
53
53
|
ExtensionRunner,
|
|
54
|
+
type ExtensionUIContext,
|
|
54
55
|
type LoadExtensionsResult,
|
|
55
|
-
type LoadedExtension,
|
|
56
56
|
loadExtensionFromFactory,
|
|
57
|
+
loadExtensions,
|
|
57
58
|
type ToolDefinition,
|
|
58
59
|
wrapRegisteredTools,
|
|
59
60
|
wrapToolWithExtensions,
|
|
@@ -66,7 +67,7 @@ import { formatModelString, parseModelString } from "./model-resolver";
|
|
|
66
67
|
import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate } from "./prompt-templates";
|
|
67
68
|
import { SessionManager } from "./session-manager";
|
|
68
69
|
import { type Settings, SettingsManager, type SkillsSettings } from "./settings-manager";
|
|
69
|
-
import { loadSkills as loadSkillsInternal, type Skill } from "./skills";
|
|
70
|
+
import { loadSkills as loadSkillsInternal, type Skill, type SkillWarning } from "./skills";
|
|
70
71
|
import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal } from "./slash-commands";
|
|
71
72
|
import { closeAllConnections } from "./ssh/connection-manager";
|
|
72
73
|
import { unmountAll } from "./ssh/sshfs-mount";
|
|
@@ -129,11 +130,13 @@ export interface CreateAgentSessionOptions {
|
|
|
129
130
|
extensions?: ExtensionFactory[];
|
|
130
131
|
/** Additional extension paths to load (merged with discovery). */
|
|
131
132
|
additionalExtensionPaths?: string[];
|
|
133
|
+
/** Disable extension discovery (explicit paths still load). */
|
|
134
|
+
disableExtensionDiscovery?: boolean;
|
|
132
135
|
/**
|
|
133
136
|
* Pre-loaded extensions (skips file discovery).
|
|
134
137
|
* @internal Used by CLI when extensions are loaded early to parse custom flags.
|
|
135
138
|
*/
|
|
136
|
-
preloadedExtensions?:
|
|
139
|
+
preloadedExtensions?: LoadExtensionsResult;
|
|
137
140
|
|
|
138
141
|
/** Shared event bus for tool/extension communication. Default: creates new bus. */
|
|
139
142
|
eventBus?: EventBus;
|
|
@@ -172,8 +175,10 @@ export interface CreateAgentSessionOptions {
|
|
|
172
175
|
export interface CreateAgentSessionResult {
|
|
173
176
|
/** The created session */
|
|
174
177
|
session: AgentSession;
|
|
175
|
-
/** Extensions result (
|
|
178
|
+
/** Extensions result (loaded extensions + runtime) */
|
|
176
179
|
extensionsResult: LoadExtensionsResult;
|
|
180
|
+
/** Update tool UI context (interactive mode) */
|
|
181
|
+
setToolUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void;
|
|
177
182
|
/** MCP manager for server lifecycle management (undefined if MCP disabled) */
|
|
178
183
|
mcpManager?: MCPManager;
|
|
179
184
|
/** Warning if session was restored with a different model than saved */
|
|
@@ -274,12 +279,15 @@ export async function discoverExtensions(cwd?: string): Promise<LoadExtensionsRe
|
|
|
274
279
|
/**
|
|
275
280
|
* Discover skills from cwd and agentDir.
|
|
276
281
|
*/
|
|
277
|
-
export function discoverSkills(
|
|
278
|
-
|
|
282
|
+
export function discoverSkills(
|
|
283
|
+
cwd?: string,
|
|
284
|
+
_agentDir?: string,
|
|
285
|
+
settings?: SkillsSettings,
|
|
286
|
+
): { skills: Skill[]; warnings: SkillWarning[] } {
|
|
287
|
+
return loadSkillsInternal({
|
|
279
288
|
...settings,
|
|
280
289
|
cwd: cwd ?? process.cwd(),
|
|
281
290
|
});
|
|
282
|
-
return skills;
|
|
283
291
|
}
|
|
284
292
|
|
|
285
293
|
/**
|
|
@@ -380,6 +388,7 @@ export function loadSettings(cwd?: string, agentDir?: string): Settings {
|
|
|
380
388
|
extensions: manager.getExtensionPaths(),
|
|
381
389
|
skills: manager.getSkillsSettings(),
|
|
382
390
|
terminal: { showImages: manager.getShowImages() },
|
|
391
|
+
images: { autoResize: manager.getImageAutoResize(), blockImages: manager.getBlockImages() },
|
|
383
392
|
};
|
|
384
393
|
}
|
|
385
394
|
|
|
@@ -614,7 +623,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
614
623
|
thinkingLevel = "off";
|
|
615
624
|
}
|
|
616
625
|
|
|
617
|
-
|
|
626
|
+
let skills: Skill[];
|
|
627
|
+
let skillWarnings: SkillWarning[];
|
|
628
|
+
if (options.skills !== undefined) {
|
|
629
|
+
skills = options.skills;
|
|
630
|
+
skillWarnings = [];
|
|
631
|
+
} else {
|
|
632
|
+
const discovered = discoverSkills(cwd, agentDir, settingsManager.getSkillsSettings());
|
|
633
|
+
skills = discovered.skills;
|
|
634
|
+
skillWarnings = discovered.warnings;
|
|
635
|
+
}
|
|
618
636
|
time("discoverSkills");
|
|
619
637
|
|
|
620
638
|
// Discover rules
|
|
@@ -723,12 +741,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
723
741
|
|
|
724
742
|
// Load extensions (discovers from standard locations + configured paths)
|
|
725
743
|
let extensionsResult: LoadExtensionsResult;
|
|
726
|
-
if (options.
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
744
|
+
if (options.disableExtensionDiscovery) {
|
|
745
|
+
const configuredPaths = options.additionalExtensionPaths ?? [];
|
|
746
|
+
extensionsResult = await loadExtensions(configuredPaths, cwd, eventBus);
|
|
747
|
+
time("loadExtensions");
|
|
748
|
+
for (const { path, error } of extensionsResult.errors) {
|
|
749
|
+
logger.error("Failed to load extension", { path, error });
|
|
750
|
+
}
|
|
751
|
+
} else if (options.preloadedExtensions) {
|
|
752
|
+
extensionsResult = options.preloadedExtensions;
|
|
732
753
|
} else {
|
|
733
754
|
// Merge CLI extension paths with settings extension paths
|
|
734
755
|
const configuredPaths = [...(options.additionalExtensionPaths ?? []), ...settingsManager.getExtensionPaths()];
|
|
@@ -746,36 +767,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
746
767
|
|
|
747
768
|
// Load inline extensions from factories
|
|
748
769
|
if (inlineExtensions.length > 0) {
|
|
749
|
-
const uiHolder: { ui: any; hasUI: boolean } = {
|
|
750
|
-
ui: {
|
|
751
|
-
select: async () => undefined,
|
|
752
|
-
confirm: async () => false,
|
|
753
|
-
input: async () => undefined,
|
|
754
|
-
notify: () => {},
|
|
755
|
-
setStatus: () => {},
|
|
756
|
-
setWidget: () => {},
|
|
757
|
-
setTitle: () => {},
|
|
758
|
-
custom: async () => undefined as never,
|
|
759
|
-
setEditorText: () => {},
|
|
760
|
-
getEditorText: () => "",
|
|
761
|
-
editor: async () => undefined,
|
|
762
|
-
get theme() {
|
|
763
|
-
return {} as any;
|
|
764
|
-
},
|
|
765
|
-
},
|
|
766
|
-
hasUI: false,
|
|
767
|
-
};
|
|
768
770
|
for (let i = 0; i < inlineExtensions.length; i++) {
|
|
769
771
|
const factory = inlineExtensions[i];
|
|
770
|
-
const loaded = loadExtensionFromFactory(
|
|
772
|
+
const loaded = await loadExtensionFromFactory(
|
|
773
|
+
factory,
|
|
774
|
+
cwd,
|
|
775
|
+
eventBus,
|
|
776
|
+
extensionsResult.runtime,
|
|
777
|
+
`<inline-${i}>`,
|
|
778
|
+
);
|
|
771
779
|
extensionsResult.extensions.push(loaded);
|
|
772
780
|
}
|
|
773
|
-
const originalSetUIContext = extensionsResult.setUIContext;
|
|
774
|
-
extensionsResult.setUIContext = (uiContext, hasUI) => {
|
|
775
|
-
originalSetUIContext(uiContext, hasUI);
|
|
776
|
-
uiHolder.ui = uiContext;
|
|
777
|
-
uiHolder.hasUI = hasUI;
|
|
778
|
-
};
|
|
779
781
|
}
|
|
780
782
|
|
|
781
783
|
// Discover custom commands (TypeScript slash commands)
|
|
@@ -787,7 +789,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
787
789
|
|
|
788
790
|
let extensionRunner: ExtensionRunner | undefined;
|
|
789
791
|
if (extensionsResult.extensions.length > 0) {
|
|
790
|
-
extensionRunner = new ExtensionRunner(
|
|
792
|
+
extensionRunner = new ExtensionRunner(
|
|
793
|
+
extensionsResult.extensions,
|
|
794
|
+
extensionsResult.runtime,
|
|
795
|
+
cwd,
|
|
796
|
+
sessionManager,
|
|
797
|
+
modelRegistry,
|
|
798
|
+
);
|
|
791
799
|
}
|
|
792
800
|
|
|
793
801
|
const getSessionContext = () => ({
|
|
@@ -810,35 +818,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
810
818
|
return { definition, extensionPath: "<sdk>" };
|
|
811
819
|
}) ?? []),
|
|
812
820
|
];
|
|
813
|
-
const wrappedExtensionTools = wrapRegisteredTools(allCustomTools,
|
|
814
|
-
ui: extensionRunner?.getUIContext() ?? {
|
|
815
|
-
select: async () => undefined,
|
|
816
|
-
confirm: async () => false,
|
|
817
|
-
input: async () => undefined,
|
|
818
|
-
notify: () => {},
|
|
819
|
-
setStatus: () => {},
|
|
820
|
-
setWidget: () => {},
|
|
821
|
-
setTitle: () => {},
|
|
822
|
-
custom: async () => undefined as never,
|
|
823
|
-
setEditorText: () => {},
|
|
824
|
-
getEditorText: () => "",
|
|
825
|
-
editor: async () => undefined,
|
|
826
|
-
get theme() {
|
|
827
|
-
return {} as any;
|
|
828
|
-
},
|
|
829
|
-
},
|
|
830
|
-
hasUI: extensionRunner?.getHasUI() ?? false,
|
|
831
|
-
cwd,
|
|
832
|
-
sessionManager,
|
|
833
|
-
modelRegistry,
|
|
834
|
-
model: agent.state.model,
|
|
835
|
-
isIdle: () => !session.isStreaming,
|
|
836
|
-
abort: () => {
|
|
837
|
-
session.abort();
|
|
838
|
-
},
|
|
839
|
-
hasPendingMessages: () => session.queuedMessageCount > 0,
|
|
840
|
-
hasQueuedMessages: () => session.queuedMessageCount > 0,
|
|
841
|
-
}));
|
|
821
|
+
const wrappedExtensionTools = extensionRunner ? wrapRegisteredTools(allCustomTools, extensionRunner) : [];
|
|
842
822
|
|
|
843
823
|
// All built-in tools are active (conditional tools like git/ask return null from factory if disabled)
|
|
844
824
|
const toolRegistry = new Map<string, AgentTool>();
|
|
@@ -894,9 +874,44 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
894
874
|
const slashCommands = options.slashCommands ?? discoverSlashCommands(cwd);
|
|
895
875
|
time("discoverSlashCommands");
|
|
896
876
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
877
|
+
// Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)
|
|
878
|
+
const convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {
|
|
879
|
+
const converted = convertToLlm(messages);
|
|
880
|
+
// Check setting dynamically so mid-session changes take effect
|
|
881
|
+
if (!settingsManager.getBlockImages()) {
|
|
882
|
+
return converted;
|
|
883
|
+
}
|
|
884
|
+
// Filter out ImageContent from all messages, replacing with text placeholder
|
|
885
|
+
return converted.map((msg) => {
|
|
886
|
+
if (msg.role === "user" || msg.role === "toolResult") {
|
|
887
|
+
const content = msg.content;
|
|
888
|
+
if (Array.isArray(content)) {
|
|
889
|
+
const hasImages = content.some((c) => c.type === "image");
|
|
890
|
+
if (hasImages) {
|
|
891
|
+
const filteredContent = content
|
|
892
|
+
.map((c) =>
|
|
893
|
+
c.type === "image" ? { type: "text" as const, text: "Image reading is disabled." } : c,
|
|
894
|
+
)
|
|
895
|
+
.filter(
|
|
896
|
+
(c, i, arr) =>
|
|
897
|
+
// Dedupe consecutive "Image reading is disabled." texts
|
|
898
|
+
!(
|
|
899
|
+
c.type === "text" &&
|
|
900
|
+
c.text === "Image reading is disabled." &&
|
|
901
|
+
i > 0 &&
|
|
902
|
+
arr[i - 1].type === "text" &&
|
|
903
|
+
(arr[i - 1] as { type: "text"; text: string }).text === "Image reading is disabled."
|
|
904
|
+
),
|
|
905
|
+
);
|
|
906
|
+
return { ...msg, content: filteredContent };
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return msg;
|
|
911
|
+
});
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
const setToolUIContext = (uiContext: ExtensionUIContext, hasUI: boolean) => {
|
|
900
915
|
toolContextStore.setUIContext(uiContext, hasUI);
|
|
901
916
|
};
|
|
902
917
|
|
|
@@ -907,7 +922,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
907
922
|
thinkingLevel,
|
|
908
923
|
tools: Array.from(toolRegistry.values()),
|
|
909
924
|
},
|
|
910
|
-
convertToLlm,
|
|
925
|
+
convertToLlm: convertToLlmWithBlockImages,
|
|
926
|
+
sessionId: sessionManager.getSessionId(),
|
|
911
927
|
transformContext: extensionRunner
|
|
912
928
|
? async (messages) => {
|
|
913
929
|
return extensionRunner.emitContext(messages);
|
|
@@ -916,6 +932,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
916
932
|
steeringMode: settingsManager.getSteeringMode(),
|
|
917
933
|
followUpMode: settingsManager.getFollowUpMode(),
|
|
918
934
|
interruptMode: settingsManager.getInterruptMode(),
|
|
935
|
+
thinkingBudgets: settingsManager.getThinkingBudgets(),
|
|
919
936
|
getToolContext: toolContextStore.getContext,
|
|
920
937
|
getApiKey: async () => {
|
|
921
938
|
const currentModel = agent.state.model;
|
|
@@ -951,6 +968,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
951
968
|
slashCommands,
|
|
952
969
|
extensionRunner,
|
|
953
970
|
customCommands: customCommandsResult.commands,
|
|
971
|
+
skills,
|
|
972
|
+
skillWarnings,
|
|
954
973
|
skillsSettings: settingsManager.getSkillsSettings(),
|
|
955
974
|
modelRegistry,
|
|
956
975
|
toolRegistry,
|
|
@@ -980,6 +999,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
980
999
|
return {
|
|
981
1000
|
session,
|
|
982
1001
|
extensionsResult,
|
|
1002
|
+
setToolUIContext,
|
|
983
1003
|
mcpManager,
|
|
984
1004
|
modelFallbackMessage,
|
|
985
1005
|
lspServers,
|
|
@@ -45,6 +45,14 @@ export interface TerminalSettings {
|
|
|
45
45
|
|
|
46
46
|
export interface ImageSettings {
|
|
47
47
|
autoResize?: boolean; // default: true (resize images to 2000x2000 max for better model compatibility)
|
|
48
|
+
blockImages?: boolean; // default: false - when true, prevents all images from being sent to LLM providers
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ThinkingBudgetsSettings {
|
|
52
|
+
minimal?: number;
|
|
53
|
+
low?: number;
|
|
54
|
+
medium?: number;
|
|
55
|
+
high?: number;
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
export type NotificationMethod = "bell" | "osc99" | "osc9" | "auto" | "off";
|
|
@@ -179,6 +187,7 @@ export interface Settings {
|
|
|
179
187
|
shellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)
|
|
180
188
|
collapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full)
|
|
181
189
|
doubleEscapeAction?: "branch" | "tree"; // Action for double-escape with empty editor (default: "tree")
|
|
190
|
+
thinkingBudgets?: ThinkingBudgetsSettings; // Custom token budgets for thinking levels
|
|
182
191
|
/** Environment variables to set automatically on startup */
|
|
183
192
|
env?: Record<string, string>;
|
|
184
193
|
extensions?: string[]; // Array of extension file paths
|
|
@@ -489,23 +498,29 @@ export class SettingsManager {
|
|
|
489
498
|
}
|
|
490
499
|
|
|
491
500
|
private save(): void {
|
|
492
|
-
if (
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
}
|
|
501
|
+
if (this.persist && this.settingsPath) {
|
|
502
|
+
try {
|
|
503
|
+
const dir = dirname(this.settingsPath);
|
|
504
|
+
if (!existsSync(dir)) {
|
|
505
|
+
mkdirSync(dir, { recursive: true });
|
|
506
|
+
}
|
|
499
507
|
|
|
500
|
-
|
|
501
|
-
|
|
508
|
+
// Re-read current file to preserve any settings added externally while running
|
|
509
|
+
const currentFileSettings = SettingsManager.loadFromFile(this.settingsPath);
|
|
510
|
+
// Merge: file settings as base, globalSettings (in-memory changes) as overrides
|
|
511
|
+
const mergedSettings = deepMergeSettings(currentFileSettings, this.globalSettings);
|
|
512
|
+
this.globalSettings = mergedSettings;
|
|
502
513
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
514
|
+
// Save merged settings (project settings are read-only)
|
|
515
|
+
writeFileSync(this.settingsPath, JSON.stringify(this.globalSettings, null, 2), "utf-8");
|
|
516
|
+
} catch (error) {
|
|
517
|
+
console.error(`Warning: Could not save settings file: ${error}`);
|
|
518
|
+
}
|
|
508
519
|
}
|
|
520
|
+
|
|
521
|
+
// Always re-merge to update active settings (needed for both file and inMemory modes)
|
|
522
|
+
const projectSettings = this.loadProjectSettings();
|
|
523
|
+
this.rebuildSettings(projectSettings);
|
|
509
524
|
}
|
|
510
525
|
|
|
511
526
|
getLastChangelogVersion(): string | undefined {
|
|
@@ -668,6 +683,10 @@ export class SettingsManager {
|
|
|
668
683
|
};
|
|
669
684
|
}
|
|
670
685
|
|
|
686
|
+
getThinkingBudgets(): ThinkingBudgetsSettings | undefined {
|
|
687
|
+
return this.settings.thinkingBudgets;
|
|
688
|
+
}
|
|
689
|
+
|
|
671
690
|
getHideThinkingBlock(): boolean {
|
|
672
691
|
return this.settings.hideThinkingBlock ?? false;
|
|
673
692
|
}
|
|
@@ -773,6 +792,18 @@ export class SettingsManager {
|
|
|
773
792
|
this.save();
|
|
774
793
|
}
|
|
775
794
|
|
|
795
|
+
getBlockImages(): boolean {
|
|
796
|
+
return this.settings.images?.blockImages ?? false;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
setBlockImages(blocked: boolean): void {
|
|
800
|
+
if (!this.globalSettings.images) {
|
|
801
|
+
this.globalSettings.images = {};
|
|
802
|
+
}
|
|
803
|
+
this.globalSettings.images.blockImages = blocked;
|
|
804
|
+
this.save();
|
|
805
|
+
}
|
|
806
|
+
|
|
776
807
|
getEnabledModels(): string[] | undefined {
|
|
777
808
|
return this.settings.enabledModels;
|
|
778
809
|
}
|
|
@@ -9,7 +9,6 @@ import chalk from "chalk";
|
|
|
9
9
|
import { contextFileCapability } from "../capability/context-file";
|
|
10
10
|
import type { Rule } from "../capability/rule";
|
|
11
11
|
import { systemPromptCapability } from "../capability/system-prompt";
|
|
12
|
-
import { getDocsPath, getExamplesPath, getReadmePath } from "../config";
|
|
13
12
|
import { type ContextFile, loadSync, type SystemPrompt as SystemPromptFile } from "../discovery/index";
|
|
14
13
|
import systemPromptTemplate from "../prompts/system-prompt.md" with { type: "text" };
|
|
15
14
|
import type { SkillsSettings } from "./settings-manager";
|
|
@@ -772,7 +771,11 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
772
771
|
const contextFiles = providedContextFiles ?? loadProjectContextFiles({ cwd: resolvedCwd });
|
|
773
772
|
|
|
774
773
|
// Build tools list based on selected tools
|
|
775
|
-
const
|
|
774
|
+
const selectedToolNames = toolNames ?? (["read", "bash", "edit", "write"] as ToolName[]);
|
|
775
|
+
const toolsList =
|
|
776
|
+
selectedToolNames.length > 0
|
|
777
|
+
? selectedToolNames.map((name) => `- ${name}: ${toolDescriptions[name as ToolName]}`).join("\n")
|
|
778
|
+
: "(none)";
|
|
776
779
|
|
|
777
780
|
// Resolve skills: use provided or discover
|
|
778
781
|
const skills =
|
|
@@ -804,11 +807,6 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
804
807
|
return prompt;
|
|
805
808
|
}
|
|
806
809
|
|
|
807
|
-
// Get absolute paths to documentation and examples
|
|
808
|
-
const readmePath = getReadmePath();
|
|
809
|
-
const docsPath = getDocsPath();
|
|
810
|
-
const examplesPath = getExamplesPath();
|
|
811
|
-
|
|
812
810
|
// Generate anti-bash rules (returns null if not applicable)
|
|
813
811
|
const antiBashSection = generateAntiBashRules(Array.from(tools?.keys() ?? []));
|
|
814
812
|
const environmentInfo = formatEnvironmentInfo();
|
|
@@ -821,11 +819,6 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
821
819
|
const hasWrite = tools?.has("write");
|
|
822
820
|
const hasRead = tools?.has("read");
|
|
823
821
|
|
|
824
|
-
// Read-only mode notice (no bash, edit, or write)
|
|
825
|
-
if (!hasBash && !hasEdit && !hasWrite) {
|
|
826
|
-
guidelinesList.push("You are in READ-ONLY mode - you cannot modify files or execute arbitrary commands");
|
|
827
|
-
}
|
|
828
|
-
|
|
829
822
|
// Bash without edit/write = read-only bash mode
|
|
830
823
|
if (hasBash && !hasEdit && !hasWrite) {
|
|
831
824
|
guidelinesList.push(
|
|
@@ -870,9 +863,6 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
870
863
|
antiBashSection: antiBashBlock,
|
|
871
864
|
guidelines,
|
|
872
865
|
environmentInfo,
|
|
873
|
-
readmePath,
|
|
874
|
-
docsPath,
|
|
875
|
-
examplesPath,
|
|
876
866
|
});
|
|
877
867
|
|
|
878
868
|
prompt = appendBlock(prompt, resolvedAppendPrompt);
|
package/src/core/tools/bash.ts
CHANGED
|
@@ -3,12 +3,14 @@ import type { AgentTool, AgentToolContext } from "@oh-my-pi/pi-agent-core";
|
|
|
3
3
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
5
5
|
import { Type } from "@sinclair/typebox";
|
|
6
|
+
import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate";
|
|
6
7
|
import type { Theme } from "../../modes/interactive/theme/theme";
|
|
7
8
|
import bashDescription from "../../prompts/tools/bash.md" with { type: "text" };
|
|
8
|
-
import { executeBash } from "../bash-executor";
|
|
9
|
+
import { type BashExecutorOptions, executeBash, executeBashWithOperations } from "../bash-executor";
|
|
9
10
|
import type { RenderResultOptions } from "../custom-tools/types";
|
|
10
11
|
import { checkBashInterception, checkSimpleLsInterception } from "./bash-interceptor";
|
|
11
12
|
import type { ToolSession } from "./index";
|
|
13
|
+
import { resolveToCwd } from "./path-utils";
|
|
12
14
|
import { createToolUIKit } from "./render-utils";
|
|
13
15
|
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateTail } from "./truncate";
|
|
14
16
|
|
|
@@ -25,7 +27,28 @@ export interface BashToolDetails {
|
|
|
25
27
|
fullOutputPath?: string;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Pluggable operations for bash execution.
|
|
32
|
+
* Override to delegate command execution to remote systems.
|
|
33
|
+
*/
|
|
34
|
+
export interface BashOperations {
|
|
35
|
+
exec: (
|
|
36
|
+
command: string,
|
|
37
|
+
cwd: string,
|
|
38
|
+
options: {
|
|
39
|
+
onData: (data: Buffer) => void;
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
timeout?: number;
|
|
42
|
+
},
|
|
43
|
+
) => Promise<{ exitCode: number | null }>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BashToolOptions {
|
|
47
|
+
/** Custom operations for command execution. Default: local shell */
|
|
48
|
+
operations?: BashOperations;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createBashTool(session: ToolSession, options?: BashToolOptions): AgentTool<typeof bashSchema> {
|
|
29
52
|
return {
|
|
30
53
|
name: "bash",
|
|
31
54
|
label: "Bash",
|
|
@@ -53,11 +76,22 @@ export function createBashTool(session: ToolSession): AgentTool<typeof bashSchem
|
|
|
53
76
|
}
|
|
54
77
|
}
|
|
55
78
|
|
|
79
|
+
const commandCwd = workdir ? resolveToCwd(workdir, session.cwd) : session.cwd;
|
|
80
|
+
let cwdStat: Awaited<ReturnType<Bun.BunFile["stat"]>>;
|
|
81
|
+
try {
|
|
82
|
+
cwdStat = await Bun.file(commandCwd).stat();
|
|
83
|
+
} catch {
|
|
84
|
+
throw new Error(`Working directory does not exist: ${commandCwd}`);
|
|
85
|
+
}
|
|
86
|
+
if (!cwdStat.isDirectory()) {
|
|
87
|
+
throw new Error(`Working directory is not a directory: ${commandCwd}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
56
90
|
// Track output for streaming updates
|
|
57
91
|
let currentOutput = "";
|
|
58
92
|
|
|
59
|
-
const
|
|
60
|
-
cwd:
|
|
93
|
+
const executorOptions: BashExecutorOptions = {
|
|
94
|
+
cwd: commandCwd,
|
|
61
95
|
timeout: timeout ? timeout * 1000 : undefined, // Convert to milliseconds
|
|
62
96
|
signal,
|
|
63
97
|
onChunk: (chunk) => {
|
|
@@ -72,7 +106,12 @@ export function createBashTool(session: ToolSession): AgentTool<typeof bashSchem
|
|
|
72
106
|
});
|
|
73
107
|
}
|
|
74
108
|
},
|
|
75
|
-
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Use custom operations if provided, otherwise use default local executor
|
|
112
|
+
const result = options?.operations
|
|
113
|
+
? await executeBashWithOperations(command, commandCwd, options.operations, executorOptions)
|
|
114
|
+
: await executeBash(command, executorOptions);
|
|
76
115
|
|
|
77
116
|
// Handle errors
|
|
78
117
|
if (result.cancelled) {
|
|
@@ -125,12 +164,12 @@ interface BashRenderArgs {
|
|
|
125
164
|
}
|
|
126
165
|
|
|
127
166
|
interface BashRenderContext {
|
|
128
|
-
/**
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
|
|
167
|
+
/** Raw output text */
|
|
168
|
+
output?: string;
|
|
169
|
+
/** Whether output is expanded */
|
|
170
|
+
expanded?: boolean;
|
|
171
|
+
/** Number of preview lines when collapsed */
|
|
172
|
+
previewLines?: number;
|
|
134
173
|
}
|
|
135
174
|
|
|
136
175
|
export const bashToolRenderer = {
|
|
@@ -171,51 +210,18 @@ export const bashToolRenderer = {
|
|
|
171
210
|
uiTheme: Theme,
|
|
172
211
|
): Component {
|
|
173
212
|
const ui = createToolUIKit(uiTheme);
|
|
174
|
-
const {
|
|
213
|
+
const { renderContext } = options;
|
|
175
214
|
const details = result.details;
|
|
176
|
-
const lines: string[] = [];
|
|
177
|
-
|
|
178
|
-
// Get output text
|
|
179
|
-
const textContent = result.content?.find((c) => c.type === "text")?.text ?? "";
|
|
180
|
-
const output = textContent.trim();
|
|
181
|
-
|
|
182
|
-
if (output) {
|
|
183
|
-
if (expanded) {
|
|
184
|
-
// Show all lines when expanded
|
|
185
|
-
const styledOutput = output
|
|
186
|
-
.split("\n")
|
|
187
|
-
.map((line) => uiTheme.fg("toolOutput", line))
|
|
188
|
-
.join("\n");
|
|
189
|
-
lines.push(styledOutput);
|
|
190
|
-
} else if (renderContext?.visualLines) {
|
|
191
|
-
// Use pre-computed visual lines from tool-execution
|
|
192
|
-
const { visualLines, skippedCount = 0, totalVisualLines = visualLines.length } = renderContext;
|
|
193
|
-
if (skippedCount > 0) {
|
|
194
|
-
lines.push(
|
|
195
|
-
uiTheme.fg(
|
|
196
|
-
"dim",
|
|
197
|
-
`${uiTheme.format.ellipsis} (${skippedCount} earlier lines, showing ${visualLines.length} of ${totalVisualLines}) (ctrl+o to expand)`,
|
|
198
|
-
),
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
lines.push(...visualLines);
|
|
202
|
-
} else {
|
|
203
|
-
// Fallback: show first few lines
|
|
204
|
-
const outputLines = output.split("\n");
|
|
205
|
-
const maxLines = 5;
|
|
206
|
-
const displayLines = outputLines.slice(0, maxLines);
|
|
207
|
-
const remaining = outputLines.length - maxLines;
|
|
208
|
-
|
|
209
|
-
lines.push(...displayLines.map((line) => uiTheme.fg("toolOutput", line)));
|
|
210
|
-
if (remaining > 0) {
|
|
211
|
-
lines.push(uiTheme.fg("dim", `${uiTheme.format.ellipsis} (${remaining} more lines) (ctrl+o to expand)`));
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
215
|
|
|
216
|
-
//
|
|
216
|
+
// Get output from context (preferred) or fall back to result content
|
|
217
|
+
const output = renderContext?.output ?? (result.content?.find((c) => c.type === "text")?.text ?? "").trim();
|
|
218
|
+
const expanded = renderContext?.expanded ?? options.expanded;
|
|
219
|
+
const previewLines = renderContext?.previewLines ?? 5;
|
|
220
|
+
|
|
221
|
+
// Build truncation warning lines (static, doesn't depend on width)
|
|
217
222
|
const truncation = details?.truncation;
|
|
218
223
|
const fullOutputPath = details?.fullOutputPath;
|
|
224
|
+
let warningLine: string | undefined;
|
|
219
225
|
if (truncation?.truncated || fullOutputPath) {
|
|
220
226
|
const warnings: string[] = [];
|
|
221
227
|
if (fullOutputPath) {
|
|
@@ -230,9 +236,64 @@ export const bashToolRenderer = {
|
|
|
230
236
|
);
|
|
231
237
|
}
|
|
232
238
|
}
|
|
233
|
-
|
|
239
|
+
warningLine = uiTheme.fg("warning", ui.wrapBrackets(warnings.join(". ")));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!output) {
|
|
243
|
+
// No output - just show warning if any
|
|
244
|
+
return new Text(warningLine ?? "", 0, 0);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (expanded) {
|
|
248
|
+
// Show all lines when expanded
|
|
249
|
+
const styledOutput = output
|
|
250
|
+
.split("\n")
|
|
251
|
+
.map((line) => uiTheme.fg("toolOutput", line))
|
|
252
|
+
.join("\n");
|
|
253
|
+
const lines = warningLine ? [styledOutput, warningLine] : [styledOutput];
|
|
254
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
234
255
|
}
|
|
235
256
|
|
|
236
|
-
|
|
257
|
+
// Collapsed: use width-aware caching component
|
|
258
|
+
const styledOutput = output
|
|
259
|
+
.split("\n")
|
|
260
|
+
.map((line) => uiTheme.fg("toolOutput", line))
|
|
261
|
+
.join("\n");
|
|
262
|
+
const textContent = `\n${styledOutput}`;
|
|
263
|
+
|
|
264
|
+
let cachedWidth: number | undefined;
|
|
265
|
+
let cachedLines: string[] | undefined;
|
|
266
|
+
let cachedSkipped: number | undefined;
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
render: (width: number): string[] => {
|
|
270
|
+
if (cachedLines === undefined || cachedWidth !== width) {
|
|
271
|
+
const result = truncateToVisualLines(textContent, previewLines, width);
|
|
272
|
+
cachedLines = result.visualLines;
|
|
273
|
+
cachedSkipped = result.skippedCount;
|
|
274
|
+
cachedWidth = width;
|
|
275
|
+
}
|
|
276
|
+
const outputLines: string[] = [];
|
|
277
|
+
if (cachedSkipped && cachedSkipped > 0) {
|
|
278
|
+
outputLines.push("");
|
|
279
|
+
outputLines.push(
|
|
280
|
+
uiTheme.fg(
|
|
281
|
+
"dim",
|
|
282
|
+
`${uiTheme.format.ellipsis} (${cachedSkipped} earlier lines, showing ${cachedLines.length} of ${cachedSkipped + cachedLines.length}) (ctrl+o to expand)`,
|
|
283
|
+
),
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
outputLines.push(...cachedLines);
|
|
287
|
+
if (warningLine) {
|
|
288
|
+
outputLines.push(warningLine);
|
|
289
|
+
}
|
|
290
|
+
return outputLines;
|
|
291
|
+
},
|
|
292
|
+
invalidate: () => {
|
|
293
|
+
cachedWidth = undefined;
|
|
294
|
+
cachedLines = undefined;
|
|
295
|
+
cachedSkipped = undefined;
|
|
296
|
+
},
|
|
297
|
+
};
|
|
237
298
|
},
|
|
238
299
|
};
|