From 5500830861feda4d2437719157463c590a2b4091 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:56:08 -0700 Subject: [PATCH 1/3] Native chat: let users turn off inline visuals for new chats --- .../claude/claude-structured-launch-flags.ts | 21 +-- ...native-chat-inline-visuals-setting.test.ts | 152 ++++++++++++++++++ .../native-chat-visuals-delivery.test.ts | 28 +++- .../native-chat-visuals-delivery.ts | 4 + .../runtime/orca-runtime-get-worktree-ps.ts | 1 + src/main/runtime/runtime-store-contract.ts | 1 + .../structured-agent-runtime-registrations.ts | 3 +- .../structured-agent-session-runtime.ts | 2 + .../NativeChatMarkdown.visual.test.tsx | 18 +++ .../NativeChatExperimentalSetting.test.tsx | 45 ++++++ .../NativeChatExperimentalSetting.tsx | 5 + .../NativeChatInlineVisualsSetting.tsx | 30 ++++ ...ive-chat-experimental-search-entry.test.ts | 4 + .../native-chat-experimental-search-entry.ts | 4 + src/renderer/src/i18n/locales/en.json | 3 + src/shared/default-global-settings.ts | 1 + src/shared/global-settings-types.ts | 2 + 17 files changed, 313 insertions(+), 11 deletions(-) create mode 100644 src/main/native-chat/native-chat-inline-visuals-setting.test.ts create mode 100644 src/renderer/src/components/settings/NativeChatInlineVisualsSetting.tsx diff --git a/src/main/claude/claude-structured-launch-flags.ts b/src/main/claude/claude-structured-launch-flags.ts index b3c2cefae13..32ccef04dff 100644 --- a/src/main/claude/claude-structured-launch-flags.ts +++ b/src/main/claude/claude-structured-launch-flags.ts @@ -39,21 +39,24 @@ export async function resolveClaudeLaunchFlags( deps.cliFlags?.supports(CLAUDE_THINKING_DISPLAY_FLAG, launch, budgetMs, startProbe) ?? false // Unlike the thinking display, a missed answer costs the chat its skill for its whole life, so // the plugin check waits for the probe up to its own kill time: bounded, and instant once known. + const preparedVisuals = deps.prepareVisuals?.(sessionId) ?? Promise.resolve(null) const [thinksFirst, visuals, loadsPlugins] = await Promise.all([ thinking(), - deps.prepareVisuals?.(sessionId) ?? null, - deps.prepareVisuals - ? (deps.cliFlags?.supports( - CLAUDE_PLUGIN_DIR_FLAG, - launch, - CLAUDE_CLI_FLAG_PROBE_KILL_AFTER_MS - ) ?? false) - : false + preparedVisuals, + preparedVisuals.then((visuals) => + visuals + ? (deps.cliFlags?.supports( + CLAUDE_PLUGIN_DIR_FLAG, + launch, + CLAUDE_CLI_FLAG_PROBE_KILL_AFTER_MS + ) ?? false) + : false + ) ]) // The plugin check may have outwaited the thinking budget and learned the version meanwhile; a // probe that gave none is not asked again here. const thinks = - thinksFirst || (deps.prepareVisuals ? await thinking(THINKING_RECHECK_BUDGET_MS, false) : false) + thinksFirst || (visuals ? await thinking(THINKING_RECHECK_BUDGET_MS, false) : false) return { thinkingDisplayArgs: thinks ? THINKING_DISPLAY_ARGS : {}, visuals: visuals ? { visuals, pluginDir: loadsPlugins ? visuals.skill.pluginDir : null } : null diff --git a/src/main/native-chat/native-chat-inline-visuals-setting.test.ts b/src/main/native-chat/native-chat-inline-visuals-setting.test.ts new file mode 100644 index 00000000000..8605deb6e4b --- /dev/null +++ b/src/main/native-chat/native-chat-inline-visuals-setting.test.ts @@ -0,0 +1,152 @@ +import { existsSync, mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' +import { createClaudeStructuredLaunchResolver } from '../claude/claude-structured-launch-resolution' +import { CLAUDE_PLUGIN_DIR_FLAG, type ClaudeCliFlag } from '../claude/claude-cli-flag-support' +import { createCodexStructuredLaunchResolver } from '../codex/codex-structured-launch-resolution' +import { + adapterFor, + fakeCodex, + identityFor +} from '../codex/codex-structured-session-adapter-fixture' +import { agentSessionRecordFixture } from './agent-session-record-test-fixture' +import { + createNativeChatVisualsDelivery, + NATIVE_CHAT_VISUALS_DIR_ENV +} from './native-chat-visuals-delivery' +import { nativeChatVisualsFolderFor } from './native-chat-visuals-folder' + +const SKILL = { pluginDir: '/app/plugin', skillsRoot: '/app/plugin/skills' } +const MANUAL = { approvalPolicy: 'on-request', sandbox: 'workspace-write' } as const +const scratch: string[] = [] +afterEach(() => { + for (const dir of scratch.splice(0)) rmSync(dir, { recursive: true, force: true }) +}) + +function delivery(isEnabled: () => boolean) { + const stateDirectory = mkdtempSync(join(tmpdir(), 'orca-visuals-setting-')) + scratch.push(stateDirectory) + const prepareVisuals = createNativeChatVisualsDelivery({ + stateDirectory, + logger: { warn: vi.fn(), error: vi.fn() }, + isEnabled, + resolveSkill: async () => SKILL + }) + return { stateDirectory, prepareVisuals } +} + +describe('inline visuals launch preference', () => { + it('starts Claude without visuals while preserving configured plugins, user folders and attachment access', async () => { + const sessionId = 'claude-disabled' + const { stateDirectory, prepareVisuals } = delivery(() => false) + const supports = vi.fn(async (_flag: ClaudeCliFlag) => true) + const record = agentSessionRecordFixture({ sessionId }) + const launch = await createClaudeStructuredLaunchResolver({ + store: { getRecord: () => record, pinLaunchDirectory: vi.fn() }, + resolveWorkspacePath: async () => '/repo', + resolveCommand: () => '/bin/claude', + resolveAuthPolicy: () => ({ stripAuthEnv: false }), + resolveLaunchArgs: () => ['--add-dir', '/user/notes', '--plugin-dir', '/user/plugin'], + resolveEnv: () => ({ [NATIVE_CHAT_VISUALS_DIR_ENV]: '/inherited', KEEP: 'yes' }), + resolveInheritedEnv: async () => ({ PATH: '/bin' }), + attachmentDirectory: '/state/agent-session-attachments', + hasTranscript: async () => false, + cliFlags: { supports }, + prepareVisuals + })({ identity: { ...identityFor(sessionId), agent: 'claude', providerHandle: null } }) + expect(launch.options).not.toHaveProperty('plugins') + expect(launch.options.extraArgs).toHaveProperty('plugin-dir', '/user/plugin') + expect(launch.options.additionalDirectories).toEqual([ + '/user/notes', + '/state/agent-session-attachments' + ]) + expect(launch.env).not.toHaveProperty(NATIVE_CHAT_VISUALS_DIR_ENV) + expect(launch.env?.KEEP).toBe('yes') + expect(supports.mock.calls.map(([flag]) => flag)).not.toContain(CLAUDE_PLUGIN_DIR_FLAG) + expect(existsSync(nativeChatVisualsFolderFor(stateDirectory, sessionId))).toBe(false) + }) + + it('gives new Codex chats the current preference without changing another live chat', async () => { + let enabled = true + const { stateDirectory, prepareVisuals } = delivery(() => enabled) + const codex = fakeCodex({ + 'config/read': () => ({ + config: { sandbox_workspace_write: { writable_roots: ['/user/notes'] } } + }) + }) + const resolveLaunch = createCodexStructuredLaunchResolver({ + store: { + getRecord: (sessionId) => + agentSessionRecordFixture({ + sessionId, + provider: 'codex', + accountHome: { variable: 'CODEX_HOME', path: '/home/codex' } + }), + pinLaunchDirectory: vi.fn() + }, + resolveWorkspacePath: async () => '/repo', + resolveCommand: () => '/bin/codex', + resolveLaunchArgs: () => [], + resolvePermissionPolicy: () => MANUAL, + resolveEnvironment: async () => ({ + [NATIVE_CHAT_VISUALS_DIR_ENV]: '/inherited', + KEEP: 'yes' + }), + prepareVisuals + }) + const adapter = adapterFor(codex, {}, [], { resolveLaunch }) + const start = (sessionId: string) => + adapter.acquire({ + identity: { + ...identityFor(sessionId), + hostId: LOCAL_EXECUTION_HOST_ID, + providerHandle: null + }, + fence: 1, + spawnToken: sessionId + }) + try { + await start('first') + const first = codex.connections[0]! + const firstCalls = [...first.calls] + const firstFolder = nativeChatVisualsFolderFor(stateDirectory, 'first') + expect(first.launch.env?.[NATIVE_CHAT_VISUALS_DIR_ENV]).toBe(firstFolder) + expect(first.calls.find(({ method }) => method === 'thread/start')?.params).toMatchObject({ + config: { 'sandbox_workspace_write.writable_roots': ['/user/notes', firstFolder] } + }) + + enabled = false + await start('disabled') + const disabled = codex.connections[1]! + expect(disabled.launch.env).not.toHaveProperty(NATIVE_CHAT_VISUALS_DIR_ENV) + expect(disabled.launch.env?.KEEP).toBe('yes') + expect(disabled.calls.map(({ method }) => method)).not.toContain('skills/extraRoots/set') + const openIndex = disabled.calls.findIndex(({ method }) => method === 'thread/start') + expect(disabled.calls.slice(0, openIndex).map(({ method }) => method)).not.toContain( + 'config/read' + ) + expect( + disabled.calls.find(({ method }) => method === 'thread/start')?.params + ).not.toHaveProperty('config') + expect(existsSync(nativeChatVisualsFolderFor(stateDirectory, 'disabled'))).toBe(false) + expect(first.closed).toBe(false) + expect(first.calls).toEqual(firstCalls) + + enabled = true + await start('reenabled') + const reenabled = codex.connections[2]! + expect( + reenabled.calls.find(({ method }) => method === 'skills/extraRoots/set')?.params + ).toEqual({ extraRoots: [SKILL.skillsRoot] }) + expect(reenabled.launch.env?.[NATIVE_CHAT_VISUALS_DIR_ENV]).toBe( + nativeChatVisualsFolderFor(stateDirectory, 'reenabled') + ) + expect(first.closed).toBe(false) + expect(first.calls).toEqual(firstCalls) + } finally { + await adapter.closeAll() + } + }) +}) diff --git a/src/main/native-chat/native-chat-visuals-delivery.test.ts b/src/main/native-chat/native-chat-visuals-delivery.test.ts index c383eabde5a..97ab31e467e 100644 --- a/src/main/native-chat/native-chat-visuals-delivery.test.ts +++ b/src/main/native-chat/native-chat-visuals-delivery.test.ts @@ -1,4 +1,4 @@ -import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs' +import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' @@ -50,6 +50,32 @@ function tempDir(): string { const logger = () => ({ warn: vi.fn(), error: vi.fn() }) describe('preparing a chat for visuals', () => { + it('reads the preference for each launch and prepares nothing while it is off', async () => { + const state = tempDir() + let enabled = false + const resolveSkill = vi.fn(async () => SKILL) + const prepare = createNativeChatVisualsDelivery({ + stateDirectory: state, + logger: logger(), + isEnabled: () => enabled, + resolveSkill + }) + await expect(prepare('disabled-chat')).resolves.toBeNull() + expect(resolveSkill).not.toHaveBeenCalled() + expect(existsSync(join(state, 'native-chat-visuals'))).toBe(false) + enabled = true + const launched = await prepare('enabled-chat') + expect(launched).toEqual({ + folder: nativeChatVisualsFolderFor(state, 'enabled-chat'), + skill: SKILL + }) + enabled = false + await expect(prepare('next-chat')).resolves.toBeNull() + expect(resolveSkill).toHaveBeenCalledOnce() + expect(existsSync(nativeChatVisualsFolderFor(state, 'next-chat'))).toBe(false) + expect(existsSync(launched!.folder)).toBe(true) + }) + it("creates the chat's own private folder and hands back the skill", async () => { const state = tempDir() const prepare = createNativeChatVisualsDelivery({ diff --git a/src/main/native-chat/native-chat-visuals-delivery.ts b/src/main/native-chat/native-chat-visuals-delivery.ts index a1500e179a0..662dce2b567 100644 --- a/src/main/native-chat/native-chat-visuals-delivery.ts +++ b/src/main/native-chat/native-chat-visuals-delivery.ts @@ -28,11 +28,15 @@ export type PrepareNativeChatVisuals = ( export function createNativeChatVisualsDelivery(deps: { stateDirectory: string logger: StructuredAgentSessionLogger + isEnabled?: () => boolean resolveSkill?: () => Promise }): PrepareNativeChatVisuals { const resolveSkill = deps.resolveSkill ?? (() => resolveNativeChatVisualsSkillLocation()) return async (sessionId) => { try { + if (deps.isEnabled?.() === false) { + return null + } const skill = await resolveSkill() if (!skill) { deps.logger.warn('native-chat visuals skill is missing from this install', { diff --git a/src/main/runtime/orca-runtime-get-worktree-ps.ts b/src/main/runtime/orca-runtime-get-worktree-ps.ts index 17ea276bfbb..0e9af5e444c 100644 --- a/src/main/runtime/orca-runtime-get-worktree-ps.ts +++ b/src/main/runtime/orca-runtime-get-worktree-ps.ts @@ -229,6 +229,7 @@ export class OrcaRuntimeWithGetWorktreePs extends OrcaRuntimeWithStartTuiIdleVis // Wired only here, so a test runtime never runs a real `claude --version`. claudeCliFlags: claudeCliFlagSupport, nativeChatVisuals: { + isEnabled: () => this.requireStore().getSettings().nativeChatInlineVisuals !== false, // Chats and their visuals are shared by every profile; each profile keeps its own catalog. workspaceVerdicts: createNativeChatVisualsWorkspaceVerdicts(() => this.store ? readNativeChatVisualsWorkspaceCatalogs(this.store) : null diff --git a/src/main/runtime/runtime-store-contract.ts b/src/main/runtime/runtime-store-contract.ts index a3787e7f302..24940d7f2db 100644 --- a/src/main/runtime/runtime-store-contract.ts +++ b/src/main/runtime/runtime-store-contract.ts @@ -130,6 +130,7 @@ export type RuntimeStore = { hostSettingOverrides?: GlobalSettings['hostSettingOverrides'] agentSkillSharingEnabled?: GlobalSettings['agentSkillSharingEnabled'] nativeChatSessionOptions?: GlobalSettings['nativeChatSessionOptions'] + nativeChatInlineVisuals?: GlobalSettings['nativeChatInlineVisuals'] nativeChatInheritShellEnvironment?: GlobalSettings['nativeChatInheritShellEnvironment'] nativeChatShellEnvironmentVariables?: GlobalSettings['nativeChatShellEnvironmentVariables'] aiVaultSearch?: GlobalSettings['aiVaultSearch'] diff --git a/src/main/runtime/structured-agent-runtime-registrations.ts b/src/main/runtime/structured-agent-runtime-registrations.ts index 9134b19c7e2..ed0b7be9f86 100644 --- a/src/main/runtime/structured-agent-runtime-registrations.ts +++ b/src/main/runtime/structured-agent-runtime-registrations.ts @@ -100,7 +100,8 @@ function nativeChatVisualsFor(deps: StructuredAgentSessionRuntimeDeps): { ? { prepareVisuals: createNativeChatVisualsDelivery({ stateDirectory: deps.stateDirectory, - logger: deps.logger + logger: deps.logger, + isEnabled: deps.nativeChatVisuals.isEnabled }) } : {} diff --git a/src/main/runtime/structured-agent-session-runtime.ts b/src/main/runtime/structured-agent-session-runtime.ts index 00cd43ab018..fe7415b1e9d 100644 --- a/src/main/runtime/structured-agent-session-runtime.ts +++ b/src/main/runtime/structured-agent-session-runtime.ts @@ -104,6 +104,8 @@ export type StructuredAgentSessionRuntimeDeps = { /** Gives each chat a visuals folder and the skill that teaches it, and sweeps folders whose chat * is gone. Wired by the real hosts only, so a test runtime never loads the bundled skill. */ nativeChatVisuals?: { + /** Read this host's preference when a chat starts a provider process. */ + isEnabled: () => boolean workspaceVerdicts: NonNullable } /** Provider transports are overridden only to drive the runtime against scripted children. */ diff --git a/src/renderer/src/components/native-chat/NativeChatMarkdown.visual.test.tsx b/src/renderer/src/components/native-chat/NativeChatMarkdown.visual.test.tsx index 98a08e57436..c0414b00e7e 100644 --- a/src/renderer/src/components/native-chat/NativeChatMarkdown.visual.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMarkdown.visual.test.tsx @@ -19,6 +19,8 @@ import { } from './native-chat-visual-owner' import { clearNativeChatVisualCacheForTests } from './native-chat-visual-read-client' import { TooltipProvider } from '@/components/ui/tooltip' +import { useAppStore } from '@/store' +import { getDefaultSettings } from '../../../../shared/constants' const owner: NativeChatVisualOwner = { target: { kind: 'local' }, @@ -61,6 +63,22 @@ afterEach(() => { }) describe('NativeChatMarkdown visuals', () => { + it('still renders an existing reply after inline visuals are turned off', async () => { + const settings = useAppStore.getState().settings + useAppStore.setState({ + settings: { ...getDefaultSettings('/tmp'), nativeChatInlineVisuals: false } + }) + try { + const { container } = render( + withOwner() + ) + await waitFor(() => expect(container.querySelector('iframe')).not.toBeNull()) + expect(container).not.toHaveTextContent('::orca-visual') + } finally { + useAppStore.setState({ settings }) + } + }) + it('holds back an unfinished directive at the end of a streaming reply', () => { const { container } = render( withOwner( diff --git a/src/renderer/src/components/settings/NativeChatExperimentalSetting.test.tsx b/src/renderer/src/components/settings/NativeChatExperimentalSetting.test.tsx index d876e33f00c..36e8fabd402 100644 --- a/src/renderer/src/components/settings/NativeChatExperimentalSetting.test.tsx +++ b/src/renderer/src/components/settings/NativeChatExperimentalSetting.test.tsx @@ -258,3 +258,48 @@ describe('NativeChatExperimentalSetting queue follow-ups', () => { expect(container.querySelector(QUEUE_TOGGLE)).toBeNull() }) }) + +describe('NativeChatExperimentalSetting inline visuals', () => { + const structuredOn = { + experimentalNativeChat: true, + openAgentTabsInChatByDefault: true, + experimentalStructuredNativeChat: true + } + + it.each([true, undefined])( + 'defaults visuals on for a saved preference of %s', + (nativeChatInlineVisuals) => { + expect(getDefaultSettings('/tmp').nativeChatInlineVisuals).toBe(true) + const updateSettings = vi.fn() + const { getByRole } = renderSetting( + { ...structuredOn, nativeChatInlineVisuals }, + updateSettings + ) + const toggle = getByRole('switch', { name: 'Toggle inline visuals' }) + expect(toggle.getAttribute('aria-checked')).toBe('true') + fireEvent.click(toggle) + expect(updateSettings).toHaveBeenCalledWith({ nativeChatInlineVisuals: false }) + } + ) + + it('uses the saved off preference and lets the user turn it back on', () => { + const updateSettings = vi.fn() + const { getByRole, getByText } = renderSetting( + { ...structuredOn, nativeChatInlineVisuals: false }, + updateSettings + ) + expect(getByText(/Applies to newly started chats/)).toBeTruthy() + const toggle = getByRole('switch', { name: 'Toggle inline visuals' }) + expect(toggle.getAttribute('aria-checked')).toBe('false') + fireEvent.click(toggle) + expect(updateSettings).toHaveBeenCalledWith({ nativeChatInlineVisuals: true }) + }) + + it('hides the switch outside structured chat', () => { + const { queryByRole } = renderSetting({ + ...structuredOn, + experimentalStructuredNativeChat: false + }) + expect(queryByRole('switch', { name: 'Toggle inline visuals' })).toBeNull() + }) +}) diff --git a/src/renderer/src/components/settings/NativeChatExperimentalSetting.tsx b/src/renderer/src/components/settings/NativeChatExperimentalSetting.tsx index fae3362eb74..458904a724b 100644 --- a/src/renderer/src/components/settings/NativeChatExperimentalSetting.tsx +++ b/src/renderer/src/components/settings/NativeChatExperimentalSetting.tsx @@ -4,6 +4,7 @@ import { translate } from '@/i18n/i18n' import { Label } from '../ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select' import { NativeChatQueueFollowUpsSetting } from './NativeChatQueueFollowUpsSetting' +import { NativeChatInlineVisualsSetting } from './NativeChatInlineVisualsSetting' import { NativeChatShellEnvironmentSetting } from './NativeChatShellEnvironmentSetting' import { NativeChatSupportedAgents } from './NativeChatSupportedAgents' import { SearchableSetting } from './SearchableSetting' @@ -194,6 +195,10 @@ export function NativeChatExperimentalSetting({ ) : null} + {structuredChatActive ? ( + + ) : null} + {structuredChatActive ? ( ) => void +} + +export function NativeChatInlineVisualsSetting({ + settings, + updateSettings +}: NativeChatInlineVisualsSettingProps): React.JSX.Element { + const enabled = settings.nativeChatInlineVisuals !== false + return ( + updateSettings({ nativeChatInlineVisuals: !enabled })} + /> + ) +} diff --git a/src/renderer/src/components/settings/native-chat-experimental-search-entry.test.ts b/src/renderer/src/components/settings/native-chat-experimental-search-entry.test.ts index 1bedfdb164b..eacdbc65f8a 100644 --- a/src/renderer/src/components/settings/native-chat-experimental-search-entry.test.ts +++ b/src/renderer/src/components/settings/native-chat-experimental-search-entry.test.ts @@ -10,4 +10,8 @@ describe('native chat experimental search entry', () => { it('matches "queue" so the Queue follow-ups switch is findable', () => { expect(matchesSettingsSearch('queue', getNativeChatExperimentalSearchEntry())).toBe(true) }) + + it.each(['inline visuals', 'visuals'])('finds the Inline visuals switch for %s', (query) => { + expect(matchesSettingsSearch(query, getNativeChatExperimentalSearchEntry())).toBe(true) + }) }) diff --git a/src/renderer/src/components/settings/native-chat-experimental-search-entry.ts b/src/renderer/src/components/settings/native-chat-experimental-search-entry.ts index 39a09056b7f..f4b12f5a96c 100644 --- a/src/renderer/src/components/settings/native-chat-experimental-search-entry.ts +++ b/src/renderer/src/components/settings/native-chat-experimental-search-entry.ts @@ -50,6 +50,10 @@ export function getNativeChatExperimentalSearchEntry(): SettingsSearchEntry { 'auto.components.settings.experimental.search.nativeChat.agent', 'agent' ), + ...translateSearchKeyword( + 'components.settings.nativeChat.inlineVisualsTitle', + 'Inline visuals' + ), ...translateSearchKeyword( 'auto.components.settings.experimental.search.nativeChat.queue', 'queue' diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 2b087c53efd..036776c0976 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -17498,6 +17498,9 @@ } }, "nativeChat": { + "inlineVisualsTitle": "Inline visuals", + "inlineVisualsCopy": "Let the agent show charts, diagrams and mockups inside its replies. Applies to newly started chats.", + "inlineVisualsToggleLabel": "Toggle inline visuals", "contextMenu": { "copy": "Copy" } diff --git a/src/shared/default-global-settings.ts b/src/shared/default-global-settings.ts index 3e8bb8abbca..524ea6f67b2 100644 --- a/src/shared/default-global-settings.ts +++ b/src/shared/default-global-settings.ts @@ -137,6 +137,7 @@ export function buildDefaultSettings(args: { experimentalStructuredNativeChat: false, nativeChatResumeWorkOnRestart: false, nativeChatQueueFollowUps: true, + nativeChatInlineVisuals: true, nativeChatInheritShellEnvironment: true, nativeChatShellEnvironmentVariables: [], nativeChatSessionOptions: {}, diff --git a/src/shared/global-settings-types.ts b/src/shared/global-settings-types.ts index d7a51e53fbe..0f48e192471 100644 --- a/src/shared/global-settings-types.ts +++ b/src/shared/global-settings-types.ts @@ -228,6 +228,8 @@ export type GlobalSettings = NativeChatGlobalSettings & { /** Chat-wide: hold a mid-turn send as an editable queued draft that goes when the turn ends * (capable hosts only). Absent = on; off keeps mid-turn sends immediate. */ nativeChatQueueFollowUps?: boolean + /** Teach newly started native chats to create inline visuals; absent means on. */ + nativeChatInlineVisuals?: boolean /** Structured chat only: Codex/Claude children inherit the whole login-shell environment. * Off passes only `nativeChatShellEnvironmentVariables` (plus a PATH/locale baseline). */ nativeChatInheritShellEnvironment?: boolean From e7bd0c7263a4c2da9f978883ce3755518289471d Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:15:59 -0700 Subject: [PATCH 2/3] Native chat: keep inline visuals guidance light --- .../skills/orca-chat-visuals/SKILL.md | 65 ++++++------------- ...native-chat-inline-visuals-setting.test.ts | 4 +- .../native-chat-visuals-delivery.test.ts | 10 +-- src/renderer/src/i18n/locales/en.json | 6 +- 4 files changed, 29 insertions(+), 56 deletions(-) diff --git a/resources/native-chat-visuals/skills/orca-chat-visuals/SKILL.md b/resources/native-chat-visuals/skills/orca-chat-visuals/SKILL.md index 2276e92d4e3..dc63d3c8d20 100644 --- a/resources/native-chat-visuals/skills/orca-chat-visuals/SKILL.md +++ b/resources/native-chat-visuals/skills/orca-chat-visuals/SKILL.md @@ -1,58 +1,33 @@ --- name: orca-chat-visuals -description: Show a chart, diagram, timeline, comparison or UI mockup inline in this Orca chat by writing one self-contained HTML file and adding a visual line to your reply. Use when seeing something would explain the answer faster than prose or a small Markdown table, and only when the ORCA_CHAT_VISUALS_DIR environment variable is set. +description: Show a chart, diagram, table, report or mockup inline in this Orca chat as an HTML + page. Use when a visual would help the user understand your answer. Only available when the + ORCA_CHAT_VISUALS_DIR environment variable is set. user-invocable: false --- # Inline visuals in an Orca chat -Orca can render an HTML page inside your reply. Reach for it when the reader gains from seeing the -answer: trends, many values side by side, flows, architecture, layouts. Answers that read fine as -text, a short list or a small table don't need one. - -## When you can't make one - -- This chat's folder is the absolute path in the `ORCA_CHAT_VISUALS_DIR` environment variable. Read - it from your shell (`printenv ORCA_CHAT_VISUALS_DIR`; in PowerShell `$env:ORCA_CHAT_VISUALS_DIR`). -- If the variable is unset or empty, or writing there fails, don't make a visual. -- Stay inside what the user allowed. In plan or read-only mode don't make a visual. If a write - there is refused, don't retry it; never switch modes or ask for wider access for a visual. -- Never put visuals in the user's project and never edit `.gitignore` or other files for them. - -Without a visual, use a Markdown table, a Mermaid code block or plain prose instead. - -## Writing the file - -- One visual is one complete HTML document saved directly in that folder (no subfolders). -- Every visual gets a file name not used before in this chat: a few lowercase words and a short - random suffix, like `latency-by-region-7c1e.html`. Never overwrite or edit an earlier visual; - write a new file. Allowed: letters, digits, `.`, `_` and `-`; starts with a letter or digit; - ends in `.html`; at most 128 characters; no `..`; not a Windows device name like `con` or `nul`. -- Put everything in the file: inline `