diff --git a/mobile/patches/query-string@9.5.1.patch b/mobile/patches/query-string@9.5.1.patch new file mode 100644 index 00000000000..eb8c575d5ec --- /dev/null +++ b/mobile/patches/query-string@9.5.1.patch @@ -0,0 +1,10 @@ +diff --git a/index.js b/index.js +index 0480a96d718c997b0a5a54b775e635e2e048e796..85f71b13d3f0f31c3c2140581d1e4051afb21dc3 100644 +--- a/index.js ++++ b/index.js +@@ -1,3 +1,5 @@ + import * as queryString from './base.js'; + + export default queryString; ++ ++export * from './base.js'; diff --git a/mobile/pnpm-lock.yaml b/mobile/pnpm-lock.yaml index 201d564805c..63a1f5dec01 100644 --- a/mobile/pnpm-lock.yaml +++ b/mobile/pnpm-lock.yaml @@ -12,6 +12,9 @@ patchedDependencies: expo-notifications@55.0.27: hash: ce20843a3daad4185d7e8571788fa323ba4d11984936188790858650a61749c0 path: patches/expo-notifications@55.0.27.patch + query-string@9.5.1: + hash: 8624f1beedd0705bfdaf287519ed6363662a0c96f01fd594508b34583bf30d8b + path: patches/query-string@9.5.1.patch react-native-webview@13.16.2: hash: de6761dfa76a5491a23e49f1262566a8831cab26736a336fdffc5d4e7d05ff27 path: patches/react-native-webview@13.16.2.patch @@ -9966,7 +9969,7 @@ snapshots: escape-string-regexp: 4.0.0 fast-deep-equal: 3.1.3 nanoid: 3.3.18 - query-string: 9.5.1 + query-string: 9.5.1(patch_hash=8624f1beedd0705bfdaf287519ed6363662a0c96f01fd594508b34583bf30d8b) react: 19.2.8 react-is: 19.2.6 use-latest-callback: 0.2.6(react@19.2.8) @@ -12230,7 +12233,7 @@ snapshots: fast-deep-equal: 3.1.3 invariant: 2.2.4 nanoid: 3.3.18 - query-string: 9.5.1 + query-string: 9.5.1(patch_hash=8624f1beedd0705bfdaf287519ed6363662a0c96f01fd594508b34583bf30d8b) react: 19.2.8 react-fast-compare: 3.2.2 react-native: 0.83.10(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.8) @@ -14283,7 +14286,7 @@ snapshots: pure-rand@6.1.0: {} - query-string@9.5.1: + query-string@9.5.1(patch_hash=8624f1beedd0705bfdaf287519ed6363662a0c96f01fd594508b34583bf30d8b): dependencies: decode-uri-component: 0.5.0 filter-obj: 5.1.0 diff --git a/mobile/pnpm-workspace.yaml b/mobile/pnpm-workspace.yaml index 9f0a43c71fe..4e4bb54fc3d 100644 --- a/mobile/pnpm-workspace.yaml +++ b/mobile/pnpm-workspace.yaml @@ -9,5 +9,6 @@ overrides: patchedDependencies: expo-notifications@55.0.27: patches/expo-notifications@55.0.27.patch + query-string@9.5.1: patches/query-string@9.5.1.patch react-native-webview@13.16.2: patches/react-native-webview@13.16.2.patch react-native@0.83.10: patches/react-native@0.83.10.patch diff --git a/mobile/src/navigation/query-string-named-exports.test.ts b/mobile/src/navigation/query-string-named-exports.test.ts new file mode 100644 index 00000000000..9cd34aedaea --- /dev/null +++ b/mobile/src/navigation/query-string-named-exports.test.ts @@ -0,0 +1,28 @@ +import { execFileSync } from 'node:child_process' +import { createRequire } from 'node:module' +import { pathToFileURL } from 'node:url' +import { describe, expect, it } from 'vitest' + +/** + * expo-router and React Navigation serialise route params through `import * as queryString from + * 'query-string'`. The lockfile overrides `query-string` to 9.x for a `decode-uri-component` + * advisory, and 9.x's entry has a default export only, so without `patches/query-string@9.5.1.patch` + * every push carrying a param outside the path pattern throws `queryString.stringify is not a + * function` and every href with a query throws on `parse`. Resolved from expo-router's own location, + * the way Metro and the web bundler resolve it for that consumer, and imported by a plain Node + * child: vitest's default-export interop would paper over the missing names in-process. + */ +describe('query-string, as expo-router resolves it', () => { + it('exposes the named API the namespace import needs', () => { + const requireFromHere = createRequire(import.meta.url) + const requireFromExpoRouter = createRequire(requireFromHere.resolve('expo-router/package.json')) + const entry = pathToFileURL(requireFromExpoRouter.resolve('query-string')).href + const script = `const ns = await import(${JSON.stringify(entry)}); process.stdout.write(JSON.stringify({ names: Object.keys(ns).sort(), stringified: typeof ns.stringify === 'function' ? ns.stringify({ from: 'worktrees' }) : null }))` + const output = execFileSync(process.execPath, ['--input-type=module', '-e', script], { + encoding: 'utf8' + }) + const { names, stringified } = JSON.parse(output) + expect(names).toEqual(expect.arrayContaining(['parse', 'stringify'])) + expect(stringified).toBe('from=worktrees') + }) +}) diff --git a/src/main/opencode-usage/opencode-usage-row-queries.ts b/src/main/opencode-usage/opencode-usage-row-queries.ts index 2e633d7cda2..db8e84010b2 100644 --- a/src/main/opencode-usage/opencode-usage-row-queries.ts +++ b/src/main/opencode-usage/opencode-usage-row-queries.ts @@ -27,6 +27,7 @@ type OpenCodeSessionUsageRow = { tokens_output: number tokens_reasoning: number tokens_cache_read: number + tokens_cache_write: number } function getProjectJoin(db: Database.Database): string { @@ -46,6 +47,7 @@ function getAssistantSessionMessageCount(db: Database.Database): number { const assistantPredicate = columnExists(db, 'session_message', 'type') ? "type = 'assistant' AND json_extract(data, '$.tokens.input') IS NOT NULL" : "json_extract(data, '$.tokens.input') IS NOT NULL" + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: SQLite aggregate rows are validated by the typed count field below. const row = db .prepare(`SELECT COUNT(*) AS count FROM session_message WHERE ${assistantPredicate}`) .get() as { count?: number } | undefined @@ -61,16 +63,29 @@ function canReadSessionUsageRows(db: Database.Database): boolean { ) } +function getSessionCacheWriteSelect(db: Database.Database): string { + return columnExists(db, 'session', 'tokens_cache_write') ? 's.tokens_cache_write' : '0' +} + +function getSessionTokenTotalExpression(db: Database.Database): string { + const cacheWrite = columnExists(db, 'session', 'tokens_cache_write') + ? ' + tokens_cache_write' + : '' + return `tokens_input + tokens_output + tokens_reasoning + tokens_cache_read${cacheWrite}` +} + function getSessionUsageRowCount(db: Database.Database): number { if (!canReadSessionUsageRows(db)) { return 0 } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: SQLite aggregate rows are validated by the typed count field below. const row = db .prepare( `SELECT COUNT(*) AS count FROM session - WHERE tokens_input + tokens_output + tokens_reasoning + tokens_cache_read > 0` + WHERE ${getSessionTokenTotalExpression(db)} > 0` ) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: SQLite aggregate rows are validated by the typed count field below. .get() as { count?: number } | undefined return row?.count ?? 0 } @@ -78,14 +93,18 @@ function getSessionUsageRowCount(db: Database.Database): number { function selectSessionUsageRows(db: Database.Database): OpenCodeUsageRow[] { const projectJoin = getProjectJoin(db) const sessionModelSelect = getSessionModelSelect(db) + const cacheWriteSelect = getSessionCacheWriteSelect(db) + const tokenTotalExpression = getSessionTokenTotalExpression(db) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: SELECT aliases match OpenCodeSessionUsageRow across supported schemas. const rows = db .prepare( `SELECT s.id, s.id AS session_id, s.time_created, s.time_updated, s.directory, s.title, p.worktree, ${sessionModelSelect}, - s.cost, s.tokens_input, s.tokens_output, s.tokens_reasoning, s.tokens_cache_read + s.cost, s.tokens_input, s.tokens_output, s.tokens_reasoning, s.tokens_cache_read, + ${cacheWriteSelect} AS tokens_cache_write FROM session s ${projectJoin} - WHERE s.tokens_input + s.tokens_output + s.tokens_reasoning + s.tokens_cache_read > 0 + WHERE ${tokenTotalExpression.replaceAll('tokens_', 's.tokens_')} > 0 ORDER BY s.time_created, s.id` ) .all() as OpenCodeSessionUsageRow[] @@ -105,10 +124,15 @@ function selectSessionUsageRows(db: Database.Database): OpenCodeUsageRow[] { input: row.tokens_input, output: row.tokens_output, reasoning: row.tokens_reasoning, - total: row.tokens_input + row.tokens_output + row.tokens_reasoning, + total: + row.tokens_input + + row.tokens_output + + row.tokens_reasoning + + row.tokens_cache_read + + row.tokens_cache_write, cache: { read: row.tokens_cache_read, - write: 0 + write: row.tokens_cache_write } } }) diff --git a/src/main/opencode-usage/scanner.test.ts b/src/main/opencode-usage/scanner.test.ts index 4188a0f93a9..88559b0ec7d 100644 --- a/src/main/opencode-usage/scanner.test.ts +++ b/src/main/opencode-usage/scanner.test.ts @@ -43,6 +43,7 @@ function createSessionTotalsSchema(db: Database.Database): void { tokens_output INTEGER, tokens_reasoning INTEGER, tokens_cache_read INTEGER, + tokens_cache_write INTEGER, time_created INTEGER, time_updated INTEGER ); @@ -57,9 +58,9 @@ function insertSessionTotalsRow( db.prepare( `INSERT INTO session ( id, directory, title, model, cost, - tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, + tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write, time_created, time_updated - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ).run( sessionId, `${WORKTREE}/packages/app`, @@ -70,6 +71,7 @@ function insertSessionTotalsRow( 100, 0, 0, + 0, 1_777_777_700_000, 1_777_777_800_000 ) @@ -235,6 +237,7 @@ describe('parseOpenCodeUsageDatabase', () => { tokens_output INTEGER, tokens_reasoning INTEGER, tokens_cache_read INTEGER, + tokens_cache_write INTEGER, time_created INTEGER, time_updated INTEGER ); @@ -243,9 +246,9 @@ describe('parseOpenCodeUsageDatabase', () => { db.prepare( `INSERT INTO session ( id, project_id, directory, title, model, cost, - tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, + tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write, time_created, time_updated - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ).run( 'session-1', 'project-1', @@ -257,6 +260,7 @@ describe('parseOpenCodeUsageDatabase', () => { 500, 100, 250, + 75, 1_777_777_700_000, 1_777_777_800_000 ) @@ -274,7 +278,7 @@ describe('parseOpenCodeUsageDatabase', () => { totalCachedInputTokens: 250, totalOutputTokens: 500, totalReasoningOutputTokens: 100, - totalTokens: 1850, + totalTokens: 1925, estimatedCostUsd: 0.06 }) expect(parsed.dailyAggregates).toEqual([ @@ -284,7 +288,7 @@ describe('parseOpenCodeUsageDatabase', () => { cachedInputTokens: 250, outputTokens: 500, reasoningOutputTokens: 100, - totalTokens: 1850, + totalTokens: 1925, estimatedCostUsd: 0.06 }) ]) diff --git a/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-07.spec.ts b/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-07.spec.ts index 3ae648ab0ef..5e1de184c9c 100644 --- a/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-07.spec.ts +++ b/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-07.spec.ts @@ -3,7 +3,7 @@ import { AGENT_PROMPT_BRACKETED_PASTE_END, AGENT_PROMPT_BRACKETED_PASTE_START, buildAgentPromptPasteBytes, - getAgentPromptSubmitDelayMs + resolveAgentPromptSubmitDelayForAgent } from '../../../shared/agent-prompt-injection' import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config' import type { TuiAgent } from '../../../shared/tui-agent' @@ -610,9 +610,13 @@ describe('OrcaRuntimeService', () => { launchAgent: agent }) - const submitDelayMs = getAgentPromptSubmitDelayMs( + // The agent's own policy, not the byte-only delay: antigravity adds a per-line settle + // (#21665), and advancing fake timers by less than the policy waits leaves the submit + // pending until the real 30 s timeout. + const submitDelayMs = resolveAgentPromptSubmitDelayForAgent( process.platform, - Buffer.byteLength(buildAgentPromptPasteBytes('review this change'), 'utf8') + 'review this change', + agent ) const sendPromise = runtime.sendTerminalAgentPrompt(handle, 'review this change') if (agent === 'omp') { diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index f28c32e0b8a..a915bd8e132 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -1074,17 +1074,24 @@ }, "settings": { "AccountsPane": { + "0023cc336e": "Paste either the raw token value (e.g.", "15e831350e": "Configure MiniMax usage tracking from platform.minimax.io.", "1fd1b1b6b4": "Cookie not set", + "338820326a": ") or the full cookie header (e.g.", "3455cf43fa": "Claude login.", "350b2a1aa7": "Use your current", "4e32e030b2": "Stored locally. Orca sends it only to platform.minimax.io for usage refreshes.", "566d9a99ab": "_token=…; minimax_group_id_v2=…", "5e08b0fe57": "Stored locally and sent only to platform.minimax.io for usage refreshes.", "79418c782a": "Open platform.minimax.io/console/usage in your browser, sign in, then copy the Cookie request header from DevTools (Network → any remains request → Cookie).", + "7ce0e1907c": "). Find it in your browser's DevTools → Network → any opencode.ai request → Cookie header. OpenCode Go auth is web-based and shared across Windows and WSL terminals.", + "8951c5309f": "auth=Fe26.2**…", "9107406589": "Could not load Claude accounts.", + "922b51e02d": "Fe26.2**…", + "a7e38affcd": "Fe26.2**… token or auth=Fe26.2**… header", "b10cb4f696": "adding", "b11078a9c2": "wsl", + "b2b1aa936d": "Paste your opencode.ai session cookie for rate limit fetching.", "b43e761fe5": "MiniMax cookie update failed.", "b8c2905c2b": "Could not load Codex accounts.", "f5d8d2a6a1": "Open platform.minimax.io/console/usage in your browser and sign in." @@ -1557,6 +1564,11 @@ "7c3bb36706": "remove", "e2b0ee267f": "stale" }, + "accounts": { + "search": { + "d1d2ae383c": "Paste your opencode.ai session cookie for rate limit fetching." + } + }, "agent-awake-copy": { "95d3031db2": "Keeps this computer and display awake while agents are working. Lid-close behavior follows this device's power settings.", "a42f6fbdd8": "Keeps this computer and display awake while agents are working. Orca also asks this device to stay awake when the lid is closed, subject to its power policy.",