message.go 20 KB

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