@@ -9,14 +9,25 @@ import type { Model, Project } from '@/api/types';
99import { ConcurrentLimitModal } from '@/components/ConcurrentLimitModal' ;
1010import { Icons , providerIconForUrl } from '@/components/Icons' ;
1111import { MicButton } from '@/components/MicButton' ;
12- import { ModelSheet } from '@/components/sheets' ;
12+ import { ModelSheet , RepoUrlSheet } from '@/components/sheets' ;
1313import { Card , IconButton , MonkeyLogo , PickerSheet , PrimaryButton , type PickerOption } from '@/components/ui' ;
1414import { useSpeechToText } from '@/speech/useSpeechToText' ;
1515import { DEFAULT_SKILL_IDS , modelLabel , pickDefaultImage , pickDefaultModel , TASK_DEFAULTS } from '@/config' ;
1616import { spacing , useTheme , type Theme } from '@/theme' ;
1717
1818const 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 ( / \. g i t $ / i, '' ) . replace ( / \/ + $ / , '' ) ;
26+ const m = cleaned . match ( / [ / : ] ( [ ^ / : ] + \/ [ ^ / : ] + ) $ / ) ;
27+ if ( m ) return m [ 1 ] ;
28+ return cleaned . split ( / [ / : ] / ) . filter ( Boolean ) . pop ( ) || url ;
29+ }
30+
2031function 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 ) ; } } />
0 commit comments