updates
This commit is contained in:
parent
e7e16478e3
commit
6b8bcee7ea
1
apps/expo-github-mobile/.gitignore
vendored
1
apps/expo-github-mobile/.gitignore
vendored
@ -31,6 +31,7 @@ yarn-error.*
|
|||||||
*.pem
|
*.pem
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
|
.env
|
||||||
.env*.local
|
.env*.local
|
||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
|
|||||||
121
apps/expo-github-mobile/package-lock.json
generated
121
apps/expo-github-mobile/package-lock.json
generated
@ -16,6 +16,7 @@
|
|||||||
"expo-status-bar": "~3.0.9",
|
"expo-status-bar": "~3.0.9",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
|
"react-native-markdown-display": "^7.0.2",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0"
|
"react-native-screens": "~4.16.0"
|
||||||
},
|
},
|
||||||
@ -3905,6 +3906,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/camelize": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001766",
|
"version": "1.0.30001766",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
|
||||||
@ -4254,6 +4264,26 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-color-keywords": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-to-react-native": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"camelize": "^1.0.0",
|
||||||
|
"css-color-keywords": "^1.0.0",
|
||||||
|
"postcss-value-parser": "^4.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
@ -4408,6 +4438,12 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
"node_modules/env-editor": {
|
"node_modules/env-editor": {
|
||||||
"version": "0.4.2",
|
"version": "0.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
|
||||||
@ -6361,6 +6397,15 @@
|
|||||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/linkify-it": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"uc.micro": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||||
@ -6427,12 +6472,34 @@
|
|||||||
"tmpl": "1.0.5"
|
"tmpl": "1.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/markdown-it": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^1.0.7",
|
||||||
|
"entities": "~2.0.0",
|
||||||
|
"linkify-it": "^2.0.0",
|
||||||
|
"mdurl": "^1.0.1",
|
||||||
|
"uc.micro": "^1.0.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"markdown-it": "bin/markdown-it.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/marky": {
|
"node_modules/marky": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz",
|
||||||
"integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==",
|
"integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/mdurl": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/memoize-one": {
|
"node_modules/memoize-one": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||||
@ -7348,6 +7415,12 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postcss-value-parser": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/pretty-bytes": {
|
"node_modules/pretty-bytes": {
|
||||||
"version": "5.6.0",
|
"version": "5.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
|
||||||
@ -7426,6 +7499,23 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prop-types": {
|
||||||
|
"version": "15.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"react-is": "^16.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prop-types/node_modules/react-is": {
|
||||||
|
"version": "16.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@ -7588,6 +7678,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-fit-image": {
|
||||||
|
"version": "1.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-fit-image/-/react-native-fit-image-1.5.5.tgz",
|
||||||
|
"integrity": "sha512-Wl3Vq2DQzxgsWKuW4USfck9zS7YzhvLNPpkwUUCF90bL32e1a0zOVQ3WsJILJOwzmPdHfzZmWasiiAUNBkhNkg==",
|
||||||
|
"license": "Beerware",
|
||||||
|
"dependencies": {
|
||||||
|
"prop-types": "^15.5.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-is-edge-to-edge": {
|
"node_modules/react-native-is-edge-to-edge": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz",
|
||||||
@ -7598,6 +7697,22 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-markdown-display": {
|
||||||
|
"version": "7.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-markdown-display/-/react-native-markdown-display-7.0.2.tgz",
|
||||||
|
"integrity": "sha512-Mn4wotMvMfLAwbX/huMLt202W5DsdpMO/kblk+6eUs55S57VVNni1gzZCh5qpznYLjIQELNh50VIozEfY6fvaQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"css-to-react-native": "^3.0.0",
|
||||||
|
"markdown-it": "^10.0.0",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react-native-fit-image": "^1.5.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.2.0",
|
||||||
|
"react-native": ">=0.50.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-safe-area-context": {
|
"node_modules/react-native-safe-area-context": {
|
||||||
"version": "5.6.2",
|
"version": "5.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
|
||||||
@ -8706,6 +8821,12 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uc.micro": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "6.23.0",
|
"version": "6.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
"expo-status-bar": "~3.0.9",
|
"expo-status-bar": "~3.0.9",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
|
"react-native-markdown-display": "^7.0.2",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0"
|
"react-native-screens": "~4.16.0"
|
||||||
},
|
},
|
||||||
|
|||||||
89
apps/expo-github-mobile/src/components/CodeBlock.tsx
Normal file
89
apps/expo-github-mobile/src/components/CodeBlock.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { View, Text, TouchableOpacity, StyleSheet, ScrollView } from 'react-native'
|
||||||
|
import { Ionicons } from '@expo/vector-icons'
|
||||||
|
import { Colors, Spacing, Radius } from '../theme/colors'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
code: string
|
||||||
|
language?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const CodeBlock: React.FC<Props> = ({ code, language = 'text' }) => {
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
|
const handleCopy = () => {
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format language for display
|
||||||
|
const displayLanguage = language || 'text'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{/* Header with language and copy button */}
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text style={styles.language}>{displayLanguage}</Text>
|
||||||
|
<TouchableOpacity style={styles.copyButton} onPress={handleCopy}>
|
||||||
|
<Ionicons
|
||||||
|
name={copied ? 'checkmark' : 'copy-outline'}
|
||||||
|
size={14}
|
||||||
|
color={Colors.secondaryText}
|
||||||
|
/>
|
||||||
|
<Text style={styles.copyText}>
|
||||||
|
{copied ? 'Copied!' : 'Copy'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Code content */}
|
||||||
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||||
|
<Text style={styles.codeText}>{code}</Text>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
backgroundColor: Colors.surfaceBackground,
|
||||||
|
borderRadius: Radius.MD,
|
||||||
|
overflow: 'hidden',
|
||||||
|
marginVertical: Spacing.SM,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: Spacing.MD,
|
||||||
|
paddingVertical: Spacing.SM,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: Colors.chipBorder,
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: '500',
|
||||||
|
color: Colors.secondaryText,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
},
|
||||||
|
copyButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: Spacing.XS,
|
||||||
|
paddingHorizontal: Spacing.SM,
|
||||||
|
paddingVertical: Spacing.XS,
|
||||||
|
},
|
||||||
|
copyText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.secondaryText,
|
||||||
|
},
|
||||||
|
codeText: {
|
||||||
|
fontFamily: 'Menlo',
|
||||||
|
fontSize: 13,
|
||||||
|
lineHeight: 20,
|
||||||
|
color: Colors.primaryText,
|
||||||
|
padding: Spacing.MD,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default CodeBlock
|
||||||
296
apps/expo-github-mobile/src/components/MarkdownView.tsx
Normal file
296
apps/expo-github-mobile/src/components/MarkdownView.tsx
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { View, Text, StyleSheet, ScrollView } from 'react-native'
|
||||||
|
import Markdown, { RenderRules } from 'react-native-markdown-display'
|
||||||
|
import CodeBlock from './CodeBlock'
|
||||||
|
import { Colors, Spacing, Radius } from '../theme/colors'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const MarkdownView: React.FC<Props> = ({ content }) => {
|
||||||
|
// Function to extract language and code from fenced code block
|
||||||
|
const parseCodeBlock = (content: string): { language: string; code: string } => {
|
||||||
|
const lines = content.split('\n')
|
||||||
|
if (lines.length === 0) return { language: 'text', code: content }
|
||||||
|
|
||||||
|
const firstLine = lines[0].trim()
|
||||||
|
// Check if first line looks like a language identifier
|
||||||
|
if (firstLine.length > 0 && firstLine.length < 20 && /^[a-zA-Z0-9+#+-]*$/.test(firstLine)) {
|
||||||
|
return {
|
||||||
|
language: firstLine,
|
||||||
|
code: lines.slice(1).join('\n'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { language: 'text', code: content }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom render rules
|
||||||
|
const rules: RenderRules = {
|
||||||
|
// Code blocks (fenced)
|
||||||
|
fence: (node, children, parent, styles) => {
|
||||||
|
const { language, code } = parseCodeBlock(node.content)
|
||||||
|
return <CodeBlock key={node.key} code={code} language={language} />
|
||||||
|
},
|
||||||
|
// Inline code
|
||||||
|
codeinline: (node, children, parent, styles) => {
|
||||||
|
return (
|
||||||
|
<Text key={node.key} style={markdownStyles.codeinline}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
// Headings
|
||||||
|
heading1: (node, children, parent, styles) => (
|
||||||
|
<Text key={node.key} style={markdownStyles.heading1}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
heading2: (node, children, parent, styles) => (
|
||||||
|
<Text key={node.key} style={markdownStyles.heading2}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
heading3: (node, children, parent, styles) => (
|
||||||
|
<Text key={node.key} style={markdownStyles.heading3}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
heading4: (node, children, parent, styles) => (
|
||||||
|
<Text key={node.key} style={markdownStyles.heading4}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
heading5: (node, children, parent, styles) => (
|
||||||
|
<Text key={node.key} style={markdownStyles.heading5}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
heading6: (node, children, parent, styles) => (
|
||||||
|
<Text key={node.key} style={markdownStyles.heading6}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
// Lists
|
||||||
|
bullet_list: (node, children, parent, styles) => (
|
||||||
|
<View key={node.key} style={markdownStyles.bulletList}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
),
|
||||||
|
ordered_list: (node, children, parent, styles) => (
|
||||||
|
<View key={node.key} style={markdownStyles.bulletList}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
),
|
||||||
|
list_item: (node, children, parent, styles) => (
|
||||||
|
<View key={node.key} style={markdownStyles.listItem}>
|
||||||
|
<Text style={markdownStyles.bullet}>{'\u2022 '}</Text>
|
||||||
|
<View style={markdownStyles.listItemContent}>{children}</View>
|
||||||
|
</View>
|
||||||
|
),
|
||||||
|
// Blockquote
|
||||||
|
blockquote: (node, children, parent, styles) => (
|
||||||
|
<View key={node.key} style={markdownStyles.blockquote}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
),
|
||||||
|
// Links
|
||||||
|
link: (node, children, parent, styles) => (
|
||||||
|
<Text key={node.key} style={markdownStyles.link}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
// Bold
|
||||||
|
strong: (node, children, parent, styles) => (
|
||||||
|
<Text key={node.key} style={markdownStyles.strong}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
// Italic
|
||||||
|
italic: (node, children, parent, styles) => (
|
||||||
|
<Text key={node.key} style={markdownStyles.italic}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
// Strikethrough
|
||||||
|
s: (node, children, parent, styles) => (
|
||||||
|
<Text key={node.key} style={markdownStyles.s}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
// Horizontal rule
|
||||||
|
hr: (node, children, parent, styles) => <View key={node.key} style={markdownStyles.hr} />,
|
||||||
|
// Paragraph
|
||||||
|
paragraph: (node, children, parent, styles) => (
|
||||||
|
<Text key={node.key} style={markdownStyles.paragraph}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
// Hard break
|
||||||
|
hardbreak: (node, children, parent, styles) => <Text key={node.key}>{"\n"}</Text>,
|
||||||
|
// Soft break
|
||||||
|
softbreak: (node, children, parent, styles) => <Text key={node.key}>{"\n"}</Text>,
|
||||||
|
// Table
|
||||||
|
table: (node, children, parent, styles) => (
|
||||||
|
<ScrollView horizontal key={node.key} style={markdownStyles.tableContainer}>
|
||||||
|
<View>{children}</View>
|
||||||
|
</ScrollView>
|
||||||
|
),
|
||||||
|
tbody: (node, children, parent, styles) => (
|
||||||
|
<View key={node.key}>{children}</View>
|
||||||
|
),
|
||||||
|
thead: (node, children, parent, styles) => (
|
||||||
|
<View key={node.key}>{children}</View>
|
||||||
|
),
|
||||||
|
tr: (node, children, parent, styles) => (
|
||||||
|
<View key={node.key} style={markdownStyles.tableRow}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
),
|
||||||
|
th: (node, children, parent, styles) => (
|
||||||
|
<View key={node.key} style={markdownStyles.tableCellHeader}>
|
||||||
|
<Text style={markdownStyles.tableCellText}>{children}</Text>
|
||||||
|
</View>
|
||||||
|
),
|
||||||
|
td: (node, children, parent, styles) => (
|
||||||
|
<View key={node.key} style={markdownStyles.tableCell}>
|
||||||
|
<Text style={markdownStyles.tableCellText}>{children}</Text>
|
||||||
|
</View>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
const markdownStyles = StyleSheet.create({
|
||||||
|
body: {
|
||||||
|
color: Colors.primaryText,
|
||||||
|
fontSize: 16,
|
||||||
|
lineHeight: 24,
|
||||||
|
},
|
||||||
|
paragraph: {
|
||||||
|
marginBottom: Spacing.MD,
|
||||||
|
lineHeight: 24,
|
||||||
|
},
|
||||||
|
heading1: {
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: Colors.primaryText,
|
||||||
|
marginTop: Spacing.LG,
|
||||||
|
marginBottom: Spacing.SM,
|
||||||
|
},
|
||||||
|
heading2: {
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: Colors.primaryText,
|
||||||
|
marginTop: Spacing.MD,
|
||||||
|
marginBottom: Spacing.SM,
|
||||||
|
},
|
||||||
|
heading3: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: Colors.primaryText,
|
||||||
|
marginTop: Spacing.SM,
|
||||||
|
marginBottom: Spacing.XS,
|
||||||
|
},
|
||||||
|
heading4: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: Colors.primaryText,
|
||||||
|
marginTop: Spacing.SM,
|
||||||
|
marginBottom: Spacing.XS,
|
||||||
|
},
|
||||||
|
heading5: {
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: Colors.primaryText,
|
||||||
|
marginTop: Spacing.SM,
|
||||||
|
marginBottom: Spacing.XS,
|
||||||
|
},
|
||||||
|
heading6: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: Colors.primaryText,
|
||||||
|
marginTop: Spacing.SM,
|
||||||
|
marginBottom: Spacing.XS,
|
||||||
|
},
|
||||||
|
codeinline: {
|
||||||
|
backgroundColor: Colors.inputBackground,
|
||||||
|
color: Colors.accentLight,
|
||||||
|
fontFamily: 'Menlo',
|
||||||
|
fontSize: 13,
|
||||||
|
paddingHorizontal: Spacing.SM,
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
bulletList: {
|
||||||
|
marginVertical: Spacing.SM,
|
||||||
|
},
|
||||||
|
listItem: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
marginBottom: Spacing.XS,
|
||||||
|
},
|
||||||
|
bullet: {
|
||||||
|
color: Colors.primaryText,
|
||||||
|
marginRight: Spacing.SM,
|
||||||
|
},
|
||||||
|
listItemContent: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
blockquote: {
|
||||||
|
borderLeftWidth: 3,
|
||||||
|
borderLeftColor: Colors.accent,
|
||||||
|
paddingLeft: Spacing.MD,
|
||||||
|
marginVertical: Spacing.SM,
|
||||||
|
opacity: 0.85,
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
color: Colors.accent,
|
||||||
|
textDecorationLine: 'underline',
|
||||||
|
},
|
||||||
|
strong: {
|
||||||
|
fontWeight: '700',
|
||||||
|
color: Colors.primaryText,
|
||||||
|
},
|
||||||
|
italic: {
|
||||||
|
fontStyle: 'italic',
|
||||||
|
},
|
||||||
|
s: {
|
||||||
|
textDecorationLine: 'line-through',
|
||||||
|
},
|
||||||
|
hr: {
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: Colors.chipBorder,
|
||||||
|
marginVertical: Spacing.MD,
|
||||||
|
},
|
||||||
|
tableContainer: {
|
||||||
|
marginVertical: Spacing.SM,
|
||||||
|
},
|
||||||
|
tableRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: Colors.chipBorder,
|
||||||
|
},
|
||||||
|
tableCell: {
|
||||||
|
padding: Spacing.SM,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
borderRightColor: Colors.chipBorder,
|
||||||
|
},
|
||||||
|
tableCellHeader: {
|
||||||
|
padding: Spacing.SM,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
borderRightColor: Colors.chipBorder,
|
||||||
|
backgroundColor: Colors.inputBackground,
|
||||||
|
},
|
||||||
|
tableCellText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.primaryText,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Markdown style={markdownStyles} rules={rules} mergeStyle={false}>
|
||||||
|
{content}
|
||||||
|
</Markdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MarkdownView
|
||||||
@ -1,8 +1,9 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { View, Text, StyleSheet } from 'react-native'
|
import { View, StyleSheet } from 'react-native'
|
||||||
import { Colors, Spacing } from '../theme/colors'
|
import { Colors, Spacing } from '../theme/colors'
|
||||||
import { Message } from '../models/types'
|
import { Message, MessageRole } from '../models/types'
|
||||||
import ToolCallView from './ToolCallView'
|
import ToolCallView from './ToolCallView'
|
||||||
|
import MarkdownView from './MarkdownView'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: Message
|
message: Message
|
||||||
@ -23,11 +24,21 @@ const MessageView: React.FC<Props> = ({ message }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isUser = message.role === MessageRole.User
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
{/* Message content */}
|
{/* Message content */}
|
||||||
{message.content && (
|
{message.content && (
|
||||||
<Text style={styles.messageContent}>{message.content}</Text>
|
<View style={isUser ? styles.userMessageWrapper : styles.assistantMessageWrapper}>
|
||||||
|
{isUser ? (
|
||||||
|
<View style={styles.userBubble}>
|
||||||
|
<MarkdownView content={message.content} />
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<MarkdownView content={message.content} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tool calls */}
|
{/* Tool calls */}
|
||||||
@ -47,10 +58,19 @@ const styles = StyleSheet.create({
|
|||||||
container: {
|
container: {
|
||||||
gap: Spacing.MD,
|
gap: Spacing.MD,
|
||||||
},
|
},
|
||||||
messageContent: {
|
userMessageWrapper: {
|
||||||
fontSize: 16,
|
alignItems: 'flex-end',
|
||||||
color: Colors.primaryText,
|
},
|
||||||
paddingHorizontal: Spacing.LG,
|
assistantMessageWrapper: {
|
||||||
|
alignItems: 'stretch',
|
||||||
|
},
|
||||||
|
userBubble: {
|
||||||
|
backgroundColor: '#3a3a3a',
|
||||||
|
borderRadius: 18,
|
||||||
|
borderBottomRightRadius: 4,
|
||||||
|
paddingHorizontal: Spacing.MD,
|
||||||
|
paddingVertical: Spacing.SM,
|
||||||
|
maxWidth: '80%',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -82,11 +82,12 @@ const NewSessionSheet: React.FC<Props> = ({ visible, onClose, onSessionCreate })
|
|||||||
if (!messageText.trim() || !selectedRepo) return
|
if (!messageText.trim() || !selectedRepo) return
|
||||||
|
|
||||||
// Convert GitHubRepo to app Repository
|
// Convert GitHubRepo to app Repository
|
||||||
|
// Use selectedBranch (user's choice) not default_branch
|
||||||
const repository: Repository = {
|
const repository: Repository = {
|
||||||
id: String(selectedRepo.id),
|
id: String(selectedRepo.id),
|
||||||
name: selectedRepo.name,
|
name: selectedRepo.name,
|
||||||
owner: selectedRepo.owner.login,
|
owner: selectedRepo.owner.login,
|
||||||
defaultBranch: selectedRepo.default_branch,
|
defaultBranch: selectedBranch || selectedRepo.default_branch,
|
||||||
language: selectedRepo.language || undefined,
|
language: selectedRepo.language || undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { View, Text, ScrollView, TouchableOpacity, StyleSheet } from 'react-native'
|
import { View, Text, ScrollView, TouchableOpacity, StyleSheet, ActivityIndicator } from 'react-native'
|
||||||
import { Ionicons } from '@expo/vector-icons'
|
import { Ionicons } from '@expo/vector-icons'
|
||||||
import { Colors, Spacing, Radius } from '../theme/colors'
|
import { Colors, Spacing, Radius } from '../theme/colors'
|
||||||
import { ToolCall } from '../models/types'
|
import { ToolCall, ToolCallStatus } from '../models/types'
|
||||||
import { getToolColor } from '../theme/types'
|
import { getToolColor } from '../theme/types'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -13,31 +13,103 @@ interface Props {
|
|||||||
|
|
||||||
const ToolCallView: React.FC<Props> = ({ toolCall, isExpanded, onToggle }) => {
|
const ToolCallView: React.FC<Props> = ({ toolCall, isExpanded, onToggle }) => {
|
||||||
const toolColor = getToolColor(toolCall.type)
|
const toolColor = getToolColor(toolCall.type)
|
||||||
|
const isRunning = toolCall.status === ToolCallStatus.Running
|
||||||
|
const isCompleted = toolCall.status === ToolCallStatus.Completed
|
||||||
|
const isFailed = toolCall.status === ToolCallStatus.Failed
|
||||||
|
|
||||||
|
const getStatusIcon = () => {
|
||||||
|
if (isRunning) {
|
||||||
|
return <ActivityIndicator size={14} color={toolColor} />
|
||||||
|
}
|
||||||
|
if (isFailed) {
|
||||||
|
return <Ionicons name="close-circle" size={16} color={Colors.error} />
|
||||||
|
}
|
||||||
|
if (isCompleted) {
|
||||||
|
return <Ionicons name="checkmark-circle" size={16} color={Colors.success} />
|
||||||
|
}
|
||||||
|
return <Ionicons name="ellipse" size={16} color={Colors.tertiaryText} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = () => {
|
||||||
|
if (isRunning) return 'Running'
|
||||||
|
if (isFailed) return 'Failed'
|
||||||
|
if (isCompleted) return 'Completed'
|
||||||
|
return 'Pending'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse input to extract relevant info
|
||||||
|
const getInputDisplay = () => {
|
||||||
|
// For Write/Edit, show just the file path
|
||||||
|
if (toolCall.type === 'Write' || toolCall.type === 'Edit') {
|
||||||
|
const match = toolCall.input.match(/^[\s\S]{0,100}?/)
|
||||||
|
return match ? match[0].trim() : toolCall.input
|
||||||
|
}
|
||||||
|
// For Bash, show the command
|
||||||
|
if (toolCall.type === 'Bash') {
|
||||||
|
const lines = toolCall.input.split('\n')
|
||||||
|
return lines[0]?.trim() || toolCall.input
|
||||||
|
}
|
||||||
|
return toolCall.input
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={[styles.container, isFailed && styles.containerFailed]}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<TouchableOpacity style={styles.header} onPress={onToggle}>
|
<TouchableOpacity
|
||||||
<Text style={[styles.toolType, { color: toolColor }]}>{toolCall.type}</Text>
|
style={[styles.header, isExpanded && styles.headerExpanded]}
|
||||||
<Text style={styles.toolInput} numberOfLines={1}>
|
onPress={onToggle}
|
||||||
{toolCall.input}
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<View style={styles.toolInfo}>
|
||||||
|
{getStatusIcon()}
|
||||||
|
<Text style={[styles.toolType, { color: toolColor }]}>{toolCall.type}</Text>
|
||||||
|
<Text style={styles.statusText}>({getStatusText()})</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.toolInput} numberOfLines={isExpanded ? undefined : 1}>
|
||||||
|
{getInputDisplay()}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={isExpanded ? 'chevron-up' : 'chevron-down'}
|
name={isExpanded ? 'chevron-up' : 'chevron-down'}
|
||||||
size={12}
|
size={16}
|
||||||
color={Colors.tertiaryText}
|
color={Colors.tertiaryText}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* Expanded content */}
|
{/* Expanded input/output */}
|
||||||
{isExpanded && toolCall.output && (
|
{isExpanded && (
|
||||||
<ScrollView
|
<View style={styles.expandedContent}>
|
||||||
horizontal
|
{/* Input section */}
|
||||||
showsHorizontalScrollIndicator={false}
|
<View style={styles.section}>
|
||||||
style={styles.outputContainer}
|
<Text style={styles.sectionLabel}>Input</Text>
|
||||||
>
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||||
<Text style={styles.outputText}>{toolCall.output}</Text>
|
<Text style={styles.inputText}>{toolCall.input}</Text>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Output section */}
|
||||||
|
{toolCall.output && (
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.sectionLabel}>Output</Text>
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
style={styles.outputScroll}
|
||||||
|
>
|
||||||
|
<Text style={styles.outputText}>{toolCall.output}</Text>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Running indicator */}
|
||||||
|
{isRunning && !toolCall.output && (
|
||||||
|
<View style={styles.runningIndicator}>
|
||||||
|
<ActivityIndicator size="small" color={toolColor} />
|
||||||
|
<Text style={styles.runningText}>Executing...</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
@ -49,6 +121,11 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: Radius.MD,
|
borderRadius: Radius.MD,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
marginHorizontal: Spacing.LG,
|
marginHorizontal: Spacing.LG,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
},
|
||||||
|
containerFailed: {
|
||||||
|
borderColor: `${Colors.error}40`,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@ -57,24 +134,73 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: Spacing.MD,
|
paddingHorizontal: Spacing.MD,
|
||||||
paddingVertical: Spacing.SM,
|
paddingVertical: Spacing.SM,
|
||||||
},
|
},
|
||||||
|
headerExpanded: {
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: Colors.chipBorder,
|
||||||
|
},
|
||||||
|
toolInfo: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: Spacing.XS,
|
||||||
|
},
|
||||||
toolType: {
|
toolType: {
|
||||||
fontSize: 14,
|
fontSize: 13,
|
||||||
fontWeight: '500',
|
fontWeight: '600',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
},
|
||||||
|
statusText: {
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.tertiaryText,
|
||||||
},
|
},
|
||||||
toolInput: {
|
toolInput: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: 14,
|
fontSize: 13,
|
||||||
color: Colors.secondaryText,
|
color: Colors.secondaryText,
|
||||||
|
fontFamily: 'Menlo',
|
||||||
},
|
},
|
||||||
outputContainer: {
|
expandedContent: {
|
||||||
backgroundColor: Colors.surfaceBackground,
|
backgroundColor: Colors.surfaceBackground,
|
||||||
|
},
|
||||||
|
section: {
|
||||||
|
paddingVertical: Spacing.SM,
|
||||||
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderBottomColor: Colors.chipBorder,
|
||||||
|
},
|
||||||
|
sectionLabel: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: Colors.tertiaryText,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
paddingHorizontal: Spacing.MD,
|
||||||
|
paddingBottom: Spacing.XS,
|
||||||
|
},
|
||||||
|
inputText: {
|
||||||
|
fontFamily: 'Menlo',
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.primaryText,
|
||||||
|
paddingHorizontal: Spacing.MD,
|
||||||
|
paddingBottom: Spacing.SM,
|
||||||
|
},
|
||||||
|
outputScroll: {
|
||||||
maxHeight: 200,
|
maxHeight: 200,
|
||||||
},
|
},
|
||||||
outputText: {
|
outputText: {
|
||||||
fontFamily: 'Menlo',
|
fontFamily: 'Menlo',
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Colors.secondaryText,
|
color: Colors.secondaryText,
|
||||||
padding: Spacing.MD,
|
paddingHorizontal: Spacing.MD,
|
||||||
|
paddingBottom: Spacing.SM,
|
||||||
|
},
|
||||||
|
runningIndicator: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: Spacing.SM,
|
||||||
|
paddingVertical: Spacing.MD,
|
||||||
|
},
|
||||||
|
runningText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.tertiaryText,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { useEffect, useState, useRef, useCallback } from 'react'
|
import { useEffect, useState, useRef, useCallback } from 'react'
|
||||||
import { GatewayClient, type AgentEventPayload } from '../services/gateway'
|
import { GatewayClient, type AgentEventPayload, type AgentFinalResult } from '../services/gateway'
|
||||||
import { useSettings } from './useSettings'
|
import { useSettings } from './useSettings'
|
||||||
|
|
||||||
|
export type { AgentFinalResult }
|
||||||
|
|
||||||
export type ToolCallEvent = {
|
export type ToolCallEvent = {
|
||||||
toolCallId: string
|
toolCallId: string
|
||||||
name: string
|
name: string
|
||||||
@ -41,7 +43,10 @@ export const useGateway = () => {
|
|||||||
if (settingsLoading) return
|
if (settingsLoading) return
|
||||||
if (!settings.gatewayUrl) return
|
if (!settings.gatewayUrl) return
|
||||||
|
|
||||||
const client = new GatewayClient(settings.gatewayUrl)
|
const url = settings.gatewayUrl.trim()
|
||||||
|
if (!url) return
|
||||||
|
|
||||||
|
const client = new GatewayClient(url, settings.gatewayToken)
|
||||||
clientRef.current = client
|
clientRef.current = client
|
||||||
|
|
||||||
setState((s) => ({ ...s, connecting: true, error: null }))
|
setState((s) => ({ ...s, connecting: true, error: null }))
|
||||||
@ -49,10 +54,12 @@ export const useGateway = () => {
|
|||||||
// Handle agent events
|
// Handle agent events
|
||||||
const unsubscribe = client.onEvent('agent', (payload) => {
|
const unsubscribe = client.onEvent('agent', (payload) => {
|
||||||
const evt = payload as AgentEventPayload
|
const evt = payload as AgentEventPayload
|
||||||
|
console.log('[useGateway] Agent event:', evt.stream, JSON.stringify(evt.data).slice(0, 100))
|
||||||
|
|
||||||
switch (evt.stream) {
|
switch (evt.stream) {
|
||||||
case 'tool': {
|
case 'tool': {
|
||||||
const data = evt.data as ToolCallEvent
|
const data = evt.data as ToolCallEvent
|
||||||
|
console.log('[useGateway] Tool event:', data.name, data.phase)
|
||||||
for (const handler of toolCallHandlersRef.current) {
|
for (const handler of toolCallHandlersRef.current) {
|
||||||
handler(data)
|
handler(data)
|
||||||
}
|
}
|
||||||
@ -60,12 +67,14 @@ export const useGateway = () => {
|
|||||||
}
|
}
|
||||||
case 'assistant': {
|
case 'assistant': {
|
||||||
const data = evt.data as AssistantEvent
|
const data = evt.data as AssistantEvent
|
||||||
|
console.log('[useGateway] Assistant event, delta length:', data.delta?.length ?? 0)
|
||||||
for (const handler of assistantHandlersRef.current) {
|
for (const handler of assistantHandlersRef.current) {
|
||||||
handler(data)
|
handler(data)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'lifecycle': {
|
case 'lifecycle': {
|
||||||
|
console.log('[useGateway] Lifecycle event:', evt.data)
|
||||||
for (const handler of lifecycleHandlersRef.current) {
|
for (const handler of lifecycleHandlersRef.current) {
|
||||||
handler(evt.data)
|
handler(evt.data)
|
||||||
}
|
}
|
||||||
@ -88,11 +97,12 @@ export const useGateway = () => {
|
|||||||
clientRef.current = null
|
clientRef.current = null
|
||||||
setState({ connected: false, connecting: false, error: null })
|
setState({ connected: false, connecting: false, error: null })
|
||||||
}
|
}
|
||||||
}, [settings.gatewayUrl, settingsLoading])
|
}, [settings.gatewayUrl?.trim(), settingsLoading])
|
||||||
|
|
||||||
const sendMessage = useCallback(async (
|
const sendMessage = useCallback(async (
|
||||||
message: string,
|
message: string,
|
||||||
repoContext?: { owner: string; name: string; branch: string }
|
repoContext?: { owner: string; name: string; branch: string },
|
||||||
|
onFinalResult?: (result: AgentFinalResult) => void
|
||||||
): Promise<{ status: string; runId: string } | null> => {
|
): Promise<{ status: string; runId: string } | null> => {
|
||||||
const client = clientRef.current
|
const client = clientRef.current
|
||||||
if (!client?.connected) {
|
if (!client?.connected) {
|
||||||
@ -101,11 +111,14 @@ export const useGateway = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await client.startAgent({
|
const result = await client.startAgent(
|
||||||
message,
|
{
|
||||||
repoContext,
|
message,
|
||||||
idempotencyKey: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
repoContext,
|
||||||
})
|
idempotencyKey: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||||
|
},
|
||||||
|
onFinalResult
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[useGateway] Failed to send message', e)
|
console.error('[useGateway] Failed to send message', e)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||||
|
|
||||||
const SETTINGS_KEY = '@clawdbot_settings'
|
const SETTINGS_KEY = '@clawdbot_settings'
|
||||||
@ -6,24 +6,32 @@ const SETTINGS_KEY = '@clawdbot_settings'
|
|||||||
export interface Settings {
|
export interface Settings {
|
||||||
githubUsername: string
|
githubUsername: string
|
||||||
gatewayUrl: string
|
gatewayUrl: string
|
||||||
|
gatewayToken?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default settings from environment variables
|
||||||
const DEFAULT_SETTINGS: Settings = {
|
const DEFAULT_SETTINGS: Settings = {
|
||||||
githubUsername: '',
|
githubUsername: process.env.EXPO_PUBLIC_GITHUB_USERNAME || '',
|
||||||
gatewayUrl: 'ws://localhost:18789',
|
gatewayUrl: process.env.EXPO_PUBLIC_GATEWAY_URL || 'ws://localhost:18789',
|
||||||
|
gatewayToken: process.env.EXPO_PUBLIC_GATEWAY_TOKEN || '',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadSettings = async (): Promise<Settings> => {
|
export const loadSettings = async (): Promise<Settings> => {
|
||||||
try {
|
try {
|
||||||
const raw = await AsyncStorage.getItem(SETTINGS_KEY)
|
const raw = await AsyncStorage.getItem(SETTINGS_KEY)
|
||||||
if (raw) {
|
if (raw) {
|
||||||
const parsed = JSON.parse(raw)
|
const saved = JSON.parse(raw) as Partial<Settings>
|
||||||
return { ...DEFAULT_SETTINGS, ...parsed }
|
// Merge with defaults (env vars take precedence for empty saved values)
|
||||||
|
return {
|
||||||
|
githubUsername: saved.githubUsername || DEFAULT_SETTINGS.githubUsername,
|
||||||
|
gatewayUrl: saved.gatewayUrl || DEFAULT_SETTINGS.gatewayUrl,
|
||||||
|
gatewayToken: saved.gatewayToken || DEFAULT_SETTINGS.gatewayToken,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
return DEFAULT_SETTINGS
|
||||||
console.error('Failed to load settings', e)
|
} catch {
|
||||||
|
return DEFAULT_SETTINGS
|
||||||
}
|
}
|
||||||
return DEFAULT_SETTINGS
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const saveSettings = async (settings: Settings): Promise<void> => {
|
export const saveSettings = async (settings: Settings): Promise<void> => {
|
||||||
@ -39,17 +47,17 @@ export const useSettings = () => {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSettings().then((loaded) => {
|
loadSettings().then((s) => {
|
||||||
setSettings(loaded)
|
setSettings(s)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const updateSettings = async (updates: Partial<Settings>) => {
|
const updateSettings = useCallback(async (updates: Partial<Settings>) => {
|
||||||
const newSettings = { ...settings, ...updates }
|
const newSettings = { ...settings, ...updates }
|
||||||
setSettings(newSettings)
|
setSettings(newSettings)
|
||||||
await saveSettings(newSettings)
|
await saveSettings(newSettings)
|
||||||
}
|
}, [settings])
|
||||||
|
|
||||||
return { settings, updateSettings, loading }
|
return { settings, updateSettings, loading }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,21 @@ export type AgentEventPayload = {
|
|||||||
data: Record<string, unknown>
|
data: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AgentFinalResult = {
|
||||||
|
runId: string
|
||||||
|
status: 'ok' | 'error'
|
||||||
|
summary?: string
|
||||||
|
error?: string
|
||||||
|
result?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protocol version must match gateway (see src/gateway/protocol/schema/protocol-schemas.ts)
|
||||||
|
const PROTOCOL_VERSION = 3
|
||||||
|
|
||||||
|
// Valid client IDs from gateway
|
||||||
|
const CLIENT_ID = 'clawdbot-ios'
|
||||||
|
const CLIENT_MODE = 'webchat'
|
||||||
|
|
||||||
export class GatewayClient {
|
export class GatewayClient {
|
||||||
private ws: WebSocket | null = null
|
private ws: WebSocket | null = null
|
||||||
private pending = new Map<string, {
|
private pending = new Map<string, {
|
||||||
@ -33,30 +48,52 @@ export class GatewayClient {
|
|||||||
reject: (err: Error) => void
|
reject: (err: Error) => void
|
||||||
}>()
|
}>()
|
||||||
private eventHandlers = new Map<string, Set<(payload: unknown) => void>>()
|
private eventHandlers = new Map<string, Set<(payload: unknown) => void>>()
|
||||||
|
private agentCallbacks = new Map<string, (result: AgentFinalResult) => void>()
|
||||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
private reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
private reconnectDelay = 1000
|
private reconnectDelay = 1000
|
||||||
private messageQueue: string[] = []
|
private messageQueue: string[] = []
|
||||||
|
private handshakeComplete = false
|
||||||
|
private authToken?: string
|
||||||
|
|
||||||
constructor(private url: string) {}
|
constructor(private url: string, authToken?: string) {
|
||||||
|
this.authToken = authToken
|
||||||
|
}
|
||||||
|
|
||||||
connect(): Promise<void> {
|
connect(): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
|
this.handshakeComplete = false
|
||||||
this.ws = new WebSocket(this.url)
|
this.ws = new WebSocket(this.url)
|
||||||
|
|
||||||
this.ws.onopen = () => {
|
this.ws.onopen = () => {
|
||||||
console.log('[Gateway] Connected')
|
console.log('[Gateway] WebSocket opened, sending connect handshake...')
|
||||||
// Send queued messages
|
// Send connect handshake as first message
|
||||||
for (const msg of this.messageQueue) {
|
this.sendConnectHandshake()
|
||||||
this.ws?.send(msg)
|
.then(() => {
|
||||||
}
|
console.log('[Gateway] Handshake complete')
|
||||||
this.messageQueue = []
|
this.handshakeComplete = true
|
||||||
resolve()
|
// Send queued messages
|
||||||
|
for (const msg of this.messageQueue) {
|
||||||
|
this.ws?.send(msg)
|
||||||
|
}
|
||||||
|
this.messageQueue = []
|
||||||
|
this.reconnectDelay = 1000 // Reset on successful connect
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('[Gateway] Handshake failed', err)
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
this.ws.onmessage = (event) => this.handleMessage(event.data)
|
|
||||||
|
this.ws.onmessage = (event) => this.handleMessage(event.data as string)
|
||||||
|
|
||||||
this.ws.onclose = (event) => {
|
this.ws.onclose = (event) => {
|
||||||
console.log('[Gateway] Disconnected', event.code, event.reason)
|
console.log('[Gateway] Disconnected', event.code, event.reason)
|
||||||
|
this.handshakeComplete = false
|
||||||
this.scheduleReconnect()
|
this.scheduleReconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ws.onerror = (error) => {
|
this.ws.onerror = (error) => {
|
||||||
console.error('[Gateway] Error', error)
|
console.error('[Gateway] Error', error)
|
||||||
reject(error)
|
reject(error)
|
||||||
@ -67,6 +104,29 @@ export class GatewayClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async sendConnectHandshake(): Promise<void> {
|
||||||
|
const connectParams = {
|
||||||
|
client: {
|
||||||
|
id: CLIENT_ID,
|
||||||
|
displayName: 'Clawdbot Mobile',
|
||||||
|
version: '1.0.0',
|
||||||
|
mode: CLIENT_MODE,
|
||||||
|
platform: 'ios',
|
||||||
|
},
|
||||||
|
minProtocol: PROTOCOL_VERSION,
|
||||||
|
maxProtocol: PROTOCOL_VERSION,
|
||||||
|
...(this.authToken ? { auth: { token: this.authToken } } : {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.sendRaw('connect', connectParams)
|
||||||
|
|
||||||
|
// Check if we got hello-ok
|
||||||
|
const payload = result as { type?: string; protocol?: number }
|
||||||
|
if (payload?.type !== 'hello-ok') {
|
||||||
|
throw new Error('Expected hello-ok response from gateway')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
if (this.reconnectTimer) {
|
if (this.reconnectTimer) {
|
||||||
clearTimeout(this.reconnectTimer)
|
clearTimeout(this.reconnectTimer)
|
||||||
@ -76,6 +136,7 @@ export class GatewayClient {
|
|||||||
this.ws.close()
|
this.ws.close()
|
||||||
this.ws = null
|
this.ws = null
|
||||||
}
|
}
|
||||||
|
this.handshakeComplete = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private scheduleReconnect() {
|
private scheduleReconnect() {
|
||||||
@ -93,20 +154,43 @@ export class GatewayClient {
|
|||||||
private handleMessage(data: string) {
|
private handleMessage(data: string) {
|
||||||
try {
|
try {
|
||||||
const frame: GatewayFrame = JSON.parse(data)
|
const frame: GatewayFrame = JSON.parse(data)
|
||||||
|
console.log('[Gateway] <<< Received frame:', frame.type, 'event' in frame ? (frame as { event?: string }).event : '')
|
||||||
|
|
||||||
if (frame.type === 'res') {
|
if (frame.type === 'res') {
|
||||||
const pending = this.pending.get(frame.id)
|
const pending = this.pending.get(frame.id)
|
||||||
if (pending) {
|
if (pending) {
|
||||||
this.pending.delete(frame.id)
|
this.pending.delete(frame.id)
|
||||||
if (frame.ok) {
|
if (frame.ok) {
|
||||||
|
console.log('[Gateway] Response OK:', JSON.stringify(frame.payload).slice(0, 200))
|
||||||
pending.resolve(frame.payload)
|
pending.resolve(frame.payload)
|
||||||
} else {
|
} else {
|
||||||
|
console.log('[Gateway] Response ERROR:', frame.error?.message)
|
||||||
pending.reject(new Error(frame.error?.message || 'Request failed'))
|
pending.reject(new Error(frame.error?.message || 'Request failed'))
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// This is a follow-up response (e.g., agent completion/error)
|
||||||
|
const payload = frame.payload as { runId?: string; status?: string; summary?: string; error?: unknown } | undefined
|
||||||
|
console.log('[Gateway] Follow-up response:', frame.ok ? 'OK' : 'ERROR', JSON.stringify(payload).slice(0, 300))
|
||||||
|
|
||||||
|
if (payload?.runId) {
|
||||||
|
const callback = this.agentCallbacks.get(payload.runId)
|
||||||
|
if (callback) {
|
||||||
|
this.agentCallbacks.delete(payload.runId)
|
||||||
|
callback({
|
||||||
|
runId: payload.runId,
|
||||||
|
status: payload.status === 'error' || !frame.ok ? 'error' : 'ok',
|
||||||
|
summary: payload.summary,
|
||||||
|
error: frame.error?.message || (payload.error ? String(payload.error) : undefined),
|
||||||
|
result: payload,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (frame.type === 'event') {
|
} else if (frame.type === 'event') {
|
||||||
|
console.log('[Gateway] Event received:', frame.event, JSON.stringify(frame.payload).slice(0, 200))
|
||||||
const handlers = this.eventHandlers.get(frame.event)
|
const handlers = this.eventHandlers.get(frame.event)
|
||||||
if (handlers) {
|
if (handlers) {
|
||||||
|
console.log('[Gateway] Found', handlers.size, 'handlers for event:', frame.event)
|
||||||
for (const handler of handlers) {
|
for (const handler of handlers) {
|
||||||
try {
|
try {
|
||||||
handler(frame.payload)
|
handler(frame.payload)
|
||||||
@ -114,6 +198,8 @@ export class GatewayClient {
|
|||||||
console.error('[Gateway] Event handler error', e)
|
console.error('[Gateway] Event handler error', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.log('[Gateway] No handlers for event:', frame.event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -121,6 +207,24 @@ export class GatewayClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send without waiting for handshake (used for connect handshake itself)
|
||||||
|
private sendRaw(method: string, params: Record<string, unknown>): Promise<unknown> {
|
||||||
|
const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||||
|
const frame = { type: 'req', method, params, id }
|
||||||
|
const message = JSON.stringify(frame)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.pending.set(id, { resolve, reject })
|
||||||
|
|
||||||
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.send(message)
|
||||||
|
} else {
|
||||||
|
reject(new Error('WebSocket not open'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send after handshake complete
|
||||||
private send(frame: Record<string, unknown>): Promise<unknown> {
|
private send(frame: Record<string, unknown>): Promise<unknown> {
|
||||||
const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||||
const message = JSON.stringify({ ...frame, id })
|
const message = JSON.stringify({ ...frame, id })
|
||||||
@ -128,7 +232,7 @@ export class GatewayClient {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.pending.set(id, { resolve, reject })
|
this.pending.set(id, { resolve, reject })
|
||||||
|
|
||||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
if (this.ws?.readyState === WebSocket.OPEN && this.handshakeComplete) {
|
||||||
this.ws.send(message)
|
this.ws.send(message)
|
||||||
} else {
|
} else {
|
||||||
this.messageQueue.push(message)
|
this.messageQueue.push(message)
|
||||||
@ -140,9 +244,23 @@ export class GatewayClient {
|
|||||||
return this.send({ type: 'req', method, params })
|
return this.send({ type: 'req', method, params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async startAgent(params: AgentRequestParams): Promise<{ status: string; runId: string }> {
|
async startAgent(
|
||||||
const result = await this.call('agent', params)
|
params: AgentRequestParams,
|
||||||
return result as { status: string; runId: string }
|
onFinalResult?: (result: AgentFinalResult) => void
|
||||||
|
): Promise<{ status: string; runId: string }> {
|
||||||
|
// Default to "main" agent if not specified
|
||||||
|
const agentId = params.agentId || 'main'
|
||||||
|
const fullParams = { ...params, agentId }
|
||||||
|
console.log('[Gateway] >>> Sending agent request:', params.message.slice(0, 50), 'agentId:', agentId)
|
||||||
|
const result = await this.call('agent', fullParams) as { status: string; runId: string }
|
||||||
|
console.log('[Gateway] <<< Agent accepted:', JSON.stringify(result))
|
||||||
|
|
||||||
|
// Register callback for final result
|
||||||
|
if (onFinalResult && result.runId) {
|
||||||
|
this.agentCallbacks.set(result.runId, onFinalResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
onEvent(event: string, handler: (payload: unknown) => void): () => void {
|
onEvent(event: string, handler: (payload: unknown) => void): () => void {
|
||||||
@ -156,6 +274,6 @@ export class GatewayClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get connected() {
|
get connected() {
|
||||||
return this.ws?.readyState === WebSocket.OPEN
|
return this.ws?.readyState === WebSocket.OPEN && this.handshakeComplete
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -173,20 +173,41 @@ const ChatView: React.FC<Props> = ({ session, onBack }) => {
|
|||||||
scrollViewRef.current?.scrollToEnd({ animated: true })
|
scrollViewRef.current?.scrollToEnd({ animated: true })
|
||||||
}, 50)
|
}, 50)
|
||||||
|
|
||||||
// Send to gateway
|
// Send to gateway with repo context for workspace management
|
||||||
const result = await sendGatewayMessage(textToSend, {
|
const repoContext = {
|
||||||
owner: currentSession.repository.owner,
|
owner: currentSession.repository.owner,
|
||||||
name: currentSession.repository.name,
|
name: currentSession.repository.name,
|
||||||
branch: currentSession.repository.defaultBranch,
|
branch: currentSession.repository.defaultBranch,
|
||||||
})
|
}
|
||||||
|
console.log('[ChatView] Sending with repoContext:', repoContext)
|
||||||
|
|
||||||
|
// Handle final result (success or error from agent)
|
||||||
|
const handleFinalResult = (finalResult: { status: string; error?: string; summary?: string }) => {
|
||||||
|
console.log('[ChatView] Agent final result:', finalResult)
|
||||||
|
if (finalResult.status === 'error') {
|
||||||
|
setIsRunning(false)
|
||||||
|
setMessages((prev) => {
|
||||||
|
const errorMsg: Message = {
|
||||||
|
id: `msg-${Date.now()}-agent-error`,
|
||||||
|
role: MessageRole.Assistant,
|
||||||
|
content: `⚠️ Agent Error\n\n${finalResult.error || finalResult.summary || 'Unknown error'}\n\nRepo: ${repoContext.owner}/${repoContext.name}\nBranch: ${repoContext.branch}`,
|
||||||
|
timestamp: new Date(),
|
||||||
|
toolCalls: [],
|
||||||
|
}
|
||||||
|
return [...prev, errorMsg]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await sendGatewayMessage(textToSend, repoContext, handleFinalResult)
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
// Failed to send - show error
|
// Failed to send - show error with details
|
||||||
setMessages((prev) => {
|
setMessages((prev) => {
|
||||||
const errorMsg: Message = {
|
const errorMsg: Message = {
|
||||||
id: `msg-${Date.now()}-error`,
|
id: `msg-${Date.now()}-error`,
|
||||||
role: MessageRole.Assistant,
|
role: MessageRole.Assistant,
|
||||||
content: 'Failed to connect to clawdbot gateway. Please check your settings.',
|
content: `Failed to connect to clawdbot gateway.\n\nRepo: ${repoContext.owner}/${repoContext.name}\nBranch: ${repoContext.branch}\n\nPlease check:\n• Gateway is running\n• Settings are correct\n• The repository exists on GitHub`,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
toolCalls: [],
|
toolCalls: [],
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,9 +20,14 @@ const SettingsView: React.FC<Props> = ({ onBack }) => {
|
|||||||
const { settings, updateSettings } = useSettings()
|
const { settings, updateSettings } = useSettings()
|
||||||
const [githubUsername, setGithubUsername] = useState(settings.githubUsername)
|
const [githubUsername, setGithubUsername] = useState(settings.githubUsername)
|
||||||
const [gatewayUrl, setGatewayUrl] = useState(settings.gatewayUrl)
|
const [gatewayUrl, setGatewayUrl] = useState(settings.gatewayUrl)
|
||||||
|
const [gatewayToken, setGatewayToken] = useState(settings.gatewayToken || '')
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
await updateSettings({ githubUsername, gatewayUrl })
|
await updateSettings({
|
||||||
|
githubUsername,
|
||||||
|
gatewayUrl: gatewayUrl.trim(),
|
||||||
|
gatewayToken: gatewayToken.trim() || undefined,
|
||||||
|
})
|
||||||
Alert.alert('Settings Saved', 'Your settings have been saved successfully.')
|
Alert.alert('Settings Saved', 'Your settings have been saved successfully.')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,7 +66,7 @@ const SettingsView: React.FC<Props> = ({ onBack }) => {
|
|||||||
style={styles.input}
|
style={styles.input}
|
||||||
value={gatewayUrl}
|
value={gatewayUrl}
|
||||||
onChangeText={setGatewayUrl}
|
onChangeText={setGatewayUrl}
|
||||||
placeholder="ws://localhost:18789"
|
placeholder="ws://192.168.1.x:18789"
|
||||||
placeholderTextColor={Colors.secondaryText}
|
placeholderTextColor={Colors.secondaryText}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
@ -69,6 +74,22 @@ const SettingsView: React.FC<Props> = ({ onBack }) => {
|
|||||||
<Text style={styles.hint}>WebSocket URL of your clawdbot gateway</Text>
|
<Text style={styles.hint}>WebSocket URL of your clawdbot gateway</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Gateway Token */}
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.label}>Gateway Token (Optional)</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
value={gatewayToken}
|
||||||
|
onChangeText={setGatewayToken}
|
||||||
|
placeholder="Enter gateway token if required"
|
||||||
|
placeholderTextColor={Colors.secondaryText}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
secureTextEntry
|
||||||
|
/>
|
||||||
|
<Text style={styles.hint}>Required if gateway auth is enabled (check ~/.clawdbot/clawdbot.json)</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Save Button */}
|
{/* Save Button */}
|
||||||
<TouchableOpacity style={styles.saveButton} onPress={handleSave}>
|
<TouchableOpacity style={styles.saveButton} onPress={handleSave}>
|
||||||
<Text style={styles.saveButtonText}>Save Settings</Text>
|
<Text style={styles.saveButtonText}>Save Settings</Text>
|
||||||
|
|||||||
1
dist/control-ui/assets/index-BPDeGGxb.css
vendored
1
dist/control-ui/assets/index-BPDeGGxb.css
vendored
File diff suppressed because one or more lines are too long
3047
dist/control-ui/assets/index-bYQnHP3a.js
vendored
3047
dist/control-ui/assets/index-bYQnHP3a.js
vendored
File diff suppressed because one or more lines are too long
1
dist/control-ui/assets/index-bYQnHP3a.js.map
vendored
1
dist/control-ui/assets/index-bYQnHP3a.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/control-ui/index.html
vendored
4
dist/control-ui/index.html
vendored
@ -6,8 +6,8 @@
|
|||||||
<title>Clawdbot Control</title>
|
<title>Clawdbot Control</title>
|
||||||
<meta name="color-scheme" content="dark light" />
|
<meta name="color-scheme" content="dark light" />
|
||||||
<link rel="icon" href="./favicon.ico" sizes="any" />
|
<link rel="icon" href="./favicon.ico" sizes="any" />
|
||||||
<script type="module" crossorigin src="./assets/index-bYQnHP3a.js"></script>
|
<script type="module" crossorigin src="./assets/index-B0_pGVGK.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-BPDeGGxb.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-B_dnGukZ.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<clawdbot-app></clawdbot-app>
|
<clawdbot-app></clawdbot-app>
|
||||||
|
|||||||
4651
pnpm-lock.yaml
generated
4651
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user