Hide Pi thinking in Orca overlays
This commit is contained in:
parent
2e3f29200f
commit
d23cd0e451
|
|
@ -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'), {
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue