@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.
- package/.npmrc.tmp +2 -0
- package/README.md +72 -0
- package/cmd/cline/main.go +348 -0
- package/cmd/cline-host/main.go +71 -0
- package/e2e/default_update_test.go +154 -0
- package/e2e/helpers_test.go +378 -0
- package/e2e/main_test.go +47 -0
- package/e2e/mixed_stress_test.go +120 -0
- package/e2e/sqlite_helper.go +161 -0
- package/e2e/start_list_test.go +178 -0
- package/go.mod +64 -0
- package/go.sum +162 -0
- package/man/cline.1 +331 -0
- package/man/cline.1.md +332 -0
- package/package.json +54 -0
- package/pkg/cli/auth/auth_cline_provider.go +285 -0
- package/pkg/cli/auth/auth_menu.go +323 -0
- package/pkg/cli/auth/auth_subscription.go +130 -0
- package/pkg/cli/auth/byo_quick_setup.go +247 -0
- package/pkg/cli/auth/models_cline.go +141 -0
- package/pkg/cli/auth/models_list_fetch.go +156 -0
- package/pkg/cli/auth/models_list_static.go +69 -0
- package/pkg/cli/auth/providers_byo.go +184 -0
- package/pkg/cli/auth/providers_list.go +517 -0
- package/pkg/cli/auth/update_api_configurations.go +647 -0
- package/pkg/cli/auth/wizard_byo.go +764 -0
- package/pkg/cli/auth/wizard_byo_bedrock.go +193 -0
- package/pkg/cli/auth/wizard_byo_oca.go +366 -0
- package/pkg/cli/auth.go +43 -0
- package/pkg/cli/clerror/cline_error.go +187 -0
- package/pkg/cli/config/manager.go +208 -0
- package/pkg/cli/config/settings_renderer.go +198 -0
- package/pkg/cli/config.go +152 -0
- package/pkg/cli/display/ansi.go +27 -0
- package/pkg/cli/display/banner.go +211 -0
- package/pkg/cli/display/deduplicator.go +95 -0
- package/pkg/cli/display/markdown_renderer.go +139 -0
- package/pkg/cli/display/renderer.go +304 -0
- package/pkg/cli/display/segment_streamer.go +212 -0
- package/pkg/cli/display/streaming.go +134 -0
- package/pkg/cli/display/system_renderer.go +269 -0
- package/pkg/cli/display/tool_renderer.go +455 -0
- package/pkg/cli/display/tool_result_parser.go +371 -0
- package/pkg/cli/display/typewriter.go +210 -0
- package/pkg/cli/doctor.go +65 -0
- package/pkg/cli/global/cline-clients.go +501 -0
- package/pkg/cli/global/global.go +113 -0
- package/pkg/cli/global/registry.go +304 -0
- package/pkg/cli/handlers/ask_handlers.go +339 -0
- package/pkg/cli/handlers/handler.go +130 -0
- package/pkg/cli/handlers/say_handlers.go +521 -0
- package/pkg/cli/instances.go +506 -0
- package/pkg/cli/logs.go +382 -0
- package/pkg/cli/output/coordinator.go +167 -0
- package/pkg/cli/output/input_model.go +497 -0
- package/pkg/cli/sqlite/locks.go +366 -0
- package/pkg/cli/task/history_handler.go +72 -0
- package/pkg/cli/task/input_handler.go +577 -0
- package/pkg/cli/task/manager.go +1283 -0
- package/pkg/cli/task/settings_parser.go +754 -0
- package/pkg/cli/task/stream_coordinator.go +60 -0
- package/pkg/cli/task.go +675 -0
- package/pkg/cli/terminal/keyboard.go +695 -0
- package/pkg/cli/tui/HELP_WANTED.md +1 -0
- package/pkg/cli/types/history.go +17 -0
- package/pkg/cli/types/messages.go +329 -0
- package/pkg/cli/types/state.go +59 -0
- package/pkg/cli/updater/updater.go +409 -0
- package/pkg/cli/version.go +43 -0
- package/pkg/common/constants.go +6 -0
- package/pkg/common/schema.go +54 -0
- package/pkg/common/types.go +54 -0
- package/pkg/common/utils.go +185 -0
- package/pkg/generated/field_overrides.go +39 -0
- package/pkg/generated/providers.go +1584 -0
- package/pkg/hostbridge/diff.go +351 -0
- package/pkg/hostbridge/disabled/watch.go +39 -0
- package/pkg/hostbridge/disabled/window.go +63 -0
- package/pkg/hostbridge/disabled/workspace.go +66 -0
- package/pkg/hostbridge/env.go +166 -0
- package/pkg/hostbridge/grpc_server.go +113 -0
- package/pkg/hostbridge/simple.go +43 -0
- package/pkg/hostbridge/simple_workspace.go +85 -0
- package/pkg/hostbridge/window.go +129 -0
- 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
|
+
}
|