@vladimirven/openswe 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/AGENTS.md +203 -0
  2. package/CLAUDE.md +203 -0
  3. package/README.md +166 -0
  4. package/bun.lock +447 -0
  5. package/bunfig.toml +4 -0
  6. package/package.json +42 -0
  7. package/src/app.tsx +84 -0
  8. package/src/components/App.tsx +526 -0
  9. package/src/components/ConfirmDialog.tsx +88 -0
  10. package/src/components/Footer.tsx +50 -0
  11. package/src/components/HelpModal.tsx +136 -0
  12. package/src/components/IssueSelectorModal.tsx +701 -0
  13. package/src/components/ManualSessionModal.tsx +191 -0
  14. package/src/components/PhaseProgress.tsx +45 -0
  15. package/src/components/Preview.tsx +249 -0
  16. package/src/components/ProviderSwitcherModal.tsx +156 -0
  17. package/src/components/ScrollableText.tsx +120 -0
  18. package/src/components/SessionCard.tsx +60 -0
  19. package/src/components/SessionList.tsx +79 -0
  20. package/src/components/SessionTerminal.tsx +89 -0
  21. package/src/components/StatusBar.tsx +84 -0
  22. package/src/components/ThemeSwitcherModal.tsx +237 -0
  23. package/src/components/index.ts +58 -0
  24. package/src/components/session-utils.ts +337 -0
  25. package/src/components/theme.ts +206 -0
  26. package/src/components/types.ts +215 -0
  27. package/src/config/defaults.ts +44 -0
  28. package/src/config/env.ts +67 -0
  29. package/src/config/global.ts +252 -0
  30. package/src/config/index.ts +171 -0
  31. package/src/config/types.ts +131 -0
  32. package/src/core/.gitkeep +0 -0
  33. package/src/core/index.ts +5 -0
  34. package/src/core/parser.ts +62 -0
  35. package/src/core/process-manager.ts +52 -0
  36. package/src/core/session.ts +423 -0
  37. package/src/core/tmux.ts +206 -0
  38. package/src/git/.gitkeep +0 -0
  39. package/src/git/index.ts +8 -0
  40. package/src/git/repo.ts +443 -0
  41. package/src/git/worktree.ts +317 -0
  42. package/src/github/.gitkeep +0 -0
  43. package/src/github/client.ts +208 -0
  44. package/src/github/index.ts +8 -0
  45. package/src/github/issues.ts +351 -0
  46. package/src/index.ts +369 -0
  47. package/src/prompts/.gitkeep +0 -0
  48. package/src/prompts/index.ts +1 -0
  49. package/src/prompts/swe-system.ts +22 -0
  50. package/src/providers/claude.ts +103 -0
  51. package/src/providers/index.ts +21 -0
  52. package/src/providers/opencode.ts +98 -0
  53. package/src/providers/registry.ts +53 -0
  54. package/src/providers/types.ts +117 -0
  55. package/src/store/buffers.ts +234 -0
  56. package/src/store/db.test.ts +579 -0
  57. package/src/store/db.ts +249 -0
  58. package/src/store/index.ts +101 -0
  59. package/src/store/project.ts +119 -0
  60. package/src/store/schema.sql +71 -0
  61. package/src/store/sessions.ts +454 -0
  62. package/src/store/types.ts +194 -0
  63. package/src/theme/context.tsx +170 -0
  64. package/src/theme/custom.ts +134 -0
  65. package/src/theme/index.ts +58 -0
  66. package/src/theme/loader.ts +264 -0
  67. package/src/theme/themes/aura.json +69 -0
  68. package/src/theme/themes/ayu.json +80 -0
  69. package/src/theme/themes/carbonfox.json +248 -0
  70. package/src/theme/themes/catppuccin-frappe.json +233 -0
  71. package/src/theme/themes/catppuccin-macchiato.json +233 -0
  72. package/src/theme/themes/catppuccin.json +112 -0
  73. package/src/theme/themes/cobalt2.json +228 -0
  74. package/src/theme/themes/cursor.json +249 -0
  75. package/src/theme/themes/dracula.json +219 -0
  76. package/src/theme/themes/everforest.json +241 -0
  77. package/src/theme/themes/flexoki.json +237 -0
  78. package/src/theme/themes/github.json +233 -0
  79. package/src/theme/themes/gruvbox.json +242 -0
  80. package/src/theme/themes/kanagawa.json +77 -0
  81. package/src/theme/themes/lucent-orng.json +237 -0
  82. package/src/theme/themes/material.json +235 -0
  83. package/src/theme/themes/matrix.json +77 -0
  84. package/src/theme/themes/mercury.json +252 -0
  85. package/src/theme/themes/monokai.json +221 -0
  86. package/src/theme/themes/nightowl.json +221 -0
  87. package/src/theme/themes/nord.json +223 -0
  88. package/src/theme/themes/one-dark.json +84 -0
  89. package/src/theme/themes/opencode.json +245 -0
  90. package/src/theme/themes/orng.json +249 -0
  91. package/src/theme/themes/osaka-jade.json +93 -0
  92. package/src/theme/themes/palenight.json +222 -0
  93. package/src/theme/themes/rosepine.json +234 -0
  94. package/src/theme/themes/solarized.json +223 -0
  95. package/src/theme/themes/synthwave84.json +226 -0
  96. package/src/theme/themes/tokyonight.json +243 -0
  97. package/src/theme/themes/vercel.json +245 -0
  98. package/src/theme/themes/vesper.json +218 -0
  99. package/src/theme/themes/zenburn.json +223 -0
  100. package/src/theme/types.ts +225 -0
  101. package/src/types/sql.d.ts +4 -0
  102. package/src/utils/ansi-parser.ts +225 -0
  103. package/src/utils/format.ts +46 -0
  104. package/src/utils/id.ts +15 -0
  105. package/src/utils/logger.ts +112 -0
  106. package/src/utils/prerequisites.ts +118 -0
  107. package/src/utils/shell.ts +9 -0
  108. package/src/wizard/flows.ts +419 -0
  109. package/src/wizard/index.ts +37 -0
  110. package/src/wizard/prompts.ts +190 -0
  111. package/src/workspace/detect.test.ts +51 -0
  112. package/src/workspace/detect.ts +223 -0
  113. package/src/workspace/index.ts +71 -0
  114. package/src/workspace/init.ts +131 -0
  115. package/src/workspace/paths.ts +143 -0
  116. package/src/workspace/project.ts +164 -0
  117. package/tsconfig.json +22 -0
