permission.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. package dialog
  2. import (
  3. "fmt"
  4. "github.com/charmbracelet/bubbles/key"
  5. "github.com/charmbracelet/bubbles/viewport"
  6. tea "github.com/charmbracelet/bubbletea"
  7. "github.com/charmbracelet/lipgloss"
  8. "github.com/sst/opencode/internal/layout"
  9. "github.com/sst/opencode/internal/styles"
  10. "github.com/sst/opencode/internal/theme"
  11. "github.com/sst/opencode/internal/util"
  12. "strings"
  13. )
  14. type PermissionAction string
  15. // Permission responses
  16. const (
  17. PermissionAllow PermissionAction = "allow"
  18. PermissionAllowForSession PermissionAction = "allow_session"
  19. PermissionDeny PermissionAction = "deny"
  20. )
  21. // PermissionResponseMsg represents the user's response to a permission request
  22. type PermissionResponseMsg struct {
  23. // Permission permission.PermissionRequest
  24. Action PermissionAction
  25. }
  26. // PermissionDialogCmp interface for permission dialog component
  27. type PermissionDialogCmp interface {
  28. tea.Model
  29. layout.Bindings
  30. // SetPermissions(permission permission.PermissionRequest) tea.Cmd
  31. }
  32. type permissionsMapping struct {
  33. Left key.Binding
  34. Right key.Binding
  35. EnterSpace key.Binding
  36. Allow key.Binding
  37. AllowSession key.Binding
  38. Deny key.Binding
  39. Tab key.Binding
  40. }
  41. var permissionsKeys = permissionsMapping{
  42. Left: key.NewBinding(
  43. key.WithKeys("left"),
  44. key.WithHelp("←", "switch options"),
  45. ),
  46. Right: key.NewBinding(
  47. key.WithKeys("right"),
  48. key.WithHelp("→", "switch options"),
  49. ),
  50. EnterSpace: key.NewBinding(
  51. key.WithKeys("enter", " "),
  52. key.WithHelp("enter/space", "confirm"),
  53. ),
  54. Allow: key.NewBinding(
  55. key.WithKeys("a"),
  56. key.WithHelp("a", "allow"),
  57. ),
  58. AllowSession: key.NewBinding(
  59. key.WithKeys("s"),
  60. key.WithHelp("s", "allow for session"),
  61. ),
  62. Deny: key.NewBinding(
  63. key.WithKeys("d"),
  64. key.WithHelp("d", "deny"),
  65. ),
  66. Tab: key.NewBinding(
  67. key.WithKeys("tab"),
  68. key.WithHelp("tab", "switch options"),
  69. ),
  70. }
  71. // permissionDialogCmp is the implementation of PermissionDialog
  72. type permissionDialogCmp struct {
  73. width int
  74. height int
  75. // permission permission.PermissionRequest
  76. windowSize tea.WindowSizeMsg
  77. contentViewPort viewport.Model
  78. selectedOption int // 0: Allow, 1: Allow for session, 2: Deny
  79. diffCache map[string]string
  80. markdownCache map[string]string
  81. }
  82. func (p *permissionDialogCmp) Init() tea.Cmd {
  83. return p.contentViewPort.Init()
  84. }
  85. func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  86. var cmds []tea.Cmd
  87. switch msg := msg.(type) {
  88. case tea.WindowSizeMsg:
  89. p.windowSize = msg
  90. cmd := p.SetSize()
  91. cmds = append(cmds, cmd)
  92. p.markdownCache = make(map[string]string)
  93. p.diffCache = make(map[string]string)
  94. // case tea.KeyMsg:
  95. // switch {
  96. // case key.Matches(msg, permissionsKeys.Right) || key.Matches(msg, permissionsKeys.Tab):
  97. // p.selectedOption = (p.selectedOption + 1) % 3
  98. // return p, nil
  99. // case key.Matches(msg, permissionsKeys.Left):
  100. // p.selectedOption = (p.selectedOption + 2) % 3
  101. // case key.Matches(msg, permissionsKeys.EnterSpace):
  102. // return p, p.selectCurrentOption()
  103. // case key.Matches(msg, permissionsKeys.Allow):
  104. // return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionAllow, Permission: p.permission})
  105. // case key.Matches(msg, permissionsKeys.AllowSession):
  106. // return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionAllowForSession, Permission: p.permission})
  107. // case key.Matches(msg, permissionsKeys.Deny):
  108. // return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionDeny, Permission: p.permission})
  109. // default:
  110. // // Pass other keys to viewport
  111. // viewPort, cmd := p.contentViewPort.Update(msg)
  112. // p.contentViewPort = viewPort
  113. // cmds = append(cmds, cmd)
  114. // }
  115. }
  116. return p, tea.Batch(cmds...)
  117. }
  118. func (p *permissionDialogCmp) selectCurrentOption() tea.Cmd {
  119. var action PermissionAction
  120. switch p.selectedOption {
  121. case 0:
  122. action = PermissionAllow
  123. case 1:
  124. action = PermissionAllowForSession
  125. case 2:
  126. action = PermissionDeny
  127. }
  128. return util.CmdHandler(PermissionResponseMsg{Action: action}) // , Permission: p.permission})
  129. }
  130. func (p *permissionDialogCmp) renderButtons() string {
  131. t := theme.CurrentTheme()
  132. baseStyle := styles.BaseStyle()
  133. allowStyle := baseStyle
  134. allowSessionStyle := baseStyle
  135. denyStyle := baseStyle
  136. spacerStyle := baseStyle.Background(t.Background())
  137. // Style the selected button
  138. switch p.selectedOption {
  139. case 0:
  140. allowStyle = allowStyle.Background(t.Primary()).Foreground(t.Background())
  141. allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary())
  142. denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary())
  143. case 1:
  144. allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary())
  145. allowSessionStyle = allowSessionStyle.Background(t.Primary()).Foreground(t.Background())
  146. denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary())
  147. case 2:
  148. allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary())
  149. allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary())
  150. denyStyle = denyStyle.Background(t.Primary()).Foreground(t.Background())
  151. }
  152. allowButton := allowStyle.Padding(0, 1).Render("Allow (a)")
  153. allowSessionButton := allowSessionStyle.Padding(0, 1).Render("Allow for session (s)")
  154. denyButton := denyStyle.Padding(0, 1).Render("Deny (d)")
  155. content := lipgloss.JoinHorizontal(
  156. lipgloss.Left,
  157. allowButton,
  158. spacerStyle.Render(" "),
  159. allowSessionButton,
  160. spacerStyle.Render(" "),
  161. denyButton,
  162. spacerStyle.Render(" "),
  163. )
  164. remainingWidth := p.width - lipgloss.Width(content)
  165. if remainingWidth > 0 {
  166. content = spacerStyle.Render(strings.Repeat(" ", remainingWidth)) + content
  167. }
  168. return content
  169. }
  170. func (p *permissionDialogCmp) renderHeader() string {
  171. return "NOT IMPLEMENTED"
  172. // t := theme.CurrentTheme()
  173. // baseStyle := styles.BaseStyle()
  174. //
  175. // toolKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Tool")
  176. // toolValue := baseStyle.
  177. // Foreground(t.Text()).
  178. // Width(p.width - lipgloss.Width(toolKey)).
  179. // Render(fmt.Sprintf(": %s", p.permission.ToolName))
  180. //
  181. // pathKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Path")
  182. //
  183. // // Get the current working directory to display relative path
  184. // relativePath := p.permission.Path
  185. // if filepath.IsAbs(relativePath) {
  186. // if cwd, err := filepath.Rel(config.WorkingDirectory(), relativePath); err == nil {
  187. // relativePath = cwd
  188. // }
  189. // }
  190. //
  191. // pathValue := baseStyle.
  192. // Foreground(t.Text()).
  193. // Width(p.width - lipgloss.Width(pathKey)).
  194. // Render(fmt.Sprintf(": %s", relativePath))
  195. //
  196. // headerParts := []string{
  197. // lipgloss.JoinHorizontal(
  198. // lipgloss.Left,
  199. // toolKey,
  200. // toolValue,
  201. // ),
  202. // baseStyle.Render(strings.Repeat(" ", p.width)),
  203. // lipgloss.JoinHorizontal(
  204. // lipgloss.Left,
  205. // pathKey,
  206. // pathValue,
  207. // ),
  208. // baseStyle.Render(strings.Repeat(" ", p.width)),
  209. // }
  210. //
  211. // // Add tool-specific header information
  212. // switch p.permission.ToolName {
  213. // case "bash":
  214. // headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Command"))
  215. // case "edit":
  216. // headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Diff"))
  217. // case "write":
  218. // headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Diff"))
  219. // case "fetch":
  220. // headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("URL"))
  221. // }
  222. //
  223. // return lipgloss.NewStyle().Background(t.Background()).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
  224. }
  225. func (p *permissionDialogCmp) renderBashContent() string {
  226. // t := theme.CurrentTheme()
  227. // baseStyle := styles.BaseStyle()
  228. //
  229. // if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok {
  230. // content := fmt.Sprintf("```bash\n%s\n```", pr.Command)
  231. //
  232. // // Use the cache for markdown rendering
  233. // renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
  234. // r := styles.GetMarkdownRenderer(p.width - 10)
  235. // s, err := r.Render(content)
  236. // return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
  237. // })
  238. //
  239. // finalContent := baseStyle.
  240. // Width(p.contentViewPort.Width).
  241. // Render(renderedContent)
  242. // p.contentViewPort.SetContent(finalContent)
  243. // return p.styleViewport()
  244. // }
  245. return ""
  246. }
  247. func (p *permissionDialogCmp) renderEditContent() string {
  248. // if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
  249. // diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
  250. // return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width))
  251. // })
  252. //
  253. // p.contentViewPort.SetContent(diff)
  254. // return p.styleViewport()
  255. // }
  256. return ""
  257. }
  258. func (p *permissionDialogCmp) renderPatchContent() string {
  259. // if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
  260. // diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
  261. // return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width))
  262. // })
  263. //
  264. // p.contentViewPort.SetContent(diff)
  265. // return p.styleViewport()
  266. // }
  267. return ""
  268. }
  269. func (p *permissionDialogCmp) renderWriteContent() string {
  270. // if pr, ok := p.permission.Params.(tools.WritePermissionsParams); ok {
  271. // // Use the cache for diff rendering
  272. // diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
  273. // return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width))
  274. // })
  275. //
  276. // p.contentViewPort.SetContent(diff)
  277. // return p.styleViewport()
  278. // }
  279. return ""
  280. }
  281. func (p *permissionDialogCmp) renderFetchContent() string {
  282. // t := theme.CurrentTheme()
  283. // baseStyle := styles.BaseStyle()
  284. //
  285. // if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok {
  286. // content := fmt.Sprintf("```bash\n%s\n```", pr.URL)
  287. //
  288. // // Use the cache for markdown rendering
  289. // renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
  290. // r := styles.GetMarkdownRenderer(p.width - 10)
  291. // s, err := r.Render(content)
  292. // return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
  293. // })
  294. //
  295. // finalContent := baseStyle.
  296. // Width(p.contentViewPort.Width).
  297. // Render(renderedContent)
  298. // p.contentViewPort.SetContent(finalContent)
  299. // return p.styleViewport()
  300. // }
  301. return ""
  302. }
  303. func (p *permissionDialogCmp) renderDefaultContent() string {
  304. // t := theme.CurrentTheme()
  305. // baseStyle := styles.BaseStyle()
  306. //
  307. // content := p.permission.Description
  308. //
  309. // // Use the cache for markdown rendering
  310. // renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
  311. // r := styles.GetMarkdownRenderer(p.width - 10)
  312. // s, err := r.Render(content)
  313. // return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
  314. // })
  315. //
  316. // finalContent := baseStyle.
  317. // Width(p.contentViewPort.Width).
  318. // Render(renderedContent)
  319. // p.contentViewPort.SetContent(finalContent)
  320. //
  321. // if renderedContent == "" {
  322. // return ""
  323. // }
  324. //
  325. return p.styleViewport()
  326. }
  327. func (p *permissionDialogCmp) styleViewport() string {
  328. t := theme.CurrentTheme()
  329. contentStyle := lipgloss.NewStyle().
  330. Background(t.Background())
  331. return contentStyle.Render(p.contentViewPort.View())
  332. }
  333. func (p *permissionDialogCmp) render() string {
  334. return "NOT IMPLEMENTED"
  335. // t := theme.CurrentTheme()
  336. // baseStyle := styles.BaseStyle()
  337. //
  338. // title := baseStyle.
  339. // Bold(true).
  340. // Width(p.width - 4).
  341. // Foreground(t.Primary()).
  342. // Render("Permission Required")
  343. // // Render header
  344. // headerContent := p.renderHeader()
  345. // // Render buttons
  346. // buttons := p.renderButtons()
  347. //
  348. // // Calculate content height dynamically based on window size
  349. // p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(buttons) - 2 - lipgloss.Height(title)
  350. // p.contentViewPort.Width = p.width - 4
  351. //
  352. // // Render content based on tool type
  353. // var contentFinal string
  354. // switch p.permission.ToolName {
  355. // case "bash":
  356. // contentFinal = p.renderBashContent()
  357. // case "edit":
  358. // contentFinal = p.renderEditContent()
  359. // case "patch":
  360. // contentFinal = p.renderPatchContent()
  361. // case "write":
  362. // contentFinal = p.renderWriteContent()
  363. // case "fetch":
  364. // contentFinal = p.renderFetchContent()
  365. // default:
  366. // contentFinal = p.renderDefaultContent()
  367. // }
  368. //
  369. // content := lipgloss.JoinVertical(
  370. // lipgloss.Top,
  371. // title,
  372. // baseStyle.Render(strings.Repeat(" ", lipgloss.Width(title))),
  373. // headerContent,
  374. // contentFinal,
  375. // buttons,
  376. // baseStyle.Render(strings.Repeat(" ", p.width-4)),
  377. // )
  378. //
  379. // return baseStyle.
  380. // Padding(1, 0, 0, 1).
  381. // Border(lipgloss.RoundedBorder()).
  382. // BorderBackground(t.Background()).
  383. // BorderForeground(t.TextMuted()).
  384. // Width(p.width).
  385. // Height(p.height).
  386. // Render(
  387. // content,
  388. // )
  389. }
  390. func (p *permissionDialogCmp) View() string {
  391. return p.render()
  392. }
  393. func (p *permissionDialogCmp) BindingKeys() []key.Binding {
  394. return layout.KeyMapToSlice(permissionsKeys)
  395. }
  396. func (p *permissionDialogCmp) SetSize() tea.Cmd {
  397. // if p.permission.ID == "" {
  398. // return nil
  399. // }
  400. // switch p.permission.ToolName {
  401. // case "bash":
  402. // p.width = int(float64(p.windowSize.Width) * 0.4)
  403. // p.height = int(float64(p.windowSize.Height) * 0.3)
  404. // case "edit":
  405. // p.width = int(float64(p.windowSize.Width) * 0.8)
  406. // p.height = int(float64(p.windowSize.Height) * 0.8)
  407. // case "write":
  408. // p.width = int(float64(p.windowSize.Width) * 0.8)
  409. // p.height = int(float64(p.windowSize.Height) * 0.8)
  410. // case "fetch":
  411. // p.width = int(float64(p.windowSize.Width) * 0.4)
  412. // p.height = int(float64(p.windowSize.Height) * 0.3)
  413. // default:
  414. // p.width = int(float64(p.windowSize.Width) * 0.7)
  415. // p.height = int(float64(p.windowSize.Height) * 0.5)
  416. // }
  417. return nil
  418. }
  419. // func (p *permissionDialogCmp) SetPermissions(permission permission.PermissionRequest) tea.Cmd {
  420. // p.permission = permission
  421. // return p.SetSize()
  422. // }
  423. // Helper to get or set cached diff content
  424. func (c *permissionDialogCmp) GetOrSetDiff(key string, generator func() (string, error)) string {
  425. if cached, ok := c.diffCache[key]; ok {
  426. return cached
  427. }
  428. content, err := generator()
  429. if err != nil {
  430. return fmt.Sprintf("Error formatting diff: %v", err)
  431. }
  432. c.diffCache[key] = content
  433. return content
  434. }
  435. // Helper to get or set cached markdown content
  436. func (c *permissionDialogCmp) GetOrSetMarkdown(key string, generator func() (string, error)) string {
  437. if cached, ok := c.markdownCache[key]; ok {
  438. return cached
  439. }
  440. content, err := generator()
  441. if err != nil {
  442. return fmt.Sprintf("Error rendering markdown: %v", err)
  443. }
  444. c.markdownCache[key] = content
  445. return content
  446. }
  447. func NewPermissionDialogCmp() PermissionDialogCmp {
  448. // Create viewport for content
  449. contentViewport := viewport.New(0, 0)
  450. return &permissionDialogCmp{
  451. contentViewPort: contentViewport,
  452. selectedOption: 0, // Default to "Allow"
  453. diffCache: make(map[string]string),
  454. markdownCache: make(map[string]string),
  455. }
  456. }