Hide Pi thinking in Orca overlays

This commit is contained in:
Neil 2026-05-30 14:25:34 -07:00 committed by GitHub
parent 2e3f29200f
commit d23cd0e451
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 185 additions and 1 deletions

View File

@ -59,6 +59,18 @@ describe('PiTitlebarExtensionService', () => {
mkdirSync(join(piHome, 'sessions'), { recursive: true })
writeFileSync(join(piHome, 'sessions', 'session-1.json'), '{}')
writeFileSync(join(piHome, 'auth.json'), 'secret token')
writeFileSync(
join(piHome, 'settings.json'),
JSON.stringify({
defaultProvider: 'amazon-bedrock',
hideThinkingBlock: false,
packages: ['npm:pi-web-access'],
terminal: {
showImages: false,
clearOnShrink: false
}
})
)
})
afterEach(() => {
@ -79,6 +91,15 @@ describe('PiTitlebarExtensionService', () => {
'user extension'
)
expect(readFileSync(join(piHome, 'sessions', 'session-1.json'), 'utf-8')).toBe('{}')
expect(JSON.parse(readFileSync(join(piHome, 'settings.json'), 'utf-8'))).toEqual({
defaultProvider: 'amazon-bedrock',
hideThinkingBlock: false,
packages: ['npm:pi-web-access'],
terminal: {
showImages: false,
clearOnShrink: false
}
})
}
it('buildPtyEnv mirrors the user agent dir into an overlay under userData', () => {
@ -101,6 +122,17 @@ describe('PiTitlebarExtensionService', () => {
expect(statusExtensionSource).toContain('/hook/pi')
expect(statusExtensionSource).toContain('process.title')
expect(statusExtensionSource).toContain("return '/hook/omp'")
expect(
JSON.parse(readFileSync(join(env.PI_CODING_AGENT_DIR!, 'settings.json'), 'utf-8'))
).toEqual({
defaultProvider: 'amazon-bedrock',
hideThinkingBlock: true,
packages: ['npm:pi-web-access'],
terminal: {
showImages: false,
clearOnShrink: true
}
})
// User's top-level resources are reachable via the overlay.
expect(existsSync(join(env.PI_CODING_AGENT_DIR!, 'skills', 'my-skill', 'SKILL.md'))).toBe(true)
expect(existsSync(join(env.PI_CODING_AGENT_DIR!, 'auth.json'))).toBe(true)
@ -264,6 +296,12 @@ describe('PiTitlebarExtensionService', () => {
'orca-prefill.ts',
'orca-titlebar-spinner.ts'
])
expect(
JSON.parse(readFileSync(join(env.PI_CODING_AGENT_DIR!, 'settings.json'), 'utf-8'))
).toEqual({
hideThinkingBlock: true,
terminal: { clearOnShrink: true }
})
} finally {
homedirOverride.current = ''
rmSync(join(userDataDir, 'omp-agent-overlays', 'pty-omp-empty'), {

View File

@ -1,4 +1,4 @@
import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'fs'
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'fs'
import { homedir } from 'os'
import { basename, join } from 'path'
import { app } from 'electron'
@ -11,6 +11,7 @@ import {
mirrorEntry,
safeRemoveOverlay
} from '../pty/overlay-mirror'
import { mergePiOverlayUiSettings } from '../../shared/pi-overlay-ui-settings'
import type { PiAgentKind } from '../../shared/pi-agent-kind'
// Why: the Pi test suite imports `isSafeDescendCandidate` from this module's
@ -22,6 +23,7 @@ export const isSafeDescendCandidate = sharedIsSafeDescendCandidate
const ORCA_PI_EXTENSION_FILE = 'orca-titlebar-spinner.ts'
const ORCA_PI_PREFILL_EXTENSION_FILE = 'orca-prefill.ts'
const PI_AGENT_SUBDIR = 'agent'
const PI_AGENT_SETTINGS_FILE = 'settings.json'
// Why: each agent owns its own overlay tree so OMP launches never touch
// Pi's overlay dir (and vice versa). Shadowing one inside the other would
@ -175,6 +177,10 @@ export class PiTitlebarExtensionService {
for (const entry of readdirSync(sourceAgentDir, { withFileTypes: true })) {
const sourcePath = join(sourceAgentDir, entry.name)
if (entry.name === PI_AGENT_SETTINGS_FILE) {
continue
}
if (entry.name === 'extensions' && entry.isDirectory()) {
const overlayExtensionsDir = join(overlayDir, 'extensions')
mkdirSync(overlayExtensionsDir, { recursive: true })
@ -195,6 +201,29 @@ export class PiTitlebarExtensionService {
}
}
private readPiSettings(sourceAgentDir: string): unknown {
const settingsPath = join(sourceAgentDir, PI_AGENT_SETTINGS_FILE)
if (!existsSync(settingsPath)) {
return {}
}
try {
return JSON.parse(readFileSync(settingsPath, 'utf8'))
} catch {
return {}
}
}
private writeOverlaySettings(sourceAgentDir: string, overlayDir: string): void {
// Why: settings.json is a real overlay file, not a mirror, so Orca can
// apply UI-only safeguards without modifying the user's Pi / OMP config.
const settings = mergePiOverlayUiSettings(this.readPiSettings(sourceAgentDir))
writeFileSync(
join(overlayDir, PI_AGENT_SETTINGS_FILE),
`${JSON.stringify(settings, null, 2)}\n`
)
}
buildPtyEnv(
ptyId: string,
existingAgentDir: string | undefined,
@ -218,6 +247,7 @@ export class PiTitlebarExtensionService {
try {
mkdirSync(overlayDir, { recursive: true })
this.mirrorAgentDir(sourceAgentDir, overlayDir)
this.writeOverlaySettings(sourceAgentDir, overlayDir)
const extensionsDir = join(overlayDir, 'extensions')
mkdirSync(extensionsDir, { recursive: true })

View File

@ -104,6 +104,17 @@ describe('PluginOverlayManager', () => {
writeFileSync(join(piAgentDir, 'auth.json'), 'secret token')
writeFileSync(join(piAgentDir, 'skills', 'my-skill', 'SKILL.md'), 'critical user skill')
writeFileSync(join(piAgentDir, 'extensions', 'user-ext', 'ext.ts'), 'user extension')
writeFileSync(
join(piAgentDir, 'settings.json'),
JSON.stringify({
defaultProvider: 'amazon-bedrock',
hideThinkingBlock: false,
terminal: {
showImages: false,
clearOnShrink: false
}
})
)
manager.setSources({ piExtensionSource: '// pi extension' })
const dir = manager.materializePi('tab-pi:0')
@ -120,6 +131,22 @@ describe('PluginOverlayManager', () => {
'orca-agent-status.ts',
'user-ext'
])
expect(JSON.parse(readFileSync(join(dir!, 'settings.json'), 'utf8'))).toEqual({
defaultProvider: 'amazon-bedrock',
hideThinkingBlock: true,
terminal: {
showImages: false,
clearOnShrink: true
}
})
expect(JSON.parse(readFileSync(join(piAgentDir, 'settings.json'), 'utf8'))).toEqual({
defaultProvider: 'amazon-bedrock',
hideThinkingBlock: false,
terminal: {
showImages: false,
clearOnShrink: false
}
})
})
it('mirrors a preexisting remote Pi agent dir instead of the default', () => {
@ -208,6 +235,10 @@ describe('PluginOverlayManager', () => {
expect(existsSync(join(dir!, 'auth.json'))).toBe(false)
const overlayExtensions = readdirSync(join(dir!, 'extensions')).sort()
expect(overlayExtensions).toEqual(['orca-agent-status.ts'])
expect(JSON.parse(readFileSync(join(dir!, 'settings.json'), 'utf8'))).toEqual({
hideThinkingBlock: true,
terminal: { clearOnShrink: true }
})
})
})

View File

@ -20,6 +20,7 @@ import { createHash } from 'crypto'
import {
existsSync,
mkdirSync,
readFileSync,
readdirSync,
realpathSync,
statSync,
@ -29,6 +30,7 @@ import {
import { homedir } from 'os'
import { basename, join } from 'path'
import { mirrorEntry, safeRemoveOverlay } from '../main/pty/overlay-mirror'
import { mergePiOverlayUiSettings } from '../shared/pi-overlay-ui-settings'
import type { PiAgentKind } from '../shared/pi-agent-kind'
const RELAY_HOOKS_DIR = '.orca-relay'
@ -40,6 +42,7 @@ const PI_OVERLAY_SUBDIR_BY_KIND: Record<PiAgentKind, string> = {
const OPENCODE_PLUGIN_FILE = 'orca-opencode-status.js'
const PI_EXTENSION_FILE = 'orca-agent-status.ts'
const PI_AGENT_SUBDIR = 'agent'
const PI_AGENT_SETTINGS_FILE = 'settings.json'
// Why: source-dir resolution is keyed off the launching agent (Pi or OMP).
// Both consume `PI_CODING_AGENT_DIR` but default to different `~/.<kind>/agent`
// paths on the remote disk. The renderer-chosen launch command flows in via
@ -223,6 +226,10 @@ export class PluginOverlayManager {
for (const entry of readdirSync(sourceAgentDir, { withFileTypes: true })) {
const sourcePath = join(sourceAgentDir, entry.name)
if (entry.name === PI_AGENT_SETTINGS_FILE) {
continue
}
if (entry.name === 'extensions') {
const isSymlink = entry.isSymbolicLink()
let isLinkPointingToDir = false
@ -255,6 +262,29 @@ export class PluginOverlayManager {
}
}
private readPiSettings(sourceAgentDir: string): unknown {
const settingsPath = join(sourceAgentDir, PI_AGENT_SETTINGS_FILE)
if (!existsSync(settingsPath)) {
return {}
}
try {
return JSON.parse(readFileSync(settingsPath, 'utf8'))
} catch {
return {}
}
}
private writePiOverlaySettings(sourceAgentDir: string, overlayDir: string): void {
// Why: relay overlays run on the remote disk, but the same Pi UI guardrails
// need to stay overlay-local so SSH sessions do not mutate user config.
const settings = mergePiOverlayUiSettings(this.readPiSettings(sourceAgentDir))
writeFileSync(
join(overlayDir, PI_AGENT_SETTINGS_FILE),
`${JSON.stringify(settings, null, 2)}\n`
)
}
/** Materialize the Pi extension overlay for `id` and return the directory
* path that should be assigned to PI_CODING_AGENT_DIR. `kind` selects which
* Pi-compatible agent's source dir to mirror when `existingAgentDir` is
@ -281,6 +311,7 @@ export class PluginOverlayManager {
return null
}
this.mirrorPiAgentDir(sourceAgentDir, dir)
this.writePiOverlaySettings(sourceAgentDir, dir)
const extensionsDir = join(dir, 'extensions')
mkdirSync(extensionsDir, { recursive: true })
writeFileSync(join(extensionsDir, PI_EXTENSION_FILE), extensionSource)

View File

@ -0,0 +1,37 @@
import { describe, expect, it } from 'vitest'
import { mergePiOverlayUiSettings } from './pi-overlay-ui-settings'
describe('mergePiOverlayUiSettings', () => {
it('preserves user settings while forcing Orca-only Pi UI safety settings', () => {
const merged = mergePiOverlayUiSettings({
defaultProvider: 'amazon-bedrock',
hideThinkingBlock: false,
packages: ['npm:pi-web-access'],
terminal: {
showImages: false,
clearOnShrink: false
}
})
expect(merged).toEqual({
defaultProvider: 'amazon-bedrock',
hideThinkingBlock: true,
packages: ['npm:pi-web-access'],
terminal: {
showImages: false,
clearOnShrink: true
}
})
})
it('creates a valid settings object from malformed shapes', () => {
expect(mergePiOverlayUiSettings(null)).toEqual({
hideThinkingBlock: true,
terminal: { clearOnShrink: true }
})
expect(mergePiOverlayUiSettings({ terminal: 'compact' })).toEqual({
hideThinkingBlock: true,
terminal: { clearOnShrink: true }
})
})
})

View File

@ -0,0 +1,17 @@
const PI_OVERLAY_HIDE_THINKING_BLOCK = true
const PI_OVERLAY_CLEAR_ON_SHRINK = true
function isPlainRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
export function mergePiOverlayUiSettings(settings: unknown): Record<string, unknown> {
const merged = isPlainRecord(settings) ? { ...settings } : {}
const terminal = isPlainRecord(merged.terminal) ? { ...merged.terminal } : {}
terminal.clearOnShrink = PI_OVERLAY_CLEAR_ON_SHRINK
merged.terminal = terminal
merged.hideThinkingBlock = PI_OVERLAY_HIDE_THINKING_BLOCK
return merged
}