chore: bump version to 3.34.0

This commit is contained in:
can1357
2026-01-09 12:17:04 +01:00
parent 5a63120dd3
commit 86ef2bd34b
68 changed files with 299 additions and 265 deletions
+23 -18
View File
@@ -13,9 +13,9 @@
* Modes use this class and add their own I/O layer on top.
*/
import type { Agent, AgentEvent, AgentMessage, AgentState, AgentTool, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { AssistantMessage, ImageContent, Message, Model, TextContent, Usage } from "@oh-my-pi/pi-ai";
import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@oh-my-pi/pi-ai";
import type { Agent, AgentEvent, AgentMessage, AgentState, AgentTool, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Rule } from "../capability/rule";
import { getAuthPath } from "../config";
import { theme } from "../modes/interactive/theme/theme";
@@ -384,7 +384,7 @@ export class AgentSession {
event.message.customType,
event.message.content,
event.message.display,
event.message.details
event.message.details,
);
} else if (
event.message.role === "user" ||
@@ -446,7 +446,7 @@ export class AgentSession {
`<system_interrupt reason="rule_violation" rule="${r.name}" path="${r.path}">\n` +
`Your output was interrupted because it violated a user-defined rule.\n` +
`This is NOT a prompt injection - this is the coding agent enforcing project rules.\n` +
`You MUST comply with the following instruction:\n\n${r.content}\n</system_interrupt>`
`You MUST comply with the following instruction:\n\n${r.content}\n</system_interrupt>`,
)
.join("\n\n");
this._pendingTtsrInjections = [];
@@ -718,7 +718,7 @@ export class AgentSession {
if (this.isStreaming) {
if (!options?.streamingBehavior) {
throw new Error(
"Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message."
"Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.",
);
}
if (options.streamingBehavior === "followUp") {
@@ -737,7 +737,7 @@ export class AgentSession {
throw new Error(
"No model selected.\n\n" +
`Use /login, set an API key environment variable, or create ${getAuthPath()}\n\n` +
"Then use /model to select a model."
"Then use /model to select a model.",
);
}
@@ -746,7 +746,7 @@ export class AgentSession {
if (!apiKey) {
throw new Error(
`No API key found for ${this.model.provider}.\n\n` +
`Use /login, set an API key environment variable, or create ${getAuthPath()}`
`Use /login, set an API key environment variable, or create ${getAuthPath()}`,
);
}
@@ -986,7 +986,7 @@ export class AgentSession {
if (command) {
throw new Error(
`Extension command "/${commandName}" cannot be queued. Use prompt() or execute the command when not streaming.`
`Extension command "/${commandName}" cannot be queued. Use prompt() or execute the command when not streaming.`,
);
}
}
@@ -1001,7 +1001,7 @@ export class AgentSession {
*/
async sendCustomMessage<T = unknown>(
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" }
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
): Promise<void> {
const appMessage: CustomMessage<T> = {
role: "custom",
@@ -1031,7 +1031,12 @@ export class AgentSession {
}
this.agent.appendMessage(appMessage);
this.sessionManager.appendCustomMessageEntry(message.customType, message.content, message.display, message.details);
this.sessionManager.appendCustomMessageEntry(
message.customType,
message.content,
message.display,
message.details,
);
}
/**
@@ -1176,7 +1181,7 @@ export class AgentSession {
*/
async cycleRoleModels(
roleOrder: string[],
options?: { temporary?: boolean }
options?: { temporary?: boolean },
): Promise<RoleModelCycleResult | undefined> {
const availableModels = this._modelRegistry.getAvailable();
if (availableModels.length === 0) return undefined;
@@ -1189,7 +1194,7 @@ export class AgentSession {
for (const role of roleOrder) {
const roleModelStr =
role === "default"
? this.settingsManager.getModelRole("default") ?? `${currentModel.provider}/${currentModel.id}`
? (this.settingsManager.getModelRole("default") ?? `${currentModel.provider}/${currentModel.id}`)
: this.settingsManager.getModelRole(role);
if (!roleModelStr) continue;
@@ -1454,7 +1459,7 @@ export class AgentSession {
this.model,
apiKey,
customInstructions,
this._compactionAbortController.signal
this._compactionAbortController.signal,
);
summary = result.summary;
firstKeptEntryId = result.firstKeptEntryId;
@@ -1627,7 +1632,7 @@ export class AgentSession {
this.model,
apiKey,
undefined,
this._autoCompactionAbortController.signal
this._autoCompactionAbortController.signal,
);
summary = compactResult.summary;
firstKeptEntryId = compactResult.firstKeptEntryId;
@@ -1684,7 +1689,7 @@ export class AgentSession {
throw new Error(
`Context overflow: ${
error instanceof Error ? error.message : "compaction failed"
}. Your input may be too large for the context window.`
}. Your input may be too large for the context window.`,
);
}
} finally {
@@ -1722,7 +1727,7 @@ export class AgentSession {
const err = message.errorMessage;
// Match: overloaded_error, rate limit, 429, 500, 502, 503, 504, service unavailable, connection error
return /overloaded|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server error|internal error|connection.?error/i.test(
err
err,
);
}
@@ -1876,7 +1881,7 @@ export class AgentSession {
async executeBash(
command: string,
onChunk?: (chunk: string) => void,
options?: { excludeFromContext?: boolean }
options?: { excludeFromContext?: boolean },
): Promise<BashResult> {
this._bashAbortController = new AbortController();
@@ -2107,7 +2112,7 @@ export class AgentSession {
*/
async navigateTree(
targetId: string,
options: { summarize?: boolean; customInstructions?: string } = {}
options: { summarize?: boolean; customInstructions?: string } = {},
): Promise<{ editorText?: string; cancelled: boolean; aborted?: boolean; summaryEntry?: BranchSummaryEntry }> {
const oldLeafId = this.sessionManager.getLeafId();
@@ -2130,7 +2135,7 @@ export class AgentSession {
const { entries: entriesToSummarize, commonAncestorId } = collectEntriesForBranchSummary(
this.sessionManager,
oldLeafId,
targetId
targetId,
);
// Prepare event data
@@ -44,7 +44,10 @@ export class AuthStorage {
* @param authPath - Primary path for reading/writing auth.json
* @param fallbackPaths - Additional paths to check when reading (legacy support)
*/
constructor(private authPath: string, private fallbackPaths: string[] = []) {}
constructor(
private authPath: string,
private fallbackPaths: string[] = [],
) {}
/**
* Set a runtime API key override (not persisted to disk).
@@ -173,7 +176,7 @@ export class AuthStorage {
onAuth: (info: { url: string; instructions?: string }) => void;
onPrompt: (prompt: { message: string; placeholder?: string }) => Promise<string>;
onProgress?: (message: string) => void;
}
},
): Promise<void> {
let credentials: OAuthCredentials;
@@ -181,7 +184,7 @@ export class AuthStorage {
case "anthropic":
credentials = await loginAnthropic(
(url) => callbacks.onAuth({ url }),
() => callbacks.onPrompt({ message: "Paste the authorization code:" })
() => callbacks.onPrompt({ message: "Paste the authorization code:" }),
);
break;
case "github-copilot":
@@ -5,9 +5,9 @@
* a summary of the branch being left so context isn't lost.
*/
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
import type { Model } from "@oh-my-pi/pi-ai";
import { completeSimple } from "@oh-my-pi/pi-ai";
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
import branchSummaryPrompt from "../../prompts/branch-summary.md" with { type: "text" };
import branchSummaryPreamble from "../../prompts/branch-summary-preamble.md" with { type: "text" };
import {
@@ -5,9 +5,9 @@
* and after compaction the session is reloaded.
*/
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
import type { AssistantMessage, Model, Usage } from "@oh-my-pi/pi-ai";
import { complete, completeSimple } from "@oh-my-pi/pi-ai";
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
import compactionSummaryPrompt from "../../prompts/compaction-summary.md" with { type: "text" };
import compactionTurnPrefixPrompt from "../../prompts/compaction-turn-prefix.md" with { type: "text" };
import compactionUpdateSummaryPrompt from "../../prompts/compaction-update-summary.md" with { type: "text" };
@@ -2,8 +2,8 @@
* Shared utilities for compaction and branch summarization.
*/
import type { Message } from "@oh-my-pi/pi-ai";
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
import type { Message } from "@oh-my-pi/pi-ai";
import summarizationSystemPrompt from "../../prompts/summarization-system.md" with { type: "text" };
// ============================================================================
@@ -5,8 +5,8 @@
* They can provide custom rendering for tool calls and results in the TUI.
*/
import type { Model } from "@oh-my-pi/pi-ai";
import type { AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import type { Model } from "@oh-my-pi/pi-ai";
import type { Component } from "@oh-my-pi/pi-tui";
import type { Static, TSchema } from "@sinclair/typebox";
import type { Theme } from "../../modes/interactive/theme/theme";
@@ -140,7 +140,7 @@ export interface CustomTool<TParams extends TSchema = TSchema, TDetails = any> {
params: Static<TParams>,
onUpdate: AgentToolUpdateCallback<TDetails> | undefined,
ctx: CustomToolContext,
signal?: AbortSignal
signal?: AbortSignal,
): Promise<AgentToolResult<TDetails>>;
/** Called on session lifecycle events - use to reconstruct state or cleanup resources */
@@ -154,7 +154,7 @@ export interface CustomTool<TParams extends TSchema = TSchema, TDetails = any> {
/** Factory function that creates a custom tool or array of tools */
export type CustomToolFactory = (
pi: CustomToolAPI
pi: CustomToolAPI,
) => CustomTool<any, any> | CustomTool<any, any>[] | Promise<CustomTool<any, any> | CustomTool<any, any>[]>;
/** Loaded custom tool with metadata and wrapped AgentTool */
@@ -2,8 +2,8 @@
* Extension runner - executes extensions and manages their lifecycle.
*/
import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
import type { KeyId } from "@oh-my-pi/pi-tui";
import { theme } from "../../modes/interactive/theme/theme";
import type { ModelRegistry } from "../model-registry";
@@ -53,7 +53,7 @@ export type BranchHandler = (entryId: string) => Promise<{ cancelled: boolean }>
export type NavigateTreeHandler = (
targetId: string,
options?: { summarize?: boolean }
options?: { summarize?: boolean },
) => Promise<{ cancelled: boolean }>;
const noOpUIContext: ExtensionUIContext = {
@@ -94,7 +94,7 @@ export class ExtensionRunner {
extensions: LoadedExtension[],
cwd: string,
sessionManager: SessionManager,
modelRegistry: ModelRegistry
modelRegistry: ModelRegistry,
) {
this.extensions = extensions;
this.uiContext = noOpUIContext;
@@ -215,7 +215,7 @@ export class ExtensionRunner {
if (ExtensionRunner.RESERVED_SHORTCUTS.has(normalizedKey)) {
console.warn(
`Extension shortcut '${key}' from ${shortcut.extensionPath} conflicts with built-in shortcut. Skipping.`
`Extension shortcut '${key}' from ${shortcut.extensionPath} conflicts with built-in shortcut. Skipping.`,
);
continue;
}
@@ -223,7 +223,7 @@ export class ExtensionRunner {
const existing = allShortcuts.get(normalizedKey);
if (existing) {
console.warn(
`Extension shortcut conflict: '${key}' registered by both ${existing.extensionPath} and ${shortcut.extensionPath}. Using ${shortcut.extensionPath}.`
`Extension shortcut conflict: '${key}' registered by both ${existing.extensionPath} and ${shortcut.extensionPath}. Using ${shortcut.extensionPath}.`,
);
}
allShortcuts.set(normalizedKey, shortcut);
@@ -309,7 +309,7 @@ export class ExtensionRunner {
}
private isSessionBeforeEvent(
type: string
type: string,
): type is "session_before_switch" | "session_before_branch" | "session_before_compact" | "session_before_tree" {
return (
type === "session_before_switch" ||
@@ -320,7 +320,7 @@ export class ExtensionRunner {
}
async emit(
event: ExtensionEvent
event: ExtensionEvent,
): Promise<SessionBeforeCompactResult | SessionBeforeTreeResult | ToolResultEventResult | undefined> {
const ctx = this.createContext();
let result: SessionBeforeCompactResult | SessionBeforeTreeResult | ToolResultEventResult | undefined;
@@ -428,7 +428,7 @@ export class ExtensionRunner {
async emitBeforeAgentStart(
prompt: string,
images?: ImageContent[]
images?: ImageContent[],
): Promise<BeforeAgentStartCombinedResult | undefined> {
const ctx = this.createContext();
const messages: NonNullable<BeforeAgentStartEventResult["message"]>[] = [];
@@ -8,8 +8,8 @@
* - Interact with the user via UI primitives
*/
import type { ImageContent, Model, TextContent, ToolResultMessage } from "@oh-my-pi/pi-ai";
import type { AgentMessage, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import type { ImageContent, Model, TextContent, ToolResultMessage } from "@oh-my-pi/pi-ai";
import type { Component, KeyId, TUI } from "@oh-my-pi/pi-tui";
import type { Static, TSchema } from "@sinclair/typebox";
import type { Theme } from "../../modes/interactive/theme/theme";
@@ -67,8 +67,8 @@ export interface ExtensionUIContext {
factory: (
tui: TUI,
theme: Theme,
done: (result: T) => void
) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>
done: (result: T) => void,
) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
): Promise<T>;
/** Set the text in the core input editor. */
@@ -178,7 +178,7 @@ export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = un
params: Static<TParams>,
onUpdate: AgentToolUpdateCallback<TDetails> | undefined,
ctx: ExtensionContext,
signal?: AbortSignal
signal?: AbortSignal,
): Promise<AgentToolResult<TDetails>>;
/** Called on session lifecycle events - use to reconstruct state or cleanup resources */
@@ -490,7 +490,7 @@ export interface MessageRenderOptions {
export type MessageRenderer<T = unknown> = (
message: CustomMessage<T>,
options: MessageRenderOptions,
theme: Theme
theme: Theme,
) => Component | undefined;
// ============================================================================
@@ -535,17 +535,17 @@ export interface ExtensionAPI {
on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
on(
event: "session_before_switch",
handler: ExtensionHandler<SessionBeforeSwitchEvent, SessionBeforeSwitchResult>
handler: ExtensionHandler<SessionBeforeSwitchEvent, SessionBeforeSwitchResult>,
): void;
on(event: "session_switch", handler: ExtensionHandler<SessionSwitchEvent>): void;
on(
event: "session_before_branch",
handler: ExtensionHandler<SessionBeforeBranchEvent, SessionBeforeBranchResult>
handler: ExtensionHandler<SessionBeforeBranchEvent, SessionBeforeBranchResult>,
): void;
on(event: "session_branch", handler: ExtensionHandler<SessionBranchEvent>): void;
on(
event: "session_before_compact",
handler: ExtensionHandler<SessionBeforeCompactEvent, SessionBeforeCompactResult>
handler: ExtensionHandler<SessionBeforeCompactEvent, SessionBeforeCompactResult>,
): void;
on(event: "session_compact", handler: ExtensionHandler<SessionCompactEvent>): void;
on(event: "session_shutdown", handler: ExtensionHandler<SessionShutdownEvent>): void;
@@ -580,7 +580,7 @@ export interface ExtensionAPI {
options: {
description?: string;
handler: (ctx: ExtensionContext) => Promise<void> | void;
}
},
): void;
/** Register a CLI flag. */
@@ -590,7 +590,7 @@ export interface ExtensionAPI {
description?: string;
type: "boolean" | "string";
default?: boolean | string;
}
},
): void;
/** Get the value of a registered CLI flag. */
@@ -610,7 +610,7 @@ export interface ExtensionAPI {
/** Send a custom message to the session. */
sendMessage<T = unknown>(
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" }
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
): void;
/** Append a custom entry to the session for state persistence (not sent to LLM). */
@@ -663,7 +663,7 @@ type HandlerFn = (...args: unknown[]) => Promise<unknown>;
export type SendMessageHandler = <T = unknown>(
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" }
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
) => void;
export type AppendEntryHandler = <T = unknown>(customType: string, data?: T) => void;
@@ -2,8 +2,8 @@
* Tool wrappers for extensions.
*/
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
import type { AgentTool, AgentToolContext, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
import type { Theme } from "../../modes/interactive/theme/theme";
import type { ExtensionRunner } from "./runner";
import type { ExtensionContext, RegisteredTool, ToolCallEventResult, ToolResultEventResult } from "./types";
@@ -26,7 +26,7 @@ export function wrapRegisteredTool(registeredTool: RegisteredTool, getContext: (
definition.renderResult?.(
result,
{ expanded: options.expanded, isPartial: options.isPartial, spinnerFrame: options.spinnerFrame },
theme as Theme
theme as Theme,
)
: undefined,
};
@@ -37,7 +37,7 @@ export function wrapRegisteredTool(registeredTool: RegisteredTool, getContext: (
*/
export function wrapRegisteredTools(
registeredTools: RegisteredTool[],
getContext: () => ExtensionContext
getContext: () => ExtensionContext,
): AgentTool[] {
return registeredTools.map((rt) => wrapRegisteredTool(rt, getContext));
}
@@ -55,7 +55,7 @@ export function wrapToolWithExtensions<T>(tool: AgentTool<any, T>, runner: Exten
params: Record<string, unknown>,
signal?: AbortSignal,
onUpdate?: AgentToolUpdateCallback<T>,
context?: AgentToolContext
context?: AgentToolContext,
) => {
// Emit tool_call event - extensions can block execution
if (runner.hasHandlers("tool_call")) {
@@ -113,7 +113,8 @@ export function wrapToolWithExtensions<T>(tool: AgentTool<any, T>, runner: Exten
if (resultResult.isError === true && !executionError) {
// Extension marks a successful result as error
const textBlocks = (modifiedContent ?? []).filter((c): c is TextContent => c.type === "text");
const errorText = textBlocks.map((t) => t.text).join("\n") || "Tool result marked as error by extension";
const errorText =
textBlocks.map((t) => t.text).join("\n") || "Tool result marked as error by extension";
throw new Error(errorText);
}
if (resultResult.isError === false && executionError) {
@@ -2,8 +2,8 @@
* Hook runner - executes hooks and manages their lifecycle.
*/
import type { Model } from "@oh-my-pi/pi-ai";
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
import type { Model } from "@oh-my-pi/pi-ai";
import { theme } from "../../modes/interactive/theme/theme";
import type { ModelRegistry } from "../model-registry";
import type { SessionManager } from "../session-manager";
@@ -272,7 +272,7 @@ export class HookRunner {
* Check if event type is a session "before_*" event that can be cancelled.
*/
private isSessionBeforeEvent(
type: string
type: string,
): type is "session_before_switch" | "session_before_branch" | "session_before_compact" | "session_before_tree" {
return (
type === "session_before_switch" ||
@@ -287,7 +287,7 @@ export class HookRunner {
* Returns the result from session before_* / tool_result events (if any handler returns one).
*/
async emit(
event: HookEvent
event: HookEvent,
): Promise<SessionBeforeCompactResult | SessionBeforeTreeResult | ToolResultEventResult | undefined> {
const ctx = this.createContext();
let result: SessionBeforeCompactResult | SessionBeforeTreeResult | ToolResultEventResult | undefined;
@@ -400,7 +400,7 @@ export class HookRunner {
*/
async emitBeforeAgentStart(
prompt: string,
images?: import("@oh-my-pi/pi-ai").ImageContent[]
images?: import("@oh-my-pi/pi-ai").ImageContent[],
): Promise<BeforeAgentStartEventResult | undefined> {
const ctx = this.createContext();
let result: BeforeAgentStartEventResult | undefined;
@@ -5,8 +5,8 @@
* and interact with the user via UI primitives.
*/
import type { ImageContent, Message, Model, TextContent, ToolResultMessage } from "@oh-my-pi/pi-ai";
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
import type { ImageContent, Message, Model, TextContent, ToolResultMessage } from "@oh-my-pi/pi-ai";
import type { Component, TUI } from "@oh-my-pi/pi-tui";
import type { Theme } from "../../modes/interactive/theme/theme";
import type { CompactionPreparation, CompactionResult } from "../compaction/index";
@@ -97,8 +97,8 @@ export interface HookUIContext {
factory: (
tui: TUI,
theme: Theme,
done: (result: T) => void
) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>
done: (result: T) => void,
) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
): Promise<T>;
/**
@@ -632,7 +632,7 @@ export interface HookMessageRenderOptions {
export type HookMessageRenderer<T = unknown> = (
message: HookMessage<T>,
options: HookMessageRenderOptions,
theme: Theme
theme: Theme,
) => Component | undefined;
/**
@@ -658,7 +658,7 @@ export interface HookAPI {
on(event: "session_branch", handler: HookHandler<SessionBranchEvent>): void;
on(
event: "session_before_compact",
handler: HookHandler<SessionBeforeCompactEvent, SessionBeforeCompactResult>
handler: HookHandler<SessionBeforeCompactEvent, SessionBeforeCompactResult>,
): void;
on(event: "session_compact", handler: HookHandler<SessionCompactEvent>): void;
on(event: "session_shutdown", handler: HookHandler<SessionShutdownEvent>): void;
@@ -692,7 +692,7 @@ export interface HookAPI {
*/
sendMessage<T = unknown>(
message: Pick<HookMessage<T>, "customType" | "content" | "display" | "details">,
triggerTurn?: boolean
triggerTurn?: boolean,
): void;
/**
+3 -3
View File
@@ -5,8 +5,8 @@
* and provides a transformer to convert them to LLM-compatible messages.
*/
import type { ImageContent, Message, TextContent } from "@oh-my-pi/pi-ai";
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
import type { ImageContent, Message, TextContent } from "@oh-my-pi/pi-ai";
export const COMPACTION_SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:
@@ -136,7 +136,7 @@ export function createBranchSummaryMessage(summary: string, fromId: string, time
export function createCompactionSummaryMessage(
summary: string,
tokensBefore: number,
timestamp: string
timestamp: string,
): CompactionSummaryMessage {
return {
role: "compactionSummary",
@@ -152,7 +152,7 @@ export function createCustomMessage(
content: string | (TextContent | ImageContent)[],
display: boolean,
details: unknown | undefined,
timestamp: string
timestamp: string,
): CustomMessage {
return {
role: "custom",
@@ -39,7 +39,7 @@ const ModelDefinitionSchema = Type.Object({
Type.Literal("anthropic-messages"),
Type.Literal("google-generative-ai"),
Type.Literal("google-vertex"),
])
]),
),
reasoning: Type.Boolean(),
input: Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")])),
@@ -66,7 +66,7 @@ const ProviderConfigSchema = Type.Object({
Type.Literal("anthropic-messages"),
Type.Literal("google-generative-ai"),
Type.Literal("google-vertex"),
])
]),
),
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
authHeader: Type.Optional(Type.Boolean()),
@@ -126,7 +126,7 @@ export class ModelRegistry {
constructor(
readonly authStorage: AuthStorage,
private modelsJsonPath: string | undefined = undefined,
private fallbackPaths: string[] = []
private fallbackPaths: string[] = [],
) {
// Set up fallback resolver for custom provider API keys
this.authStorage.setFallbackResolver((provider) => {
@@ -189,7 +189,9 @@ export class ModelRegistry {
// Update github-copilot base URL based on OAuth credentials
const copilotCred = this.authStorage.get("github-copilot");
if (copilotCred?.type === "oauth") {
const domain = copilotCred.enterpriseUrl ? normalizeDomain(copilotCred.enterpriseUrl) ?? undefined : undefined;
const domain = copilotCred.enterpriseUrl
? (normalizeDomain(copilotCred.enterpriseUrl) ?? undefined)
: undefined;
const baseUrl = getGitHubCopilotBaseUrl(copilotCred.access, domain);
this.models = combined.map((m) => (m.provider === "github-copilot" ? { ...m, baseUrl } : m));
} else {
@@ -265,7 +267,7 @@ export class ModelRegistry {
return emptyCustomModelsResult(`Failed to parse models.json: ${error.message}\n\nFile: ${modelsJsonPath}`);
}
return emptyCustomModelsResult(
`Failed to load models.json: ${error instanceof Error ? error.message : error}\n\nFile: ${modelsJsonPath}`
`Failed to load models.json: ${error instanceof Error ? error.message : error}\n\nFile: ${modelsJsonPath}`,
);
}
}
@@ -279,7 +281,7 @@ export class ModelRegistry {
// Override-only config: just needs baseUrl (to override built-in)
if (!providerConfig.baseUrl) {
throw new Error(
`Provider ${providerName}: must specify either "baseUrl" (for override) or "models" (for replacement).`
`Provider ${providerName}: must specify either "baseUrl" (for override) or "models" (for replacement).`,
);
}
} else {
@@ -297,7 +299,7 @@ export class ModelRegistry {
if (!hasProviderApi && !hasModelApi) {
throw new Error(
`Provider ${providerName}, model ${modelDef.id}: no "api" specified. Set at provider or model level.`
`Provider ${providerName}, model ${modelDef.id}: no "api" specified. Set at provider or model level.`,
);
}
@@ -329,7 +331,9 @@ export class ModelRegistry {
// Merge headers: provider headers are base, model headers override
let headers =
providerConfig.headers || modelDef.headers ? { ...providerConfig.headers, ...modelDef.headers } : undefined;
providerConfig.headers || modelDef.headers
? { ...providerConfig.headers, ...modelDef.headers }
: undefined;
// If authHeader is true, add Authorization header with resolved API key
if (providerConfig.authHeader && providerConfig.apiKey) {
@@ -2,8 +2,8 @@
* Model resolution, scoping, and initial selection
*/
import { type Api, type KnownProvider, type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import { type Api, type KnownProvider, type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
import chalk from "chalk";
import { minimatch } from "minimatch";
import { isValidThinkingLevel } from "../cli/args";
@@ -25,6 +25,7 @@ export const defaultModelPerProvider: Record<KnownProvider, string> = {
cerebras: "zai-glm-4.6",
zai: "glm-4.6",
mistral: "devstral-medium-latest",
opencode: "claude-sonnet-4-5",
};
export interface ScopedModel {
@@ -79,7 +80,7 @@ function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Mod
const provider = modelPattern.substring(0, slashIndex);
const modelId = modelPattern.substring(slashIndex + 1);
const providerMatch = availableModels.find(
(m) => m.provider.toLowerCase() === provider.toLowerCase() && m.id.toLowerCase() === modelId.toLowerCase()
(m) => m.provider.toLowerCase() === provider.toLowerCase() && m.id.toLowerCase() === modelId.toLowerCase(),
);
if (providerMatch) {
return providerMatch;
@@ -97,7 +98,7 @@ function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Mod
const matches = availableModels.filter(
(m) =>
m.id.toLowerCase().includes(modelPattern.toLowerCase()) ||
m.name?.toLowerCase().includes(modelPattern.toLowerCase())
m.name?.toLowerCase().includes(modelPattern.toLowerCase()),
);
if (matches.length === 0) {
@@ -351,7 +352,7 @@ export async function restoreModelFromSession(
savedModelId: string,
currentModel: Model<Api> | undefined,
shouldPrintMessages: boolean,
modelRegistry: ModelRegistry
modelRegistry: ModelRegistry,
): Promise<{ model: Model<Api> | undefined; fallbackMessage: string | undefined }> {
const restoredModel = modelRegistry.find(savedProvider, savedModelId);
@@ -427,7 +428,7 @@ export async function restoreModelFromSession(
*/
export async function findSmolModel(
modelRegistry: ModelRegistry,
savedModel?: string
savedModel?: string,
): Promise<Model<Api> | undefined> {
const availableModels = modelRegistry.getAvailable();
if (availableModels.length === 0) return undefined;
@@ -470,7 +471,7 @@ export async function findSmolModel(
*/
export async function findSlowModel(
modelRegistry: ModelRegistry,
savedModel?: string
savedModel?: string,
): Promise<Model<Api> | undefined> {
const availableModels = modelRegistry.getAvailable();
if (availableModels.length === 0) return undefined;
+13 -13
View File
@@ -27,8 +27,8 @@
*/
import { join } from "node:path";
import type { Model } from "@oh-my-pi/pi-ai";
import { Agent, type AgentTool, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Model } from "@oh-my-pi/pi-ai";
import type { Component } from "@oh-my-pi/pi-tui";
import chalk from "chalk";
// Import discovery to register all providers on startup
@@ -248,7 +248,7 @@ export async function discoverAuthStorage(agentDir: string = getDefaultAgentDir(
*/
export async function discoverModels(
authStorage: AuthStorage,
agentDir: string = getDefaultAgentDir()
agentDir: string = getDefaultAgentDir(),
): Promise<ModelRegistry> {
const primaryPath = join(agentDir, "models.json");
// Get all models.json paths (user-level only), excluding the primary
@@ -288,7 +288,7 @@ export function discoverSkills(cwd?: string, _agentDir?: string, settings?: Skil
*/
export function discoverContextFiles(
cwd?: string,
_agentDir?: string
_agentDir?: string,
): Array<{ path: string; content: string; depth?: number }> {
return loadContextFilesInternal({
cwd: cwd ?? process.cwd(),
@@ -436,11 +436,11 @@ function customToolToDefinition(tool: CustomTool): ToolDefinition {
const component = tool.renderResult?.(
result,
{ expanded: options.expanded, isPartial: options.isPartial, spinnerFrame: options.spinnerFrame },
theme
theme,
);
// Return empty component if undefined to match Component type requirement
return component ?? ({ render: () => [] } as unknown as Component);
}
}
: undefined,
[TOOL_DEFINITION_MARKER]: true,
};
@@ -465,19 +465,19 @@ function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory {
};
api.on("session_start", async (_event, ctx) =>
runOnSession({ reason: "start", previousSessionFile: undefined }, ctx)
runOnSession({ reason: "start", previousSessionFile: undefined }, ctx),
);
api.on("session_switch", async (event, ctx) =>
runOnSession({ reason: "switch", previousSessionFile: event.previousSessionFile }, ctx)
runOnSession({ reason: "switch", previousSessionFile: event.previousSessionFile }, ctx),
);
api.on("session_branch", async (event, ctx) =>
runOnSession({ reason: "branch", previousSessionFile: event.previousSessionFile }, ctx)
runOnSession({ reason: "branch", previousSessionFile: event.previousSessionFile }, ctx),
);
api.on("session_tree", async (_event, ctx) =>
runOnSession({ reason: "tree", previousSessionFile: undefined }, ctx)
runOnSession({ reason: "tree", previousSessionFile: undefined }, ctx),
);
api.on("session_shutdown", async (_event, ctx) =>
runOnSession({ reason: "shutdown", previousSessionFile: undefined }, ctx)
runOnSession({ reason: "shutdown", previousSessionFile: undefined }, ctx),
);
};
}
@@ -659,7 +659,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
if (options.hasUI && serverNames.length > 0) {
process.stderr.write(
chalk.gray(`Connecting to MCP servers: ${serverNames.join(", ")}...
`)
`),
);
}
},
@@ -728,7 +728,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
configuredPaths,
cwd,
eventBus,
settingsManager.getDisabledExtensions()
settingsManager.getDisabledExtensions(),
);
time("discoverAndLoadExtensions");
for (const { path, error } of extensionsResult.errors) {
@@ -905,7 +905,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
transformContext: extensionRunner
? async (messages) => {
return extensionRunner.emitContext(messages);
}
}
: undefined,
steeringMode: settingsManager.getSteeringMode(),
followUpMode: settingsManager.getFollowUpMode(),
@@ -1,6 +1,6 @@
import { basename, join, resolve } from "node:path";
import type { ImageContent, Message, TextContent, Usage } from "@oh-my-pi/pi-ai";
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
import type { ImageContent, Message, TextContent, Usage } from "@oh-my-pi/pi-ai";
import { nanoid } from "nanoid";
import { getAgentDir as getDefaultAgentDir } from "../config";
import { resizeImage } from "../utils/image-resize";
@@ -304,7 +304,7 @@ function toError(value: unknown): Error {
export function buildSessionContext(
entries: SessionEntry[],
leafId?: string | null,
byId?: Map<string, SessionEntry>
byId?: Map<string, SessionEntry>,
): SessionContext {
// Build uuid index if not available
if (!byId) {
@@ -382,7 +382,7 @@ export function buildSessionContext(
messages.push(entry.message);
} else if (entry.type === "custom_message") {
messages.push(
createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp)
createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp),
);
} else if (entry.type === "branch_summary" && entry.summary) {
messages.push(createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp));
@@ -540,7 +540,7 @@ function getSortedSessions(sessionDir: string, storage: SessionStorage): RecentS
/** Exported for testing */
export function findMostRecentSession(
sessionDir: string,
storage: SessionStorage = new FileSessionStorage()
storage: SessionStorage = new FileSessionStorage(),
): string | null {
const sessions = getSortedSessions(sessionDir, storage);
return sessions[0]?.path || null;
@@ -645,7 +645,7 @@ async function truncateForPersistence<T>(obj: T, key?: string): Promise<T> {
const newItem = await truncateForPersistence(item, key);
if (newItem !== item) changed = true;
return newItem;
})
}),
);
return changed ? (result as T) : obj;
}
@@ -800,7 +800,7 @@ class NdjsonFileWriter {
export function getRecentSessions(
sessionDir: string,
limit = 3,
storage: SessionStorage = new FileSessionStorage()
storage: SessionStorage = new FileSessionStorage(),
): RecentSessionInfo[] {
return getSortedSessions(sessionDir, storage).slice(0, limit);
}
@@ -1021,7 +1021,7 @@ export class SessionManager {
async () => {
await this._closePersistWriterInternal();
},
{ ignoreError: true }
{ ignoreError: true },
);
}
@@ -1229,7 +1229,7 @@ export class SessionManager {
firstKeptEntryId: string,
tokensBefore: number,
details?: T,
fromExtension?: boolean
fromExtension?: boolean,
): string {
const entry: CompactionEntry<T> = {
type: "compaction",
@@ -1272,7 +1272,7 @@ export class SessionManager {
customType: string,
content: string | (TextContent | ImageContent)[],
display: boolean,
details?: T
details?: T,
): string {
const entry: CustomMessageEntry<T> = {
type: "custom_message",
@@ -1632,7 +1632,7 @@ export class SessionManager {
static async open(
path: string,
sessionDir?: string,
storage: SessionStorage = new FileSessionStorage()
storage: SessionStorage = new FileSessionStorage(),
): Promise<SessionManager> {
// Extract cwd from session header if possible, otherwise use process.cwd()
const entries = loadEntriesFromFile(path, storage);
@@ -1653,7 +1653,7 @@ export class SessionManager {
static async continueRecent(
cwd: string,
sessionDir?: string,
storage: SessionStorage = new FileSessionStorage()
storage: SessionStorage = new FileSessionStorage(),
): Promise<SessionManager> {
const dir = sessionDir ?? getDefaultSessionDir(cwd, storage);
const mostRecent = findMostRecentSession(dir, storage);
@@ -6,8 +6,8 @@
import * as fs from "node:fs";
import * as path from "node:path";
import type { TextContent } from "@oh-my-pi/pi-ai";
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
import type { TextContent } from "@oh-my-pi/pi-ai";
import type { Component } from "@oh-my-pi/pi-tui";
import { Text } from "@oh-my-pi/pi-tui";
import { Type } from "@sinclair/typebox";
+1 -1
View File
@@ -1,7 +1,7 @@
import { homedir } from "node:os";
import path from "node:path";
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
import type { Component } from "@oh-my-pi/pi-tui";
import { Text } from "@oh-my-pi/pi-tui";
import { Type } from "@sinclair/typebox";
@@ -13,8 +13,8 @@
* - Session artifacts for debugging
*/
import type { Usage } from "@oh-my-pi/pi-ai";
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
import type { Usage } from "@oh-my-pi/pi-ai";
import type { Theme } from "../../../modes/interactive/theme/theme";
import taskDescriptionTemplate from "../../../prompts/tools/task.md" with { type: "text" };
import { formatDuration } from "../render-utils";
@@ -59,12 +59,12 @@ export const taskSchema = Type.Object({
model: Type.Optional(
Type.String({
description: "Model override for all tasks (fuzzy matching, e.g. 'sonnet', 'opus')",
})
}),
),
output: Type.Optional(
Type.Any({
description: "JTD schema for structured subagent output (used by the complete tool)",
})
}),
),
tasks: Type.Array(taskItemSchema, {
description: "Tasks to run in parallel",
@@ -13,8 +13,8 @@
* 5. Parent can send { type: "abort" } to request cancellation
*/
import type { Api, Model } from "@oh-my-pi/pi-ai";
import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, Model } from "@oh-my-pi/pi-ai";
import type { AgentSessionEvent } from "../../agent-session";
import { parseModelPattern, parseModelString } from "../../model-resolver";
import { createAgentSession, discoverAuthStorage, discoverModels } from "../../sdk";
@@ -88,7 +88,7 @@ let pendingAbort = false;
*/
function resolveModelOverride(
override: string | undefined,
modelRegistry: { getAvailable: () => Model<Api>[]; find: (provider: string, id: string) => Model<Api> | undefined }
modelRegistry: { getAvailable: () => Model<Api>[]; find: (provider: string, id: string) => Model<Api> | undefined },
): { model?: Model<Api>; thinkingLevel?: ThinkingLevel } {
if (!override) return {};
@@ -190,7 +190,7 @@ async function runTask(runState: RunState, payload: SubagentWorkerStartPayload):
() => {
void session.abort();
},
{ once: true, signal: sessionAbortController.signal }
{ once: true, signal: sessionAbortController.signal },
);
// Initialize extensions (equivalent to CLI's extension initialization)
+5 -5
View File
@@ -63,7 +63,7 @@ async function runInteractiveMode(
lspServers: Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }> | undefined,
initialMessage?: string,
initialImages?: ImageContent[],
fdPath: string | undefined = undefined
fdPath: string | undefined = undefined,
): Promise<void> {
const mode = new InteractiveMode(session, version, changelogMarkdown, setExtensionUIContext, lspServers, fdPath);
@@ -120,7 +120,7 @@ async function runInteractiveMode(
async function prepareInitialMessage(
parsed: Args,
autoResizeImages: boolean
autoResizeImages: boolean,
): Promise<{
initialMessage?: string;
initialImages?: ImageContent[];
@@ -262,7 +262,7 @@ async function buildSessionOptions(
scopedModels: ScopedModel[],
sessionManager: SessionManager | undefined,
modelRegistry: ModelRegistry,
settingsManager: SettingsManager
settingsManager: SettingsManager,
): Promise<CreateAgentSessionOptions> {
const options: CreateAgentSessionOptions = {
cwd: parsed.cwd ?? process.cwd(),
@@ -484,7 +484,7 @@ export async function main(args: string[]) {
scopedModels,
sessionManager,
modelRegistry,
settingsManager
settingsManager,
);
sessionOptions.authStorage = authStorage;
sessionOptions.modelRegistry = modelRegistry;
@@ -575,7 +575,7 @@ export async function main(args: string[]) {
lspServers,
initialMessage,
initialImages,
fdPath
fdPath,
);
} else {
await runPrintMode(session, mode, parsed.messages, initialMessage, initialImages);
@@ -33,7 +33,9 @@ export class AssistantMessageComponent extends Container {
if (
message.content.length > 0 &&
message.content.some((c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()))
message.content.some(
(c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()),
)
) {
this.contentContainer.addChild(new Spacer(1));
}
@@ -61,7 +63,7 @@ export class AssistantMessageComponent extends Container {
new Markdown(content.thinking.trim(), 1, 0, getMarkdownTheme(), {
color: (text: string) => theme.fg("thinkingText", text),
italic: true,
})
}),
);
this.contentContainer.addChild(new Spacer(1));
}
@@ -89,7 +89,7 @@ export class CustomMessageComponent extends Container {
this.box.addChild(
new Markdown(text, 0, 0, getMarkdownTheme(), {
color: (value: string) => theme.fg("customMessageText", value),
})
}),
);
}
}
@@ -181,9 +181,9 @@ export class FooterComponent implements Component {
// Calculate context percentage from last message (input + output + cacheRead + cacheWrite)
const contextTokens = lastAssistantMessage
? lastAssistantMessage.usage.input +
lastAssistantMessage.usage.output +
lastAssistantMessage.usage.cacheRead +
lastAssistantMessage.usage.cacheWrite
lastAssistantMessage.usage.output +
lastAssistantMessage.usage.cacheRead +
lastAssistantMessage.usage.cacheWrite
: 0;
const contextWindow = state.model?.contextWindow || 0;
const contextPercentValue = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
@@ -90,7 +90,7 @@ export class HookMessageComponent extends Container {
this.box.addChild(
new Markdown(text, 0, 0, getMarkdownTheme(), {
color: (text: string) => theme.fg("customMessageText", text),
})
}),
);
}
}
@@ -93,7 +93,7 @@ export class ModelSelectorComponent extends Container {
scopedModels: ReadonlyArray<ScopedModelItem>,
onSelect: (model: Model<any>, role: string) => void,
onCancel: () => void,
options?: { temporaryOnly?: boolean }
options?: { temporaryOnly?: boolean },
) {
super();
@@ -327,7 +327,7 @@ export class ModelSelectorComponent extends Container {
const maxVisible = 10;
const startIndex = Math.max(
0,
Math.min(this.selectedIndex - Math.floor(maxVisible / 2), this.filteredModels.length - maxVisible)
Math.min(this.selectedIndex - Math.floor(maxVisible / 2), this.filteredModels.length - maxVisible),
);
const endIndex = Math.min(startIndex + maxVisible, this.filteredModels.length);
@@ -419,7 +419,7 @@ export class ModelSelectorComponent extends Container {
const menuWidth = Math.max(
visibleWidth(headerText),
visibleWidth(hintText),
...actionLines.map((line) => visibleWidth(line))
...actionLines.map((line) => visibleWidth(line)),
);
// Menu header
@@ -21,7 +21,7 @@ export class OAuthSelectorComponent extends Container {
mode: "login" | "logout",
authStorage: AuthStorage,
onSelect: (providerId: string) => void,
onCancel: () => void
onCancel: () => void,
) {
super();
@@ -225,9 +225,9 @@ export class StatusLineComponent implements Component {
const contextTokens = lastAssistantMessage
? lastAssistantMessage.usage.input +
lastAssistantMessage.usage.output +
lastAssistantMessage.usage.cacheRead +
lastAssistantMessage.usage.cacheWrite
lastAssistantMessage.usage.output +
lastAssistantMessage.usage.cacheRead +
lastAssistantMessage.usage.cacheWrite
: 0;
const contextWindow = state.model?.contextWindow || 0;
const contextPercent = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
@@ -6,8 +6,8 @@
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import type { AssistantMessage, ImageContent, Message, OAuthProvider } from "@oh-my-pi/pi-ai";
import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { AssistantMessage, ImageContent, Message, OAuthProvider } from "@oh-my-pi/pi-ai";
import type { SlashCommand } from "@oh-my-pi/pi-tui";
import {
CombinedAutocompleteProvider,
@@ -181,8 +181,10 @@ export class InteractiveMode {
version: string,
changelogMarkdown: string | undefined = undefined,
private setToolUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void = () => {},
private lspServers: Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }> | undefined = undefined,
fdPath: string | undefined = undefined
private lspServers:
| Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }>
| undefined = undefined,
fdPath: string | undefined = undefined,
) {
this.session = session;
this.version = version;
@@ -268,7 +270,7 @@ export class InteractiveMode {
const autocompleteProvider = new CombinedAutocompleteProvider(
[...slashCommands, ...fileSlashCommands, ...hookCommands, ...customCommands],
process.cwd(),
fdPath
fdPath,
);
this.editor.setAutocompleteProvider(autocompleteProvider);
}
@@ -453,7 +455,7 @@ export class InteractiveMode {
this.chatContainer.addChild(new Spacer(1));
this.chatContainer.addChild(
new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1)
new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
);
this.ui.requestRender();
@@ -583,7 +585,7 @@ export class InteractiveMode {
this.chatContainer.addChild(new Spacer(1));
this.chatContainer.addChild(
new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1)
new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
);
this.ui.requestRender();
@@ -649,8 +651,8 @@ export class InteractiveMode {
_factory: (
tui: TUI,
theme: Theme,
done: (result: T) => void
) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>
done: (result: T) => void,
) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
) => undefined as T,
setEditorText: () => {},
getEditorText: () => "",
@@ -666,7 +668,7 @@ export class InteractiveMode {
*/
private async emitCustomToolSessionEvent(
reason: "start" | "switch" | "branch" | "tree" | "shutdown",
previousSessionFile?: string
previousSessionFile?: string,
): Promise<void> {
const event = { reason, previousSessionFile };
const uiContext = this.session.extensionRunner?.getUIContext();
@@ -736,7 +738,7 @@ export class InteractiveMode {
() => {
this.hideHookSelector();
resolve(undefined);
}
},
);
this.editorContainer.clear();
@@ -780,7 +782,7 @@ export class InteractiveMode {
() => {
this.hideHookInput();
resolve(undefined);
}
},
);
this.editorContainer.clear();
@@ -817,7 +819,7 @@ export class InteractiveMode {
() => {
this.hideHookEditor();
resolve(undefined);
}
},
);
this.editorContainer.clear();
@@ -858,8 +860,8 @@ export class InteractiveMode {
factory: (
tui: TUI,
theme: Theme,
done: (result: T) => void
) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>
done: (result: T) => void,
) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
): Promise<T> {
const savedText = this.editor.getText();
@@ -1197,7 +1199,7 @@ export class InteractiveMode {
(spinner) => theme.fg("accent", spinner),
(text) => theme.fg("muted", text),
`Working${theme.format.ellipsis} (esc to interrupt)`,
getSymbolTheme().spinnerFrames
getSymbolTheme().spinnerFrames,
);
this.statusContainer.addChild(this.loadingAnimation);
this.startVoiceProgressTimer();
@@ -1243,7 +1245,7 @@ export class InteractiveMode {
},
tool,
this.ui,
this.sessionManager.getCwd()
this.sessionManager.getCwd(),
);
component.setExpanded(this.toolOutputExpanded);
this.chatContainer.addChild(component);
@@ -1313,7 +1315,7 @@ export class InteractiveMode {
},
tool,
this.ui,
this.sessionManager.getCwd()
this.sessionManager.getCwd(),
);
component.setExpanded(this.toolOutputExpanded);
this.chatContainer.addChild(component);
@@ -1384,7 +1386,7 @@ export class InteractiveMode {
(spinner) => theme.fg("accent", spinner),
(text) => theme.fg("muted", text),
`${reasonText}Auto-compacting${theme.format.ellipsis} (esc to cancel)`,
getSymbolTheme().spinnerFrames
getSymbolTheme().spinnerFrames,
);
this.statusContainer.addChild(this.autoCompactionLoader);
this.ui.requestRender();
@@ -1440,7 +1442,7 @@ export class InteractiveMode {
(spinner) => theme.fg("warning", spinner),
(text) => theme.fg("muted", text),
`Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s${theme.format.ellipsis} (esc to cancel)`,
getSymbolTheme().spinnerFrames
getSymbolTheme().spinnerFrames,
);
this.statusContainer.addChild(this.retryLoader);
this.ui.requestRender();
@@ -1536,7 +1538,7 @@ export class InteractiveMode {
message.exitCode,
message.cancelled,
message.truncated ? ({ truncated: true } as TruncationResult) : undefined,
message.fullOutputPath
message.fullOutputPath,
);
this.chatContainer.addChild(component);
break;
@@ -1569,7 +1571,7 @@ export class InteractiveMode {
for (const file of message.files) {
const text = `${theme.fg("dim", `${theme.tree.last} `)}${theme.fg("muted", "Read")} ${theme.fg(
"accent",
file.path
file.path,
)} ${theme.fg("dim", `(${file.lineCount} lines)`)}`;
this.chatContainer.addChild(new Text(text, 0, 0));
}
@@ -1609,7 +1611,7 @@ export class InteractiveMode {
*/
private renderSessionContext(
sessionContext: SessionContext,
options: { updateFooter?: boolean; populateHistory?: boolean } = {}
options: { updateFooter?: boolean; populateHistory?: boolean } = {},
): void {
this.pendingTools.clear();
@@ -1632,7 +1634,7 @@ export class InteractiveMode {
{ showImages: this.settingsManager.getShowImages() },
tool,
this.ui,
this.sessionManager.getCwd()
this.sessionManager.getCwd(),
);
component.setExpanded(this.toolOutputExpanded);
this.chatContainer.addChild(component);
@@ -2115,8 +2117,8 @@ export class InteractiveMode {
theme.fg("muted", `New version ${newVersion} is available. Run: `) +
theme.fg("accent", "omp update"),
1,
0
)
0,
),
);
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
this.ui.requestRender();
@@ -2208,7 +2210,7 @@ export class InteractiveMode {
this.updateEditorTopBorder();
this.ui.requestRender();
},
}
},
);
return { component: selector, focus: selector };
});
@@ -2383,7 +2385,7 @@ export class InteractiveMode {
done();
this.ui.requestRender();
},
options
options,
);
return { component: selector, focus: selector };
});
@@ -2418,7 +2420,7 @@ export class InteractiveMode {
() => {
done();
this.ui.requestRender();
}
},
);
return { component: selector, focus: selector.getMessageList() };
});
@@ -2477,7 +2479,7 @@ export class InteractiveMode {
(spinner) => theme.fg("accent", spinner),
(text) => theme.fg("muted", text),
"Summarizing branch... (esc to cancel)",
getSymbolTheme().spinnerFrames
getSymbolTheme().spinnerFrames,
);
this.statusContainer.addChild(summaryLoader);
this.ui.requestRender();
@@ -2521,7 +2523,7 @@ export class InteractiveMode {
(entryId, label) => {
this.sessionManager.appendLabelChange(entryId, label);
this.ui.requestRender();
}
},
);
return { component: selector, focus: selector };
});
@@ -2542,7 +2544,7 @@ export class InteractiveMode {
},
() => {
void this.shutdown();
}
},
);
return { component: selector, focus: selector.getSessionList() };
});
@@ -2575,7 +2577,7 @@ export class InteractiveMode {
if (mode === "logout") {
const providers = this.session.modelRegistry.authStorage.list();
const loggedInProviders = providers.filter(
(p) => this.session.modelRegistry.authStorage.get(p)?.type === "oauth"
(p) => this.session.modelRegistry.authStorage.get(p)?.type === "oauth",
);
if (loggedInProviders.length === 0) {
this.showStatus("No OAuth providers logged in. Use /login first.");
@@ -2640,9 +2642,15 @@ export class InteractiveMode {
await this.session.modelRegistry.refresh();
this.chatContainer.addChild(new Spacer(1));
this.chatContainer.addChild(
new Text(theme.fg("success", `${theme.status.success} Successfully logged in to ${providerId}`), 1, 0)
new Text(
theme.fg("success", `${theme.status.success} Successfully logged in to ${providerId}`),
1,
0,
),
);
this.chatContainer.addChild(
new Text(theme.fg("dim", `Credentials saved to ${getAuthPath()}`), 1, 0),
);
this.chatContainer.addChild(new Text(theme.fg("dim", `Credentials saved to ${getAuthPath()}`), 1, 0));
this.ui.requestRender();
} catch (error: unknown) {
this.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -2654,9 +2662,15 @@ export class InteractiveMode {
await this.session.modelRegistry.refresh();
this.chatContainer.addChild(new Spacer(1));
this.chatContainer.addChild(
new Text(theme.fg("success", `${theme.status.success} Successfully logged out of ${providerId}`), 1, 0)
new Text(
theme.fg("success", `${theme.status.success} Successfully logged out of ${providerId}`),
1,
0,
),
);
this.chatContainer.addChild(
new Text(theme.fg("dim", `Credentials removed from ${getAuthPath()}`), 1, 0),
);
this.chatContainer.addChild(new Text(theme.fg("dim", `Credentials removed from ${getAuthPath()}`), 1, 0));
this.ui.requestRender();
} catch (error: unknown) {
this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -2666,7 +2680,7 @@ export class InteractiveMode {
() => {
done();
this.ui.requestRender();
}
},
);
return { component: selector, focus: selector };
});
@@ -2682,8 +2696,8 @@ export class InteractiveMode {
process.platform === "darwin"
? ["open", urlOrPath]
: process.platform === "win32"
? ["cmd", "/c", "start", "", urlOrPath]
: ["xdg-open", urlOrPath];
? ["cmd", "/c", "start", "", urlOrPath]
: ["xdg-open", urlOrPath];
Bun.spawn(args, { stdin: "ignore", stdout: "ignore", stderr: "ignore" });
} catch {
// Best-effort: browser opening is non-critical
@@ -3001,7 +3015,9 @@ export class InteractiveMode {
this.pendingTools.clear();
this.chatContainer.addChild(new Spacer(1));
this.chatContainer.addChild(new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1));
this.chatContainer.addChild(
new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
);
this.ui.requestRender();
}
@@ -3035,8 +3051,8 @@ export class InteractiveMode {
new Text(
`${theme.fg("accent", `${theme.status.success} Debug log written`)}\n${theme.fg("muted", debugLogPath)}`,
1,
1
)
1,
),
);
this.ui.requestRender();
}
@@ -3070,7 +3086,7 @@ export class InteractiveMode {
this.ui.requestRender();
}
},
{ excludeFromContext }
{ excludeFromContext },
);
if (this.bashComponent) {
@@ -3078,7 +3094,7 @@ export class InteractiveMode {
result.exitCode,
result.cancelled,
result.truncated ? ({ truncated: true, content: result.output } as TruncationResult) : undefined,
result.fullOutputPath
result.fullOutputPath,
);
}
} catch (error) {
@@ -3126,7 +3142,7 @@ export class InteractiveMode {
(spinner) => theme.fg("accent", spinner),
(text) => theme.fg("muted", text),
label,
getSymbolTheme().spinnerFrames
getSymbolTheme().spinnerFrames,
);
this.statusContainer.addChild(compactingLoader);
this.ui.requestRender();
@@ -24,7 +24,7 @@ export async function runPrintMode(
mode: "text" | "json",
messages: string[],
initialMessage?: string,
initialImages?: ImageContent[]
initialImages?: ImageContent[],
): Promise<void> {
// Extension runner already has no-op UI context by default (set in loader)
// Set up extensions for print mode (no UI)
@@ -4,8 +4,8 @@
* Spawns the agent in RPC mode and provides a typed API for all operations.
*/
import type { ImageContent } from "@oh-my-pi/pi-ai";
import type { AgentEvent, AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { ImageContent } from "@oh-my-pi/pi-ai";
import type { Subprocess } from "bun";
import type { SessionStats } from "../../core/agent-session";
import type { BashResult } from "../../core/bash-executor";
@@ -114,7 +114,7 @@ export class RpcClient {
}
}
},
})
}),
)
.getReader() as ReadableStreamDefaultReader<string>;
@@ -5,8 +5,8 @@
* Responses and events are emitted as JSON lines on stdout.
*/
import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
import type { SessionStats } from "../../core/agent-session";
import type { BashResult } from "../../core/bash-executor";
import type { CompactionResult } from "../../core/compaction/index";
@@ -33,7 +33,7 @@ const DEFAULT_OPTIONS: Required<ImageResizeOptions> = {
*/
async function resizeImageWithImageMagick(
img: ImageContent,
opts: Required<ImageResizeOptions>
opts: Required<ImageResizeOptions>,
): Promise<ResizedImage> {
// Try to get dimensions first
const dims = await getImageDimensionsWithImageMagick(img.data);
@@ -47,7 +47,7 @@ async function resizeImageWithImageMagick(
opts.maxWidth,
opts.maxHeight,
opts.maxBytes,
opts.jpegQuality
opts.jpegQuality,
);
if (result) {
@@ -77,7 +77,7 @@ async function resizeImageWithImageMagick(
/** Helper to pick the smaller of two buffers */
function pickSmaller(
a: { buffer: Buffer; mimeType: string },
b: { buffer: Buffer; mimeType: string }
b: { buffer: Buffer; mimeType: string },
): { buffer: Buffer; mimeType: string } {
return a.buffer.length <= b.buffer.length ? a : b;
}
@@ -145,9 +145,11 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
async function tryBothFormats(
width: number,
height: number,
jpegQuality: number
jpegQuality: number,
): Promise<{ buffer: Buffer; mimeType: string }> {
const resized = await sharp!(buffer).resize(width, height, { fit: "inside", withoutEnlargement: true }).toBuffer();
const resized = await sharp!(buffer)
.resize(width, height, { fit: "inside", withoutEnlargement: true })
.toBuffer();
const [pngBuffer, jpegBuffer] = await Promise.all([
sharp!(resized).png({ compressionLevel: 9 }).toBuffer(),