@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,304 @@
1
+ package display
2
+
3
+ import (
4
+ "fmt"
5
+ "strings"
6
+
7
+ "github.com/charmbracelet/lipgloss"
8
+ "github.com/cline/cli/pkg/cli/global"
9
+ "github.com/cline/cli/pkg/cli/output"
10
+ "github.com/cline/cli/pkg/cli/types"
11
+ "github.com/cline/grpc-go/cline"
12
+ )
13
+
14
+ type Renderer struct {
15
+ typewriter *TypewriterPrinter
16
+ mdRenderer *MarkdownRenderer
17
+ outputFormat string
18
+
19
+ // Lipgloss styles that respect outputFormat
20
+ dimStyle lipgloss.Style
21
+ greenStyle lipgloss.Style
22
+ redStyle lipgloss.Style
23
+ yellowStyle lipgloss.Style
24
+ blueStyle lipgloss.Style
25
+ whiteStyle lipgloss.Style
26
+ boldStyle lipgloss.Style
27
+ successStyle lipgloss.Style
28
+ }
29
+
30
+ func NewRenderer(outputFormat string) *Renderer {
31
+ mdRenderer, err := NewMarkdownRenderer()
32
+ if err != nil {
33
+ mdRenderer = nil
34
+ }
35
+
36
+ r := &Renderer{
37
+ typewriter: NewTypewriterPrinter(DefaultTypewriterConfig()),
38
+ mdRenderer: mdRenderer,
39
+ outputFormat: outputFormat,
40
+ }
41
+
42
+ // Initialize lipgloss styles (will respect the global color profile)
43
+ r.dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
44
+ r.greenStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("2"))
45
+ r.redStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("1"))
46
+ r.yellowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("3"))
47
+ r.blueStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("39"))
48
+ r.whiteStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("7"))
49
+ r.boldStyle = lipgloss.NewStyle().Bold(true)
50
+ r.successStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("2")).Bold(true)
51
+
52
+ return r
53
+ }
54
+
55
+ func (r *Renderer) RenderMessage(prefix, text string, newline bool) error {
56
+ if text == "" {
57
+ return nil
58
+ }
59
+
60
+ clean := r.sanitizeText(text)
61
+ if clean == "" {
62
+ return nil
63
+ }
64
+
65
+ if newline {
66
+ output.Printf("%s: %s\n", prefix, clean)
67
+ } else {
68
+ output.Printf("%s: %s", prefix, clean)
69
+ }
70
+ return nil
71
+ }
72
+
73
+ // formatNumber formats numbers with k/m abbreviations
74
+ func formatNumber(n int) string {
75
+ if n >= 1000000 {
76
+ return fmt.Sprintf("%.1fm", float64(n)/1000000.0)
77
+ } else if n >= 1000 {
78
+ return fmt.Sprintf("%.1fk", float64(n)/1000.0)
79
+ }
80
+ return fmt.Sprintf("%d", n)
81
+ }
82
+
83
+ // formatUsageInfo formats token usage information (extracted from RenderAPI)
84
+ func (r *Renderer) formatUsageInfo(tokensIn, tokensOut, cacheReads, cacheWrites int, cost float64) string {
85
+ parts := make([]string, 0, 4)
86
+
87
+ if tokensIn != 0 {
88
+ parts = append(parts, fmt.Sprintf("↑ %s", formatNumber(tokensIn)))
89
+ }
90
+ if tokensOut != 0 {
91
+ parts = append(parts, fmt.Sprintf("↓ %s", formatNumber(tokensOut)))
92
+ }
93
+ if cacheReads != 0 {
94
+ parts = append(parts, fmt.Sprintf("→ %s", formatNumber(cacheReads)))
95
+ }
96
+ if cacheWrites != 0 {
97
+ parts = append(parts, fmt.Sprintf("← %s", formatNumber(cacheWrites)))
98
+ }
99
+
100
+ if len(parts) == 0 {
101
+ return fmt.Sprintf("$%.4f", cost)
102
+ }
103
+
104
+ return fmt.Sprintf("%s $%.4f", strings.Join(parts, " "), cost)
105
+ }
106
+
107
+
108
+ func (r *Renderer) RenderAPI(status string, apiInfo *types.APIRequestInfo) error {
109
+ if apiInfo.Cost >= 0 {
110
+ usageInfo := r.formatUsageInfo(apiInfo.TokensIn, apiInfo.TokensOut, apiInfo.CacheReads, apiInfo.CacheWrites, apiInfo.Cost)
111
+ markdown := fmt.Sprintf("## API %s `%s`", status, usageInfo)
112
+ rendered := r.RenderMarkdown(markdown)
113
+ output.Print(rendered)
114
+ } else {
115
+ // honestly i see no point in showing "### API processing request" here...
116
+ // markdown := fmt.Sprintf("## API %s", status)
117
+ // rendered := r.RenderMarkdown(markdown)
118
+ // output.Printf("\n%s\n", rendered)
119
+ }
120
+ return nil
121
+ }
122
+
123
+ func (r *Renderer) RenderRetry(attempt, maxAttempts, delaySec int) error {
124
+ message := fmt.Sprintf("Retrying failed attempt %d/%d", attempt, maxAttempts)
125
+ if delaySec > 0 {
126
+ message += fmt.Sprintf(" in %d seconds", delaySec)
127
+ }
128
+ message += "..."
129
+ r.typewriter.PrintMessageLine("API INFO", message)
130
+ return nil
131
+ }
132
+
133
+ func (r *Renderer) RenderTaskCancelled() error {
134
+ markdown := "## Task cancelled"
135
+ rendered := r.RenderMarkdown(markdown)
136
+ output.Printf("\n%s\n", rendered)
137
+ return nil
138
+ }
139
+
140
+ // RenderTaskList displays task history with improved formatting
141
+ func (r *Renderer) RenderTaskList(tasks []*cline.TaskItem) error {
142
+ const maxTasks = 20
143
+
144
+ startIndex := 0
145
+ if len(tasks) > maxTasks {
146
+ startIndex = len(tasks) - maxTasks
147
+ }
148
+
149
+ recentTasks := tasks[startIndex:]
150
+
151
+ r.typewriter.PrintfLn("=== Task History (showing last %d of %d total tasks) ===\n", len(recentTasks), len(tasks))
152
+
153
+ for i, taskItem := range recentTasks {
154
+ r.typewriter.PrintfLn("Task ID: %s", taskItem.Id)
155
+
156
+ description := taskItem.Task
157
+ if len(description) > 1000 {
158
+ description = description[:1000] + "..."
159
+ }
160
+ r.typewriter.PrintfLn("Message: %s", description)
161
+
162
+ usageInfo := r.formatUsageInfo(int(taskItem.TokensIn), int(taskItem.TokensOut), int(taskItem.CacheReads), int(taskItem.CacheWrites), taskItem.TotalCost)
163
+ r.typewriter.PrintfLn("Usage : %s", usageInfo)
164
+
165
+ // Single space between tasks (except last)
166
+ if i < len(recentTasks)-1 {
167
+ r.typewriter.PrintfLn("")
168
+ }
169
+ }
170
+
171
+ return nil
172
+ }
173
+
174
+ func (r *Renderer) RenderDebug(format string, args ...interface{}) error {
175
+ if global.Config.Verbose {
176
+ message := fmt.Sprintf(format, args...)
177
+ r.typewriter.PrintMessageLine("[DEBUG]", message)
178
+ }
179
+ return nil
180
+ }
181
+
182
+ func (r *Renderer) ClearLine() {
183
+ output.Print("\r\033[K")
184
+ }
185
+
186
+ func (r *Renderer) MoveCursorUp(n int) {
187
+ output.Printf("\033[%dA", n)
188
+ }
189
+
190
+ func (r *Renderer) sanitizeText(text string) string {
191
+ text = strings.TrimSpace(text)
192
+
193
+ if text == "" {
194
+ return ""
195
+ }
196
+
197
+ // Remove control characters and escape sequences
198
+ var result strings.Builder
199
+ for _, r := range text {
200
+ // Keep printable characters, spaces, tabs, and newlines
201
+ if r >= 32 || r == '\t' || r == '\n' || r == '\r' {
202
+ result.WriteRune(r)
203
+ }
204
+ // Skip control characters (0-31 except tab, newline, carriage return)
205
+ }
206
+
207
+ return result.String()
208
+ }
209
+
210
+ func (r *Renderer) SetTypewriterEnabled(enabled bool) {
211
+ r.typewriter.SetEnabled(enabled)
212
+ }
213
+
214
+ func (r *Renderer) IsTypewriterEnabled() bool {
215
+ return r.typewriter.IsEnabled()
216
+ }
217
+
218
+ func (r *Renderer) SetTypewriterSpeed(multiplier float64) {
219
+ r.typewriter.SetSpeed(multiplier)
220
+ }
221
+
222
+ func (r *Renderer) GetTypewriter() *TypewriterPrinter {
223
+ return r.typewriter
224
+ }
225
+
226
+ func (r *Renderer) GetMdRenderer() *MarkdownRenderer {
227
+ return r.mdRenderer
228
+ }
229
+
230
+ // RenderMarkdown renders markdown text to terminal format with ANSI codes
231
+ // Falls back to plaintext if markdown rendering is unavailable or fails
232
+ // Respects output format - skips rendering in plain mode or non-TTY contexts
233
+ func (r *Renderer) RenderMarkdown(markdown string) string {
234
+ // Skip markdown rendering if:
235
+ // 1. Output format is explicitly "plain"
236
+ // 2. Not in a TTY (piped output, file redirect, CI, etc.)
237
+ if r.outputFormat == "plain" || !isTTY() {
238
+ return markdown
239
+ }
240
+
241
+ if r.mdRenderer == nil {
242
+ return markdown
243
+ }
244
+
245
+ rendered, err := r.mdRenderer.Render(markdown)
246
+ if err != nil {
247
+ return markdown
248
+ }
249
+
250
+ return rendered
251
+ }
252
+
253
+ // Lipgloss-based color rendering methods
254
+ // These automatically respect the output format via lipgloss color profile
255
+
256
+ // Dim renders text in dim gray (bright black)
257
+ func (r *Renderer) Dim(text string) string {
258
+ return r.dimStyle.Render(text)
259
+ }
260
+
261
+ // Green renders text in green
262
+ func (r *Renderer) Green(text string) string {
263
+ return r.greenStyle.Render(text)
264
+ }
265
+
266
+ // Red renders text in red
267
+ func (r *Renderer) Red(text string) string {
268
+ return r.redStyle.Render(text)
269
+ }
270
+
271
+ // Yellow renders text in yellow
272
+ func (r *Renderer) Yellow(text string) string {
273
+ return r.yellowStyle.Render(text)
274
+ }
275
+
276
+ // Blue renders text in 256-color blue (index 39)
277
+ func (r *Renderer) Blue(text string) string {
278
+ return r.blueStyle.Render(text)
279
+ }
280
+
281
+ // White renders text in white
282
+ func (r *Renderer) White(text string) string {
283
+ return r.whiteStyle.Render(text)
284
+ }
285
+
286
+ // Bold renders text in bold
287
+ func (r *Renderer) Bold(text string) string {
288
+ return r.boldStyle.Render(text)
289
+ }
290
+
291
+ // Success renders text in green with bold
292
+ func (r *Renderer) Success(text string) string {
293
+ return r.successStyle.Render(text)
294
+ }
295
+
296
+ // SuccessWithCheckmark renders text in green with bold and a checkmark prefix
297
+ func (r *Renderer) SuccessWithCheckmark(text string) string {
298
+ return r.Success("✓ " + text)
299
+ }
300
+
301
+ // ErrorWithX renders text in red with an X prefix
302
+ func (r *Renderer) ErrorWithX(text string) string {
303
+ return r.Red("✗ " + text)
304
+ }
@@ -0,0 +1,212 @@
1
+ package display
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "strings"
7
+ "sync"
8
+
9
+ "github.com/cline/cli/pkg/cli/output"
10
+ "github.com/cline/cli/pkg/cli/types"
11
+ )
12
+
13
+ type StreamingSegment struct {
14
+ mu sync.Mutex
15
+ sayType string
16
+ prefix string
17
+ buffer strings.Builder
18
+ frozen bool
19
+ mdRenderer *MarkdownRenderer
20
+ toolRenderer *ToolRenderer
21
+ shouldMarkdown bool
22
+ outputFormat string
23
+ msg *types.ClineMessage
24
+ toolParser *ToolResultParser
25
+ }
26
+
27
+ func NewStreamingSegment(sayType, prefix string, mdRenderer *MarkdownRenderer, shouldMarkdown bool, msg *types.ClineMessage, outputFormat string) *StreamingSegment {
28
+ ss := &StreamingSegment{
29
+ sayType: sayType,
30
+ prefix: prefix,
31
+ mdRenderer: mdRenderer,
32
+ toolRenderer: NewToolRenderer(mdRenderer, outputFormat),
33
+ shouldMarkdown: shouldMarkdown,
34
+ outputFormat: outputFormat,
35
+ msg: msg,
36
+ toolParser: NewToolResultParser(mdRenderer),
37
+ }
38
+
39
+ // Render rich header immediately when creating segment (if in rich mode and TTY)
40
+ if shouldMarkdown && outputFormat != "plain" && isTTY() {
41
+ header := ss.generateRichHeader()
42
+ rendered, _ := mdRenderer.Render(header)
43
+ output.Println("")
44
+ output.Print(rendered)
45
+ }
46
+
47
+ return ss
48
+ }
49
+
50
+ func (ss *StreamingSegment) AppendText(text string) {
51
+ ss.mu.Lock()
52
+ defer ss.mu.Unlock()
53
+
54
+ if ss.frozen {
55
+ return
56
+ }
57
+
58
+ // Replace buffer with FULL text - msg.Text contains complete accumulated content
59
+ ss.buffer.Reset()
60
+ ss.buffer.WriteString(text)
61
+
62
+ // No rendering during streaming - we'll render once on Freeze()
63
+ }
64
+
65
+
66
+ func (ss *StreamingSegment) Freeze() {
67
+ ss.mu.Lock()
68
+ defer ss.mu.Unlock()
69
+
70
+ if ss.frozen {
71
+ return
72
+ }
73
+
74
+ ss.frozen = true
75
+ currentBuffer := ss.buffer.String()
76
+
77
+ // Render and print the final markdown
78
+ ss.renderFinal(currentBuffer)
79
+ }
80
+
81
+ func (ss *StreamingSegment) renderFinal(currentBuffer string) {
82
+ var bodyContent string
83
+
84
+ // Use ToolRenderer for all body rendering to centralize logic
85
+ if ss.sayType == "ask" {
86
+ // Handle ASK messages
87
+ if ss.msg.Ask == string(types.AskTypeTool) {
88
+ // Tool approval: use ToolRenderer for body
89
+ var tool types.ToolMessage
90
+ if err := json.Unmarshal([]byte(currentBuffer), &tool); err == nil {
91
+ // For approval requests in streaming, use the preview method
92
+ bodyContent = ss.toolRenderer.GenerateToolContentPreview(&tool)
93
+ }
94
+ } else if ss.msg.Ask == string(types.AskTypeFollowup) {
95
+ // Followup question: use ToolRenderer
96
+ bodyContent = ss.toolRenderer.GenerateAskFollowupBody(currentBuffer)
97
+ } else if ss.msg.Ask == string(types.AskTypePlanModeRespond) {
98
+ // Plan mode respond: use ToolRenderer
99
+ bodyContent = ss.toolRenderer.GeneratePlanModeRespondBody(currentBuffer)
100
+ } else if ss.msg.Ask == string(types.AskTypeCommand) {
101
+ // Command approval: no body needed - header shows command, output shown separately later
102
+ bodyContent = ""
103
+ } else {
104
+ // For other ask types, render as-is
105
+ bodyContent = currentBuffer
106
+ }
107
+ } else if ss.sayType == string(types.SayTypeTool) {
108
+ // Tool execution (SAY): use ToolRenderer for body
109
+ var tool types.ToolMessage
110
+ if err := json.Unmarshal([]byte(currentBuffer), &tool); err == nil {
111
+ bodyContent = ss.toolRenderer.GenerateToolContentBody(&tool)
112
+ }
113
+ } else if ss.sayType == string(types.SayTypeCommand) {
114
+ // Command output
115
+ bodyContent = "```shell\n" + currentBuffer + "\n```"
116
+ // Render markdown only in rich mode and TTY
117
+ if ss.shouldMarkdown && ss.outputFormat != "plain" && isTTY() {
118
+ rendered, err := ss.mdRenderer.Render(bodyContent)
119
+ if err == nil {
120
+ bodyContent = rendered
121
+ }
122
+ }
123
+ } else {
124
+ // For other types (reasoning, text, etc.), render markdown as-is
125
+ if ss.shouldMarkdown && ss.outputFormat != "plain" && isTTY() {
126
+ rendered, err := ss.mdRenderer.Render(currentBuffer)
127
+ if err == nil {
128
+ bodyContent = rendered
129
+ } else {
130
+ bodyContent = currentBuffer
131
+ }
132
+ } else {
133
+ bodyContent = currentBuffer
134
+ }
135
+ }
136
+
137
+ // Print the body content
138
+ if bodyContent != "" {
139
+ if !strings.HasSuffix(bodyContent, "\n") {
140
+ output.Print(bodyContent)
141
+ output.Println("")
142
+ } else {
143
+ output.Print(bodyContent)
144
+ }
145
+ }
146
+ }
147
+
148
+
149
+ // generateRichHeader generates a contextual header for the segment
150
+ func (ss *StreamingSegment) generateRichHeader() string {
151
+ switch ss.sayType {
152
+ case string(types.SayTypeReasoning):
153
+ return "### Cline is thinking\n"
154
+
155
+ case string(types.SayTypeText):
156
+ return "### Cline responds\n"
157
+
158
+ case string(types.SayTypeCompletionResult):
159
+ return "### Task completed\n"
160
+
161
+ case string(types.SayTypeTool):
162
+ return ss.generateToolHeader()
163
+
164
+ case "ask":
165
+ // Check the specific ask type
166
+ if ss.msg.Ask == string(types.AskTypePlanModeRespond) {
167
+ return ss.toolRenderer.GeneratePlanModeRespondHeader()
168
+ }
169
+
170
+ // For tool approvals, show proper tool header
171
+ if ss.msg.Ask == string(types.AskTypeTool) {
172
+ var tool types.ToolMessage
173
+ if err := json.Unmarshal([]byte(ss.msg.Text), &tool); err == nil {
174
+ // Use ToolRenderer for approval header with "wants to" verbs
175
+ return ss.toolRenderer.RenderToolApprovalHeader(&tool)
176
+ }
177
+ }
178
+
179
+ // For command approvals, show command header
180
+ if ss.msg.Ask == string(types.AskTypeCommand) {
181
+ command := strings.TrimSpace(ss.msg.Text)
182
+ if strings.HasSuffix(command, "REQ_APP") {
183
+ command = strings.TrimSuffix(command, "REQ_APP")
184
+ command = strings.TrimSpace(command)
185
+ }
186
+ return fmt.Sprintf("### Cline wants to run `%s`\n", command)
187
+ }
188
+
189
+ // For followup questions, show question header
190
+ if ss.msg.Ask == string(types.AskTypeFollowup) {
191
+ return ss.toolRenderer.GenerateAskFollowupHeader()
192
+ }
193
+
194
+ // For other ask types, show generic message
195
+ return fmt.Sprintf("### Cline is asking (%s)\n", ss.msg.Ask)
196
+
197
+ default:
198
+ return fmt.Sprintf("### %s\n", ss.prefix)
199
+ }
200
+ }
201
+
202
+ // generateToolHeader generates a contextual header for tool operations
203
+ func (ss *StreamingSegment) generateToolHeader() string {
204
+ // Parse tool JSON from message text
205
+ var tool types.ToolMessage
206
+ if err := json.Unmarshal([]byte(ss.msg.Text), &tool); err != nil {
207
+ return "### Tool operation\n"
208
+ }
209
+
210
+ // Use unified ToolRenderer for header
211
+ return ss.toolRenderer.RenderToolExecutionHeader(&tool)
212
+ }
@@ -0,0 +1,134 @@
1
+ package display
2
+
3
+ import (
4
+ "fmt"
5
+ "strings"
6
+ "sync"
7
+
8
+ "github.com/cline/cli/pkg/cli/types"
9
+ )
10
+
11
+ // StreamingDisplay manages streaming message display with deduplication
12
+ type StreamingDisplay struct {
13
+ mu sync.RWMutex
14
+ state *types.ConversationState
15
+ renderer *Renderer
16
+ dedupe *MessageDeduplicator
17
+ activeSegment *StreamingSegment
18
+ mdRenderer *MarkdownRenderer
19
+ }
20
+
21
+ // NewStreamingDisplay creates a new streaming display manager
22
+ func NewStreamingDisplay(state *types.ConversationState, renderer *Renderer) *StreamingDisplay {
23
+ mdRenderer, err := NewMarkdownRenderer()
24
+ if err != nil {
25
+ panic(fmt.Sprintf("Failed to initialize markdown renderer: %v", err))
26
+ }
27
+
28
+ return &StreamingDisplay{
29
+ state: state,
30
+ renderer: renderer,
31
+ dedupe: NewMessageDeduplicator(),
32
+ mdRenderer: mdRenderer,
33
+ }
34
+ }
35
+
36
+ // HandlePartialMessage processes partial messages with streaming support
37
+ func (s *StreamingDisplay) HandlePartialMessage(msg *types.ClineMessage) error {
38
+ s.mu.Lock()
39
+ defer s.mu.Unlock()
40
+
41
+ // Check for deduplication
42
+ if s.dedupe.IsDuplicate(msg) {
43
+ return nil
44
+ }
45
+
46
+ // Segment-based header-only streaming
47
+ // Partial stream only shows headers immediately, state stream will handle content bodies
48
+ sayType := msg.Say
49
+ if msg.Type == types.MessageTypeAsk {
50
+ sayType = "ask"
51
+ }
52
+
53
+ // Detect segment boundary
54
+ if s.activeSegment != nil && s.activeSegment.sayType != sayType {
55
+ // Just cleanup, don't freeze (no body to print)
56
+ s.activeSegment = nil
57
+ }
58
+
59
+ // On first partial message for a new segment type, create segment (prints header)
60
+ if s.activeSegment == nil && msg.Partial {
61
+ shouldMd := s.shouldRenderMarkdown(sayType)
62
+ prefix := s.getPrefix(sayType)
63
+ // NewStreamingSegment prints the header immediately
64
+ s.activeSegment = NewStreamingSegment(sayType, prefix, s.mdRenderer, shouldMd, msg, s.renderer.outputFormat)
65
+ // Header printed, done - don't append text or freeze
66
+ return nil
67
+ }
68
+
69
+ // For subsequent partial messages, do nothing (header already shown)
70
+ if msg.Partial {
71
+ return nil
72
+ }
73
+
74
+ // When message is complete (partial=false), render the content body
75
+ if s.activeSegment != nil {
76
+ // Had an active segment from partial messages - freeze to render body
77
+ s.activeSegment.AppendText(msg.Text)
78
+ s.activeSegment.Freeze()
79
+ s.activeSegment = nil
80
+ } else if !msg.Partial {
81
+ // Message arrived complete without partial phase - create segment and render immediately
82
+ shouldMd := s.shouldRenderMarkdown(sayType)
83
+ prefix := s.getPrefix(sayType)
84
+ segment := NewStreamingSegment(sayType, prefix, s.mdRenderer, shouldMd, msg, s.renderer.outputFormat)
85
+ segment.AppendText(msg.Text)
86
+ segment.Freeze()
87
+ }
88
+
89
+ return nil
90
+ }
91
+
92
+ func (s *StreamingDisplay) shouldRenderMarkdown(sayType string) bool {
93
+ switch sayType {
94
+ case string(types.SayTypeReasoning), string(types.SayTypeText), string(types.SayTypeCompletionResult), string(types.SayTypeTool), "ask":
95
+ return true
96
+ default:
97
+ return false
98
+ }
99
+ }
100
+
101
+ func (s *StreamingDisplay) getPrefix(sayType string) string {
102
+ switch sayType {
103
+ case string(types.SayTypeReasoning):
104
+ return "THINKING"
105
+ case string(types.SayTypeText):
106
+ return "CLINE"
107
+ case string(types.SayTypeCompletionResult):
108
+ return "RESULT"
109
+ case "ask":
110
+ return "ASK"
111
+ case string(types.SayTypeCommand):
112
+ return "TERMINAL"
113
+ default:
114
+ return strings.ToUpper(sayType)
115
+ }
116
+ }
117
+
118
+ func (s *StreamingDisplay) FreezeActiveSegment() {
119
+ s.mu.Lock()
120
+ defer s.mu.Unlock()
121
+
122
+ if s.activeSegment != nil {
123
+ s.activeSegment.Freeze()
124
+ s.activeSegment = nil
125
+ }
126
+ }
127
+
128
+ // Cleanup cleans up streaming display resources
129
+ func (s *StreamingDisplay) Cleanup() {
130
+ s.FreezeActiveSegment()
131
+ if s.dedupe != nil {
132
+ s.dedupe.Stop()
133
+ }
134
+ }