tui.go

  1package tui
  2
  3import (
  4	"context"
  5	"fmt"
  6
  7	"github.com/charmbracelet/bubbles/key"
  8	tea "github.com/charmbracelet/bubbletea"
  9	"github.com/charmbracelet/lipgloss"
 10	"github.com/opencode-ai/opencode/internal/app"
 11	"github.com/opencode-ai/opencode/internal/config"
 12	"github.com/opencode-ai/opencode/internal/logging"
 13	"github.com/opencode-ai/opencode/internal/permission"
 14	"github.com/opencode-ai/opencode/internal/pubsub"
 15	"github.com/opencode-ai/opencode/internal/tui/components/chat"
 16	"github.com/opencode-ai/opencode/internal/tui/components/core"
 17	"github.com/opencode-ai/opencode/internal/tui/components/dialog"
 18	"github.com/opencode-ai/opencode/internal/tui/layout"
 19	"github.com/opencode-ai/opencode/internal/tui/page"
 20	"github.com/opencode-ai/opencode/internal/tui/util"
 21)
 22
 23type keyMap struct {
 24	Logs          key.Binding
 25	Quit          key.Binding
 26	Help          key.Binding
 27	SwitchSession key.Binding
 28	Commands      key.Binding
 29	Models        key.Binding
 30	SwitchTheme   key.Binding
 31}
 32
 33var keys = keyMap{
 34	Logs: key.NewBinding(
 35		key.WithKeys("ctrl+l"),
 36		key.WithHelp("ctrl+l", "logs"),
 37	),
 38
 39	Quit: key.NewBinding(
 40		key.WithKeys("ctrl+c"),
 41		key.WithHelp("ctrl+c", "quit"),
 42	),
 43	Help: key.NewBinding(
 44		key.WithKeys("ctrl+_"),
 45		key.WithHelp("ctrl+?", "toggle help"),
 46	),
 47
 48	SwitchSession: key.NewBinding(
 49		key.WithKeys("ctrl+s"),
 50		key.WithHelp("ctrl+s", "switch session"),
 51	),
 52
 53	Commands: key.NewBinding(
 54		key.WithKeys("ctrl+k"),
 55		key.WithHelp("ctrl+k", "commands"),
 56	),
 57
 58	Models: key.NewBinding(
 59		key.WithKeys("ctrl+o"),
 60		key.WithHelp("ctrl+o", "model selection"),
 61	),
 62
 63	SwitchTheme: key.NewBinding(
 64		key.WithKeys("ctrl+t"),
 65		key.WithHelp("ctrl+t", "switch theme"),
 66	),
 67}
 68
 69var helpEsc = key.NewBinding(
 70	key.WithKeys("?"),
 71	key.WithHelp("?", "toggle help"),
 72)
 73
 74var returnKey = key.NewBinding(
 75	key.WithKeys("esc"),
 76	key.WithHelp("esc", "close"),
 77)
 78
 79var logsKeyReturnKey = key.NewBinding(
 80	key.WithKeys("esc", "backspace", "q"),
 81	key.WithHelp("esc/q", "go back"),
 82)
 83
 84type appModel struct {
 85	width, height int
 86	currentPage   page.PageID
 87	previousPage  page.PageID
 88	pages         map[page.PageID]tea.Model
 89	loadedPages   map[page.PageID]bool
 90	status        core.StatusCmp
 91	app           *app.App
 92
 93	showPermissions bool
 94	permissions     dialog.PermissionDialogCmp
 95
 96	showHelp bool
 97	help     dialog.HelpCmp
 98
 99	showQuit bool
100	quit     dialog.QuitDialog
101
102	showSessionDialog bool
103	sessionDialog     dialog.SessionDialog
104
105	showCommandDialog bool
106	commandDialog     dialog.CommandDialog
107	commands          []dialog.Command
108
109	showModelDialog bool
110	modelDialog     dialog.ModelDialog
111
112	showInitDialog bool
113	initDialog     dialog.InitDialogCmp
114
115	showThemeDialog bool
116	themeDialog     dialog.ThemeDialog
117}
118
119func (a appModel) Init() tea.Cmd {
120	var cmds []tea.Cmd
121	cmd := a.pages[a.currentPage].Init()
122	a.loadedPages[a.currentPage] = true
123	cmds = append(cmds, cmd)
124	cmd = a.status.Init()
125	cmds = append(cmds, cmd)
126	cmd = a.quit.Init()
127	cmds = append(cmds, cmd)
128	cmd = a.help.Init()
129	cmds = append(cmds, cmd)
130	cmd = a.sessionDialog.Init()
131	cmds = append(cmds, cmd)
132	cmd = a.commandDialog.Init()
133	cmds = append(cmds, cmd)
134	cmd = a.modelDialog.Init()
135	cmds = append(cmds, cmd)
136	cmd = a.initDialog.Init()
137	cmds = append(cmds, cmd)
138	cmd = a.themeDialog.Init()
139	cmds = append(cmds, cmd)
140
141	// Check if we should show the init dialog
142	cmds = append(cmds, func() tea.Msg {
143		shouldShow, err := config.ShouldShowInitDialog()
144		if err != nil {
145			return util.InfoMsg{
146				Type: util.InfoTypeError,
147				Msg:  "Failed to check init status: " + err.Error(),
148			}
149		}
150		return dialog.ShowInitDialogMsg{Show: shouldShow}
151	})
152
153	return tea.Batch(cmds...)
154}
155
156func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
157	var cmds []tea.Cmd
158	var cmd tea.Cmd
159	switch msg := msg.(type) {
160	case tea.WindowSizeMsg:
161		msg.Height -= 1 // Make space for the status bar
162		a.width, a.height = msg.Width, msg.Height
163
164		s, _ := a.status.Update(msg)
165		a.status = s.(core.StatusCmp)
166		a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
167		cmds = append(cmds, cmd)
168
169		prm, permCmd := a.permissions.Update(msg)
170		a.permissions = prm.(dialog.PermissionDialogCmp)
171		cmds = append(cmds, permCmd)
172
173		help, helpCmd := a.help.Update(msg)
174		a.help = help.(dialog.HelpCmp)
175		cmds = append(cmds, helpCmd)
176
177		session, sessionCmd := a.sessionDialog.Update(msg)
178		a.sessionDialog = session.(dialog.SessionDialog)
179		cmds = append(cmds, sessionCmd)
180
181		command, commandCmd := a.commandDialog.Update(msg)
182		a.commandDialog = command.(dialog.CommandDialog)
183		cmds = append(cmds, commandCmd)
184
185		a.initDialog.SetSize(msg.Width, msg.Height)
186
187		return a, tea.Batch(cmds...)
188	// Status
189	case util.InfoMsg:
190		s, cmd := a.status.Update(msg)
191		a.status = s.(core.StatusCmp)
192		cmds = append(cmds, cmd)
193		return a, tea.Batch(cmds...)
194	case pubsub.Event[logging.LogMessage]:
195		if msg.Payload.Persist {
196			switch msg.Payload.Level {
197			case "error":
198				s, cmd := a.status.Update(util.InfoMsg{
199					Type: util.InfoTypeError,
200					Msg:  msg.Payload.Message,
201					TTL:  msg.Payload.PersistTime,
202				})
203				a.status = s.(core.StatusCmp)
204				cmds = append(cmds, cmd)
205			case "info":
206				s, cmd := a.status.Update(util.InfoMsg{
207					Type: util.InfoTypeInfo,
208					Msg:  msg.Payload.Message,
209					TTL:  msg.Payload.PersistTime,
210				})
211				a.status = s.(core.StatusCmp)
212				cmds = append(cmds, cmd)
213
214			case "warn":
215				s, cmd := a.status.Update(util.InfoMsg{
216					Type: util.InfoTypeWarn,
217					Msg:  msg.Payload.Message,
218					TTL:  msg.Payload.PersistTime,
219				})
220
221				a.status = s.(core.StatusCmp)
222				cmds = append(cmds, cmd)
223			default:
224				s, cmd := a.status.Update(util.InfoMsg{
225					Type: util.InfoTypeInfo,
226					Msg:  msg.Payload.Message,
227					TTL:  msg.Payload.PersistTime,
228				})
229				a.status = s.(core.StatusCmp)
230				cmds = append(cmds, cmd)
231			}
232		}
233	case util.ClearStatusMsg:
234		s, _ := a.status.Update(msg)
235		a.status = s.(core.StatusCmp)
236
237	// Permission
238	case pubsub.Event[permission.PermissionRequest]:
239		a.showPermissions = true
240		return a, a.permissions.SetPermissions(msg.Payload)
241	case dialog.PermissionResponseMsg:
242		var cmd tea.Cmd
243		switch msg.Action {
244		case dialog.PermissionAllow:
245			a.app.Permissions.Grant(msg.Permission)
246		case dialog.PermissionAllowForSession:
247			a.app.Permissions.GrantPersistant(msg.Permission)
248		case dialog.PermissionDeny:
249			a.app.Permissions.Deny(msg.Permission)
250		}
251		a.showPermissions = false
252		return a, cmd
253
254	case page.PageChangeMsg:
255		return a, a.moveToPage(msg.ID)
256
257	case dialog.CloseQuitMsg:
258		a.showQuit = false
259		return a, nil
260
261	case dialog.CloseSessionDialogMsg:
262		a.showSessionDialog = false
263		return a, nil
264
265	case dialog.CloseCommandDialogMsg:
266		a.showCommandDialog = false
267		return a, nil
268
269	case dialog.CloseThemeDialogMsg:
270		a.showThemeDialog = false
271		return a, nil
272
273	case dialog.ThemeChangedMsg:
274		a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
275		a.showThemeDialog = false
276		return a, tea.Batch(cmd, util.ReportInfo("Theme changed to: "+msg.ThemeName))
277
278	case dialog.CloseModelDialogMsg:
279		a.showModelDialog = false
280		return a, nil
281
282	case dialog.ModelSelectedMsg:
283		a.showModelDialog = false
284
285		model, err := a.app.CoderAgent.Update(config.AgentCoder, msg.Model.ID)
286		if err != nil {
287			return a, util.ReportError(err)
288		}
289
290		return a, util.ReportInfo(fmt.Sprintf("Model changed to %s", model.Name))
291
292	case dialog.ShowInitDialogMsg:
293		a.showInitDialog = msg.Show
294		return a, nil
295
296	case dialog.CloseInitDialogMsg:
297		a.showInitDialog = false
298		if msg.Initialize {
299			// Run the initialization command
300			for _, cmd := range a.commands {
301				if cmd.ID == "init" {
302					// Mark the project as initialized
303					if err := config.MarkProjectInitialized(); err != nil {
304						return a, util.ReportError(err)
305					}
306					return a, cmd.Handler(cmd)
307				}
308			}
309		} else {
310			// Mark the project as initialized without running the command
311			if err := config.MarkProjectInitialized(); err != nil {
312				return a, util.ReportError(err)
313			}
314		}
315		return a, nil
316
317	case chat.SessionSelectedMsg:
318		a.sessionDialog.SetSelectedSession(msg.ID)
319	case dialog.SessionSelectedMsg:
320		a.showSessionDialog = false
321		if a.currentPage == page.ChatPage {
322			return a, util.CmdHandler(chat.SessionSelectedMsg(msg.Session))
323		}
324		return a, nil
325
326	case dialog.CommandSelectedMsg:
327		a.showCommandDialog = false
328		// Execute the command handler if available
329		if msg.Command.Handler != nil {
330			return a, msg.Command.Handler(msg.Command)
331		}
332		return a, util.ReportInfo("Command selected: " + msg.Command.Title)
333
334	case tea.KeyMsg:
335		switch {
336		case key.Matches(msg, keys.Quit):
337			a.showQuit = !a.showQuit
338			if a.showHelp {
339				a.showHelp = false
340			}
341			if a.showSessionDialog {
342				a.showSessionDialog = false
343			}
344			if a.showCommandDialog {
345				a.showCommandDialog = false
346			}
347			if a.showModelDialog {
348				a.showModelDialog = false
349			}
350			return a, nil
351		case key.Matches(msg, keys.SwitchSession):
352			if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showCommandDialog {
353				// Load sessions and show the dialog
354				sessions, err := a.app.Sessions.List(context.Background())
355				if err != nil {
356					return a, util.ReportError(err)
357				}
358				if len(sessions) == 0 {
359					return a, util.ReportWarn("No sessions available")
360				}
361				a.sessionDialog.SetSessions(sessions)
362				a.showSessionDialog = true
363				return a, nil
364			}
365			return a, nil
366		case key.Matches(msg, keys.Commands):
367			if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showThemeDialog {
368				// Show commands dialog
369				if len(a.commands) == 0 {
370					return a, util.ReportWarn("No commands available")
371				}
372				a.commandDialog.SetCommands(a.commands)
373				a.showCommandDialog = true
374				return a, nil
375			}
376			return a, nil
377		case key.Matches(msg, keys.Models):
378			if a.showModelDialog {
379				a.showModelDialog = false
380				return a, nil
381			}
382			if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog {
383				a.showModelDialog = true
384				return a, nil
385			}
386			return a, nil
387		case key.Matches(msg, keys.SwitchTheme):
388			if !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog {
389				// Show theme switcher dialog
390				a.showThemeDialog = true
391				// Theme list is dynamically loaded by the dialog component
392				return a, a.themeDialog.Init()
393			}
394			return a, nil
395		case key.Matches(msg, logsKeyReturnKey):
396			if a.currentPage == page.LogsPage {
397				return a, a.moveToPage(page.ChatPage)
398			}
399		case key.Matches(msg, returnKey):
400			if a.showQuit {
401				a.showQuit = !a.showQuit
402				return a, nil
403			}
404			if a.showHelp {
405				a.showHelp = !a.showHelp
406				return a, nil
407			}
408			if a.showInitDialog {
409				a.showInitDialog = false
410				// Mark the project as initialized without running the command
411				if err := config.MarkProjectInitialized(); err != nil {
412					return a, util.ReportError(err)
413				}
414				return a, nil
415			}
416		case key.Matches(msg, keys.Logs):
417			return a, a.moveToPage(page.LogsPage)
418		case key.Matches(msg, keys.Help):
419			if a.showQuit {
420				return a, nil
421			}
422			a.showHelp = !a.showHelp
423			return a, nil
424		case key.Matches(msg, helpEsc):
425			if a.app.CoderAgent.IsBusy() {
426				if a.showQuit {
427					return a, nil
428				}
429				a.showHelp = !a.showHelp
430				return a, nil
431			}
432		}
433
434	}
435
436	if a.showQuit {
437		q, quitCmd := a.quit.Update(msg)
438		a.quit = q.(dialog.QuitDialog)
439		cmds = append(cmds, quitCmd)
440		// Only block key messages send all other messages down
441		if _, ok := msg.(tea.KeyMsg); ok {
442			return a, tea.Batch(cmds...)
443		}
444	}
445	if a.showPermissions {
446		d, permissionsCmd := a.permissions.Update(msg)
447		a.permissions = d.(dialog.PermissionDialogCmp)
448		cmds = append(cmds, permissionsCmd)
449		// Only block key messages send all other messages down
450		if _, ok := msg.(tea.KeyMsg); ok {
451			return a, tea.Batch(cmds...)
452		}
453	}
454
455	if a.showSessionDialog {
456		d, sessionCmd := a.sessionDialog.Update(msg)
457		a.sessionDialog = d.(dialog.SessionDialog)
458		cmds = append(cmds, sessionCmd)
459		// Only block key messages send all other messages down
460		if _, ok := msg.(tea.KeyMsg); ok {
461			return a, tea.Batch(cmds...)
462		}
463	}
464
465	if a.showCommandDialog {
466		d, commandCmd := a.commandDialog.Update(msg)
467		a.commandDialog = d.(dialog.CommandDialog)
468		cmds = append(cmds, commandCmd)
469		// Only block key messages send all other messages down
470		if _, ok := msg.(tea.KeyMsg); ok {
471			return a, tea.Batch(cmds...)
472		}
473	}
474
475	if a.showModelDialog {
476		d, modelCmd := a.modelDialog.Update(msg)
477		a.modelDialog = d.(dialog.ModelDialog)
478		cmds = append(cmds, modelCmd)
479		// Only block key messages send all other messages down
480		if _, ok := msg.(tea.KeyMsg); ok {
481			return a, tea.Batch(cmds...)
482		}
483	}
484
485	if a.showInitDialog {
486		d, initCmd := a.initDialog.Update(msg)
487		a.initDialog = d.(dialog.InitDialogCmp)
488		cmds = append(cmds, initCmd)
489		// Only block key messages send all other messages down
490		if _, ok := msg.(tea.KeyMsg); ok {
491			return a, tea.Batch(cmds...)
492		}
493	}
494
495	if a.showThemeDialog {
496		d, themeCmd := a.themeDialog.Update(msg)
497		a.themeDialog = d.(dialog.ThemeDialog)
498		cmds = append(cmds, themeCmd)
499		// Only block key messages send all other messages down
500		if _, ok := msg.(tea.KeyMsg); ok {
501			return a, tea.Batch(cmds...)
502		}
503	}
504
505	s, _ := a.status.Update(msg)
506	a.status = s.(core.StatusCmp)
507	a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
508	cmds = append(cmds, cmd)
509	return a, tea.Batch(cmds...)
510}
511
512// RegisterCommand adds a command to the command dialog
513func (a *appModel) RegisterCommand(cmd dialog.Command) {
514	a.commands = append(a.commands, cmd)
515}
516
517func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
518	if a.app.CoderAgent.IsBusy() {
519		// For now we don't move to any page if the agent is busy
520		return util.ReportWarn("Agent is busy, please wait...")
521	}
522	var cmds []tea.Cmd
523	if _, ok := a.loadedPages[pageID]; !ok {
524		cmd := a.pages[pageID].Init()
525		cmds = append(cmds, cmd)
526		a.loadedPages[pageID] = true
527	}
528	a.previousPage = a.currentPage
529	a.currentPage = pageID
530	if sizable, ok := a.pages[a.currentPage].(layout.Sizeable); ok {
531		cmd := sizable.SetSize(a.width, a.height)
532		cmds = append(cmds, cmd)
533	}
534
535	return tea.Batch(cmds...)
536}
537
538func (a appModel) View() string {
539	components := []string{
540		a.pages[a.currentPage].View(),
541	}
542
543	components = append(components, a.status.View())
544
545	appView := lipgloss.JoinVertical(lipgloss.Top, components...)
546
547	if a.showPermissions {
548		overlay := a.permissions.View()
549		row := lipgloss.Height(appView) / 2
550		row -= lipgloss.Height(overlay) / 2
551		col := lipgloss.Width(appView) / 2
552		col -= lipgloss.Width(overlay) / 2
553		appView = layout.PlaceOverlay(
554			col,
555			row,
556			overlay,
557			appView,
558			true,
559		)
560	}
561
562	if !a.app.CoderAgent.IsBusy() {
563		a.status.SetHelpWidgetMsg("ctrl+? help")
564	} else {
565		a.status.SetHelpWidgetMsg("? help")
566	}
567
568	if a.showHelp {
569		bindings := layout.KeyMapToSlice(keys)
570		if p, ok := a.pages[a.currentPage].(layout.Bindings); ok {
571			bindings = append(bindings, p.BindingKeys()...)
572		}
573		if a.showPermissions {
574			bindings = append(bindings, a.permissions.BindingKeys()...)
575		}
576		if a.currentPage == page.LogsPage {
577			bindings = append(bindings, logsKeyReturnKey)
578		}
579		if !a.app.CoderAgent.IsBusy() {
580			bindings = append(bindings, helpEsc)
581		}
582		a.help.SetBindings(bindings)
583
584		overlay := a.help.View()
585		row := lipgloss.Height(appView) / 2
586		row -= lipgloss.Height(overlay) / 2
587		col := lipgloss.Width(appView) / 2
588		col -= lipgloss.Width(overlay) / 2
589		appView = layout.PlaceOverlay(
590			col,
591			row,
592			overlay,
593			appView,
594			true,
595		)
596	}
597
598	if a.showQuit {
599		overlay := a.quit.View()
600		row := lipgloss.Height(appView) / 2
601		row -= lipgloss.Height(overlay) / 2
602		col := lipgloss.Width(appView) / 2
603		col -= lipgloss.Width(overlay) / 2
604		appView = layout.PlaceOverlay(
605			col,
606			row,
607			overlay,
608			appView,
609			true,
610		)
611	}
612
613	if a.showSessionDialog {
614		overlay := a.sessionDialog.View()
615		row := lipgloss.Height(appView) / 2
616		row -= lipgloss.Height(overlay) / 2
617		col := lipgloss.Width(appView) / 2
618		col -= lipgloss.Width(overlay) / 2
619		appView = layout.PlaceOverlay(
620			col,
621			row,
622			overlay,
623			appView,
624			true,
625		)
626	}
627
628	if a.showModelDialog {
629		overlay := a.modelDialog.View()
630		row := lipgloss.Height(appView) / 2
631		row -= lipgloss.Height(overlay) / 2
632		col := lipgloss.Width(appView) / 2
633		col -= lipgloss.Width(overlay) / 2
634		appView = layout.PlaceOverlay(
635			col,
636			row,
637			overlay,
638			appView,
639			true,
640		)
641	}
642
643	if a.showCommandDialog {
644		overlay := a.commandDialog.View()
645		row := lipgloss.Height(appView) / 2
646		row -= lipgloss.Height(overlay) / 2
647		col := lipgloss.Width(appView) / 2
648		col -= lipgloss.Width(overlay) / 2
649		appView = layout.PlaceOverlay(
650			col,
651			row,
652			overlay,
653			appView,
654			true,
655		)
656	}
657
658	if a.showInitDialog {
659		overlay := a.initDialog.View()
660		appView = layout.PlaceOverlay(
661			a.width/2-lipgloss.Width(overlay)/2,
662			a.height/2-lipgloss.Height(overlay)/2,
663			overlay,
664			appView,
665			true,
666		)
667	}
668
669	if a.showThemeDialog {
670		overlay := a.themeDialog.View()
671		row := lipgloss.Height(appView) / 2
672		row -= lipgloss.Height(overlay) / 2
673		col := lipgloss.Width(appView) / 2
674		col -= lipgloss.Width(overlay) / 2
675		appView = layout.PlaceOverlay(
676			col,
677			row,
678			overlay,
679			appView,
680			true,
681		)
682	}
683
684	return appView
685}
686
687func New(app *app.App) tea.Model {
688	startPage := page.ChatPage
689	model := &appModel{
690		currentPage:   startPage,
691		loadedPages:   make(map[page.PageID]bool),
692		status:        core.NewStatusCmp(app.LSPClients),
693		help:          dialog.NewHelpCmp(),
694		quit:          dialog.NewQuitCmp(),
695		sessionDialog: dialog.NewSessionDialogCmp(),
696		commandDialog: dialog.NewCommandDialogCmp(),
697		modelDialog:   dialog.NewModelDialogCmp(),
698		permissions:   dialog.NewPermissionDialogCmp(),
699		initDialog:    dialog.NewInitDialogCmp(),
700		themeDialog:   dialog.NewThemeDialogCmp(),
701		app:           app,
702		commands:      []dialog.Command{},
703		pages: map[page.PageID]tea.Model{
704			page.ChatPage: page.NewChatPage(app),
705			page.LogsPage: page.NewLogsPage(),
706		},
707	}
708
709	model.RegisterCommand(dialog.Command{
710		ID:          "init",
711		Title:       "Initialize Project",
712		Description: "Create/Update the OpenCode.md memory file",
713		Handler: func(cmd dialog.Command) tea.Cmd {
714			prompt := `Please analyze this codebase and create a OpenCode.md file containing:
7151. Build/lint/test commands - especially for running a single test
7162. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
717
718The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.
719If there's already a opencode.md, improve it.
720If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.`
721			return tea.Batch(
722				util.CmdHandler(chat.SendMsg{
723					Text: prompt,
724				}),
725			)
726		},
727	})
728	return model
729}