@caretive/caret-cli 0.0.1

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 (85) hide show
  1. package/.npmrc.tmp +2 -0
  2. package/README.md +72 -0
  3. package/cmd/cline/main.go +348 -0
  4. package/cmd/cline-host/main.go +71 -0
  5. package/e2e/default_update_test.go +154 -0
  6. package/e2e/helpers_test.go +378 -0
  7. package/e2e/main_test.go +47 -0
  8. package/e2e/mixed_stress_test.go +120 -0
  9. package/e2e/sqlite_helper.go +161 -0
  10. package/e2e/start_list_test.go +178 -0
  11. package/go.mod +64 -0
  12. package/go.sum +162 -0
  13. package/man/cline.1 +331 -0
  14. package/man/cline.1.md +332 -0
  15. package/package.json +54 -0
  16. package/pkg/cli/auth/auth_cline_provider.go +285 -0
  17. package/pkg/cli/auth/auth_menu.go +323 -0
  18. package/pkg/cli/auth/auth_subscription.go +130 -0
  19. package/pkg/cli/auth/byo_quick_setup.go +247 -0
  20. package/pkg/cli/auth/models_cline.go +141 -0
  21. package/pkg/cli/auth/models_list_fetch.go +156 -0
  22. package/pkg/cli/auth/models_list_static.go +69 -0
  23. package/pkg/cli/auth/providers_byo.go +184 -0
  24. package/pkg/cli/auth/providers_list.go +517 -0
  25. package/pkg/cli/auth/update_api_configurations.go +647 -0
  26. package/pkg/cli/auth/wizard_byo.go +764 -0
  27. package/pkg/cli/auth/wizard_byo_bedrock.go +193 -0
  28. package/pkg/cli/auth/wizard_byo_oca.go +366 -0
  29. package/pkg/cli/auth.go +43 -0
  30. package/pkg/cli/clerror/cline_error.go +187 -0
  31. package/pkg/cli/config/manager.go +208 -0
  32. package/pkg/cli/config/settings_renderer.go +198 -0
  33. package/pkg/cli/config.go +152 -0
  34. package/pkg/cli/display/ansi.go +27 -0
  35. package/pkg/cli/display/banner.go +211 -0
  36. package/pkg/cli/display/deduplicator.go +95 -0
  37. package/pkg/cli/display/markdown_renderer.go +139 -0
  38. package/pkg/cli/display/renderer.go +304 -0
  39. package/pkg/cli/display/segment_streamer.go +212 -0
  40. package/pkg/cli/display/streaming.go +134 -0
  41. package/pkg/cli/display/system_renderer.go +269 -0
  42. package/pkg/cli/display/tool_renderer.go +455 -0
  43. package/pkg/cli/display/tool_result_parser.go +371 -0
  44. package/pkg/cli/display/typewriter.go +210 -0
  45. package/pkg/cli/doctor.go +65 -0
  46. package/pkg/cli/global/cline-clients.go +501 -0
  47. package/pkg/cli/global/global.go +113 -0
  48. package/pkg/cli/global/registry.go +304 -0
  49. package/pkg/cli/handlers/ask_handlers.go +339 -0
  50. package/pkg/cli/handlers/handler.go +130 -0
  51. package/pkg/cli/handlers/say_handlers.go +521 -0
  52. package/pkg/cli/instances.go +506 -0
  53. package/pkg/cli/logs.go +382 -0
  54. package/pkg/cli/output/coordinator.go +167 -0
  55. package/pkg/cli/output/input_model.go +497 -0
  56. package/pkg/cli/sqlite/locks.go +366 -0
  57. package/pkg/cli/task/history_handler.go +72 -0
  58. package/pkg/cli/task/input_handler.go +577 -0
  59. package/pkg/cli/task/manager.go +1283 -0
  60. package/pkg/cli/task/settings_parser.go +754 -0
  61. package/pkg/cli/task/stream_coordinator.go +60 -0
  62. package/pkg/cli/task.go +675 -0
  63. package/pkg/cli/terminal/keyboard.go +695 -0
  64. package/pkg/cli/tui/HELP_WANTED.md +1 -0
  65. package/pkg/cli/types/history.go +17 -0
  66. package/pkg/cli/types/messages.go +329 -0
  67. package/pkg/cli/types/state.go +59 -0
  68. package/pkg/cli/updater/updater.go +409 -0
  69. package/pkg/cli/version.go +43 -0
  70. package/pkg/common/constants.go +6 -0
  71. package/pkg/common/schema.go +54 -0
  72. package/pkg/common/types.go +54 -0
  73. package/pkg/common/utils.go +185 -0
  74. package/pkg/generated/field_overrides.go +39 -0
  75. package/pkg/generated/providers.go +1584 -0
  76. package/pkg/hostbridge/diff.go +351 -0
  77. package/pkg/hostbridge/disabled/watch.go +39 -0
  78. package/pkg/hostbridge/disabled/window.go +63 -0
  79. package/pkg/hostbridge/disabled/workspace.go +66 -0
  80. package/pkg/hostbridge/env.go +166 -0
  81. package/pkg/hostbridge/grpc_server.go +113 -0
  82. package/pkg/hostbridge/simple.go +43 -0
  83. package/pkg/hostbridge/simple_workspace.go +85 -0
  84. package/pkg/hostbridge/window.go +129 -0
  85. package/scripts/publish-caret-cli.sh +39 -0
