@oh-my-pi/pi-coding-agent 15.0.0 → 15.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 +41 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +9 -9
- package/scripts/build-binary.ts +5 -0
- package/src/autoresearch/helpers.ts +17 -0
- package/src/autoresearch/tools/log-experiment.ts +9 -17
- package/src/autoresearch/tools/run-experiment.ts +2 -17
- package/src/capability/skill.ts +7 -0
- package/src/cli/list-models.ts +1 -1
- package/src/cli/shell-cli.ts +3 -13
- package/src/cli/update-cli.ts +1 -1
- package/src/cli.ts +10 -29
- package/src/commit/agentic/tools/propose-changelog.ts +8 -1
- package/src/commit/analysis/conventional.ts +8 -66
- package/src/commit/map-reduce/reduce-phase.ts +6 -65
- package/src/commit/pipeline.ts +2 -2
- package/src/commit/shared-llm.ts +89 -0
- package/src/config/config-file.ts +210 -0
- package/src/config/model-equivalence.ts +8 -11
- package/src/config/model-registry.ts +13 -2
- package/src/config/model-resolver.ts +1 -4
- package/src/config/settings-schema.ts +71 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/edit/renderer.ts +7 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +2 -2
- package/src/eval/py/executor.ts +5 -0
- package/src/exa/factory.ts +2 -2
- package/src/exa/mcp-client.ts +74 -1
- package/src/exec/bash-executor.ts +5 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -11
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +89 -223
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +9 -0
- package/src/goals/index.ts +3 -0
- package/src/goals/runtime.ts +500 -0
- package/src/goals/state.ts +37 -0
- package/src/goals/tools/goal-tool.ts +237 -0
- package/src/hashline/anchors.ts +2 -2
- package/src/hindsight/mental-models.ts +1 -1
- package/src/internal-urls/agent-protocol.ts +1 -20
- package/src/internal-urls/artifact-protocol.ts +1 -19
- package/src/internal-urls/docs-index.generated.ts +5 -6
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/main.ts +11 -2
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +79 -45
- package/src/modes/components/assistant-message.ts +14 -8
- package/src/modes/components/bash-execution.ts +24 -63
- package/src/modes/components/custom-message.ts +14 -40
- package/src/modes/components/eval-execution.ts +27 -57
- package/src/modes/components/execution-shared.ts +102 -0
- package/src/modes/components/hook-message.ts +17 -49
- package/src/modes/components/mcp-add-wizard.ts +26 -5
- package/src/modes/components/message-frame.ts +88 -0
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +6 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +55 -4
- package/src/modes/components/status-line/types.ts +4 -0
- package/src/modes/components/status-line.ts +28 -10
- package/src/modes/components/tool-execution.ts +7 -8
- package/src/modes/controllers/command-controller-shared.ts +108 -0
- package/src/modes/controllers/command-controller.ts +13 -4
- package/src/modes/controllers/event-controller.ts +36 -7
- package/src/modes/controllers/input-controller.ts +13 -0
- package/src/modes/controllers/mcp-command-controller.ts +56 -61
- package/src/modes/controllers/ssh-command-controller.ts +18 -57
- package/src/modes/interactive-mode.ts +624 -52
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/rpc-mode.ts +14 -87
- package/src/modes/runtime-init.ts +115 -0
- package/src/modes/theme/defaults/dark-poimandres.json +2 -0
- package/src/modes/theme/defaults/light-poimandres.json +2 -0
- package/src/modes/theme/theme.ts +18 -6
- package/src/modes/types.ts +14 -3
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +10 -3
- package/src/plan-mode/approved-plan.ts +35 -1
- package/src/prompts/goals/goal-budget-limit.md +16 -0
- package/src/prompts/goals/goal-continuation.md +28 -0
- package/src/prompts/goals/goal-mode-active.md +23 -0
- package/src/prompts/system/plan-mode-active.md +5 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/tools/bash.md +6 -0
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +102 -114
- package/src/prompts/tools/read.md +1 -0
- package/src/prompts/tools/resolve.md +6 -5
- package/src/sdk.ts +12 -5
- package/src/session/agent-session.ts +428 -106
- package/src/session/blob-store.ts +36 -3
- package/src/session/messages.ts +67 -2
- package/src/session/session-manager.ts +131 -12
- package/src/session/session-storage.ts +33 -15
- package/src/session/streaming-output.ts +309 -13
- package/src/slash-commands/builtin-registry.ts +18 -0
- package/src/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/executor.ts +17 -7
- package/src/task/index.ts +3 -0
- package/src/task/render.ts +21 -15
- package/src/task/types.ts +4 -0
- package/src/tools/ast-edit.ts +21 -120
- package/src/tools/ast-grep.ts +21 -119
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +27 -4
- package/src/tools/browser/attach.ts +3 -3
- package/src/tools/browser/launch.ts +81 -18
- package/src/tools/browser/registry.ts +1 -5
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +15 -4
- package/src/tools/eval.ts +3 -1
- package/src/tools/find.ts +20 -38
- package/src/tools/gh.ts +7 -6
- package/src/tools/index.ts +22 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/output-meta.ts +176 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +516 -233
- package/src/tools/render-utils.ts +92 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +72 -44
- package/src/tools/search.ts +120 -186
- package/src/tools/write.ts +44 -9
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/image-loading.ts +7 -3
- package/src/utils/image-resize.ts +32 -43
- package/src/vim/parser.ts +0 -17
- package/src/vim/render.ts +1 -1
- package/src/vim/types.ts +1 -1
- package/src/web/search/providers/gemini.ts +35 -95
- package/src/prompts/tools/exit-plan-mode.md +0 -6
- package/src/tools/exit-plan-mode.ts +0 -97
- package/src/utils/fuzzy.ts +0 -108
- package/src/utils/image-convert.ts +0 -27
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for internal-url protocol handlers that resolve IDs against
|
|
3
|
+
* registered agent sessions.
|
|
4
|
+
*/
|
|
5
|
+
import { AgentRegistry } from "../registry/agent-registry";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Snapshot of artifacts dirs for every registered session, deduped.
|
|
9
|
+
*
|
|
10
|
+
* Prefers `sessionManager.getArtifactsDir()` because subagents adopt their
|
|
11
|
+
* parent's `ArtifactManager` and report the parent's dir there; dedup then
|
|
12
|
+
* collapses parent + N subagents (the whole agent tree) to one entry. Falls
|
|
13
|
+
* back to the raw session file (with the `.jsonl` suffix stripped) when no
|
|
14
|
+
* live session reference is attached.
|
|
15
|
+
*/
|
|
16
|
+
export function artifactsDirsFromRegistry(): string[] {
|
|
17
|
+
const dirs: string[] = [];
|
|
18
|
+
for (const ref of AgentRegistry.global().list()) {
|
|
19
|
+
const dir =
|
|
20
|
+
ref.session?.sessionManager.getArtifactsDir() ?? (ref.sessionFile ? ref.sessionFile.slice(0, -6) : null);
|
|
21
|
+
if (!dir) continue;
|
|
22
|
+
if (!dirs.includes(dir)) dirs.push(dir);
|
|
23
|
+
}
|
|
24
|
+
return dirs;
|
|
25
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -129,7 +129,7 @@ export async function submitInteractiveInput(
|
|
|
129
129
|
InteractiveMode,
|
|
130
130
|
"markPendingSubmissionStarted" | "finishPendingSubmission" | "showError" | "checkShutdownRequested"
|
|
131
131
|
>,
|
|
132
|
-
session: Pick<AgentSession, "prompt">,
|
|
132
|
+
session: Pick<AgentSession, "prompt" | "promptCustomMessage">,
|
|
133
133
|
input: SubmittedUserInput,
|
|
134
134
|
): Promise<void> {
|
|
135
135
|
if (input.cancelled) {
|
|
@@ -141,7 +141,16 @@ export async function submitInteractiveInput(
|
|
|
141
141
|
if (!input.started && !mode.markPendingSubmissionStarted(input)) {
|
|
142
142
|
return;
|
|
143
143
|
}
|
|
144
|
-
|
|
144
|
+
if (input.customType) {
|
|
145
|
+
await session.promptCustomMessage({
|
|
146
|
+
customType: input.customType,
|
|
147
|
+
content: input.text,
|
|
148
|
+
display: input.display ?? false,
|
|
149
|
+
attribution: "agent",
|
|
150
|
+
});
|
|
151
|
+
} else {
|
|
152
|
+
await session.prompt(input.text, { images: input.images });
|
|
153
|
+
}
|
|
145
154
|
} catch (error: unknown) {
|
|
146
155
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
147
156
|
mode.showError(errorMessage);
|
package/src/mcp/oauth-flow.ts
CHANGED
|
@@ -133,6 +133,26 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
133
133
|
this.#resolvedClientId = this.#resolveClientId(config);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Client id used during the authorization request. Returns the value supplied
|
|
138
|
+
* via {@link MCPOAuthConfig.clientId} or, when the server required dynamic
|
|
139
|
+
* client registration, the id issued during registration. `undefined` until
|
|
140
|
+
* {@link generateAuthUrl} (or {@link login}) has run for a server that needs
|
|
141
|
+
* a client id.
|
|
142
|
+
*/
|
|
143
|
+
get resolvedClientId(): string | undefined {
|
|
144
|
+
return this.#resolvedClientId;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Client secret issued by dynamic client registration, if any. Always
|
|
149
|
+
* `undefined` for PKCE-only/public clients and when the caller supplies the
|
|
150
|
+
* client id via config.
|
|
151
|
+
*/
|
|
152
|
+
get registeredClientSecret(): string | undefined {
|
|
153
|
+
return this.#registeredClientSecret;
|
|
154
|
+
}
|
|
155
|
+
|
|
136
156
|
async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
|
|
137
157
|
if (!this.#resolvedClientId) {
|
|
138
158
|
await this.#tryRegisterClient(redirectUri);
|
|
@@ -53,7 +53,7 @@ import type { MCPServerConfig } from "../../mcp/types";
|
|
|
53
53
|
import { loadAllExtensions } from "../../modes/components/extensions/state-manager";
|
|
54
54
|
import { theme } from "../../modes/theme/theme";
|
|
55
55
|
import type { AgentSession, AgentSessionEvent } from "../../session/agent-session";
|
|
56
|
-
import { SKILL_PROMPT_MESSAGE_TYPE } from "../../session/messages";
|
|
56
|
+
import { isSilentAbort, SKILL_PROMPT_MESSAGE_TYPE } from "../../session/messages";
|
|
57
57
|
import {
|
|
58
58
|
SessionManager,
|
|
59
59
|
type SessionInfo as StoredSessionInfo,
|
|
@@ -73,6 +73,15 @@ const MODEL_CONFIG_ID = "model";
|
|
|
73
73
|
const THINKING_CONFIG_ID = "thinking";
|
|
74
74
|
const THINKING_OFF = "off";
|
|
75
75
|
const SESSION_PAGE_SIZE = 50;
|
|
76
|
+
/**
|
|
77
|
+
* Delay between `session/new` (or `session/load` / `session/resume` /
|
|
78
|
+
* `unstable_session/fork`) returning and the agent firing the first
|
|
79
|
+
* notifications against the new session id. Mitigates Zed's
|
|
80
|
+
* `Received session notification for unknown session` race — see
|
|
81
|
+
* `#scheduleBootstrapUpdates`. Exported so the ACP test harness can
|
|
82
|
+
* wait past this guard without hard-coding the literal.
|
|
83
|
+
*/
|
|
84
|
+
export const ACP_BOOTSTRAP_RACE_GUARD_MS = 50;
|
|
76
85
|
|
|
77
86
|
type AgentImageContent = {
|
|
78
87
|
type: "image";
|
|
@@ -97,6 +106,9 @@ type ManagedSessionRecord = {
|
|
|
97
106
|
liveMessageId: string | undefined;
|
|
98
107
|
liveMessageProgress: { textEmitted: boolean; thoughtEmitted: boolean } | undefined;
|
|
99
108
|
extensionsConfigured: boolean;
|
|
109
|
+
// Installed inside `#scheduleBootstrapUpdates` (post-race-guard); released
|
|
110
|
+
// in `#disposeSessionRecord`. Lives independent of any prompt turn.
|
|
111
|
+
lifetimeUnsubscribe: (() => void) | undefined;
|
|
100
112
|
};
|
|
101
113
|
|
|
102
114
|
type ReplayableMessage = {
|
|
@@ -314,13 +326,7 @@ export class AcpAgent implements Agent {
|
|
|
314
326
|
sessionId: record.session.sessionId,
|
|
315
327
|
update: this.#buildCurrentModeUpdate(record.session),
|
|
316
328
|
});
|
|
317
|
-
await this.#
|
|
318
|
-
sessionId: record.session.sessionId,
|
|
319
|
-
update: {
|
|
320
|
-
sessionUpdate: "config_option_update",
|
|
321
|
-
configOptions: this.#buildConfigOptions(record.session),
|
|
322
|
-
},
|
|
323
|
-
});
|
|
329
|
+
await this.#pushConfigOptionUpdate(record);
|
|
324
330
|
return {};
|
|
325
331
|
}
|
|
326
332
|
|
|
@@ -354,27 +360,21 @@ export class AcpAgent implements Agent {
|
|
|
354
360
|
});
|
|
355
361
|
}
|
|
356
362
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
365
|
-
return { configOptions };
|
|
363
|
+
// For `thinking` the lifetime subscription pushes post-bootstrap; only
|
|
364
|
+
// push here when it's not yet installed so pre-bootstrap callers still
|
|
365
|
+
// see the change without a post-bootstrap duplicate.
|
|
366
|
+
const thinkingHandledBySubscription =
|
|
367
|
+
params.configId === THINKING_CONFIG_ID && record.lifetimeUnsubscribe !== undefined;
|
|
368
|
+
if (!thinkingHandledBySubscription) {
|
|
369
|
+
await this.#pushConfigOptionUpdate(record);
|
|
370
|
+
}
|
|
371
|
+
return { configOptions: this.#buildConfigOptions(record.session) };
|
|
366
372
|
}
|
|
367
373
|
|
|
368
374
|
async unstable_setSessionModel(params: SetSessionModelRequest): Promise<SetSessionModelResponse> {
|
|
369
375
|
const record = this.#getSessionRecord(params.sessionId);
|
|
370
376
|
await this.#setModelById(record.session, params.modelId);
|
|
371
|
-
await this.#
|
|
372
|
-
sessionId: record.session.sessionId,
|
|
373
|
-
update: {
|
|
374
|
-
sessionUpdate: "config_option_update",
|
|
375
|
-
configOptions: this.#buildConfigOptions(record.session),
|
|
376
|
-
},
|
|
377
|
-
});
|
|
377
|
+
await this.#pushConfigOptionUpdate(record);
|
|
378
378
|
return {};
|
|
379
379
|
}
|
|
380
380
|
|
|
@@ -432,13 +432,7 @@ export class AcpAgent implements Agent {
|
|
|
432
432
|
});
|
|
433
433
|
},
|
|
434
434
|
notifyConfigChanged: async () => {
|
|
435
|
-
await this.#
|
|
436
|
-
sessionId: record.session.sessionId,
|
|
437
|
-
update: {
|
|
438
|
-
sessionUpdate: "config_option_update",
|
|
439
|
-
configOptions: this.#buildConfigOptions(record.session),
|
|
440
|
-
},
|
|
441
|
-
});
|
|
435
|
+
await this.#pushConfigOptionUpdate(record);
|
|
442
436
|
},
|
|
443
437
|
});
|
|
444
438
|
if (builtinResult !== false) {
|
|
@@ -688,6 +682,8 @@ export class AcpAgent implements Agent {
|
|
|
688
682
|
async #registerPreparedSession(session: AgentSession, mcpServers: McpServer[]): Promise<ManagedSessionRecord> {
|
|
689
683
|
const record = this.#createManagedSessionRecord(session);
|
|
690
684
|
session.setClientBridge(createAcpClientBridge(this.#connection, session.sessionId, this.#clientCapabilities));
|
|
685
|
+
// `record.lifetimeUnsubscribe` is installed in `#scheduleBootstrapUpdates`
|
|
686
|
+
// so it shares the bootstrap race guard — see that comment for why.
|
|
691
687
|
try {
|
|
692
688
|
await this.#configureExtensions(record);
|
|
693
689
|
await this.#configureMcpServers(record, mcpServers);
|
|
@@ -707,9 +703,24 @@ export class AcpAgent implements Agent {
|
|
|
707
703
|
liveMessageId: undefined,
|
|
708
704
|
liveMessageProgress: undefined,
|
|
709
705
|
extensionsConfigured: false,
|
|
706
|
+
lifetimeUnsubscribe: undefined,
|
|
710
707
|
};
|
|
711
708
|
}
|
|
712
709
|
|
|
710
|
+
async #handleLifetimeEvent(record: ManagedSessionRecord, event: AgentSessionEvent): Promise<void> {
|
|
711
|
+
if (event.type !== "thinking_level_changed") {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
try {
|
|
715
|
+
await this.#pushConfigOptionUpdate(record);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
logger.warn("Failed to push thinking-level config_option_update", {
|
|
718
|
+
sessionId: record.session.sessionId,
|
|
719
|
+
error,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
713
724
|
#getSessionRecord(sessionId: string): ManagedSessionRecord {
|
|
714
725
|
const record = this.#sessions.get(sessionId);
|
|
715
726
|
if (!record) {
|
|
@@ -912,6 +923,16 @@ export class AcpAgent implements Agent {
|
|
|
912
923
|
};
|
|
913
924
|
}
|
|
914
925
|
|
|
926
|
+
async #pushConfigOptionUpdate(record: ManagedSessionRecord): Promise<void> {
|
|
927
|
+
await this.#connection.sessionUpdate({
|
|
928
|
+
sessionId: record.session.sessionId,
|
|
929
|
+
update: {
|
|
930
|
+
sessionUpdate: "config_option_update",
|
|
931
|
+
configOptions: this.#buildConfigOptions(record.session),
|
|
932
|
+
},
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
|
|
915
936
|
#buildConfigOptions(session: AgentSession): SessionConfigOption[] {
|
|
916
937
|
const currentModeId = this.#getCurrentModeId(session);
|
|
917
938
|
const modeOptions = this.#getAvailableModes(session).map(mode => ({
|
|
@@ -1124,18 +1145,25 @@ export class AcpAgent implements Agent {
|
|
|
1124
1145
|
}
|
|
1125
1146
|
|
|
1126
1147
|
#scheduleBootstrapUpdates(sessionId: string): void {
|
|
1127
|
-
//
|
|
1128
|
-
//
|
|
1129
|
-
//
|
|
1130
|
-
//
|
|
1131
|
-
//
|
|
1132
|
-
//
|
|
1133
|
-
//
|
|
1134
|
-
//
|
|
1135
|
-
//
|
|
1136
|
-
//
|
|
1137
|
-
//
|
|
1138
|
-
//
|
|
1148
|
+
// Defer first notifications until the response has reached the client.
|
|
1149
|
+
// Zed's agent-client-protocol reader dispatches responses and
|
|
1150
|
+
// notifications to different async tasks; sending the first
|
|
1151
|
+
// `available_commands_update` from `setTimeout(0)` reliably loses the
|
|
1152
|
+
// race against the response handler and Zed logs `Received session
|
|
1153
|
+
// notification for unknown session` then drops the update — leaving
|
|
1154
|
+
// the slash-command palette empty (#1015 follow-up; see
|
|
1155
|
+
// zed-industries/zed#55965 for the same race biting other ACP agents).
|
|
1156
|
+
// `ACP_BOOTSTRAP_RACE_GUARD_MS` is invisible to the operator and large
|
|
1157
|
+
// enough that the response future has scheduled before our timer fires
|
|
1158
|
+
// on stdio-only transports.
|
|
1159
|
+
//
|
|
1160
|
+
// The session-lifetime subscription is installed inside the same timer
|
|
1161
|
+
// so it shares this guard — without it, an extension's `session_start`
|
|
1162
|
+
// handler (or any async work it schedules) calling `setThinkingLevel`
|
|
1163
|
+
// would push a `config_option_update` for a session id the client
|
|
1164
|
+
// hasn't been told about yet. The pre-bootstrap thinking level is
|
|
1165
|
+
// reported in the response's `configOptions`, so deferring the
|
|
1166
|
+
// notification loses no state.
|
|
1139
1167
|
setTimeout(() => {
|
|
1140
1168
|
if (this.#connection.signal.aborted) {
|
|
1141
1169
|
return;
|
|
@@ -1144,8 +1172,13 @@ export class AcpAgent implements Agent {
|
|
|
1144
1172
|
if (!record) {
|
|
1145
1173
|
return;
|
|
1146
1174
|
}
|
|
1175
|
+
if (!record.lifetimeUnsubscribe) {
|
|
1176
|
+
record.lifetimeUnsubscribe = record.session.subscribe(event => {
|
|
1177
|
+
void this.#handleLifetimeEvent(record, event);
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1147
1180
|
void this.#emitBootstrapUpdates(sessionId, record);
|
|
1148
|
-
},
|
|
1181
|
+
}, ACP_BOOTSTRAP_RACE_GUARD_MS);
|
|
1149
1182
|
}
|
|
1150
1183
|
|
|
1151
1184
|
async #emitBootstrapUpdates(sessionId: string, record: ManagedSessionRecord): Promise<void> {
|
|
@@ -1393,7 +1426,7 @@ export class AcpAgent implements Agent {
|
|
|
1393
1426
|
}
|
|
1394
1427
|
}
|
|
1395
1428
|
}
|
|
1396
|
-
if (notifications.length === 0 && message.errorMessage) {
|
|
1429
|
+
if (notifications.length === 0 && message.errorMessage && !isSilentAbort(message.errorMessage)) {
|
|
1397
1430
|
notifications.push({
|
|
1398
1431
|
sessionId,
|
|
1399
1432
|
update: {
|
|
@@ -1674,6 +1707,7 @@ export class AcpAgent implements Agent {
|
|
|
1674
1707
|
}
|
|
1675
1708
|
|
|
1676
1709
|
async #disposeSessionRecord(record: ManagedSessionRecord): Promise<void> {
|
|
1710
|
+
record.lifetimeUnsubscribe?.();
|
|
1677
1711
|
if (record.mcpManager) {
|
|
1678
1712
|
try {
|
|
1679
1713
|
await record.mcpManager.disconnectAll();
|
|
@@ -3,8 +3,8 @@ import { Container, Image, ImageProtocol, Markdown, Spacer, TERMINAL, Text } fro
|
|
|
3
3
|
import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { settings } from "../../config/settings";
|
|
5
5
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
6
|
+
import { isSilentAbort } from "../../session/messages";
|
|
6
7
|
import { resolveImageOptions } from "../../tools/render-utils";
|
|
7
|
-
import { convertToPng } from "../../utils/image-convert";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Component that renders a complete assistant message
|
|
@@ -76,14 +76,15 @@ export class AssistantMessageComponent extends Container {
|
|
|
76
76
|
const key = `${toolCallId}:${index}`;
|
|
77
77
|
if (this.#convertedKittyImages.has(key) || this.#kittyConversionsInFlight.has(key)) continue;
|
|
78
78
|
this.#kittyConversionsInFlight.add(key);
|
|
79
|
-
|
|
80
|
-
.
|
|
79
|
+
new Bun.Image(Buffer.from(image.data, "base64"))
|
|
80
|
+
.png()
|
|
81
|
+
.toBase64()
|
|
82
|
+
.then(data => {
|
|
81
83
|
this.#kittyConversionsInFlight.delete(key);
|
|
82
|
-
if (!converted) return;
|
|
83
84
|
this.#convertedKittyImages.set(key, {
|
|
84
85
|
type: "image",
|
|
85
|
-
data
|
|
86
|
-
mimeType:
|
|
86
|
+
data,
|
|
87
|
+
mimeType: "image/png",
|
|
87
88
|
});
|
|
88
89
|
if (this.#lastMessage) {
|
|
89
90
|
this.updateContent(this.#lastMessage);
|
|
@@ -184,7 +185,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
184
185
|
// But only if there are no tool calls (tool execution components will show the error)
|
|
185
186
|
const hasToolCalls = message.content.some(c => c.type === "toolCall");
|
|
186
187
|
if (!hasToolCalls) {
|
|
187
|
-
if (message.stopReason === "aborted") {
|
|
188
|
+
if (message.stopReason === "aborted" && !isSilentAbort(message.errorMessage)) {
|
|
188
189
|
const abortMessage =
|
|
189
190
|
message.errorMessage && message.errorMessage !== "Request was aborted"
|
|
190
191
|
? message.errorMessage
|
|
@@ -201,7 +202,12 @@ export class AssistantMessageComponent extends Container {
|
|
|
201
202
|
this.#contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
|
|
202
203
|
}
|
|
203
204
|
}
|
|
204
|
-
if (
|
|
205
|
+
if (
|
|
206
|
+
message.errorMessage &&
|
|
207
|
+
!isSilentAbort(message.errorMessage) &&
|
|
208
|
+
message.stopReason !== "aborted" &&
|
|
209
|
+
message.stopReason !== "error"
|
|
210
|
+
) {
|
|
205
211
|
this.#contentContainer.addChild(new Spacer(1));
|
|
206
212
|
this.#contentContainer.addChild(new Text(theme.fg("error", `Error: ${message.errorMessage}`), 1, 0));
|
|
207
213
|
}
|
|
@@ -7,19 +7,23 @@ import {
|
|
|
7
7
|
Container,
|
|
8
8
|
Ellipsis,
|
|
9
9
|
ImageProtocol,
|
|
10
|
-
Loader,
|
|
11
|
-
Spacer,
|
|
10
|
+
type Loader,
|
|
12
11
|
TERMINAL,
|
|
13
12
|
Text,
|
|
14
13
|
type TUI,
|
|
15
14
|
truncateToWidth,
|
|
16
15
|
visibleWidth,
|
|
17
16
|
} from "@oh-my-pi/pi-tui";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
17
|
+
import { theme } from "../../modes/theme/theme";
|
|
18
|
+
import type { TruncationMeta } from "../../tools/output-meta";
|
|
20
19
|
import { getSixelLineMask, isSixelPassthroughEnabled, sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
|
|
21
|
-
import {
|
|
22
|
-
|
|
20
|
+
import {
|
|
21
|
+
buildExecutionFrame,
|
|
22
|
+
buildStatusFooter,
|
|
23
|
+
createCollapsedPreview,
|
|
24
|
+
type ExecutionStatus,
|
|
25
|
+
resolveExecutionStatus,
|
|
26
|
+
} from "./execution-shared";
|
|
23
27
|
|
|
24
28
|
// Preview line limit when not expanded (matches tool execution behavior)
|
|
25
29
|
const PREVIEW_LINES = 20;
|
|
@@ -31,7 +35,7 @@ const CHUNK_THROTTLE_MS = 50;
|
|
|
31
35
|
|
|
32
36
|
export class BashExecutionComponent extends Container {
|
|
33
37
|
#outputLines: string[] = [];
|
|
34
|
-
#status:
|
|
38
|
+
#status: ExecutionStatus = "running";
|
|
35
39
|
#exitCode: number | undefined = undefined;
|
|
36
40
|
#loader: Loader;
|
|
37
41
|
#truncation?: TruncationMeta;
|
|
@@ -50,34 +54,14 @@ export class BashExecutionComponent extends Container {
|
|
|
50
54
|
|
|
51
55
|
// Use dim border for excluded-from-context commands (!! prefix)
|
|
52
56
|
const colorKey = excludeFromContext ? "dim" : "bashMode";
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
this.addChild(new Spacer(1));
|
|
57
|
-
|
|
58
|
-
// Top border
|
|
59
|
-
this.addChild(new DynamicBorder(borderColor));
|
|
60
|
-
|
|
61
|
-
// Content container (holds dynamic content between borders)
|
|
62
|
-
this.#contentContainer = new Container();
|
|
63
|
-
this.addChild(this.#contentContainer);
|
|
57
|
+
const { contentContainer, loader } = buildExecutionFrame(this, ui, colorKey);
|
|
58
|
+
this.#contentContainer = contentContainer;
|
|
59
|
+
this.#loader = loader;
|
|
64
60
|
|
|
65
61
|
// Command header
|
|
66
62
|
this.#headerText = new Text(theme.fg(colorKey, theme.bold(`$ ${command}`)), 1, 0);
|
|
67
63
|
this.#contentContainer.addChild(this.#headerText);
|
|
68
|
-
|
|
69
|
-
// Loader
|
|
70
|
-
this.#loader = new Loader(
|
|
71
|
-
ui,
|
|
72
|
-
spinner => theme.fg(colorKey, spinner),
|
|
73
|
-
text => theme.fg("muted", text),
|
|
74
|
-
`Running… (esc to cancel)`,
|
|
75
|
-
getSymbolTheme().spinnerFrames,
|
|
76
|
-
);
|
|
77
64
|
this.#contentContainer.addChild(this.#loader);
|
|
78
|
-
|
|
79
|
-
// Bottom border
|
|
80
|
-
this.addChild(new DynamicBorder(borderColor));
|
|
81
65
|
}
|
|
82
66
|
|
|
83
67
|
/**
|
|
@@ -130,11 +114,7 @@ export class BashExecutionComponent extends Container {
|
|
|
130
114
|
options?: { output?: string; truncation?: TruncationMeta },
|
|
131
115
|
): void {
|
|
132
116
|
this.#exitCode = exitCode;
|
|
133
|
-
this.#status = cancelled
|
|
134
|
-
? "cancelled"
|
|
135
|
-
: exitCode !== 0 && exitCode !== undefined && exitCode !== null
|
|
136
|
-
? "error"
|
|
137
|
-
: "complete";
|
|
117
|
+
this.#status = resolveExecutionStatus(exitCode, cancelled);
|
|
138
118
|
this.#truncation = options?.truncation;
|
|
139
119
|
if (options?.output !== undefined) {
|
|
140
120
|
this.#setOutput(options.output);
|
|
@@ -182,14 +162,7 @@ export class BashExecutionComponent extends Container {
|
|
|
182
162
|
} else {
|
|
183
163
|
// Use shared visual truncation utility, recomputed per render width
|
|
184
164
|
const styledOutput = previewLogicalLines.map(line => theme.fg("muted", line)).join("\n");
|
|
185
|
-
|
|
186
|
-
this.#contentContainer.addChild({
|
|
187
|
-
render: (width: number) => {
|
|
188
|
-
const { visualLines } = truncateToVisualLines(previewText, PREVIEW_LINES, width, 1);
|
|
189
|
-
return visualLines;
|
|
190
|
-
},
|
|
191
|
-
invalidate: () => {},
|
|
192
|
-
});
|
|
165
|
+
this.#contentContainer.addChild(createCollapsedPreview(`\n${styledOutput}`, PREVIEW_LINES));
|
|
193
166
|
}
|
|
194
167
|
}
|
|
195
168
|
|
|
@@ -197,26 +170,14 @@ export class BashExecutionComponent extends Container {
|
|
|
197
170
|
if (this.#status === "running") {
|
|
198
171
|
this.#contentContainer.addChild(this.#loader);
|
|
199
172
|
} else {
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (this.#
|
|
208
|
-
statusParts.push(theme.fg("warning", "(cancelled)"));
|
|
209
|
-
} else if (this.#status === "error") {
|
|
210
|
-
statusParts.push(theme.fg("error", `(exit ${this.#exitCode})`));
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (this.#truncation) {
|
|
214
|
-
statusParts.push(theme.fg("warning", formatTruncationMetaNotice(this.#truncation)));
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (statusParts.length > 0) {
|
|
218
|
-
this.#contentContainer.addChild(new Text(`\n${statusParts.join("\n")}`, 1, 0));
|
|
219
|
-
}
|
|
173
|
+
const footer = buildStatusFooter({
|
|
174
|
+
status: this.#status,
|
|
175
|
+
exitCode: this.#exitCode,
|
|
176
|
+
truncation: this.#truncation,
|
|
177
|
+
hiddenLineCount,
|
|
178
|
+
suppressHiddenCount: hasSixelOutput,
|
|
179
|
+
});
|
|
180
|
+
if (footer) this.#contentContainer.addChild(footer);
|
|
220
181
|
}
|
|
221
182
|
}
|
|
222
183
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { TextContent } from "@oh-my-pi/pi-ai";
|
|
2
1
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
3
|
-
import { Box, Container,
|
|
2
|
+
import { Box, Container, Spacer } from "@oh-my-pi/pi-tui";
|
|
4
3
|
import type { MessageRenderer } from "../../extensibility/extensions/types";
|
|
5
|
-
import {
|
|
4
|
+
import { theme } from "../../modes/theme/theme";
|
|
6
5
|
import type { CustomMessage } from "../../session/messages";
|
|
6
|
+
import { renderFramedMessage } from "./message-frame";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Component that renders a custom message entry from extensions.
|
|
@@ -41,51 +41,25 @@ export class CustomMessageComponent extends Container {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
#rebuild(): void {
|
|
44
|
-
// Remove previous content component
|
|
45
44
|
if (this.#customComponent) {
|
|
46
45
|
this.removeChild(this.#customComponent);
|
|
47
46
|
this.#customComponent = undefined;
|
|
48
47
|
}
|
|
49
48
|
this.removeChild(this.#box);
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
} catch {
|
|
61
|
-
// Fall through to default rendering
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Default rendering uses our box
|
|
66
|
-
this.addChild(this.#box);
|
|
67
|
-
this.#box.clear();
|
|
50
|
+
const custom = renderFramedMessage({
|
|
51
|
+
message: this.message,
|
|
52
|
+
box: this.#box,
|
|
53
|
+
expanded: this.#expanded,
|
|
54
|
+
customRenderer: this.customRenderer,
|
|
55
|
+
// Extension messages render full content; no collapse-on-fold behaviour.
|
|
56
|
+
});
|
|
68
57
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
this.#box.addChild(new Spacer(1));
|
|
73
|
-
|
|
74
|
-
// Extract text content
|
|
75
|
-
let text: string;
|
|
76
|
-
if (typeof this.message.content === "string") {
|
|
77
|
-
text = this.message.content;
|
|
58
|
+
if (custom) {
|
|
59
|
+
this.#customComponent = custom;
|
|
60
|
+
this.addChild(custom);
|
|
78
61
|
} else {
|
|
79
|
-
|
|
80
|
-
.filter((c): c is TextContent => c.type === "text")
|
|
81
|
-
.map(c => c.text)
|
|
82
|
-
.join("\n");
|
|
62
|
+
this.addChild(this.#box);
|
|
83
63
|
}
|
|
84
|
-
|
|
85
|
-
this.#box.addChild(
|
|
86
|
-
new Markdown(text, 0, 0, getMarkdownTheme(), {
|
|
87
|
-
color: (value: string) => theme.fg("customMessageText", value),
|
|
88
|
-
}),
|
|
89
|
-
);
|
|
90
64
|
}
|
|
91
65
|
}
|