diff --git a/docs/reference/ci-runner-efficiency.md b/docs/reference/ci-runner-efficiency.md index 0af400ef203..611ef54b8cb 100644 --- a/docs/reference/ci-runner-efficiency.md +++ b/docs/reference/ci-runner-efficiency.md @@ -2519,3 +2519,67 @@ catalog subjects and declared qualification inputs survive the rebase unchanged. These are fresh-process wall times for this cohort, excluding dependency setup, queues and other test shards. They do not measure full-shard balance, total PR runner demand, or a change to the dashboard's PR runtime percentiles. + +## October 5 fused terminal cursor row scans + +Readiness checks repeatedly read terminal rows to recognize composer text. +The shared reader now collects undimmed text and the first visible glyph's style +in one pass. The cursor suffix remains a separate scan; dim glyph attributes, +empty cells, wide characters and wrapped spaces keep their existing behavior. +No grid data is retained between calls. + +The [hosted comparison](https://github.com/stablyai/orca/actions/runs/37263358478) +used source `1bec53ceb23b5f296a38ffa0e085778d32fc7849`, Linux ARM, +Node 24.21.0, pnpm 12.8.1 and one worker. All six fresh processes ran the same +49-case readiness census module, with separate empty Vite caches, filesystem +transform caching disabled and Node compile caching disabled. + +| Complete focused process | Original | Candidate | Paired saving | +| ------------------------ | -------- | --------- | ------------- | +| Pair 1 | 66.412s | 60.464s | 5.948s | +| Pair 2, reversed order | 66.880s | 61.061s | 5.819s | +| Pair 3 | 68.056s | 65.821s | 2.234s | + +The median paired saving is 5.819 seconds, or 8.7% of this fixed workload. +All 294 timed case outcomes passed. Separate qualification preserves complete +context/composer outputs on 98 recordings and the logical cursor/projection +outputs of all 192 Runtime census cases. Nine actual scanner faults fail the +intended assertions; the 89-case IME/composer slice also passes. Blank-row tests +bound cell reads to 72 instead of the original 132 for a 12-column, five-row grid, +with and without a reusable cell adapter. + +The raw timer-driven polling trace differed and was excluded from equivalence +evidence. Logical per-frame output captures match; no raw polling-count equality +is claimed. These measurements exclude setup, queues and other modules and do +not establish a change in full-shard balance, PR percentiles or runner demand. + +## October 5 Qoder test import guards + +The direct Qoder Runtime tests now import the existing unused-default-RPC guard +before their Runtime fixture. Their complete test bodies remain unchanged. +The guard rejects an unexpected registry access instead of loading the full +default-method graph. The three real registry catalogs remain unmocked. + +The [hosted comparison](https://github.com/stablyai/orca/actions/runs/37266875139) +used fixed source `1bec53ceb23b5f296a38ffa0e085778d32fc7849`, Linux ARM, +Node 24.21.0, pnpm 12.8.1 and four isolated fork workers. Every fresh process ran +both Qoder files and all three catalogs: 85 cases across five files. Each invocation +used a distinct empty Vite cache, with results, filesystem transform and Node +compile caching disabled. All 510 timed outcomes passed. + +| Complete five-file process | Original | Candidate | Paired saving | +| -------------------------- | -------- | --------- | ------------- | +| Pair 1 | 17.677s | 16.323s | 1.354s | +| Pair 2, reversed order | 16.827s | 16.021s | 0.806s | +| Pair 3 | 17.324s | 16.211s | 1.113s | + +The median paired saving is 1.113 seconds, or 6.4% of this fixed workload. +Separate actual faults in retained launch recipes, Qoder command selection and +method registration fail the same intended assertions before and after the +imports. Generated launch IDs and shifted stack lines differ in the raw failure +messages; they were preserved and are not claimed byte-identical. + +The single hosted trial occupied 134 runner-seconds including all six samples, +shared setup and upload. That is trial cost, not a production saving. These +focused process measurements do not establish full-shard savings, PR runtime +percentiles, queue relief or a change in the organization's runner allowance. diff --git a/src/main/daemon/terminal-cursor-line-context.test.ts b/src/main/daemon/terminal-cursor-line-context.test.ts index 29afe00618b..3184d53ca17 100644 --- a/src/main/daemon/terminal-cursor-line-context.test.ts +++ b/src/main/daemon/terminal-cursor-line-context.test.ts @@ -157,4 +157,79 @@ describe('readTerminalCursorLineContext', () => { expect(detectTerminalComposerDraft(context)?.text).toBe(draft) emulator.dispose() }) + + it.each([true, false])('bounds blank-row cell reads with reusable cells %s', (reusable) => { + const terminal = new Terminal({ cols: 12, rows: 5, allowProposedApi: true }) + try { + const rig = observeCellReads(terminal) + const active = rig.source.buffer.active + const source = { + ...rig.source, + buffer: { active: { ...active, getNullCell: reusable ? active.getNullCell : undefined } } + } + const context = readTerminalCursorLineContext(source, terminal.rows) + expect(context?.typedRows).toEqual(['']) + expect(context?.typedRowsBelow).toEqual(['', '', '', '']) + expect(context?.promptGlyphBoldRows).toEqual([false]) + expect(context?.rowsBelowCustomForeground).toEqual([false, false, false, false]) + // A blank glyph search must share the full row scan; the cursor suffix is separate. + expect(rig.targets.length).toBeLessThanOrEqual(terminal.cols * (terminal.rows + 1)) + expect(rig.allocated).toHaveLength(reusable ? 1 : 0) + expect(rig.targets.every((cell) => cell === rig.allocated[0])).toBe(true) + } finally { + terminal.dispose() + } + }) + + it('keeps dimmed first-glyph styles while excluding their text', () => { + const terminal = new Terminal({ cols: 32, rows: 5, allowProposedApi: true }) + try { + writeSync(terminal, '\x1b[2;1m›\x1b[0m typed\x1b7\r\n\x1b[2;31mfooter\x1b[0m\x1b8') + const context = readTerminalCursorLineContext(terminal, terminal.rows) + expect(context?.typedRows).toEqual([' typed']) + expect(context?.promptGlyphBoldRows).toEqual([true]) + expect(context?.typedRowsBelow[0]).toBe('') + expect(context?.rowsBelowCustomForeground?.[0]).toBe(true) + } finally { + terminal.dispose() + } + }) + + it('uses the first visible glyph even when later glyphs have styles', () => { + const terminal = new Terminal({ cols: 32, rows: 5, allowProposedApi: true }) + try { + writeSync( + terminal, + '\x1b[1m \u00a0\x1b[22m›\x1b[1mX\x1b[0m\x1b7\r\n' + + '\x1b[31m \x1b[39mplain \x1b[32mlater\x1b[0m\x1b8' + ) + const context = readTerminalCursorLineContext(terminal, terminal.rows) + expect(context?.promptGlyphBoldRows).toEqual([false]) + expect(context?.rowsBelowCustomForeground?.[0]).toBe(false) + } finally { + terminal.dispose() + } + }) + + it('finds styled first glyphs after unstyled blank cells', () => { + const terminal = new Terminal({ cols: 32, rows: 5, allowProposedApi: true }) + try { + writeSync(terminal, ' \x1b[1m›\x1b[0m typed\x1b7\r\n \x1b[31mfooter\x1b[0m\x1b8') + const context = readTerminalCursorLineContext(terminal, terminal.rows) + expect(context?.promptGlyphBoldRows).toEqual([true]) + expect(context?.rowsBelowCustomForeground?.[0]).toBe(true) + } finally { + terminal.dispose() + } + }) + + it('keeps an unwritten cell between typed glyphs as a space', () => { + const terminal = new Terminal({ cols: 12, rows: 5, allowProposedApi: true }) + try { + writeSync(terminal, 'a\x1b[3Gb') + expect(readTerminalCursorLineContext(terminal, terminal.rows)?.typedRows).toEqual(['a b']) + } finally { + terminal.dispose() + } + }) }) diff --git a/src/main/runtime/qoder-independent-spawn-review.test.ts b/src/main/runtime/qoder-independent-spawn-review.test.ts index 87f2c7cef56..bfc32c1f78c 100644 --- a/src/main/runtime/qoder-independent-spawn-review.test.ts +++ b/src/main/runtime/qoder-independent-spawn-review.test.ts @@ -1,3 +1,4 @@ +import './rpc/unused-default-rpc-methods.test-fixture' import { beforeEach, expect, it, vi } from 'vitest' import filesystem from 'node:fs/promises' import { tmpdir } from 'node:os' diff --git a/src/main/runtime/qoder-mobile-resume-adoption.test.ts b/src/main/runtime/qoder-mobile-resume-adoption.test.ts index cde2dabacd5..2f244313a9b 100644 --- a/src/main/runtime/qoder-mobile-resume-adoption.test.ts +++ b/src/main/runtime/qoder-mobile-resume-adoption.test.ts @@ -1,3 +1,4 @@ +import './rpc/unused-default-rpc-methods.test-fixture' import { expect, it, vi } from 'vitest' import type { SleepingAgentLaunchConfig } from '../../shared/agent-session-resume' import { buildAgentResumeStartupPlan } from '../../shared/tui-agent-startup' diff --git a/src/shared/terminal-cursor-line-context.ts b/src/shared/terminal-cursor-line-context.ts index 28fc2402fe2..857adb037e5 100644 --- a/src/shared/terminal-cursor-line-context.ts +++ b/src/shared/terminal-cursor-line-context.ts @@ -47,32 +47,30 @@ function undimmedText( return trimRight ? text.trimEnd() : text } -function firstVisibleCellIsBold( +function readUndimmedTextAndFirstVisibleStyle( line: TerminalCursorLine, + trimRight: boolean, + style: 'bold' | 'customForeground', reusableCell?: Cell -): boolean { +): { text: string; firstVisibleStyle: boolean } { + let text = '' + let firstVisibleStyle = false + let foundVisibleCell = false for (let x = 0; x < line.length; x += 1) { const cell = line.getCell(x, reusableCell) - if (!cell || cell.getWidth() === 0 || !cell.getChars().trim()) { + if (!cell || cell.getWidth() === 0) { continue } - return Boolean(cell.isBold()) - } - return false -} - -function firstVisibleCellHasCustomForeground( - line: TerminalCursorLine, - reusableCell?: Cell -): boolean { - for (let x = 0; x < line.length; x += 1) { - const cell = line.getCell(x, reusableCell) - if (!cell || cell.getWidth() === 0 || !cell.getChars().trim()) { - continue + const chars = cell.getChars() + if (!foundVisibleCell && chars.trim()) { + firstVisibleStyle = style === 'bold' ? Boolean(cell.isBold()) : !cell.isFgDefault() + foundVisibleCell = true + } + if (!cell.isDim()) { + text += chars || ' ' } - return !cell.isFgDefault() } - return false + return { text: trimRight ? text.trimEnd() : text, firstVisibleStyle } } export function readTerminalCursorLineContext( @@ -96,8 +94,11 @@ export function readTerminalCursorLineContext( const line = buffer.getLine(row) const nextLineIsWrapped = buffer.getLine(row + 1)?.isWrapped ?? false rows.push(line?.translateToString(!nextLineIsWrapped) ?? '') - typedRows.push(line ? undimmedText(line, 0, !nextLineIsWrapped, reusableCell) : '') - promptGlyphBoldRows.push(line ? firstVisibleCellIsBold(line, reusableCell) : false) + const scan = line + ? readUndimmedTextAndFirstVisibleStyle(line, !nextLineIsWrapped, 'bold', reusableCell) + : undefined + typedRows.push(scan?.text ?? '') + promptGlyphBoldRows.push(scan?.firstVisibleStyle ?? false) rowsWrapped.push(line?.isWrapped ?? false) } const rowsBelow: string[] = [] @@ -109,11 +110,17 @@ export function readTerminalCursorLineContext( const line = buffer.getLine(row) const nextLineIsWrapped = buffer.getLine(row + 1)?.isWrapped ?? false rowsBelow.push(line?.translateToString(!nextLineIsWrapped) ?? '') - typedRowsBelow.push(line ? undimmedText(line, 0, !nextLineIsWrapped, reusableCell) : '') + const scan = line + ? readUndimmedTextAndFirstVisibleStyle( + line, + !nextLineIsWrapped, + 'customForeground', + reusableCell + ) + : undefined + typedRowsBelow.push(scan?.text ?? '') rowsBelowWrapped.push(line?.isWrapped ?? false) - rowsBelowCustomForeground.push( - line ? firstVisibleCellHasCustomForeground(line, reusableCell) : false - ) + rowsBelowCustomForeground.push(scan?.firstVisibleStyle ?? false) } return { rows,