Skip to content

Commit c095f10

Browse files
authored
Merge pull request #700 from xiaomakuaiz/260614-mobile-manual-repo-url
feat(mobile): 新建任务选择仓库列表支持手动输入仓库地址
2 parents d226712 + 54f80b6 commit c095f10

2 files changed

Lines changed: 99 additions & 7 deletions

File tree

mobile/app/new-task.tsx

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,25 @@ import type { Model, Project } from '@/api/types';
99
import { ConcurrentLimitModal } from '@/components/ConcurrentLimitModal';
1010
import { Icons, providerIconForUrl } from '@/components/Icons';
1111
import { MicButton } from '@/components/MicButton';
12-
import { ModelSheet } from '@/components/sheets';
12+
import { ModelSheet, RepoUrlSheet } from '@/components/sheets';
1313
import { Card, IconButton, MonkeyLogo, PickerSheet, PrimaryButton, type PickerOption } from '@/components/ui';
1414
import { useSpeechToText } from '@/speech/useSpeechToText';
1515
import { DEFAULT_SKILL_IDS, modelLabel, pickDefaultImage, pickDefaultModel, TASK_DEFAULTS } from '@/config';
1616
import { spacing, useTheme, type Theme } from '@/theme';
1717

1818
const SUGGESTIONS = ['修复一个线上 bug', '为这个仓库写单元测试', '重构这个模块', '解释这段代码做了什么'];
1919

