chore: bump version to 3.34.0

This commit is contained in:
can1357
2026-01-09 12:11:50 +01:00
parent 5a63120dd3
commit 86ef2bd34b
68 changed files with 299 additions and 265 deletions
+5 -13
View File
@@ -20,7 +20,7 @@
},
"packages/agent": {
"name": "@oh-my-pi/pi-agent-core",
"version": "3.33.0",
"version": "3.34.0",
"dependencies": {
"@oh-my-pi/pi-ai": "workspace:*",
"@oh-my-pi/pi-tui": "workspace:*",
@@ -32,7 +32,7 @@
},
"packages/ai": {
"name": "@oh-my-pi/pi-ai",
"version": "0.42.0",
"version": "3.34.0",
"bin": {
"pi-ai": "./src/cli.ts",
},
@@ -54,7 +54,7 @@
},
"packages/coding-agent": {
"name": "@oh-my-pi/pi-coding-agent",
"version": "3.33.0",
"version": "3.34.0",
"bin": {
"omp": "src/cli.ts",
},
@@ -95,7 +95,7 @@
},
"packages/git-tool": {
"name": "@oh-my-pi/pi-git-tool",
"version": "3.33.0",
"version": "3.34.0",
"devDependencies": {
"@types/node": "^24.3.0",
"vitest": "^3.2.4",
@@ -103,7 +103,7 @@
},
"packages/tui": {
"name": "@oh-my-pi/pi-tui",
"version": "3.33.0",
"version": "3.34.0",
"dependencies": {
"@types/mime-types": "^2.1.4",
"chalk": "^5.5.0",
@@ -652,8 +652,6 @@
"jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
@@ -970,12 +968,6 @@
"@oh-my-pi/pi-git-tool/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="],
"@openai/agents/openai": ["openai@6.15.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-F1Lvs5BoVvmZtzkUEVyh8mDQPPFolq4F+xdsx/DO8Hee8YF3IGAlZqUIsF+DVGhqf4aU0a3bTghsxB6OIsRy1g=="],
"@openai/agents-core/openai": ["openai@6.15.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-F1Lvs5BoVvmZtzkUEVyh8mDQPPFolq4F+xdsx/DO8Hee8YF3IGAlZqUIsF+DVGhqf4aU0a3bTghsxB6OIsRy1g=="],
"@openai/agents-openai/openai": ["openai@6.15.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-F1Lvs5BoVvmZtzkUEVyh8mDQPPFolq4F+xdsx/DO8Hee8YF3IGAlZqUIsF+DVGhqf4aU0a3bTghsxB6OIsRy1g=="],
"@types/ndjson/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="],
"@types/through/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="],
+2
View File
@@ -2,6 +2,8 @@
## [Unreleased]
## [3.34.0] - 2026-01-09
## [3.33.0] - 2026-01-08
### Fixed
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@oh-my-pi/pi-agent-core",
"version": "3.33.0",
"version": "3.34.0",
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
"type": "module",
"main": "./src/index.ts",
+11 -11
View File
@@ -30,7 +30,7 @@ export function agentLoop(
context: AgentContext,
config: AgentLoopConfig,
signal?: AbortSignal,
streamFn?: StreamFn
streamFn?: StreamFn,
): EventStream<AgentEvent, AgentMessage[]> {
const stream = createAgentStream();
@@ -66,7 +66,7 @@ export function agentLoopContinue(
context: AgentContext,
config: AgentLoopConfig,
signal?: AbortSignal,
streamFn?: StreamFn
streamFn?: StreamFn,
): EventStream<AgentEvent, AgentMessage[]> {
if (context.messages.length === 0) {
throw new Error("Cannot continue: no messages in context");
@@ -94,13 +94,13 @@ export function agentLoopContinue(
function createAgentStream(): EventStream<AgentEvent, AgentMessage[]> {
return new EventStream<AgentEvent, AgentMessage[]>(
(event: AgentEvent) => event.type === "agent_end",
(event: AgentEvent) => (event.type === "agent_end" ? event.messages : [])
(event: AgentEvent) => (event.type === "agent_end" ? event.messages : []),
);
}
function normalizeMessagesForProvider(
messages: Context["messages"],
model: AgentLoopConfig["model"]
model: AgentLoopConfig["model"],
): Context["messages"] {
if (model.provider !== "cerebras") {
return messages;
@@ -133,7 +133,7 @@ async function runLoop(
config: AgentLoopConfig,
signal: AbortSignal | undefined,
stream: EventStream<AgentEvent, AgentMessage[]>,
streamFn?: StreamFn
streamFn?: StreamFn,
): Promise<void> {
let firstTurn = true;
// Check for steering messages at start (user may have typed while waiting)
@@ -187,7 +187,7 @@ async function runLoop(
stream,
config.getSteeringMessages,
config.getToolContext,
config.interruptMode
config.interruptMode,
);
toolResults.push(...toolExecution.toolResults);
steeringAfterTools = toolExecution.steeringMessages ?? null;
@@ -234,7 +234,7 @@ async function streamAssistantResponse(
config: AgentLoopConfig,
signal: AbortSignal | undefined,
stream: EventStream<AgentEvent, AgentMessage[]>,
streamFn?: StreamFn
streamFn?: StreamFn,
): Promise<AssistantMessage> {
// Apply context transform if configured (AgentMessage[] → AgentMessage[])
let messages = context.messages;
@@ -291,7 +291,7 @@ async function streamAssistantResponse(
stopReason: "aborted",
errorMessage,
timestamp: Date.now(),
};
};
if (addedPartial) {
context.messages[context.messages.length - 1] = abortedMessage;
} else {
@@ -360,7 +360,7 @@ async function executeToolCalls(
stream: EventStream<AgentEvent, AgentMessage[]>,
getSteeringMessages?: AgentLoopConfig["getSteeringMessages"],
getToolContext?: AgentLoopConfig["getToolContext"],
interruptMode: AgentLoopConfig["interruptMode"] = "immediate"
interruptMode: AgentLoopConfig["interruptMode"] = "immediate",
): Promise<{ toolResults: ToolResultMessage[]; steeringMessages?: AgentMessage[] }> {
const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
const results: ToolResultMessage[] = [];
@@ -400,7 +400,7 @@ async function executeToolCalls(
partialResult,
});
},
toolContext
toolContext,
);
} catch (e) {
result = {
@@ -451,7 +451,7 @@ async function executeToolCalls(
function skipToolCall(
toolCall: Extract<AssistantMessage["content"][number], { type: "toolCall" }>,
stream: EventStream<AgentEvent, AgentMessage[]>
stream: EventStream<AgentEvent, AgentMessage[]>,
): ToolResultMessage {
const result: AgentToolResult<any> = {
content: [{ type: "text", text: "Skipped due to queued user message." }],
+2 -2
View File
@@ -264,7 +264,7 @@ export class Agent {
async prompt(input: string | AgentMessage | AgentMessage[], images?: ImageContent[]) {
if (this._state.isStreaming) {
throw new Error(
"Agent is already processing a prompt. Use steer() or followUp() to queue messages, or wait for completion."
"Agent is already processing a prompt. Use steer() or followUp() to queue messages, or wait for completion.",
);
}
@@ -438,7 +438,7 @@ export class Agent {
(c) =>
(c.type === "thinking" && c.thinking.trim().length > 0) ||
(c.type === "text" && c.text.trim().length > 0) ||
(c.type === "toolCall" && c.name.trim().length > 0)
(c.type === "toolCall" && c.name.trim().length > 0),
);
if (!onlyEmpty) {
this.appendMessage(partial);
+2 -2
View File
@@ -24,7 +24,7 @@ class ProxyMessageEventStream extends EventStream<AssistantMessageEvent, Assista
if (event.type === "done") return event.message;
if (event.type === "error") return event.error;
throw new Error("Unexpected event type");
}
},
);
}
}
@@ -209,7 +209,7 @@ export function streamProxy(model: Model<any>, context: Context, options: ProxyS
*/
function processProxyEvent(
proxyEvent: ProxyAssistantMessageEvent,
partial: AssistantMessage
partial: AssistantMessage,
): AssistantMessageEvent | undefined {
switch (proxyEvent.type) {
case "start":
+1 -1
View File
@@ -198,7 +198,7 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
params: Static<TParameters>,
signal?: AbortSignal,
onUpdate?: AgentToolUpdateCallback<TDetails>,
context?: AgentToolContext
context?: AgentToolContext,
) => Promise<AgentToolResult<TDetails>>;
/** Optional custom rendering for tool call display (returns UI component) */
+8 -8
View File
@@ -20,7 +20,7 @@ class MockAssistantStream extends EventStream<AssistantMessageEvent, AssistantMe
if (event.type === "done") return event.message;
if (event.type === "error") return event.error;
throw new Error("Unexpected event type");
}
},
);
}
}
@@ -53,7 +53,7 @@ function createModel(): Model<"openai-responses"> {
function createAssistantMessage(
content: AssistantMessage["content"],
stopReason: AssistantMessage["stopReason"] = "stop"
stopReason: AssistantMessage["stopReason"] = "stop",
): AssistantMessage {
return {
role: "assistant",
@@ -209,7 +209,7 @@ describe("agentLoop with AgentMessage", () => {
},
convertToLlm: (messages) => {
convertedMessages = messages.filter(
(m) => m.role === "user" || m.role === "assistant" || m.role === "toolResult"
(m) => m.role === "user" || m.role === "assistant" || m.role === "toolResult",
) as Message[];
return convertedMessages;
},
@@ -274,7 +274,7 @@ describe("agentLoop with AgentMessage", () => {
// First call: return tool call
const message = createAssistantMessage(
[{ type: "toolCall", id: "tool-1", name: "echo", arguments: { value: "hello" } }],
"toolUse"
"toolUse",
);
stream.push({ type: "done", reason: "toolUse", message });
} else {
@@ -355,7 +355,7 @@ describe("agentLoop with AgentMessage", () => {
// Check if interrupt message is in context on second call
if (callIndex === 1) {
sawInterruptInContext = ctx.messages.some(
(m) => m.role === "user" && typeof m.content === "string" && m.content === "interrupt"
(m) => m.role === "user" && typeof m.content === "string" && m.content === "interrupt",
);
}
@@ -368,7 +368,7 @@ describe("agentLoop with AgentMessage", () => {
{ type: "toolCall", id: "tool-1", name: "echo", arguments: { value: "first" } },
{ type: "toolCall", id: "tool-2", name: "echo", arguments: { value: "second" } },
],
"toolUse"
"toolUse",
);
mockStream.push({ type: "done", reason: "toolUse", message });
} else {
@@ -390,7 +390,7 @@ describe("agentLoop with AgentMessage", () => {
// Second tool should be skipped
const toolEnds = events.filter(
(e): e is Extract<AgentEvent, { type: "tool_execution_end" }> => e.type === "tool_execution_end"
(e): e is Extract<AgentEvent, { type: "tool_execution_end" }> => e.type === "tool_execution_end",
);
expect(toolEnds.length).toBe(2);
expect(toolEnds[0].isError).toBeFalsy();
@@ -405,7 +405,7 @@ describe("agentLoop with AgentMessage", () => {
e.type === "message_start" &&
e.message.role === "user" &&
typeof e.message.content === "string" &&
e.message.content === "interrupt"
e.message.content === "interrupt",
);
expect(queuedMessageEvent).toBeDefined();
+2 -1
View File
@@ -414,7 +414,8 @@ describe("Agent.continue()", () => {
it("should continue and process tool results", async () => {
const agent = new Agent({
initialState: {
systemPrompt: "You are a helpful assistant. After getting a calculation result, state the answer clearly.",
systemPrompt:
"You are a helpful assistant. After getting a calculation result, state the answer clearly.",
model,
thinkingLevel: "off",
tools: [calculateTool],
+2
View File
@@ -2,6 +2,8 @@
## [Unreleased]
## [3.34.0] - 2026-01-09
## [0.42.0] - 2026-01-09
### Added
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@oh-my-pi/pi-ai",
"version": "0.42.0",
"version": "3.34.0",
"description": "Unified LLM API with automatic model discovery and provider configuration",
"type": "module",
"main": "./src/index.ts",
+3 -3
View File
@@ -46,7 +46,7 @@ async function login(provider: OAuthProvider): Promise<void> {
},
async () => {
return await promptFn("Paste the authorization code:");
}
},
);
break;
@@ -71,7 +71,7 @@ async function login(provider: OAuthProvider): Promise<void> {
if (info.instructions) console.log(info.instructions);
console.log();
},
(msg) => console.log(msg)
(msg) => console.log(msg),
);
break;
@@ -82,7 +82,7 @@ async function login(provider: OAuthProvider): Promise<void> {
if (info.instructions) console.log(info.instructions);
console.log();
},
(msg) => console.log(msg)
(msg) => console.log(msg),
);
break;
case "openai-codex":
@@ -35,11 +35,7 @@ import {
import { getCodexInstructions } from "./openai-codex/prompts/codex";
import { buildCodexPiBridge } from "./openai-codex/prompts/pi-codex-bridge";
import { buildCodexSystemPrompt } from "./openai-codex/prompts/system-prompt";
import {
type CodexRequestOptions,
type RequestBody,
transformRequestBody,
} from "./openai-codex/request-transformer";
import { type CodexRequestOptions, type RequestBody, transformRequestBody } from "./openai-codex/request-transformer";
import { parseCodexError, parseCodexSseStream } from "./openai-codex/response-handler";
import { transformMessages } from "./transorm-messages";
+6 -5
View File
@@ -29,11 +29,12 @@ export interface ApiOptionsMap {
}
// Compile-time exhaustiveness check - this will fail if ApiOptionsMap doesn't have all KnownApi keys
type _CheckExhaustive = ApiOptionsMap extends Record<Api, StreamOptions>
? Record<Api, StreamOptions> extends ApiOptionsMap
? true
: ["ApiOptionsMap is missing some KnownApi values", Exclude<Api, keyof ApiOptionsMap>]
: ["ApiOptionsMap doesn't extend Record<KnownApi, StreamOptions>"];
type _CheckExhaustive =
ApiOptionsMap extends Record<Api, StreamOptions>
? Record<Api, StreamOptions> extends ApiOptionsMap
? true
: ["ApiOptionsMap is missing some KnownApi values", Exclude<Api, keyof ApiOptionsMap>]
: ["ApiOptionsMap doesn't extend Record<KnownApi, StreamOptions>"];
const _exhaustive: _CheckExhaustive = true;
// Helper type to get options for a specific API
+4 -4
View File
@@ -1,9 +1,9 @@
import { describe, expect, it } from "vitest";
import { MODELS } from "../src/models.generated.js";
import { complete } from "../src/stream.js";
import type { Model } from "../src/types.js";
import { MODELS } from "../src/models.generated";
import { complete } from "../src/stream";
import type { Model } from "../src/types";
describe("OpenCode Zen Models Smoke Test", () => {
describe.skipIf(!process.env.OPENCODE_API_KEY)("OpenCode Zen Models Smoke Test", () => {
const zenModels = Object.values(MODELS.opencode);
zenModels.forEach((model) => {
+2
View File
@@ -2,6 +2,8 @@
## [Unreleased]
## [3.34.0] - 2026-01-09
### Added
- Added caching for system environment detection to improve startup performance
@@ -263,7 +263,7 @@ export default function (pi: ExtensionAPI) {
return new Text(
`${theme.fg("success", "✓ Added ") + theme.fg("accent", `#${added.id}`)} ${theme.fg("muted", added.text)}`,
0,
0
0,
);
}
@@ -45,7 +45,7 @@ export default function (pi: HookAPI) {
`Custom compaction: summarizing ${allMessages.length} messages (${tokensBefore.toLocaleString()} tokens) with ${
model.id
}...`,
"info"
"info",
);
// Convert messages to readable text format
@@ -96,7 +96,7 @@ export default function (pi: HookAPI) {
const response = await complete(
ctx.model!,
{ systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
{ apiKey, signal: loader.signal }
{ apiKey, signal: loader.signal },
);
if (response.stopReason === "aborted") {
+1 -1
View File
@@ -87,7 +87,7 @@ export default function (pi: HookAPI) {
const response = await complete(
ctx.model!,
{ systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
{ apiKey, signal: loader.signal }
{ apiKey, signal: loader.signal },
);
if (response.stopReason === "aborted") {
@@ -27,7 +27,7 @@ if (customModel) {
const available = modelRegistry.getAvailable();
console.log(
"Available models:",
available.map((m) => `${m.provider}/${m.id}`)
available.map((m) => `${m.provider}/${m.id}`),
);
if (available.length > 0) {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@oh-my-pi/pi-coding-agent",
"version": "3.33.0",
"version": "3.34.0",
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
"type": "module",
"ompConfig": {
+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(),
@@ -11,8 +11,8 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { existsSync, mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { getModel } from "@oh-my-pi/pi-ai";
import { Agent } from "@oh-my-pi/pi-agent-core";
import { getModel } from "@oh-my-pi/pi-ai";
import { nanoid } from "nanoid";
import { AgentSession } from "../src/core/agent-session";
import { AuthStorage } from "../src/core/auth-storage";
@@ -11,8 +11,8 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { existsSync, mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { getModel } from "@oh-my-pi/pi-ai";
import { Agent } from "@oh-my-pi/pi-agent-core";
import { getModel } from "@oh-my-pi/pi-ai";
import { nanoid } from "nanoid";
import { AgentSession, type AgentSessionEvent } from "../src/core/agent-session";
import { AuthStorage } from "../src/core/auth-storage";
@@ -201,7 +201,7 @@ describe.skipIf(!API_KEY)("AgentSession compaction e2e", () => {
// Check that no auto_compaction events were emitted for manual compaction
const autoCompactionEvents = events.filter(
(e) => e.type === "auto_compaction_start" || e.type === "auto_compaction_end"
(e) => e.type === "auto_compaction_start" || e.type === "auto_compaction_end",
);
// Manual compaction doesn't emit auto_compaction events
expect(autoCompactionEvents.length).toBe(0);
@@ -6,8 +6,8 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { existsSync, mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { type AssistantMessage, type AssistantMessageEvent, EventStream, getModel } from "@oh-my-pi/pi-ai";
import { Agent } from "@oh-my-pi/pi-agent-core";
import { type AssistantMessage, type AssistantMessageEvent, EventStream, getModel } from "@oh-my-pi/pi-ai";
import { nanoid } from "nanoid";
import { AgentSession } from "../src/core/agent-session";
import { AuthStorage } from "../src/core/auth-storage";
@@ -24,7 +24,7 @@ class MockAssistantStream extends EventStream<AssistantMessageEvent, AssistantMe
if (event.type === "done") return event.message;
if (event.type === "error") return event.error;
throw new Error("Unexpected event type");
}
},
);
}
}
@@ -128,7 +128,7 @@ describe("AgentSession concurrent prompt guard", () => {
// Second prompt should reject
await expect(session.prompt("Second message")).rejects.toThrow(
"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.",
);
// Cleanup
@@ -6,8 +6,8 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { existsSync, mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { getModel } from "@oh-my-pi/pi-ai";
import { Agent } from "@oh-my-pi/pi-agent-core";
import { getModel } from "@oh-my-pi/pi-ai";
import { nanoid } from "nanoid";
import { AgentSession } from "../src/core/agent-session";
import { AuthStorage } from "../src/core/auth-storage";
@@ -49,7 +49,7 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
function createHook(
onBeforeCompact?: (event: SessionBeforeCompactEvent) => { cancel?: boolean; compaction?: any } | undefined,
onCompact?: (event: SessionCompactEvent) => void
onCompact?: (event: SessionCompactEvent) => void,
): LoadedHook {
const handlers = new Map<string, ((event: any, ctx: any) => Promise<any>)[]>();
@@ -154,7 +154,7 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
await session.compact();
const beforeCompactEvents = capturedEvents.filter(
(e): e is SessionBeforeCompactEvent => e.type === "session_before_compact"
(e): e is SessionBeforeCompactEvent => e.type === "session_before_compact",
);
const compactEvents = capturedEvents.filter((e): e is SessionCompactEvent => e.type === "session_compact");
@@ -12,8 +12,8 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { existsSync, mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { getModel, type Model } from "@oh-my-pi/pi-ai";
import { Agent, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import { getModel, type Model } from "@oh-my-pi/pi-ai";
import { nanoid } from "nanoid";
import { AgentSession } from "../src/core/agent-session";
import { AuthStorage } from "../src/core/auth-storage";
@@ -47,7 +47,7 @@ describe.skipIf(!HAS_ANTIGRAVITY_AUTH)("Compaction with thinking models (Antigra
async function createSession(
modelId: "claude-opus-4-5-thinking" | "claude-sonnet-4-5",
thinkingLevel: ThinkingLevel = "high"
thinkingLevel: ThinkingLevel = "high",
) {
const toolSession: ToolSession = {
cwd: tempDir,
@@ -1,9 +1,9 @@
import { beforeEach, describe, expect, it } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
import type { AssistantMessage, Usage } from "@oh-my-pi/pi-ai";
import { getModel } from "@oh-my-pi/pi-ai";
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
import {
type CompactionSettings,
calculateContextTokens,
@@ -216,7 +216,7 @@ describe("findCutPoint", () => {
for (let i = 0; i < 10; i++) {
entries.push(createMessageEntry(createUserMessage(`User ${i}`)));
entries.push(
createMessageEntry(createAssistantMessage(`Assistant ${i}`, createMockUsage(0, 100, (i + 1) * 1000, 0)))
createMessageEntry(createAssistantMessage(`Assistant ${i}`, createMockUsage(0, 100, (i + 1) * 1000, 0))),
);
}
@@ -17,7 +17,7 @@ initTheme("dark");
// Load the real fixture that caused the bug
const fixtureMessage: AssistantMessage = JSON.parse(
readFileSync(join(import.meta.dir, "fixtures/assistant-message-with-thinking-code.json"), "utf-8")
readFileSync(join(import.meta.dir, "fixtures/assistant-message-with-thinking-code.json"), "utf-8"),
);
// Extract thinking and text content
+2 -2
View File
@@ -5,8 +5,8 @@
import { existsSync, mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { getModel } from "@oh-my-pi/pi-ai";
import { Agent } from "@oh-my-pi/pi-agent-core";
import { getModel } from "@oh-my-pi/pi-ai";
import { nanoid } from "nanoid";
import { AgentSession } from "../src/core/agent-session";
import { AuthStorage } from "../src/core/auth-storage";
@@ -145,7 +145,7 @@ export function buildTestTree(
session: SessionManager,
structure: {
messages: Array<{ role: "user" | "assistant"; text: string; branchFrom?: string }>;
}
},
): Map<string, string> {
const ids = new Map<string, string>();
+2
View File
@@ -2,6 +2,8 @@
## [Unreleased]
## [3.34.0] - 2026-01-09
## [3.33.0] - 2026-01-08
## [3.32.0] - 2026-01-08
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@oh-my-pi/pi-git-tool",
"version": "3.33.0",
"version": "3.34.0",
"description": "Structured Git tool with safety guards and typed output",
"type": "module",
"main": "./src/index.ts",
+2
View File
@@ -2,6 +2,8 @@
## [Unreleased]
## [3.34.0] - 2026-01-09
## [3.33.0] - 2026-01-08
## [3.32.0] - 2026-01-08
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@oh-my-pi/pi-tui",
"version": "3.33.0",
"version": "3.34.0",
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
"type": "module",
"main": "src/index.ts",