mirror of
https://github.com/stablyai/orca.git
synced 2026-09-24 08:02:33 +00:00
* 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>
159 lines
4.4 KiB
TypeScript
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)
|
|
}
|
|
}
|
|
}
|