From d23cd0e451e90e24d4ab68a7e0b6866b89bf61de Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 30 May 2026 14:25:34 -0700 Subject: [PATCH] Hide Pi thinking in Orca overlays --- .../pi/titlebar-extension-service.test.ts | 38 +++++++++++++++++++ src/main/pi/titlebar-extension-service.ts | 32 +++++++++++++++- src/relay/plugin-overlay.test.ts | 31 +++++++++++++++ src/relay/plugin-overlay.ts | 31 +++++++++++++++ src/shared/pi-overlay-ui-settings.test.ts | 37 ++++++++++++++++++ src/shared/pi-overlay-ui-settings.ts | 17 +++++++++ 6 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 src/shared/pi-overlay-ui-settings.test.ts create mode 100644 src/shared/pi-overlay-ui-settings.ts diff --git a/src/main/pi/titlebar-extension-service.test.ts b/src/main/pi/titlebar-extension-service.test.ts index b5d9259ba..3a8f93c1f 100644 --- a/src/main/pi/titlebar-extension-service.test.ts +++ b/src/main/pi/titlebar-extension-service.test.ts @@ -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'), { diff --git a/src/main/pi/titlebar-extension-service.ts b/src/main/pi/titlebar-extension-service.ts index 16282341a..071e19498 100644 --- a/src/main/pi/titlebar-extension-service.ts +++ b/src/main/pi/titlebar-extension-service.ts @@ -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 }) diff --git a/src/relay/plugin-overlay.test.ts b/src/relay/plugin-overlay.test.ts index 2adad59e5..422f0b0ce 100644 --- a/src/relay/plugin-overlay.test.ts +++ b/src/relay/plugin-overlay.test.ts @@ -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 } + }) }) }) diff --git a/src/relay/plugin-overlay.ts b/src/relay/plugin-overlay.ts index f4904898f..745664a96 100644 --- a/src/relay/plugin-overlay.ts +++ b/src/relay/plugin-overlay.ts @@ -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 = { 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 `~/./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) diff --git a/src/shared/pi-overlay-ui-settings.test.ts b/src/shared/pi-overlay-ui-settings.test.ts new file mode 100644 index 000000000..b0ab7a595 --- /dev/null +++ b/src/shared/pi-overlay-ui-settings.test.ts @@ -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 } + }) + }) +}) diff --git a/src/shared/pi-overlay-ui-settings.ts b/src/shared/pi-overlay-ui-settings.ts new file mode 100644 index 000000000..cf420a16a --- /dev/null +++ b/src/shared/pi-overlay-ui-settings.ts @@ -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 { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +export function mergePiOverlayUiSettings(settings: unknown): Record { + 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 +}