@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,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
|
+
}
|