Files
orca/src/shared/grok-session-path-lookup-queue.ts
T
BingZandJinwoo Hong 96d1fa1d62 fix(grok): clipboard, native chat, hooks, sessions, ConPTY KKP (#7944)
* fix(grok): restore clipboard and native-chat parity

Grok CLI already supports argv prompts, OSC 52 copy, and image paste chips.
Orca was blocking those paths: stdin-after-start keystroke injection, OSC 52
writes default-off, image-attachment denylist, and native-chat allowlist.

- Launch Grok with positional argv prompts
- Default OSC 52 TUI clipboard writes on (still user-toggleable)
- Treat Grok as image-attachment capable
- Parse ~/.grok/.../chat_history.jsonl for native chat

OSC 52 clipboard *query* remains ignored by design (host clipboard exfil risk);
xAI docs only require OSC 52 write for remote copy.

* fix(grok): sync OSC 52 docs and locale catalog with default-on

Update terminalAllowOsc52Clipboard type docs for the true default, and
refresh locale strings so settings UI mentions Grok alongside other TUIs.

* fix(grok): tool hook matcher, StopFailure, previews, AskUser waiting

Grok tool-event matchers are real regexes; bare `*` failed as match-all.
Install `.*` for Pre/Post tool hooks, add StopFailure for API-error ends,
recognize Grok-native tool input keys, and map ask_user_question PreToolUse
to waiting with interactivePrompt (Kimi-style live card path).

* fix(grok): resolve chat_history under GROK_HOME and long-cwd layouts

Centralize Grok session path helpers so hooks and native-chat honor
GROK_HOME and find chat_history.jsonl by session id when the cwd group
is slug-encoded (encoded name > 255 bytes) instead of only
encodeURIComponent(cwd).

* fix(terminal): keep Kitty keyboard for Grok on Windows ConPTY

Local Windows ConPTY withholds KKP so CSI-u-blind CLIs (e.g. Antigravity)
keep Enter/nav working (#2434). Grok needs KKP for Ctrl+Enter interject and
modified-Enter newline chords; blanking the advertisement for Orca-launched
Grok left those actions broken.

- Prefer KKP when tuiAgent is grok despite ConPTY withhold
- Wire launchAgent from tab/startup into keyboard protocol options

* fix(grok): restore OSC52 default-off, split decoders, honor GROK_HOME hooks

- Keep terminalAllowOsc52Clipboard default false (clipboard exfil risk)
- Split transcript-line-decoders under max-lines without suppressions
- Install local Grok hooks under resolveGrokHomeDir() / GROK_HOME

* refactor(grok): share CLI home resolution

* fix(grok): harden terminal and native chat integration

* test(grok): align CI coverage with native chat support

---------

Co-authored-by: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com>
2026-07-10 13:16:55 -07:00

159 lines
4.4 KiB
TypeScript

import { resolve } from 'node:path'
export const GROK_SESSION_PATH_CACHE_MAX_ENTRIES = 64
export const GROK_SESSION_SCAN_ACTIVE_ROOT_MAX = 4
export const GROK_SESSION_SCAN_QUEUE_MAX_ENTRIES = 64
export type GrokSessionPathScanner = (
sessionsDir: string,
sessionId: string,
maxGroupEntries: number
) => Promise<string | null>
type PendingLookup = {
key: string
rootKey: string
sessionsDir: string
sessionId: string
maxGroupEntries: number
resolve: (path: string | null) => void
}
export class GrokSessionPathLookupQueue {
private readonly successfulPaths = new Map<string, string>()
private readonly inflight = new Map<string, Promise<string | null>>()
private readonly activeRoots = new Set<string>()
private readonly pending: PendingLookup[] = []
private scanner: GrokSessionPathScanner
constructor(private readonly defaultScanner: GrokSessionPathScanner) {
this.scanner = defaultScanner
}
getCached(sessionsDir: string, sessionId: string): string | null {
const key = this.lookupKey(sessionsDir, sessionId)
const cached = this.successfulPaths.get(key)
if (!cached) {
return null
}
this.successfulPaths.delete(key)
this.successfulPaths.set(key, cached)
return cached
}
find(sessionsDir: string, sessionId: string, maxGroupEntries: number): Promise<string | null> {
const key = this.lookupKey(sessionsDir, sessionId)
const cached = this.getCached(sessionsDir, sessionId)
if (cached) {
return Promise.resolve(cached)
}
const existing = this.inflight.get(key)
if (existing) {
return existing
}
const rootKey = this.rootKey(sessionsDir)
let resolveLookup: (path: string | null) => void = () => undefined
const lookup = new Promise<string | null>((resolvePromise) => {
resolveLookup = resolvePromise
})
const pending = {
key,
rootKey,
sessionsDir,
sessionId,
maxGroupEntries,
resolve: resolveLookup
}
if (this.mustQueue(rootKey)) {
if (this.pending.length >= GROK_SESSION_SCAN_QUEUE_MAX_ENTRIES) {
return Promise.resolve(null)
}
this.inflight.set(key, lookup)
this.pending.push(pending)
this.drain()
return lookup
}
this.inflight.set(key, lookup)
this.start(pending)
return lookup
}
clearForTests(): void {
this.successfulPaths.clear()
this.inflight.clear()
this.activeRoots.clear()
for (const pending of this.pending.splice(0)) {
pending.resolve(null)
}
this.scanner = this.defaultScanner
}
setScannerForTests(scanner: GrokSessionPathScanner): void {
this.scanner = scanner
}
private rootKey(sessionsDir: string): string {
const root = resolve(sessionsDir)
return process.platform === 'win32' ? root.toLowerCase() : root
}
private lookupKey(sessionsDir: string, sessionId: string): string {
return `${this.rootKey(sessionsDir)}\0${sessionId}`
}
private mustQueue(rootKey: string): boolean {
return (
this.pending.length > 0 ||
this.activeRoots.has(rootKey) ||
this.activeRoots.size >= GROK_SESSION_SCAN_ACTIVE_ROOT_MAX
)
}
private cache(key: string, path: string): void {
this.successfulPaths.delete(key)
this.successfulPaths.set(key, path)
while (this.successfulPaths.size > GROK_SESSION_PATH_CACHE_MAX_ENTRIES) {
const oldest = this.successfulPaths.keys().next().value
if (typeof oldest !== 'string') {
return
}
this.successfulPaths.delete(oldest)
}
}
private start(pending: PendingLookup): void {
this.activeRoots.add(pending.rootKey)
void (async () => {
try {
const path = await this.scanner(
pending.sessionsDir,
pending.sessionId,
pending.maxGroupEntries
)
if (path) {
this.cache(pending.key, path)
}
pending.resolve(path)
} catch {
pending.resolve(null)
} finally {
this.activeRoots.delete(pending.rootKey)
this.inflight.delete(pending.key)
this.drain()
}
})()
}
private drain(): void {
while (this.pending.length > 0 && this.activeRoots.size < GROK_SESSION_SCAN_ACTIVE_ROOT_MAX) {
const next = this.pending[0]
// Why: strict FIFO avoids starving repeated lookups for one sessions root.
if (this.activeRoots.has(next.rootKey)) {
return
}
this.pending.shift()
this.start(next)
}
}
}