@oh-my-pi/pi-coding-agent 1.340.0 → 2.0.1337
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 +115 -1
- package/README.md +1 -1
- package/examples/custom-tools/subagent/index.ts +1 -1
- package/package.json +5 -3
- package/src/cli/args.ts +13 -6
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/list-models.ts +2 -2
- package/src/cli/plugin-cli.ts +1 -1
- package/src/cli/session-picker.ts +2 -2
- package/src/cli.ts +1 -1
- package/src/config.ts +3 -3
- package/src/core/agent-session.ts +189 -29
- package/src/core/bash-executor.ts +50 -10
- package/src/core/compaction/branch-summarization.ts +5 -5
- package/src/core/compaction/compaction.ts +3 -3
- package/src/core/compaction/index.ts +3 -3
- package/src/core/custom-commands/bundled/review/index.ts +156 -0
- package/src/core/custom-commands/index.ts +15 -0
- package/src/core/custom-commands/loader.ts +232 -0
- package/src/core/custom-commands/types.ts +112 -0
- package/src/core/custom-tools/index.ts +3 -3
- package/src/core/custom-tools/loader.ts +10 -8
- package/src/core/custom-tools/types.ts +11 -6
- package/src/core/custom-tools/wrapper.ts +2 -1
- package/src/core/exec.ts +22 -12
- package/src/core/export-html/index.ts +5 -5
- package/src/core/file-mentions.ts +54 -0
- package/src/core/hooks/index.ts +5 -5
- package/src/core/hooks/loader.ts +21 -16
- package/src/core/hooks/runner.ts +6 -6
- package/src/core/hooks/tool-wrapper.ts +2 -2
- package/src/core/hooks/types.ts +12 -15
- package/src/core/index.ts +6 -6
- package/src/core/logger.ts +112 -0
- package/src/core/mcp/client.ts +3 -3
- package/src/core/mcp/config.ts +1 -1
- package/src/core/mcp/index.ts +12 -12
- package/src/core/mcp/loader.ts +2 -2
- package/src/core/mcp/manager.ts +6 -6
- package/src/core/mcp/tool-bridge.ts +3 -3
- package/src/core/mcp/transports/http.ts +1 -1
- package/src/core/mcp/transports/index.ts +2 -2
- package/src/core/mcp/transports/stdio.ts +1 -1
- package/src/core/messages.ts +22 -0
- package/src/core/model-registry.ts +2 -2
- package/src/core/model-resolver.ts +103 -2
- package/src/core/plugins/doctor.ts +1 -1
- package/src/core/plugins/index.ts +6 -6
- package/src/core/plugins/installer.ts +4 -4
- package/src/core/plugins/loader.ts +4 -9
- package/src/core/plugins/manager.ts +5 -5
- package/src/core/plugins/paths.ts +3 -3
- package/src/core/sdk.ts +127 -52
- package/src/core/session-manager.ts +123 -20
- package/src/core/settings-manager.ts +106 -22
- package/src/core/skills.ts +5 -5
- package/src/core/slash-commands.ts +60 -45
- package/src/core/system-prompt.ts +6 -6
- package/src/core/title-generator.ts +94 -0
- package/src/core/tools/bash.ts +33 -157
- package/src/core/tools/context.ts +2 -2
- package/src/core/tools/edit-diff.ts +5 -5
- package/src/core/tools/edit.ts +60 -9
- package/src/core/tools/exa/company.ts +3 -3
- package/src/core/tools/exa/index.ts +16 -17
- package/src/core/tools/exa/linkedin.ts +3 -3
- package/src/core/tools/exa/mcp-client.ts +9 -9
- package/src/core/tools/exa/render.ts +5 -5
- package/src/core/tools/exa/researcher.ts +3 -3
- package/src/core/tools/exa/search.ts +6 -5
- package/src/core/tools/exa/types.ts +5 -6
- package/src/core/tools/exa/websets.ts +3 -3
- package/src/core/tools/find.ts +3 -3
- package/src/core/tools/grep.ts +6 -5
- package/src/core/tools/index.ts +114 -40
- package/src/core/tools/ls.ts +4 -4
- package/src/core/tools/lsp/client.ts +204 -108
- package/src/core/tools/lsp/config.ts +709 -35
- package/src/core/tools/lsp/edits.ts +2 -2
- package/src/core/tools/lsp/index.ts +432 -30
- package/src/core/tools/lsp/render.ts +2 -2
- package/src/core/tools/lsp/rust-analyzer.ts +3 -3
- package/src/core/tools/lsp/types.ts +5 -0
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/notebook.ts +1 -1
- package/src/core/tools/output.ts +175 -0
- package/src/core/tools/read.ts +7 -7
- package/src/core/tools/renderers.ts +92 -13
- package/src/core/tools/review.ts +268 -0
- package/src/core/tools/task/agents.ts +1 -1
- package/src/core/tools/task/bundled-agents/explore.md +1 -1
- package/src/core/tools/task/bundled-agents/reviewer.md +53 -38
- package/src/core/tools/task/discovery.ts +2 -2
- package/src/core/tools/task/executor.ts +145 -28
- package/src/core/tools/task/index.ts +78 -30
- package/src/core/tools/task/model-resolver.ts +72 -13
- package/src/core/tools/task/parallel.ts +1 -1
- package/src/core/tools/task/render.ts +219 -30
- package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
- package/src/core/tools/task/types.ts +36 -2
- package/src/core/tools/web-fetch.ts +5 -3
- package/src/core/tools/web-search/auth.ts +1 -1
- package/src/core/tools/web-search/index.ts +17 -15
- package/src/core/tools/web-search/providers/anthropic.ts +2 -2
- package/src/core/tools/web-search/providers/exa.ts +3 -5
- package/src/core/tools/web-search/providers/perplexity.ts +1 -1
- package/src/core/tools/web-search/render.ts +3 -3
- package/src/core/tools/write.ts +70 -7
- package/src/index.ts +33 -17
- package/src/main.ts +60 -34
- package/src/migrations.ts +3 -3
- package/src/modes/index.ts +5 -5
- package/src/modes/interactive/components/armin.ts +1 -1
- package/src/modes/interactive/components/assistant-message.ts +1 -1
- package/src/modes/interactive/components/bash-execution.ts +4 -4
- package/src/modes/interactive/components/bordered-loader.ts +2 -2
- package/src/modes/interactive/components/branch-summary-message.ts +2 -2
- package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
- package/src/modes/interactive/components/diff.ts +1 -1
- package/src/modes/interactive/components/dynamic-border.ts +1 -1
- package/src/modes/interactive/components/footer.ts +5 -5
- package/src/modes/interactive/components/hook-editor.ts +2 -2
- package/src/modes/interactive/components/hook-input.ts +2 -2
- package/src/modes/interactive/components/hook-message.ts +3 -3
- package/src/modes/interactive/components/hook-selector.ts +2 -2
- package/src/modes/interactive/components/model-selector.ts +341 -41
- package/src/modes/interactive/components/oauth-selector.ts +3 -3
- package/src/modes/interactive/components/plugin-settings.ts +4 -4
- package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
- package/src/modes/interactive/components/session-selector.ts +24 -11
- package/src/modes/interactive/components/settings-defs.ts +51 -3
- package/src/modes/interactive/components/settings-selector.ts +13 -16
- package/src/modes/interactive/components/show-images-selector.ts +2 -2
- package/src/modes/interactive/components/theme-selector.ts +2 -2
- package/src/modes/interactive/components/thinking-selector.ts +2 -2
- package/src/modes/interactive/components/tool-execution.ts +44 -8
- package/src/modes/interactive/components/tree-selector.ts +5 -5
- package/src/modes/interactive/components/user-message-selector.ts +2 -2
- package/src/modes/interactive/components/user-message.ts +1 -1
- package/src/modes/interactive/components/welcome.ts +42 -5
- package/src/modes/interactive/interactive-mode.ts +169 -48
- package/src/modes/interactive/theme/theme.ts +8 -7
- package/src/modes/print-mode.ts +4 -3
- package/src/modes/rpc/rpc-client.ts +4 -4
- package/src/modes/rpc/rpc-mode.ts +21 -11
- package/src/modes/rpc/rpc-types.ts +3 -3
- package/src/utils/changelog.ts +2 -2
- package/src/utils/clipboard.ts +1 -1
- package/src/utils/shell-snapshot.ts +218 -0
- package/src/utils/shell.ts +93 -13
- package/src/utils/tools-manager.ts +1 -1
- package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
- package/src/core/tools/exa/logger.ts +0 -56
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
import type { Agent, AgentEvent, AgentMessage, AgentState, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
17
17
|
import type { AssistantMessage, ImageContent, Message, Model, TextContent } from "@oh-my-pi/pi-ai";
|
|
18
18
|
import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@oh-my-pi/pi-ai";
|
|
19
|
-
import { getAuthPath } from "../config
|
|
20
|
-
import { type BashResult, executeBash as executeBashCommand } from "./bash-executor
|
|
19
|
+
import { getAuthPath } from "../config";
|
|
20
|
+
import { type BashResult, executeBash as executeBashCommand } from "./bash-executor";
|
|
21
21
|
import {
|
|
22
22
|
type CompactionResult,
|
|
23
23
|
calculateContextTokens,
|
|
@@ -26,9 +26,11 @@ import {
|
|
|
26
26
|
generateBranchSummary,
|
|
27
27
|
prepareCompaction,
|
|
28
28
|
shouldCompact,
|
|
29
|
-
} from "./compaction/index
|
|
30
|
-
import type {
|
|
31
|
-
import {
|
|
29
|
+
} from "./compaction/index";
|
|
30
|
+
import type { LoadedCustomCommand } from "./custom-commands/index";
|
|
31
|
+
import type { CustomToolContext, CustomToolSessionEvent, LoadedCustomTool } from "./custom-tools/index";
|
|
32
|
+
import { exportSessionToHtml } from "./export-html/index";
|
|
33
|
+
import { extractFileMentions, generateFileMentionMessages } from "./file-mentions";
|
|
32
34
|
import type {
|
|
33
35
|
HookRunner,
|
|
34
36
|
SessionBeforeBranchResult,
|
|
@@ -38,12 +40,13 @@ import type {
|
|
|
38
40
|
TreePreparation,
|
|
39
41
|
TurnEndEvent,
|
|
40
42
|
TurnStartEvent,
|
|
41
|
-
} from "./hooks/index
|
|
42
|
-
import
|
|
43
|
-
import type {
|
|
44
|
-
import type {
|
|
45
|
-
import type {
|
|
46
|
-
import {
|
|
43
|
+
} from "./hooks/index";
|
|
44
|
+
import { logger } from "./logger";
|
|
45
|
+
import type { BashExecutionMessage, HookMessage } from "./messages";
|
|
46
|
+
import type { ModelRegistry } from "./model-registry";
|
|
47
|
+
import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions, SessionManager } from "./session-manager";
|
|
48
|
+
import type { SettingsManager, SkillsSettings } from "./settings-manager";
|
|
49
|
+
import { expandSlashCommand, type FileSlashCommand, parseCommandArgs } from "./slash-commands";
|
|
47
50
|
|
|
48
51
|
/** Session-specific events that extend the core AgentEvent */
|
|
49
52
|
export type AgentSessionEvent =
|
|
@@ -72,6 +75,8 @@ export interface AgentSessionConfig {
|
|
|
72
75
|
hookRunner?: HookRunner;
|
|
73
76
|
/** Custom tools for session lifecycle events */
|
|
74
77
|
customTools?: LoadedCustomTool[];
|
|
78
|
+
/** Custom commands (TypeScript slash commands) */
|
|
79
|
+
customCommands?: LoadedCustomCommand[];
|
|
75
80
|
skillsSettings?: Required<SkillsSettings>;
|
|
76
81
|
/** Model registry for API key resolution and model discovery */
|
|
77
82
|
modelRegistry: ModelRegistry;
|
|
@@ -166,6 +171,9 @@ export class AgentSession {
|
|
|
166
171
|
// Custom tools for session lifecycle
|
|
167
172
|
private _customTools: LoadedCustomTool[] = [];
|
|
168
173
|
|
|
174
|
+
// Custom commands (TypeScript slash commands)
|
|
175
|
+
private _customCommands: LoadedCustomCommand[] = [];
|
|
176
|
+
|
|
169
177
|
private _skillsSettings: Required<SkillsSettings> | undefined;
|
|
170
178
|
|
|
171
179
|
// Model registry for API key resolution
|
|
@@ -179,6 +187,7 @@ export class AgentSession {
|
|
|
179
187
|
this._fileCommands = config.fileCommands ?? [];
|
|
180
188
|
this._hookRunner = config.hookRunner;
|
|
181
189
|
this._customTools = config.customTools ?? [];
|
|
190
|
+
this._customCommands = config.customCommands ?? [];
|
|
182
191
|
this._skillsSettings = config.skillsSettings;
|
|
183
192
|
this._modelRegistry = config.modelRegistry;
|
|
184
193
|
|
|
@@ -198,7 +207,9 @@ export class AgentSession {
|
|
|
198
207
|
|
|
199
208
|
/** Emit an event to all listeners */
|
|
200
209
|
private _emit(event: AgentSessionEvent): void {
|
|
201
|
-
|
|
210
|
+
// Copy array before iteration to avoid mutation during iteration
|
|
211
|
+
const listeners = [...this._eventListeners];
|
|
212
|
+
for (const l of listeners) {
|
|
202
213
|
l(event);
|
|
203
214
|
}
|
|
204
215
|
}
|
|
@@ -424,6 +435,11 @@ export class AgentSession {
|
|
|
424
435
|
return this.agent.getQueueMode();
|
|
425
436
|
}
|
|
426
437
|
|
|
438
|
+
/** Current interrupt mode */
|
|
439
|
+
get interruptMode(): "immediate" | "wait" {
|
|
440
|
+
return this.agent.getInterruptMode();
|
|
441
|
+
}
|
|
442
|
+
|
|
427
443
|
/** Current session file path, or undefined if sessions are disabled */
|
|
428
444
|
get sessionFile(): string | undefined {
|
|
429
445
|
return this.sessionManager.getSessionFile();
|
|
@@ -444,6 +460,11 @@ export class AgentSession {
|
|
|
444
460
|
return this._fileCommands;
|
|
445
461
|
}
|
|
446
462
|
|
|
463
|
+
/** Custom commands (TypeScript slash commands) */
|
|
464
|
+
get customCommands(): ReadonlyArray<LoadedCustomCommand> {
|
|
465
|
+
return this._customCommands;
|
|
466
|
+
}
|
|
467
|
+
|
|
447
468
|
// =========================================================================
|
|
448
469
|
// Prompting
|
|
449
470
|
// =========================================================================
|
|
@@ -468,6 +489,17 @@ export class AgentSession {
|
|
|
468
489
|
// Hook command executed, no prompt to send
|
|
469
490
|
return;
|
|
470
491
|
}
|
|
492
|
+
|
|
493
|
+
// Try custom commands (TypeScript slash commands)
|
|
494
|
+
const customResult = await this._tryExecuteCustomCommand(text);
|
|
495
|
+
if (customResult !== null) {
|
|
496
|
+
if (customResult === "") {
|
|
497
|
+
// Command handled, nothing to send
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
// Command returned a prompt - use it instead of the original text
|
|
501
|
+
text = customResult;
|
|
502
|
+
}
|
|
471
503
|
}
|
|
472
504
|
|
|
473
505
|
// Validate model
|
|
@@ -511,6 +543,13 @@ export class AgentSession {
|
|
|
511
543
|
timestamp: Date.now(),
|
|
512
544
|
});
|
|
513
545
|
|
|
546
|
+
// Auto-read @filepath mentions
|
|
547
|
+
const fileMentions = extractFileMentions(expandedText);
|
|
548
|
+
if (fileMentions.length > 0) {
|
|
549
|
+
const fileMentionMessages = await generateFileMentionMessages(fileMentions, this.sessionManager.getCwd());
|
|
550
|
+
messages.push(...fileMentionMessages);
|
|
551
|
+
}
|
|
552
|
+
|
|
514
553
|
// Emit before_agent_start hook event
|
|
515
554
|
if (this._hookRunner) {
|
|
516
555
|
const result = await this._hookRunner.emitBeforeAgentStart(expandedText, options?.images);
|
|
@@ -561,6 +600,43 @@ export class AgentSession {
|
|
|
561
600
|
}
|
|
562
601
|
}
|
|
563
602
|
|
|
603
|
+
/**
|
|
604
|
+
* Try to execute a custom command. Returns the prompt string if found, null otherwise.
|
|
605
|
+
* If the command returns void, returns empty string to indicate it was handled.
|
|
606
|
+
*/
|
|
607
|
+
private async _tryExecuteCustomCommand(text: string): Promise<string | null> {
|
|
608
|
+
if (this._customCommands.length === 0) return null;
|
|
609
|
+
if (!this._hookRunner) return null; // Need hook runner for command context
|
|
610
|
+
|
|
611
|
+
// Parse command name and args
|
|
612
|
+
const spaceIndex = text.indexOf(" ");
|
|
613
|
+
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
614
|
+
const argsString = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1);
|
|
615
|
+
|
|
616
|
+
// Find matching command
|
|
617
|
+
const loaded = this._customCommands.find((c) => c.command.name === commandName);
|
|
618
|
+
if (!loaded) return null;
|
|
619
|
+
|
|
620
|
+
// Get command context from hook runner (includes session control methods)
|
|
621
|
+
const ctx = this._hookRunner.createCommandContext();
|
|
622
|
+
|
|
623
|
+
try {
|
|
624
|
+
const args = parseCommandArgs(argsString);
|
|
625
|
+
const result = await loaded.command.execute(args, ctx);
|
|
626
|
+
// If result is a string, it's a prompt to send to LLM
|
|
627
|
+
// If void/undefined, command handled everything
|
|
628
|
+
return result ?? "";
|
|
629
|
+
} catch (err) {
|
|
630
|
+
// Emit error via hook runner
|
|
631
|
+
this._hookRunner.emitError({
|
|
632
|
+
hookPath: `custom-command:${commandName}`,
|
|
633
|
+
event: "command",
|
|
634
|
+
error: err instanceof Error ? err.message : String(err),
|
|
635
|
+
});
|
|
636
|
+
return ""; // Command was handled (with error)
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
564
640
|
/**
|
|
565
641
|
* Queue a message to be sent after the current response completes.
|
|
566
642
|
* Use when agent is currently streaming.
|
|
@@ -701,15 +777,15 @@ export class AgentSession {
|
|
|
701
777
|
* Validates API key, saves to session and settings.
|
|
702
778
|
* @throws Error if no API key available for the model
|
|
703
779
|
*/
|
|
704
|
-
async setModel(model: Model<any
|
|
780
|
+
async setModel(model: Model<any>, role: string = "default"): Promise<void> {
|
|
705
781
|
const apiKey = await this._modelRegistry.getApiKey(model);
|
|
706
782
|
if (!apiKey) {
|
|
707
783
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
708
784
|
}
|
|
709
785
|
|
|
710
786
|
this.agent.setModel(model);
|
|
711
|
-
this.sessionManager.appendModelChange(model.provider
|
|
712
|
-
this.settingsManager.
|
|
787
|
+
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, role);
|
|
788
|
+
this.settingsManager.setModelRole(role, `${model.provider}/${model.id}`);
|
|
713
789
|
|
|
714
790
|
// Re-clamp thinking level for new model's capabilities
|
|
715
791
|
this.setThinkingLevel(this.thinkingLevel);
|
|
@@ -747,8 +823,8 @@ export class AgentSession {
|
|
|
747
823
|
|
|
748
824
|
// Apply model
|
|
749
825
|
this.agent.setModel(next.model);
|
|
750
|
-
this.sessionManager.appendModelChange(next.model.provider
|
|
751
|
-
this.settingsManager.
|
|
826
|
+
this.sessionManager.appendModelChange(`${next.model.provider}/${next.model.id}`);
|
|
827
|
+
this.settingsManager.setModelRole("default", `${next.model.provider}/${next.model.id}`);
|
|
752
828
|
|
|
753
829
|
// Apply thinking level (setThinkingLevel clamps to model capabilities)
|
|
754
830
|
this.setThinkingLevel(next.thinkingLevel);
|
|
@@ -774,8 +850,8 @@ export class AgentSession {
|
|
|
774
850
|
}
|
|
775
851
|
|
|
776
852
|
this.agent.setModel(nextModel);
|
|
777
|
-
this.sessionManager.appendModelChange(nextModel.provider
|
|
778
|
-
this.settingsManager.
|
|
853
|
+
this.sessionManager.appendModelChange(`${nextModel.provider}/${nextModel.id}`);
|
|
854
|
+
this.settingsManager.setModelRole("default", `${nextModel.provider}/${nextModel.id}`);
|
|
779
855
|
|
|
780
856
|
// Re-clamp thinking level for new model's capabilities
|
|
781
857
|
this.setThinkingLevel(this.thinkingLevel);
|
|
@@ -861,6 +937,15 @@ export class AgentSession {
|
|
|
861
937
|
this.settingsManager.setQueueMode(mode);
|
|
862
938
|
}
|
|
863
939
|
|
|
940
|
+
/**
|
|
941
|
+
* Set interrupt mode.
|
|
942
|
+
* Saves to settings.
|
|
943
|
+
*/
|
|
944
|
+
setInterruptMode(mode: "immediate" | "wait"): void {
|
|
945
|
+
this.agent.setInterruptMode(mode);
|
|
946
|
+
this.settingsManager.setInterruptMode(mode);
|
|
947
|
+
}
|
|
948
|
+
|
|
864
949
|
// =========================================================================
|
|
865
950
|
// Compaction
|
|
866
951
|
// =========================================================================
|
|
@@ -1044,6 +1129,10 @@ export class AgentSession {
|
|
|
1044
1129
|
const settings = this.settingsManager.getCompactionSettings();
|
|
1045
1130
|
|
|
1046
1131
|
this._emit({ type: "auto_compaction_start", reason });
|
|
1132
|
+
// Properly abort and null existing controller before replacing
|
|
1133
|
+
if (this._autoCompactionAbortController) {
|
|
1134
|
+
this._autoCompactionAbortController.abort();
|
|
1135
|
+
}
|
|
1047
1136
|
this._autoCompactionAbortController = new AbortController();
|
|
1048
1137
|
|
|
1049
1138
|
try {
|
|
@@ -1217,7 +1306,8 @@ export class AgentSession {
|
|
|
1217
1306
|
this._retryAttempt++;
|
|
1218
1307
|
|
|
1219
1308
|
// Create retry promise on first attempt so waitForRetry() can await it
|
|
1220
|
-
|
|
1309
|
+
// Ensure only one promise exists (avoid orphaned promises from concurrent calls)
|
|
1310
|
+
if (!this._retryPromise) {
|
|
1221
1311
|
this._retryPromise = new Promise((resolve) => {
|
|
1222
1312
|
this._retryResolve = resolve;
|
|
1223
1313
|
});
|
|
@@ -1253,6 +1343,10 @@ export class AgentSession {
|
|
|
1253
1343
|
}
|
|
1254
1344
|
|
|
1255
1345
|
// Wait with exponential backoff (abortable)
|
|
1346
|
+
// Properly abort and null existing controller before replacing
|
|
1347
|
+
if (this._retryAbortController) {
|
|
1348
|
+
this._retryAbortController.abort();
|
|
1349
|
+
}
|
|
1256
1350
|
this._retryAbortController = new AbortController();
|
|
1257
1351
|
try {
|
|
1258
1352
|
await this._sleep(delayMs, this._retryAbortController.signal);
|
|
@@ -1472,13 +1566,17 @@ export class AgentSession {
|
|
|
1472
1566
|
this.agent.replaceMessages(sessionContext.messages);
|
|
1473
1567
|
|
|
1474
1568
|
// Restore model if saved
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
const
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
this.
|
|
1569
|
+
const defaultModelStr = sessionContext.models.default;
|
|
1570
|
+
if (defaultModelStr) {
|
|
1571
|
+
const slashIdx = defaultModelStr.indexOf("/");
|
|
1572
|
+
if (slashIdx > 0) {
|
|
1573
|
+
const provider = defaultModelStr.slice(0, slashIdx);
|
|
1574
|
+
const modelId = defaultModelStr.slice(slashIdx + 1);
|
|
1575
|
+
const availableModels = await this._modelRegistry.getAvailable();
|
|
1576
|
+
const match = availableModels.find((m) => m.provider === provider && m.id === modelId);
|
|
1577
|
+
if (match) {
|
|
1578
|
+
this.agent.setModel(match);
|
|
1579
|
+
}
|
|
1482
1580
|
}
|
|
1483
1581
|
}
|
|
1484
1582
|
|
|
@@ -1840,6 +1938,68 @@ export class AgentSession {
|
|
|
1840
1938
|
return text.trim() || undefined;
|
|
1841
1939
|
}
|
|
1842
1940
|
|
|
1941
|
+
/**
|
|
1942
|
+
* Format the entire session as plain text for clipboard export.
|
|
1943
|
+
* Includes user messages, assistant text, thinking blocks, tool calls, and tool results.
|
|
1944
|
+
*/
|
|
1945
|
+
formatSessionAsText(): string {
|
|
1946
|
+
const lines: string[] = [];
|
|
1947
|
+
|
|
1948
|
+
for (const msg of this.messages) {
|
|
1949
|
+
if (msg.role === "user") {
|
|
1950
|
+
lines.push("## User\n");
|
|
1951
|
+
if (typeof msg.content === "string") {
|
|
1952
|
+
lines.push(msg.content);
|
|
1953
|
+
} else {
|
|
1954
|
+
for (const c of msg.content) {
|
|
1955
|
+
if (c.type === "text") {
|
|
1956
|
+
lines.push(c.text);
|
|
1957
|
+
} else if (c.type === "image") {
|
|
1958
|
+
lines.push("[Image]");
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
lines.push("\n");
|
|
1963
|
+
} else if (msg.role === "assistant") {
|
|
1964
|
+
const assistantMsg = msg as AssistantMessage;
|
|
1965
|
+
lines.push("## Assistant\n");
|
|
1966
|
+
|
|
1967
|
+
for (const c of assistantMsg.content) {
|
|
1968
|
+
if (c.type === "text") {
|
|
1969
|
+
lines.push(c.text);
|
|
1970
|
+
} else if (c.type === "thinking") {
|
|
1971
|
+
lines.push("<thinking>");
|
|
1972
|
+
lines.push(c.thinking);
|
|
1973
|
+
lines.push("</thinking>\n");
|
|
1974
|
+
} else if (c.type === "toolCall") {
|
|
1975
|
+
lines.push(`### Tool: ${c.name}`);
|
|
1976
|
+
lines.push("```json");
|
|
1977
|
+
lines.push(JSON.stringify(c.arguments, null, 2));
|
|
1978
|
+
lines.push("```\n");
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
lines.push("");
|
|
1982
|
+
} else if (msg.role === "toolResult") {
|
|
1983
|
+
lines.push(`### Tool Result: ${msg.toolName}`);
|
|
1984
|
+
if (msg.isError) {
|
|
1985
|
+
lines.push("(error)");
|
|
1986
|
+
}
|
|
1987
|
+
for (const c of msg.content) {
|
|
1988
|
+
if (c.type === "text") {
|
|
1989
|
+
lines.push("```");
|
|
1990
|
+
lines.push(c.text);
|
|
1991
|
+
lines.push("```");
|
|
1992
|
+
} else if (c.type === "image") {
|
|
1993
|
+
lines.push("[Image output]");
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
lines.push("");
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
return lines.join("\n").trim();
|
|
2001
|
+
}
|
|
2002
|
+
|
|
1843
2003
|
// =========================================================================
|
|
1844
2004
|
// Hook System
|
|
1845
2005
|
// =========================================================================
|
|
@@ -1891,8 +2051,8 @@ export class AgentSession {
|
|
|
1891
2051
|
if (tool.onSession) {
|
|
1892
2052
|
try {
|
|
1893
2053
|
await tool.onSession(event, ctx);
|
|
1894
|
-
} catch (
|
|
1895
|
-
|
|
2054
|
+
} catch (err) {
|
|
2055
|
+
logger.warn("Tool onSession error", { error: String(err) });
|
|
1896
2056
|
}
|
|
1897
2057
|
}
|
|
1898
2058
|
}
|
|
@@ -11,14 +11,19 @@ import { tmpdir } from "node:os";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import type { Subprocess } from "bun";
|
|
13
13
|
import stripAnsi from "strip-ansi";
|
|
14
|
-
import { getShellConfig, killProcessTree, sanitizeBinaryOutput } from "../utils/shell
|
|
15
|
-
import {
|
|
14
|
+
import { getShellConfig, killProcessTree, sanitizeBinaryOutput } from "../utils/shell";
|
|
15
|
+
import { getOrCreateSnapshot, getSnapshotSourceCommand } from "../utils/shell-snapshot";
|
|
16
|
+
import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate";
|
|
16
17
|
|
|
17
18
|
// ============================================================================
|
|
18
19
|
// Types
|
|
19
20
|
// ============================================================================
|
|
20
21
|
|
|
21
22
|
export interface BashExecutorOptions {
|
|
23
|
+
/** Working directory for command execution */
|
|
24
|
+
cwd?: string;
|
|
25
|
+
/** Timeout in milliseconds */
|
|
26
|
+
timeout?: number;
|
|
22
27
|
/** Callback for streaming output chunks (already sanitized) */
|
|
23
28
|
onChunk?: (chunk: string) => void;
|
|
24
29
|
/** AbortSignal for cancellation */
|
|
@@ -56,13 +61,24 @@ export interface BashResult {
|
|
|
56
61
|
* @param options - Optional streaming callback and abort signal
|
|
57
62
|
* @returns Promise resolving to execution result
|
|
58
63
|
*/
|
|
59
|
-
export function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {
|
|
64
|
+
export async function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {
|
|
65
|
+
const { shell, args, env, prefix } = getShellConfig();
|
|
66
|
+
|
|
67
|
+
// Get or create shell snapshot (for aliases, functions, options)
|
|
68
|
+
const snapshotPath = await getOrCreateSnapshot(shell, env);
|
|
69
|
+
const snapshotPrefix = getSnapshotSourceCommand(snapshotPath);
|
|
70
|
+
|
|
71
|
+
// Build final command: snapshot + prefix + command
|
|
72
|
+
const prefixedCommand = prefix ? `${prefix} ${command}` : command;
|
|
73
|
+
const finalCommand = `${snapshotPrefix}${prefixedCommand}`;
|
|
74
|
+
|
|
60
75
|
return new Promise((resolve, reject) => {
|
|
61
|
-
const
|
|
62
|
-
|
|
76
|
+
const child: Subprocess = Bun.spawn([shell, ...args, finalCommand], {
|
|
77
|
+
cwd: options?.cwd,
|
|
63
78
|
stdin: "ignore",
|
|
64
79
|
stdout: "pipe",
|
|
65
80
|
stderr: "pipe",
|
|
81
|
+
env,
|
|
66
82
|
});
|
|
67
83
|
|
|
68
84
|
// Track sanitized output for truncation
|
|
@@ -74,16 +90,27 @@ export function executeBash(command: string, options?: BashExecutorOptions): Pro
|
|
|
74
90
|
let tempFilePath: string | undefined;
|
|
75
91
|
let tempFileStream: WriteStream | undefined;
|
|
76
92
|
let totalBytes = 0;
|
|
93
|
+
let timedOut = false;
|
|
77
94
|
|
|
78
|
-
// Handle abort signal
|
|
95
|
+
// Handle abort signal and timeout
|
|
79
96
|
const abortHandler = () => {
|
|
80
97
|
killProcessTree(child.pid);
|
|
81
98
|
};
|
|
82
99
|
|
|
100
|
+
// Set up timeout if specified
|
|
101
|
+
let timeoutHandle: Timer | undefined;
|
|
102
|
+
if (options?.timeout && options.timeout > 0) {
|
|
103
|
+
timeoutHandle = setTimeout(() => {
|
|
104
|
+
timedOut = true;
|
|
105
|
+
abortHandler();
|
|
106
|
+
}, options.timeout);
|
|
107
|
+
}
|
|
108
|
+
|
|
83
109
|
if (options?.signal) {
|
|
84
110
|
if (options.signal.aborted) {
|
|
85
111
|
// Already aborted, don't even start
|
|
86
112
|
child.kill();
|
|
113
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
87
114
|
resolve({
|
|
88
115
|
output: "",
|
|
89
116
|
exitCode: undefined,
|
|
@@ -156,11 +183,11 @@ export function executeBash(command: string, options?: BashExecutorOptions): Pro
|
|
|
156
183
|
|
|
157
184
|
const exitCode = await child.exited;
|
|
158
185
|
|
|
159
|
-
// Clean up
|
|
186
|
+
// Clean up
|
|
187
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
160
188
|
if (options?.signal) {
|
|
161
189
|
options.signal.removeEventListener("abort", abortHandler);
|
|
162
190
|
}
|
|
163
|
-
|
|
164
191
|
if (tempFileStream) {
|
|
165
192
|
tempFileStream.end();
|
|
166
193
|
}
|
|
@@ -169,6 +196,19 @@ export function executeBash(command: string, options?: BashExecutorOptions): Pro
|
|
|
169
196
|
const fullOutput = outputChunks.join("");
|
|
170
197
|
const truncationResult = truncateTail(fullOutput);
|
|
171
198
|
|
|
199
|
+
// Handle timeout
|
|
200
|
+
if (timedOut) {
|
|
201
|
+
const timeoutSecs = Math.round((options?.timeout || 0) / 1000);
|
|
202
|
+
resolve({
|
|
203
|
+
output: `${fullOutput}\n\nCommand timed out after ${timeoutSecs} seconds`,
|
|
204
|
+
exitCode: undefined,
|
|
205
|
+
cancelled: true,
|
|
206
|
+
truncated: truncationResult.truncated,
|
|
207
|
+
fullOutputPath: tempFilePath,
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
172
212
|
// Non-zero exit codes or signal-killed processes are considered cancelled if killed via signal
|
|
173
213
|
const cancelled = exitCode === null || (exitCode !== 0 && (options?.signal?.aborted ?? false));
|
|
174
214
|
|
|
@@ -180,11 +220,11 @@ export function executeBash(command: string, options?: BashExecutorOptions): Pro
|
|
|
180
220
|
fullOutputPath: tempFilePath,
|
|
181
221
|
});
|
|
182
222
|
} catch (err) {
|
|
183
|
-
// Clean up
|
|
223
|
+
// Clean up
|
|
224
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
184
225
|
if (options?.signal) {
|
|
185
226
|
options.signal.removeEventListener("abort", abortHandler);
|
|
186
227
|
}
|
|
187
|
-
|
|
188
228
|
if (tempFileStream) {
|
|
189
229
|
tempFileStream.end();
|
|
190
230
|
}
|
|
@@ -13,9 +13,9 @@ import {
|
|
|
13
13
|
createBranchSummaryMessage,
|
|
14
14
|
createCompactionSummaryMessage,
|
|
15
15
|
createHookMessage,
|
|
16
|
-
} from "../messages
|
|
17
|
-
import type { ReadonlySessionManager, SessionEntry } from "../session-manager
|
|
18
|
-
import { estimateTokens } from "./compaction
|
|
16
|
+
} from "../messages";
|
|
17
|
+
import type { ReadonlySessionManager, SessionEntry } from "../session-manager";
|
|
18
|
+
import { estimateTokens } from "./compaction";
|
|
19
19
|
import {
|
|
20
20
|
computeFileLists,
|
|
21
21
|
createFileOps,
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
formatFileOperations,
|
|
25
25
|
SUMMARIZATION_SYSTEM_PROMPT,
|
|
26
26
|
serializeConversation,
|
|
27
|
-
} from "./utils
|
|
27
|
+
} from "./utils";
|
|
28
28
|
|
|
29
29
|
// ============================================================================
|
|
30
30
|
// Types
|
|
@@ -44,7 +44,7 @@ export interface BranchSummaryDetails {
|
|
|
44
44
|
modifiedFiles: string[];
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export type { FileOperations } from "./utils
|
|
47
|
+
export type { FileOperations } from "./utils";
|
|
48
48
|
|
|
49
49
|
export interface BranchPreparation {
|
|
50
50
|
/** Messages extracted for summarization, in chronological order */
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
9
9
|
import type { AssistantMessage, Model, Usage } from "@oh-my-pi/pi-ai";
|
|
10
10
|
import { complete, completeSimple } from "@oh-my-pi/pi-ai";
|
|
11
|
-
import { convertToLlm, createBranchSummaryMessage, createHookMessage } from "../messages
|
|
12
|
-
import type { CompactionEntry, SessionEntry } from "../session-manager
|
|
11
|
+
import { convertToLlm, createBranchSummaryMessage, createHookMessage } from "../messages";
|
|
12
|
+
import type { CompactionEntry, SessionEntry } from "../session-manager";
|
|
13
13
|
import {
|
|
14
14
|
computeFileLists,
|
|
15
15
|
createFileOps,
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
formatFileOperations,
|
|
19
19
|
SUMMARIZATION_SYSTEM_PROMPT,
|
|
20
20
|
serializeConversation,
|
|
21
|
-
} from "./utils
|
|
21
|
+
} from "./utils";
|
|
22
22
|
|
|
23
23
|
// ============================================================================
|
|
24
24
|
// File Operation Tracking
|