Merge branch 'main' into feat/cosmetic-fixes

This commit is contained in:
Can Bölük
2026-02-20 14:00:56 +01:00
committed by GitHub
59 changed files with 16344 additions and 285 deletions
@@ -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`);
};
+2 -4
View File
@@ -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,
+8 -1
View File
@@ -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,
+6 -4
View File
@@ -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);
}
+2
View File
@@ -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;