@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,1283 @@
1
+ package task
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "errors"
7
+ "fmt"
8
+ "os"
9
+ "os/signal"
10
+ "sync"
11
+ "syscall"
12
+ "time"
13
+
14
+ "github.com/cline/cli/pkg/cli/display"
15
+ "github.com/cline/cli/pkg/cli/global"
16
+ "github.com/cline/cli/pkg/cli/handlers"
17
+ "github.com/cline/cli/pkg/cli/types"
18
+ "github.com/cline/grpc-go/client"
19
+ "github.com/cline/grpc-go/cline"
20
+ )
21
+
22
+ // Sentinel errors for CheckSendEnabled
23
+ var (
24
+ ErrNoActiveTask = fmt.Errorf("no active task")
25
+ ErrTaskBusy = fmt.Errorf("task is currently busy")
26
+ )
27
+
28
+ // Manager handles task execution and message display
29
+ type Manager struct {
30
+ mu sync.RWMutex
31
+ client *client.ClineClient
32
+ clientAddress string
33
+ state *types.ConversationState
34
+ renderer *display.Renderer
35
+ toolRenderer *display.ToolRenderer
36
+ systemRenderer *display.SystemMessageRenderer
37
+ streamingDisplay *display.StreamingDisplay
38
+ handlerRegistry *handlers.HandlerRegistry
39
+ isStreamingMode bool
40
+ isInteractive bool
41
+ currentMode string // "plan" or "act"
42
+ }
43
+
44
+ // NewManager creates a new task manager
45
+ func NewManager(client *client.ClineClient) *Manager {
46
+ state := types.NewConversationState()
47
+ renderer := display.NewRenderer(global.Config.OutputFormat)
48
+ toolRenderer := display.NewToolRenderer(renderer.GetMdRenderer(), global.Config.OutputFormat)
49
+ systemRenderer := display.NewSystemMessageRenderer(renderer, renderer.GetMdRenderer(), global.Config.OutputFormat)
50
+ streamingDisplay := display.NewStreamingDisplay(state, renderer)
51
+
52
+ // Create handler registry and register handlers
53
+ registry := handlers.NewHandlerRegistry()
54
+ registry.Register(handlers.NewAskHandler())
55
+ registry.Register(handlers.NewSayHandler())
56
+
57
+ return &Manager{
58
+ client: client,
59
+ clientAddress: "", // Will be set when client is provided
60
+ state: state,
61
+ renderer: renderer,
62
+ toolRenderer: toolRenderer,
63
+ systemRenderer: systemRenderer,
64
+ streamingDisplay: streamingDisplay,
65
+ handlerRegistry: registry,
66
+ currentMode: "plan", // Default mode
67
+ }
68
+ }
69
+
70
+ // NewManagerForAddress creates a new task manager for a specific instance address
71
+ func NewManagerForAddress(ctx context.Context, address string) (*Manager, error) {
72
+ client, err := global.GetClientForAddress(ctx, address)
73
+ if err != nil {
74
+ return nil, fmt.Errorf("failed to get client for address %s: %w", address, err)
75
+ }
76
+
77
+ manager := NewManager(client)
78
+ manager.clientAddress = address
79
+ return manager, nil
80
+ }
81
+
82
+ // NewManagerForDefault creates a new task manager using the default instance
83
+ func NewManagerForDefault(ctx context.Context) (*Manager, error) {
84
+ client, err := global.GetDefaultClient(ctx)
85
+ if err != nil {
86
+ return nil, fmt.Errorf("failed to get default client: %w", err)
87
+ }
88
+
89
+ manager := NewManager(client)
90
+
91
+ // Get the default instance address
92
+ if global.Clients != nil {
93
+ manager.clientAddress = global.Clients.GetRegistry().GetDefaultInstance()
94
+ }
95
+
96
+ return manager, nil
97
+ }
98
+
99
+ // SwitchToInstance switches the manager to use a different Cline instance
100
+ func (m *Manager) SwitchToInstance(ctx context.Context, address string) error {
101
+ m.mu.Lock()
102
+ defer m.mu.Unlock()
103
+
104
+ // Get client for the new address
105
+ newClient, err := global.GetClientForAddress(ctx, address)
106
+ if err != nil {
107
+ return fmt.Errorf("failed to get client for address %s: %w", address, err)
108
+ }
109
+
110
+ // Update the client and address
111
+ m.client = newClient
112
+ m.clientAddress = address
113
+
114
+ if global.Config.Verbose {
115
+ m.renderer.RenderDebug("Switched to instance: %s", address)
116
+ }
117
+
118
+ return nil
119
+ }
120
+
121
+ // GetCurrentInstance returns the address of the current instance
122
+ func (m *Manager) GetCurrentInstance() string {
123
+ m.mu.RLock()
124
+ defer m.mu.RUnlock()
125
+ return m.clientAddress
126
+ }
127
+
128
+ // CreateTask creates a new task
129
+ func (m *Manager) CreateTask(ctx context.Context, prompt string, images, files []string, settingsFlags []string) (string, error) {
130
+ m.mu.Lock()
131
+ defer m.mu.Unlock()
132
+
133
+ if global.Config.Verbose {
134
+ m.renderer.RenderDebug("Creating task: %s", prompt)
135
+ if len(files) > 0 {
136
+ m.renderer.RenderDebug("Files: %v", files)
137
+ }
138
+ if len(images) > 0 {
139
+ m.renderer.RenderDebug("Images: %v", images)
140
+ }
141
+ if len(settingsFlags) > 0 {
142
+ m.renderer.RenderDebug("Settings: %v", settingsFlags)
143
+ }
144
+ }
145
+
146
+ // Check if there's an active task and cancel it first
147
+ if err := m.cancelExistingTaskIfNeeded(ctx); err != nil {
148
+ return "", fmt.Errorf("failed to cancel existing task: %w", err)
149
+ }
150
+
151
+ // Parse task settings if provided
152
+ var taskSettings *cline.Settings
153
+ if len(settingsFlags) > 0 {
154
+ var err error
155
+ taskSettings, _, err = ParseTaskSettings(settingsFlags)
156
+ if err != nil {
157
+ return "", fmt.Errorf("failed to parse task settings: %w", err)
158
+ }
159
+ }
160
+
161
+ // Create task request
162
+ req := &cline.NewTaskRequest{
163
+ Text: prompt,
164
+ Images: images,
165
+ Files: files,
166
+ TaskSettings: taskSettings,
167
+ }
168
+
169
+ resp, err := m.client.Task.NewTask(ctx, req)
170
+ if err != nil {
171
+ return "", fmt.Errorf("failed to create task: %w", err)
172
+ }
173
+
174
+ taskID := resp.Value
175
+
176
+ return taskID, nil
177
+ }
178
+
179
+ // cancelExistingTaskIfNeeded checks if there's an active task and cancels it
180
+ func (m *Manager) cancelExistingTaskIfNeeded(ctx context.Context) error {
181
+ // Try to get the current state to check if there's an active task
182
+ state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
183
+ if err != nil {
184
+ // If we can't get state, assume no active task and continue
185
+ if global.Config.Verbose {
186
+ m.renderer.RenderDebug("Could not get state to check for active task: %v", err)
187
+ }
188
+ return nil
189
+ }
190
+
191
+ // Properly parse the state to check if there's actually an active task
192
+ if state.StateJson != "" {
193
+ var stateData types.ExtensionState
194
+ if err := json.Unmarshal([]byte(state.StateJson), &stateData); err != nil {
195
+ // If we can't parse state, assume no active task
196
+ if global.Config.Verbose {
197
+ m.renderer.RenderDebug("Could not parse state JSON: %v", err)
198
+ }
199
+ return nil
200
+ }
201
+
202
+ // Check if there's actually an active task
203
+ if stateData.CurrentTaskItem != nil && stateData.CurrentTaskItem.Id != "" {
204
+ if global.Config.Verbose {
205
+ m.renderer.RenderDebug("Found active task %s, cancelling...", stateData.CurrentTaskItem.Id)
206
+ }
207
+
208
+ // Cancel the existing task
209
+ _, err := m.client.Task.CancelTask(ctx, &cline.EmptyRequest{})
210
+ if err != nil {
211
+ if global.Config.Verbose {
212
+ m.renderer.RenderDebug("Cancel task returned error: %v", err)
213
+ }
214
+ } else {
215
+ fmt.Println("Cancelled existing task to start new one")
216
+ }
217
+ }
218
+ }
219
+
220
+ return nil
221
+ }
222
+
223
+ // ValidateCheckpointExists checks if a checkpoint ID is valid
224
+ func (m *Manager) ValidateCheckpointExists(ctx context.Context, checkpointID int64) error {
225
+ // Get current state
226
+ state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
227
+ if err != nil {
228
+ return fmt.Errorf("failed to get state: %w", err)
229
+ }
230
+
231
+ // Extract messages
232
+ messages, err := m.extractMessagesFromState(state.StateJson)
233
+ if err != nil {
234
+ return fmt.Errorf("failed to extract messages: %w", err)
235
+ }
236
+
237
+ // Find and validate the checkpoint message
238
+ for _, msg := range messages {
239
+ if msg.Timestamp == checkpointID {
240
+ if msg.Say != string(types.SayTypeCheckpointCreated) {
241
+ return fmt.Errorf("timestamp %d is not a checkpoint (type: %s)", checkpointID, msg.Type)
242
+ }
243
+ return nil // Valid checkpoint
244
+ }
245
+ }
246
+
247
+ return fmt.Errorf("checkpoint ID %d not found in task history", checkpointID)
248
+ }
249
+
250
+ // CheckSendEnabled checks if we can send a message to the current task
251
+ // Returns nil if sending is allowed, or an error indicating why it's not allowed
252
+ // We duplicate the logic from buttonConfig::getButtonConfig
253
+ func (m *Manager) CheckSendEnabled(ctx context.Context) error {
254
+ state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
255
+ if err != nil {
256
+ return fmt.Errorf("failed to get latest state: %w", err)
257
+ }
258
+
259
+ var stateData types.ExtensionState
260
+ if err := json.Unmarshal([]byte(state.StateJson), &stateData); err != nil {
261
+ return fmt.Errorf("failed to parse state: %w", err)
262
+ }
263
+
264
+ // Check if there is an active task
265
+ if stateData.CurrentTaskItem == nil {
266
+ return ErrNoActiveTask
267
+ }
268
+
269
+ messages, err := m.extractMessagesFromState(state.StateJson)
270
+ if err != nil {
271
+ return fmt.Errorf("failed to extract messages: %w", err)
272
+ }
273
+
274
+ if len(messages) == 0 {
275
+ return nil
276
+ }
277
+
278
+ // Use final message to perform validation
279
+ lastMessage := messages[len(messages)-1]
280
+
281
+ // Error types which we allow sending on
282
+ errorTypes := []string{
283
+ string(types.AskTypeAPIReqFailed), // "api_req_failed"
284
+ string(types.AskTypeMistakeLimitReached), // "mistake_limit_reached"
285
+ }
286
+
287
+ isError := false
288
+
289
+ // Check if message is an error type
290
+ if lastMessage.Type == types.MessageTypeAsk {
291
+ for _, errType := range errorTypes {
292
+ if lastMessage.Ask == errType {
293
+ isError = true
294
+ break
295
+ }
296
+ }
297
+ }
298
+
299
+ // Streaming and error check
300
+ if lastMessage.Partial && !isError {
301
+ if global.Config.Verbose {
302
+ m.renderer.RenderDebug("Send disabled: task is streaming and non-error")
303
+ }
304
+ return ErrTaskBusy
305
+ }
306
+
307
+ // All ask messages allow sending, EXCEPT command_output
308
+ if lastMessage.Type == types.MessageTypeAsk {
309
+ // Special case: command_output means command is actively streaming
310
+ // In the CLI, we don't want to show input during streaming output (too messy)
311
+ // The webview can show "Proceed While Running" button, but CLI should wait
312
+ if lastMessage.Ask == string(types.AskTypeCommandOutput) {
313
+ if global.Config.Verbose {
314
+ m.renderer.RenderDebug("Send disabled: command output is streaming")
315
+ }
316
+ return ErrTaskBusy
317
+ }
318
+
319
+ if global.Config.Verbose {
320
+ m.renderer.RenderDebug("Send enabled: ask message")
321
+ }
322
+ return nil
323
+ }
324
+
325
+ // Technically unnecessary but implements getButtonConfig 1-1
326
+ if lastMessage.Type == types.MessageTypeSay && lastMessage.Say == string(types.SayTypeAPIReqStarted) {
327
+ if global.Config.Verbose {
328
+ m.renderer.RenderDebug("Send disabled: API request is active")
329
+ }
330
+ return ErrTaskBusy
331
+ }
332
+
333
+ if global.Config.Verbose {
334
+ m.renderer.RenderDebug("Send disabled: default fallback")
335
+ }
336
+
337
+ return ErrTaskBusy
338
+ }
339
+
340
+ // CheckNeedsApproval determines if the current task is waiting for approval
341
+ // Returns (needsApproval, lastMessage, error)
342
+ func (m *Manager) CheckNeedsApproval(ctx context.Context) (bool, *types.ClineMessage, error) {
343
+ state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
344
+ if err != nil {
345
+ return false, nil, fmt.Errorf("failed to get latest state: %w", err)
346
+ }
347
+
348
+ messages, err := m.extractMessagesFromState(state.StateJson)
349
+ if err != nil {
350
+ return false, nil, fmt.Errorf("failed to extract messages: %w", err)
351
+ }
352
+
353
+ if len(messages) == 0 {
354
+ return false, nil, nil
355
+ }
356
+
357
+ // Use final message to check if approval is needed
358
+ lastMessage := messages[len(messages)-1]
359
+
360
+ // Only check non-partial ask messages
361
+ if lastMessage.Partial {
362
+ return false, nil, nil
363
+ }
364
+
365
+ // Check if this is an approval-required ask type
366
+ if lastMessage.Type == types.MessageTypeAsk {
367
+ approvalTypes := []string{
368
+ string(types.AskTypeTool),
369
+ string(types.AskTypeCommand),
370
+ string(types.AskTypeBrowserActionLaunch),
371
+ string(types.AskTypeUseMcpServer),
372
+ }
373
+
374
+ for _, approvalType := range approvalTypes {
375
+ if lastMessage.Ask == approvalType {
376
+ return true, lastMessage, nil
377
+ }
378
+ }
379
+ }
380
+
381
+ return false, nil, nil
382
+ }
383
+
384
+ // SendMessage sends a followup message to the current task
385
+ func (m *Manager) SendMessage(ctx context.Context, message string, images, files []string, approve string) error {
386
+ responseType := "messageResponse"
387
+
388
+ if approve == "true" {
389
+ responseType = "yesButtonClicked"
390
+ }
391
+
392
+ if approve == "false" {
393
+ responseType = "noButtonClicked"
394
+ }
395
+
396
+ if global.Config.Verbose {
397
+ m.renderer.RenderDebug("Sending message: %s", message)
398
+ if len(files) > 0 {
399
+ m.renderer.RenderDebug("Files: %v", files)
400
+ }
401
+ if len(images) > 0 {
402
+ m.renderer.RenderDebug("Images: %v", images)
403
+ }
404
+ }
405
+
406
+ // Send the followup message using AskResponse
407
+ req := &cline.AskResponseRequest{
408
+ ResponseType: responseType,
409
+ Text: message,
410
+ Images: images,
411
+ Files: files,
412
+ }
413
+
414
+ _, err := m.client.Task.AskResponse(ctx, req)
415
+ if err != nil {
416
+ return fmt.Errorf("failed to send message: %w", err)
417
+ }
418
+
419
+ return nil
420
+ }
421
+
422
+ // SetMode sets the Plan/Act mode for the current Cline instance and optionally sends message
423
+ func (m *Manager) SetMode(ctx context.Context, mode string, message *string, images, files []string) error {
424
+ if mode != "act" && mode != "plan" {
425
+ return fmt.Errorf("invalid mode '%s': must be 'act' or 'plan'", mode)
426
+ }
427
+
428
+ var protoMode cline.PlanActMode
429
+ if mode == "plan" {
430
+ protoMode = cline.PlanActMode_PLAN
431
+ } else {
432
+ protoMode = cline.PlanActMode_ACT
433
+ }
434
+
435
+ req := &cline.TogglePlanActModeRequest{
436
+ Metadata: &cline.Metadata{},
437
+ Mode: protoMode,
438
+ }
439
+
440
+ if message != nil {
441
+ req.ChatContent = &cline.ChatContent{
442
+ Message: message,
443
+ Images: images,
444
+ Files: files,
445
+ }
446
+ }
447
+
448
+ _, err := m.client.State.TogglePlanActModeProto(ctx, req)
449
+ if err != nil {
450
+ return fmt.Errorf("failed to set mode to '%s': %w", mode, err)
451
+ }
452
+
453
+ return nil
454
+ }
455
+
456
+ // SetModeAndSendMessage sets the mode and sends a message in one operation
457
+ // Handles task restoration internally if the mode switch cancels the current task
458
+ func (m *Manager) SetModeAndSendMessage(ctx context.Context, mode, message string, images, files []string) error {
459
+ if mode != "act" && mode != "plan" {
460
+ return fmt.Errorf("invalid mode '%s': must be 'act' or 'plan'", mode)
461
+ }
462
+
463
+ taskId, err := m.getCurrentTaskId(ctx)
464
+ if err != nil {
465
+ return fmt.Errorf("failed to get current task ID: %w", err)
466
+ }
467
+ fmt.Printf("Current task ID: %s\n", taskId)
468
+
469
+ var protoMode cline.PlanActMode
470
+ if mode == "plan" {
471
+ protoMode = cline.PlanActMode_PLAN
472
+ } else {
473
+ protoMode = cline.PlanActMode_ACT
474
+ }
475
+
476
+ req := &cline.TogglePlanActModeRequest{
477
+ Metadata: &cline.Metadata{},
478
+ Mode: protoMode,
479
+ ChatContent: &cline.ChatContent{
480
+ Message: &message,
481
+ Images: images,
482
+ Files: files,
483
+ },
484
+ }
485
+
486
+ result, err := m.client.State.TogglePlanActModeProto(ctx, req)
487
+ if err != nil {
488
+ return fmt.Errorf("failed to set mode to '%s': %w", mode, err)
489
+ }
490
+
491
+ taskPreserved := result.Value
492
+
493
+ if taskPreserved {
494
+ fmt.Printf("Message sent as part of mode change\n")
495
+ return nil
496
+ } else {
497
+ if message != "" || len(images) > 0 || len(files) > 0 {
498
+ fmt.Printf("Task was cancelled, restoring task ID: %s\n", taskId)
499
+
500
+ err = m.ReinitExistingTaskFromId(ctx, taskId)
501
+ if err != nil {
502
+ return fmt.Errorf("Failed to restore task: %w", err)
503
+ }
504
+ fmt.Printf("Task restored successfully\n")
505
+
506
+ // Hardcoded sleep should be replaced with a way to fetch whether task is ready algorithmically
507
+ time.Sleep(1 * time.Second)
508
+
509
+ err = m.SendMessage(ctx, message, images, files, "")
510
+ if err != nil {
511
+ return fmt.Errorf("Failed to send message: %w", err)
512
+ }
513
+ fmt.Printf("Message sent to restored task\n")
514
+ }
515
+ }
516
+
517
+ return nil
518
+ }
519
+
520
+ // getCurrentTaskId extracts the current task ID from the server state
521
+ func (m *Manager) getCurrentTaskId(ctx context.Context) (string, error) {
522
+ // Get the latest state
523
+ state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
524
+ if err != nil {
525
+ return "", fmt.Errorf("failed to get state: %w", err)
526
+ }
527
+
528
+ // Parse the server state JSON
529
+ var stateData types.ExtensionState
530
+ if err := json.Unmarshal([]byte(state.StateJson), &stateData); err != nil {
531
+ return "", fmt.Errorf("failed to parse state JSON: %w", err)
532
+ }
533
+
534
+ // Extract current task ID
535
+ if stateData.CurrentTaskItem != nil && stateData.CurrentTaskItem.Id != "" {
536
+ return stateData.CurrentTaskItem.Id, nil
537
+ }
538
+
539
+ return "", fmt.Errorf("no current task found in state")
540
+ }
541
+
542
+ // ReinitExistingTaskFromId reinitializes an existing task from the given task ID
543
+ func (m *Manager) ReinitExistingTaskFromId(ctx context.Context, taskId string) error {
544
+ req := &cline.StringRequest{Value: taskId}
545
+ resp, err := m.client.Task.ShowTaskWithId(ctx, req)
546
+ if err != nil {
547
+ return fmt.Errorf("Failed to reinitialize task %s: %w", taskId, err)
548
+ }
549
+
550
+ fmt.Printf("Successfully reinitialized task: %s (ID: %s)\n", taskId, resp.Id)
551
+
552
+ return nil
553
+ }
554
+
555
+ // ResumeTask resumes an existing task by ID
556
+ func (m *Manager) ResumeTask(ctx context.Context, taskID string) error {
557
+ m.mu.Lock()
558
+ defer m.mu.Unlock()
559
+
560
+ if global.Config.Verbose {
561
+ m.renderer.RenderDebug("Resuming task: %s", taskID)
562
+ }
563
+
564
+ // This call handles cancellation of any active task
565
+ if err := m.ReinitExistingTaskFromId(ctx, taskID); err != nil {
566
+ return fmt.Errorf("failed to resume task %s: %w", taskID, err)
567
+ }
568
+
569
+ fmt.Printf("Task %s resumed successfully\n", taskID)
570
+
571
+ return nil
572
+ }
573
+
574
+ // RestoreCheckpoint restores the task to a specific checkpoint
575
+ func (m *Manager) RestoreCheckpoint(ctx context.Context, checkpointID int64, restoreType string) error {
576
+ if global.Config.Verbose {
577
+ m.renderer.RenderDebug("Restoring checkpoint: %d (type: %s)", checkpointID, restoreType)
578
+ }
579
+
580
+ // Create the checkpoint restore request
581
+ req := &cline.CheckpointRestoreRequest{
582
+ Metadata: &cline.Metadata{},
583
+ Number: checkpointID,
584
+ RestoreType: restoreType,
585
+ }
586
+
587
+ _, err := m.client.Checkpoints.CheckpointRestore(ctx, req)
588
+ if err != nil {
589
+ return fmt.Errorf("failed to restore checkpoint %d: %w", checkpointID, err)
590
+ }
591
+
592
+ return nil
593
+ }
594
+
595
+ // CancelTask cancels the current task
596
+ func (m *Manager) CancelTask(ctx context.Context) error {
597
+ m.mu.Lock()
598
+ defer m.mu.Unlock()
599
+
600
+ _, err := m.client.Task.CancelTask(ctx, &cline.EmptyRequest{})
601
+ if err != nil {
602
+ return fmt.Errorf("failed to cancel task: %w", err)
603
+ }
604
+
605
+ return nil
606
+ }
607
+
608
+ // ShowConversation displays the current conversation
609
+ func (m *Manager) ShowConversation(ctx context.Context) error {
610
+ // Check if there's an active task before showing conversation
611
+ err := m.CheckSendEnabled(ctx)
612
+ if err != nil {
613
+ // Handle specific error cases
614
+ if errors.Is(err, ErrNoActiveTask) {
615
+ fmt.Println("No active task found. Use 'cline task new' to create a task first.")
616
+ return nil
617
+ }
618
+ // For other errors (like task busy), we can still show the conversation
619
+ }
620
+
621
+ // Disable streaming mode for static view
622
+ m.mu.Lock()
623
+ m.isStreamingMode = false
624
+ m.mu.Unlock()
625
+
626
+ m.mu.RLock()
627
+ defer m.mu.RUnlock()
628
+
629
+ // Get the latest state which contains messages
630
+ state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
631
+ if err != nil {
632
+ return fmt.Errorf("failed to get state: %w", err)
633
+ }
634
+
635
+ // Parse the state JSON to extract messages
636
+ messages, err := m.extractMessagesFromState(state.StateJson)
637
+ if err != nil {
638
+ return fmt.Errorf("failed to extract messages: %w", err)
639
+ }
640
+
641
+ if len(messages) == 0 {
642
+ fmt.Println("No conversation history found.")
643
+ return nil
644
+ }
645
+
646
+ for i, msg := range messages {
647
+ if msg.Partial {
648
+ continue
649
+ }
650
+ m.displayMessage(msg, false, false, i)
651
+ }
652
+
653
+ return nil
654
+ }
655
+
656
+ func (m *Manager) FollowConversation(ctx context.Context, instanceAddress string, interactive bool) error {
657
+ // Enable streaming mode
658
+ m.mu.Lock()
659
+ m.isStreamingMode = true
660
+ m.isInteractive = interactive
661
+ m.mu.Unlock()
662
+
663
+ if global.Config.OutputFormat != "plain" {
664
+ markdown := fmt.Sprintf("*Using instance: %s*\n*Press Ctrl+C to exit*", instanceAddress)
665
+ rendered := m.renderer.RenderMarkdown(markdown)
666
+ fmt.Printf("%s", rendered)
667
+ } else {
668
+ fmt.Printf("Using instance: %s\n", instanceAddress)
669
+ if interactive {
670
+ fmt.Println("Following task conversation in interactive mode... (Press Ctrl+C to exit)")
671
+ } else {
672
+ fmt.Println("Following task conversation... (Press Ctrl+C to exit)")
673
+ }
674
+ }
675
+
676
+ ctx, cancel := context.WithCancel(ctx)
677
+ defer cancel()
678
+
679
+ // Create stream coordinator
680
+ coordinator := NewStreamCoordinator()
681
+
682
+ // Load history first
683
+ totalMessageCount, err := m.loadAndDisplayRecentHistory(ctx)
684
+ if err != nil {
685
+ m.renderer.RenderDebug("Warning: Failed to load conversation history: %v", err)
686
+ totalMessageCount = 0
687
+ }
688
+ coordinator.SetConversationTurnStartIndex(totalMessageCount)
689
+
690
+ // Start both streams concurrently
691
+ errChan := make(chan error, 3)
692
+
693
+ if global.Config.OutputFormat == "json" {
694
+ go m.handleStateStream(ctx, coordinator, errChan, nil)
695
+ } else {
696
+ go m.handleStateStream(ctx, coordinator, errChan, nil)
697
+ go m.handlePartialMessageStream(ctx, coordinator, errChan)
698
+
699
+ // Start input handler if interactive mode is enabled
700
+ if interactive {
701
+ inputHandler := NewInputHandler(m, coordinator, cancel)
702
+ go inputHandler.Start(ctx, errChan)
703
+ }
704
+ }
705
+
706
+ // Handle Ctrl+C signals
707
+ sigChan := make(chan os.Signal, 1)
708
+ signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
709
+ go func() {
710
+ defer signal.Stop(sigChan) // Clean up signal handler when goroutine exits
711
+ for {
712
+ select {
713
+ case <-ctx.Done():
714
+ return
715
+ case <-sigChan:
716
+ if interactive {
717
+ // Interactive mode (task chat)
718
+ // Check if input is currently being shown
719
+ if coordinator.IsInputAllowed() {
720
+ // Input form is showing - huh will handle the signal via ErrUserAborted
721
+ // Do nothing here, let the input handler deal with it
722
+ } else {
723
+ // Streaming mode - cancel the task and stay in follow mode
724
+ m.renderer.RenderTaskCancelled()
725
+ if err := m.CancelTask(context.Background()); err != nil {
726
+ fmt.Printf("Error cancelling task: %v\n", err)
727
+ }
728
+ // Don't cancel main context - stay in follow mode
729
+ }
730
+ } else {
731
+ // Non-interactive mode (task view --follow)
732
+ // Just exit without canceling the task
733
+ cancel()
734
+ return // Exit the loop after canceling in non-interactive mode
735
+ }
736
+ }
737
+ }
738
+ }()
739
+
740
+ // Wait for either stream to error or context cancellation
741
+ select {
742
+ case <-ctx.Done():
743
+ // Check if this was a user-initiated cancellation (Ctrl+C)
744
+ // Return nil for clean exit instead of context.Canceled error
745
+ if ctx.Err() == context.Canceled {
746
+ return nil
747
+ }
748
+ return ctx.Err()
749
+ case err := <-errChan:
750
+ cancel()
751
+ return err
752
+ }
753
+ }
754
+
755
+ // FollowConversationUntilCompletion streams conversation updates until task completion
756
+ func (m *Manager) FollowConversationUntilCompletion(ctx context.Context) error {
757
+ // Enable streaming mode
758
+ m.mu.Lock()
759
+ m.isStreamingMode = true
760
+ m.mu.Unlock()
761
+
762
+ fmt.Println("Following task conversation until completion... (Press Ctrl+C to exit)")
763
+
764
+ ctx, cancel := context.WithCancel(ctx)
765
+ defer cancel()
766
+
767
+ // Create stream coordinator
768
+ coordinator := NewStreamCoordinator()
769
+
770
+ // Load history first
771
+ totalMessageCount, err := m.loadAndDisplayRecentHistory(ctx)
772
+ if err != nil {
773
+ m.renderer.RenderDebug("Warning: Failed to load conversation history: %v", err)
774
+ totalMessageCount = 0
775
+ }
776
+ coordinator.SetConversationTurnStartIndex(totalMessageCount)
777
+
778
+ // Start both streams concurrently
779
+ errChan := make(chan error, 2)
780
+ completionChan := make(chan bool, 1)
781
+
782
+ if global.Config.OutputFormat == "json" {
783
+ go m.handleStateStream(ctx, coordinator, errChan, completionChan)
784
+ } else {
785
+ go m.handleStateStream(ctx, coordinator, errChan, completionChan)
786
+ go m.handlePartialMessageStream(ctx, coordinator, errChan)
787
+ }
788
+
789
+ // Wait for completion, error, or context cancellation
790
+ select {
791
+ case <-ctx.Done():
792
+ return ctx.Err()
793
+ case <-completionChan:
794
+ cancel()
795
+ return nil
796
+ case err := <-errChan:
797
+ cancel()
798
+ return err
799
+ }
800
+ }
801
+
802
+ // handleStateStream handles the SubscribeToState stream
803
+ func (m *Manager) handleStateStream(ctx context.Context, coordinator *StreamCoordinator, errChan chan error, completionChan chan bool) {
804
+ stateStream, err := m.client.State.SubscribeToState(ctx, &cline.EmptyRequest{})
805
+ if err != nil {
806
+ errChan <- fmt.Errorf("failed to subscribe to state: %w", err)
807
+ return
808
+ }
809
+
810
+ for {
811
+ select {
812
+ case <-ctx.Done():
813
+ return
814
+ default:
815
+ stateUpdate, err := stateStream.Recv()
816
+ if err != nil {
817
+ m.renderer.RenderDebug("State stream receive error: %v", err)
818
+ errChan <- fmt.Errorf("failed to receive state update: %w", err)
819
+ return
820
+ }
821
+
822
+ var pErr error
823
+
824
+ if global.Config.OutputFormat == "json" {
825
+ pErr = m.processStateUpdateJsonMode(stateUpdate, coordinator, completionChan)
826
+ } else {
827
+ pErr = m.processStateUpdate(stateUpdate, coordinator, completionChan)
828
+ }
829
+
830
+ if pErr != nil {
831
+ m.renderer.RenderDebug("State processing error: %v", pErr)
832
+ }
833
+ }
834
+ }
835
+ }
836
+
837
+ func (m *Manager) processStateUpdateJsonMode(stateUpdate *cline.State, coordinator *StreamCoordinator, completionChan chan bool) error {
838
+ messages, err := m.extractMessagesFromState(stateUpdate.StateJson)
839
+ if err != nil {
840
+ return err
841
+ }
842
+
843
+ // Process messages from current conversation turn onwards
844
+ startIndex := coordinator.GetConversationTurnStartIndex()
845
+
846
+ var foundCompletion bool
847
+ var displayedUsage bool
848
+
849
+ for i := startIndex; i < len(messages); i++ {
850
+ msg := messages[i]
851
+
852
+ if global.Config.Verbose {
853
+ m.renderer.RenderDebug("State message %d: type=%s, say=%s", i, msg.Type, msg.Say)
854
+ }
855
+
856
+ // Exit after we've seen a task completion & printed out the usage info
857
+ if msg.Say == string(types.SayTypeCompletionResult) {
858
+ foundCompletion = true
859
+ }
860
+
861
+ // Determine if message is ready to be displayed now
862
+ shouldDisplay := true
863
+
864
+ switch {
865
+ case msg.Say == string(types.SayTypeAPIReqStarted):
866
+ shouldDisplay = false
867
+ apiInfo := types.APIRequestInfo{Cost: -1}
868
+ if err := json.Unmarshal([]byte(msg.Text), &apiInfo); err == nil && apiInfo.Cost >= 0 {
869
+ shouldDisplay = true
870
+ displayedUsage = true
871
+ }
872
+ }
873
+
874
+ // Skip if message is partial, except for a specific edge case
875
+ if msg.Partial {
876
+ // Exception: display if type=say, text="", say="text"
877
+ if msg.IsSay() && msg.Text == "" && msg.Say == string(types.SayTypeText) {
878
+ shouldDisplay = true
879
+ } else {
880
+ shouldDisplay = false
881
+ }
882
+ }
883
+
884
+ // Display valid messages, exit as soon as we hit a non-valid message
885
+ if shouldDisplay {
886
+ coordinator.CompleteTurn(i + 1) // Mark the message as complete as soon as we print it
887
+ m.displayMessage(msg, false, false, i)
888
+ } else {
889
+ break
890
+ }
891
+ }
892
+
893
+ // We only want to exit after we've displayed the usage, for the case of seeing completion result
894
+ if completionChan != nil && foundCompletion && displayedUsage {
895
+ completionChan <- true
896
+ }
897
+
898
+ return nil
899
+ }
900
+
901
+ // processStateUpdate processes state updates and supports logic for handling task competion markers
902
+ func (m *Manager) processStateUpdate(stateUpdate *cline.State, coordinator *StreamCoordinator, completionChan chan bool) error {
903
+ // Update current mode from state
904
+ m.updateMode(stateUpdate.StateJson)
905
+
906
+ messages, err := m.extractMessagesFromState(stateUpdate.StateJson)
907
+ if err != nil {
908
+ return err
909
+ }
910
+
911
+ // Process messages from current conversation turn onwards
912
+ startIndex := coordinator.GetConversationTurnStartIndex()
913
+
914
+ var foundCompletion bool
915
+ var displayedUsage bool
916
+
917
+ for i := startIndex; i < len(messages); i++ {
918
+ msg := messages[i]
919
+
920
+ if global.Config.Verbose {
921
+ m.renderer.RenderDebug("State message %d: type=%s, say=%s", i, msg.Type, msg.Say)
922
+ }
923
+
924
+ // Exit after we've seen a task completion & printed out the usage info
925
+ if msg.Say == string(types.SayTypeCompletionResult) {
926
+ foundCompletion = true
927
+ }
928
+
929
+ switch {
930
+ case msg.Say == string(types.SayTypeUserFeedback):
931
+ msgKey := fmt.Sprintf("%d", msg.Timestamp)
932
+ if !coordinator.IsProcessedInCurrentTurn(msgKey) {
933
+ fmt.Println()
934
+ m.displayMessage(msg, false, false, i)
935
+ coordinator.MarkProcessedInCurrentTurn(msgKey)
936
+ }
937
+
938
+ case msg.Say == string(types.SayTypeCommand):
939
+ msgKey := fmt.Sprintf("%d", msg.Timestamp)
940
+ if !coordinator.IsProcessedInCurrentTurn(msgKey) {
941
+ fmt.Println()
942
+ m.displayMessage(msg, false, false, i)
943
+
944
+ coordinator.MarkProcessedInCurrentTurn(msgKey)
945
+ }
946
+
947
+ case msg.Say == string(types.SayTypeCommandOutput):
948
+ msgKey := fmt.Sprintf("%d", msg.Timestamp)
949
+ if !coordinator.IsProcessedInCurrentTurn(msgKey) {
950
+ m.displayMessage(msg, false, false, i)
951
+
952
+ coordinator.MarkProcessedInCurrentTurn(msgKey)
953
+ }
954
+
955
+ case msg.Say == string(types.SayTypeBrowserActionLaunch):
956
+ msgKey := fmt.Sprintf("%d", msg.Timestamp)
957
+ if !coordinator.IsProcessedInCurrentTurn(msgKey) {
958
+ fmt.Println()
959
+ m.displayMessage(msg, false, false, i)
960
+
961
+ coordinator.MarkProcessedInCurrentTurn(msgKey)
962
+ }
963
+
964
+ case msg.Say == string(types.SayTypeMcpServerRequestStarted):
965
+ msgKey := fmt.Sprintf("%d", msg.Timestamp)
966
+ if !coordinator.IsProcessedInCurrentTurn(msgKey) {
967
+ fmt.Println()
968
+ m.displayMessage(msg, false, false, i)
969
+
970
+ coordinator.MarkProcessedInCurrentTurn(msgKey)
971
+ }
972
+
973
+ case msg.Say == string(types.SayTypeCheckpointCreated):
974
+ msgKey := fmt.Sprintf("%d", msg.Timestamp)
975
+ if !coordinator.IsProcessedInCurrentTurn(msgKey) {
976
+ fmt.Println()
977
+ m.displayMessage(msg, false, false, i)
978
+
979
+ coordinator.MarkProcessedInCurrentTurn(msgKey)
980
+ }
981
+
982
+ case msg.Say == string(types.SayTypeAPIReqStarted):
983
+ msgKey := fmt.Sprintf("%d", msg.Timestamp)
984
+ apiInfo := types.APIRequestInfo{Cost: -1}
985
+ if err := json.Unmarshal([]byte(msg.Text), &apiInfo); err == nil && apiInfo.Cost >= 0 {
986
+ if !coordinator.IsProcessedInCurrentTurn(msgKey) {
987
+ fmt.Println() // adds a separator between cline message and usage message
988
+ m.displayMessage(msg, false, false, i)
989
+
990
+ coordinator.MarkProcessedInCurrentTurn(msgKey)
991
+ coordinator.CompleteTurn(len(messages))
992
+ displayedUsage = true
993
+ }
994
+ }
995
+
996
+ case msg.Ask == string(types.AskTypeCommandOutput):
997
+ msgKey := fmt.Sprintf("%d", msg.Timestamp)
998
+ if !coordinator.IsProcessedInCurrentTurn(msgKey) {
999
+ m.displayMessage(msg, false, false, i)
1000
+
1001
+ coordinator.MarkProcessedInCurrentTurn(msgKey)
1002
+ }
1003
+
1004
+ case msg.Ask == string(types.AskTypePlanModeRespond):
1005
+ msgKey := fmt.Sprintf("%d", msg.Timestamp)
1006
+ // Non-streaming mode: render normally when message is complete
1007
+ if !msg.Partial && !coordinator.IsProcessedInCurrentTurn(msgKey) {
1008
+ m.displayMessage(msg, false, false, i)
1009
+
1010
+ coordinator.MarkProcessedInCurrentTurn(msgKey)
1011
+ }
1012
+
1013
+ case msg.Type == types.MessageTypeAsk:
1014
+ msgKey := fmt.Sprintf("%d", msg.Timestamp)
1015
+ // Only render if not already handled by partial stream
1016
+ if !msg.Partial && !coordinator.IsProcessedInCurrentTurn(msgKey) {
1017
+ m.displayMessage(msg, false, false, i)
1018
+ coordinator.MarkProcessedInCurrentTurn(msgKey)
1019
+ }
1020
+ }
1021
+ }
1022
+
1023
+ // We only want to exit after we've displayed the usage, for the case of seeing completion result
1024
+ if completionChan != nil && foundCompletion && displayedUsage {
1025
+ completionChan <- true
1026
+ }
1027
+
1028
+ return nil
1029
+ }
1030
+
1031
+ // handlePartialMessageStream handles the SubscribeToPartialMessage stream for streaming assistant text
1032
+ func (m *Manager) handlePartialMessageStream(ctx context.Context, coordinator *StreamCoordinator, errChan chan error) {
1033
+ partialStream, err := m.client.Ui.SubscribeToPartialMessage(ctx, &cline.EmptyRequest{})
1034
+ if err != nil {
1035
+ errChan <- fmt.Errorf("failed to subscribe to partial messages: %w", err)
1036
+ return
1037
+ }
1038
+
1039
+ defer func() {
1040
+ m.streamingDisplay.FreezeActiveSegment()
1041
+ }()
1042
+
1043
+ for {
1044
+ select {
1045
+ case <-ctx.Done():
1046
+ return
1047
+ default:
1048
+ protoMsg, err := partialStream.Recv()
1049
+ if err != nil {
1050
+ m.renderer.RenderDebug("Partial stream receive error: %v", err)
1051
+ errChan <- fmt.Errorf("failed to receive partial message: %w", err)
1052
+ return
1053
+ }
1054
+
1055
+ // Convert proto message to our Message struct
1056
+ msg := types.ConvertProtoToMessage(protoMsg)
1057
+
1058
+ // Debug: Log received message (always show for debugging)
1059
+ m.renderer.RenderDebug("Received streaming message: type=%s, partial=%v, text_len=%d",
1060
+ msg.Type, msg.Partial, len(msg.Text))
1061
+
1062
+ // Handle the message with streaming support for de-dupping
1063
+ if err := m.handleStreamingMessage(msg, coordinator); err != nil {
1064
+ m.renderer.RenderDebug("Error handling streaming message: %v", err)
1065
+ }
1066
+ }
1067
+ }
1068
+ }
1069
+
1070
+ // handleStreamingMessage handles a streaming message
1071
+ func (m *Manager) handleStreamingMessage(msg *types.ClineMessage, coordinator *StreamCoordinator) error {
1072
+ // Debug: Always log what we're processing
1073
+ m.renderer.RenderDebug("Processing message: timestamp=%d, partial=%v, type=%s, text_preview=%s",
1074
+ msg.Timestamp, msg.Partial, msg.Type, m.truncateText(msg.Text, 50))
1075
+
1076
+ // Use streaming display which handles deduplication internally
1077
+ if err := m.streamingDisplay.HandlePartialMessage(msg); err != nil {
1078
+ m.renderer.RenderDebug("Streaming display failed, using fallback: %v", err)
1079
+ // Fallback to regular display
1080
+ m.displayMessage(msg, true, false, -1)
1081
+ }
1082
+
1083
+ return nil
1084
+ }
1085
+
1086
+ // truncateText truncates text for debug display
1087
+ func (m *Manager) truncateText(text string, maxLen int) string {
1088
+ if len(text) <= maxLen {
1089
+ return text
1090
+ }
1091
+ return text[:maxLen] + "..."
1092
+ }
1093
+
1094
+ // displayMessage displays a single message using the handler system
1095
+ func (m *Manager) displayMessage(msg *types.ClineMessage, isLast, isPartial bool, messageIndex int) error {
1096
+ if global.Config.OutputFormat == "json" {
1097
+ return m.outputMessageAsJSON(msg)
1098
+ } else {
1099
+ m.mu.RLock()
1100
+ isStreaming := m.isStreamingMode
1101
+ isInteractive := m.isInteractive
1102
+ m.mu.RUnlock()
1103
+
1104
+ dc := &handlers.DisplayContext{
1105
+ State: m.state,
1106
+ Renderer: m.renderer,
1107
+ ToolRenderer: m.toolRenderer,
1108
+ SystemRenderer: m.systemRenderer,
1109
+ IsLast: isLast,
1110
+ IsPartial: isPartial,
1111
+ MessageIndex: messageIndex,
1112
+ IsStreamingMode: isStreaming,
1113
+ IsInteractive: isInteractive,
1114
+ }
1115
+
1116
+ return m.handlerRegistry.Handle(msg, dc)
1117
+ }
1118
+ }
1119
+
1120
+ // outputMessageAsJSON prints a single cline message as json
1121
+ func (m *Manager) outputMessageAsJSON(msg *types.ClineMessage) error {
1122
+ jsonBytes, err := json.MarshalIndent(msg, "", " ")
1123
+ if err != nil {
1124
+ return fmt.Errorf("failed to marshal message as JSON: %w", err)
1125
+ }
1126
+
1127
+ fmt.Println(string(jsonBytes))
1128
+ return nil
1129
+ }
1130
+
1131
+ // loadAndDisplayRecentHistory loads and displays recent conversation history and returns the total number of existing messages
1132
+ func (m *Manager) loadAndDisplayRecentHistory(ctx context.Context) (int, error) {
1133
+ // Get the latest state which contains messages
1134
+ state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
1135
+ if err != nil {
1136
+ return 0, fmt.Errorf("failed to get state: %w", err)
1137
+ }
1138
+
1139
+ // Parse the state JSON to extract messages
1140
+ messages, err := m.extractMessagesFromState(state.StateJson)
1141
+ if err != nil {
1142
+ return 0, fmt.Errorf("failed to extract messages: %w", err)
1143
+ }
1144
+
1145
+ if len(messages) == 0 {
1146
+ fmt.Println("No conversation history found.")
1147
+ return 0, nil
1148
+ }
1149
+
1150
+ // Show only the last 100 messages by default
1151
+ const maxHistoryMessages = 100
1152
+ totalMessages := len(messages)
1153
+ startIndex := 0
1154
+
1155
+ if totalMessages > maxHistoryMessages {
1156
+ startIndex = totalMessages - maxHistoryMessages
1157
+ if global.Config.OutputFormat != "plain" {
1158
+ markdown := fmt.Sprintf("*Conversation history (%d of %d messages)*", maxHistoryMessages, totalMessages)
1159
+ rendered := m.renderer.RenderMarkdown(markdown)
1160
+ fmt.Printf("\n%s\n\n", rendered)
1161
+ } else {
1162
+ fmt.Printf("--- Conversation history (%d of %d messages) ---\n", maxHistoryMessages, totalMessages)
1163
+ }
1164
+ } else {
1165
+ if global.Config.OutputFormat != "plain" {
1166
+ markdown := fmt.Sprintf("*Conversation history (%d messages)*", totalMessages)
1167
+ rendered := m.renderer.RenderMarkdown(markdown)
1168
+ fmt.Printf("\n%s\n\n", rendered)
1169
+ } else {
1170
+ fmt.Printf("--- Conversation history (%d messages) ---\n", totalMessages)
1171
+ }
1172
+ }
1173
+
1174
+ for i := startIndex; i < len(messages); i++ {
1175
+ msg := messages[i]
1176
+
1177
+ if msg.Partial {
1178
+ continue
1179
+ }
1180
+
1181
+ m.displayMessage(msg, false, false, i)
1182
+ }
1183
+
1184
+ // Return the total number of messages in the conversation
1185
+ return totalMessages, nil
1186
+ }
1187
+
1188
+ // extractMessagesFromState parses the state JSON and extracts messages
1189
+ func (m *Manager) extractMessagesFromState(stateJson string) ([]*types.ClineMessage, error) {
1190
+ return types.ExtractMessagesFromStateJSON(stateJson)
1191
+ }
1192
+
1193
+ // GetState returns the current conversation state
1194
+ func (m *Manager) GetState() *types.ConversationState {
1195
+ return m.state
1196
+ }
1197
+
1198
+ // GetClient returns the underlying ClineClient for direct gRPC calls
1199
+ func (m *Manager) GetClient() *client.ClineClient {
1200
+ m.mu.RLock()
1201
+ defer m.mu.RUnlock()
1202
+ return m.client
1203
+ }
1204
+
1205
+ // GetRenderer returns the renderer for formatting output
1206
+ func (m *Manager) GetRenderer() *display.Renderer {
1207
+ return m.renderer
1208
+ }
1209
+
1210
+ // GetCurrentMode returns the current plan/act mode
1211
+ func (m *Manager) GetCurrentMode() string {
1212
+ m.mu.RLock()
1213
+ defer m.mu.RUnlock()
1214
+ return m.currentMode
1215
+ }
1216
+
1217
+ // extractModeFromState extracts the current mode from state JSON
1218
+ func (m *Manager) extractModeFromState(stateJson string) string {
1219
+ var rawState map[string]interface{}
1220
+ if err := json.Unmarshal([]byte(stateJson), &rawState); err != nil {
1221
+ return m.currentMode // Return current mode if parsing fails
1222
+ }
1223
+
1224
+ if mode, ok := rawState["mode"].(string); ok {
1225
+ return mode
1226
+ }
1227
+
1228
+ return m.currentMode // Return current mode if not found in state
1229
+ }
1230
+
1231
+ // updateMode updates the current mode from state
1232
+ func (m *Manager) updateMode(stateJson string) {
1233
+ mode := m.extractModeFromState(stateJson)
1234
+ m.mu.Lock()
1235
+ m.currentMode = mode
1236
+ m.mu.Unlock()
1237
+ }
1238
+
1239
+ // UpdateTaskAutoApprovalAction enables a specific auto-approval action for the current task
1240
+ func (m *Manager) UpdateTaskAutoApprovalAction(ctx context.Context, actionKey string) error {
1241
+ boolPtr := func(b bool) *bool { return &b }
1242
+
1243
+ settings := &cline.Settings{
1244
+ AutoApprovalSettings: &cline.AutoApprovalSettings{
1245
+ Actions: &cline.AutoApprovalActions{},
1246
+ },
1247
+ }
1248
+
1249
+ // Set the specific action to true based on actionKey
1250
+ truePtr := boolPtr(true)
1251
+
1252
+ switch actionKey {
1253
+ case "read_files":
1254
+ settings.AutoApprovalSettings.Actions.ReadFiles = truePtr
1255
+ case "edit_files":
1256
+ settings.AutoApprovalSettings.Actions.EditFiles = truePtr
1257
+ case "execute_all_commands":
1258
+ settings.AutoApprovalSettings.Actions.ExecuteAllCommands = truePtr
1259
+ case "use_browser":
1260
+ settings.AutoApprovalSettings.Actions.UseBrowser = truePtr
1261
+ case "use_mcp":
1262
+ settings.AutoApprovalSettings.Actions.UseMcp = truePtr
1263
+ default:
1264
+ return fmt.Errorf("unknown auto-approval action: %s", actionKey)
1265
+ }
1266
+
1267
+ _, err := m.client.State.UpdateTaskSettings(ctx, &cline.UpdateTaskSettingsRequest{
1268
+ Settings: settings,
1269
+ })
1270
+ if err != nil {
1271
+ return fmt.Errorf("failed to update task settings: %w", err)
1272
+ }
1273
+
1274
+ return nil
1275
+ }
1276
+
1277
+ // Cleanup cleans up resources
1278
+ func (m *Manager) Cleanup() {
1279
+ // Clean up streaming display resources if needed
1280
+ if m.streamingDisplay != nil {
1281
+ m.streamingDisplay.Cleanup()
1282
+ }
1283
+ }