@@ -14,23 +14,7 @@ import (
1414)
1515
1616func (s * appState ) modelMenu () tview.Primitive {
17- items := make ([]MenuItem , 0 , 2 + len (s .config .ModelList ))
18- items = append (items ,
19- MenuItem {Label : "Back" , Description : "Return to main menu" , Action : func () { s .pop () }},
20- MenuItem {
21- Label : "Add model" ,
22- Description : "Append a new model entry" ,
23- Action : func () {
24- s .addModel (
25- picoclawconfig.ModelConfig {ModelName : "new-model" , Model : "openai/gpt-5.2" },
26- )
27- s .push (
28- fmt .Sprintf ("model-%d" , len (s .config .ModelList )- 1 ),
29- s .modelForm (len (s .config .ModelList )- 1 ),
30- )
31- },
32- },
33- )
17+ items := make ([]MenuItem , 0 , 1 + len (s .config .ModelList ))
3418 currentModel := strings .TrimSpace (s .config .Agents .Defaults .Model )
3519 for i := range s .config .ModelList {
3620 index := i
@@ -57,21 +41,35 @@ func (s *appState) modelMenu() tview.Primitive {
5741 },
5842 })
5943 }
44+ // Add model entry appended at the end so the models map to rows 1..N
45+ items = append (items ,
46+ MenuItem {
47+ Label : "**Add model**" ,
48+ Description : "Append a new model entry" ,
49+ Action : func () {
50+ newName := s .nextAvailableModelName ("new-model" )
51+ s .addModel (
52+ picoclawconfig.ModelConfig {ModelName : newName , Model : "openai/gpt-5.2" },
53+ )
54+ s .push (
55+ fmt .Sprintf ("model-%d" , len (s .config .ModelList )- 1 ),
56+ s .modelForm (len (s .config .ModelList )- 1 ),
57+ )
58+ },
59+ },
60+ )
6061
6162 menu := NewMenu ("Models" , items )
6263 menu .SetInputCapture (func (event * tcell.EventKey ) * tcell.EventKey {
6364 if event .Key () == tcell .KeyEsc {
6465 s .pop ()
6566 return nil
6667 }
67- if event .Rune () == 'q' {
68- s .pop ()
69- return nil
70- }
68+
7169 if event .Rune () == ' ' {
7270 row , _ := menu .GetSelection ()
73- if row > 0 && row <= len (s .config .ModelList ) {
74- model := s .config .ModelList [row - 1 ]
71+ if row >= 0 && row < len (s .config .ModelList ) {
72+ model := s .config .ModelList [row ]
7573 if ! isModelValid (model ) {
7674 s .showMessage (
7775 "Invalid model" ,
@@ -95,12 +93,23 @@ func (s *appState) modelForm(index int) tview.Primitive {
9593 model := & s .config .ModelList [index ]
9694 form := tview .NewForm ()
9795 form .SetBorder (true ).SetTitle (fmt .Sprintf ("Model: %s" , model .ModelName ))
98- form .SetButtonBackgroundColor (tcell .NewRGBColor (80 , 250 , 123 ))
99- form .SetButtonTextColor (tcell .NewRGBColor (12 , 13 , 22 ))
10096
10197 addInput (form , "Model Name" , model .ModelName , func (value string ) {
98+ if value == "" {
99+ s .showMessage ("Invalid model name" , "Model Name cannot be empty" )
100+ return
101+ }
102+ if s .modelNameExists (value , index ) {
103+ s .showMessage ("Duplicate model name" , fmt .Sprintf ("Model Name '%s' already exists" , value ))
104+ return
105+ }
106+ oldName := model .ModelName
102107 model .ModelName = value
108+ if s .config .Agents .Defaults .Model == oldName {
109+ s .config .Agents .Defaults .Model = value
110+ }
103111 s .dirty = true
112+ form .SetTitle (fmt .Sprintf ("Model: %s" , model .ModelName ))
104113 refreshMainMenuIfPresent (s )
105114 if menu , ok := s .menus ["model" ]; ok {
106115 refreshModelMenuFromState (menu , s )
@@ -158,7 +167,21 @@ func (s *appState) modelForm(index int) tview.Primitive {
158167 })
159168
160169 form .AddButton ("Delete" , func () {
161- s .deleteModel (index )
170+ pageName := "confirm-delete-model"
171+ if s .pages .HasPage (pageName ) {
172+ return
173+ }
174+ modal := tview .NewModal ().
175+ SetText ("Are you sure you want to delete this model?" ).
176+ AddButtons ([]string {"Cancel" , "Delete" }).
177+ SetDoneFunc (func (buttonIndex int , buttonLabel string ) {
178+ s .pages .RemovePage (pageName )
179+ if buttonLabel == "Delete" {
180+ s .deleteModel (index )
181+ }
182+ })
183+ modal .SetTitle ("Confirm Delete" ).SetBorder (true )
184+ s .pages .AddPage (pageName , modal , true , true )
162185 })
163186 form .AddButton ("Test" , func () {
164187 s .testModel (model )
@@ -215,7 +238,7 @@ func modelStatusColor(valid bool, selected bool) *tcell.Color {
215238
216239func refreshModelMenu (menu * Menu , currentModel string , models []picoclawconfig.ModelConfig ) {
217240 for i , model := range models {
218- row := i + 1
241+ row := i
219242 label := fmt .Sprintf ("%s (%s)" , model .ModelName , model .Model )
220243 isValid := isModelValid (model )
221244 if model .ModelName == currentModel && currentModel != "" {
@@ -234,23 +257,7 @@ func refreshModelMenu(menu *Menu, currentModel string, models []picoclawconfig.M
234257}
235258
236259func refreshModelMenuFromState (menu * Menu , s * appState ) {
237- items := make ([]MenuItem , 0 , 2 + len (s .config .ModelList ))
238- items = append (items ,
239- MenuItem {Label : "Back" , Description : "Return to main menu" , Action : func () { s .pop () }},
240- MenuItem {
241- Label : "Add model" ,
242- Description : "Append a new model entry" ,
243- Action : func () {
244- s .addModel (
245- picoclawconfig.ModelConfig {ModelName : "new-model" , Model : "openai/gpt-5.2" },
246- )
247- s .push (
248- fmt .Sprintf ("model-%d" , len (s .config .ModelList )- 1 ),
249- s .modelForm (len (s .config .ModelList )- 1 ),
250- )
251- },
252- },
253- )
260+ items := make ([]MenuItem , 0 , 1 + len (s .config .ModelList ))
254261 currentModel := strings .TrimSpace (s .config .Agents .Defaults .Model )
255262 for i := range s .config .ModelList {
256263 index := i
@@ -277,6 +284,19 @@ func refreshModelMenuFromState(menu *Menu, s *appState) {
277284 },
278285 })
279286 }
287+ items = append (items ,
288+ MenuItem {
289+ Label : "**Add Model**" ,
290+ Description : "Append a new model entry" ,
291+ Action : func () {
292+ newName := s .nextAvailableModelName ("new-model" )
293+ s .addModel (
294+ picoclawconfig.ModelConfig {ModelName : newName , Model : "openai/gpt-5.2" },
295+ )
296+ s .push (fmt .Sprintf ("model-%d" , len (s .config .ModelList )- 1 ), s .modelForm (len (s .config .ModelList )- 1 ))
297+ },
298+ },
299+ )
280300 menu .applyItems (items )
281301}
282302
@@ -287,6 +307,38 @@ func isModelValid(model picoclawconfig.ModelConfig) bool {
287307 return hasKey && hasModel
288308}
289309
310+ func (s * appState ) modelNameExists (name string , excludeIndex int ) bool {
311+ target := strings .TrimSpace (name )
312+ if target == "" {
313+ return false
314+ }
315+ for i := range s .config .ModelList {
316+ if i == excludeIndex {
317+ continue
318+ }
319+ if strings .TrimSpace (s .config .ModelList [i ].ModelName ) == target {
320+ return true
321+ }
322+ }
323+ return false
324+ }
325+
326+ func (s * appState ) nextAvailableModelName (base string ) string {
327+ name := strings .TrimSpace (base )
328+ if name == "" {
329+ name = "new-model"
330+ }
331+ if ! s .modelNameExists (name , - 1 ) {
332+ return name
333+ }
334+ for i := 2 ; ; i ++ {
335+ candidate := fmt .Sprintf ("%s-%d" , name , i )
336+ if ! s .modelNameExists (candidate , - 1 ) {
337+ return candidate
338+ }
339+ }
340+ }
341+
290342func (s * appState ) testModel (model * picoclawconfig.ModelConfig ) {
291343 if model == nil {
292344 return
0 commit comments