error-component.tsx 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. import { TextAttributes } from "@opentui/core"
  2. import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
  3. import { createSignal } from "solid-js"
  4. import { getScrollAcceleration } from "../util/scroll"
  5. import { useClipboard } from "../context/clipboard"
  6. import { InstallationVersion } from "@opencode-ai/core/installation/version"
  7. import { destroyRenderer } from "../util/renderer"
  8. export function ErrorComponent(props: { error: Error; reset: () => void; mode?: "dark" | "light" }) {
  9. const term = useTerminalDimensions()
  10. const renderer = useRenderer()
  11. const clipboard = useClipboard()
  12. useKeyboard((evt) => {
  13. if (evt.ctrl && evt.name === "c") {
  14. destroyRenderer(renderer)
  15. }
  16. })
  17. const [copied, setCopied] = createSignal(false)
  18. const issueURL = new URL("https://github.com/anomalyco/opencode/issues/new?template=bug-report.yml")
  19. // Choose safe fallback colors per mode since theme context may not be available
  20. const isLight = props.mode === "light"
  21. const colors = {
  22. bg: isLight ? "#ffffff" : "#0a0a0a",
  23. text: isLight ? "#1a1a1a" : "#eeeeee",
  24. muted: isLight ? "#8a8a8a" : "#808080",
  25. primary: isLight ? "#3b7dd8" : "#fab283",
  26. }
  27. if (props.error.message) {
  28. issueURL.searchParams.set("title", `opentui: fatal: ${props.error.message}`)
  29. }
  30. if (props.error.stack) {
  31. issueURL.searchParams.set(
  32. "description",
  33. "```\n" + props.error.stack.substring(0, 6000 - issueURL.toString().length) + "...\n```",
  34. )
  35. }
  36. issueURL.searchParams.set("opencode-version", InstallationVersion)
  37. const copyIssueURL = () => {
  38. void clipboard.write?.(issueURL.toString()).then(() => {
  39. setCopied(true)
  40. })
  41. }
  42. return (
  43. <box flexDirection="column" gap={1} backgroundColor={colors.bg}>
  44. <box flexDirection="row" gap={1} alignItems="center">
  45. <text attributes={TextAttributes.BOLD} fg={colors.text}>
  46. Please report an issue.
  47. </text>
  48. <box onMouseUp={copyIssueURL} backgroundColor={colors.primary} padding={1}>
  49. <text attributes={TextAttributes.BOLD} fg={colors.bg}>
  50. Copy issue URL (exception info pre-filled)
  51. </text>
  52. </box>
  53. {copied() && <text fg={colors.muted}>Successfully copied</text>}
  54. </box>
  55. <box flexDirection="row" gap={2} alignItems="center">
  56. <text fg={colors.text}>A fatal error occurred!</text>
  57. <box onMouseUp={props.reset} backgroundColor={colors.primary} padding={1}>
  58. <text fg={colors.bg}>Reset TUI</text>
  59. </box>
  60. <box onMouseUp={() => destroyRenderer(renderer)} backgroundColor={colors.primary} padding={1}>
  61. <text fg={colors.bg}>Exit</text>
  62. </box>
  63. </box>
  64. <scrollbox height={Math.floor(term().height * 0.7)} scrollAcceleration={getScrollAcceleration()}>
  65. <text fg={colors.muted}>{props.error.stack}</text>
  66. </scrollbox>
  67. <text fg={colors.text}>{props.error.message}</text>
  68. </box>
  69. )
  70. }