diff --git a/adapters/whatsapp/__tests__/session.test.ts b/adapters/whatsapp/__tests__/session.test.ts new file mode 100644 index 00000000..a93d9d4a --- /dev/null +++ b/adapters/whatsapp/__tests__/session.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from 'bun:test' +import * as fs from 'node:fs' +import * as os from 'node:os' +import * as path from 'node:path' +import { + clearWhatsAppAuth, + closeWhatsAppSocket, + getWhatsAppDisconnectStatus, + hasWhatsAppAuth, + isWhatsAppLoggedOut, + waitForWhatsAppCredsSave, +} from '../session.js' +import type { WhatsAppSocket } from '../session.js' + +function makeTempAuthDir(): string { + return fs.mkdtempSync(path.join(os.tmpdir(), 'wa-session-test-')) +} + +describe('whatsapp session helpers', () => { + it('detects and clears persisted auth credentials', () => { + const authDir = makeTempAuthDir() + + expect(hasWhatsAppAuth(authDir)).toBe(false) + + fs.writeFileSync(path.join(authDir, 'creds.json'), '{}') + expect(hasWhatsAppAuth(authDir)).toBe(true) + + clearWhatsAppAuth(authDir) + expect(fs.existsSync(authDir)).toBe(false) + }) + + it('extracts disconnect status from Baileys and direct error shapes', () => { + expect(getWhatsAppDisconnectStatus({ output: { statusCode: 401 } })).toBe(401) + expect(getWhatsAppDisconnectStatus({ statusCode: 515 })).toBe(515) + expect(getWhatsAppDisconnectStatus({ output: { statusCode: '401' } })).toBeUndefined() + expect(getWhatsAppDisconnectStatus(null)).toBeUndefined() + expect(isWhatsAppLoggedOut({ output: { statusCode: 401 } })).toBe(true) + expect(isWhatsAppLoggedOut({ output: { statusCode: 515 } })).toBe(false) + }) + + it('closes sockets best-effort with a reason', () => { + let received: Error | undefined + const sock = { + end: (error?: Error) => { + received = error + }, + } as unknown as WhatsAppSocket + + closeWhatsAppSocket(sock, 'test close') + expect(received?.message).toBe('test close') + + expect(() => closeWhatsAppSocket({} as WhatsAppSocket)).not.toThrow() + expect(() => closeWhatsAppSocket({ + end: () => { + throw new Error('already closed') + }, + } as unknown as WhatsAppSocket)).not.toThrow() + }) + + it('returns immediately when no credential save is queued', async () => { + await expect(waitForWhatsAppCredsSave(makeTempAuthDir())).resolves.toBeUndefined() + }) +}) diff --git a/desktop/src/components/chat/PermissionDialog.tsx b/desktop/src/components/chat/PermissionDialog.tsx index 1297b596..703cf8de 100644 --- a/desktop/src/components/chat/PermissionDialog.tsx +++ b/desktop/src/components/chat/PermissionDialog.tsx @@ -5,6 +5,12 @@ import { useTranslation } from '../../i18n' import type { TranslationKey } from '../../i18n' import { Button } from '../shared/Button' import { DiffViewer } from './DiffViewer' +import { + PlanPreviewCard, + buildPromptPermissionUpdates, + extractPlanPreview, + isExitPlanModeTool, +} from './PlanModePreview' type Props = { sessionId?: string | null @@ -121,6 +127,18 @@ export function PermissionDialog({ sessionId, requestId, toolName, input, descri const isPending = pendingPermission?.requestId === requestId const [showRaw, setShowRaw] = useState(false) + if (isExitPlanModeTool(toolName)) { + return ( + + ) + } + const meta = TOOL_META[toolName] || { icon: 'shield', label: toolName, color: 'var(--color-text-tertiary)' } const details = extractToolDetails(toolName, input, t) const rawInput = typeof input === 'string' ? input : JSON.stringify(input, null, 2) @@ -262,3 +280,104 @@ export function PermissionDialog({ sessionId, requestId, toolName, input, descri ) } + +function ExitPlanModePermissionDialog({ + sessionId, + requestId, + input, + description, + isPending, +}: { + sessionId?: string | null + requestId: string + input: unknown + description?: string + isPending: boolean +}) { + const { respondToPermission } = useChatStore() + const t = useTranslation() + const [feedback, setFeedback] = useState('') + const preview = extractPlanPreview(input) + const permissionUpdates = buildPromptPermissionUpdates(preview.allowedPrompts) + const trimmedFeedback = feedback.trim() + + return ( +
+
+
+ architecture +
+
+
+ + {t('permission.planReadyTitle')} + + {isPending ? ( + + + {t('permission.awaitingApproval')} + + ) : ( + + {t('permission.responded')} + + )} +
+ {description ? ( +

{description}

+ ) : null} +
+
+ +
+ + {isPending ? ( +