feat(coding-agent): added streamed tool intent display and hashline edit operations
- Added streamed tool intent display in working message to show real-time intent tracking during agent execution. - Changed intent tracing field name from `$intent` to `_intent` across tool schemas and agent core for consistency. - Added support for file deletion and renaming operations in hashline edit mode. - Renamed hashline edit operation fields: `set` to `target`/`new_content`, `set_range` to `first`/`last`/`new_content`, `insert` to `inserted_lines`.
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added `intentTracing` option to enable intent goal extraction from tool calls, allowing models to specify high-level goals via a `$intent` field that is automatically injected into tool schemas and stripped from arguments before execution
|
||||
- Added `intentTracing` option to enable intent goal extraction from tool calls, allowing models to specify high-level goals via a required `_intent` field that is automatically injected into tool schemas and stripped from arguments before execution
|
||||
|
||||
## [12.11.0] - 2026-02-19
|
||||
|
||||
@@ -29,6 +30,7 @@
|
||||
- Added `preferWebsockets` option to hint that websocket transport should be preferred when supported by the provider implementation
|
||||
|
||||
## [11.10.0] - 2026-02-10
|
||||
|
||||
### Added
|
||||
|
||||
- Added `temperature` option to `AgentOptions` to control LLM sampling temperature
|
||||
@@ -47,6 +49,7 @@
|
||||
- Added `skipInitialSteeringPoll` option to `_runLoop()` for correct queue resume ordering
|
||||
|
||||
## [11.3.0] - 2026-02-06
|
||||
|
||||
### Added
|
||||
|
||||
- Added `maxRetryDelayMs` option to AgentOptions to cap server-requested retry delays, allowing higher-level retry logic to handle long waits with user visibility
|
||||
@@ -56,11 +59,13 @@
|
||||
- Updated ThinkingLevel documentation to include support for gpt-5.3 and gpt-5.3-codex models with 'xhigh' thinking level
|
||||
|
||||
## [11.2.0] - 2026-02-05
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed handling of aborted requests to properly throw abort errors when stream terminates without a terminal event
|
||||
|
||||
## [10.5.0] - 2026-02-04
|
||||
|
||||
### Added
|
||||
|
||||
- Added `concurrency` option to `AgentTool` to control tool scheduling: "shared" (default, runs in parallel) or "exclusive" (runs alone)
|
||||
@@ -73,34 +78,42 @@
|
||||
## [9.2.2] - 2026-01-31
|
||||
|
||||
### Added
|
||||
|
||||
- Added toolChoice option to AgentPromptOptions for controlling tool selection
|
||||
|
||||
## [8.2.0] - 2026-01-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated TypeScript configuration for better publish-time configuration handling with tsconfig.publish.json
|
||||
|
||||
## [8.0.0] - 2026-01-23
|
||||
|
||||
### Added
|
||||
|
||||
- Added `nonAbortable` option to tools to ignore abort signals during execution
|
||||
|
||||
## [6.8.0] - 2026-01-20
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated proxy stream processing to use utility function for reading lines
|
||||
|
||||
## [6.2.0] - 2026-01-19
|
||||
|
||||
### Added
|
||||
|
||||
- Enhanced getToolContext to receive tool call batch information including batchId, index, total count, and tool call details
|
||||
|
||||
## [5.6.7] - 2026-01-18
|
||||
|
||||
### Fixed
|
||||
|
||||
- Added proper tool result messages for tool calls that are aborted or error out
|
||||
- Ensured tool_use/tool_result pairing is maintained when tool execution fails
|
||||
|
||||
## [4.6.0] - 2026-01-12
|
||||
|
||||
### Changed
|
||||
|
||||
- Modified assistant message handling to split messages around tool results for improved readability when using Cursor tools
|
||||
@@ -110,12 +123,14 @@
|
||||
- Fixed tool result ordering in Cursor mode by buffering results and emitting them at the correct position within assistant messages
|
||||
|
||||
## [4.3.0] - 2026-01-11
|
||||
|
||||
### Added
|
||||
|
||||
- Added `cursorExecHandlers` and `cursorOnToolResult` options for local tool execution with cursor-based streaming
|
||||
- Added `emitExternalEvent` method to allow external event injection into the agent state
|
||||
|
||||
## [4.0.0] - 2026-01-10
|
||||
|
||||
### Added
|
||||
|
||||
- Added `popLastSteer()` and `popLastFollowUp()` methods to remove and return the last queued message (LIFO) for dequeue operations
|
||||
@@ -197,7 +212,6 @@ Initial release under @oh-my-pi scope. See previous releases at [badlogic/pi-mon
|
||||
- **Transport abstraction removed**: `ProviderTransport`, `AppTransport`, and `AgentTransport` interface have been removed. Use the `streamFn` option directly for custom streaming implementations.
|
||||
|
||||
- **Agent options renamed**:
|
||||
|
||||
- `transport` → removed (use `streamFn` instead)
|
||||
- `messageTransformer` → `convertToLlm`
|
||||
- `preprocessor` → `transformContext`
|
||||
@@ -226,4 +240,4 @@ Initial release under @oh-my-pi scope. See previous releases at [badlogic/pi-mon
|
||||
|
||||
- `Agent` constructor now has all options optional (empty options use defaults).
|
||||
|
||||
- `queueMessage()` is now synchronous (no longer returns a Promise).
|
||||
- `queueMessage()` is now synchronous (no longer returns a Promise).
|
||||
|
||||
@@ -123,9 +123,9 @@ function normalizeMessagesForProvider(
|
||||
return changed ? normalized : messages;
|
||||
}
|
||||
|
||||
export const INTENT_FIELD = "$intent";
|
||||
export const INTENT_FIELD = "_intent";
|
||||
|
||||
function injectIntentGoalIntoSchema(schema: unknown): unknown {
|
||||
function injectIntentIntoSchema(schema: unknown): unknown {
|
||||
if (!schema || typeof schema !== "object" || Array.isArray(schema)) return schema;
|
||||
const schemaRecord = schema as Record<string, unknown>;
|
||||
const propertiesValue = schemaRecord.properties;
|
||||
@@ -133,38 +133,46 @@ function injectIntentGoalIntoSchema(schema: unknown): unknown {
|
||||
propertiesValue && typeof propertiesValue === "object" && !Array.isArray(propertiesValue)
|
||||
? (propertiesValue as Record<string, unknown>)
|
||||
: {};
|
||||
if (INTENT_FIELD in properties) return schema;
|
||||
const requiredValue = schemaRecord.required;
|
||||
const required = Array.isArray(requiredValue)
|
||||
? requiredValue.filter((item): item is string => typeof item === "string")
|
||||
: [];
|
||||
if (INTENT_FIELD in properties) {
|
||||
if (required.includes(INTENT_FIELD)) return schema;
|
||||
return {
|
||||
...schemaRecord,
|
||||
required: [...required, INTENT_FIELD],
|
||||
};
|
||||
}
|
||||
return {
|
||||
...schemaRecord,
|
||||
properties: {
|
||||
...properties,
|
||||
[INTENT_FIELD]: {
|
||||
type: "string",
|
||||
description: "High-level goal for this tool call.",
|
||||
description:
|
||||
"Describe intent as one sentence in present participle form (e.g., Inserting comment before the function) with no trailing period",
|
||||
},
|
||||
},
|
||||
required: [...required, INTENT_FIELD],
|
||||
};
|
||||
}
|
||||
|
||||
function injectIntentGoalIntoTools(tools: Context["tools"]): Context["tools"] {
|
||||
if (!tools || tools.length === 0) return tools;
|
||||
return tools.map(tool => ({
|
||||
function injectIntentIntoTools(tools: Context["tools"]): Context["tools"] {
|
||||
return tools?.map(tool => ({
|
||||
...tool,
|
||||
parameters: injectIntentGoalIntoSchema(tool.parameters) as typeof tool.parameters,
|
||||
parameters: injectIntentIntoSchema(tool.parameters) as typeof tool.parameters,
|
||||
}));
|
||||
}
|
||||
|
||||
function stripIntentGoalFromArgs(args: Record<string, unknown>): Record<string, unknown> {
|
||||
if (!(INTENT_FIELD in args)) return args;
|
||||
const { [INTENT_FIELD]: _goal, ...rest } = args;
|
||||
return rest;
|
||||
}
|
||||
|
||||
function extractIntentGoal(args: Record<string, unknown>): string | undefined {
|
||||
const goal = args[INTENT_FIELD];
|
||||
if (typeof goal !== "string") return undefined;
|
||||
const trimmed = goal.trim();
|
||||
return trimmed.length > 0 ? trimmed : undefined;
|
||||
function extractIntent(args: Record<string, unknown>): { intent?: string; strippedArgs: Record<string, unknown> } {
|
||||
const intent = args[INTENT_FIELD];
|
||||
if (typeof intent !== "string") {
|
||||
return { strippedArgs: args };
|
||||
}
|
||||
const { [INTENT_FIELD]: _ignored, ...strippedArgs } = args;
|
||||
const trimmed = intent.trim();
|
||||
return { intent: trimmed.length > 0 ? trimmed : undefined, strippedArgs };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -306,7 +314,7 @@ async function streamAssistantResponse(
|
||||
const llmContext: Context = {
|
||||
systemPrompt: context.systemPrompt,
|
||||
messages: normalizedMessages,
|
||||
tools: config.intentTracing ? injectIntentGoalIntoTools(context.tools) : context.tools,
|
||||
tools: config.intentTracing ? injectIntentIntoTools(context.tools) : context.tools,
|
||||
};
|
||||
|
||||
const streamFunction = streamFn || streamSimple;
|
||||
@@ -458,6 +466,7 @@ async function executeToolCalls(
|
||||
const records = toolCalls.map(toolCall => ({
|
||||
toolCall,
|
||||
tool: tools?.find(t => t.name === toolCall.name),
|
||||
args: toolCall.arguments as Record<string, unknown>,
|
||||
started: false,
|
||||
result: undefined as AgentToolResult<any> | undefined,
|
||||
isError: false,
|
||||
@@ -471,25 +480,22 @@ async function executeToolCalls(
|
||||
}
|
||||
|
||||
const { toolCall, tool } = record;
|
||||
if (
|
||||
intentTracing &&
|
||||
toolCall.arguments &&
|
||||
typeof toolCall.arguments === "object" &&
|
||||
!Array.isArray(toolCall.arguments)
|
||||
) {
|
||||
const toolArgs = toolCall.arguments as Record<string, unknown>;
|
||||
const intent = extractIntentGoal(toolArgs);
|
||||
let argsForExecution = toolCall.arguments as Record<string, unknown>;
|
||||
if (intentTracing) {
|
||||
const { intent, strippedArgs } = extractIntent(toolCall.arguments);
|
||||
argsForExecution = strippedArgs;
|
||||
if (intent) {
|
||||
toolCall.intent = intent;
|
||||
}
|
||||
toolCall.arguments = stripIntentGoalFromArgs(toolArgs);
|
||||
}
|
||||
record.args = argsForExecution;
|
||||
record.started = true;
|
||||
stream.push({
|
||||
type: "tool_execution_start",
|
||||
toolCallId: toolCall.id,
|
||||
toolName: toolCall.name,
|
||||
args: toolCall.arguments,
|
||||
args: argsForExecution,
|
||||
intent: toolCall.intent,
|
||||
});
|
||||
|
||||
let result: AgentToolResult<any>;
|
||||
@@ -498,7 +504,7 @@ async function executeToolCalls(
|
||||
try {
|
||||
if (!tool) throw new Error(`Tool ${toolCall.name} not found`);
|
||||
|
||||
const validatedArgs = validateToolArguments(tool, toolCall);
|
||||
const validatedArgs = validateToolArguments(tool, { ...toolCall, arguments: argsForExecution });
|
||||
const toolContext = getToolContext
|
||||
? getToolContext({
|
||||
batchId,
|
||||
@@ -517,7 +523,7 @@ async function executeToolCalls(
|
||||
type: "tool_execution_update",
|
||||
toolCallId: toolCall.id,
|
||||
toolName: toolCall.name,
|
||||
args: toolCall.arguments,
|
||||
args: argsForExecution,
|
||||
partialResult,
|
||||
});
|
||||
},
|
||||
@@ -571,7 +577,8 @@ async function executeToolCalls(
|
||||
type: "tool_execution_start",
|
||||
toolCallId: toolCall.id,
|
||||
toolName: toolCall.name,
|
||||
args: toolCall.arguments,
|
||||
args: record.args,
|
||||
intent: toolCall.intent,
|
||||
});
|
||||
}
|
||||
stream.push({
|
||||
@@ -627,6 +634,7 @@ function createAbortedToolResult(
|
||||
toolCallId: toolCall.id,
|
||||
toolName: toolCall.name,
|
||||
args: toolCall.arguments,
|
||||
intent: toolCall.intent,
|
||||
});
|
||||
stream.push({
|
||||
type: "tool_execution_end",
|
||||
|
||||
@@ -119,8 +119,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
||||
transformToolCallArguments?: (args: Record<string, unknown>, toolName: string) => Record<string, unknown>;
|
||||
/**
|
||||
* Enable intent tracing for tool calls.
|
||||
* When enabled, the harness injects a `$intent: string` field into tool schemas sent to the model,
|
||||
* then strips `$intent` from arguments before executing tools.
|
||||
* When enabled, the harness injects a `_intent: string` field into tool schemas sent to the model,
|
||||
* then strips `_intent` from arguments before executing tools.
|
||||
*/
|
||||
intentTracing?: boolean;
|
||||
}
|
||||
@@ -271,6 +271,6 @@ export type AgentEvent =
|
||||
| { type: "message_update"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent }
|
||||
| { type: "message_end"; message: AgentMessage }
|
||||
// Tool execution lifecycle
|
||||
| { type: "tool_execution_start"; toolCallId: string; toolName: string; args: any }
|
||||
| { type: "tool_execution_start"; toolCallId: string; toolName: string; args: any; intent?: string }
|
||||
| { type: "tool_execution_update"; toolCallId: string; toolName: string; args: any; partialResult: any }
|
||||
| { type: "tool_execution_end"; toolCallId: string; toolName: string; result: any; isError?: boolean };
|
||||
|
||||
@@ -444,6 +444,7 @@ describe("agentLoop with AgentMessage", () => {
|
||||
value: { type: "string" },
|
||||
[INTENT_FIELD]: { type: "string" },
|
||||
});
|
||||
expect(firstRequestToolSchema?.required).toEqual(expect.arrayContaining([INTENT_FIELD]));
|
||||
expect(executedParams).toEqual([{ value: "hello" }]);
|
||||
expect(tracedToolCall?.type).toBe("toolCall");
|
||||
if (tracedToolCall?.type === "toolCall") {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added `tools.intentTracing` setting to enable intent tracing, which injects `$intent` into all tool schemas and strips it before tool execution
|
||||
- Display streamed tool intent in working message during agent execution
|
||||
- Added `tools.intentTracing` setting to enable intent tracing, which asks the agent to describe the intent of each tool call before executing it
|
||||
- Support for file deletion in hashline edit mode via `delete: true` parameter
|
||||
- Support for file renaming/moving in hashline edit mode via `rename` parameter
|
||||
- Optional content-replace edit variant in hashline mode (enabled via `PI_HL_REPLACETXT=1` environment variable)
|
||||
@@ -11,6 +13,7 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- Intent tracing now uses `_intent` field name in tool schemas
|
||||
- Hashline edit API: renamed `set` operation to `target`/`new_content` for clearer semantics
|
||||
- Hashline edit API: renamed `set_range` operation to `first`/`last`/`new_content`
|
||||
- Hashline edit API: renamed `insert` operation fields from `body` to `inserted_lines` and made `inserted_lines` required non-empty
|
||||
@@ -20,8 +23,10 @@
|
||||
### Fixed
|
||||
|
||||
- Grep tool now properly handles internal URL resolution when searching artifact paths
|
||||
- Working message intent updates now fall back to tool execution events when streamed tool arguments omit the intent field
|
||||
|
||||
## [12.13.0] - 2026-02-19
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Removed automatic line relocation when hash references become stale; edits with mismatched line hashes now fail with an error instead of silently relocating to matching lines elsewhere in the file
|
||||
@@ -190,6 +195,7 @@
|
||||
- Refactored session directory naming to use single-dash format for home-relative paths and double-dash format for absolute paths, with automatic migration of legacy session directories on first access
|
||||
|
||||
## [12.8.2] - 2026-02-17
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed system environment context to use built-in `os` values for distro, kernel, and CPU model instead of native system-info data
|
||||
@@ -202,11 +208,14 @@
|
||||
## [12.8.0] - 2026-02-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved `/changelog` performance by displaying only the most recent 3 versions by default, with a `--full` flag for the complete history ([#85](https://github.com/can1357/oh-my-pi/pull/85) by [@tctev](https://github.com/tctev))
|
||||
- Centralized builtin slash command definitions and handlers into a shared registry, replacing the large input-controller if-chain dispatch
|
||||
|
||||
## [12.7.0] - 2026-02-16
|
||||
|
||||
### Added
|
||||
|
||||
- Added abort signal support to LSP file operations (`ensureFileOpen`, `refreshFile`) for cancellable file synchronization
|
||||
- Added abort signal propagation through LSP request handlers (definition, references, hover, symbols, rename) enabling operation cancellation
|
||||
- Added `shouldBypassAutocompleteOnEscape` callback to custom editor for context-aware escape key handling during active operations
|
||||
@@ -221,7 +230,9 @@
|
||||
- Added secret obfuscation: env vars matching secret patterns and `secrets.json` entries are replaced with placeholders before sending to LLM providers, deobfuscated in tool call arguments
|
||||
- Added `secrets.enabled` setting to toggle secret obfuscation
|
||||
- Added full regex literal support for `secrets.json` entries (`"/pattern/flags"` syntax with escaped `/` handling, automatic `g` flag enforcement)
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed context promotion to trigger on context overflow instead of a configurable threshold, promoting to a larger model before attempting compaction
|
||||
- Changed context promotion behavior to retry immediately on the promoted model without compacting, providing faster recovery from context limits
|
||||
- Changed default grep context lines from 1 before/3 after to 0 before/0 after for more focused search results
|
||||
@@ -236,20 +247,25 @@
|
||||
- Updated web search provider priority order to include Brave (Exa → Brave → Jina → Perplexity → Anthropic → Gemini → Codex → Z.AI)
|
||||
- Extended recency filter support to Brave provider alongside Perplexity
|
||||
- Changed GitHub issue comment fetching to use paginated API requests with 100 comments per page instead of single request with 50-comment limit
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed `contextPromotion.thresholdPercent` setting as context promotion now triggers only on overflow
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed LSP operations to properly respect abort signals and throw `ToolAbortError` when cancelled
|
||||
- Fixed workspace diagnostics process cleanup to remove abort event listeners in finally block
|
||||
- Fixed PTY-backed bash execution to enforce timeout completion when detached child processes keep the PTY stream open ([#88](https://github.com/can1357/oh-my-pi/issues/88))
|
||||
|
||||
## [12.5.1] - 2026-02-15
|
||||
|
||||
### Added
|
||||
|
||||
- Added `repeatToolDescriptions` setting to render full tool descriptions in the system prompt instead of a tool name list
|
||||
|
||||
## [12.5.0] - 2026-02-15
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Replaced `theme` setting with `theme.dark` and `theme.light` (auto-migrated)
|
||||
@@ -300,6 +316,7 @@
|
||||
- Sanitized debug log display to strip control codes, normalize tabs, and trim width
|
||||
|
||||
## [12.4.0] - 2026-02-14
|
||||
|
||||
### Changed
|
||||
|
||||
- Moved `sanitizeText` function from `@oh-my-pi/pi-utils` to `@oh-my-pi/pi-natives` for better code organization
|
||||
@@ -315,6 +332,7 @@
|
||||
- Fixed Cloudflare returning corrupted bytes when compression is negotiated in web scraper requests
|
||||
|
||||
## [12.3.0] - 2026-02-14
|
||||
|
||||
### Added
|
||||
|
||||
- Added autonomous memory extraction and consolidation system with configurable settings
|
||||
@@ -360,6 +378,7 @@
|
||||
- Fixed fetch tool to preserve actual response metadata (finalUrl, contentType) instead of defaults when requests fail
|
||||
|
||||
||||||| parent of a70a34c8b (fix(coding-agent/debug): Sanitized debug log rendering)
|
||||
|
||||
## [12.1.0] - 2026-02-13
|
||||
|
||||
### Added
|
||||
@@ -411,6 +430,7 @@
|
||||
- Removed @types/jsdom dependency
|
||||
|
||||
## [11.14.1] - 2026-02-12
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved Bun binary detection to check `Bun.env.PI_COMPILED` environment variable
|
||||
@@ -422,6 +442,7 @@
|
||||
- Fixed Bun update process to properly handle version pinning and report installation mismatches
|
||||
|
||||
## [11.14.0] - 2026-02-12
|
||||
|
||||
### Added
|
||||
|
||||
- Added SwiftLint linter client with JSON reporter support for Swift file linting
|
||||
@@ -474,6 +495,7 @@
|
||||
- Refactored browser/file opening across multiple modules to use unified `openPath` utility for improved maintainability
|
||||
|
||||
## [11.12.0] - 2026-02-11
|
||||
|
||||
### Added
|
||||
|
||||
- Added `resolveFileDisplayMode` utility to centralize file display mode resolution across tools (read, grep, file mentions)
|
||||
@@ -562,6 +584,7 @@
|
||||
- Refactored hash line formatting to use async `streamHashLinesFromLines` for better performance
|
||||
|
||||
## [11.10.3] - 2026-02-10
|
||||
|
||||
### Added
|
||||
|
||||
- Exported `./patch/*` subpath for direct access to patch utilities
|
||||
@@ -587,6 +610,7 @@
|
||||
- Removed AggregateError unwrapping from console.warn in CLI initialization
|
||||
|
||||
## [11.10.1] - 2026-02-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Migrated CLI framework from oclif to lightweight pi-utils CLI runner
|
||||
@@ -601,6 +625,7 @@
|
||||
- Removed custom oclif help renderer (oclif-help.ts)
|
||||
|
||||
## [11.10.0] - 2026-02-10
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Changed `HashlineEdit.src` from string format (e.g., `"5:ab"`, `"5:ab..9:ef"`) to structured `SrcSpec` object with discriminated union types (`{ kind: "single", ref: "..." }`, `{ kind: "range", start: "...", end: "..." }`, etc.)
|
||||
@@ -751,6 +776,7 @@
|
||||
- Improved bash tool output draining after foreground completion to reduce tail output truncation
|
||||
|
||||
## [11.8.0] - 2026-02-10
|
||||
|
||||
### Added
|
||||
|
||||
- Added `ctx.reload()` method to extension command context to reload extensions, skills, prompts, and themes from disk
|
||||
@@ -773,6 +799,7 @@
|
||||
- Fixed archive extraction error handling to provide clear error messages on failure
|
||||
|
||||
## [11.7.0] - 2026-02-07
|
||||
|
||||
### Changed
|
||||
|
||||
- Enhanced error messages for failed Python cells to include full combined output context instead of just the error message
|
||||
@@ -784,6 +811,7 @@
|
||||
- Fixed tab character rendering in Python tool output display to properly format whitespace in cell output and status events
|
||||
|
||||
## [11.6.1] - 2026-02-07
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed potential crash when rendering results with undefined details.results
|
||||
@@ -832,6 +860,7 @@
|
||||
- Removed ability to save screenshots to custom paths or artifacts directory
|
||||
|
||||
## [11.4.1] - 2026-02-06
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed tab character display in error messages and bash tool output by properly replacing tabs with spaces
|
||||
@@ -4805,4 +4834,4 @@ Initial public release.
|
||||
- Git branch display in footer
|
||||
- Message queueing during streaming responses
|
||||
- OAuth integration for Gmail and Google Calendar access
|
||||
- HTML export with syntax highlighting and collapsible sections
|
||||
- HTML export with syntax highlighting and collapsible sections
|
||||
|
||||
@@ -508,7 +508,7 @@ export const SETTINGS_SCHEMA = {
|
||||
ui: {
|
||||
tab: "tools",
|
||||
label: "Intent tracing",
|
||||
description: "Inject `$intent` into all tool schemas and strip it before tool execution",
|
||||
description: "Ask the agent to describe the intent of each tool call before executing it",
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -503,6 +503,7 @@ export interface ToolExecutionStartEvent {
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: unknown;
|
||||
intent?: string;
|
||||
}
|
||||
|
||||
/** Fired during tool execution with partial/streaming output */
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
|
||||
import { Loader, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
||||
import { settings } from "../../config/settings";
|
||||
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
||||
@@ -14,6 +15,7 @@ export class EventController {
|
||||
#lastReadGroup: ReadToolGroupComponent | undefined = undefined;
|
||||
#lastThinkingCount = 0;
|
||||
#renderedCustomMessages = new Set<string>();
|
||||
#lastIntent: string | undefined = undefined;
|
||||
|
||||
constructor(private ctx: InteractiveModeContext) {}
|
||||
|
||||
@@ -32,6 +34,13 @@ export class EventController {
|
||||
return this.#lastReadGroup;
|
||||
}
|
||||
|
||||
#updateWorkingMessageFromIntent(intent: string | undefined): void {
|
||||
const trimmed = intent?.trim();
|
||||
if (!trimmed || trimmed === this.#lastIntent) return;
|
||||
this.#lastIntent = trimmed;
|
||||
this.ctx.setWorkingMessage(`${trimmed} (esc to interrupt)`);
|
||||
}
|
||||
|
||||
subscribeToAgent(): void {
|
||||
this.ctx.unsubscribe = this.ctx.session.subscribe(async (event: AgentSessionEvent) => {
|
||||
await this.handleEvent(event);
|
||||
@@ -48,6 +57,7 @@ export class EventController {
|
||||
|
||||
switch (event.type) {
|
||||
case "agent_start":
|
||||
this.#lastIntent = undefined;
|
||||
if (this.ctx.retryEscapeHandler) {
|
||||
this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
|
||||
this.ctx.retryEscapeHandler = undefined;
|
||||
@@ -155,6 +165,15 @@ export class EventController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update working message with intent from streamed tool arguments
|
||||
for (const content of this.ctx.streamingMessage.content) {
|
||||
if (content.type !== "toolCall") continue;
|
||||
const args = content.arguments;
|
||||
if (!args || typeof args !== "object" || !(INTENT_FIELD in args)) continue;
|
||||
this.#updateWorkingMessageFromIntent(args[INTENT_FIELD] as string | undefined);
|
||||
}
|
||||
|
||||
this.ctx.ui.requestRender();
|
||||
}
|
||||
break;
|
||||
@@ -196,6 +215,7 @@ export class EventController {
|
||||
break;
|
||||
|
||||
case "tool_execution_start": {
|
||||
this.#updateWorkingMessageFromIntent(event.intent);
|
||||
if (!this.ctx.pendingTools.has(event.toolCallId)) {
|
||||
if (event.toolName === "read") {
|
||||
const group = this.#getReadGroup();
|
||||
|
||||
@@ -140,8 +140,8 @@ export type HashlineEdit =
|
||||
const hashlineContentFormat = (kind: string) =>
|
||||
Type.Union([
|
||||
Type.Null(),
|
||||
Type.Array(Type.String(), { minItems: 1, description: `${kind} lines` }),
|
||||
Type.String({ minLength: 1, description: `${kind} text, \\n delimited if multiple lines` }),
|
||||
Type.Array(Type.String(), { description: `${kind} lines` }),
|
||||
Type.String({ description: `${kind} line` }),
|
||||
]);
|
||||
|
||||
function hashlineParseContent(edit: string | string[] | null): string[] {
|
||||
@@ -153,6 +153,12 @@ function hashlineParseContent(edit: string | string[] | null): string[] {
|
||||
return lines;
|
||||
}
|
||||
|
||||
function hashlineParseContentString(edit: string | string[] | null): string {
|
||||
if (edit === null) return "";
|
||||
if (Array.isArray(edit)) return edit.join("\n");
|
||||
return edit;
|
||||
}
|
||||
|
||||
const hashlineTargetEditSchema = Type.Object({
|
||||
target: Type.String({ description: 'Line reference "LINE#ID"' }),
|
||||
new_content: hashlineContentFormat("Replacement"),
|
||||
@@ -190,18 +196,19 @@ function hashlineParseRangeEdit(edit: HashlineEditUnion): HashlineEdit | null {
|
||||
}
|
||||
|
||||
const hashlineInsertEditSchema = Type.Object({
|
||||
before: Type.Optional(Type.String({ minLength: 1, description: 'Insert before this line "LINE#ID"' })),
|
||||
after: Type.Optional(Type.String({ minLength: 1, description: 'Insert after this line "LINE#ID"' })),
|
||||
before: Type.Optional(Type.String({ description: 'Insert before this line "LINE#ID"' })),
|
||||
after: Type.Optional(Type.String({ description: 'Insert after this line "LINE#ID"' })),
|
||||
inserted_lines: hashlineContentFormat("Inserted"),
|
||||
});
|
||||
|
||||
function hashlineParseInsertEdit(edit: HashlineEditUnion): HashlineEdit | null {
|
||||
if ("inserted_lines" in edit) {
|
||||
const { before = "", after = "", inserted_lines } = edit;
|
||||
return {
|
||||
insert: {
|
||||
before: edit.before,
|
||||
after: edit.after,
|
||||
body: hashlineParseContent(edit.inserted_lines),
|
||||
before: before.length > 0 ? before : undefined,
|
||||
after: after.length > 0 ? after : undefined,
|
||||
body: hashlineParseContent(inserted_lines),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -216,11 +223,12 @@ const hashlineReplaceTextEditSchema = Type.Object({
|
||||
|
||||
function hashlineParseReplaceTextEdit(edit: HashlineEditUnion): HashlineReplaceEdit | null {
|
||||
if ("old_text" in edit && "new_text" in edit) {
|
||||
const { old_text, new_text, all } = edit;
|
||||
return {
|
||||
replace: {
|
||||
old_text: edit.old_text,
|
||||
new_text: hashlineParseContent(edit.new_text).join("\n"),
|
||||
all: edit.all,
|
||||
old_text: old_text,
|
||||
new_text: hashlineParseContentString(new_text),
|
||||
all: all,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -239,24 +247,18 @@ const hashlineEditItemSchemaWithReplace = Type.Union([
|
||||
hashlineReplaceTextEditSchema,
|
||||
]);
|
||||
|
||||
const hashlineEditSchema = Type.Object(
|
||||
{
|
||||
path: Type.String({ description: "File path (relative or absolute)" }),
|
||||
edits: Type.Array(hashlineEditItemSchema, { description: "Array of edit operations" }),
|
||||
delete: Type.Optional(Type.Literal(true, { description: "Delete the file" })),
|
||||
rename: Type.Optional(Type.String({ description: "New path for move" })),
|
||||
},
|
||||
{ additionalProperties: true },
|
||||
);
|
||||
const hashlineEditSchemaWithReplace = Type.Object(
|
||||
{
|
||||
path: Type.String({ description: "File path (relative or absolute)" }),
|
||||
edits: Type.Array(hashlineEditItemSchemaWithReplace, { description: "Array of edit operations" }),
|
||||
delete: Type.Optional(Type.Literal(true, { description: "Delete the file" })),
|
||||
rename: Type.Optional(Type.String({ description: "New path for move" })),
|
||||
},
|
||||
{ additionalProperties: true },
|
||||
);
|
||||
const hashlineEditSchema = Type.Object({
|
||||
path: Type.String({ description: "File path (relative or absolute)" }),
|
||||
edits: Type.Array(hashlineEditItemSchema, { description: "Array of edit operations" }),
|
||||
delete: Type.Optional(Type.Literal(true, { description: "Delete the file" })),
|
||||
rename: Type.Optional(Type.String({ description: "New path for move" })),
|
||||
});
|
||||
const hashlineEditSchemaWithReplace = Type.Object({
|
||||
path: Type.String({ description: "File path (relative or absolute)" }),
|
||||
edits: Type.Array(hashlineEditItemSchemaWithReplace, { description: "Array of edit operations" }),
|
||||
delete: Type.Optional(Type.Literal(true, { description: "Delete the file" })),
|
||||
rename: Type.Optional(Type.String({ description: "New path for move" })),
|
||||
});
|
||||
|
||||
export type HashlineToolEdit = Static<typeof hashlineEditItemSchemaWithReplace>;
|
||||
export type HashlineParams = Static<typeof hashlineEditSchemaWithReplace>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Edit (Hash Anchored)
|
||||
# Edit
|
||||
|
||||
Apply precise file edits using `LINE#ID` anchors from `read` output.
|
||||
**CRITICAL:** anchors are `LINE#ID` only. Copy verbatim from the prefix (example: `{{hlineref 42 "const x = 1"}}`). Never include `|content`.
|
||||
@@ -15,10 +15,10 @@ Apply precise file edits using `LINE#ID` anchors from `read` output.
|
||||
<operations>
|
||||
- **Single line replace/delete**
|
||||
- `{ target: "LINE#ID", new_content: ["..."] }`
|
||||
- `new_content: []` deletes the line; `new_content: [""]` keeps a blank line.
|
||||
- `new_content: null` deletes the line; `new_content: [""]` keeps a blank line.
|
||||
- **Range replace/delete**
|
||||
- `{ first: "LINE#ID", last: "LINE#ID", new_content: ["..."] }`
|
||||
- Use for swaps, block rewrites, or deleting a full span (`new_content: []`).
|
||||
- Use for swaps, block rewrites, or deleting a full span (`new_content: null`).
|
||||
- **Insert** (new content)
|
||||
- `{ before: "LINE#ID", inserted_lines: ["..."] }`
|
||||
- `{ after: "LINE#ID", inserted_lines: ["..."] }`
|
||||
@@ -67,83 +67,154 @@ Apply precise file edits using `LINE#ID` anchors from `read` output.
|
||||
- After two no-ops on same area, re-read the full function/block before retry.
|
||||
</recovery>
|
||||
|
||||
<example name="single-line token fix (set)">
|
||||
<example name="single line replace — fix a value or type">
|
||||
```ts
|
||||
{{hlinefull 41 " return record != null && record.status === 'fulfilled';"}}
|
||||
{{hlinefull 23 " const timeout: number = 5000;"}}
|
||||
```
|
||||
```json
|
||||
{ target: "{{hlineref 41 " return record != null && record.status === 'fulfilled';"}}", new_content: [" return record != null && record?.status === 'fulfilled';"] }
|
||||
```
|
||||
target: "{{hlineref 23 " const timeout: number = 5000;"}}"
|
||||
new_content: [" const timeout: number = 30_000;"]
|
||||
```
|
||||
</example>
|
||||
|
||||
<example name="restore missing declaration (insert before)">
|
||||
<example name="single line delete — remove a line entirely">
|
||||
```ts
|
||||
{{hlinefull 15 "export function useX(...): boolean {"}}
|
||||
{{hlinefull 16 " useEffect(() => {"}}
|
||||
{{hlinefull 7 "// @ts-ignore"}}
|
||||
{{hlinefull 8 "const data = fetchSync(url);"}}
|
||||
```
|
||||
```json
|
||||
{ before: "{{hlineref 16 " useEffect(() => {"}}", inserted_lines: [" const [isVisible, setIsVisible] = useState(true);"] }
|
||||
```
|
||||
target: "{{hlineref 7 "// @ts-ignore"}}"
|
||||
new_content: null
|
||||
```
|
||||
</example>
|
||||
|
||||
<example name="insert between siblings (after+before)">
|
||||
<example name="single line blank — clear content but keep the line break">
|
||||
```ts
|
||||
{{hlinefull 120 " doFirst();"}}
|
||||
{{hlinefull 121 " doThird();"}}
|
||||
{{hlinefull 14 " placeholder: \"DO NOT SHIP\","}}
|
||||
```
|
||||
```json
|
||||
{ after: "{{hlineref 120 " doFirst();"}}", before: "{{hlineref 121 " doThird();"}}", inserted_lines: [" doSecond();"] }
|
||||
```
|
||||
target: "{{hlineref 14 " placeholder: \"DO NOT SHIP\","}}"
|
||||
new_content: [""]
|
||||
```
|
||||
</example>
|
||||
|
||||
<example name="swap adjacent lines atomically (set_range)">
|
||||
<example name="range replace — rewrite a block of logic">
|
||||
```ts
|
||||
{{hlinefull 190 " thenable.then(resolve, ignoreReject);"}}
|
||||
{{hlinefull 191 " chunkCache.set(chunkId, thenable);"}}
|
||||
{{hlinefull 60 " } catch (err) {"}}
|
||||
{{hlinefull 61 " console.error(err);"}}
|
||||
{{hlinefull 62 " return null;"}}
|
||||
{{hlinefull 63 " }"}}
|
||||
```
|
||||
```json
|
||||
{ first: "{{hlineref 190 " thenable.then(resolve, ignoreReject);"}}", last: "{{hlineref 191 " chunkCache.set(chunkId, thenable);"}}", new_content: [" chunkCache.set(chunkId, thenable);", " thenable.then(resolve, ignoreReject);"] }
|
||||
```
|
||||
first: "{{hlineref 60 " } catch (err) {"}}"
|
||||
last: "{{hlineref 63 " }"}}"
|
||||
new_content: [" } catch (err) {", " if (isEnoent(err)) return null;", " throw err;", " }"]
|
||||
```
|
||||
</example>
|
||||
|
||||
<example name="insert guard before comment">
|
||||
<example name="range delete — remove a full block">
|
||||
```ts
|
||||
{{hlinefull 188 ""}}
|
||||
{{hlinefull 189 " // If we don't find a Fiber on the comment..."}}
|
||||
{{hlinefull 80 " // TODO: remove after migration"}}
|
||||
{{hlinefull 81 " if (legacy) {"}}
|
||||
{{hlinefull 82 " legacyHandler(req);"}}
|
||||
{{hlinefull 83 " }"}}
|
||||
```
|
||||
```json
|
||||
{ after: "{{hlineref 188 ""}}", inserted_lines: [" if (targetFiber) {", " targetInst = targetFiber;", " }"] }
|
||||
```
|
||||
first: "{{hlineref 80 " // TODO: remove after migration"}}"
|
||||
last: "{{hlineref 83 " }"}}"
|
||||
new_content: null
|
||||
```
|
||||
</example>
|
||||
|
||||
<example name="anti-pattern: interior anchor vs boundary anchor">
|
||||
Bad:
|
||||
```json
|
||||
{ after: "195#d3", inserted_lines: [" { id: \"nanogpt\", available: true },"] }
|
||||
<example name="insert with before — add an import above the first import">
|
||||
```ts
|
||||
{{hlinefull 1 "import * as fs from \"node:fs/promises\";"}}
|
||||
{{hlinefull 2 "import * as path from \"node:path\";"}}
|
||||
```
|
||||
Good:
|
||||
```json
|
||||
{ after: "196#f6", before: "197#fc", inserted_lines: [" { id: \"nanogpt\", available: true },"] }
|
||||
```
|
||||
before: "{{hlineref 1 "import * as fs from \"node:fs/promises\";"}}"
|
||||
inserted_lines: ["import * as os from \"node:os\";"]
|
||||
```
|
||||
Use `before` when prepending at the top of a block or file — there is no meaningful anchor above.
|
||||
</example>
|
||||
|
||||
<example name="explicit EOF append">
|
||||
<example name="insert with after — append at end of file">
|
||||
```ts
|
||||
{{hlinefull 260 "// last existing line"}}
|
||||
{{hlinefull 260 "export { serialize, deserialize };"}}
|
||||
```
|
||||
```json
|
||||
{ after: "{{hlineref 260 "// last existing line"}}", inserted_lines: ["// end marker"] }
|
||||
```
|
||||
after: "{{hlineref 260 "export { serialize, deserialize };"}}"
|
||||
inserted_lines: ["export { validate };"]
|
||||
```
|
||||
Use `after` when appending at the bottom — there is no anchor below.
|
||||
</example>
|
||||
|
||||
<example name="insert with after + before (dual anchor) — add an entry between known siblings">
|
||||
```ts
|
||||
{{hlinefull 44 " \"build\": \"bun run compile\","}}
|
||||
{{hlinefull 45 " \"test\": \"bun test\""}}
|
||||
```
|
||||
```
|
||||
after: "{{hlineref 44 " \"build\": \"bun run compile\","}}"
|
||||
before: "{{hlineref 45 " \"test\": \"bun test\""}}"
|
||||
inserted_lines: [" \"lint\": \"biome check\","]
|
||||
```
|
||||
Dual anchors pin the insert to exactly one gap, preventing drift from edits elsewhere in the file. **Always prefer dual anchors when both boundaries are content lines.**
|
||||
</example>
|
||||
|
||||
<example name="insert a function before another function — anchor to the target, not whitespace">
|
||||
```ts
|
||||
{{hlinefull 100 " return buf.toString(\"hex\");"}}
|
||||
{{hlinefull 101 "}"}}
|
||||
{{hlinefull 102 ""}}
|
||||
{{hlinefull 103 "export function serialize(data: unknown): string {"}}
|
||||
```
|
||||
```
|
||||
before: "{{hlineref 103 "export function serialize(data: unknown): string {"}}"
|
||||
inserted_lines: ["function validate(data: unknown): boolean {", " return data != null && typeof data === \"object\";", "}", ""]
|
||||
```
|
||||
The trailing `""` in `inserted_lines` preserves the blank-line separator. **Anchor to the structural line (`export function ...`), not the blank line above it** — blank lines are ambiguous and may be added or removed by other edits.
|
||||
</example>
|
||||
|
||||
{{#if allowReplaceText}}
|
||||
<example name="replace fallback only">
|
||||
```json
|
||||
{ old_text: "x = 42", new_text: "x = 99" }
|
||||
<example name="content replace (fallback) — when anchors are unavailable">
|
||||
```
|
||||
old_text: "x = 42"
|
||||
new_text: "x = 99"
|
||||
```
|
||||
Use only when line anchors aren't available. `old_text` must match exactly one location in the file (or set `"all": true` for all occurrences).
|
||||
</example>
|
||||
{{/if}}
|
||||
|
||||
<example name="file delete">
|
||||
```
|
||||
path: "src/deprecated/legacy.ts"
|
||||
delete: true
|
||||
```
|
||||
</example>
|
||||
|
||||
<example name="file rename with edits — move and modify in one atomic call">
|
||||
```
|
||||
path: "src/utils.ts"
|
||||
rename: "src/helpers/utils.ts"
|
||||
edits: [..]
|
||||
```
|
||||
</example>
|
||||
|
||||
<example name="anti-pattern: anchoring to whitespace">
|
||||
Bad — anchors to a blank line; fragile if blank lines shift:
|
||||
```
|
||||
after: "{{hlineref 102 ""}}"
|
||||
inserted_lines: ["function validate() { ... }"]
|
||||
```
|
||||
|
||||
Good — anchors to the structural target:
|
||||
```
|
||||
before: "{{hlineref 103 "export function serialize(data: unknown): string {"}}"
|
||||
inserted_lines: ["function validate() { ... }", ""]
|
||||
```
|
||||
</example>
|
||||
|
||||
<validation>
|
||||
- [ ] Payload shape is `{ "path": string, "edits": [operation, ...], "delete"?: true, "rename"?: string }`
|
||||
- [ ] Every operation matches exactly one variant
|
||||
|
||||
@@ -1140,6 +1140,7 @@ export class AgentSession {
|
||||
toolCallId: event.toolCallId,
|
||||
toolName: event.toolName,
|
||||
args: event.args,
|
||||
intent: event.intent,
|
||||
};
|
||||
await this.#extensionRunner.emit(extensionEvent);
|
||||
} else if (event.type === "tool_execution_update") {
|
||||
|
||||
Reference in New Issue
Block a user