@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,577 @@
1
+ package task
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "errors"
7
+ "fmt"
8
+ "strings"
9
+ "sync"
10
+ "time"
11
+
12
+ tea "github.com/charmbracelet/bubbletea"
13
+ "github.com/charmbracelet/lipgloss"
14
+ "github.com/cline/cli/pkg/cli/global"
15
+ "github.com/cline/cli/pkg/cli/output"
16
+ "github.com/cline/cli/pkg/cli/types"
17
+ )
18
+
19
+ // InputHandler manages interactive user input during follow mode
20
+ type InputHandler struct {
21
+ manager *Manager
22
+ coordinator *StreamCoordinator
23
+ cancelFunc context.CancelFunc
24
+ mu sync.RWMutex
25
+ isRunning bool
26
+ pollTicker *time.Ticker
27
+ program *tea.Program
28
+ programRunning bool
29
+ programDoneChan chan struct{} // Signals when program actually exits
30
+ resultChan chan output.InputSubmitMsg
31
+ cancelChan chan struct{}
32
+ feedbackApproval bool // Track if we're in feedback after approval
33
+ feedbackApproved bool // Track the approval decision
34
+ approvalMessage *types.ClineMessage // Store the approval message for determining action
35
+ ctx context.Context // Context for restart callback
36
+ }
37
+
38
+ // NewInputHandler creates a new input handler
39
+ func NewInputHandler(manager *Manager, coordinator *StreamCoordinator, cancelFunc context.CancelFunc) *InputHandler {
40
+ return &InputHandler{
41
+ manager: manager,
42
+ coordinator: coordinator,
43
+ cancelFunc: cancelFunc,
44
+ isRunning: false,
45
+ pollTicker: time.NewTicker(500 * time.Millisecond),
46
+ resultChan: make(chan output.InputSubmitMsg, 1),
47
+ cancelChan: make(chan struct{}, 1),
48
+ }
49
+ }
50
+
51
+ // Start begins monitoring for input opportunities
52
+ func (ih *InputHandler) Start(ctx context.Context, errChan chan error) {
53
+ ih.mu.Lock()
54
+ ih.isRunning = true
55
+ ih.mu.Unlock()
56
+
57
+ defer func() {
58
+ ih.mu.Lock()
59
+ ih.isRunning = false
60
+ ih.mu.Unlock()
61
+ ih.pollTicker.Stop()
62
+ if ih.program != nil {
63
+ ih.program.Quit()
64
+ }
65
+ }()
66
+
67
+ for {
68
+ select {
69
+ case <-ctx.Done():
70
+ return
71
+ case <-ih.pollTicker.C:
72
+ // First check if approval is needed
73
+ needsApproval, approvalMsg, err := ih.manager.CheckNeedsApproval(ctx)
74
+ if err != nil {
75
+ if global.Config.Verbose {
76
+ output.Printf("\nDebug: CheckNeedsApproval error: %v\n", err)
77
+ }
78
+ continue
79
+ }
80
+
81
+ if needsApproval {
82
+ ih.coordinator.SetInputAllowed(true)
83
+
84
+ // Show approval prompt
85
+ approved, feedback, err := ih.promptForApproval(ctx, approvalMsg)
86
+
87
+ if err != nil {
88
+ // Check if the error is due to interrupt (Ctrl+C) or context cancellation
89
+ if errors.Is(err, context.Canceled) || ctx.Err() != nil {
90
+ // User pressed Ctrl+C - cancel context to exit FollowConversation
91
+ ih.cancelFunc()
92
+ return
93
+ }
94
+ if global.Config.Verbose {
95
+ output.Printf("\nDebug: Approval prompt error: %v\n", err)
96
+ }
97
+ continue
98
+ }
99
+
100
+ ih.coordinator.SetInputAllowed(false)
101
+
102
+ // Send approval response
103
+ approveStr := "false"
104
+ if approved {
105
+ approveStr = "true"
106
+ }
107
+
108
+ if err := ih.manager.SendMessage(ctx, feedback, nil, nil, approveStr); err != nil {
109
+ output.Printf("\nError sending approval: %v\n", err)
110
+ continue
111
+ }
112
+
113
+ if global.Config.Verbose {
114
+ output.Printf("\nDebug: Approval sent (approved=%s, feedback=%q)\n", approveStr, feedback)
115
+ }
116
+
117
+ // Give the system a moment to process before re-polling
118
+ time.Sleep(1 * time.Second)
119
+ continue
120
+ }
121
+
122
+ // Check if we can send a regular message
123
+ err = ih.manager.CheckSendEnabled(ctx)
124
+ if err != nil {
125
+ // Handle specific error cases
126
+ if errors.Is(err, ErrNoActiveTask) {
127
+ // No active task - don't show input prompt
128
+ ih.coordinator.SetInputAllowed(false)
129
+ continue
130
+ }
131
+ if errors.Is(err, ErrTaskBusy) {
132
+ // Task is busy - don't show input prompt
133
+ ih.coordinator.SetInputAllowed(false)
134
+ continue
135
+ }
136
+ // Unexpected error
137
+ if global.Config.Verbose {
138
+ output.Printf("\nDebug: CheckSendEnabled error: %v\n", err)
139
+ }
140
+ continue
141
+ }
142
+
143
+ // If we reach here, we can send a message
144
+ ih.coordinator.SetInputAllowed(true)
145
+
146
+ // Show prompt and get input
147
+ message, shouldSend, err := ih.promptForInput(ctx)
148
+
149
+ if err != nil {
150
+ // Check if the error is due to interrupt (Ctrl+C) or context cancellation
151
+ if errors.Is(err, context.Canceled) || ctx.Err() != nil {
152
+ // User pressed Ctrl+C - cancel context to exit FollowConversation
153
+ ih.cancelFunc()
154
+ return
155
+ }
156
+ if global.Config.Verbose {
157
+ output.Printf("\nDebug: Input prompt error: %v\n", err)
158
+ }
159
+ continue
160
+ }
161
+
162
+ ih.coordinator.SetInputAllowed(false)
163
+
164
+ if shouldSend {
165
+ // Check for mode switch commands first
166
+ newMode, remainingMessage, isModeSwitch := ih.parseModeSwitch(message)
167
+ if isModeSwitch {
168
+ // Create styles for mode switch messages (respect global color profile)
169
+ actStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("39")).Bold(true)
170
+ planStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("3")).Bold(true)
171
+
172
+ if remainingMessage != "" {
173
+ // Switching with a message - behavior differs by mode
174
+ if newMode == "act" {
175
+ // Act mode: can send mode + message in one call
176
+ if err := ih.manager.SetMode(ctx, newMode, &remainingMessage, nil, nil); err != nil {
177
+ output.Printf("\nError switching to act mode with message: %v\n", err)
178
+ continue
179
+ }
180
+ output.Printf("\n%s\n", actStyle.Render("Switched to act mode"))
181
+ } else {
182
+ // Plan mode: must switch first, then send message separately
183
+ if err := ih.manager.SetMode(ctx, newMode, nil, nil, nil); err != nil {
184
+ output.Printf("\nError switching to plan mode: %v\n", err)
185
+ continue
186
+ }
187
+ output.Printf("\n%s\n", planStyle.Render("Switched to plan mode"))
188
+
189
+ // Now send the message separately
190
+ time.Sleep(500 * time.Millisecond) // Give mode switch time to process
191
+ if err := ih.manager.SendMessage(ctx, remainingMessage, nil, nil, ""); err != nil {
192
+ output.Printf("\nError sending message after mode switch: %v\n", err)
193
+ continue
194
+ }
195
+ }
196
+ } else {
197
+ // Just switch mode, no message
198
+ if err := ih.manager.SetMode(ctx, newMode, nil, nil, nil); err != nil {
199
+ output.Printf("\nError switching to %s mode: %v\n", newMode, err)
200
+ continue
201
+ }
202
+ // Color based on mode
203
+ if newMode == "act" {
204
+ output.Printf("\n%s\n", actStyle.Render("Switched to act mode"))
205
+ } else {
206
+ output.Printf("\n%s\n", planStyle.Render("Switched to plan mode"))
207
+ }
208
+ }
209
+
210
+ // Mode switch handled, continue to next poll
211
+ time.Sleep(1 * time.Second)
212
+ continue
213
+ }
214
+
215
+ // Handle special commands
216
+ if handled := ih.handleSpecialCommand(ctx, message); handled {
217
+ continue
218
+ }
219
+
220
+ // Send the message
221
+ if err := ih.manager.SendMessage(ctx, message, nil, nil, ""); err != nil {
222
+ output.Printf("\nError sending message: %v\n", err)
223
+ continue
224
+ }
225
+
226
+ if global.Config.Verbose {
227
+ output.Printf("\nDebug: Message sent successfully\n")
228
+ }
229
+
230
+ // Give the system a moment to process before re-polling
231
+ time.Sleep(1 * time.Second)
232
+ }
233
+ }
234
+ }
235
+ }
236
+
237
+ // determineAutoApprovalAction determines which auto-approval action to enable based on the ask type
238
+ func determineAutoApprovalAction(msg *types.ClineMessage) (string, error) {
239
+ switch types.AskType(msg.Ask) {
240
+ case types.AskTypeTool:
241
+ // Parse tool message to determine if it's a read or edit operation
242
+ var toolMsg types.ToolMessage
243
+ if err := json.Unmarshal([]byte(msg.Text), &toolMsg); err != nil {
244
+ return "", fmt.Errorf("failed to parse tool message: %w", err)
245
+ }
246
+
247
+ // Determine action based on tool type
248
+ switch types.ToolType(toolMsg.Tool) {
249
+ case types.ToolTypeReadFile,
250
+ types.ToolTypeListFilesTopLevel,
251
+ types.ToolTypeListFilesRecursive,
252
+ types.ToolTypeListCodeDefinitionNames,
253
+ types.ToolTypeSearchFiles,
254
+ types.ToolTypeWebFetch:
255
+ return "read_files", nil
256
+ case types.ToolTypeEditedExistingFile,
257
+ types.ToolTypeNewFileCreated:
258
+ return "edit_files", nil
259
+ case types.ToolTypeFileDeleted:
260
+ return "apply_patch", nil
261
+ default:
262
+ return "", fmt.Errorf("unsupported tool type: %s", toolMsg.Tool)
263
+ }
264
+
265
+ case types.AskTypeCommand:
266
+ return "execute_all_commands", nil
267
+
268
+ case types.AskTypeBrowserActionLaunch:
269
+ return "use_browser", nil
270
+
271
+ case types.AskTypeUseMcpServer:
272
+ return "use_mcp", nil
273
+
274
+ default:
275
+ return "", fmt.Errorf("unsupported ask type: %s", msg.Ask)
276
+ }
277
+ }
278
+
279
+ // promptForInput displays an interactive prompt and waits for user input
280
+ func (ih *InputHandler) promptForInput(ctx context.Context) (string, bool, error) {
281
+ currentMode := ih.manager.GetCurrentMode()
282
+
283
+ model := output.NewInputModel(
284
+ output.InputTypeMessage,
285
+ "Cline is ready for your message...",
286
+ "/plan or /act to switch modes\nctrl+e to open editor",
287
+ currentMode,
288
+ )
289
+
290
+ return ih.runInputProgram(ctx, model)
291
+ }
292
+
293
+ // promptForApproval displays an approval prompt for tool/command requests
294
+ func (ih *InputHandler) promptForApproval(ctx context.Context, msg *types.ClineMessage) (bool, string, error) {
295
+ // Store the approval message for later use in determining auto-approval action
296
+ ih.approvalMessage = msg
297
+
298
+ model := output.NewInputModel(
299
+ output.InputTypeApproval,
300
+ "Let Cline use this tool?",
301
+ "",
302
+ ih.manager.GetCurrentMode(),
303
+ )
304
+
305
+ message, shouldSend, err := ih.runInputProgram(ctx, model)
306
+ if err != nil {
307
+ return false, "", err
308
+ }
309
+
310
+ if !shouldSend {
311
+ return false, "", nil
312
+ }
313
+
314
+ // The approval and feedback are handled via the model state
315
+ return ih.feedbackApproved, message, nil
316
+ }
317
+
318
+ // runInputProgram runs the bubbletea program and waits for result
319
+ func (ih *InputHandler) runInputProgram(ctx context.Context, model output.InputModel) (string, bool, error) {
320
+ ih.mu.Lock()
321
+
322
+ // Create the program with custom update wrapper
323
+ wrappedModel := &inputProgramWrapper{
324
+ model: &model,
325
+ resultChan: ih.resultChan,
326
+ cancelChan: ih.cancelChan,
327
+ handler: ih,
328
+ }
329
+
330
+ ih.program = tea.NewProgram(wrappedModel)
331
+ ih.programDoneChan = make(chan struct{})
332
+ ih.ctx = ctx
333
+
334
+ // Set up coordinator references
335
+ output.SetProgram(ih.program)
336
+ output.SetInputModel(wrappedModel.model)
337
+ output.SetRestartCallback(ih.restartProgram)
338
+ output.SetInputVisible(true)
339
+ ih.programRunning = true
340
+ ih.mu.Unlock()
341
+
342
+ // Run program in goroutine
343
+ programErrChan := make(chan error, 1)
344
+ go func() {
345
+ if _, err := ih.program.Run(); err != nil {
346
+ programErrChan <- err
347
+ }
348
+ // Signal that program is done
349
+ close(ih.programDoneChan)
350
+ }()
351
+
352
+ // Wait for result, cancellation, or context done
353
+ select {
354
+ case <-ctx.Done():
355
+ ih.mu.Lock()
356
+ output.SetInputVisible(false)
357
+ if ih.program != nil {
358
+ ih.program.Quit()
359
+ }
360
+ ih.programRunning = false
361
+ ih.mu.Unlock()
362
+ return "", false, ctx.Err()
363
+
364
+ case <-ih.cancelChan:
365
+ ih.mu.Lock()
366
+ output.SetInputVisible(false)
367
+ ih.programRunning = false
368
+ ih.mu.Unlock()
369
+ return "", false, context.Canceled
370
+
371
+ case err := <-programErrChan:
372
+ ih.mu.Lock()
373
+ output.SetInputVisible(false)
374
+ ih.programRunning = false
375
+ ih.mu.Unlock()
376
+ return "", false, err
377
+
378
+ case result := <-ih.resultChan:
379
+ ih.mu.Lock()
380
+ output.SetInputVisible(false)
381
+ ih.programRunning = false
382
+ ih.mu.Unlock()
383
+
384
+ // Handle different input types
385
+ switch result.InputType {
386
+ case output.InputTypeMessage:
387
+ if result.Value == "" {
388
+ return "", false, nil
389
+ }
390
+ return result.Value, true, nil
391
+
392
+ case output.InputTypeApproval:
393
+ if result.NeedsFeedback {
394
+ // Need to collect feedback - will be handled by model state change
395
+ return "", false, nil
396
+ }
397
+
398
+ // Check if NoAskAgain was selected
399
+ if result.NoAskAgain && result.Approved && ih.approvalMessage != nil {
400
+ // Determine which auto-approval action to enable
401
+ action, err := determineAutoApprovalAction(ih.approvalMessage)
402
+ if err != nil {
403
+ output.Printf("\nWarning: Could not determine auto-approval action: %v\n", err)
404
+ } else {
405
+ // Enable the auto-approval action
406
+ if err := ih.manager.UpdateTaskAutoApprovalAction(ctx, action); err != nil {
407
+ output.Printf("\nWarning: Could not update auto-approval: %v\n", err)
408
+ } else {
409
+ output.Printf("\nAuto-approval enabled for %s\n", action)
410
+ }
411
+ }
412
+ }
413
+
414
+ // Store approval state for when feedback comes back
415
+ ih.feedbackApproval = false
416
+ ih.feedbackApproved = result.Approved
417
+ return "", true, nil
418
+
419
+ case output.InputTypeFeedback:
420
+ // This came from approval flow
421
+ ih.feedbackApproval = true
422
+ ih.feedbackApproved = result.Approved // Use the approval decision from the feedback
423
+ return result.Value, true, nil
424
+ }
425
+
426
+ return "", false, nil
427
+ }
428
+ }
429
+
430
+ // inputProgramWrapper wraps the InputModel to handle message routing
431
+ type inputProgramWrapper struct {
432
+ model *output.InputModel
433
+ resultChan chan output.InputSubmitMsg
434
+ cancelChan chan struct{}
435
+ handler *InputHandler
436
+ }
437
+
438
+ func (w *inputProgramWrapper) Init() tea.Cmd {
439
+ return w.model.Init()
440
+ }
441
+
442
+ func (w *inputProgramWrapper) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
443
+ switch msg := msg.(type) {
444
+ case output.InputSubmitMsg:
445
+ // Handle input submission - clear the screen before quitting
446
+ w.resultChan <- msg
447
+ clearCodes := w.model.ClearScreen()
448
+ if clearCodes != "" {
449
+ fmt.Print(clearCodes)
450
+ }
451
+ return w, tea.Quit
452
+
453
+ case output.InputCancelMsg:
454
+ // Handle cancellation - clear the screen before quitting
455
+ w.cancelChan <- struct{}{}
456
+ clearCodes := w.model.ClearScreen()
457
+ if clearCodes != "" {
458
+ fmt.Print(clearCodes)
459
+ }
460
+ return w, tea.Quit
461
+
462
+ case output.ChangeInputTypeMsg:
463
+ // Change input type (approval -> feedback)
464
+ _, cmd := w.model.Update(msg)
465
+ return w, cmd
466
+ }
467
+
468
+ // Forward to wrapped model
469
+ _, cmd := w.model.Update(msg)
470
+ return w, cmd
471
+ }
472
+
473
+ func (w *inputProgramWrapper) View() string {
474
+ return w.model.View()
475
+ }
476
+
477
+ // parseModeSwitch checks if message starts with /act or /plan and extracts the mode and remaining message
478
+ func (ih *InputHandler) parseModeSwitch(message string) (string, string, bool) {
479
+ trimmed := strings.TrimSpace(message)
480
+ lower := strings.ToLower(trimmed)
481
+
482
+ if strings.HasPrefix(lower, "/plan") {
483
+ remaining := strings.TrimSpace(trimmed[5:])
484
+ return "plan", remaining, true
485
+ }
486
+
487
+ if strings.HasPrefix(lower, "/act") {
488
+ remaining := strings.TrimSpace(trimmed[4:])
489
+ return "act", remaining, true
490
+ }
491
+
492
+ return "", message, false
493
+ }
494
+
495
+ // handleSpecialCommand processes special commands like /cancel, /exit
496
+ func (ih *InputHandler) handleSpecialCommand(ctx context.Context, message string) bool {
497
+ switch strings.ToLower(strings.TrimSpace(message)) {
498
+ case "/cancel":
499
+ ih.manager.GetRenderer().RenderTaskCancelled()
500
+ if err := ih.manager.CancelTask(ctx); err != nil {
501
+ output.Printf("Error cancelling task: %v\n", err)
502
+ } else {
503
+ output.Println("Task cancelled successfully")
504
+ }
505
+ return true
506
+ case "/exit", "/quit":
507
+ output.Println("\nExiting follow mode...")
508
+ return true
509
+ default:
510
+ return false
511
+ }
512
+ }
513
+
514
+ // Stop stops the input handler
515
+ func (ih *InputHandler) Stop() {
516
+ ih.mu.Lock()
517
+ defer ih.mu.Unlock()
518
+ if ih.pollTicker != nil {
519
+ ih.pollTicker.Stop()
520
+ }
521
+ if ih.program != nil && ih.programRunning {
522
+ ih.program.Quit()
523
+ }
524
+ ih.isRunning = false
525
+ }
526
+
527
+ // IsRunning returns whether the input handler is currently running
528
+ func (ih *InputHandler) IsRunning() bool {
529
+ ih.mu.RLock()
530
+ defer ih.mu.RUnlock()
531
+ return ih.isRunning
532
+ }
533
+
534
+ // restartProgram restarts the Bubble Tea program with preserved state
535
+ func (ih *InputHandler) restartProgram(savedModel *output.InputModel) {
536
+ ih.mu.Lock()
537
+
538
+ // Wait for old program to actually quit
539
+ if ih.programDoneChan != nil {
540
+ select {
541
+ case <-ih.programDoneChan:
542
+ // Program quit successfully
543
+ case <-time.After(100 * time.Millisecond):
544
+ // Timeout - continue anyway
545
+ }
546
+ }
547
+
548
+ // Create new wrapper with the saved model
549
+ wrappedModel := &inputProgramWrapper{
550
+ model: savedModel,
551
+ resultChan: ih.resultChan,
552
+ cancelChan: ih.cancelChan,
553
+ handler: ih,
554
+ }
555
+
556
+ // Start new program
557
+ ih.program = tea.NewProgram(wrappedModel)
558
+ ih.programDoneChan = make(chan struct{})
559
+
560
+ // Update coordinator references
561
+ output.SetProgram(ih.program)
562
+ output.SetInputModel(savedModel)
563
+ output.SetInputVisible(true)
564
+ ih.programRunning = true
565
+ ih.mu.Unlock()
566
+
567
+ // Run in goroutine
568
+ go func() {
569
+ if _, err := ih.program.Run(); err != nil {
570
+ // Log error if needed
571
+ if global.Config.Verbose {
572
+ output.Printf("\nDebug: Program restart error: %v\n", err)
573
+ }
574
+ }
575
+ close(ih.programDoneChan)
576
+ }()
577
+ }