message.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804
  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. stderr := metadata["stderr"]
  398. if stderr != nil {
  399. body += ansi.Strip(fmt.Sprintf("%s", stderr))
  400. }
  401. body += "```"
  402. body = util.ToMarkdown(body, width, backgroundColor)
  403. case "webfetch":
  404. if format, ok := toolInputMap["format"].(string); ok && result != nil {
  405. body = *result
  406. body = util.TruncateHeight(body, 10)
  407. if format == "html" || format == "markdown" {
  408. body = util.ToMarkdown(body, width, backgroundColor)
  409. }
  410. }
  411. case "todowrite":
  412. todos := metadata["todos"]
  413. if todos != nil {
  414. for _, item := range todos.([]any) {
  415. todo := item.(map[string]any)
  416. content := todo["content"].(string)
  417. switch todo["status"] {
  418. case "completed":
  419. body += fmt.Sprintf("- [x] %s\n", content)
  420. case "cancelled":
  421. // strike through cancelled todo
  422. body += fmt.Sprintf("- [ ] ~~%s~~\n", content)
  423. case "in_progress":
  424. // highlight in progress todo
  425. body += fmt.Sprintf("- [ ] `%s`\n", content)
  426. default:
  427. body += fmt.Sprintf("- [ ] %s\n", content)
  428. }
  429. }
  430. body = util.ToMarkdown(body, width, backgroundColor)
  431. }
  432. case "task":
  433. summary := metadata["summary"]
  434. if summary != nil {
  435. toolcalls := summary.([]any)
  436. steps := []string{}
  437. for _, item := range toolcalls {
  438. data, _ := json.Marshal(item)
  439. var toolCall opencode.ToolPart
  440. _ = json.Unmarshal(data, &toolCall)
  441. step := renderToolTitle(toolCall, width-2)
  442. step = "∟ " + step
  443. steps = append(steps, step)
  444. }
  445. body = strings.Join(steps, "\n")
  446. }
  447. body = defaultStyle(body)
  448. default:
  449. if result == nil {
  450. empty := ""
  451. result = &empty
  452. }
  453. body = *result
  454. body = util.TruncateHeight(body, 10)
  455. body = defaultStyle(body)
  456. }
  457. }
  458. error := ""
  459. if toolCall.State.Status == opencode.ToolPartStateStatusError {
  460. error = toolCall.State.Error
  461. }
  462. if error != "" {
  463. body = styles.NewStyle().
  464. Width(width - 6).
  465. Foreground(t.Error()).
  466. Background(backgroundColor).
  467. Render(error)
  468. }
  469. if body == "" && error == "" && result != nil {
  470. body = *result
  471. body = util.TruncateHeight(body, 10)
  472. body = defaultStyle(body)
  473. }
  474. if body == "" {
  475. body = defaultStyle("")
  476. }
  477. title := renderToolTitle(toolCall, width)
  478. content := title + "\n\n" + body
  479. if permissionContent != "" {
  480. content += "\n\n\n" + permissionContent
  481. }
  482. return renderContentBlock(
  483. app,
  484. content,
  485. width,
  486. WithBorderColor(borderColor),
  487. WithBorderBoth(permission.ID != ""),
  488. )
  489. }
  490. func renderToolName(name string) string {
  491. switch name {
  492. case "webfetch":
  493. return "Fetch"
  494. default:
  495. normalizedName := name
  496. if after, ok := strings.CutPrefix(name, "opencode_"); ok {
  497. normalizedName = after
  498. }
  499. return cases.Title(language.Und).String(normalizedName)
  500. }
  501. }
  502. func getTodoPhase(metadata map[string]any) string {
  503. todos, ok := metadata["todos"].([]any)
  504. if !ok || len(todos) == 0 {
  505. return "Plan"
  506. }
  507. counts := map[string]int{"pending": 0, "completed": 0}
  508. for _, item := range todos {
  509. if todo, ok := item.(map[string]any); ok {
  510. if status, ok := todo["status"].(string); ok {
  511. counts[status]++
  512. }
  513. }
  514. }
  515. total := len(todos)
  516. switch {
  517. case counts["pending"] == total:
  518. return "Creating plan"
  519. case counts["completed"] == total:
  520. return "Completing plan"
  521. default:
  522. return "Updating plan"
  523. }
  524. }
  525. func getTodoTitle(toolCall opencode.ToolPart) string {
  526. if toolCall.State.Status == opencode.ToolPartStateStatusCompleted {
  527. if metadata, ok := toolCall.State.Metadata.(map[string]any); ok {
  528. return getTodoPhase(metadata)
  529. }
  530. }
  531. return "Plan"
  532. }
  533. func renderToolTitle(
  534. toolCall opencode.ToolPart,
  535. width int,
  536. ) string {
  537. if toolCall.State.Status == opencode.ToolPartStateStatusPending {
  538. title := renderToolAction(toolCall.Tool)
  539. return styles.NewStyle().Width(width - 6).Render(title)
  540. }
  541. toolArgs := ""
  542. toolArgsMap := make(map[string]any)
  543. if toolCall.State.Input != nil {
  544. value := toolCall.State.Input
  545. if m, ok := value.(map[string]any); ok {
  546. toolArgsMap = m
  547. keys := make([]string, 0, len(toolArgsMap))
  548. for key := range toolArgsMap {
  549. keys = append(keys, key)
  550. }
  551. slices.Sort(keys)
  552. firstKey := ""
  553. if len(keys) > 0 {
  554. firstKey = keys[0]
  555. }
  556. toolArgs = renderArgs(&toolArgsMap, firstKey)
  557. }
  558. }
  559. title := renderToolName(toolCall.Tool)
  560. switch toolCall.Tool {
  561. case "read":
  562. toolArgs = renderArgs(&toolArgsMap, "filePath")
  563. title = fmt.Sprintf("%s %s", title, toolArgs)
  564. case "edit", "write":
  565. if filename, ok := toolArgsMap["filePath"].(string); ok {
  566. title = fmt.Sprintf("%s %s", title, util.Relative(filename))
  567. }
  568. case "bash":
  569. if description, ok := toolArgsMap["description"].(string); ok {
  570. title = fmt.Sprintf("%s %s", title, description)
  571. }
  572. case "task":
  573. description := toolArgsMap["description"]
  574. subagent := toolArgsMap["subagent_type"]
  575. if description != nil && subagent != nil {
  576. title = fmt.Sprintf("%s[%s] %s", title, subagent, description)
  577. } else if description != nil {
  578. title = fmt.Sprintf("%s %s", title, description)
  579. }
  580. case "webfetch":
  581. toolArgs = renderArgs(&toolArgsMap, "url")
  582. title = fmt.Sprintf("%s %s", title, toolArgs)
  583. case "todowrite":
  584. title = getTodoTitle(toolCall)
  585. case "todoread":
  586. return "Plan"
  587. default:
  588. toolName := renderToolName(toolCall.Tool)
  589. title = fmt.Sprintf("%s %s", toolName, toolArgs)
  590. }
  591. title = truncate.StringWithTail(title, uint(width-6), "...")
  592. if toolCall.State.Error != "" {
  593. t := theme.CurrentTheme()
  594. title = styles.NewStyle().Foreground(t.Error()).Render(title)
  595. }
  596. return title
  597. }
  598. func renderToolAction(name string) string {
  599. switch name {
  600. case "task":
  601. return "Delegating..."
  602. case "bash":
  603. return "Writing command..."
  604. case "edit":
  605. return "Preparing edit..."
  606. case "webfetch":
  607. return "Fetching from the web..."
  608. case "glob":
  609. return "Finding files..."
  610. case "grep":
  611. return "Searching content..."
  612. case "list":
  613. return "Listing directory..."
  614. case "read":
  615. return "Reading file..."
  616. case "write":
  617. return "Preparing write..."
  618. case "todowrite", "todoread":
  619. return "Planning..."
  620. case "patch":
  621. return "Preparing patch..."
  622. }
  623. return "Working..."
  624. }
  625. func renderArgs(args *map[string]any, titleKey string) string {
  626. if args == nil || len(*args) == 0 {
  627. return ""
  628. }
  629. keys := make([]string, 0, len(*args))
  630. for key := range *args {
  631. keys = append(keys, key)
  632. }
  633. slices.Sort(keys)
  634. title := ""
  635. parts := []string{}
  636. for _, key := range keys {
  637. value := (*args)[key]
  638. if value == nil {
  639. continue
  640. }
  641. if key == "filePath" || key == "path" {
  642. value = util.Relative(value.(string))
  643. }
  644. if key == titleKey {
  645. title = fmt.Sprintf("%s", value)
  646. continue
  647. }
  648. parts = append(parts, fmt.Sprintf("%s=%v", key, value))
  649. }
  650. if len(parts) == 0 {
  651. return title
  652. }
  653. return fmt.Sprintf("%s (%s)", title, strings.Join(parts, ", "))
  654. }
  655. // Diagnostic represents an LSP diagnostic
  656. type Diagnostic struct {
  657. Range struct {
  658. Start struct {
  659. Line int `json:"line"`
  660. Character int `json:"character"`
  661. } `json:"start"`
  662. } `json:"range"`
  663. Severity int `json:"severity"`
  664. Message string `json:"message"`
  665. }
  666. // renderDiagnostics formats LSP diagnostics for display in the TUI
  667. func renderDiagnostics(
  668. metadata map[string]any,
  669. filePath string,
  670. backgroundColor compat.AdaptiveColor,
  671. width int,
  672. ) string {
  673. if diagnosticsData, ok := metadata["diagnostics"].(map[string]any); ok {
  674. if fileDiagnostics, ok := diagnosticsData[filePath].([]any); ok {
  675. var errorDiagnostics []string
  676. for _, diagInterface := range fileDiagnostics {
  677. diagMap, ok := diagInterface.(map[string]any)
  678. if !ok {
  679. continue
  680. }
  681. // Parse the diagnostic
  682. var diag Diagnostic
  683. diagBytes, err := json.Marshal(diagMap)
  684. if err != nil {
  685. continue
  686. }
  687. if err := json.Unmarshal(diagBytes, &diag); err != nil {
  688. continue
  689. }
  690. // Only show error diagnostics (severity === 1)
  691. if diag.Severity != 1 {
  692. continue
  693. }
  694. line := diag.Range.Start.Line + 1 // 1-based
  695. column := diag.Range.Start.Character + 1 // 1-based
  696. errorDiagnostics = append(
  697. errorDiagnostics,
  698. fmt.Sprintf("Error [%d:%d] %s", line, column, diag.Message),
  699. )
  700. }
  701. if len(errorDiagnostics) == 0 {
  702. return ""
  703. }
  704. t := theme.CurrentTheme()
  705. var result strings.Builder
  706. for _, diagnostic := range errorDiagnostics {
  707. if result.Len() > 0 {
  708. result.WriteString("\n\n")
  709. }
  710. diagnostic = ansi.WordwrapWc(diagnostic, width, " -")
  711. result.WriteString(
  712. styles.NewStyle().
  713. Background(backgroundColor).
  714. Foreground(t.Error()).
  715. Render(diagnostic),
  716. )
  717. }
  718. return result.String()
  719. }
  720. }
  721. return ""
  722. // diagnosticsData should be a map[string][]Diagnostic
  723. // strDiagnosticsData := diagnosticsData.Raw()
  724. // diagnosticsMap := gjson.Parse(strDiagnosticsData).Value().(map[string]any)
  725. // fileDiagnostics, ok := diagnosticsMap[filePath]
  726. // if !ok {
  727. // return ""
  728. // }
  729. // diagnosticsList, ok := fileDiagnostics.([]any)
  730. // if !ok {
  731. // return ""
  732. // }
  733. }