mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 08:02:21 +00:00
fix(jcode): stop the OSC color skip from crashing every pane connect
Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>
This commit is contained in:
committed by
Neil
co-authored by
czzczz
parent
33cab243ab
commit
7fb201f60f
@@ -345,6 +345,7 @@ describe('registerPtyHandlers', () => {
|
||||
const tabId = 'tab-1'
|
||||
const paneKey = makePaneKey(tabId, leafId)
|
||||
handlers.clear()
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the suite's mock BrowserWindow, widened the same way every other registerPtyHandlers call in this file does; the handler only touches webContents.send.
|
||||
registerPtyHandlers(mainWindow as never)
|
||||
await handlers.get('pty:spawn')!(null, {
|
||||
cols: 80,
|
||||
@@ -354,12 +355,13 @@ describe('registerPtyHandlers', () => {
|
||||
leafId,
|
||||
worktreeId: 'wt-1'
|
||||
})
|
||||
const spawnOptions = spawnMock.mock.calls.at(-1)![2] as { env: Record<string, string> }
|
||||
const spawnedEnv: Record<string, string | undefined> =
|
||||
spawnMock.mock.calls.at(-1)?.[2]?.env ?? {}
|
||||
// Why: buildJcodeRuntimeDirEnv intentionally omits the var on win32.
|
||||
if (shouldInjectJcodeRuntimeDir(process.platform)) {
|
||||
expect(spawnOptions.env.JCODE_RUNTIME_DIR).toBe(buildJcodeRuntimeDir(paneKey))
|
||||
expect(spawnedEnv.JCODE_RUNTIME_DIR).toBe(buildJcodeRuntimeDir(paneKey))
|
||||
} else {
|
||||
expect(spawnOptions.env.JCODE_RUNTIME_DIR).toBeUndefined()
|
||||
expect(spawnedEnv.JCODE_RUNTIME_DIR).toBeUndefined()
|
||||
}
|
||||
})
|
||||
it('strips inherited Claude child-session stamps from a local spawn env', async () => {
|
||||
|
||||
@@ -16,19 +16,26 @@ export function resetJcodeDaemonPrewarmForTests(): void {
|
||||
prewarmedRuntimeDirs.clear()
|
||||
}
|
||||
|
||||
/** The runtime dir to warm, or null when this pane is not a local jcode launch. */
|
||||
export function resolveJcodePrewarmRuntimeDir(args: {
|
||||
launchAgent?: string
|
||||
runtimeDir?: string
|
||||
platform?: NodeJS.Platform
|
||||
}): string | null {
|
||||
// Why non-Windows only: the runtime dir is a unix-socket directory, and Orca
|
||||
// only stamps it off Windows (see shouldInjectJcodeRuntimeDir).
|
||||
if (args.launchAgent !== 'jcode' || (args.platform ?? process.platform) === 'win32') {
|
||||
return null
|
||||
}
|
||||
return args.runtimeDir !== undefined && args.runtimeDir.length > 0 ? args.runtimeDir : null
|
||||
}
|
||||
|
||||
export function shouldPrewarmJcodeDaemon(args: {
|
||||
launchAgent?: string
|
||||
runtimeDir?: string
|
||||
platform?: NodeJS.Platform
|
||||
}): boolean {
|
||||
// Why non-Windows only: the runtime dir is a unix-socket directory, and Orca
|
||||
// only stamps it off Windows (see shouldInjectJcodeRuntimeDir).
|
||||
return (
|
||||
args.launchAgent === 'jcode' &&
|
||||
typeof args.runtimeDir === 'string' &&
|
||||
args.runtimeDir.length > 0 &&
|
||||
(args.platform ?? process.platform) !== 'win32'
|
||||
)
|
||||
return resolveJcodePrewarmRuntimeDir(args) !== null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,10 +52,10 @@ export function prewarmJcodeDaemon(args: {
|
||||
env?: Record<string, string>
|
||||
platform?: NodeJS.Platform
|
||||
}): boolean {
|
||||
if (!shouldPrewarmJcodeDaemon(args) || prewarmedRuntimeDirs.has(args.runtimeDir as string)) {
|
||||
const runtimeDir = resolveJcodePrewarmRuntimeDir(args)
|
||||
if (runtimeDir === null || prewarmedRuntimeDirs.has(runtimeDir)) {
|
||||
return false
|
||||
}
|
||||
const runtimeDir = args.runtimeDir as string
|
||||
prewarmedRuntimeDirs.add(runtimeDir)
|
||||
try {
|
||||
const child = spawnProcess({
|
||||
|
||||
@@ -7,7 +7,7 @@ import { dirname, join } from 'node:path'
|
||||
|
||||
const { homedirMock } = vi.hoisted(() => ({ homedirMock: vi.fn<() => string>() }))
|
||||
vi.mock('os', async () => {
|
||||
const actual = (await vi.importActual('os')) as Record<string, unknown>
|
||||
const actual = await vi.importActual<typeof import('os')>('os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import()
|
||||
return { ...actual, homedir: homedirMock }
|
||||
})
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ const { homedirMock } = vi.hoisted(() => ({
|
||||
}))
|
||||
|
||||
vi.mock('os', async () => {
|
||||
const actual = (await vi.importActual('os')) as Record<string, unknown>
|
||||
const actual = await vi.importActual<typeof import('os')>('os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import()
|
||||
return { ...actual, homedir: homedirMock }
|
||||
})
|
||||
|
||||
|
||||
+24
-17
@@ -134,6 +134,19 @@ function expectNoGlobalAtlasRecovery(): void {
|
||||
expect(resetAndRefreshAllTerminalWebglAtlases).not.toHaveBeenCalled()
|
||||
}
|
||||
|
||||
// Why: connectPanePty takes the real pane/manager/deps types and every test in this file
|
||||
// passes structural fakes. Funnelling the widening through one helper keeps the casts in
|
||||
// a single reviewable place instead of three per test.
|
||||
function connectFakePane(
|
||||
connect: (pane: never, manager: never, deps: never) => { dispose: () => void },
|
||||
pane: unknown,
|
||||
manager: unknown,
|
||||
deps: Parameters<typeof createDeps>[0]
|
||||
): { dispose: () => void } {
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the three values are this suite's structural fakes for pane/manager/deps; connectPanePty only reads the fields they define.
|
||||
return connect(pane as never, manager as never, createDeps(deps) as never)
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
@@ -552,24 +565,18 @@ describe('connectPanePty', () => {
|
||||
})
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
const pane = createPane(1)
|
||||
const manager = createManager(1)
|
||||
const binding = connectPanePty(
|
||||
pane as never,
|
||||
manager as never,
|
||||
createDeps({
|
||||
isVisibleRef: { current: false },
|
||||
startup: {
|
||||
command: 'jcode',
|
||||
launchAgent: 'jcode',
|
||||
telemetry: {
|
||||
agent_kind: 'jcode',
|
||||
launch_source: 'tab_bar_quick_launch',
|
||||
request_kind: 'new'
|
||||
}
|
||||
const binding = connectFakePane(connectPanePty, createPane(1), createManager(1), {
|
||||
isVisibleRef: { current: false },
|
||||
startup: {
|
||||
command: 'jcode',
|
||||
launchAgent: 'jcode',
|
||||
telemetry: {
|
||||
agent_kind: 'jcode',
|
||||
launch_source: 'tab_bar_quick_launch',
|
||||
request_kind: 'new'
|
||||
}
|
||||
}) as never
|
||||
)
|
||||
}
|
||||
})
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
expect(capturedDataCallback.current).not.toBeNull()
|
||||
|
||||
@@ -17,6 +17,11 @@ import { resolveCommittedTitleAgentType } from '@/lib/pane-agent-evidence'
|
||||
import type { TuiAgent } from '../../../../../shared/tui-agent'
|
||||
import { isTuiAgent, TUI_AGENT_CONFIG } from '../../../../../shared/tui-agent-config'
|
||||
|
||||
import {
|
||||
paneShouldAnswerOscColorQueries,
|
||||
resolvePaneLaunchAgentCandidate,
|
||||
type PaneLaunchAgentPaneSlice
|
||||
} from './pane-launch-agent-candidate'
|
||||
import type { ConnectPanePtySession } from './connect-pane-pty-session'
|
||||
|
||||
/** Pane agent identity, foreground-agent sampling, and command lifecycle handling. */
|
||||
@@ -28,22 +33,18 @@ export function installPaneAgentIdentity(session: ConnectPanePtySession): void {
|
||||
const entry = state.agentStatusByPaneKey[session.cacheKey]
|
||||
return entry?.state !== 'done' && Boolean(agentTypeToIconAgent(entry?.agentType))
|
||||
}
|
||||
// Why: one ladder for both launch-agent signals; a second copy could drift.
|
||||
// Why the shared module: the same ladder answers the 133;D guard, the visible-pane
|
||||
// resampler, and the OSC color-reply skip, and it is the one piece of this installer
|
||||
// that is a pure rule worth testing on its own.
|
||||
const panePane = (): PaneLaunchAgentPaneSlice => ({
|
||||
worktreeId: session.deps.worktreeId,
|
||||
tabId: session.deps.tabId,
|
||||
paneKey: session.cacheKey,
|
||||
startup: session.paneStartup
|
||||
})
|
||||
const resolveLaunchAgentCandidate = (
|
||||
state: ReturnType<typeof useAppStore.getState>
|
||||
): string | undefined => {
|
||||
const tab = (state.tabsByWorktree[session.deps.worktreeId] ?? []).find(
|
||||
(candidate) => candidate.id === session.deps.tabId
|
||||
)
|
||||
const registeredLaunchAgent =
|
||||
state.agentLaunchConfigByPaneKey[session.cacheKey]?.identity.agentType
|
||||
return (
|
||||
tab?.launchAgent ??
|
||||
session.paneStartup?.launchAgent ??
|
||||
session.paneStartup?.initialAgentStatus?.agent ??
|
||||
(isTuiAgent(registeredLaunchAgent) ? registeredLaunchAgent : undefined)
|
||||
)
|
||||
}
|
||||
): string | undefined => resolvePaneLaunchAgentCandidate(state, panePane())
|
||||
session.paneExpectsLaunchAgent = (state: ReturnType<typeof useAppStore.getState>): boolean =>
|
||||
Boolean(resolveLaunchAgentCandidate(state))
|
||||
// Why: the concrete TUI agent a fresh spawn is expected to launch, used to seed
|
||||
@@ -57,14 +58,14 @@ export function installPaneAgentIdentity(session: ConnectPanePtySession): void {
|
||||
// composer is ready and render the reply as pre-typed text (the main-side
|
||||
// startup ingress already skips it, the renderer must skip the answer too).
|
||||
session.shouldAnswerPaneOscColorQueries = (): boolean =>
|
||||
session.resolveExpectedLaunchTuiAgent() !== 'jcode'
|
||||
paneShouldAnswerOscColorQueries(useAppStore.getState(), panePane())
|
||||
// Why: a launched/hook-known agent pane must confirm — not trust — a 133;D so a
|
||||
// full-screen agent's leaked nested-shell 133;D can't clear its tab identity,
|
||||
// even on a restore where no command-start read has recorded evidence yet.
|
||||
session.paneHasKnownAgentIdentity = (): boolean => {
|
||||
const state = useAppStore.getState()
|
||||
const registeredLaunchAgent =
|
||||
state.agentLaunchConfigByPaneKey[session.cacheKey]?.identity.agentType
|
||||
state.agentLaunchConfigByPaneKey[session.cacheKey]?.identity?.agentType
|
||||
return (
|
||||
Boolean(state.paneForegroundAgentByPaneKey[session.cacheKey]?.agent) ||
|
||||
session.paneHasLiveHookAgentIcon(state) ||
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
paneShouldAnswerOscColorQueries,
|
||||
resolvePaneLaunchAgentCandidate,
|
||||
resolvePaneLaunchTuiAgent,
|
||||
type PaneLaunchAgentPaneSlice,
|
||||
type PaneLaunchAgentStoreSlice
|
||||
} from './pane-launch-agent-candidate'
|
||||
|
||||
const PANE: PaneLaunchAgentPaneSlice = {
|
||||
worktreeId: 'wt-1',
|
||||
tabId: 'tab-1',
|
||||
paneKey: 'tab-1:leaf-1'
|
||||
}
|
||||
|
||||
function store(overrides: Partial<PaneLaunchAgentStoreSlice> = {}): PaneLaunchAgentStoreSlice {
|
||||
return { tabsByWorktree: {}, agentLaunchConfigByPaneKey: {}, ...overrides }
|
||||
}
|
||||
|
||||
describe('pane launch-agent candidate', () => {
|
||||
it('survives a launch config registered before its identity lands', () => {
|
||||
// Why: the OSC color-reply skip made this ladder run on EVERY pane connect, and
|
||||
// reading through a missing `identity` threw "Cannot read properties of undefined
|
||||
// (reading 'agentType')" — 24 failures across 8 connectPanePty suites.
|
||||
const state = store({ agentLaunchConfigByPaneKey: { 'tab-1:leaf-1': {} } })
|
||||
expect(resolvePaneLaunchAgentCandidate(state, PANE)).toBeUndefined()
|
||||
expect(resolvePaneLaunchTuiAgent(state, PANE)).toBeNull()
|
||||
expect(paneShouldAnswerOscColorQueries(state, PANE)).toBe(true)
|
||||
})
|
||||
|
||||
it('prefers the tab launch agent over every later signal', () => {
|
||||
const state = store({ tabsByWorktree: { 'wt-1': [{ id: 'tab-1', launchAgent: 'jcode' }] } })
|
||||
const pane = { ...PANE, startup: { launchAgent: 'claude' } }
|
||||
expect(resolvePaneLaunchTuiAgent(state, pane)).toBe('jcode')
|
||||
})
|
||||
|
||||
it('falls back through startup, then initial status, then the registered config', () => {
|
||||
expect(resolvePaneLaunchTuiAgent(store(), { ...PANE, startup: { launchAgent: 'jcode' } })).toBe(
|
||||
'jcode'
|
||||
)
|
||||
expect(
|
||||
resolvePaneLaunchTuiAgent(store(), {
|
||||
...PANE,
|
||||
startup: { initialAgentStatus: { agent: 'jcode' } }
|
||||
})
|
||||
).toBe('jcode')
|
||||
expect(
|
||||
resolvePaneLaunchTuiAgent(
|
||||
store({
|
||||
agentLaunchConfigByPaneKey: { 'tab-1:leaf-1': { identity: { agentType: 'jcode' } } }
|
||||
}),
|
||||
PANE
|
||||
)
|
||||
).toBe('jcode')
|
||||
})
|
||||
|
||||
it('ignores a registered agent name that is not a known TUI agent', () => {
|
||||
const state = store({
|
||||
agentLaunchConfigByPaneKey: { 'tab-1:leaf-1': { identity: { agentType: 'not-an-agent' } } }
|
||||
})
|
||||
expect(resolvePaneLaunchAgentCandidate(state, PANE)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('skips the OSC color answer for jcode and no one else', () => {
|
||||
for (const [launchAgent, answers] of [
|
||||
['jcode', false],
|
||||
['claude', true],
|
||||
['codex', true],
|
||||
[undefined, true]
|
||||
] as const) {
|
||||
const state = store({ tabsByWorktree: { 'wt-1': [{ id: 'tab-1', launchAgent }] } })
|
||||
expect(paneShouldAnswerOscColorQueries(state, PANE)).toBe(answers)
|
||||
}
|
||||
})
|
||||
})
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { isTuiAgent } from '../../../../../shared/tui-agent-config'
|
||||
import type { TuiAgent } from '../../../../../shared/tui-agent'
|
||||
|
||||
/** The store slice the launch-agent ladder reads. */
|
||||
export type PaneLaunchAgentStoreSlice = {
|
||||
tabsByWorktree: Record<string, { id: string; launchAgent?: string }[] | undefined>
|
||||
agentLaunchConfigByPaneKey: Record<string, { identity?: { agentType?: string } } | undefined>
|
||||
}
|
||||
|
||||
export type PaneLaunchAgentPaneSlice = {
|
||||
worktreeId: string
|
||||
tabId: string
|
||||
paneKey: string
|
||||
startup?: { launchAgent?: string; initialAgentStatus?: { agent?: string } }
|
||||
}
|
||||
|
||||
/**
|
||||
* The launch agent a pane is expected to run, from the first signal that has one.
|
||||
*
|
||||
* Why one ladder: the 133;D confirmation guard, the visible-pane resampler, and the
|
||||
* OSC color-reply skip all key off "what agent is this pane", and a second copy would
|
||||
* drift. Why `identity` is optional: a launch config is registered before its identity
|
||||
* lands, and reading through it unguarded threw on every pane connect once the
|
||||
* color-reply skip started calling this on all of them.
|
||||
*/
|
||||
export function resolvePaneLaunchAgentCandidate(
|
||||
state: PaneLaunchAgentStoreSlice,
|
||||
pane: PaneLaunchAgentPaneSlice
|
||||
): string | undefined {
|
||||
const tab = (state.tabsByWorktree[pane.worktreeId] ?? []).find(
|
||||
(candidate) => candidate.id === pane.tabId
|
||||
)
|
||||
const registeredLaunchAgent = state.agentLaunchConfigByPaneKey[pane.paneKey]?.identity?.agentType
|
||||
return (
|
||||
tab?.launchAgent ??
|
||||
pane.startup?.launchAgent ??
|
||||
pane.startup?.initialAgentStatus?.agent ??
|
||||
(isTuiAgent(registeredLaunchAgent) ? registeredLaunchAgent : undefined)
|
||||
)
|
||||
}
|
||||
|
||||
/** The same ladder, narrowed to a recognized TUI agent. */
|
||||
export function resolvePaneLaunchTuiAgent(
|
||||
state: PaneLaunchAgentStoreSlice,
|
||||
pane: PaneLaunchAgentPaneSlice
|
||||
): TuiAgent | null {
|
||||
const candidate = resolvePaneLaunchAgentCandidate(state, pane)
|
||||
return isTuiAgent(candidate) ? candidate : null
|
||||
}
|
||||
|
||||
// Why: jcode paints its own theme and fires its OSC 10/11 burst before its TUI input
|
||||
// loop is ready, so the cooked reply lands in the composer as pre-typed text (the same
|
||||
// class as #12112, which fixed opencode). The main-side startup ingress already skips
|
||||
// it; the renderer's capability handlers must skip the answer too.
|
||||
export function paneShouldAnswerOscColorQueries(
|
||||
state: PaneLaunchAgentStoreSlice,
|
||||
pane: PaneLaunchAgentPaneSlice
|
||||
): boolean {
|
||||
return resolvePaneLaunchTuiAgent(state, pane) !== 'jcode'
|
||||
}
|
||||
@@ -215,7 +215,7 @@ export function installPtyInputRecovery(session: ConnectPanePtySession): void {
|
||||
sixelSupported: () =>
|
||||
resolveTerminalInlineImagesEnabled(useAppStore.getState().settings?.terminalInlineImages) &&
|
||||
terminalRendersInlineImages(session.pane.terminal),
|
||||
skipOscColorQueryReplies: !session.shouldAnswerPaneOscColorQueries(),
|
||||
skipOscColorQueryReplies: () => !session.shouldAnswerPaneOscColorQueries(),
|
||||
...(session.isNativeWindowsConpty ? { da1Response: CONPTY_DA1_RESPONSE } : {})
|
||||
})
|
||||
session.respondToTerminalPixelSizeQueries = createTerminalPixelSizeQueryResponder(
|
||||
|
||||
@@ -173,7 +173,7 @@ describe('installTerminalCapabilityReplyHandlers', () => {
|
||||
parser: term.parser,
|
||||
sendInput,
|
||||
isReplaying: () => false,
|
||||
skipOscColorQueryReplies: true
|
||||
skipOscColorQueryReplies: () => true
|
||||
})
|
||||
|
||||
try {
|
||||
|
||||
@@ -22,9 +22,11 @@ type TerminalCapabilityRepliesDeps = {
|
||||
da1Response?: string
|
||||
// Resolved per query so a live inline-images toggle changes what the next DA1 advertises.
|
||||
sixelSupported?: () => boolean
|
||||
/** Why: jcode themes itself; answering its OSC color burst can land before its
|
||||
* composer is ready and render the reply as pre-typed text. */
|
||||
skipOscColorQueryReplies?: boolean
|
||||
/** Why a thunk, like sixelSupported: the pane's launch agent may not be registered
|
||||
* in the store yet when the handlers are installed, so a boolean captured here
|
||||
* answers jcode's burst anyway. Why skip at all: jcode themes itself, and the reply
|
||||
* can land before its composer is ready and render as pre-typed text. */
|
||||
skipOscColorQueryReplies?: () => boolean
|
||||
}
|
||||
|
||||
// Adds Sixel to a DA1 response so DA1-detecting image tools emit Sixel; idempotent.
|
||||
@@ -168,7 +170,7 @@ export function installTerminalCapabilityReplyHandlers(
|
||||
if (!slots) {
|
||||
return false
|
||||
}
|
||||
if (deps.isReplaying() || deps.skipOscColorQueryReplies === true) {
|
||||
if (deps.isReplaying() || deps.skipOscColorQueryReplies?.() === true) {
|
||||
return true
|
||||
}
|
||||
return sendTerminalOscColorQueryRepliesForSlots(slots, deps.terminal, deps.sendInput)
|
||||
@@ -181,7 +183,7 @@ export function installTerminalCapabilityReplyHandlers(
|
||||
if (!slots) {
|
||||
return false
|
||||
}
|
||||
if (deps.isReplaying() || deps.skipOscColorQueryReplies === true) {
|
||||
if (deps.isReplaying() || deps.skipOscColorQueryReplies?.() === true) {
|
||||
return true
|
||||
}
|
||||
return sendTerminalOscColorQueryRepliesForSlots(slots, deps.terminal, deps.sendInput)
|
||||
|
||||
@@ -39,7 +39,7 @@ function readJcodeIntent(toolInput: unknown): string | undefined {
|
||||
if (typeof toolInput !== 'object' || toolInput === null) {
|
||||
return undefined
|
||||
}
|
||||
const intent = (toolInput as Record<string, unknown>).intent
|
||||
const intent: unknown = Reflect.get(toolInput, 'intent')
|
||||
return typeof intent === 'string' && intent.trim().length > 0 ? intent : undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -11,13 +11,11 @@ export function cleanGeneratedCommitMessage(raw: string): string {
|
||||
// answer field when the output is such an envelope.
|
||||
if (text.startsWith('{')) {
|
||||
try {
|
||||
const parsed = JSON.parse(text) as unknown
|
||||
if (
|
||||
typeof parsed === 'object' &&
|
||||
parsed !== null &&
|
||||
typeof (parsed as Record<string, unknown>).text === 'string'
|
||||
) {
|
||||
text = (parsed as Record<string, string>).text.trim()
|
||||
const parsed: unknown = JSON.parse(text)
|
||||
const answer: unknown =
|
||||
typeof parsed === 'object' && parsed !== null ? Reflect.get(parsed, 'text') : undefined
|
||||
if (typeof answer === 'string') {
|
||||
text = answer.trim()
|
||||
}
|
||||
} catch {
|
||||
// not a JSON envelope; fall through to the plain-text cleanup
|
||||
|
||||
@@ -8,7 +8,7 @@ const { homedirMock } = vi.hoisted(() => ({
|
||||
}))
|
||||
|
||||
vi.mock('os', async () => {
|
||||
const actual = (await vi.importActual('os')) as Record<string, unknown>
|
||||
const actual = await vi.importActual<typeof import('os')>('os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import()
|
||||
return { ...actual, homedir: homedirMock }
|
||||
})
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@ const JCODE_JOURNAL_CHUNK_BYTES = 64 * 1024
|
||||
const JCODE_JSON_DOC_MAX_PARSE_BYTES = 8 * 1024 * 1024
|
||||
const EMPTY_REGION = Buffer.alloc(0)
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null
|
||||
}
|
||||
|
||||
export function resolveJcodeSessionsDir(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
homeDir: string = homedir()
|
||||
@@ -58,11 +62,8 @@ function messageText(message: Record<string, unknown>): string | null {
|
||||
parts.push(block)
|
||||
continue
|
||||
}
|
||||
if (block && typeof block === 'object') {
|
||||
const text = (block as Record<string, unknown>).text
|
||||
if (typeof text === 'string') {
|
||||
parts.push(text)
|
||||
}
|
||||
if (isRecord(block) && typeof block.text === 'string') {
|
||||
parts.push(block.text)
|
||||
}
|
||||
}
|
||||
const joined = parts.join(' ').replace(/\s+/g, ' ').trim()
|
||||
@@ -99,19 +100,18 @@ function readLastUserMessageFromJournalLines(
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
if (typeof entry !== 'object' || entry === null) {
|
||||
if (!isRecord(entry)) {
|
||||
continue
|
||||
}
|
||||
const appendMessages = (entry as Record<string, unknown>).append_messages
|
||||
const appendMessages = entry.append_messages
|
||||
if (!Array.isArray(appendMessages)) {
|
||||
continue
|
||||
}
|
||||
for (let messageIndex = appendMessages.length - 1; messageIndex >= 0; messageIndex -= 1) {
|
||||
const message = appendMessages[messageIndex]
|
||||
if (typeof message !== 'object' || message === null) {
|
||||
const record = appendMessages[messageIndex]
|
||||
if (!isRecord(record)) {
|
||||
continue
|
||||
}
|
||||
const record = message as Record<string, unknown>
|
||||
if (record.role !== 'user' || isInjectedContextMessage(record)) {
|
||||
continue
|
||||
}
|
||||
@@ -214,20 +214,19 @@ function readLastUserMessageFromJson(
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(jsonPath, 'utf8')) as unknown
|
||||
if (typeof parsed !== 'object' || parsed === null) {
|
||||
const parsed: unknown = JSON.parse(readFileSync(jsonPath, 'utf8'))
|
||||
if (!isRecord(parsed)) {
|
||||
return null
|
||||
}
|
||||
const messages = (parsed as Record<string, unknown>).messages
|
||||
const messages = parsed.messages
|
||||
if (!Array.isArray(messages)) {
|
||||
return null
|
||||
}
|
||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||
const message = messages[index]
|
||||
if (typeof message !== 'object' || message === null) {
|
||||
const record = messages[index]
|
||||
if (!isRecord(record)) {
|
||||
continue
|
||||
}
|
||||
const record = message as Record<string, unknown>
|
||||
if (record.role !== 'user' || isInjectedContextMessage(record)) {
|
||||
continue
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user