message.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800
  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. // Build list of attachment filenames for highlighting
  194. for _, filePart := range fileParts {
  195. atFilename := "@" + filePart.Filename
  196. // Find and highlight complete @filename references
  197. highlightStyle := base.Foreground(t.Secondary())
  198. text = strings.ReplaceAll(text, atFilename, highlightStyle.Render(atFilename))
  199. }
  200. content = base.Width(width - 6).Render(text)
  201. }
  202. timestamp := ts.
  203. Local().
  204. Format("02 Jan 2006 03:04 PM")
  205. if time.Now().Format("02 Jan 2006") == timestamp[:11] {
  206. // don't show the date if it's today
  207. timestamp = timestamp[12:]
  208. }
  209. info := fmt.Sprintf("%s (%s)", author, timestamp)
  210. info = styles.NewStyle().Foreground(t.TextMuted()).Render(info)
  211. if !showToolDetails && toolCalls != nil && len(toolCalls) > 0 {
  212. content = content + "\n\n"
  213. for _, toolCall := range toolCalls {
  214. title := renderToolTitle(toolCall, width-2)
  215. style := styles.NewStyle()
  216. if toolCall.State.Status == opencode.ToolPartStateStatusError {
  217. style = style.Foreground(t.Error())
  218. }
  219. title = style.Render(title)
  220. title = "∟ " + title + "\n"
  221. content = content + title
  222. }
  223. }
  224. sections := []string{content, info}
  225. if extra != "" {
  226. sections = append(sections, "\n"+extra)
  227. }
  228. content = strings.Join(sections, "\n")
  229. switch message.(type) {
  230. case opencode.UserMessage:
  231. return renderContentBlock(
  232. app,
  233. content,
  234. width,
  235. WithTextColor(t.Text()),
  236. WithBorderColor(t.Secondary()),
  237. WithBorderRight(),
  238. )
  239. case opencode.AssistantMessage:
  240. return renderContentBlock(
  241. app,
  242. content,
  243. width,
  244. WithBorderColor(t.Accent()),
  245. )
  246. }
  247. return ""
  248. }
  249. func renderToolDetails(
  250. app *app.App,
  251. toolCall opencode.ToolPart,
  252. permission opencode.Permission,
  253. width int,
  254. ) string {
  255. measure := util.Measure("chat.renderToolDetails")
  256. defer measure("tool", toolCall.Tool)
  257. ignoredTools := []string{"todoread"}
  258. if slices.Contains(ignoredTools, toolCall.Tool) {
  259. return ""
  260. }
  261. if toolCall.State.Status == opencode.ToolPartStateStatusPending {
  262. title := renderToolTitle(toolCall, width)
  263. return renderContentBlock(app, title, width)
  264. }
  265. var result *string
  266. if toolCall.State.Output != "" {
  267. result = &toolCall.State.Output
  268. }
  269. toolInputMap := make(map[string]any)
  270. if toolCall.State.Input != nil {
  271. value := toolCall.State.Input
  272. if m, ok := value.(map[string]any); ok {
  273. toolInputMap = m
  274. keys := make([]string, 0, len(toolInputMap))
  275. for key := range toolInputMap {
  276. keys = append(keys, key)
  277. }
  278. slices.Sort(keys)
  279. }
  280. }
  281. body := ""
  282. t := theme.CurrentTheme()
  283. backgroundColor := t.BackgroundPanel()
  284. borderColor := t.BackgroundPanel()
  285. defaultStyle := styles.NewStyle().Background(backgroundColor).Width(width - 6).Render
  286. permissionContent := ""
  287. if permission.ID != "" {
  288. borderColor = t.Warning()
  289. base := styles.NewStyle().Background(backgroundColor)
  290. text := base.Foreground(t.Text()).Bold(true).Render
  291. muted := base.Foreground(t.TextMuted()).Render
  292. permissionContent = "Permission required to run this tool:\n\n"
  293. permissionContent += text(
  294. "enter ",
  295. ) + muted(
  296. "accept ",
  297. ) + text(
  298. "a",
  299. ) + muted(
  300. " accept always ",
  301. ) + text(
  302. "esc",
  303. ) + muted(
  304. " reject",
  305. )
  306. }
  307. if permission.Metadata != nil {
  308. metadata := toolCall.State.Metadata.(map[string]any)
  309. if metadata == nil {
  310. metadata = map[string]any{}
  311. }
  312. maps.Copy(metadata, permission.Metadata)
  313. toolCall.State.Metadata = metadata
  314. }
  315. if toolCall.State.Metadata != nil {
  316. metadata := toolCall.State.Metadata.(map[string]any)
  317. switch toolCall.Tool {
  318. case "read":
  319. var preview any
  320. if metadata != nil {
  321. preview = metadata["preview"]
  322. }
  323. if preview != nil && toolInputMap["filePath"] != nil {
  324. filename := toolInputMap["filePath"].(string)
  325. body = preview.(string)
  326. body = util.RenderFile(filename, body, width, util.WithTruncate(6))
  327. }
  328. case "edit":
  329. if filename, ok := toolInputMap["filePath"].(string); ok {
  330. var diffField any
  331. if metadata != nil {
  332. diffField = metadata["diff"]
  333. }
  334. if diffField != nil {
  335. patch := diffField.(string)
  336. var formattedDiff string
  337. if width < 120 {
  338. formattedDiff, _ = diff.FormatUnifiedDiff(
  339. filename,
  340. patch,
  341. diff.WithWidth(width-2),
  342. )
  343. } else {
  344. formattedDiff, _ = diff.FormatDiff(
  345. filename,
  346. patch,
  347. diff.WithWidth(width-2),
  348. )
  349. }
  350. body = strings.TrimSpace(formattedDiff)
  351. style := styles.NewStyle().
  352. Background(backgroundColor).
  353. Foreground(t.TextMuted()).
  354. Padding(1, 2).
  355. Width(width - 4)
  356. if diagnostics := renderDiagnostics(metadata, filename, backgroundColor, width-6); diagnostics != "" {
  357. diagnostics = style.Render(diagnostics)
  358. body += "\n" + diagnostics
  359. }
  360. title := renderToolTitle(toolCall, width)
  361. title = style.Render(title)
  362. content := title + "\n" + body
  363. if permissionContent != "" {
  364. permissionContent = styles.NewStyle().
  365. Background(backgroundColor).
  366. Padding(1, 2).
  367. Render(permissionContent)
  368. content += "\n" + permissionContent
  369. }
  370. content = renderContentBlock(
  371. app,
  372. content,
  373. width,
  374. WithPadding(0),
  375. WithBorderColor(borderColor),
  376. WithBorderBoth(permission.ID != ""),
  377. )
  378. return content
  379. }
  380. }
  381. case "write":
  382. if filename, ok := toolInputMap["filePath"].(string); ok {
  383. if content, ok := toolInputMap["content"].(string); ok {
  384. body = util.RenderFile(filename, content, width)
  385. if diagnostics := renderDiagnostics(metadata, filename, backgroundColor, width-4); diagnostics != "" {
  386. body += "\n\n" + diagnostics
  387. }
  388. }
  389. }
  390. case "bash":
  391. command := toolInputMap["command"].(string)
  392. body = fmt.Sprintf("```console\n$ %s\n", command)
  393. stdout := metadata["stdout"]
  394. if stdout != nil {
  395. body += ansi.Strip(fmt.Sprintf("%s", stdout))
  396. }
  397. body += "```"
  398. body = util.ToMarkdown(body, width, backgroundColor)
  399. case "webfetch":
  400. if format, ok := toolInputMap["format"].(string); ok && result != nil {
  401. body = *result
  402. body = util.TruncateHeight(body, 10)
  403. if format == "html" || format == "markdown" {
  404. body = util.ToMarkdown(body, width, backgroundColor)
  405. }
  406. }
  407. case "todowrite":
  408. todos := metadata["todos"]
  409. if todos != nil {
  410. for _, item := range todos.([]any) {
  411. todo := item.(map[string]any)
  412. content := todo["content"].(string)
  413. switch todo["status"] {
  414. case "completed":
  415. body += fmt.Sprintf("- [x] %s\n", content)
  416. case "cancelled":
  417. // strike through cancelled todo
  418. body += fmt.Sprintf("- [ ] ~~%s~~\n", content)
  419. case "in_progress":
  420. // highlight in progress todo
  421. body += fmt.Sprintf("- [ ] `%s`\n", content)
  422. default:
  423. body += fmt.Sprintf("- [ ] %s\n", content)
  424. }
  425. }
  426. body = util.ToMarkdown(body, width, backgroundColor)
  427. }
  428. case "task":
  429. summary := metadata["summary"]
  430. if summary != nil {
  431. toolcalls := summary.([]any)
  432. steps := []string{}
  433. for _, item := range toolcalls {
  434. data, _ := json.Marshal(item)
  435. var toolCall opencode.ToolPart
  436. _ = json.Unmarshal(data, &toolCall)
  437. step := renderToolTitle(toolCall, width-2)
  438. step = "∟ " + step
  439. steps = append(steps, step)
  440. }
  441. body = strings.Join(steps, "\n")
  442. }
  443. body = defaultStyle(body)
  444. default:
  445. if result == nil {
  446. empty := ""
  447. result = &empty
  448. }
  449. body = *result
  450. body = util.TruncateHeight(body, 10)
  451. body = defaultStyle(body)
  452. }
  453. }
  454. error := ""
  455. if toolCall.State.Status == opencode.ToolPartStateStatusError {
  456. error = toolCall.State.Error
  457. }
  458. if error != "" {
  459. body = styles.NewStyle().
  460. Width(width - 6).
  461. Foreground(t.Error()).
  462. Background(backgroundColor).
  463. Render(error)
  464. }
  465. if body == "" && error == "" && result != nil {
  466. body = *result
  467. body = util.TruncateHeight(body, 10)
  468. body = defaultStyle(body)
  469. }
  470. if body == "" {
  471. body = defaultStyle("")
  472. }
  473. title := renderToolTitle(toolCall, width)
  474. content := title + "\n\n" + body
  475. if permissionContent != "" {
  476. content += "\n\n\n" + permissionContent
  477. }
  478. return renderContentBlock(
  479. app,
  480. content,
  481. width,
  482. WithBorderColor(borderColor),
  483. WithBorderBoth(permission.ID != ""),
  484. )
  485. }
  486. func renderToolName(name string) string {
  487. switch name {
  488. case "webfetch":
  489. return "Fetch"
  490. default:
  491. normalizedName := name
  492. if after, ok := strings.CutPrefix(name, "opencode_"); ok {
  493. normalizedName = after
  494. }
  495. return cases.Title(language.Und).String(normalizedName)
  496. }
  497. }
  498. func getTodoPhase(metadata map[string]any) string {
  499. todos, ok := metadata["todos"].([]any)
  500. if !ok || len(todos) == 0 {
  501. return "Plan"
  502. }
  503. counts := map[string]int{"pending": 0, "completed": 0}
  504. for _, item := range todos {
  505. if todo, ok := item.(map[string]any); ok {
  506. if status, ok := todo["status"].(string); ok {
  507. counts[status]++
  508. }
  509. }
  510. }
  511. total := len(todos)
  512. switch {
  513. case counts["pending"] == total:
  514. return "Creating plan"
  515. case counts["completed"] == total:
  516. return "Completing plan"
  517. default:
  518. return "Updating plan"
  519. }
  520. }
  521. func getTodoTitle(toolCall opencode.ToolPart) string {
  522. if toolCall.State.Status == opencode.ToolPartStateStatusCompleted {
  523. if metadata, ok := toolCall.State.Metadata.(map[string]any); ok {
  524. return getTodoPhase(metadata)
  525. }
  526. }
  527. return "Plan"
  528. }
  529. func renderToolTitle(
  530. toolCall opencode.ToolPart,
  531. width int,
  532. ) string {
  533. if toolCall.State.Status == opencode.ToolPartStateStatusPending {
  534. title := renderToolAction(toolCall.Tool)
  535. return styles.NewStyle().Width(width - 6).Render(title)
  536. }
  537. toolArgs := ""
  538. toolArgsMap := make(map[string]any)
  539. if toolCall.State.Input != nil {
  540. value := toolCall.State.Input
  541. if m, ok := value.(map[string]any); ok {
  542. toolArgsMap = m
  543. keys := make([]string, 0, len(toolArgsMap))
  544. for key := range toolArgsMap {
  545. keys = append(keys, key)
  546. }
  547. slices.Sort(keys)
  548. firstKey := ""
  549. if len(keys) > 0 {
  550. firstKey = keys[0]
  551. }
  552. toolArgs = renderArgs(&toolArgsMap, firstKey)
  553. }
  554. }
  555. title := renderToolName(toolCall.Tool)
  556. switch toolCall.Tool {
  557. case "read":
  558. toolArgs = renderArgs(&toolArgsMap, "filePath")
  559. title = fmt.Sprintf("%s %s", title, toolArgs)
  560. case "edit", "write":
  561. if filename, ok := toolArgsMap["filePath"].(string); ok {
  562. title = fmt.Sprintf("%s %s", title, util.Relative(filename))
  563. }
  564. case "bash":
  565. if description, ok := toolArgsMap["description"].(string); ok {
  566. title = fmt.Sprintf("%s %s", title, description)
  567. }
  568. case "task":
  569. description := toolArgsMap["description"]
  570. subagent := toolArgsMap["subagent_type"]
  571. if description != nil && subagent != nil {
  572. title = fmt.Sprintf("%s[%s] %s", title, subagent, description)
  573. } else if description != nil {
  574. title = fmt.Sprintf("%s %s", title, description)
  575. }
  576. case "webfetch":
  577. toolArgs = renderArgs(&toolArgsMap, "url")
  578. title = fmt.Sprintf("%s %s", title, toolArgs)
  579. case "todowrite":
  580. title = getTodoTitle(toolCall)
  581. case "todoread":
  582. return "Plan"
  583. default:
  584. toolName := renderToolName(toolCall.Tool)
  585. title = fmt.Sprintf("%s %s", toolName, toolArgs)
  586. }
  587. title = truncate.StringWithTail(title, uint(width-6), "...")
  588. if toolCall.State.Error != "" {
  589. t := theme.CurrentTheme()
  590. title = styles.NewStyle().Foreground(t.Error()).Render(title)
  591. }
  592. return title
  593. }
  594. func renderToolAction(name string) string {
  595. switch name {
  596. case "task":
  597. return "Delegating..."
  598. case "bash":
  599. return "Writing command..."
  600. case "edit":
  601. return "Preparing edit..."
  602. case "webfetch":
  603. return "Fetching from the web..."
  604. case "glob":
  605. return "Finding files..."
  606. case "grep":
  607. return "Searching content..."
  608. case "list":
  609. return "Listing directory..."
  610. case "read":
  611. return "Reading file..."
  612. case "write":
  613. return "Preparing write..."
  614. case "todowrite", "todoread":
  615. return "Planning..."
  616. case "patch":
  617. return "Preparing patch..."
  618. }
  619. return "Working..."
  620. }
  621. func renderArgs(args *map[string]any, titleKey string) string {
  622. if args == nil || len(*args) == 0 {
  623. return ""
  624. }
  625. keys := make([]string, 0, len(*args))
  626. for key := range *args {
  627. keys = append(keys, key)
  628. }
  629. slices.Sort(keys)
  630. title := ""
  631. parts := []string{}
  632. for _, key := range keys {
  633. value := (*args)[key]
  634. if value == nil {
  635. continue
  636. }
  637. if key == "filePath" || key == "path" {
  638. value = util.Relative(value.(string))
  639. }
  640. if key == titleKey {
  641. title = fmt.Sprintf("%s", value)
  642. continue
  643. }
  644. parts = append(parts, fmt.Sprintf("%s=%v", key, value))
  645. }
  646. if len(parts) == 0 {
  647. return title
  648. }
  649. return fmt.Sprintf("%s (%s)", title, strings.Join(parts, ", "))
  650. }
  651. // Diagnostic represents an LSP diagnostic
  652. type Diagnostic struct {
  653. Range struct {
  654. Start struct {
  655. Line int `json:"line"`
  656. Character int `json:"character"`
  657. } `json:"start"`
  658. } `json:"range"`
  659. Severity int `json:"severity"`
  660. Message string `json:"message"`
  661. }
  662. // renderDiagnostics formats LSP diagnostics for display in the TUI
  663. func renderDiagnostics(
  664. metadata map[string]any,
  665. filePath string,
  666. backgroundColor compat.AdaptiveColor,
  667. width int,
  668. ) string {
  669. if diagnosticsData, ok := metadata["diagnostics"].(map[string]any); ok {
  670. if fileDiagnostics, ok := diagnosticsData[filePath].([]any); ok {
  671. var errorDiagnostics []string
  672. for _, diagInterface := range fileDiagnostics {
  673. diagMap, ok := diagInterface.(map[string]any)
  674. if !ok {
  675. continue
  676. }
  677. // Parse the diagnostic
  678. var diag Diagnostic
  679. diagBytes, err := json.Marshal(diagMap)
  680. if err != nil {
  681. continue
  682. }
  683. if err := json.Unmarshal(diagBytes, &diag); err != nil {
  684. continue
  685. }
  686. // Only show error diagnostics (severity === 1)
  687. if diag.Severity != 1 {
  688. continue
  689. }
  690. line := diag.Range.Start.Line + 1 // 1-based
  691. column := diag.Range.Start.Character + 1 // 1-based
  692. errorDiagnostics = append(
  693. errorDiagnostics,
  694. fmt.Sprintf("Error [%d:%d] %s", line, column, diag.Message),
  695. )
  696. }
  697. if len(errorDiagnostics) == 0 {
  698. return ""
  699. }
  700. t := theme.CurrentTheme()
  701. var result strings.Builder
  702. for _, diagnostic := range errorDiagnostics {
  703. if result.Len() > 0 {
  704. result.WriteString("\n\n")
  705. }
  706. diagnostic = ansi.WordwrapWc(diagnostic, width, " -")
  707. result.WriteString(
  708. styles.NewStyle().
  709. Background(backgroundColor).
  710. Foreground(t.Error()).
  711. Render(diagnostic),
  712. )
  713. }
  714. return result.String()
  715. }
  716. }
  717. return ""
  718. // diagnosticsData should be a map[string][]Diagnostic
  719. // strDiagnosticsData := diagnosticsData.Raw()
  720. // diagnosticsMap := gjson.Parse(strDiagnosticsData).Value().(map[string]any)
  721. // fileDiagnostics, ok := diagnosticsMap[filePath]
  722. // if !ok {
  723. // return ""
  724. // }
  725. // diagnosticsList, ok := fileDiagnostics.([]any)
  726. // if !ok {
  727. // return ""
  728. // }
  729. }