diff --git a/src/utils/statusNoticeDefinitions.safety.test.tsx b/src/utils/statusNoticeDefinitions.safety.test.tsx index 000e255e2..8aca8d96a 100644 --- a/src/utils/statusNoticeDefinitions.safety.test.tsx +++ b/src/utils/statusNoticeDefinitions.safety.test.tsx @@ -1,6 +1,10 @@ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' import type { StatusNoticeContext } from './statusNoticeDefinitions.js' -import { getActiveNotices } from './statusNoticeDefinitions.js' +import { + getActiveNotices, + statusNoticeDefinitions, +} from './statusNoticeDefinitions.js' +import { renderToString } from './staticRender.js' // Regression coverage for issue #244 — the two safety-related status notices // that warn 3P users when they are running without the AI classifier or with @@ -21,6 +25,15 @@ function activeIds(ctx: StatusNoticeContext): string[] { return getActiveNotices(ctx).map(n => n.id) } +async function renderNoticePlainText( + id: string, + ctx: StatusNoticeContext, +): Promise { + const notice = statusNoticeDefinitions.find(n => n.id === id) + expect(notice).toBeDefined() + return renderToString(notice!.render(ctx), 80) +} + const SAVED_ARGV = process.argv const SAVED_API_KEY = process.env.ANTHROPIC_API_KEY @@ -139,3 +152,42 @@ describe('dangerously-skip-permissions sandbox notice (#244 finding 2)', () => { ) }) }) + +describe('safety notice rendering', () => { + test('separates warning icons from the notice text', async () => { + const ctx = buildContext({ + permissionMode: 'bypassPermissions', + mainLoopModel: 'llama3.1', + }) + + const thirdPartyNotice = await renderNoticePlainText( + 'third-party-permissive-mode', + ctx, + ) + const dangerouslySkipNotice = await renderNoticePlainText( + 'dangerously-skip-permissions-no-sandbox', + ctx, + ) + + expect(thirdPartyNotice).toContain('⚠ bypassPermissions') + expect(thirdPartyNotice).not.toContain('⚠bypassPermissions') + expect(dangerouslySkipNotice).toContain( + '⚠ --dangerously-skip-permissions', + ) + expect(dangerouslySkipNotice).not.toContain( + '⚠--dangerously-skip-permissions', + ) + expect( + thirdPartyNotice + .split('\n') + .slice(1) + .every(line => line.startsWith(' ')), + ).toBe(true) + expect( + dangerouslySkipNotice + .split('\n') + .slice(1) + .every(line => line.startsWith(' ')), + ).toBe(true) + }) +}) diff --git a/src/utils/statusNoticeDefinitions.tsx b/src/utils/statusNoticeDefinitions.tsx index a2f72afd1..89f079298 100644 --- a/src/utils/statusNoticeDefinitions.tsx +++ b/src/utils/statusNoticeDefinitions.tsx @@ -35,6 +35,23 @@ export type StatusNoticeDefinition = { render: (context: StatusNoticeContext) => React.ReactNode; }; +function WarningNoticeRow({ + children, + marginTop, +}: { + children: React.ReactNode; + marginTop?: number; +}): React.ReactNode { + return + + {figures.warning} + + + {children} + + ; +} + // Individual notice definitions const largeMemoryFilesNotice: StatusNoticeDefinition = { id: 'large-memory-files', @@ -45,15 +62,14 @@ const largeMemoryFilesNotice: StatusNoticeDefinition = { return <> {largeMemoryFiles.map(file => { const displayPath = file.path.startsWith(getCwd()) ? relative(getCwd(), file.path) : file.path; - return - {figures.warning} + return Large {displayPath} will impact performance ( {formatNumber(file.content.length)} chars >{' '} {formatNumber(MAX_MEMORY_CHARACTER_COUNT)}) · /memory to edit - ; + ; })} ; } @@ -67,14 +83,13 @@ const claudeAiSubscriberExternalTokenNotice: StatusNoticeDefinition = { }, render: () => { const authTokenInfo = getAuthTokenSource(); - return - {figures.warning} + return Auth conflict: Using {authTokenInfo.source} instead of Claude account subscription token. Either unset {authTokenInfo.source}, or run `claude /logout`. - ; + ; } }; const apiKeyConflictNotice: StatusNoticeDefinition = { @@ -94,13 +109,12 @@ const apiKeyConflictNotice: StatusNoticeDefinition = { } = getAnthropicApiKeyWithSource({ skipRetrievingKeyFromApiKeyHelper: true }); - return - {figures.warning} + return Auth conflict: Using {apiKeySource} instead of Anthropic Console key. Either unset {apiKeySource}, or run `openclaude /logout`. - ; + ; } }; const bothAuthMethodsNotice: StatusNoticeDefinition = { @@ -123,13 +137,12 @@ const bothAuthMethodsNotice: StatusNoticeDefinition = { }); const authTokenInfo = getAuthTokenSource(); return - - {figures.warning} + Auth conflict: Both a token ({authTokenInfo.source}) and an API key ({apiKeySource}) are set. This may lead to unexpected behavior. - + · Trying to use{' '} @@ -154,15 +167,14 @@ const largeAgentDescriptionsNotice: StatusNoticeDefinition = { }, render: context => { const totalTokens = getAgentDescriptionsTotalTokens(context.agentDefinitions); - return - {figures.warning} + return Large cumulative agent descriptions will impact performance (~ {formatNumber(totalTokens)} tokens >{' '} {formatNumber(AGENT_DESCRIPTIONS_THRESHOLD)}) · /agents to manage - ; + ; } }; const jetbrainsPluginNotice: StatusNoticeDefinition = { @@ -224,14 +236,15 @@ const thirdPartyPermissiveModeNotice: StatusNoticeDefinition = { }, render: ctx => { const mode = ctx.permissionMode; - return - {figures.warning} + return - {mode} mode is active on a third-party provider — - tool calls run without the AI safety classifier. - Inspect tool calls manually, especially when working with untrusted code. + {mode} mode is active on a third-party provider. - ; + + Tool calls run without the AI safety classifier. Inspect tool calls manually, + especially when working with untrusted code. + + ; } }; // `--dangerously-skip-permissions` (a.k.a. bypassPermissions) auto-approves @@ -250,14 +263,15 @@ const dangerouslySkipPermissionsNotice: StatusNoticeDefinition = { isActive: ctx => hasDangerouslySkipPermissionsArg() || ctx.permissionMode === 'bypassPermissions', - render: () => - {figures.warning} + render: () => - --dangerously-skip-permissions bypasses every tool - consent check. - Only use inside a sandbox with no internet access. Restart without the flag to re-enable prompts. + --dangerously-skip-permissions is active. - + + Every tool consent check is bypassed. Only use inside a sandbox with no internet access. + Restart without the flag to re-enable prompts. + + }; // All notice definitions