Files
orca/src/shared/terminal-view-attributes.ts
T
Jinjing 1def694e80 Add native chat skill picker with host-aware discovery (#9480)
* Add native chat skill and command picker with host-aware discovery

Adds a unified, keyboard-first skill and command picker to native chat that:

- Uses agent-native invocation syntax (slash for Claude/OpenClaude/Grok, dollar for Codex)
- Discovers skills only on the pane's execution host (local, WSL, SSH-unavailable, or runtime)
- Groups or separates commands and skills per agent configuration
- Deduplicates by canonical path but preserves visibility through all contributing roots
- Handles IME composition, loading states, and errors without claiming PTY-level control
- Records picker telemetry (open, item accepted, send classification, discovery outcomes)
- Extends shared agent profiles to define per-agent skill grammars and source ownership

* Remove obsolete reference and design documentation

Clean up stale design specs, implementation plans, and investigation notes from
docs/reference/. These documents predate the current implementation and are no
longer actively maintained or referenced by the codebase.

* Extract shared skill discovery utilities and add skill invocation envelo

- Move skill comparison and source classification to shared module for native/WSL reuse
- Extract display text sanitization to prevent control/zero-width character spoofing
- Add native-chat command envelope parser and surfacer for skill invocations
- Extend discovery timeout backstop to account for WSL metadata read sequence

* Localize skill picker UI for Spanish, Japanese, Korean, Chinese

Translate skill picker UI strings including commands, skills, loading
states, error messages, and scope labels for the new skill picker feature
across four language locales.

* Fix skill picker bugs and improve code robustness

- Fix i18n plural handling: rename `count` to `sourceCount` to prevent unintended plural-key resolution in localized strings
- Fix skill discovery array mutations: copy `root.providers` to prevent bugs during dedup merge
- Fix image attachments being silently dropped when message text starts with /skill or agent prefix
- Extract `quoteBashString` utility for WSL command code reuse across builders
- Add line-separator safety characters (0x2028/0x2029) to skill display filter
- Remove stale doc reference links and clarify inline comments

* Add reference docs for git compatibility and headless Linux server setup

Track previously untracked operational guides in `docs/reference/` that
explain Git binary compatibility requirements across host types and how to
run `orca serve` on headless Linux. Update AGENTS.md and README.md to link
to these references.
2026-07-19 17:31:34 -07:00

189 lines
6.7 KiB
TypeScript

/**
* Phase 5 slice 2 (View-attribute bridge): payload contract for the
* renderer→main `pty:terminalViewAttributes`
* push, plus main/renderer mirrors of xterm's XParseColor color-spec grammar
* so main's responder replies byte-identically to a visible renderer xterm.
*/
/** 8-bit-per-channel RGB triple — the same resolution xterm's theme service
* stores internally (`color.toColorRGB`). */
export type TerminalViewRgb = [number, number, number]
export const TERMINAL_VIEW_ANSI_COLOR_COUNT = 256
export type TerminalViewCursorStyle = 'bar' | 'block' | 'underline'
/** One app-global snapshot of the renderer's composed terminal appearance —
* per-pane font zoom never affects these, and terminalColorOverrides /
* cursor settings are global, so one push covers all PTYs. */
export type TerminalViewAttributes = {
foreground: TerminalViewRgb
background: TerminalViewRgb
/** Already blended over the background (xterm ThemeService blends the
* cursor color's alpha at theme-set time, e.g. terminalCursorOpacity). */
cursor: TerminalViewRgb
/** Full 256-entry palette: theme's 16 named colors + extendedAnsi/default
* tail, exactly as the renderer ThemeService resolves them. */
ansi: TerminalViewRgb[]
/** Resolved APP color-scheme mode (the 2031/997 flip source). NOT the DSR
* ?996n answer: that is computed from background/foreground relative
* luminance like a visible xterm (_reportColorScheme), and the two can
* disagree (e.g. dark terminal theme in light app mode). */
colorSchemeMode: 'dark' | 'light'
cursorStyle: TerminalViewCursorStyle
cursorBlink: boolean
}
// Mirror of @xterm XParseColor RGB_REX: r/g/b channels in 1-4 hex digits.
const X_RGB_SPEC_RE =
/^([\da-f])\/([\da-f])\/([\da-f])$|^([\da-f]{2})\/([\da-f]{2})\/([\da-f]{2})$|^([\da-f]{3})\/([\da-f]{3})\/([\da-f]{3})$|^([\da-f]{4})\/([\da-f]{4})\/([\da-f]{4})$/
const X_HASH_SPEC_RE = /^[\da-f]+$/
/** Mirror of xterm's XParseColor `parseColor` (the grammar the renderer
* accepts for OSC 4/10/11/12 SET payloads): `rgb:h/h/h`..`rgb:hhhh/hhhh/hhhh`
* and `#RGB|#RRGGBB|#RRRGGGBBB|#RRRRGGGGBBBB`. Anything else (named colors,
* rgbi:) is rejected exactly like the renderer rejects it. */
export function parseXColorSpec(spec: string): TerminalViewRgb | null {
if (!spec) {
return null
}
let low = spec.toLowerCase()
if (low.startsWith('rgb:')) {
low = low.slice(4)
const m = X_RGB_SPEC_RE.exec(low)
if (m) {
const base = m[1] ? 15 : m[4] ? 255 : m[7] ? 4095 : 65535
return [
Math.round((Number.parseInt(m[1] || m[4] || m[7] || m[10], 16) / base) * 255),
Math.round((Number.parseInt(m[2] || m[5] || m[8] || m[11], 16) / base) * 255),
Math.round((Number.parseInt(m[3] || m[6] || m[9] || m[12], 16) / base) * 255)
]
}
return null
}
if (low.startsWith('#')) {
low = low.slice(1)
if (X_HASH_SPEC_RE.exec(low) && [3, 6, 9, 12].includes(low.length)) {
const adv = low.length / 3
const result: TerminalViewRgb = [0, 0, 0]
for (let i = 0; i < 3; ++i) {
const c = Number.parseInt(low.slice(adv * i, adv * i + adv), 16)
result[i] = adv === 1 ? c << 4 : adv === 2 ? c : adv === 3 ? c >> 4 : c >> 8
}
return result
}
}
return null
}
function padChannelTo16Bit(value: number): string {
const hex = value.toString(16)
const byte = hex.length < 2 ? `0${hex}` : hex
// Why doubled: xterm reports 16-bit channels by repeating the 8-bit byte
// (XParseColor.toRgbString with bits=16) — pinned reply-format parity.
return byte + byte
}
/** Mirror of xterm's `toRgbString(color, 16)` — the exact channel format a
* visible renderer xterm uses in OSC 4/10/11/12 query replies. */
export function formatXColorRgbSpec(rgb: TerminalViewRgb): string {
return `rgb:${padChannelTo16Bit(rgb[0])}/${padChannelTo16Bit(rgb[1])}/${padChannelTo16Bit(rgb[2])}`
}
function rgbEqual(a: TerminalViewRgb, b: TerminalViewRgb): boolean {
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2]
}
/** Value equality over the whole snapshot. Lets main's store treat a
* re-push of identical attributes (fresh renderer process: second window,
* reload, macOS re-activation) as a no-op instead of a theme apply. */
export function terminalViewAttributesEqual(
a: TerminalViewAttributes,
b: TerminalViewAttributes
): boolean {
if (a === b) {
return true
}
if (
!rgbEqual(a.foreground, b.foreground) ||
!rgbEqual(a.background, b.background) ||
!rgbEqual(a.cursor, b.cursor) ||
a.colorSchemeMode !== b.colorSchemeMode ||
a.cursorStyle !== b.cursorStyle ||
a.cursorBlink !== b.cursorBlink ||
a.ansi.length !== b.ansi.length
) {
return false
}
for (let i = 0; i < a.ansi.length; i++) {
if (!rgbEqual(a.ansi[i], b.ansi[i])) {
return false
}
}
return true
}
function isRgbChannel(value: unknown): value is number {
return typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 255
}
function validateRgbTriple(value: unknown): TerminalViewRgb | null {
if (!Array.isArray(value) || value.length !== 3) {
return null
}
const [r, g, b] = value
if (!isRgbChannel(r) || !isRgbChannel(g) || !isRgbChannel(b)) {
return null
}
return [r, g, b]
}
/** IPC-boundary validation for the `pty:terminalViewAttributes` push. Returns
* a normalized copy or null — main must never store a malformed palette (a
* wrong color reply is worse than silence, the OSC-11 lesson). */
export function validateTerminalViewAttributes(payload: unknown): TerminalViewAttributes | null {
if (typeof payload !== 'object' || payload === null) {
return null
}
const candidate = payload as Record<string, unknown>
const foreground = validateRgbTriple(candidate.foreground)
const background = validateRgbTriple(candidate.background)
const cursor = validateRgbTriple(candidate.cursor)
if (!foreground || !background || !cursor) {
return null
}
if (!Array.isArray(candidate.ansi) || candidate.ansi.length !== TERMINAL_VIEW_ANSI_COLOR_COUNT) {
return null
}
const ansi: TerminalViewRgb[] = []
for (const entry of candidate.ansi) {
const triple = validateRgbTriple(entry)
if (!triple) {
return null
}
ansi.push(triple)
}
if (candidate.colorSchemeMode !== 'dark' && candidate.colorSchemeMode !== 'light') {
return null
}
if (
candidate.cursorStyle !== 'bar' &&
candidate.cursorStyle !== 'block' &&
candidate.cursorStyle !== 'underline'
) {
return null
}
if (typeof candidate.cursorBlink !== 'boolean') {
return null
}
return {
foreground,
background,
cursor,
ansi,
colorSchemeMode: candidate.colorSchemeMode,
cursorStyle: candidate.cursorStyle,
cursorBlink: candidate.cursorBlink
}
}