This commit is contained in:
Jinjing
2026-07-23 13:25:05 -07:00
parent 612bba3936
commit ce4aafef60
9 changed files with 0 additions and 327 deletions
-1
View File
@@ -1 +0,0 @@
shots/
@@ -1,4 +0,0 @@
export async function setStringAsync(_text: string): Promise<void> {}
export async function getStringAsync(): Promise<string> {
return ''
}
-31
View File
@@ -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<Response> {
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 }
}
}
-21
View File
@@ -1,21 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Orca mobile native-chat image — interactive proof</title>
<style>
html,
body,
#root {
margin: 0;
min-height: 100%;
background: #0a0a0a;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>
-20
View File
@@ -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('▸')
-187
View File
@@ -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<boolean> => {
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<NativeChatMessage[]>([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))
-6
View File
@@ -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)
}
-25
View File
@@ -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 = `<svg xmlns='http://www.w3.org/2000/svg' width='240' height='180'>
<defs><linearGradient id='s' x1='0' y1='0' x2='0' y2='1'>
<stop offset='0' stop-color='#6cb6ff'/><stop offset='1' stop-color='#cfeaff'/></linearGradient></defs>
<rect width='240' height='180' fill='url(#s)'/>
<circle cx='188' cy='42' r='24' fill='#ffe08a'/>
<path d='M0 135 L70 80 L120 120 L170 72 L240 135 Z' fill='#3f7d4f'/>
<path d='M0 152 L240 152 L240 180 L0 180 Z' fill='#2f6140'/>
<text x='120' y='172' font-family='sans-serif' font-size='14' fill='#fff' text-anchor='middle'>PHOTO.PNG</text></svg>`
export async function pickMobileImage(_source: MobileImageSource): Promise<PickedMobileImage> {
return { base64: 'iVBORw0KGgoAAAANSUhEUg==', uri: `data:image/svg+xml;base64,${btoa(PHOTO_SVG)}` }
}
-32
View File
@@ -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] } }
})