diff --git a/mobile/rnw-preview/.gitignore b/mobile/rnw-preview/.gitignore deleted file mode 100644 index f16baf5a19d..00000000000 --- a/mobile/rnw-preview/.gitignore +++ /dev/null @@ -1 +0,0 @@ -shots/ diff --git a/mobile/rnw-preview/expo-clipboard-stub.ts b/mobile/rnw-preview/expo-clipboard-stub.ts deleted file mode 100644 index fb3acda9842..00000000000 --- a/mobile/rnw-preview/expo-clipboard-stub.ts +++ /dev/null @@ -1,4 +0,0 @@ -export async function setStringAsync(_text: string): Promise {} -export async function getStringAsync(): Promise { - return '' -} diff --git a/mobile/rnw-preview/fake-rpc.ts b/mobile/rnw-preview/fake-rpc.ts deleted file mode 100644 index a0edc3500b0..00000000000 --- a/mobile/rnw-preview/fake-rpc.ts +++ /dev/null @@ -1,31 +0,0 @@ -// A fake of the paired-host RPC socket: the ONLY faked boundary. It answers the -// exact methods the real upload + send pipeline calls, so the real hooks run -// unmodified. clipboard.startImageUpload returns method_not_found so the pipeline -// takes its single-frame fallback (saveImageAsTempFile), exactly as against a host. -type Response = - | { id: string; ok: true; result: unknown; _meta: { runtimeId: string } } - | { - id: string - ok: false - error: { code: string; message: string } - _meta: { runtimeId: string } - } - -export const calls: Array<{ method: string; params: unknown }> = [] - -export const fakeClient = { - async sendRequest(method: string, params?: unknown): Promise { - calls.push({ method, params }) - const meta = { runtimeId: 'mock' as const } - if (method === 'clipboard.startImageUpload') { - return { id: 'x', ok: false, error: { code: 'method_not_found', message: 'no' }, _meta: meta } - } - if (method === 'clipboard.saveImageAsTempFile') { - return { id: 'x', ok: true, result: '/host/tmp/orca-attach-42.png', _meta: meta } - } - if (method === 'terminal.send') { - return { id: 'x', ok: true, result: { send: { accepted: true } }, _meta: meta } - } - return { id: 'x', ok: true, result: {}, _meta: meta } - } -} diff --git a/mobile/rnw-preview/index.html b/mobile/rnw-preview/index.html deleted file mode 100644 index e02d8457808..00000000000 --- a/mobile/rnw-preview/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - Orca mobile native-chat image โ€” interactive proof - - - -
- - - diff --git a/mobile/rnw-preview/lucide-stub.tsx b/mobile/rnw-preview/lucide-stub.tsx deleted file mode 100644 index 161e0a9afcd..00000000000 --- a/mobile/rnw-preview/lucide-stub.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { createElement } from 'react' - -type IconProps = { size?: number; color?: string; strokeWidth?: number; fill?: string } -function glyph(char: string) { - return function Icon({ size = 20, color = '#888' }: IconProps) { - return createElement( - 'span', - { style: { fontSize: size, lineHeight: `${size}px`, color, display: 'inline-flex' } }, - char - ) - } -} -export const ArrowUp = glyph('โ†‘') -export const ImagePlus = glyph('๐Ÿ–ผ') -export const Mic = glyph('๐ŸŽ™') -export const Square = glyph('โ– ') -export const X = glyph('โœ•') -export const ChevronDown = glyph('โ–พ') -export const Copy = glyph('โง‰') -export const SquareChevronRight = glyph('โ–ธ') diff --git a/mobile/rnw-preview/main.tsx b/mobile/rnw-preview/main.tsx deleted file mode 100644 index b785e688168..00000000000 --- a/mobile/rnw-preview/main.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import { createElement as h, useCallback, useMemo, useState } from 'react' -import { createRoot } from 'react-dom/client' -import { ScrollView, StyleSheet, Text, View } from 'react-native' -import { MobileNativeChatComposer } from '../src/session/MobileNativeChatComposer' -import { MobileNativeChatMessage } from '../src/session/MobileNativeChatMessage' -import { useMobileNativeChatDrafts } from '../src/session/use-mobile-native-chat-drafts' -import { useMobileNativeChatImageAttachments } from '../src/session/use-mobile-native-chat-image-attachments' -import { - buildMobileNativeChatTransientData, - foldMobileNativeChatMessages -} from '../src/session/mobile-native-chat-render-data' -import type { NativeChatMessage } from '../../src/shared/native-chat-types' -import type { RpcClient } from '../src/transport/rpc-client' -import { colors, radii, spacing } from '../src/theme/mobile-theme' -import { calls, fakeClient } from './fake-rpc' - -const TERMINAL = 'term-mock' - -const AGENT_MSG: NativeChatMessage = { - id: 'a1', - role: 'assistant', - blocks: [{ type: 'text', text: 'Sure โ€” send me the screenshot and Iโ€™ll take a look.' }], - timestamp: null, - source: 'transcript' -} - -// A tiny stand-in for the controller's handleNativeChatSend: capture the send -// origin, hit the (faked) terminal.send, then record the optimistic echo -// through the real drafts hook. -function useMiniController(messages: NativeChatMessage[]) { - const drafts = useMobileNativeChatDrafts({ - hostId: 'h', - worktreeId: 'w', - tabId: 't', - sessionId: 's', - messages - }) - const baseSend = useCallback( - async (text: string, images?: string[]): Promise => { - const origin = drafts.captureSendOrigin(text) - if (!origin) { - return false - } - const resp = await fakeClient.sendRequest('terminal.send', { - terminal: TERMINAL, - text, - enter: true - }) - if (!(resp.ok && (resp.result as { send?: { accepted?: boolean } })?.send?.accepted)) { - return false - } - drafts.acceptSend(origin, text, images) - return true - }, - [drafts] - ) - return { drafts, baseSend } -} - -function App(): React.ReactNode { - const [messages] = useState([AGENT_MSG]) - const [, forceRender] = useState(0) - const { drafts, baseSend } = useMiniController(messages) - const activeHandleRef = useMemo(() => ({ current: TERMINAL as string | null }), []) - const deviceTokenRef = useMemo(() => ({ current: null as string | null }), []) - - const imagesHook = useMobileNativeChatImageAttachments({ - client: fakeClient as unknown as RpcClient, - activeHandleRef, - deviceTokenRef, - getActiveWorktreeConnectionId: async () => null, - connState: 'connected', - scopeKey: 'h\0w\0t', - enabled: true, - showToast: () => {}, - baseSend, - onAttachSuccess: () => {}, - onError: () => {} - }) - - const { data } = useMemo( - () => - buildMobileNativeChatTransientData({ - folded: foldMobileNativeChatMessages(messages), - pending: drafts.pending - }), - [messages, drafts.pending] - ) - - const terminalSends = calls.filter((c) => c.method === 'terminal.send') - - return h( - View, - { style: styles.page }, - h( - Text, - { style: styles.title }, - 'Real native-chat components + hooks โ€” driven by clicks (picker & RPC faked)' - ), - h( - View, - { style: styles.phone }, - h(Text, { style: styles.header }, 'Claude ยท rich chat'), - h( - ScrollView, - { style: styles.transcript, contentContainerStyle: styles.tc }, - data.map((m) => - h(MobileNativeChatMessage, { key: m.id, message: m, toolsExpanded: false, fontScale: 1 }) - ) - ), - h(MobileNativeChatComposer, { - value: drafts.composerText, - onChangeText: (t: string) => { - drafts.setComposerText(t) - forceRender((n) => n + 1) - }, - onSend: async (t: string) => { - const ok = await imagesHook.sendNativeChat(t) - forceRender((n) => n + 1) - return ok - }, - onAttachImage: () => { - void imagesHook.attachImage('library').then(() => forceRender((n) => n + 1)) - }, - attachments: imagesHook.attachments, - onRemoveAttachment: imagesHook.removeAttachment, - onMicPress: () => {} - }) - ), - // Renders every terminal.send the pipeline wrote to the faked host socket. - h( - View, - { style: styles.trace }, - h(Text, { style: styles.traceTitle }, `terminal.send calls (${terminalSends.length}):`), - terminalSends.map((c, i) => - h( - Text, - { key: i, style: styles.traceLine }, - `${i + 1}. ${JSON.stringify((c.params as { text: string; enter: boolean }).text)} enter=${String((c.params as { enter: boolean }).enter)}` - ) - ) - ) - ) -} - -const styles = StyleSheet.create({ - page: { - minHeight: '100%', - backgroundColor: '#0a0a0a', - padding: spacing.lg, - alignItems: 'center' - }, - title: { color: colors.textPrimary, fontSize: 16, fontWeight: '600', marginBottom: spacing.md }, - phone: { - width: 390, - height: 620, - backgroundColor: colors.bgBase, - borderRadius: radii.card, - borderWidth: 1, - borderColor: colors.borderSubtle, - overflow: 'hidden', - justifyContent: 'flex-end' - }, - header: { - color: colors.textSecondary, - fontSize: 13, - paddingHorizontal: spacing.md, - paddingVertical: spacing.md, - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: colors.borderSubtle - }, - transcript: { flex: 1 }, - tc: { paddingVertical: spacing.md }, - trace: { - width: 390, - marginTop: spacing.md, - padding: spacing.sm, - backgroundColor: '#141414', - borderRadius: radii.button, - borderWidth: StyleSheet.hairlineWidth, - borderColor: colors.borderSubtle - }, - traceTitle: { color: colors.textSecondary, fontSize: 12, fontWeight: '600', marginBottom: 4 }, - traceLine: { color: colors.statusGreen, fontFamily: 'monospace', fontSize: 11, lineHeight: 16 } -}) - -createRoot(document.getElementById('root')!).render(h(App)) diff --git a/mobile/rnw-preview/markdown-stub.tsx b/mobile/rnw-preview/markdown-stub.tsx deleted file mode 100644 index 4a62d090e61..00000000000 --- a/mobile/rnw-preview/markdown-stub.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { createElement } from 'react' -import { Text } from 'react-native' - -export function MobileMarkdown({ content }: { content: string }): React.JSX.Element { - return createElement(Text, { style: { color: '#e0e0e0', fontSize: 16 } }, content) -} diff --git a/mobile/rnw-preview/picker-stub.ts b/mobile/rnw-preview/picker-stub.ts deleted file mode 100644 index 02c4ef774f2..00000000000 --- a/mobile/rnw-preview/picker-stub.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Stands in for the OS photo picker so the REAL attach flow runs without a -// native modal. Returns a tiny valid base64 (for the upload pipeline) and a -// data-URI preview (a recognizable landscape) for the composer/echo thumbnail. -export type MobileImageSource = 'library' | 'files' -export type PickedMobileImage = { readonly base64: string; readonly uri?: string } - -export class ImageLibraryPermissionError extends Error { - constructor() { - super('Photo library permission denied') - this.name = 'ImageLibraryPermissionError' - } -} - -const PHOTO_SVG = ` - - - - - - - PHOTO.PNG` - -export async function pickMobileImage(_source: MobileImageSource): Promise { - return { base64: 'iVBORw0KGgoAAAANSUhEUg==', uri: `data:image/svg+xml;base64,${btoa(PHOTO_SVG)}` } -} diff --git a/mobile/rnw-preview/vite.config.ts b/mobile/rnw-preview/vite.config.ts deleted file mode 100644 index 27ca182bb8c..00000000000 --- a/mobile/rnw-preview/vite.config.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { defineConfig } from 'vite' -import path from 'node:path' - -const harnessDir = import.meta.dirname -const mobileRoot = path.resolve(harnessDir, '..') - -// Interactive harness: mounts the REAL native-chat hooks + components. Only the -// OS boundary is faked โ€” the photo picker (picker-stub) and the RPC socket -// (fake-rpc). Everything else is the actual production code reacting to clicks. -export default defineConfig({ - root: harnessDir, - resolve: { - alias: [ - { find: /^react-native$/, replacement: 'react-native-web' }, - { find: /^react-native\//, replacement: 'react-native-web/' }, - { find: 'lucide-react-native', replacement: path.join(harnessDir, 'lucide-stub.tsx') }, - { find: 'expo-clipboard', replacement: path.join(harnessDir, 'expo-clipboard-stub.ts') }, - { find: /^.*\/MobileMarkdown$/, replacement: path.join(harnessDir, 'markdown-stub.tsx') }, - { - find: /^.*mobile-image-source-picker$/, - replacement: path.join(harnessDir, 'picker-stub.ts') - } - ], - extensions: ['.web.tsx', '.web.ts', '.web.js', '.tsx', '.ts', '.jsx', '.js', '.json'] - }, - define: { - 'process.env.NODE_ENV': JSON.stringify('development'), - __DEV__: 'true', - global: 'globalThis' - }, - server: { port: 5199, fs: { allow: [mobileRoot] } } -})