@oh-my-pi/pi-coding-agent 16.2.0 → 16.2.2
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 +35 -0
- package/dist/cli.js +2709 -2715
- package/dist/types/advisor/runtime.d.ts +15 -1
- package/dist/types/advisor/watchdog.d.ts +11 -0
- package/dist/types/config/model-roles.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +32 -12
- package/dist/types/discovery/omp-extension-roots.d.ts +3 -3
- package/dist/types/edit/index.d.ts +18 -0
- package/dist/types/edit/streaming.d.ts +30 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +0 -11
- package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +0 -2
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +32 -2
- package/dist/types/session/blob-store.d.ts +4 -0
- package/dist/types/session/messages.d.ts +6 -7
- package/dist/types/session/settings-stream-fn.d.ts +21 -0
- package/dist/types/session/turn-persistence.d.ts +88 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +217 -0
- package/src/advisor/runtime.ts +65 -2
- package/src/advisor/watchdog.ts +15 -0
- package/src/auto-thinking/classifier.ts +2 -2
- package/src/config/model-resolver.ts +5 -1
- package/src/config/model-roles.ts +3 -3
- package/src/config/settings-schema.ts +30 -8
- package/src/discovery/claude-plugins.ts +3 -2
- package/src/discovery/omp-extension-roots.ts +38 -13
- package/src/edit/index.ts +21 -0
- package/src/edit/streaming.ts +170 -0
- package/src/extensibility/custom-tools/types.ts +1 -0
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +18 -1
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +59 -2
- package/src/extensibility/plugins/manager.ts +74 -4
- package/src/extensibility/shared-events.ts +1 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/mcp/oauth-discovery.ts +5 -29
- package/src/mcp/transports/http.ts +3 -1
- package/src/mnemopi/backend.ts +2 -2
- package/src/modes/acp/acp-agent.ts +15 -2
- package/src/modes/acp/acp-event-mapper.ts +89 -25
- package/src/modes/components/assistant-message.ts +5 -5
- package/src/modes/components/status-line/component.ts +1 -9
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/controllers/event-controller.ts +8 -11
- package/src/modes/interactive-mode.ts +0 -5
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/utils/transcript-render-helpers.ts +2 -2
- package/src/modes/utils/ui-helpers.ts +5 -4
- package/src/prompts/advisor/context-files.md +8 -0
- package/src/sdk.ts +23 -27
- package/src/session/agent-session.ts +330 -219
- package/src/session/blob-store.ts +24 -0
- package/src/session/messages.ts +20 -12
- package/src/session/session-persistence.ts +1 -2
- package/src/session/settings-stream-fn.ts +49 -0
- package/src/session/turn-persistence.ts +142 -0
- package/src/session/unexpected-stop-classifier.ts +2 -2
- package/src/slash-commands/helpers/mcp.ts +2 -1
- package/src/tiny/models.ts +8 -6
- package/src/tiny/text.ts +14 -7
- package/src/tools/image-gen.ts +2 -1
- package/src/tools/tts.ts +2 -1
- package/src/utils/title-generator.ts +1 -1
- package/src/web/search/providers/tavily.ts +36 -19
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Automatically detects OAuth requirements from MCP server responses
|
|
5
5
|
* and extracts authentication endpoints.
|
|
6
6
|
*/
|
|
7
|
+
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
7
8
|
import type { FetchImpl } from "@oh-my-pi/pi-ai/types";
|
|
8
9
|
|
|
9
10
|
export interface OAuthEndpoints {
|
|
@@ -23,8 +24,8 @@ export interface AuthDetectionResult {
|
|
|
23
24
|
message?: string;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
function
|
|
27
|
-
const match =
|
|
27
|
+
export function extractMcpAuthServerUrl(error: Error, serverUrl?: string): string | undefined {
|
|
28
|
+
const match = error.message.match(/Mcp-Auth-Server:\s*([^;\]\s]+)/i);
|
|
28
29
|
if (!match?.[1]) return undefined;
|
|
29
30
|
|
|
30
31
|
try {
|
|
@@ -34,32 +35,6 @@ function parseMcpAuthServerUrl(errorMessage: string, serverUrl?: string): string
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
export function extractMcpAuthServerUrl(error: Error, serverUrl?: string): string | undefined {
|
|
38
|
-
return parseMcpAuthServerUrl(error.message, serverUrl);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Detect if an error indicates authentication is required.
|
|
43
|
-
* Checks for common auth error patterns.
|
|
44
|
-
*/
|
|
45
|
-
export function detectAuthError(error: Error): boolean {
|
|
46
|
-
const errorMsg = error.message.toLowerCase();
|
|
47
|
-
|
|
48
|
-
// Check for HTTP auth status codes
|
|
49
|
-
if (
|
|
50
|
-
errorMsg.includes("401") ||
|
|
51
|
-
errorMsg.includes("403") ||
|
|
52
|
-
errorMsg.includes("unauthorized") ||
|
|
53
|
-
errorMsg.includes("forbidden") ||
|
|
54
|
-
errorMsg.includes("authentication required") ||
|
|
55
|
-
errorMsg.includes("authentication failed")
|
|
56
|
-
) {
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
38
|
/**
|
|
64
39
|
* Extract OAuth endpoints from error response.
|
|
65
40
|
* Looks for WWW-Authenticate header format or JSON error bodies.
|
|
@@ -200,7 +175,8 @@ export function extractOAuthEndpoints(error: Error): OAuthEndpoints | null {
|
|
|
200
175
|
* Returns structured info about what auth is needed.
|
|
201
176
|
*/
|
|
202
177
|
export function analyzeAuthError(error: Error, serverUrl?: string): AuthDetectionResult {
|
|
203
|
-
|
|
178
|
+
// No auth required unless the error carries an HTTP auth status / auth-failure phrasing.
|
|
179
|
+
if (!AIError.is(AIError.classify(error), AIError.Flag.AuthFailed)) {
|
|
204
180
|
return { requiresAuth: false };
|
|
205
181
|
}
|
|
206
182
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Implements JSON-RPC 2.0 over HTTP POST with optional SSE streaming.
|
|
5
5
|
* Based on MCP spec 2025-03-26.
|
|
6
6
|
*/
|
|
7
|
+
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
7
8
|
import { logger, readSseJson, Snowflake } from "@oh-my-pi/pi-utils";
|
|
8
9
|
import type {
|
|
9
10
|
JsonRpcError,
|
|
@@ -186,7 +187,8 @@ export class HttpTransport implements MCPTransport {
|
|
|
186
187
|
return await this.#executeRequest<T>(method, params, options);
|
|
187
188
|
} catch (error) {
|
|
188
189
|
// Retry once on auth failure if onAuthError is wired
|
|
189
|
-
|
|
190
|
+
const status = error instanceof Error ? AIError.status(error) : undefined;
|
|
191
|
+
if (this.onAuthError && (status === 401 || status === 403)) {
|
|
190
192
|
const newHeaders = await this.onAuthError();
|
|
191
193
|
if (newHeaders) {
|
|
192
194
|
// Persist refreshed headers so subsequent requests use them directly
|
package/src/mnemopi/backend.ts
CHANGED
|
@@ -511,10 +511,10 @@ async function resolveMnemopiProviderOptions(
|
|
|
511
511
|
}
|
|
512
512
|
|
|
513
513
|
try {
|
|
514
|
-
const resolved = resolveRoleSelection(["smol"], settings, modelRegistry.getAvailable(), modelRegistry);
|
|
514
|
+
const resolved = resolveRoleSelection(["tiny", "smol"], settings, modelRegistry.getAvailable(), modelRegistry);
|
|
515
515
|
const model = resolved?.model;
|
|
516
516
|
if (!model) {
|
|
517
|
-
logger.warn("Mnemopi: llmMode=smol but no smol model resolved; continuing without LLM.");
|
|
517
|
+
logger.warn("Mnemopi: llmMode=smol but no tiny/smol model resolved; continuing without LLM.");
|
|
518
518
|
return base;
|
|
519
519
|
}
|
|
520
520
|
return {
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
} from "@agentclientprotocol/sdk";
|
|
43
43
|
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
44
44
|
import type { AssistantMessage, Model } from "@oh-my-pi/pi-ai";
|
|
45
|
-
import { isEnoent, logger, VERSION } from "@oh-my-pi/pi-utils";
|
|
45
|
+
import { getBlobsDir, isEnoent, logger, VERSION } from "@oh-my-pi/pi-utils";
|
|
46
46
|
import { disableProvider, enableProvider, reset as resetCapabilities } from "../../capability";
|
|
47
47
|
import { Settings } from "../../config/settings";
|
|
48
48
|
import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
|
|
@@ -62,6 +62,7 @@ import { loadAllExtensions } from "../../modes/components/extensions/state-manag
|
|
|
62
62
|
import { theme } from "../../modes/theme/theme";
|
|
63
63
|
import { type PlanApprovalDetails, resolveApprovedPlan } from "../../plan-mode/approved-plan";
|
|
64
64
|
import type { AgentSession, AgentSessionEvent } from "../../session/agent-session";
|
|
65
|
+
import { BlobStore, resolveImageDataSync } from "../../session/blob-store";
|
|
65
66
|
import { isSilentAbort, SKILL_PROMPT_MESSAGE_TYPE, USER_INTERRUPT_LABEL } from "../../session/messages";
|
|
66
67
|
import type { UsageStatistics } from "../../session/session-entries";
|
|
67
68
|
import type { SessionInfo as StoredSessionInfo } from "../../session/session-listing";
|
|
@@ -445,6 +446,7 @@ export class AcpAgent implements Agent {
|
|
|
445
446
|
#cleanupRegistered = false;
|
|
446
447
|
#clientCapabilities: ClientCapabilities | undefined;
|
|
447
448
|
#cancelCleanupTimeoutMs = ACP_CANCEL_CLEANUP_TIMEOUT_MS;
|
|
449
|
+
#blobs = new BlobStore(getBlobsDir());
|
|
448
450
|
|
|
449
451
|
constructor(connection: AgentSideConnection, createSession: CreateAcpSession, initialSession?: AgentSession) {
|
|
450
452
|
this.#connection = connection;
|
|
@@ -1187,11 +1189,21 @@ export class AcpAgent implements Agent {
|
|
|
1187
1189
|
}
|
|
1188
1190
|
|
|
1189
1191
|
this.#prepareLiveAssistantMessage(record, event);
|
|
1192
|
+
const imageDataCache = new Map<string, string>();
|
|
1193
|
+
const resolveImageDataForAcp = (data: string, mimeType: string | undefined): string => {
|
|
1194
|
+
const key = `${mimeType ?? ""}\u0000${data}`;
|
|
1195
|
+
const cached = imageDataCache.get(key);
|
|
1196
|
+
if (cached !== undefined) return cached;
|
|
1197
|
+
const resolved = resolveImageDataSync(this.#blobs, data);
|
|
1198
|
+
imageDataCache.set(key, resolved);
|
|
1199
|
+
return resolved;
|
|
1200
|
+
};
|
|
1190
1201
|
for (const notification of mapAgentSessionEventToAcpSessionUpdates(event, record.session.sessionId, {
|
|
1191
1202
|
getMessageId: message => this.#getLiveMessageId(record, message),
|
|
1192
1203
|
getMessageProgress: message => this.#getLiveMessageProgress(record, message),
|
|
1193
1204
|
getToolArgs: toolCallId => record.toolArgsById.get(toolCallId),
|
|
1194
1205
|
cwd: record.session.sessionManager.getCwd(),
|
|
1206
|
+
resolveImageData: resolveImageDataForAcp,
|
|
1195
1207
|
})) {
|
|
1196
1208
|
await this.#connection.sessionUpdate(notification);
|
|
1197
1209
|
}
|
|
@@ -2002,7 +2014,7 @@ export class AcpAgent implements Agent {
|
|
|
2002
2014
|
}
|
|
2003
2015
|
}
|
|
2004
2016
|
}
|
|
2005
|
-
if (notifications.length === 0 && message.errorMessage && !isSilentAbort(message
|
|
2017
|
+
if (notifications.length === 0 && message.errorMessage && !isSilentAbort(message)) {
|
|
2006
2018
|
notifications.push({
|
|
2007
2019
|
sessionId,
|
|
2008
2020
|
update: {
|
|
@@ -2052,6 +2064,7 @@ export class AcpAgent implements Agent {
|
|
|
2052
2064
|
const notifications = mapAgentSessionEventToAcpSessionUpdates(endEvent, sessionId, {
|
|
2053
2065
|
cwd,
|
|
2054
2066
|
getToolArgs: toolCallId => (toolCallId === message.toolCallId ? options.toolArgs : undefined),
|
|
2067
|
+
resolveImageData: (data, _mimeType) => resolveImageDataSync(this.#blobs, data),
|
|
2055
2068
|
});
|
|
2056
2069
|
if (options.includeStart === false) {
|
|
2057
2070
|
return notifications;
|
|
@@ -20,6 +20,7 @@ interface AcpEventMapperOptions {
|
|
|
20
20
|
getMessageId?: (message: unknown) => string | undefined;
|
|
21
21
|
getMessageProgress?: (message: unknown) => MessageProgress | undefined;
|
|
22
22
|
getToolArgs?: (toolCallId: string) => unknown;
|
|
23
|
+
resolveImageData?: (data: string, mimeType: string | undefined) => string;
|
|
23
24
|
/**
|
|
24
25
|
* Session cwd. Tool call locations sent to ACP clients must be absolute
|
|
25
26
|
* (the editor host needs them to open or focus files). When provided,
|
|
@@ -179,7 +180,7 @@ export function mapAgentSessionEventToAcpSessionUpdates(
|
|
|
179
180
|
case "tool_execution_update": {
|
|
180
181
|
const content = mergeToolUpdateContent(
|
|
181
182
|
buildToolStartContent(event.toolName, event.args),
|
|
182
|
-
extractToolCallContent(event.partialResult),
|
|
183
|
+
extractToolCallContent(event.partialResult, options),
|
|
183
184
|
);
|
|
184
185
|
const update: SessionUpdate = {
|
|
185
186
|
sessionUpdate: "tool_call_update",
|
|
@@ -197,7 +198,10 @@ export function mapAgentSessionEventToAcpSessionUpdates(
|
|
|
197
198
|
return [toSessionNotification(sessionId, update)];
|
|
198
199
|
}
|
|
199
200
|
case "tool_execution_end": {
|
|
200
|
-
const resultContent = [
|
|
201
|
+
const resultContent = [
|
|
202
|
+
...extractDiffToolCallContent(event.result),
|
|
203
|
+
...extractToolCallContent(event.result, options),
|
|
204
|
+
];
|
|
201
205
|
const content = mergeToolUpdateContent(
|
|
202
206
|
buildToolStartContent(event.toolName, getToolExecutionEndArgs(event, options)),
|
|
203
207
|
resultContent,
|
|
@@ -641,13 +645,15 @@ function terminalToolCallContent(terminalId: string): ToolCallContent {
|
|
|
641
645
|
return { type: "terminal", terminalId };
|
|
642
646
|
}
|
|
643
647
|
|
|
644
|
-
function extractToolCallContent(value: unknown): ToolCallContent[] {
|
|
645
|
-
const richContent = extractStructuredToolCallContent(value);
|
|
648
|
+
function extractToolCallContent(value: unknown, options: AcpEventMapperOptions): ToolCallContent[] {
|
|
649
|
+
const richContent = extractStructuredToolCallContent(value, options);
|
|
650
|
+
const detailsImageContent = extractDetailsImageToolCallContent(value, options, richContent);
|
|
651
|
+
const combinedContent = [...richContent, ...detailsImageContent];
|
|
646
652
|
const terminalId = extractTerminalId(value);
|
|
647
653
|
const content =
|
|
648
|
-
terminalId && !hasTerminalContent(
|
|
649
|
-
? [...
|
|
650
|
-
:
|
|
654
|
+
terminalId && !hasTerminalContent(combinedContent, terminalId)
|
|
655
|
+
? [...combinedContent, terminalToolCallContent(terminalId)]
|
|
656
|
+
: combinedContent;
|
|
651
657
|
const fallbackText = extractReadableText(value);
|
|
652
658
|
if (!fallbackText) {
|
|
653
659
|
return content;
|
|
@@ -658,7 +664,7 @@ function extractToolCallContent(value: unknown): ToolCallContent[] {
|
|
|
658
664
|
return [...content, textToolCallContent(fallbackText)];
|
|
659
665
|
}
|
|
660
666
|
|
|
661
|
-
function extractStructuredToolCallContent(value: unknown): ToolCallContent[] {
|
|
667
|
+
function extractStructuredToolCallContent(value: unknown, options: AcpEventMapperOptions): ToolCallContent[] {
|
|
662
668
|
const blocks = getContentBlocks(value);
|
|
663
669
|
if (!blocks) {
|
|
664
670
|
return [];
|
|
@@ -666,7 +672,7 @@ function extractStructuredToolCallContent(value: unknown): ToolCallContent[] {
|
|
|
666
672
|
|
|
667
673
|
const content: ToolCallContent[] = [];
|
|
668
674
|
for (const block of blocks) {
|
|
669
|
-
const toolCallContent = toToolCallContent(block);
|
|
675
|
+
const toolCallContent = toToolCallContent(block, options);
|
|
670
676
|
if (toolCallContent) {
|
|
671
677
|
content.push(toolCallContent);
|
|
672
678
|
}
|
|
@@ -685,7 +691,7 @@ function getContentBlocks(value: unknown): unknown[] | undefined {
|
|
|
685
691
|
return Array.isArray(content) ? content : undefined;
|
|
686
692
|
}
|
|
687
693
|
|
|
688
|
-
function toToolCallContent(value: unknown): ToolCallContent | undefined {
|
|
694
|
+
function toToolCallContent(value: unknown, options: AcpEventMapperOptions): ToolCallContent | undefined {
|
|
689
695
|
const type = getContentType(value);
|
|
690
696
|
if (!type) {
|
|
691
697
|
return undefined;
|
|
@@ -697,21 +703,8 @@ function toToolCallContent(value: unknown): ToolCallContent | undefined {
|
|
|
697
703
|
return text ? textToolCallContent(text) : undefined;
|
|
698
704
|
}
|
|
699
705
|
case "image":
|
|
700
|
-
case "audio":
|
|
701
|
-
|
|
702
|
-
const mimeType = extractStringProperty<BinaryLikeContent>(value, "mimeType");
|
|
703
|
-
if (!data || !mimeType) {
|
|
704
|
-
return undefined;
|
|
705
|
-
}
|
|
706
|
-
return {
|
|
707
|
-
type: "content",
|
|
708
|
-
content: {
|
|
709
|
-
type,
|
|
710
|
-
data,
|
|
711
|
-
mimeType,
|
|
712
|
-
},
|
|
713
|
-
};
|
|
714
|
-
}
|
|
706
|
+
case "audio":
|
|
707
|
+
return binaryToolCallContent(type, value, options);
|
|
715
708
|
case "resource_link": {
|
|
716
709
|
const uri = extractStringProperty<ResourceLinkLikeContent>(value, "uri");
|
|
717
710
|
const name = extractStringProperty<ResourceLinkLikeContent>(value, "name");
|
|
@@ -769,6 +762,64 @@ function toToolCallContent(value: unknown): ToolCallContent | undefined {
|
|
|
769
762
|
}
|
|
770
763
|
}
|
|
771
764
|
|
|
765
|
+
function binaryToolCallContent(
|
|
766
|
+
type: "image" | "audio",
|
|
767
|
+
value: unknown,
|
|
768
|
+
options: AcpEventMapperOptions,
|
|
769
|
+
): ToolCallContent | undefined {
|
|
770
|
+
const data = extractStringProperty<BinaryLikeContent>(value, "data");
|
|
771
|
+
const mimeType = extractStringProperty<BinaryLikeContent>(value, "mimeType");
|
|
772
|
+
if (!data || !mimeType) {
|
|
773
|
+
return undefined;
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
type: "content",
|
|
777
|
+
content: {
|
|
778
|
+
type,
|
|
779
|
+
data: type === "image" ? (options.resolveImageData?.(data, mimeType) ?? data) : data,
|
|
780
|
+
mimeType,
|
|
781
|
+
},
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function extractDetailsImageToolCallContent(
|
|
786
|
+
value: unknown,
|
|
787
|
+
options: AcpEventMapperOptions,
|
|
788
|
+
existing: ToolCallContent[],
|
|
789
|
+
): ToolCallContent[] {
|
|
790
|
+
const images = extractDetailsImages(value);
|
|
791
|
+
if (!images) {
|
|
792
|
+
return [];
|
|
793
|
+
}
|
|
794
|
+
const seen = new Set(existing.map(imageContentKey).filter((key): key is string => key !== undefined));
|
|
795
|
+
const content: ToolCallContent[] = [];
|
|
796
|
+
for (const image of images) {
|
|
797
|
+
const toolCallContent = binaryToolCallContent("image", image, options);
|
|
798
|
+
const key = imageContentKey(toolCallContent);
|
|
799
|
+
if (!toolCallContent || !key || seen.has(key)) {
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
seen.add(key);
|
|
803
|
+
content.push(toolCallContent);
|
|
804
|
+
}
|
|
805
|
+
return content;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function extractDetailsImages(value: unknown): unknown[] | undefined {
|
|
809
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
810
|
+
const details = (value as DetailsContainer).details;
|
|
811
|
+
if (typeof details !== "object" || details === null) return undefined;
|
|
812
|
+
const images = (details as { images?: unknown }).images;
|
|
813
|
+
return Array.isArray(images) && images.length > 0 ? images : undefined;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function imageContentKey(value: ToolCallContent | undefined): string | undefined {
|
|
817
|
+
if (value?.type !== "content" || value.content.type !== "image") {
|
|
818
|
+
return undefined;
|
|
819
|
+
}
|
|
820
|
+
return `${value.content.mimeType}\u0000${value.content.data}`;
|
|
821
|
+
}
|
|
822
|
+
|
|
772
823
|
function extractEmbeddedResource(
|
|
773
824
|
value: unknown,
|
|
774
825
|
): { uri: string; text: string; mimeType?: string } | { uri: string; blob: string; mimeType?: string } | undefined {
|
|
@@ -846,6 +897,12 @@ function extractReadableText(value: unknown): string | undefined {
|
|
|
846
897
|
if (text.length > 0) {
|
|
847
898
|
return normalizeText(text);
|
|
848
899
|
}
|
|
900
|
+
if (hasBinaryContentBlock(contentBlocks)) {
|
|
901
|
+
return undefined;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
if (extractDetailsImages(value)) {
|
|
905
|
+
return undefined;
|
|
849
906
|
}
|
|
850
907
|
if (isTerminalOnlyDetails(value)) {
|
|
851
908
|
return undefined;
|
|
@@ -895,6 +952,13 @@ function getContentType(value: unknown): string | undefined {
|
|
|
895
952
|
return typeof type === "string" ? type : undefined;
|
|
896
953
|
}
|
|
897
954
|
|
|
955
|
+
function hasBinaryContentBlock(blocks: unknown[]): boolean {
|
|
956
|
+
return blocks.some(block => {
|
|
957
|
+
const type = getContentType(block);
|
|
958
|
+
return type === "image" || type === "audio";
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
898
962
|
function extractStringProperty<T extends object>(value: unknown, key: keyof T): string | undefined {
|
|
899
963
|
if (typeof value !== "object" || value === null || !(key in value)) {
|
|
900
964
|
return undefined;
|
|
@@ -581,11 +581,11 @@ export class AssistantMessageComponent extends Container {
|
|
|
581
581
|
if (content.type === "toolCall") return false;
|
|
582
582
|
}
|
|
583
583
|
if (this.#toolImagesByCallId.size > 0) return false;
|
|
584
|
-
if (message.stopReason === "aborted" && shouldRenderAbortReason(message
|
|
584
|
+
if (message.stopReason === "aborted" && shouldRenderAbortReason(message)) return false;
|
|
585
585
|
if (message.stopReason === "error" && !this.#errorPinned) return false;
|
|
586
586
|
if (
|
|
587
587
|
message.errorMessage &&
|
|
588
|
-
shouldRenderAbortReason(message
|
|
588
|
+
shouldRenderAbortReason(message) &&
|
|
589
589
|
message.stopReason !== "aborted" &&
|
|
590
590
|
message.stopReason !== "error"
|
|
591
591
|
)
|
|
@@ -779,8 +779,8 @@ export class AssistantMessageComponent extends Container {
|
|
|
779
779
|
// But only if there are no tool calls (tool execution components will show the error)
|
|
780
780
|
const hasToolCalls = message.content.some(c => c.type === "toolCall");
|
|
781
781
|
if (!hasToolCalls) {
|
|
782
|
-
if (message.stopReason === "aborted" && shouldRenderAbortReason(message
|
|
783
|
-
const abortMessage = resolveAbortLabel(message
|
|
782
|
+
if (message.stopReason === "aborted" && shouldRenderAbortReason(message)) {
|
|
783
|
+
const abortMessage = resolveAbortLabel(message);
|
|
784
784
|
if (hasVisibleContent) {
|
|
785
785
|
this.#contentContainer.addChild(new Spacer(1));
|
|
786
786
|
} else {
|
|
@@ -793,7 +793,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
793
793
|
}
|
|
794
794
|
if (
|
|
795
795
|
message.errorMessage &&
|
|
796
|
-
shouldRenderAbortReason(message
|
|
796
|
+
shouldRenderAbortReason(message) &&
|
|
797
797
|
message.stopReason !== "aborted" &&
|
|
798
798
|
message.stopReason !== "error"
|
|
799
799
|
) {
|
|
@@ -200,7 +200,6 @@ export class StatusLineComponent implements Component {
|
|
|
200
200
|
#autoCompactEnabled: boolean = true;
|
|
201
201
|
#hookStatuses: Map<string, string> = new Map();
|
|
202
202
|
#subagentCount: number = 0;
|
|
203
|
-
#subagentHubHint: string | undefined;
|
|
204
203
|
#sessionStartTime: number = Date.now();
|
|
205
204
|
#planModeStatus: { enabled: boolean; paused: boolean } | null = null;
|
|
206
205
|
#loopModeStatus: { enabled: boolean } | null = null;
|
|
@@ -306,12 +305,6 @@ export class StatusLineComponent implements Component {
|
|
|
306
305
|
this.#subagentCount = count;
|
|
307
306
|
}
|
|
308
307
|
|
|
309
|
-
/** Hub key label shown in the forced running-subagents badge. */
|
|
310
|
-
setSubagentHubHint(hint: string | undefined): void {
|
|
311
|
-
const trimmed = hint?.trim();
|
|
312
|
-
this.#subagentHubHint = trimmed ? trimmed : undefined;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
308
|
/** Active subagent count as currently displayed (collab state mirroring). */
|
|
316
309
|
get subagentCount(): number {
|
|
317
310
|
return this.#subagentCount;
|
|
@@ -925,8 +918,7 @@ export class StatusLineComponent implements Component {
|
|
|
925
918
|
#subagentBadgeText(): string | undefined {
|
|
926
919
|
if (this.#subagentCount === 0) return undefined;
|
|
927
920
|
const noun = this.#subagentCount === 1 ? "agent" : "agents";
|
|
928
|
-
|
|
929
|
-
return theme.fg("statusLineSubagents", `${theme.icon.agents} ${this.#subagentCount} ${noun} running${hubHint}`);
|
|
921
|
+
return theme.fg("statusLineSubagents", `${theme.icon.agents} ${this.#subagentCount} ${noun}`);
|
|
930
922
|
}
|
|
931
923
|
|
|
932
924
|
#buildStatusLine(width: number): string {
|
|
@@ -21,7 +21,7 @@ function withIcon(icon: string, text: string): string {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function stripDisplayRoot(pwd: string): string {
|
|
24
|
-
for (const root of [
|
|
24
|
+
for (const root of [path.join(os.homedir(), "Projects"), "/work"]) {
|
|
25
25
|
const relative = relativePathWithinRoot(root, pwd);
|
|
26
26
|
if (relative) return relative;
|
|
27
27
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
2
|
-
import
|
|
2
|
+
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
3
|
+
import { getStreamingPartialJson } from "@oh-my-pi/pi-ai/utils/block-symbols";
|
|
3
4
|
import { type Component, Loader, TERMINAL } from "@oh-my-pi/pi-tui";
|
|
4
5
|
import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
|
|
5
6
|
import { extractTextContent } from "../../commit/utils";
|
|
@@ -609,9 +610,9 @@ export class EventController {
|
|
|
609
610
|
// reveal (write/edit/bash previews grow smoothly when a slow provider
|
|
610
611
|
// delivers large batches); once it closes, the final args render
|
|
611
612
|
// as-is — mirroring how assistant text snaps at message_end.
|
|
612
|
-
const partialJson = "partialJson" in content ? content.partialJson : undefined;
|
|
613
613
|
let renderArgs: Record<string, unknown>;
|
|
614
|
-
|
|
614
|
+
const partialJson = getStreamingPartialJson(content);
|
|
615
|
+
if (partialJson) {
|
|
615
616
|
renderArgs = this.#toolArgsReveal.setTarget(
|
|
616
617
|
content.id,
|
|
617
618
|
partialJson,
|
|
@@ -697,7 +698,7 @@ export class EventController {
|
|
|
697
698
|
this.#toolArgsReveal.flushAll();
|
|
698
699
|
let errorMessage: string | undefined;
|
|
699
700
|
const aborted = this.ctx.streamingMessage.stopReason === "aborted";
|
|
700
|
-
const silentlyAborted = aborted && isSilentAbort(this.ctx.streamingMessage
|
|
701
|
+
const silentlyAborted = aborted && isSilentAbort(this.ctx.streamingMessage);
|
|
701
702
|
const ttsrSilenced = aborted && this.ctx.viewSession.isTtsrAbortPending;
|
|
702
703
|
if (aborted && !silentlyAborted && !ttsrSilenced) {
|
|
703
704
|
// Resolve the operator-facing label: a user-interrupt (Esc) abort
|
|
@@ -707,7 +708,7 @@ export class EventController {
|
|
|
707
708
|
// AgentSession.#handleAgentEvent already stamped SILENT_ABORT_MARKER for
|
|
708
709
|
// the plan-compact transition before this controller ran, so reaching
|
|
709
710
|
// this branch implies the abort was NOT a silent internal transition.
|
|
710
|
-
errorMessage = resolveAbortLabel(this.ctx.streamingMessage
|
|
711
|
+
errorMessage = resolveAbortLabel(this.ctx.streamingMessage, this.ctx.viewSession.retryAttempt);
|
|
711
712
|
this.ctx.streamingMessage.errorMessage = errorMessage;
|
|
712
713
|
}
|
|
713
714
|
if (silentlyAborted || ttsrSilenced) {
|
|
@@ -759,11 +760,7 @@ export class EventController {
|
|
|
759
760
|
// above the editor so it survives transcript scroll. Cleared at the next
|
|
760
761
|
// turn's agent_start. Suppress the transcript's inline `Error: …` line for
|
|
761
762
|
// the same message while pinned so the error isn't rendered twice.
|
|
762
|
-
if (
|
|
763
|
-
event.message.stopReason === "error" &&
|
|
764
|
-
event.message.errorMessage &&
|
|
765
|
-
!isSilentAbort(event.message.errorMessage)
|
|
766
|
-
) {
|
|
763
|
+
if (event.message.stopReason === "error" && event.message.errorMessage && !isSilentAbort(event.message)) {
|
|
767
764
|
this.#lastAssistantComponent?.setErrorPinned(true);
|
|
768
765
|
this.#pinnedErrorComponent = this.#lastAssistantComponent;
|
|
769
766
|
this.ctx.showPinnedError(event.message.errorMessage);
|
|
@@ -1157,7 +1154,7 @@ export class EventController {
|
|
|
1157
1154
|
async #handleAutoRetryStart(event: Extract<AgentSessionEvent, { type: "auto_retry_start" }>): Promise<void> {
|
|
1158
1155
|
this.#stopWorkingLoader();
|
|
1159
1156
|
this.ctx.statusContainer.clear();
|
|
1160
|
-
if (event.
|
|
1157
|
+
if (AIError.is(event.errorId, AIError.Flag.ThinkingLoop)) {
|
|
1161
1158
|
// The retry path drops the failed assistant from runtime context. Do not
|
|
1162
1159
|
// restore its inline Error row; just unpin the fixed-region banner so the
|
|
1163
1160
|
// retry UI is the visible state.
|
|
@@ -858,11 +858,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
858
858
|
this.#observerRegistry.subscribeToEventBus(this.#eventBus);
|
|
859
859
|
}
|
|
860
860
|
this.#observerRegistry.setMainSession(this.sessionManager.getSessionFile() ?? undefined);
|
|
861
|
-
this.statusLine.setSubagentHubHint(
|
|
862
|
-
this.keybindings.getDisplayString("app.agents.hub") ||
|
|
863
|
-
this.keybindings.getDisplayString("app.session.observe") ||
|
|
864
|
-
undefined,
|
|
865
|
-
);
|
|
866
861
|
this.syncRunningSubagentBadge();
|
|
867
862
|
this.#observerRegistry.onChange(() => {
|
|
868
863
|
this.syncRunningSubagentBadge();
|
package/src/modes/print-mode.ts
CHANGED
|
@@ -83,7 +83,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
83
83
|
// Check for error/aborted — skip silent-abort (plan-mode compaction transition)
|
|
84
84
|
if (
|
|
85
85
|
(assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") &&
|
|
86
|
-
!isSilentAbort(assistantMsg
|
|
86
|
+
!isSilentAbort(assistantMsg)
|
|
87
87
|
) {
|
|
88
88
|
const errorLine = sanitizeText(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);
|
|
89
89
|
// Flush before this hard exit — it bypasses the awaited postmortem.quit()
|
|
@@ -146,11 +146,11 @@ export function resolveAssistantErrorMessage(
|
|
|
146
146
|
message: AssistantAgentMessage,
|
|
147
147
|
retryAttempt = 0,
|
|
148
148
|
): { hasErrorStop: boolean; errorMessage: string | null } {
|
|
149
|
-
const isAbortedSilently = message.stopReason === "aborted" && isSilentAbort(message
|
|
149
|
+
const isAbortedSilently = message.stopReason === "aborted" && isSilentAbort(message);
|
|
150
150
|
const hasErrorStop = !isAbortedSilently && (message.stopReason === "aborted" || message.stopReason === "error");
|
|
151
151
|
const errorMessage = hasErrorStop
|
|
152
152
|
? message.stopReason === "aborted"
|
|
153
|
-
? resolveAbortLabel(message
|
|
153
|
+
? resolveAbortLabel(message, retryAttempt)
|
|
154
154
|
: message.errorMessage || "Error"
|
|
155
155
|
: null;
|
|
156
156
|
return { hasErrorStop, errorMessage };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import type { AssistantMessage, ImageContent, Message, Usage } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import { getStreamingPartialJson } from "@oh-my-pi/pi-ai/utils/block-symbols";
|
|
3
4
|
import { type Component, Spacer, Text, TruncatedText } from "@oh-my-pi/pi-tui";
|
|
4
5
|
import type { AdvisorMessageDetails } from "../../advisor";
|
|
5
6
|
import { COLLAB_PROMPT_MESSAGE_TYPE, type CollabPromptDetails } from "../../collab/protocol";
|
|
@@ -409,10 +410,10 @@ export class UiHelpers {
|
|
|
409
410
|
readGroup?.seal();
|
|
410
411
|
readGroup = null;
|
|
411
412
|
const tool = this.ctx.viewSession.getToolByName(content.name);
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
413
|
+
const partialJson = getStreamingPartialJson(content);
|
|
414
|
+
const renderArgs = partialJson
|
|
415
|
+
? { ...content.arguments, __partialJson: partialJson }
|
|
416
|
+
: content.arguments;
|
|
416
417
|
const component = new ToolExecutionComponent(
|
|
417
418
|
content.name,
|
|
418
419
|
renderArgs,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<project-context>
|
|
2
|
+
These context files carry the user's standing instructions for this project (AGENTS.md and the like). The driving agent is bound by them. Hold the agent to them and flag drift the moment it starts; never advise against what these files mandate.
|
|
3
|
+
{{#each contextFiles}}
|
|
4
|
+
<file path="{{path}}">
|
|
5
|
+
{{content}}
|
|
6
|
+
</file>
|
|
7
|
+
{{/each}}
|
|
8
|
+
</project-context>
|