mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 08:03:13 +00:00
rm stubs
This commit is contained in:
@@ -1 +0,0 @@
|
||||
shots/
|
||||
@@ -1,4 +0,0 @@
|
||||
export async function setStringAsync(_text: string): Promise<void> {}
|
||||
export async function getStringAsync(): Promise<string> {
|
||||
return ''
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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('▸')
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)}` }
|
||||
}
|
||||
@@ -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] } }
|
||||
})
|
||||
Reference in New Issue
Block a user