message.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816
  1. package chat
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "maps"
  6. "slices"
  7. "strings"
  8. "time"
  9. "github.com/charmbracelet/lipgloss/v2"
  10. "github.com/charmbracelet/lipgloss/v2/compat"
  11. "github.com/charmbracelet/x/ansi"
  12. "github.com/muesli/reflow/truncate"
  13. "github.com/sst/opencode-sdk-go"
  14. "github.com/sst/opencode/internal/app"
  15. "github.com/sst/opencode/internal/components/diff"
  16. "github.com/sst/opencode/internal/styles"
  17. "github.com/sst/opencode/internal/theme"
  18. "github.com/sst/opencode/internal/util"
  19. "golang.org/x/text/cases"
  20. "golang.org/x/text/language"
  21. )
  22. type blockRenderer struct {
  23. textColor compat.AdaptiveColor
  24. border bool
  25. borderColor *compat.AdaptiveColor
  26. borderLeft bool
  27. borderRight bool
  28. paddingTop int
  29. paddingBottom int
  30. paddingLeft int
  31. paddingRight int
  32. marginTop int
  33. marginBottom int
  34. }
  35. type renderingOption func(*blockRenderer)
  36. func WithTextColor(color compat.AdaptiveColor) renderingOption {
  37. return func(c *blockRenderer) {
  38. c.textColor = color
  39. }
  40. }
  41. func WithNoBorder() renderingOption {
  42. return func(c *blockRenderer) {
  43. c.border = false
  44. }
  45. }
  46. func WithBorderColor(color compat.AdaptiveColor) renderingOption {
  47. return func(c *blockRenderer) {
  48. c.borderColor = &color
  49. }
  50. }
  51. func WithBorderLeft() renderingOption {
  52. return func(c *blockRenderer) {
  53. c.borderLeft = true
  54. c.borderRight = false
  55. }
  56. }
  57. func WithBorderRight() renderingOption {
  58. return func(c *blockRenderer) {
  59. c.borderLeft = false
  60. c.borderRight = true
  61. }
  62. }
  63. func WithBorderBoth(value bool) renderingOption {
  64. return func(c *blockRenderer) {
  65. if value {
  66. c.borderLeft = true
  67. c.borderRight = true
  68. }
  69. }
  70. }
  71. func WithMarginTop(padding int) renderingOption {
  72. return func(c *blockRenderer) {
  73. c.marginTop = padding
  74. }
  75. }
  76. func WithMarginBottom(padding int) renderingOption {
  77. return func(c *blockRenderer) {
  78. c.marginBottom = padding
  79. }
  80. }
  81. func WithPadding(padding int) renderingOption {
  82. return func(c *blockRenderer) {
  83. c.paddingTop = padding
  84. c.paddingBottom = padding
  85. c.paddingLeft = padding
  86. c.paddingRight = padding
  87. }
  88. }
  89. func WithPaddingLeft(padding int) renderingOption {
  90. return func(c *blockRenderer) {
  91. c.paddingLeft = padding
  92. }
  93. }
  94. func WithPaddingRight(padding int) renderingOption {
  95. return func(c *blockRenderer) {
  96. c.paddingRight = padding
  97. }
  98. }
  99. func WithPaddingTop(padding int) renderingOption {
  100. return func(c *blockRenderer) {
  101. c.paddingTop = padding
  102. }
  103. }
  104. func WithPaddingBottom(padding int) renderingOption {
  105. return func(c *blockRenderer) {
  106. c.paddingBottom = padding
  107. }
  108. }
  109. func renderContentBlock(
  110. app *app.App,
  111. content string,
  112. width int,
  113. options ...renderingOption,
  114. ) string {
  115. t := theme.CurrentTheme()
  116. renderer := &blockRenderer{
  117. textColor: t.TextMuted(),
  118. border: true,
  119. borderLeft: true,
  120. borderRight: false,
  121. paddingTop: 1,
  122. paddingBottom: 1,
  123. paddingLeft: 2,
  124. paddingRight: 2,
  125. }
  126. for _, option := range options {
  127. option(renderer)
  128. }
  129. borderColor := t.BackgroundPanel()
  130. if renderer.borderColor != nil {
  131. borderColor = *renderer.borderColor
  132. }
  133. style := styles.NewStyle().
  134. Foreground(renderer.textColor).
  135. Background(t.BackgroundPanel()).
  136. PaddingTop(renderer.paddingTop).
  137. PaddingBottom(renderer.paddingBottom).
  138. PaddingLeft(renderer.paddingLeft).
  139. PaddingRight(renderer.paddingRight).
  140. AlignHorizontal(lipgloss.Left)
  141. if renderer.border {
  142. style = style.
  143. BorderStyle(lipgloss.ThickBorder()).
  144. BorderLeft(true).
  145. BorderRight(true).
  146. BorderLeftForeground(t.BackgroundPanel()).
  147. BorderLeftBackground(t.Background()).
  148. BorderRightForeground(t.BackgroundPanel()).
  149. BorderRightBackground(t.Background())
  150. if renderer.borderLeft {
  151. style = style.BorderLeftForeground(borderColor)
  152. }
  153. if renderer.borderRight {
  154. style = style.BorderRightForeground(borderColor)
  155. }
  156. }
  157. content = style.Render(content)
  158. if renderer.marginTop > 0 {
  159. for range renderer.marginTop {
  160. content = "\n" + content
  161. }
  162. }
  163. if renderer.marginBottom > 0 {
  164. for range renderer.marginBottom {
  165. content = content + "\n"
  166. }
  167. }
  168. return content
  169. }
  170. func renderText(
  171. app *app.App,
  172. message opencode.MessageUnion,
  173. text string,
  174. author string,
  175. showToolDetails bool,
  176. width int,
  177. extra string,
  178. fileParts []opencode.FilePart,
  179. toolCalls ...opencode.ToolPart,
  180. ) string {
  181. t := theme.CurrentTheme()
  182. var ts time.Time
  183. backgroundColor := t.BackgroundPanel()
  184. var content string
  185. switch casted := message.(type) {
  186. case opencode.AssistantMessage:
  187. ts = time.UnixMilli(int64(casted.Time.Created))
  188. content = util.ToMarkdown(text, width, backgroundColor)
  189. case opencode.UserMessage:
  190. ts = time.UnixMilli(int64(casted.Time.Created))
  191. base := styles.NewStyle().Foreground(t.Text()).Background(backgroundColor)
  192. text = ansi.WordwrapWc(text, width-6, " -")
  193. var result strings.Builder
  194. lastEnd := int64(0)
  195. // Apply highlighting to filenames and base style to rest of text
  196. for _, filePart := range fileParts {
  197. highlight := base.Foreground(t.Secondary())
  198. start, end := filePart.Source.Text.Start, filePart.Source.Text.End
  199. if start > lastEnd {
  200. result.WriteString(base.Render(text[lastEnd:start]))
  201. }
  202. result.WriteString(highlight.Render(text[start:end]))
  203. lastEnd = end
  204. }
  205. if lastEnd < int64(len(text)) {
  206. result.WriteString(base.Render(text[lastEnd:]))
  207. }
  208. content = base.Width(width - 6).Render(result.String())
  209. }
  210. timestamp := ts.
  211. Local().
  212. Format("02 Jan 2006 03:04 PM")
  213. if time.Now().Format("02 Jan 2006") == timestamp[:11] {
  214. // don't show the date if it's today
  215. timestamp = timestamp[12:]
  216. }
  217. info := fmt.Sprintf("%s (%s)", author, timestamp)
  218. info = styles.NewStyle().Foreground(t.TextMuted()).Render(info)
  219. if !showToolDetails && toolCalls != nil && len(toolCalls) > 0 {
  220. content = content + "\n\n"
  221. for _, toolCall := range toolCalls {
  222. title := renderToolTitle(toolCall, width-2)
  223. style := styles.NewStyle()
  224. if toolCall.State.Status == opencode.ToolPartStateStatusError {
  225. style = style.Foreground(t.Error())
  226. }
  227. title = style.Render(title)
  228. title = "∟ " + title + "\n"
  229. content = content + title
  230. }
  231. }
  232. sections := []string{content, info}
  233. if extra != "" {
  234. sections = append(sections, "\n"+extra)
  235. }
  236. content = strings.Join(sections, "\n")
  237. switch message.(type) {
  238. case opencode.UserMessage:
  239. return renderContentBlock(
  240. app,
  241. content,
  242. width,
  243. WithTextColor(t.Text()),
  244. WithBorderColor(t.Secondary()),
  245. WithBorderRight(),
  246. )
  247. case opencode.AssistantMessage:
  248. return renderContentBlock(
  249. app,
  250. content,
  251. width,
  252. WithBorderColor(t.Accent()),
  253. )
  254. }
  255. return ""
  256. }
  257. func renderToolDetails(
  258. app *app.App,
  259. toolCall opencode.ToolPart,
  260. permission opencode.Permission,
  261. width int,
  262. ) string {
  263. measure := util.Measure("chat.renderToolDetails")
  264. defer measure("tool", toolCall.Tool)
  265. ignoredTools := []string{"todoread"}
  266. if slices.Contains(ignoredTools, toolCall.Tool) {
  267. return ""
  268. }
  269. if toolCall.State.Status == opencode.ToolPartStateStatusPending {
  270. title := renderToolTitle(toolCall, width)
  271. return renderContentBlock(app, title, width)
  272. }
  273. var result *string
  274. if toolCall.State.Output != "" {
  275. result = &toolCall.State.Output
  276. }
  277. toolInputMap := make(map[string]any)
  278. if toolCall.State.Input != nil {
  279. value := toolCall.State.Input
  280. if m, ok := value.(map[string]any); ok {
  281. toolInputMap = m
  282. keys := make([]string, 0, len(toolInputMap))
  283. for key := range toolInputMap {
  284. keys = append(keys, key)
  285. }
  286. slices.Sort(keys)
  287. }
  288. }
  289. body := ""
  290. t := theme.CurrentTheme()
  291. backgroundColor := t.BackgroundPanel()
  292. borderColor := t.BackgroundPanel()
  293. defaultStyle := styles.NewStyle().Background(backgroundColor).Width(width - 6).Render
  294. permissionContent := ""
  295. if permission.ID != "" {
  296. borderColor = t.Warning()
  297. base := styles.NewStyle().Background(backgroundColor)
  298. text := base.Foreground(t.Text()).Bold(true).Render
  299. muted := base.Foreground(t.TextMuted()).Render
  300. permissionContent = "Permission required to run this tool:\n\n"
  301. permissionContent += text(
  302. "enter ",
  303. ) + muted(
  304. "accept ",
  305. ) + text(
  306. "a",
  307. ) + muted(
  308. " accept always ",
  309. ) + text(
  310. "esc",
  311. ) + muted(
  312. " reject",
  313. )
  314. }
  315. if permission.Metadata != nil {
  316. metadata := toolCall.State.Metadata.(map[string]any)
  317. if metadata == nil {
  318. metadata = map[string]any{}
  319. }
  320. maps.Copy(metadata, permission.Metadata)
  321. toolCall.State.Metadata = metadata
  322. }
  323. if toolCall.State.Metadata != nil {
  324. metadata := toolCall.State.Metadata.(map[string]any)
  325. switch toolCall.Tool {
  326. case "read":
  327. var preview any
  328. if metadata != nil {
  329. preview = metadata["preview"]
  330. }
  331. if preview != nil && toolInputMap["filePath"] != nil {
  332. filename := toolInputMap["filePath"].(string)
  333. body = preview.(string)
  334. body = util.RenderFile(filename, body, width, util.WithTruncate(6))
  335. }
  336. case "edit":
  337. if filename, ok := toolInputMap["filePath"].(string); ok {
  338. var diffField any
  339. if metadata != nil {
  340. diffField = metadata["diff"]
  341. }
  342. if diffField != nil {
  343. patch := diffField.(string)
  344. var formattedDiff string
  345. if width < 120 {
  346. formattedDiff, _ = diff.FormatUnifiedDiff(
  347. filename,
  348. patch,
  349. diff.WithWidth(width-2),
  350. )
  351. } else {
  352. formattedDiff, _ = diff.FormatDiff(
  353. filename,
  354. patch,
  355. diff.WithWidth(width-2),
  356. )
  357. }
  358. body = strings.TrimSpace(formattedDiff)
  359. style := styles.NewStyle().
  360. Background(backgroundColor).
  361. Foreground(t.TextMuted()).
  362. Padding(1, 2).
  363. Width(width - 4)
  364. if diagnostics := renderDiagnostics(metadata, filename, backgroundColor, width-6); diagnostics != "" {
  365. diagnostics = style.Render(diagnostics)
  366. body += "\n" + diagnostics
  367. }
  368. title := renderToolTitle(toolCall, width)
  369. title = style.Render(title)
  370. content := title + "\n" + body
  371. if permissionContent != "" {
  372. permissionContent = styles.NewStyle().
  373. Background(backgroundColor).
  374. Padding(1, 2).
  375. Render(permissionContent)
  376. content += "\n" + permissionContent
  377. }
  378. content = renderContentBlock(
  379. app,
  380. content,
  381. width,
  382. WithPadding(0),
  383. WithBorderColor(borderColor),
  384. WithBorderBoth(permission.ID != ""),
  385. )
  386. return content
  387. }
  388. }
  389. case "write":
  390. if filename, ok := toolInputMap["filePath"].(string); ok {
  391. if content, ok := toolInputMap["content"].(string); ok {
  392. body = util.RenderFile(filename, content, width)
  393. if diagnostics := renderDiagnostics(metadata, filename, backgroundColor, width-4); diagnostics != "" {
  394. body += "\n\n" + diagnostics
  395. }
  396. }
  397. }
  398. case "bash":
  399. command := toolInputMap["command"].(string)
  400. body = fmt.Sprintf("```console\n$ %s\n", command)
  401. stdout := metadata["stdout"]
  402. if stdout != nil {
  403. body += ansi.Strip(fmt.Sprintf("%s", stdout))
  404. }
  405. stderr := metadata["stderr"]
  406. if stderr != nil {
  407. body += ansi.Strip(fmt.Sprintf("%s", stderr))
  408. }
  409. body += "```"
  410. body = util.ToMarkdown(body, width, backgroundColor)
  411. case "webfetch":
  412. if format, ok := toolInputMap["format"].(string); ok && result != nil {
  413. body = *result
  414. body = util.TruncateHeight(body, 10)
  415. if format == "html" || format == "markdown" {
  416. body = util.ToMarkdown(body, width, backgroundColor)
  417. }
  418. }
  419. case "todowrite":
  420. todos := metadata["todos"]
  421. if todos != nil {
  422. for _, item := range todos.([]any) {
  423. todo := item.(map[string]any)
  424. content := todo["content"].(string)
  425. switch todo["status"] {
  426. case "completed":
  427. body += fmt.Sprintf("- [x] %s\n", content)
  428. case "cancelled":
  429. // strike through cancelled todo
  430. body += fmt.Sprintf("- [ ] ~~%s~~\n", content)
  431. case "in_progress":
  432. // highlight in progress todo
  433. body += fmt.Sprintf("- [ ] `%s`\n", content)
  434. default:
  435. body += fmt.Sprintf("- [ ] %s\n", content)
  436. }
  437. }
  438. body = util.ToMarkdown(body, width, backgroundColor)
  439. }
  440. case "task":
  441. summary := metadata["summary"]
  442. if summary != nil {
  443. toolcalls := summary.([]any)
  444. steps := []string{}
  445. for _, item := range toolcalls {
  446. data, _ := json.Marshal(item)
  447. var toolCall opencode.ToolPart
  448. _ = json.Unmarshal(data, &toolCall)
  449. step := renderToolTitle(toolCall, width-2)
  450. step = "∟ " + step
  451. steps = append(steps, step)
  452. }
  453. body = strings.Join(steps, "\n")
  454. }
  455. body = defaultStyle(body)
  456. default:
  457. if result == nil {
  458. empty := ""
  459. result = &empty
  460. }
  461. body = *result
  462. body = util.TruncateHeight(body, 10)
  463. body = defaultStyle(body)
  464. }
  465. }
  466. error := ""
  467. if toolCall.State.Status == opencode.ToolPartStateStatusError {
  468. error = toolCall.State.Error
  469. }
  470. if error != "" {
  471. body = styles.NewStyle().
  472. Width(width - 6).
  473. Foreground(t.Error()).
  474. Background(backgroundColor).
  475. Render(error)
  476. }
  477. if body == "" && error == "" && result != nil {
  478. body = *result
  479. body = util.TruncateHeight(body, 10)
  480. body = defaultStyle(body)
  481. }
  482. if body == "" {
  483. body = defaultStyle("")
  484. }
  485. title := renderToolTitle(toolCall, width)
  486. content := title + "\n\n" + body
  487. if permissionContent != "" {
  488. content += "\n\n\n" + permissionContent
  489. }
  490. return renderContentBlock(
  491. app,
  492. content,
  493. width,
  494. WithBorderColor(borderColor),
  495. WithBorderBoth(permission.ID != ""),
  496. )
  497. }
  498. func renderToolName(name string) string {
  499. switch name {
  500. case "webfetch":
  501. return "Fetch"
  502. default:
  503. normalizedName := name
  504. if after, ok := strings.CutPrefix(name, "opencode_"); ok {
  505. normalizedName = after
  506. }
  507. return cases.Title(language.Und).String(normalizedName)
  508. }
  509. }
  510. func getTodoPhase(metadata map[string]any) string {
  511. todos, ok := metadata["todos"].([]any)
  512. if !ok || len(todos) == 0 {
  513. return "Plan"
  514. }
  515. counts := map[string]int{"pending": 0, "completed": 0}
  516. for _, item := range todos {
  517. if todo, ok := item.(map[string]any); ok {
  518. if status, ok := todo["status"].(string); ok {
  519. counts[status]++
  520. }
  521. }
  522. }
  523. total := len(todos)
  524. switch {
  525. case counts["pending"] == total:
  526. return "Creating plan"
  527. case counts["completed"] == total:
  528. return "Completing plan"
  529. default:
  530. return "Updating plan"
  531. }
  532. }
  533. func getTodoTitle(toolCall opencode.ToolPart) string {
  534. if toolCall.State.Status == opencode.ToolPartStateStatusCompleted {
  535. if metadata, ok := toolCall.State.Metadata.(map[string]any); ok {
  536. return getTodoPhase(metadata)
  537. }
  538. }
  539. return "Plan"
  540. }
  541. func renderToolTitle(
  542. toolCall opencode.ToolPart,
  543. width int,
  544. ) string {
  545. if toolCall.State.Status == opencode.ToolPartStateStatusPending {
  546. title := renderToolAction(toolCall.Tool)
  547. return styles.NewStyle().Width(width - 6).Render(title)
  548. }
  549. toolArgs := ""
  550. toolArgsMap := make(map[string]any)
  551. if toolCall.State.Input != nil {
  552. value := toolCall.State.Input
  553. if m, ok := value.(map[string]any); ok {
  554. toolArgsMap = m
  555. keys := make([]string, 0, len(toolArgsMap))
  556. for key := range toolArgsMap {
  557. keys = append(keys, key)
  558. }
  559. slices.Sort(keys)
  560. firstKey := ""
  561. if len(keys) > 0 {
  562. firstKey = keys[0]
  563. }
  564. toolArgs = renderArgs(&toolArgsMap, firstKey)
  565. }
  566. }
  567. title := renderToolName(toolCall.Tool)
  568. switch toolCall.Tool {
  569. case "read":
  570. toolArgs = renderArgs(&toolArgsMap, "filePath")
  571. title = fmt.Sprintf("%s %s", title, toolArgs)
  572. case "edit", "write":
  573. if filename, ok := toolArgsMap["filePath"].(string); ok {
  574. title = fmt.Sprintf("%s %s", title, util.Relative(filename))
  575. }
  576. case "bash":
  577. if description, ok := toolArgsMap["description"].(string); ok {
  578. title = fmt.Sprintf("%s %s", title, description)
  579. }
  580. case "task":
  581. description := toolArgsMap["description"]
  582. subagent := toolArgsMap["subagent_type"]
  583. if description != nil && subagent != nil {
  584. title = fmt.Sprintf("%s[%s] %s", title, subagent, description)
  585. } else if description != nil {
  586. title = fmt.Sprintf("%s %s", title, description)
  587. }
  588. case "webfetch":
  589. toolArgs = renderArgs(&toolArgsMap, "url")
  590. title = fmt.Sprintf("%s %s", title, toolArgs)
  591. case "todowrite":
  592. title = getTodoTitle(toolCall)
  593. case "todoread":
  594. return "Plan"
  595. default:
  596. toolName := renderToolName(toolCall.Tool)
  597. title = fmt.Sprintf("%s %s", toolName, toolArgs)
  598. }
  599. title = truncate.StringWithTail(title, uint(width-6), "...")
  600. if toolCall.State.Error != "" {
  601. t := theme.CurrentTheme()
  602. title = styles.NewStyle().Foreground(t.Error()).Render(title)
  603. }
  604. return title
  605. }
  606. func renderToolAction(name string) string {
  607. switch name {
  608. case "task":
  609. return "Delegating..."
  610. case "bash":
  611. return "Writing command..."
  612. case "edit":
  613. return "Preparing edit..."
  614. case "webfetch":
  615. return "Fetching from the web..."
  616. case "glob":
  617. return "Finding files..."
  618. case "grep":
  619. return "Searching content..."
  620. case "list":
  621. return "Listing directory..."
  622. case "read":
  623. return "Reading file..."
  624. case "write":
  625. return "Preparing write..."
  626. case "todowrite", "todoread":
  627. return "Planning..."
  628. case "patch":
  629. return "Preparing patch..."
  630. }
  631. return "Working..."
  632. }
  633. func renderArgs(args *map[string]any, titleKey string) string {
  634. if args == nil || len(*args) == 0 {
  635. return ""
  636. }
  637. keys := make([]string, 0, len(*args))
  638. for key := range *args {
  639. keys = append(keys, key)
  640. }
  641. slices.Sort(keys)
  642. title := ""
  643. parts := []string{}
  644. for _, key := range keys {
  645. value := (*args)[key]
  646. if value == nil {
  647. continue
  648. }
  649. if key == "filePath" || key == "path" {
  650. value = util.Relative(value.(string))
  651. }
  652. if key == titleKey {
  653. title = fmt.Sprintf("%s", value)
  654. continue
  655. }
  656. parts = append(parts, fmt.Sprintf("%s=%v", key, value))
  657. }
  658. if len(parts) == 0 {
  659. return title
  660. }
  661. return fmt.Sprintf("%s (%s)", title, strings.Join(parts, ", "))
  662. }
  663. // Diagnostic represents an LSP diagnostic
  664. type Diagnostic struct {
  665. Range struct {
  666. Start struct {
  667. Line int `json:"line"`
  668. Character int `json:"character"`
  669. } `json:"start"`
  670. } `json:"range"`
  671. Severity int `json:"severity"`
  672. Message string `json:"message"`
  673. }
  674. // renderDiagnostics formats LSP diagnostics for display in the TUI
  675. func renderDiagnostics(
  676. metadata map[string]any,
  677. filePath string,
  678. backgroundColor compat.AdaptiveColor,
  679. width int,
  680. ) string {
  681. if diagnosticsData, ok := metadata["diagnostics"].(map[string]any); ok {
  682. if fileDiagnostics, ok := diagnosticsData[filePath].([]any); ok {
  683. var errorDiagnostics []string
  684. for _, diagInterface := range fileDiagnostics {
  685. diagMap, ok := diagInterface.(map[string]any)
  686. if !ok {
  687. continue
  688. }
  689. // Parse the diagnostic
  690. var diag Diagnostic
  691. diagBytes, err := json.Marshal(diagMap)
  692. if err != nil {
  693. continue
  694. }
  695. if err := json.Unmarshal(diagBytes, &diag); err != nil {
  696. continue
  697. }
  698. // Only show error diagnostics (severity === 1)
  699. if diag.Severity != 1 {
  700. continue
  701. }
  702. line := diag.Range.Start.Line + 1 // 1-based
  703. column := diag.Range.Start.Character + 1 // 1-based
  704. errorDiagnostics = append(
  705. errorDiagnostics,
  706. fmt.Sprintf("Error [%d:%d] %s", line, column, diag.Message),
  707. )
  708. }
  709. if len(errorDiagnostics) == 0 {
  710. return ""
  711. }
  712. t := theme.CurrentTheme()
  713. var result strings.Builder
  714. for _, diagnostic := range errorDiagnostics {
  715. if result.Len() > 0 {
  716. result.WriteString("\n\n")
  717. }
  718. diagnostic = ansi.WordwrapWc(diagnostic, width, " -")
  719. result.WriteString(
  720. styles.NewStyle().
  721. Background(backgroundColor).
  722. Foreground(t.Error()).
  723. Render(diagnostic),
  724. )
  725. }
  726. return result.String()
  727. }
  728. }
  729. return ""
  730. // diagnosticsData should be a map[string][]Diagnostic
  731. // strDiagnosticsData := diagnosticsData.Raw()
  732. // diagnosticsMap := gjson.Parse(strDiagnosticsData).Value().(map[string]any)
  733. // fileDiagnostics, ok := diagnosticsMap[filePath]
  734. // if !ok {
  735. // return ""
  736. // }
  737. // diagnosticsList, ok := fileDiagnostics.([]any)
  738. // if !ok {
  739. // return ""
  740. // }
  741. }