tui.go 18 KB

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