@@ -0,0 +1,223 @@
1
+ {
2
+ "$schema": "https://opencode.ai/theme.json",
3
+ "defs": {
4
+ "bg": "#3f3f3f",
5
+ "bgAlt": "#4f4f4f",
6
+ "bgPanel": "#5f5f5f",
7
+ "fg": "#dcdccc",
8
+ "fgMuted": "#9f9f9f",
9
+ "red": "#cc9393",
10
+ "redBright": "#dca3a3",
11
+ "green": "#7f9f7f",
12
+ "greenBright": "#8fb28f",
13
+ "yellow": "#f0dfaf",
14
+ "yellowDim": "#e0cf9f",
15
+ "blue": "#8cd0d3",
16
+ "blueDim": "#7cb8bb",
17
+ "magenta": "#dc8cc3",
18
+ "cyan": "#93e0e3",
19
+ "orange": "#dfaf8f"
20
+ },
21
+ "theme": {
22
+ "primary": {
23
+ "dark": "blue",
24
+ "light": "#5f7f8f"
25
+ },
26
+ "secondary": {
27
+ "dark": "magenta",
28
+ "light": "#8f5f8f"
29
+ },
30
+ "accent": {
31
+ "dark": "cyan",
32
+ "light": "#5f8f8f"
33
+ },
34
+ "error": {
35
+ "dark": "red",
36
+ "light": "#8f5f5f"
37
+ },
38
+ "warning": {
39
+ "dark": "yellow",
40
+ "light": "#8f8f5f"
41
+ },
42
+ "success": {
43
+ "dark": "green",
44
+ "light": "#5f8f5f"
45
+ },
46
+ "info": {
47
+ "dark": "orange",
48
+ "light": "#8f7f5f"
49
+ },
50
+ "text": {
51
+ "dark": "fg",
52
+ "light": "#3f3f3f"
53
+ },
54
+ "textMuted": {
55
+ "dark": "fgMuted",
56
+ "light": "#6f6f6f"
57
+ },
58
+ "background": {
59
+ "dark": "bg",
60
+ "light": "#ffffef"
61
+ },
62
+ "backgroundPanel": {
63
+ "dark": "bgAlt",
64
+ "light": "#f5f5e5"
65
+ },
66
+ "backgroundElement": {
67
+ "dark": "bgPanel",
68
+ "light": "#ebebdb"
69
+ },
70
+ "border": {
71
+ "dark": "#5f5f5f",
72
+ "light": "#d0d0c0"
73
+ },
74
+ "borderActive": {
75
+ "dark": "blue",
76
+ "light": "#5f7f8f"
77
+ },
78
+ "borderSubtle": {
79
+ "dark": "#4f4f4f",
80
+ "light": "#e0e0d0"
81
+ },
82
+ "diffAdded": {
83
+ "dark": "green",
84
+ "light": "#5f8f5f"
85
+ },
86
+ "diffRemoved": {
87
+ "dark": "red",
88
+ "light": "#8f5f5f"
89
+ },
90
+ "diffContext": {
91
+ "dark": "fgMuted",
92
+ "light": "#6f6f6f"
93
+ },
94
+ "diffHunkHeader": {
95
+ "dark": "cyan",
96
+ "light": "#5f8f8f"
97
+ },
98
+ "diffHighlightAdded": {
99
+ "dark": "greenBright",
100
+ "light": "#5f8f5f"
101
+ },
102
+ "diffHighlightRemoved": {
103
+ "dark": "redBright",
104
+ "light": "#8f5f5f"
105
+ },
106
+ "diffAddedBg": {
107
+ "dark": "#4f5f4f",
108
+ "light": "#efffef"
109
+ },
110
+ "diffRemovedBg": {
111
+ "dark": "#5f4f4f",
112
+ "light": "#ffefef"
113
+ },
114
+ "diffContextBg": {
115
+ "dark": "bgAlt",
116
+ "light": "#f5f5e5"
117
+ },
118
+ "diffLineNumber": {
119
+ "dark": "#6f6f6f",
120
+ "light": "#b0b0a0"
121
+ },
122
+ "diffAddedLineNumberBg": {
123
+ "dark": "#4f5f4f",
124
+ "light": "#efffef"
125
+ },
126
+ "diffRemovedLineNumberBg": {
127
+ "dark": "#5f4f4f",
128
+ "light": "#ffefef"
129
+ },
130
+ "markdownText": {
131
+ "dark": "fg",
132
+ "light": "#3f3f3f"
133
+ },
134
+ "markdownHeading": {
135
+ "dark": "yellow",
136
+ "light": "#8f8f5f"
137
+ },
138
+ "markdownLink": {
139
+ "dark": "blue",
140
+ "light": "#5f7f8f"
141
+ },
142
+ "markdownLinkText": {
143
+ "dark": "cyan",
144
+ "light": "#5f8f8f"
145
+ },
146
+ "markdownCode": {
147
+ "dark": "green",
148
+ "light": "#5f8f5f"
149
+ },
150
+ "markdownBlockQuote": {
151
+ "dark": "fgMuted",
152
+ "light": "#6f6f6f"
153
+ },
154
+ "markdownEmph": {
155
+ "dark": "yellowDim",
156
+ "light": "#8f8f5f"
157
+ },
158
+ "markdownStrong": {
159
+ "dark": "orange",
160
+ "light": "#8f7f5f"
161
+ },
162
+ "markdownHorizontalRule": {
163
+ "dark": "fgMuted",
164
+ "light": "#6f6f6f"
165
+ },
166
+ "markdownListItem": {
167
+ "dark": "blue",
168
+ "light": "#5f7f8f"
169
+ },
170
+ "markdownListEnumeration": {
171
+ "dark": "cyan",
172
+ "light": "#5f8f8f"
173
+ },
174
+ "markdownImage": {
175
+ "dark": "blue",
176
+ "light": "#5f7f8f"
177
+ },
178
+ "markdownImageText": {
179
+ "dark": "cyan",
180
+ "light": "#5f8f8f"
181
+ },
182
+ "markdownCodeBlock": {
183
+ "dark": "fg",
184
+ "light": "#3f3f3f"
185
+ },
186
+ "syntaxComment": {
187
+ "dark": "#7f9f7f",
188
+ "light": "#5f7f5f"
189
+ },
190
+ "syntaxKeyword": {
191
+ "dark": "yellow",
192
+ "light": "#8f8f5f"
193
+ },
194
+ "syntaxFunction": {
195
+ "dark": "blue",
196
+ "light": "#5f7f8f"
197
+ },
198
+ "syntaxVariable": {
199
+ "dark": "fg",
200
+ "light": "#3f3f3f"
201
+ },
202
+ "syntaxString": {
203
+ "dark": "red",
204
+ "light": "#8f5f5f"
205
+ },
206
+ "syntaxNumber": {
207
+ "dark": "greenBright",
208
+ "light": "#5f8f5f"
209
+ },
210
+ "syntaxType": {
211
+ "dark": "cyan",
212
+ "light": "#5f8f8f"
213
+ },
214
+ "syntaxOperator": {
215
+ "dark": "yellow",
216
+ "light": "#8f8f5f"
217
+ },
218
+ "syntaxPunctuation": {
219
+ "dark": "fg",
220
+ "light": "#3f3f3f"
221
+ }
222
+ }
223
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Theme type definitions for OpenSWE
3
+ *
4
+ * Matches opencode's JSON theme schema for full compatibility
5
+ */
6
+
7
+ // ============================================================================
8
+ // Color Types
9
+ // ============================================================================
10
+
11
+ /** Hex color string */
12
+ export type HexColor = `#${string}`
13
+
14
+ /** Reference to a color defined in defs */
15
+ export type RefName = string
16
+
17
+ /** Dark/light mode variants */
18
+ export interface ColorVariant {
19
+ dark: HexColor | RefName
20
+ light: HexColor | RefName
21
+ }
22
+
23
+ /** A color value can be a hex, a reference, or a variant object */
24
+ export type ColorValue = HexColor | RefName | ColorVariant
25
+
26
+ // ============================================================================
27
+ // Theme JSON Schema
28
+ // ============================================================================
29
+
30
+ /** Core theme colors (required) */
31
+ export interface ThemeColors {
32
+ primary: ColorValue
33
+ secondary: ColorValue
34
+ accent: ColorValue
35
+ error: ColorValue
36
+ warning: ColorValue
37
+ success: ColorValue
38
+ info: ColorValue
39
+ text: ColorValue
40
+ textMuted: ColorValue
41
+ background: ColorValue
42
+ backgroundPanel: ColorValue
43
+ backgroundElement: ColorValue
44
+ border: ColorValue
45
+ borderActive: ColorValue
46
+ borderSubtle: ColorValue
47
+ }
48
+
49
+ /** Extended theme colors for diff views and syntax highlighting */
50
+ export interface ThemeColorsExtended extends ThemeColors {
51
+ // Diff colors
52
+ diffAdded?: ColorValue
53
+ diffRemoved?: ColorValue
54
+ diffContext?: ColorValue
55
+ diffHunkHeader?: ColorValue
56
+ diffHighlightAdded?: ColorValue
57
+ diffHighlightRemoved?: ColorValue
58
+ diffAddedBg?: ColorValue
59
+ diffRemovedBg?: ColorValue
60
+ diffContextBg?: ColorValue
61
+ diffLineNumber?: ColorValue
62
+ diffAddedLineNumberBg?: ColorValue
63
+ diffRemovedLineNumberBg?: ColorValue
64
+
65
+ // Markdown colors
66
+ markdownText?: ColorValue
67
+ markdownHeading?: ColorValue
68
+ markdownLink?: ColorValue
69
+ markdownLinkText?: ColorValue
70
+ markdownCode?: ColorValue
71
+ markdownBlockQuote?: ColorValue
72
+ markdownEmph?: ColorValue
73
+ markdownStrong?: ColorValue
74
+ markdownHorizontalRule?: ColorValue
75
+ markdownListItem?: ColorValue
76
+ markdownListEnumeration?: ColorValue
77
+ markdownImage?: ColorValue
78
+ markdownImageText?: ColorValue
79
+ markdownCodeBlock?: ColorValue
80
+
81
+ // Syntax highlighting colors
82
+ syntaxComment?: ColorValue
83
+ syntaxKeyword?: ColorValue
84
+ syntaxFunction?: ColorValue
85
+ syntaxVariable?: ColorValue
86
+ syntaxString?: ColorValue
87
+ syntaxNumber?: ColorValue
88
+ syntaxType?: ColorValue
89
+ syntaxOperator?: ColorValue
90
+ syntaxPunctuation?: ColorValue
91
+ }
92
+
93
+ /** JSON theme file structure */
94
+ export interface ThemeJson {
95
+ $schema?: string
96
+ defs?: Record<string, HexColor | RefName>
97
+ theme: ThemeColorsExtended
98
+ }
99
+
100
+ // ============================================================================
101
+ // Resolved Theme
102
+ // ============================================================================
103
+
104
+ /** Core resolved theme colors (all hex strings) */
105
+ export interface ResolvedTheme {
106
+ // Core colors
107
+ primary: string
108
+ secondary: string
109
+ accent: string
110
+ error: string
111
+ warning: string
112
+ success: string
113
+ info: string
114
+ text: string
115
+ textMuted: string
116
+ background: string
117
+ backgroundPanel: string
118
+ backgroundElement: string
119
+ border: string
120
+ borderActive: string
121
+ borderSubtle: string
122
+
123
+ // Diff colors (with defaults)
124
+ diffAdded: string
125
+ diffRemoved: string
126
+ diffContext: string
127
+ diffHunkHeader: string
128
+ diffHighlightAdded: string
129
+ diffHighlightRemoved: string
130
+ diffAddedBg: string
131
+ diffRemovedBg: string
132
+ diffContextBg: string
133
+ diffLineNumber: string
134
+ diffAddedLineNumberBg: string
135
+ diffRemovedLineNumberBg: string
136
+
137
+ // Markdown colors (with defaults)
138
+ markdownText: string
139
+ markdownHeading: string
140
+ markdownLink: string
141
+ markdownLinkText: string
142
+ markdownCode: string
143
+ markdownBlockQuote: string
144
+ markdownEmph: string
145
+ markdownStrong: string
146
+ markdownHorizontalRule: string
147
+ markdownListItem: string
148
+ markdownListEnumeration: string
149
+ markdownImage: string
150
+ markdownImageText: string
151
+ markdownCodeBlock: string
152
+
153
+ // Syntax highlighting (with defaults)
154
+ syntaxComment: string
155
+ syntaxKeyword: string
156
+ syntaxFunction: string
157
+ syntaxVariable: string
158
+ syntaxString: string
159
+ syntaxNumber: string
160
+ syntaxType: string
161
+ syntaxOperator: string
162
+ syntaxPunctuation: string
163
+ }
164
+
165
+ /** Theme mode */
166
+ export type ThemeMode = "dark" | "light"
167
+
168
+ /** Theme name from bundled themes */
169
+ export type BundledThemeName =
170
+ | "aura"
171
+ | "ayu"
172
+ | "carbonfox"
173
+ | "catppuccin"
174
+ | "catppuccin-frappe"
175
+ | "catppuccin-macchiato"
176
+ | "cobalt2"
177
+ | "cursor"
178
+ | "dracula"
179
+ | "everforest"
180
+ | "flexoki"
181
+ | "github"
182
+ | "gruvbox"
183
+ | "kanagawa"
184
+ | "lucent-orng"
185
+ | "material"
186
+ | "matrix"
187
+ | "mercury"
188
+ | "monokai"
189
+ | "nightowl"
190
+ | "nord"
191
+ | "one-dark"
192
+ | "opencode"
193
+ | "orng"
194
+ | "osaka-jade"
195
+ | "palenight"
196
+ | "rosepine"
197
+ | "solarized"
198
+ | "synthwave84"
199
+ | "tokyonight"
200
+ | "vercel"
201
+ | "vesper"
202
+ | "zenburn"
203
+
204
+ // ============================================================================
205
+ // Type Guards
206
+ // ============================================================================
207
+
208
+ /**
209
+ * Check if a value is a hex color string
210
+ */
211
+ export function isHexColor(val: unknown): val is HexColor {
212
+ return typeof val === "string" && val.startsWith("#")
213
+ }
214
+
215
+ /**
216
+ * Check if a value is a color variant object
217
+ */
218
+ export function isColorVariant(val: unknown): val is ColorVariant {
219
+ return (
220
+ typeof val === "object" &&
221
+ val !== null &&
222
+ "dark" in val &&
223
+ "light" in val
224
+ )
225
+ }
@@ -0,0 +1,4 @@
1
+ declare module "*.sql" {
2
+ const content: string
3
+ export default content
4
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * ANSI SGR sequence parser
3
+ *
4
+ * Parses ANSI escape sequences and returns styled text segments
5
+ * for rendering in the Preview component.
6
+ *
7
+ * Handles SGR (Select Graphic Rendition) sequences for colors/styles
8
+ * and strips other control sequences (cursor movement, screen clearing, etc.)
9
+ */
10
+
11
+ /**
12
+ * Strip non-SGR ANSI sequences from text
13
+ *
14
+ * Removes cursor movement, screen clearing, and other control sequences
15
+ * while preserving SGR color/style sequences (those ending with 'm')
16
+ */
17
+ function stripNonSGRSequences(text: string): string {
18
+ // Match CSI sequences that are NOT SGR (don't end with 'm')
19
+ // CSI format: ESC [ <params> <final byte>
20
+ // SGR ends with 'm', others end with A-L, P-Z, @, etc.
21
+ // Also strips OSC sequences (ESC ] ... BEL/ST)
22
+ return text
23
+ // Remove CSI sequences that don't end with 'm' (cursor movement, clearing, etc.)
24
+ .replace(/\x1B\[[0-9;]*[A-LN-Za-ln-z@`{}|~]/g, "")
25
+ // Remove OSC sequences (ESC ] ... BEL or ESC ] ... ESC \)
26
+ .replace(/\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)?/g, "")
27
+ // Remove other escape sequences (ESC followed by single char)
28
+ .replace(/\x1B[^[\]]/g, "")
29
+ }
30
+
31
+ export interface AnsiSegment {
32
+ text: string
33
+ fg?: string // Hex color
34
+ bg?: string // Hex color
35
+ bold?: boolean
36
+ }
37
+
38
+ // Standard 16-color palette (colors 0-15)
39
+ const STANDARD_COLORS: Record<number, string> = {
40
+ 0: "#000000", // Black
41
+ 1: "#cd0000", // Red
42
+ 2: "#00cd00", // Green
43
+ 3: "#cdcd00", // Yellow
44
+ 4: "#0000ee", // Blue
45
+ 5: "#cd00cd", // Magenta
46
+ 6: "#00cdcd", // Cyan
47
+ 7: "#e5e5e5", // White
48
+ 8: "#7f7f7f", // Bright Black (Gray)
49
+ 9: "#ff0000", // Bright Red
50
+ 10: "#00ff00", // Bright Green
51
+ 11: "#ffff00", // Bright Yellow
52
+ 12: "#5c5cff", // Bright Blue
53
+ 13: "#ff00ff", // Bright Magenta
54
+ 14: "#00ffff", // Bright Cyan
55
+ 15: "#ffffff", // Bright White
56
+ }
57
+
58
+ /**
59
+ * Convert 256-color index to hex color
60
+ */
61
+ function color256ToHex(n: number): string {
62
+ // Standard colors (0-15)
63
+ if (n < 16) {
64
+ return STANDARD_COLORS[n] ?? "#ffffff"
65
+ }
66
+
67
+ // Color cube (16-231): 6x6x6 cube
68
+ if (n < 232) {
69
+ const idx = n - 16
70
+ const r = Math.floor(idx / 36)
71
+ const g = Math.floor((idx % 36) / 6)
72
+ const b = idx % 6
73
+
74
+ const toHex = (v: number) => (v === 0 ? 0 : 55 + v * 40).toString(16).padStart(2, "0")
75
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`
76
+ }
77
+
78
+ // Grayscale (232-255): 24 shades
79
+ const gray = (n - 232) * 10 + 8
80
+ const hex = gray.toString(16).padStart(2, "0")
81
+ return `#${hex}${hex}${hex}`
82
+ }
83
+
84
+ /**
85
+ * Convert RGB values to hex color
86
+ */
87
+ function rgbToHex(r: number, g: number, b: number): string {
88
+ const toHex = (v: number) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")
89
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`
90
+ }
91
+
92
+ interface AnsiState {
93
+ fg?: string
94
+ bg?: string
95
+ bold: boolean
96
+ }
97
+
98
+ /**
99
+ * Parse SGR (Select Graphic Rendition) parameters and update state
100
+ */
101
+ function parseSGR(params: number[], state: AnsiState): void {
102
+ let i = 0
103
+ while (i < params.length) {
104
+ const code = params[i] ?? 0
105
+
106
+ if (code === 0) {
107
+ // Reset all
108
+ state.fg = undefined
109
+ state.bg = undefined
110
+ state.bold = false
111
+ } else if (code === 1) {
112
+ // Bold on
113
+ state.bold = true
114
+ } else if (code === 22) {
115
+ // Bold off
116
+ state.bold = false
117
+ } else if (code >= 30 && code <= 37) {
118
+ // Standard foreground colors
119
+ state.fg = STANDARD_COLORS[code - 30]
120
+ } else if (code === 39) {
121
+ // Default foreground
122
+ state.fg = undefined
123
+ } else if (code >= 40 && code <= 47) {
124
+ // Standard background colors
125
+ state.bg = STANDARD_COLORS[code - 40]
126
+ } else if (code === 49) {
127
+ // Default background
128
+ state.bg = undefined
129
+ } else if (code >= 90 && code <= 97) {
130
+ // Bright foreground colors
131
+ state.fg = STANDARD_COLORS[code - 90 + 8]
132
+ } else if (code >= 100 && code <= 107) {
133
+ // Bright background colors
134
+ state.bg = STANDARD_COLORS[code - 100 + 8]
135
+ } else if (code === 38) {
136
+ // Extended foreground color
137
+ const nextParam = params[i + 1]
138
+ const colorIndex = params[i + 2]
139
+ if (nextParam === 5 && colorIndex !== undefined) {
140
+ // 256-color mode: 38;5;N
141
+ state.fg = color256ToHex(colorIndex)
142
+ i += 2
143
+ } else if (nextParam === 2 && params[i + 4] !== undefined) {
144
+ // True color mode: 38;2;R;G;B
145
+ state.fg = rgbToHex(params[i + 2] ?? 0, params[i + 3] ?? 0, params[i + 4] ?? 0)
146
+ i += 4
147
+ }
148
+ } else if (code === 48) {
149
+ // Extended background color
150
+ const nextParam = params[i + 1]
151
+ const colorIndex = params[i + 2]
152
+ if (nextParam === 5 && colorIndex !== undefined) {
153
+ // 256-color mode: 48;5;N
154
+ state.bg = color256ToHex(colorIndex)
155
+ i += 2
156
+ } else if (nextParam === 2 && params[i + 4] !== undefined) {
157
+ // True color mode: 48;2;R;G;B
158
+ state.bg = rgbToHex(params[i + 2] ?? 0, params[i + 3] ?? 0, params[i + 4] ?? 0)
159
+ i += 4
160
+ }
161
+ }
162
+ // Ignore unhandled codes
163
+
164
+ i++
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Parse a line containing ANSI escape sequences into styled segments
170
+ */
171
+ export function parseAnsiLine(line: string): AnsiSegment[] {
172
+ const segments: AnsiSegment[] = []
173
+ const state: AnsiState = { bold: false }
174
+
175
+ // First strip non-SGR sequences (cursor movement, etc.) that would cause garbling
176
+ const cleanedLine = stripNonSGRSequences(line)
177
+
178
+ // Match ANSI SGR sequences: ESC [ params m
179
+ const regex = /\x1B\[([0-9;]*)m/g
180
+ let lastIndex = 0
181
+ let match: RegExpExecArray | null
182
+
183
+ while ((match = regex.exec(cleanedLine)) !== null) {
184
+ // Add text before this escape sequence
185
+ if (match.index > lastIndex) {
186
+ const text = cleanedLine.slice(lastIndex, match.index)
187
+ if (text) {
188
+ segments.push({
189
+ text,
190
+ fg: state.fg,
191
+ bg: state.bg,
192
+ bold: state.bold || undefined,
193
+ })
194
+ }
195
+ }
196
+
197
+ // Parse SGR parameters
198
+ const paramStr = match[1] ?? ""
199
+ const params = paramStr === "" ? [0] : paramStr.split(";").map((s) => parseInt(s, 10) || 0)
200
+ parseSGR(params, state)
201
+
202
+ lastIndex = regex.lastIndex
203
+ }
204
+
205
+ // Add remaining text after last escape sequence
206
+ if (lastIndex < cleanedLine.length) {
207
+ const text = cleanedLine.slice(lastIndex)
208
+ if (text) {
209
+ segments.push({
210
+ text,
211
+ fg: state.fg,
212
+ bg: state.bg,
213
+ bold: state.bold || undefined,
214
+ })
215
+ }
216
+ }
217
+
218
+ // If no segments, return single segment with cleaned text
219
+ if (segments.length === 0) {
220
+ const stripped = cleanedLine.replace(/\x1B\[[0-9;]*m/g, "")
221
+ return [{ text: stripped || " " }]
222
+ }
223
+
224
+ return segments
225
+ }