20+
// 「选择仓库」列表里的「手动输入仓库地址」入口标识(区别于真实 project.id)
21+
const MANUAL_REPO_KEY = '__manual_repo__';
22+
23+
/** 从 Git 地址里取 owner/repo 作为简短展示名(取不到则回退为整段地址)。 */
24+
function repoNameFromUrl(url: string): string {
25+
const cleaned = url.trim().replace(/\.git$/i, '').replace(/\/+$/, '');
26+
const m = cleaned.match(/[/:]([^/:]+\/[^/:]+)$/);
27+
if (m) return m[1];
28+
return cleaned.split(/[/:]/).filter(Boolean).pop() || url;
29+
}
30+
2031
function ConfigRow({ icon, label, value, sub, divider, onPress, t }: { icon: string; label: string; value: string; sub?: string; divider?: boolean; onPress: () => void; t: Theme }) {
2132
const I = Icons[icon];
2233
return (
@@ -50,9 +61,11 @@ export default function NewTaskScreen() {
5061
const [content, setContent] = useState('');
5162
const [modelId, setModelId] = useState('');
5263
const [imageId, setImageId] = useState('');
53-
const [repoKey, setRepoKey] = useState<string>(params.projectId || ''); // '' = 不关联仓库;否则为 project.id
64+
const [repoKey, setRepoKey] = useState<string>(params.projectId || ''); // '' = 不关联仓库;project.id = 选中项目;MANUAL_REPO_KEY = 手动输入
65+
const [manualRepo, setManualRepo] = useState(''); // 手动输入的 Git 仓库地址
5466

5567
const [picking, setPicking] = useState<'repo' | 'model' | null>(null);
68+
const [manualOpen, setManualOpen] = useState(false); // 手动输入仓库地址对话框
5669
const [limitOpen, setLimitOpen] = useState(false);
5770
const [submitting, setSubmitting] = useState(false);
5871
const [error, setError] = useState('');
@@ -95,6 +108,7 @@ export default function NewTaskScreen() {
95108

96109
const repoOptions: PickerOption[] = [
97110
{ key: '', title: '快速开始', sub: '不关联仓库', icon: 'sparkle' },
111+
{ key: MANUAL_REPO_KEY, title: '手动输入仓库地址', sub: manualRepo || '填写 Git 仓库地址', icon: manualRepo ? providerIconForUrl(manualRepo) : 'git' },
98112
...projects.map((p, i) => ({ key: p.id || `p${i}`, title: p.name || p.full_name || '项目', sub: p.repo_url, icon: providerIconForUrl(p.repo_url) })),
99113
];
100114

@@ -104,7 +118,11 @@ export default function NewTaskScreen() {
104118
if (!modelId) { setError('请选择模型'); return; }
105119
setSubmitting(true);
106120
try {
107-
const repo = selectedProject ? { repo_url: selectedProject.repo_url || undefined } : {};
121+
// 手动输入的仓库地址优先;否则用所选项目;都没有则不关联仓库(快速开始)
122+
const manualUrl = repoKey === MANUAL_REPO_KEY ? manualRepo.trim() : '';
123+
const repo = manualUrl
124+
? { repo_url: manualUrl }
125+
: selectedProject ? { repo_url: selectedProject.repo_url || undefined } : {};
108126
const task = await createTask({
109127
content: content.trim(),
110128
cli_name: TASK_DEFAULTS.cliName,
@@ -114,7 +132,7 @@ export default function NewTaskScreen() {
114132
task_type: 'develop',
115133
repo,
116134
resource: { ...TASK_DEFAULTS.resource },
117-
extra: { skill_ids: DEFAULT_SKILL_IDS, project_id: selectedProject?.id },
135+
extra: { skill_ids: DEFAULT_SKILL_IDS, project_id: manualUrl ? undefined : selectedProject?.id },
118136
});
119137
if (task?.id) router.replace(`/task/${task.id}`);
120138
else setError('任务创建成功但未返回 ID');
@@ -124,10 +142,12 @@ export default function NewTaskScreen() {
124142
} finally {
125143
setSubmitting(false);
126144
}
127-
}, [content, imageId, modelId, router, selectedProject]);
145+
}, [content, imageId, modelId, router, selectedProject, repoKey, manualRepo]);
128146

129147
// 仓库行只展示一处信息,避免「快速开始 / 不关联仓库」「名字 / 同名仓库路径」这种左右重复。
130-
const repoValue = selectedProject ? (selectedProject.full_name || selectedProject.name || '项目') : '不关联仓库';
148+
const repoValue = repoKey === MANUAL_REPO_KEY && manualRepo
149+
? repoNameFromUrl(manualRepo)
150+
: selectedProject ? (selectedProject.full_name || selectedProject.name || '项目') : '不关联仓库';
131151

132152
return (
133153
<KeyboardAvoidingView style={{ flex: 1, backgroundColor: t.bg }} behavior="padding">
@@ -199,7 +219,14 @@ export default function NewTaskScreen() {
199219
) : null}
200220

201221
<PickerSheet visible={picking === 'repo'} title="选择仓库" options={repoOptions} selected={repoKey}
202-
onPick={(k) => { setRepoKey(k); setPicking(null); }} onClose={() => setPicking(null)} />
222+
onPick={(k) => {
223+
// 「手动输入仓库地址」不直接选中,而是先收起列表、弹出输入框
224+
if (k === MANUAL_REPO_KEY) { setPicking(null); setManualOpen(true); return; }
225+
setRepoKey(k); setPicking(null);
226+
}} onClose={() => setPicking(null)} />
227+
<RepoUrlSheet visible={manualOpen} initialUrl={manualRepo}
228+
onConfirm={(u) => { setManualRepo(u); setRepoKey(MANUAL_REPO_KEY); setManualOpen(false); }}
229+
onClose={() => setManualOpen(false)} />
203230
<ModelSheet visible={picking === 'model'} models={models} selectedId={modelId} plan={plan}
204231
onPick={(k) => { setModelId(k); setPicking(null); }} onClose={() => setPicking(null)} />
205232
<ConcurrentLimitModal visible={limitOpen} onClose={() => setLimitOpen(false)} onStopped={() => { setLimitOpen(false); setTimeout(() => submit(), 400); }} />

mobile/src/components/sheets.tsx

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
import React from 'react';
77
import { Modal, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
8+
import { KeyboardAvoidingView } from 'react-native-keyboard-controller';
89
import { useSafeAreaInsets } from 'react-native-safe-area-context';
910
import type { Model } from '@/api/types';
1011
import type { PortForwardInfo } from '@/api/control';
@@ -206,3 +207,67 @@ export function CopySheet({ visible, text, onClose, onCopyAll }: {
206207
</Modal>
207208
);
208209
}
210+
211+
/**
212+
* 手动输入仓库地址对话框:在「选择仓库」列表里点「手动输入仓库地址」后弹出,
213+
* 让用户直接填写 Git 仓库地址(无需先在后台创建项目)。居中弹窗 + 键盘避让,
214+
* 避免被软键盘遮挡。
215+
*/
216+
export function RepoUrlSheet({ visible, initialUrl, onConfirm, onClose }: {
217+
visible: boolean; initialUrl?: string; onConfirm: (url: string) => void; onClose: () => void;
218+
}) {
219+
const t = useTheme();
220+
const inputRef = React.useRef<TextInput>(null);
221+
const [url, setUrl] = React.useState(initialUrl || '');
222+
const [err, setErr] = React.useState('');
223+
// 打开瞬间把输入同步成外部已有地址、清掉旧报错(用「渲染期间调整 state」的写法,避免 effect 级联渲染)
224+
const [wasOpen, setWasOpen] = React.useState(visible);
225+
if (visible !== wasOpen) {
226+
setWasOpen(visible);
227+
if (visible) { setUrl(initialUrl || ''); setErr(''); }
228+
}
229+
230+
const confirm = () => {
231+
const v = url.trim();
232+
if (!v) { setErr('请输入仓库地址'); return; }
233+
if (!/^(https?:\/\/|ssh:\/\/|git@)/i.test(v)) { setErr('请输入有效的 Git 地址(http(s):// 或 git@)'); return; }
234+
onConfirm(v);
235+
};
236+
237+
return (
238+
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose} statusBarTranslucent onShow={() => inputRef.current?.focus()}>
239+
{/* 居中对话框需压暗背景并拦截背景点击(点背景即关闭) */}
240+
<Pressable onPress={onClose} style={[StyleSheet.absoluteFill, { backgroundColor: 'rgba(0,0,0,0.45)' }]} />
241+
<KeyboardAvoidingView behavior="padding" style={StyleSheet.absoluteFill} pointerEvents="box-none">
242+
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 26 }} pointerEvents="box-none">
243+
<View style={{ width: '100%', backgroundColor: t.bg2, borderRadius: 22, borderWidth: 1, borderColor: t.line2, padding: 20, ...t.shLift }}>
244+
<Text style={{ color: t.tx, fontSize: 17, fontWeight: '700' }}>手动输入仓库地址</Text>
245+
<Text style={{ color: t.tx3, fontSize: 12.5, marginTop: 4, marginBottom: 14 }}>填写 Git 仓库地址,任务将基于该仓库运行</Text>
246+
<TextInput
247+
ref={inputRef}
248+
value={url}
249+
onChangeText={(v) => { setUrl(v); if (err) setErr(''); }}
250+
placeholder="https://github.com/owner/repo.git"
251+
placeholderTextColor={t.tx3}
252+
autoCapitalize="none"
253+
autoCorrect={false}
254+
keyboardType="url"
255+
returnKeyType="done"
256+
onSubmitEditing={confirm}
257+
style={{ borderWidth: 1, borderColor: err ? t.red : t.line2, borderRadius: 12, paddingHorizontal: 14, paddingVertical: 12, fontSize: 14, color: t.tx, backgroundColor: t.bg, fontFamily: 'monospace' }}
258+
/>
259+
{err ? <Text style={{ color: t.red, fontSize: 12.5, marginTop: 8 }}>{err}</Text> : null}
260+
<View style={{ flexDirection: 'row', gap: 10, marginTop: 18 }}>
261+
<Pressable onPress={onClose} style={({ pressed }) => [{ flex: 1, paddingVertical: 13, borderRadius: 13, alignItems: 'center', backgroundColor: t.bg4 }, pressed && { opacity: 0.8 }]}>
262+
<Text style={{ color: t.tx2, fontSize: 15, fontWeight: '600' }}>取消</Text>
263+
</Pressable>
264+
<Pressable onPress={confirm} style={({ pressed }) => [{ flex: 1, paddingVertical: 13, borderRadius: 13, alignItems: 'center', backgroundColor: t.ac }, pressed && { opacity: 0.85 }]}>
265+
<Text style={{ color: t.acInk, fontSize: 15, fontWeight: '700' }}>确定</Text>
266+
</Pressable>
267+
</View>
268+
</View>
269+
</View>
270+
</KeyboardAvoidingView>
271+
</Modal>
272+
);
273+
}

0 commit comments

Comments
 (0)