Merge branch 'main' into feat/cosmetic-fixes
This commit is contained in:
@@ -504,7 +504,7 @@ export const SETTINGS_SCHEMA = {
|
||||
},
|
||||
"tools.intentTracing": {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
default: true,
|
||||
ui: {
|
||||
tab: "tools",
|
||||
label: "Intent tracing",
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
// Auto-generated by scripts/generate-docs-index.ts - DO NOT EDIT
|
||||
|
||||
import bashToolRuntimeMd from "../../../../docs/bash-tool-runtime.md" with { type: "text" };
|
||||
import blobArtifactArchitectureMd from "../../../../docs/blob-artifact-architecture.md" with { type: "text" };
|
||||
import compactionMd from "../../../../docs/compaction.md" with { type: "text" };
|
||||
import configUsageMd from "../../../../docs/config-usage.md" with { type: "text" };
|
||||
import customToolsMd from "../../../../docs/custom-tools.md" with { type: "text" };
|
||||
import environmentVariablesMd from "../../../../docs/environment-variables.md" with { type: "text" };
|
||||
import extensionLoadingMd from "../../../../docs/extension-loading.md" with { type: "text" };
|
||||
import extensionsMd from "../../../../docs/extensions.md" with { type: "text" };
|
||||
import fsScanCacheArchitectureMd from "../../../../docs/fs-scan-cache-architecture.md" with { type: "text" };
|
||||
import geminiManifestExtensionsMd from "../../../../docs/gemini-manifest-extensions.md" with { type: "text" };
|
||||
import handoffGenerationPipelineMd from "../../../../docs/handoff-generation-pipeline.md" with { type: "text" };
|
||||
import hooksMd from "../../../../docs/hooks.md" with { type: "text" };
|
||||
import mcpProtocolTransportsMd from "../../../../docs/mcp-protocol-transports.md" with { type: "text" };
|
||||
import mcpRuntimeLifecycleMd from "../../../../docs/mcp-runtime-lifecycle.md" with { type: "text" };
|
||||
import mcpServerToolAuthoringMd from "../../../../docs/mcp-server-tool-authoring.md" with { type: "text" };
|
||||
import modelsMd from "../../../../docs/models.md" with { type: "text" };
|
||||
import nativesAddonLoaderRuntimeMd from "../../../../docs/natives-addon-loader-runtime.md" with { type: "text" };
|
||||
import nativesArchitectureMd from "../../../../docs/natives-architecture.md" with { type: "text" };
|
||||
import nativesBindingContractMd from "../../../../docs/natives-binding-contract.md" with { type: "text" };
|
||||
import nativesBuildReleaseDebuggingMd from "../../../../docs/natives-build-release-debugging.md" with { type: "text" };
|
||||
import nativesMediaSystemUtilsMd from "../../../../docs/natives-media-system-utils.md" with { type: "text" };
|
||||
import nativesRustTaskCancellationMd from "../../../../docs/natives-rust-task-cancellation.md" with { type: "text" };
|
||||
import nativesShellPtyProcessMd from "../../../../docs/natives-shell-pty-process.md" with { type: "text" };
|
||||
import nativesTextSearchPipelineMd from "../../../../docs/natives-text-search-pipeline.md" with { type: "text" };
|
||||
import nonCompactionRetryPolicyMd from "../../../../docs/non-compaction-retry-policy.md" with { type: "text" };
|
||||
import notebookToolRuntimeMd from "../../../../docs/notebook-tool-runtime.md" with { type: "text" };
|
||||
import pluginManagerInstallerPlumbingMd from "../../../../docs/plugin-manager-installer-plumbing.md" with { type: "text" };
|
||||
import portingFromPiMonoMd from "../../../../docs/porting-from-pi-mono.md" with { type: "text" };
|
||||
import portingToNativesMd from "../../../../docs/porting-to-natives.md" with { type: "text" };
|
||||
import providerStreamingInternalsMd from "../../../../docs/provider-streaming-internals.md" with { type: "text" };
|
||||
import pythonReplMd from "../../../../docs/python-repl.md" with { type: "text" };
|
||||
import rpcMd from "../../../../docs/rpc.md" with { type: "text" };
|
||||
import rulebookMatchingPipelineMd from "../../../../docs/rulebook-matching-pipeline.md" with { type: "text" };
|
||||
import sdkMd from "../../../../docs/sdk.md" with { type: "text" };
|
||||
import secretsMd from "../../../../docs/secrets.md" with { type: "text" };
|
||||
import sessionMd from "../../../../docs/session.md" with { type: "text" };
|
||||
import sessionOperationsExportShareForkResumeMd from "../../../../docs/session-operations-export-share-fork-resume.md" with { type: "text" };
|
||||
import sessionSwitchingAndRecentListingMd from "../../../../docs/session-switching-and-recent-listing.md" with { type: "text" };
|
||||
import sessionTreePlanMd from "../../../../docs/session-tree-plan.md" with { type: "text" };
|
||||
import skillsMd from "../../../../docs/skills.md" with { type: "text" };
|
||||
import slashCommandInternalsMd from "../../../../docs/slash-command-internals.md" with { type: "text" };
|
||||
import taskAgentDiscoveryMd from "../../../../docs/task-agent-discovery.md" with { type: "text" };
|
||||
import themeMd from "../../../../docs/theme.md" with { type: "text" };
|
||||
import treeMd from "../../../../docs/tree.md" with { type: "text" };
|
||||
import ttsrInjectionLifecycleMd from "../../../../docs/ttsr-injection-lifecycle.md" with { type: "text" };
|
||||
import tuiMd from "../../../../docs/tui.md" with { type: "text" };
|
||||
import tuiRuntimeInternalsMd from "../../../../docs/tui-runtime-internals.md" with { type: "text" };
|
||||
|
||||
export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
|
||||
"bash-tool-runtime.md": bashToolRuntimeMd,
|
||||
"blob-artifact-architecture.md": blobArtifactArchitectureMd,
|
||||
"compaction.md": compactionMd,
|
||||
"config-usage.md": configUsageMd,
|
||||
"custom-tools.md": customToolsMd,
|
||||
"environment-variables.md": environmentVariablesMd,
|
||||
"extension-loading.md": extensionLoadingMd,
|
||||
"extensions.md": extensionsMd,
|
||||
"fs-scan-cache-architecture.md": fsScanCacheArchitectureMd,
|
||||
"gemini-manifest-extensions.md": geminiManifestExtensionsMd,
|
||||
"handoff-generation-pipeline.md": handoffGenerationPipelineMd,
|
||||
"hooks.md": hooksMd,
|
||||
"mcp-protocol-transports.md": mcpProtocolTransportsMd,
|
||||
"mcp-runtime-lifecycle.md": mcpRuntimeLifecycleMd,
|
||||
"mcp-server-tool-authoring.md": mcpServerToolAuthoringMd,
|
||||
"models.md": modelsMd,
|
||||
"natives-addon-loader-runtime.md": nativesAddonLoaderRuntimeMd,
|
||||
"natives-architecture.md": nativesArchitectureMd,
|
||||
"natives-binding-contract.md": nativesBindingContractMd,
|
||||
"natives-build-release-debugging.md": nativesBuildReleaseDebuggingMd,
|
||||
"natives-media-system-utils.md": nativesMediaSystemUtilsMd,
|
||||
"natives-rust-task-cancellation.md": nativesRustTaskCancellationMd,
|
||||
"natives-shell-pty-process.md": nativesShellPtyProcessMd,
|
||||
"natives-text-search-pipeline.md": nativesTextSearchPipelineMd,
|
||||
"non-compaction-retry-policy.md": nonCompactionRetryPolicyMd,
|
||||
"notebook-tool-runtime.md": notebookToolRuntimeMd,
|
||||
"plugin-manager-installer-plumbing.md": pluginManagerInstallerPlumbingMd,
|
||||
"porting-from-pi-mono.md": portingFromPiMonoMd,
|
||||
"porting-to-natives.md": portingToNativesMd,
|
||||
"provider-streaming-internals.md": providerStreamingInternalsMd,
|
||||
"python-repl.md": pythonReplMd,
|
||||
"rpc.md": rpcMd,
|
||||
"rulebook-matching-pipeline.md": rulebookMatchingPipelineMd,
|
||||
"sdk.md": sdkMd,
|
||||
"secrets.md": secretsMd,
|
||||
"session.md": sessionMd,
|
||||
"session-operations-export-share-fork-resume.md": sessionOperationsExportShareForkResumeMd,
|
||||
"session-switching-and-recent-listing.md": sessionSwitchingAndRecentListingMd,
|
||||
"session-tree-plan.md": sessionTreePlanMd,
|
||||
"skills.md": skillsMd,
|
||||
"slash-command-internals.md": slashCommandInternalsMd,
|
||||
"task-agent-discovery.md": taskAgentDiscoveryMd,
|
||||
"theme.md": themeMd,
|
||||
"tree.md": treeMd,
|
||||
"ttsr-injection-lifecycle.md": ttsrInjectionLifecycleMd,
|
||||
"tui.md": tuiMd,
|
||||
"tui-runtime-internals.md": tuiRuntimeInternalsMd,
|
||||
};
|
||||
|
||||
export const EMBEDDED_DOC_FILENAMES = Object.keys(EMBEDDED_DOCS).sort();
|
||||
@@ -123,27 +123,54 @@ export class RpcClient {
|
||||
stdin: "pipe",
|
||||
});
|
||||
|
||||
// Process lines in background
|
||||
// Wait for the "ready" signal or process exit
|
||||
const { promise: readyPromise, resolve: readyResolve, reject: readyReject } = Promise.withResolvers<void>();
|
||||
let readySettled = false;
|
||||
|
||||
// Process lines in background, intercepting the ready signal
|
||||
const lines = readJsonl(this.#process.stdout, this.#abortController.signal);
|
||||
void (async () => {
|
||||
for await (const line of lines) {
|
||||
if (!readySettled && isRecord(line) && line.type === "ready") {
|
||||
readySettled = true;
|
||||
readyResolve();
|
||||
continue;
|
||||
}
|
||||
this.#handleLine(line);
|
||||
}
|
||||
})().catch(() => {});
|
||||
// Stream ended without ready signal — process exited
|
||||
if (!readySettled) {
|
||||
readySettled = true;
|
||||
readyReject(new Error(`Agent process exited before ready. Stderr: ${this.#process?.peekStderr() ?? ""}`));
|
||||
}
|
||||
})().catch((err: Error) => {
|
||||
if (!readySettled) {
|
||||
readySettled = true;
|
||||
readyReject(err);
|
||||
}
|
||||
});
|
||||
|
||||
// Wait a moment for process to initialize
|
||||
await Bun.sleep(100);
|
||||
|
||||
try {
|
||||
const exitCode = await Promise.race([this.#process.exited, Bun.sleep(500).then(() => null)]);
|
||||
if (exitCode !== null) {
|
||||
throw new Error(
|
||||
`Agent process exited immediately with code ${exitCode}. Stderr: ${this.#process.peekStderr()}`,
|
||||
// Also race against process exit (in case stdout closes before we read it)
|
||||
void this.#process.exited.then((exitCode: number) => {
|
||||
if (!readySettled) {
|
||||
readySettled = true;
|
||||
readyReject(
|
||||
new Error(`Agent process exited with code ${exitCode}. Stderr: ${this.#process?.peekStderr() ?? ""}`),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Process still running, which is what we want
|
||||
}
|
||||
});
|
||||
|
||||
// Timeout to prevent hanging forever
|
||||
void Bun.sleep(30000).then(() => {
|
||||
if (!readySettled) {
|
||||
readySettled = true;
|
||||
readyReject(
|
||||
new Error(`Timeout waiting for agent to become ready. Stderr: ${this.#process?.peekStderr() ?? ""}`),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await readyPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,6 +36,8 @@ export type {
|
||||
* Listens for JSON commands on stdin, outputs events and responses on stdout.
|
||||
*/
|
||||
export async function runRpcMode(session: AgentSession): Promise<never> {
|
||||
// Signal to RPC clients that the server is ready to accept commands
|
||||
process.stdout.write(`${JSON.stringify({ type: "ready" })}\n`);
|
||||
const output = (obj: RpcResponse | RpcExtensionUIRequest | object) => {
|
||||
process.stdout.write(`${JSON.stringify(obj)}\n`);
|
||||
};
|
||||
|
||||
@@ -663,10 +663,8 @@ export function applyHashlineEdits(
|
||||
if (edit.content.length === 0) {
|
||||
throw new Error('Insert-between edit (src "A#HH.. B#HH..") requires non-empty dst');
|
||||
}
|
||||
if (edit.before.line !== edit.after.line + 1) {
|
||||
throw new Error(
|
||||
`insert requires adjacent anchors (after ${edit.after.line}, before ${edit.before.line})`,
|
||||
);
|
||||
if (edit.before.line <= edit.after.line) {
|
||||
throw new Error(`insert requires after (${edit.after.line}) < before (${edit.before.line})`);
|
||||
}
|
||||
const afterValid = validateRef(edit.after);
|
||||
const beforeValid = validateRef(edit.before);
|
||||
|
||||
@@ -6,21 +6,28 @@ Use this tool to navigate, click, type, scroll, drag, query DOM content, and cap
|
||||
- Use `action: "open"` to start a new headless browser session (or implicitly launch on first action)
|
||||
- Use `action: "goto"` with `url` to navigate
|
||||
- Use `action: "observe"` to capture a numbered accessibility snapshot with URL/title/viewport/scroll info
|
||||
- Prefer `click_id`, `type_id`, or `fill_id` actions using the returned `element_id` values
|
||||
- Optional flags: `include_all` to include non-interactive nodes, `viewport_only` to limit to visible elements
|
||||
- Prefer `click_id`, `type_id`, or `fill_id` actions using the returned `element_id` values
|
||||
- Optional flags: `include_all` to include non-interactive nodes, `viewport_only` to limit to visible elements
|
||||
- Use `action: "click"`, `"type"`, `"fill"`, `"press"`, `"scroll"`, or `"drag"` for selector-based interactions
|
||||
- Prefer ARIA or text selectors (e.g. `p-aria/[name="Sign in"]`, `p-text/Continue`) over brittle CSS
|
||||
- Prefer ARIA or text selectors (e.g. `p-aria/[name="Sign in"]`, `p-text/Continue`) over brittle CSS
|
||||
- Use `action: "click_id"`, `"type_id"`, or `"fill_id"` to interact with observed elements without selectors
|
||||
- Use `action: "wait_for_selector"` before interacting when the page is dynamic
|
||||
- Use `action: "evaluate"` with `script` to run a JavaScript expression in the page context
|
||||
- Use `action: "get_text"`, `"get_html"`, or `"get_attribute"` for DOM queries
|
||||
- For batch queries, pass `args: [{ selector, attribute? }]` to get an array of results (attribute required for `get_attribute`)
|
||||
- For batch queries, pass `args: [{ selector, attribute? }]` to get an array of results (attribute required for `get_attribute`)
|
||||
- Use `action: "extract_readable"` to return reader-mode content (title/byline/excerpt/text or markdown)
|
||||
- Set `format` to `"markdown"` (default) or `"text"`
|
||||
- Set `format` to `"markdown"` (default) or `"text"`
|
||||
- Use `action: "screenshot"` to capture images (optionally with `selector` to capture a single element)
|
||||
- Use `action: "close"` to release the browser when done
|
||||
</instruction>
|
||||
|
||||
<critical>
|
||||
**Default to `observe`, not `screenshot`.**
|
||||
- `observe` is cheaper, faster, and returns structured data — use it to understand page state, find elements, and plan interactions.
|
||||
- Only use `screenshot` when visual appearance matters (verifying layout, debugging CSS, capturing a visual artifact for the user).
|
||||
- Never screenshot just to "see what's on the page" — `observe` gives you that with element IDs you can act on immediately.
|
||||
</critical>
|
||||
|
||||
<output>
|
||||
Returns text output for navigation and DOM queries, and image output for screenshots. Screenshots can optionally be saved to disk via the `path` parameter.
|
||||
</output>
|
||||
@@ -37,7 +37,7 @@ export interface StoredAuthCredential {
|
||||
}
|
||||
|
||||
/** Bump when schema changes require migration */
|
||||
const SCHEMA_VERSION = 3;
|
||||
const SCHEMA_VERSION = 4;
|
||||
|
||||
/**
|
||||
* Type guard for plain objects.
|
||||
@@ -124,11 +124,14 @@ export class AgentStorage {
|
||||
#deleteExpiredCacheStmt: Statement;
|
||||
#listAuthStmt: Statement;
|
||||
#listAuthByProviderStmt: Statement;
|
||||
#listActiveAuthStmt: Statement;
|
||||
#listActiveAuthByProviderStmt: Statement;
|
||||
#insertAuthStmt: Statement;
|
||||
#updateAuthStmt: Statement;
|
||||
#deleteAuthStmt: Statement;
|
||||
#deleteAuthByProviderStmt: Statement;
|
||||
#countAuthStmt: Statement;
|
||||
#disableAuthStmt: Statement;
|
||||
#upsertModelUsageStmt: Statement;
|
||||
#listModelUsageStmt: Statement;
|
||||
#modelUsageCache: string[] | null = null;
|
||||
@@ -165,6 +168,12 @@ export class AgentStorage {
|
||||
this.#listAuthByProviderStmt = this.#db.prepare(
|
||||
"SELECT id, provider, credential_type, data FROM auth_credentials WHERE provider = ? ORDER BY id ASC",
|
||||
);
|
||||
this.#listActiveAuthStmt = this.#db.prepare(
|
||||
"SELECT id, provider, credential_type, data FROM auth_credentials WHERE disabled = 0 ORDER BY id ASC",
|
||||
);
|
||||
this.#listActiveAuthByProviderStmt = this.#db.prepare(
|
||||
"SELECT id, provider, credential_type, data FROM auth_credentials WHERE provider = ? AND disabled = 0 ORDER BY id ASC",
|
||||
);
|
||||
this.#insertAuthStmt = this.#db.prepare(
|
||||
"INSERT INTO auth_credentials (provider, credential_type, data) VALUES (?, ?, ?) RETURNING id",
|
||||
);
|
||||
@@ -173,6 +182,9 @@ export class AgentStorage {
|
||||
);
|
||||
this.#deleteAuthStmt = this.#db.prepare("DELETE FROM auth_credentials WHERE id = ?");
|
||||
this.#deleteAuthByProviderStmt = this.#db.prepare("DELETE FROM auth_credentials WHERE provider = ?");
|
||||
this.#disableAuthStmt = this.#db.prepare(
|
||||
"UPDATE auth_credentials SET disabled = 1, updated_at = unixepoch() WHERE id = ?",
|
||||
);
|
||||
this.#countAuthStmt = this.#db.prepare("SELECT COUNT(*) as count FROM auth_credentials");
|
||||
|
||||
this.#upsertModelUsageStmt = this.#db.prepare(
|
||||
@@ -198,6 +210,7 @@ CREATE TABLE IF NOT EXISTS auth_credentials (
|
||||
provider TEXT NOT NULL,
|
||||
credential_type TEXT NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
disabled INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||||
);
|
||||
@@ -281,10 +294,21 @@ CREATE TABLE settings (
|
||||
current: versionRow.version,
|
||||
expected: SCHEMA_VERSION,
|
||||
});
|
||||
this.#migrateSchema(versionRow.version);
|
||||
}
|
||||
this.#db.prepare("INSERT OR REPLACE INTO schema_version(version) VALUES (?)").run(SCHEMA_VERSION);
|
||||
}
|
||||
|
||||
#migrateSchema(fromVersion: number): void {
|
||||
if (fromVersion < 4) {
|
||||
// v3 → v4: Add disabled column to auth_credentials
|
||||
const cols = this.#db.prepare("PRAGMA table_info(auth_credentials)").all() as Array<{ name?: string }>;
|
||||
if (!cols.some(c => c.name === "disabled")) {
|
||||
this.#db.exec("ALTER TABLE auth_credentials ADD COLUMN disabled INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns singleton instance for the given database path, creating if needed.
|
||||
* Retries on SQLITE_BUSY with exponential backoff.
|
||||
@@ -429,14 +453,19 @@ CREATE TABLE settings (
|
||||
|
||||
/**
|
||||
* Lists auth credentials, optionally filtered by provider.
|
||||
* Only returns active (non-disabled) credentials by default.
|
||||
* @param provider - Optional provider name to filter by
|
||||
* @param includeDisabled - If true, includes disabled credentials
|
||||
* @returns Array of stored credentials with their database IDs
|
||||
*/
|
||||
listAuthCredentials(provider?: string): StoredAuthCredential[] {
|
||||
const rows =
|
||||
(provider
|
||||
? (this.#listAuthByProviderStmt.all(provider) as AuthRow[])
|
||||
: (this.#listAuthStmt.all() as AuthRow[])) ?? [];
|
||||
listAuthCredentials(provider?: string, includeDisabled = false): StoredAuthCredential[] {
|
||||
const rows = includeDisabled
|
||||
? ((provider
|
||||
? (this.#listAuthByProviderStmt.all(provider) as AuthRow[])
|
||||
: (this.#listAuthStmt.all() as AuthRow[])) ?? [])
|
||||
: ((provider
|
||||
? (this.#listActiveAuthByProviderStmt.all(provider) as AuthRow[])
|
||||
: (this.#listActiveAuthStmt.all() as AuthRow[])) ?? []);
|
||||
|
||||
const results: StoredAuthCredential[] = [];
|
||||
for (const row of rows) {
|
||||
@@ -498,6 +527,19 @@ CREATE TABLE settings (
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables an auth credential by ID (soft-delete).
|
||||
* Disabled credentials are excluded from normal listing but remain in the database.
|
||||
* @param id - Database row ID of the credential to disable
|
||||
*/
|
||||
disableAuthCredential(id: number): void {
|
||||
try {
|
||||
this.#disableAuthStmt.run(id);
|
||||
} catch (error) {
|
||||
logger.warn("AgentStorage disableAuthCredential failed", { id, error: String(error) });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all auth credentials for a provider.
|
||||
* @param provider - Provider name whose credentials should be deleted
|
||||
|
||||
@@ -547,13 +547,14 @@ export class AuthStorage {
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes credential at index (used when OAuth refresh fails).
|
||||
* Cleans up provider entry if last credential removed.
|
||||
* Disables credential at index (used when OAuth refresh fails).
|
||||
* The credential remains in the database but is excluded from active queries.
|
||||
* Cleans up provider entry if last credential disabled.
|
||||
*/
|
||||
#removeCredentialAt(provider: string, index: number): void {
|
||||
const entries = this.#getStoredCredentials(provider);
|
||||
if (index < 0 || index >= entries.length) return;
|
||||
this.storage.deleteAuthCredential(entries[index].id);
|
||||
this.storage.disableAuthCredential(entries[index].id);
|
||||
const updated = entries.filter((_value, idx) => idx !== index);
|
||||
this.#setStoredCredentials(provider, updated);
|
||||
this.#resetProviderAssignments(provider);
|
||||
@@ -1339,7 +1340,7 @@ export class AuthStorage {
|
||||
// Keep credentials for transient errors (network, 5xx) and block temporarily
|
||||
const isDefinitiveFailure =
|
||||
/invalid_grant|invalid_token|revoked|unauthorized|expired.*refresh|refresh.*expired/i.test(errorMsg) ||
|
||||
(/401|403/.test(errorMsg) && !/timeout|network|fetch failed|ECONNREFUSED/i.test(errorMsg));
|
||||
(/\b(401|403)\b/.test(errorMsg) && !/timeout|network|fetch failed|ECONNREFUSED/i.test(errorMsg));
|
||||
|
||||
logger.warn("OAuth token refresh failed", {
|
||||
provider,
|
||||
|
||||
@@ -379,6 +379,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
status: "running",
|
||||
task,
|
||||
description: options.description,
|
||||
lastIntent: undefined,
|
||||
recentTools: [],
|
||||
recentOutput: [],
|
||||
toolCount: 0,
|
||||
@@ -645,14 +646,19 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
}
|
||||
break;
|
||||
|
||||
case "tool_execution_start":
|
||||
case "tool_execution_start": {
|
||||
progress.toolCount++;
|
||||
progress.currentTool = event.toolName;
|
||||
progress.currentToolArgs = extractToolArgsPreview(
|
||||
(event as { toolArgs?: Record<string, unknown> }).toolArgs || event.args || {},
|
||||
);
|
||||
progress.currentToolStartMs = now;
|
||||
const intent = event.intent?.trim();
|
||||
if (intent) {
|
||||
progress.lastIntent = intent;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "tool_execution_end": {
|
||||
if (progress.currentTool) {
|
||||
@@ -1174,6 +1180,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
agentSource: agent.source,
|
||||
task,
|
||||
description: options.description,
|
||||
lastIntent: progress.lastIntent,
|
||||
exitCode,
|
||||
output: truncatedOutput,
|
||||
stderr,
|
||||
|
||||
@@ -535,8 +535,9 @@ function renderAgentProgress(
|
||||
if (progress.status === "running") {
|
||||
if (progress.currentTool) {
|
||||
let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("muted", progress.currentTool)}`;
|
||||
if (progress.currentToolArgs) {
|
||||
toolLine += `: ${theme.fg("dim", truncateToWidth(progress.currentToolArgs, 40))}`;
|
||||
const toolDetail = progress.lastIntent ?? progress.currentToolArgs;
|
||||
if (toolDetail) {
|
||||
toolLine += `: ${theme.fg("dim", truncateToWidth(replaceTabs(toolDetail), 40))}`;
|
||||
}
|
||||
if (progress.currentToolStartMs) {
|
||||
const elapsed = Date.now() - progress.currentToolStartMs;
|
||||
@@ -549,8 +550,9 @@ function renderAgentProgress(
|
||||
// Show most recent completed tool when idle between tools
|
||||
const recent = progress.recentTools[0];
|
||||
let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("dim", recent.tool)}`;
|
||||
if (recent.args) {
|
||||
toolLine += `: ${theme.fg("dim", truncateToWidth(recent.args, 40))}`;
|
||||
const toolDetail = progress.lastIntent ?? recent.args;
|
||||
if (toolDetail) {
|
||||
toolLine += `: ${theme.fg("dim", truncateToWidth(replaceTabs(toolDetail), 40))}`;
|
||||
}
|
||||
lines.push(toolLine);
|
||||
}
|
||||
|
||||
@@ -140,6 +140,7 @@ export interface AgentProgress {
|
||||
status: "pending" | "running" | "completed" | "failed" | "aborted";
|
||||
task: string;
|
||||
description?: string;
|
||||
lastIntent?: string;
|
||||
currentTool?: string;
|
||||
currentToolArgs?: string;
|
||||
currentToolStartMs?: number;
|
||||
@@ -161,6 +162,7 @@ export interface SingleResult {
|
||||
agentSource: AgentSource;
|
||||
task: string;
|
||||
description?: string;
|
||||
lastIntent?: string;
|
||||
exitCode: number;
|
||||
output: string;
|
||||
stderr: string;
|
||||
|
||||
Reference in New Issue
Block a user