@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.
Files changed (67) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/cli.js +2709 -2715
  3. package/dist/types/advisor/runtime.d.ts +15 -1
  4. package/dist/types/advisor/watchdog.d.ts +11 -0
  5. package/dist/types/config/model-roles.d.ts +1 -1
  6. package/dist/types/config/settings-schema.d.ts +32 -12
  7. package/dist/types/discovery/omp-extension-roots.d.ts +3 -3
  8. package/dist/types/edit/index.d.ts +18 -0
  9. package/dist/types/edit/streaming.d.ts +30 -0
  10. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  11. package/dist/types/extensibility/shared-events.d.ts +1 -0
  12. package/dist/types/mcp/oauth-discovery.d.ts +0 -11
  13. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -0
  14. package/dist/types/modes/components/status-line/component.d.ts +0 -2
  15. package/dist/types/sdk.d.ts +1 -1
  16. package/dist/types/session/agent-session.d.ts +32 -2
  17. package/dist/types/session/blob-store.d.ts +4 -0
  18. package/dist/types/session/messages.d.ts +6 -7
  19. package/dist/types/session/settings-stream-fn.d.ts +21 -0
  20. package/dist/types/session/turn-persistence.d.ts +88 -0
  21. package/package.json +12 -12
  22. package/src/advisor/__tests__/advisor.test.ts +217 -0
  23. package/src/advisor/runtime.ts +65 -2
  24. package/src/advisor/watchdog.ts +15 -0
  25. package/src/auto-thinking/classifier.ts +2 -2
  26. package/src/config/model-resolver.ts +5 -1
  27. package/src/config/model-roles.ts +3 -3
  28. package/src/config/settings-schema.ts +30 -8
  29. package/src/discovery/claude-plugins.ts +3 -2
  30. package/src/discovery/omp-extension-roots.ts +38 -13
  31. package/src/edit/index.ts +21 -0
  32. package/src/edit/streaming.ts +170 -0
  33. package/src/extensibility/custom-tools/types.ts +1 -0
  34. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +18 -1
  35. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +59 -2
  36. package/src/extensibility/plugins/manager.ts +74 -4
  37. package/src/extensibility/shared-events.ts +1 -0
  38. package/src/internal-urls/docs-index.generated.txt +1 -1
  39. package/src/mcp/oauth-discovery.ts +5 -29
  40. package/src/mcp/transports/http.ts +3 -1
  41. package/src/mnemopi/backend.ts +2 -2
  42. package/src/modes/acp/acp-agent.ts +15 -2
  43. package/src/modes/acp/acp-event-mapper.ts +89 -25
  44. package/src/modes/components/assistant-message.ts +5 -5
  45. package/src/modes/components/status-line/component.ts +1 -9
  46. package/src/modes/components/status-line/segments.ts +1 -1
  47. package/src/modes/controllers/event-controller.ts +8 -11
  48. package/src/modes/interactive-mode.ts +0 -5
  49. package/src/modes/print-mode.ts +1 -1
  50. package/src/modes/utils/transcript-render-helpers.ts +2 -2
  51. package/src/modes/utils/ui-helpers.ts +5 -4
  52. package/src/prompts/advisor/context-files.md +8 -0
  53. package/src/sdk.ts +23 -27
  54. package/src/session/agent-session.ts +330 -219
  55. package/src/session/blob-store.ts +24 -0
  56. package/src/session/messages.ts +20 -12
  57. package/src/session/session-persistence.ts +1 -2
  58. package/src/session/settings-stream-fn.ts +49 -0
  59. package/src/session/turn-persistence.ts +142 -0
  60. package/src/session/unexpected-stop-classifier.ts +2 -2
  61. package/src/slash-commands/helpers/mcp.ts +2 -1
  62. package/src/tiny/models.ts +8 -6
  63. package/src/tiny/text.ts +14 -7
  64. package/src/tools/image-gen.ts +2 -1
  65. package/src/tools/tts.ts +2 -1
  66. package/src/utils/title-generator.ts +1 -1
  67. 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 parseMcpAuthServerUrl(errorMessage: string, serverUrl?: string): string | undefined {
27
- const match = errorMessage.match(/Mcp-Auth-Server:\s*([^;\]\s]+)/i);
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
- if (!detectAuthError(error)) {
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
- if (this.onAuthError && error instanceof Error && /^HTTP (401|403):/.test(error.message)) {
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
@@ -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.errorMessage)) {
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 = [...extractDiffToolCallContent(event.result), ...extractToolCallContent(event.result)];
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(richContent, terminalId)
649
- ? [...richContent, terminalToolCallContent(terminalId)]
650
- : richContent;
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
- const data = extractStringProperty<BinaryLikeContent>(value, "data");
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.errorMessage)) return false;
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.errorMessage) &&
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.errorMessage)) {
783
- const abortMessage = resolveAbortLabel(message.errorMessage);
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.errorMessage) &&
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
- const hubHint = this.#subagentHubHint ? ` — ${this.#subagentHubHint} hub` : " — Agent Hub";
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 ["/work", path.join(os.homedir(), "Projects")]) {
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 { THINKING_LOOP_ERROR_MARKER } from "@oh-my-pi/pi-ai/utils/thinking-loop";
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
- if (typeof partialJson === "string") {
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.errorMessage);
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.errorMessage, this.ctx.viewSession.retryAttempt);
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.errorMessage?.includes(THINKING_LOOP_ERROR_MARKER)) {
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();
@@ -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.errorMessage)
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.errorMessage);
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.errorMessage, retryAttempt)
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 renderArgs =
413
- "partialJson" in content
414
- ? { ...content.arguments, __partialJson: content.partialJson }
415
- : content.arguments;
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>