@@ -0,0 +1,695 @@
1
+ package terminal
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "os"
7
+ "path/filepath"
8
+ "runtime"
9
+ "strings"
10
+ "sync"
11
+
12
+ "github.com/cline/cli/pkg/cli/display"
13
+ "github.com/cline/cli/pkg/cli/global"
14
+ )
15
+
16
+ // KeyboardProtocol manages enhanced keyboard protocol support for detecting
17
+ // modified keys like shift+enter across all major terminals.
18
+ type KeyboardProtocol struct {
19
+ enabled bool
20
+ mu sync.Mutex
21
+ }
22
+
23
+ var globalProtocol = &KeyboardProtocol{}
24
+
25
+ // EnableEnhancedKeyboard enables enhanced keyboard protocols to support
26
+ // shift+enter and other modified keys across all major terminals:
27
+ // - VS Code integrated terminal
28
+ // - iTerm2
29
+ // - Terminal.app
30
+ // - Ghostty
31
+ // - Kitty
32
+ // - WezTerm
33
+ // - Alacritty
34
+ // - foot
35
+ // - xterm
36
+ //
37
+ // This function is safe to call multiple times and handles cleanup automatically.
38
+ // It enables both modifyOtherKeys (xterm protocol) and Kitty keyboard protocol
39
+ // for maximum compatibility.
40
+ func EnableEnhancedKeyboard() {
41
+ globalProtocol.mu.Lock()
42
+ defer globalProtocol.mu.Unlock()
43
+
44
+ if globalProtocol.enabled {
45
+ return // Already enabled
46
+ }
47
+
48
+ // Check if we're in a TTY (not piped/redirected)
49
+ if !isatty(os.Stdin.Fd()) {
50
+ return
51
+ }
52
+
53
+ // Enable modifyOtherKeys mode 2
54
+ // This tells xterm-compatible terminals (VS Code, iTerm2, Terminal.app, etc.)
55
+ // to send escape sequences for modified keys including shift+enter
56
+ // Format: CSI > 4 ; 2 m
57
+ // - Mode 2 enables for ALL keys including well-known ones
58
+ fmt.Print("\x1b[>4;2m")
59
+
60
+ // Also enable Kitty keyboard protocol for terminals that support it
61
+ // This is a more modern protocol supported by Kitty, Ghostty, WezTerm, foot, etc.
62
+ // Format: CSI = <flags> u where flags=1 means "disambiguate escape codes"
63
+ // This makes shift+enter distinguishable from plain enter
64
+ fmt.Print("\x1b[=1u")
65
+
66
+ globalProtocol.enabled = true
67
+ }
68
+
69
+ // DisableEnhancedKeyboard restores the terminal to its default keyboard mode.
70
+ // This should be called on program exit to be a good citizen.
71
+ func DisableEnhancedKeyboard() {
72
+ globalProtocol.mu.Lock()
73
+ defer globalProtocol.mu.Unlock()
74
+
75
+ if !globalProtocol.enabled {
76
+ return
77
+ }
78
+
79
+ // Disable modifyOtherKeys (restore to mode 0)
80
+ fmt.Print("\x1b[>4;0m")
81
+
82
+ // Disable Kitty keyboard protocol
83
+ fmt.Print("\x1b[<u")
84
+
85
+ globalProtocol.enabled = false
86
+ }
87
+
88
+ // isatty checks if a file descriptor is a terminal
89
+ func isatty(fd uintptr) bool {
90
+ // Use the standard library's terminal package
91
+ // This works across all platforms (Unix, Windows, etc.)
92
+ fileInfo, err := os.Stdin.Stat()
93
+ if err != nil {
94
+ return false
95
+ }
96
+ return (fileInfo.Mode() & os.ModeCharDevice) != 0
97
+ }
98
+
99
+ // SetupKeyboard detects the current terminal and configures keybindings if needed.
100
+ // Runs in background and doesn't block. Prints status when configs are modified.
101
+ func SetupKeyboard() {
102
+ go func() {
103
+ renderer := display.NewRenderer(global.Config.OutputFormat)
104
+ setupKeyboardInternal(renderer)
105
+ }()
106
+ }
107
+
108
+ // SetupKeyboardSync is the synchronous version used by doctor command.
109
+ // Blocks until complete and prints status for all terminals.
110
+ func SetupKeyboardSync() {
111
+ renderer := display.NewRenderer(global.Config.OutputFormat)
112
+ setupKeyboardInternal(renderer)
113
+ }
114
+
115
+ func setupKeyboardInternal(renderer *display.Renderer) {
116
+ terminalName := DetectTerminal()
117
+
118
+ switch terminalName {
119
+ case "vscode":
120
+ // VS Code and Cursor use the same TERM_PROGRAM value
121
+ modified, path := SetupVSCodeKeybindings()
122
+ if modified {
123
+ fmt.Printf("%s VS Code %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
124
+ fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
125
+ } else if path != "" {
126
+ fmt.Printf("%s\n", renderer.Dim("✓ VS Code shift+enter already configured"))
127
+ fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
128
+ }
129
+
130
+ modified, path = SetupCursorKeybindings()
131
+ if modified {
132
+ fmt.Printf("%s Cursor %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
133
+ fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
134
+ } else if path != "" {
135
+ fmt.Printf("%s\n", renderer.Dim("✓ Cursor shift+enter already configured"))
136
+ fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
137
+ }
138
+
139
+ case "ghostty":
140
+ modified, path := SetupGhosttyKeybindings()
141
+ if modified {
142
+ fmt.Printf("%s Ghostty %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
143
+ fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
144
+ fmt.Printf("%s\n", renderer.Dim(" Fully restart Ghostty (quit all windows) for changes to take effect"))
145
+ } else if path != "" {
146
+ fmt.Printf("%s\n", renderer.Dim("✓ Ghostty shift+enter already configured"))
147
+ fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
148
+ }
149
+
150
+ case "wezterm":
151
+ modified, path := SetupWezTermKeybindings()
152
+ if modified {
153
+ fmt.Printf("%s WezTerm %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
154
+ fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
155
+ } else if path != "" {
156
+ fmt.Printf("%s\n", renderer.Dim("✓ WezTerm shift+enter already configured"))
157
+ fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
158
+ }
159
+
160
+ case "alacritty":
161
+ modified, path := SetupAlacrittyKeybindings()
162
+ if modified {
163
+ fmt.Printf("%s Alacritty %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
164
+ fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
165
+ } else if path != "" {
166
+ fmt.Printf("%s\n", renderer.Dim("✓ Alacritty shift+enter already configured"))
167
+ fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
168
+ }
169
+
170
+ case "kitty":
171
+ modified, path := SetupKittyKeybindings()
172
+ if modified {
173
+ fmt.Printf("%s Kitty %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
174
+ fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
175
+ } else if path != "" {
176
+ fmt.Printf("%s\n", renderer.Dim("✓ Kitty shift+enter already configured"))
177
+ fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
178
+ }
179
+
180
+ case "iterm2":
181
+ fmt.Printf("%s\n", renderer.Dim("✓ iTerm2 shift+enter works by default (maps to alt+enter)"))
182
+
183
+ case "terminal.app":
184
+ fmt.Printf("%s\n", renderer.Dim("⚠ Terminal.app requires manual configuration"))
185
+ fmt.Printf("%s\n", renderer.Dim(" See: Terminal → Preferences → Profiles → Keyboard"))
186
+
187
+ case "unknown":
188
+ fmt.Printf("%s\n", renderer.Dim("ℹ Terminal not detected - use alt+enter or ctrl+j for newlines"))
189
+ }
190
+ }
191
+
192
+ // getVSCodeConfigPath returns the platform-specific path to VS Code's User directory
193
+ func getVSCodeConfigPath() (string, error) {
194
+ home, err := os.UserHomeDir()
195
+ if err != nil {
196
+ return "", err
197
+ }
198
+
199
+ switch runtime.GOOS {
200
+ case "darwin":
201
+ return filepath.Join(home, "Library", "Application Support", "Code", "User"), nil
202
+ case "windows":
203
+ appData := os.Getenv("APPDATA")
204
+ if appData == "" {
205
+ appData = filepath.Join(home, "AppData", "Roaming")
206
+ }
207
+ return filepath.Join(appData, "Code", "User"), nil
208
+ default: // linux, freebsd, etc.
209
+ return filepath.Join(home, ".config", "Code", "User"), nil
210
+ }
211
+ }
212
+
213
+ // getCursorConfigPath returns the platform-specific path to Cursor's User directory
214
+ func getCursorConfigPath() (string, error) {
215
+ home, err := os.UserHomeDir()
216
+ if err != nil {
217
+ return "", err
218
+ }
219
+
220
+ switch runtime.GOOS {
221
+ case "darwin":
222
+ return filepath.Join(home, "Library", "Application Support", "Cursor", "User"), nil
223
+ case "windows":
224
+ appData := os.Getenv("APPDATA")
225
+ if appData == "" {
226
+ appData = filepath.Join(home, "AppData", "Roaming")
227
+ }
228
+ return filepath.Join(appData, "Cursor", "User"), nil
229
+ default: // linux, freebsd, etc.
230
+ return filepath.Join(home, ".config", "Cursor", "User"), nil
231
+ }
232
+ }
233
+
234
+ // DetectTerminal identifies which terminal emulator is currently running
235
+ func DetectTerminal() string {
236
+ // Check TERM_PROGRAM (works for most terminals)
237
+ termProgram := os.Getenv("TERM_PROGRAM")
238
+ switch termProgram {
239
+ case "vscode":
240
+ return "vscode" // Also covers Cursor (uses same value)
241
+ case "WezTerm":
242
+ return "wezterm"
243
+ case "ghostty":
244
+ return "ghostty"
245
+ case "iTerm.app":
246
+ return "iterm2"
247
+ case "Apple_Terminal":
248
+ return "terminal.app"
249
+ }
250
+
251
+ // Kitty doesn't set TERM_PROGRAM, check KITTY_WINDOW_ID
252
+ if os.Getenv("KITTY_WINDOW_ID") != "" {
253
+ return "kitty"
254
+ }
255
+
256
+ // Alacritty doesn't set TERM_PROGRAM, check ALACRITTY_SOCKET
257
+ if os.Getenv("ALACRITTY_SOCKET") != "" {
258
+ return "alacritty"
259
+ }
260
+
261
+ // Ghostty fallback (cross-platform - more reliable than TERM_PROGRAM)
262
+ if os.Getenv("GHOSTTY_RESOURCES_DIR") != "" {
263
+ return "ghostty"
264
+ }
265
+
266
+ // Alacritty fallback
267
+ if os.Getenv("ALACRITTY_LOG") != "" {
268
+ return "alacritty"
269
+ }
270
+
271
+ // Check TERM variable as last resort
272
+ term := os.Getenv("TERM")
273
+ if strings.Contains(term, "kitty") {
274
+ return "kitty"
275
+ }
276
+ if term == "alacritty" {
277
+ return "alacritty"
278
+ }
279
+ if term == "xterm-ghostty" {
280
+ return "ghostty"
281
+ }
282
+
283
+ return "unknown"
284
+ }
285
+
286
+ // VSCodeKeybinding represents a VS Code keyboard shortcut
287
+ type VSCodeKeybinding struct {
288
+ Key string `json:"key"`
289
+ Command string `json:"command"`
290
+ Args map[string]interface{} `json:"args,omitempty"`
291
+ When string `json:"when,omitempty"`
292
+ }
293
+
294
+ // SetupVSCodeKeybindings adds shift+enter support to VS Code's integrated terminal
295
+ // by modifying the user's keybindings.json file.
296
+ // Returns (wasModified, configPath) to allow caller to log the change.
297
+ func SetupVSCodeKeybindings() (bool, string) {
298
+ // Get platform-specific VS Code config path
299
+ configDir, err := getVSCodeConfigPath()
300
+ if err != nil {
301
+ return false, ""
302
+ }
303
+
304
+ keybindingsPath := filepath.Join(configDir, "keybindings.json")
305
+
306
+ // Check if VS Code is installed (keybindings file or parent dir exists)
307
+ if _, err := os.Stat(filepath.Dir(keybindingsPath)); os.IsNotExist(err) {
308
+ // VS Code not installed, skip silently
309
+ return false, ""
310
+ }
311
+
312
+ // Read existing keybindings
313
+ var keybindings []VSCodeKeybinding
314
+
315
+ data, err := os.ReadFile(keybindingsPath)
316
+ if err != nil {
317
+ if !os.IsNotExist(err) {
318
+ return false, ""
319
+ }
320
+ // File doesn't exist, start with empty array
321
+ keybindings = []VSCodeKeybinding{}
322
+ } else {
323
+ // Parse existing keybindings
324
+ if err := json.Unmarshal(data, &keybindings); err != nil {
325
+ // If parse fails, don't modify the file
326
+ return false, ""
327
+ }
328
+ }
329
+
330
+ // Check if shift+enter binding already exists
331
+ for _, kb := range keybindings {
332
+ if kb.Key == "shift+enter" && kb.Command == "workbench.action.terminal.sendSequence" {
333
+ // Already configured
334
+ return false, keybindingsPath
335
+ }
336
+ }
337
+
338
+ // Add shift+enter keybinding
339
+ newBinding := VSCodeKeybinding{
340
+ Key: "shift+enter",
341
+ Command: "workbench.action.terminal.sendSequence",
342
+ Args: map[string]interface{}{
343
+ "text": "\u001b\n", // ESC + newline (alt+enter sequence)
344
+ },
345
+ When: "terminalFocus",
346
+ }
347
+
348
+ keybindings = append(keybindings, newBinding)
349
+
350
+ // Create backup
351
+ if data != nil {
352
+ backupPath := keybindingsPath + ".backup"
353
+ _ = os.WriteFile(backupPath, data, 0644)
354
+ }
355
+
356
+ // Write updated keybindings
357
+ updatedData, err := json.MarshalIndent(keybindings, "", " ")
358
+ if err != nil {
359
+ return false, ""
360
+ }
361
+
362
+ // Ensure parent directory exists
363
+ if err := os.MkdirAll(filepath.Dir(keybindingsPath), 0755); err != nil {
364
+ return false, ""
365
+ }
366
+
367
+ if err := os.WriteFile(keybindingsPath, updatedData, 0644); err != nil {
368
+ return false, ""
369
+ }
370
+
371
+ return true, keybindingsPath
372
+ }
373
+
374
+ // SetupCursorKeybindings adds shift+enter support to Cursor's integrated terminal
375
+ // by modifying the user's keybindings.json file.
376
+ // Cursor is a fork of VS Code, so it uses the same keybinding format.
377
+ // Returns (wasModified, configPath) to allow caller to log the change.
378
+ func SetupCursorKeybindings() (bool, string) {
379
+ // Get platform-specific Cursor config path
380
+ configDir, err := getCursorConfigPath()
381
+ if err != nil {
382
+ return false, ""
383
+ }
384
+
385
+ keybindingsPath := filepath.Join(configDir, "keybindings.json")
386
+
387
+ // Check if Cursor is installed (keybindings file or parent dir exists)
388
+ if _, err := os.Stat(filepath.Dir(keybindingsPath)); os.IsNotExist(err) {
389
+ // Cursor not installed, skip silently
390
+ return false, ""
391
+ }
392
+
393
+ // Read existing keybindings
394
+ var keybindings []VSCodeKeybinding
395
+
396
+ data, err := os.ReadFile(keybindingsPath)
397
+ if err != nil {
398
+ if !os.IsNotExist(err) {
399
+ return false, ""
400
+ }
401
+ // File doesn't exist, start with empty array
402
+ keybindings = []VSCodeKeybinding{}
403
+ } else {
404
+ // Parse existing keybindings
405
+ if err := json.Unmarshal(data, &keybindings); err != nil {
406
+ // If parse fails, don't modify the file
407
+ return false, ""
408
+ }
409
+ }
410
+
411
+ // Check if shift+enter binding already exists
412
+ for _, kb := range keybindings {
413
+ if kb.Key == "shift+enter" && kb.Command == "workbench.action.terminal.sendSequence" {
414
+ // Already configured
415
+ return false, keybindingsPath
416
+ }
417
+ }
418
+
419
+ // Add shift+enter keybinding
420
+ newBinding := VSCodeKeybinding{
421
+ Key: "shift+enter",
422
+ Command: "workbench.action.terminal.sendSequence",
423
+ Args: map[string]interface{}{
424
+ "text": "\u001b\n", // ESC + newline (alt+enter sequence)
425
+ },
426
+ When: "terminalFocus",
427
+ }
428
+
429
+ keybindings = append(keybindings, newBinding)
430
+
431
+ // Create backup
432
+ if data != nil {
433
+ backupPath := keybindingsPath + ".backup"
434
+ _ = os.WriteFile(backupPath, data, 0644)
435
+ }
436
+
437
+ // Write updated keybindings
438
+ updatedData, err := json.MarshalIndent(keybindings, "", " ")
439
+ if err != nil {
440
+ return false, ""
441
+ }
442
+
443
+ // Ensure parent directory exists
444
+ if err := os.MkdirAll(filepath.Dir(keybindingsPath), 0755); err != nil {
445
+ return false, ""
446
+ }
447
+
448
+ if err := os.WriteFile(keybindingsPath, updatedData, 0644); err != nil {
449
+ return false, ""
450
+ }
451
+
452
+ return true, keybindingsPath
453
+ }
454
+
455
+ // SetupGhosttyKeybindings adds shift+enter support to Ghostty terminal
456
+ // by appending to the user's config file.
457
+ // Returns (wasModified, configPath) to allow caller to log the change.
458
+ func SetupGhosttyKeybindings() (bool, string) {
459
+ home, err := os.UserHomeDir()
460
+ if err != nil {
461
+ return false, ""
462
+ }
463
+
464
+ // Ghostty config location: ~/.config/ghostty/config
465
+ configPath := filepath.Join(home, ".config", "ghostty", "config")
466
+
467
+ // Check if config directory exists
468
+ configDir := filepath.Dir(configPath)
469
+ if _, err := os.Stat(configDir); os.IsNotExist(err) {
470
+ // Ghostty not installed, skip silently
471
+ return false, ""
472
+ }
473
+
474
+ // Read existing config if it exists
475
+ var existingContent []byte
476
+ if data, err := os.ReadFile(configPath); err == nil {
477
+ existingContent = data
478
+ // Check if shift+enter already configured
479
+ if strings.Contains(string(data), "keybind = shift+enter") {
480
+ return false, configPath
481
+ }
482
+ }
483
+
484
+ // Keybinding to add - send newline character (0x0a)
485
+ // Ghostty requires \x0a hex escape syntax, verified working
486
+ keybinding := "keybind = shift+enter=text:\\x0a\n"
487
+
488
+ // Append to config
489
+ newContent := append(existingContent, []byte(keybinding)...)
490
+
491
+ // Ensure directory exists
492
+ if err := os.MkdirAll(configDir, 0755); err != nil {
493
+ return false, ""
494
+ }
495
+
496
+ // Create backup if file exists
497
+ if existingContent != nil {
498
+ backupPath := configPath + ".backup"
499
+ _ = os.WriteFile(backupPath, existingContent, 0644)
500
+ }
501
+
502
+ // Write updated config
503
+ if err := os.WriteFile(configPath, newContent, 0644); err != nil {
504
+ return false, ""
505
+ }
506
+
507
+ return true, configPath
508
+ }
509
+
510
+ // SetupWezTermKeybindings adds shift+enter support to WezTerm
511
+ // by appending to the user's .wezterm.lua file.
512
+ // Returns (wasModified, configPath)
513
+ func SetupWezTermKeybindings() (bool, string) {
514
+ home, err := os.UserHomeDir()
515
+ if err != nil {
516
+ return false, ""
517
+ }
518
+
519
+ configPath := filepath.Join(home, ".wezterm.lua")
520
+
521
+ // Check if WezTerm config exists
522
+ if _, err := os.Stat(configPath); os.IsNotExist(err) {
523
+ // WezTerm not configured, skip silently
524
+ return false, ""
525
+ }
526
+
527
+ // Read existing config
528
+ data, err := os.ReadFile(configPath)
529
+ if err != nil {
530
+ return false, ""
531
+ }
532
+
533
+ // Check if shift+enter already configured
534
+ if strings.Contains(string(data), "key = 'Enter'") && strings.Contains(string(data), "mods = 'SHIFT'") {
535
+ return false, configPath
536
+ }
537
+
538
+ // Create backup
539
+ backupPath := configPath + ".backup"
540
+ _ = os.WriteFile(backupPath, data, 0644)
541
+
542
+ // Keybinding to add (insert before final return statement)
543
+ keybinding := `
544
+ -- Shift+Enter for newlines (added by Cline CLI)
545
+ config.keys = config.keys or {}
546
+ table.insert(config.keys, {
547
+ key = 'Enter',
548
+ mods = 'SHIFT',
549
+ action = wezterm.action.SendString '\x1b\n',
550
+ })
551
+ `
552
+
553
+ content := string(data)
554
+ // Try to insert before the final return statement
555
+ if strings.Contains(content, "return config") {
556
+ content = strings.Replace(content, "return config", keybinding+"\nreturn config", 1)
557
+ } else {
558
+ // No return statement, append at end
559
+ content += keybinding
560
+ }
561
+
562
+ // Write updated config
563
+ if err := os.WriteFile(configPath, []byte(content), 0644); err != nil {
564
+ return false, ""
565
+ }
566
+
567
+ return true, configPath
568
+ }
569
+
570
+ // SetupAlacrittyKeybindings adds shift+enter support to Alacritty
571
+ // by appending to the user's alacritty.yml file.
572
+ // Returns (wasModified, configPath)
573
+ func SetupAlacrittyKeybindings() (bool, string) {
574
+ home, err := os.UserHomeDir()
575
+ if err != nil {
576
+ return false, ""
577
+ }
578
+
579
+ // Try both possible locations
580
+ configPaths := []string{
581
+ filepath.Join(home, ".config", "alacritty", "alacritty.yml"),
582
+ filepath.Join(home, ".config", "alacritty", "alacritty.toml"),
583
+ filepath.Join(home, ".alacritty.yml"),
584
+ }
585
+
586
+ var configPath string
587
+ for _, path := range configPaths {
588
+ if _, err := os.Stat(path); err == nil {
589
+ configPath = path
590
+ break
591
+ }
592
+ }
593
+
594
+ if configPath == "" {
595
+ // Alacritty not configured, skip silently
596
+ return false, ""
597
+ }
598
+
599
+ // Read existing config
600
+ data, err := os.ReadFile(configPath)
601
+ if err != nil {
602
+ return false, ""
603
+ }
604
+
605
+ // Check if shift+enter already configured
606
+ if strings.Contains(string(data), "key: Return") && strings.Contains(string(data), "mods: Shift") {
607
+ return false, configPath
608
+ }
609
+
610
+ // Create backup
611
+ backupPath := configPath + ".backup"
612
+ _ = os.WriteFile(backupPath, data, 0644)
613
+
614
+ // Keybinding to add
615
+ var keybinding string
616
+ if strings.HasSuffix(configPath, ".yml") || strings.HasSuffix(configPath, ".yaml") {
617
+ keybinding = `
618
+ # Shift+Enter for newlines (added by Cline CLI)
619
+ key_bindings:
620
+ - { key: Return, mods: Shift, chars: "\x1b\n" }
621
+ `
622
+ } else {
623
+ // TOML format
624
+ keybinding = `
625
+ # Shift+Enter for newlines (added by Cline CLI)
626
+ [[keyboard.bindings]]
627
+ key = "Return"
628
+ mods = "Shift"
629
+ chars = "\x1b\n"
630
+ `
631
+ }
632
+
633
+ // Append to config
634
+ newContent := append(data, []byte(keybinding)...)
635
+
636
+ // Write updated config
637
+ if err := os.WriteFile(configPath, newContent, 0644); err != nil {
638
+ return false, ""
639
+ }
640
+
641
+ return true, configPath
642
+ }
643
+
644
+ // SetupKittyKeybindings adds shift+enter support to Kitty terminal
645
+ // by appending to the user's kitty.conf file.
646
+ // Returns (wasModified, configPath)
647
+ func SetupKittyKeybindings() (bool, string) {
648
+ home, err := os.UserHomeDir()
649
+ if err != nil {
650
+ return false, ""
651
+ }
652
+
653
+ configPath := filepath.Join(home, ".config", "kitty", "kitty.conf")
654
+
655
+ // Check if config directory exists
656
+ configDir := filepath.Dir(configPath)
657
+ if _, err := os.Stat(configDir); os.IsNotExist(err) {
658
+ // Kitty not installed, skip silently
659
+ return false, ""
660
+ }
661
+
662
+ // Read existing config if it exists
663
+ var existingContent []byte
664
+ if data, err := os.ReadFile(configPath); err == nil {
665
+ existingContent = data
666
+ // Check if shift+enter already configured
667
+ if strings.Contains(string(data), "map shift+enter") {
668
+ return false, configPath
669
+ }
670
+ }
671
+
672
+ // Keybinding to add
673
+ keybinding := "# Shift+Enter for newlines (added by Cline CLI)\nmap shift+enter send_text all \\x1b\\n\n"
674
+
675
+ // Append to config
676
+ newContent := append(existingContent, []byte(keybinding)...)
677
+
678
+ // Ensure directory exists
679
+ if err := os.MkdirAll(configDir, 0755); err != nil {
680
+ return false, ""
681
+ }
682
+
683
+ // Create backup if file exists
684
+ if existingContent != nil {
685
+ backupPath := configPath + ".backup"
686
+ _ = os.WriteFile(backupPath, existingContent, 0644)
687
+ }
688
+
689
+ // Write updated config
690
+ if err := os.WriteFile(configPath, newContent, 0644); err != nil {
691
+ return false, ""
692
+ }
693
+
694
+ return true, configPath
695
+ }
@@ -0,0 +1 @@
1
+ if you can make a beautiful tui in go, please help!
@@ -0,0 +1,17 @@
1
+ package types
2
+
3
+ // HistoryItem represents a task history item from taskHistory.json
4
+ // This struct matches the JSON format stored on disk
5
+ type HistoryItem struct {
6
+ Id string `json:"id"`
7
+ Ulid string `json:"ulid,omitempty"`
8
+ Ts int64 `json:"ts"`
9
+ Task string `json:"task"`
10
+ TokensIn int32 `json:"tokensIn"`
11
+ TokensOut int32 `json:"tokensOut"`
12
+ CacheWrites int32 `json:"cacheWrites,omitempty"`
13
+ CacheReads int32 `json:"cacheReads,omitempty"`
14
+ TotalCost float64 `json:"totalCost"`
15
+ Size int64 `json:"size,omitempty"`
16
+ IsFavorited bool `json:"isFavorited,omitempty"`
17
+ }