@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,497 @@
|
|
|
1
|
+
package output
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
"os/exec"
|
|
7
|
+
"strings"
|
|
8
|
+
|
|
9
|
+
"github.com/charmbracelet/bubbles/textarea"
|
|
10
|
+
tea "github.com/charmbracelet/bubbletea"
|
|
11
|
+
"github.com/charmbracelet/lipgloss"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// InputType represents the type of input being collected
|
|
15
|
+
type InputType int
|
|
16
|
+
|
|
17
|
+
const INPUT_WIDTH = 46
|
|
18
|
+
|
|
19
|
+
const (
|
|
20
|
+
InputTypeMessage InputType = iota
|
|
21
|
+
InputTypeApproval
|
|
22
|
+
InputTypeFeedback
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
// InputSubmitMsg is sent when the user submits input
|
|
26
|
+
type InputSubmitMsg struct {
|
|
27
|
+
Value string
|
|
28
|
+
InputType InputType
|
|
29
|
+
Approved bool // For approval type
|
|
30
|
+
NeedsFeedback bool // For approval type
|
|
31
|
+
NoAskAgain bool // For approval type - indicates "don't ask again" was selected
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// InputCancelMsg is sent when the user cancels input (Ctrl+C)
|
|
35
|
+
type InputCancelMsg struct{}
|
|
36
|
+
|
|
37
|
+
// ChangeInputTypeMsg changes the current input type
|
|
38
|
+
type ChangeInputTypeMsg struct {
|
|
39
|
+
InputType InputType
|
|
40
|
+
Title string
|
|
41
|
+
Placeholder string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// editorFinishedMsg is sent when the external editor finishes
|
|
45
|
+
type editorFinishedMsg struct {
|
|
46
|
+
content []byte
|
|
47
|
+
err error
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// InputModel is the bubbletea model for interactive input
|
|
51
|
+
type InputModel struct {
|
|
52
|
+
textarea textarea.Model
|
|
53
|
+
suspended bool
|
|
54
|
+
savedValue string
|
|
55
|
+
inputType InputType
|
|
56
|
+
title string
|
|
57
|
+
placeholder string
|
|
58
|
+
currentMode string // "plan" or "act"
|
|
59
|
+
width int
|
|
60
|
+
lastHeight int // Track height for cleanup on submit
|
|
61
|
+
|
|
62
|
+
// For approval type
|
|
63
|
+
approvalOptions []string
|
|
64
|
+
selectedOption int
|
|
65
|
+
pendingApproval bool // Stores approval decision when transitioning to feedback input
|
|
66
|
+
|
|
67
|
+
// Styles (huh-inspired theme)
|
|
68
|
+
styles fieldStyles
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// fieldStyles holds the styling for the input field
|
|
72
|
+
type fieldStyles struct {
|
|
73
|
+
base lipgloss.Style
|
|
74
|
+
title lipgloss.Style
|
|
75
|
+
textArea lipgloss.Style
|
|
76
|
+
cursor lipgloss.Style
|
|
77
|
+
placeholder lipgloss.Style
|
|
78
|
+
selector lipgloss.Style
|
|
79
|
+
selectedOption lipgloss.Style
|
|
80
|
+
option lipgloss.Style
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// newFieldStyles creates huh-inspired styles (Charm theme)
|
|
84
|
+
func newFieldStyles() fieldStyles {
|
|
85
|
+
// Charm theme colors
|
|
86
|
+
indigo := lipgloss.AdaptiveColor{Light: "#5A56E0", Dark: "#7571F9"}
|
|
87
|
+
fuchsia := lipgloss.Color("#F780E2")
|
|
88
|
+
normalFg := lipgloss.AdaptiveColor{Light: "235", Dark: "252"}
|
|
89
|
+
green := lipgloss.AdaptiveColor{Light: "#02BA84", Dark: "#02BF87"}
|
|
90
|
+
|
|
91
|
+
return fieldStyles{
|
|
92
|
+
base: lipgloss.NewStyle().
|
|
93
|
+
PaddingLeft(1).
|
|
94
|
+
BorderStyle(lipgloss.ThickBorder()).
|
|
95
|
+
BorderLeft(true).
|
|
96
|
+
BorderForeground(lipgloss.Color("238")),
|
|
97
|
+
title: lipgloss.NewStyle().
|
|
98
|
+
Foreground(indigo).
|
|
99
|
+
Bold(true),
|
|
100
|
+
textArea: lipgloss.NewStyle().
|
|
101
|
+
Foreground(normalFg),
|
|
102
|
+
cursor: lipgloss.NewStyle().
|
|
103
|
+
Foreground(green),
|
|
104
|
+
placeholder: lipgloss.NewStyle().
|
|
105
|
+
Foreground(lipgloss.AdaptiveColor{Light: "248", Dark: "238"}),
|
|
106
|
+
selector: lipgloss.NewStyle().
|
|
107
|
+
Foreground(fuchsia).
|
|
108
|
+
SetString("> "),
|
|
109
|
+
selectedOption: lipgloss.NewStyle().
|
|
110
|
+
Foreground(normalFg),
|
|
111
|
+
option: lipgloss.NewStyle().
|
|
112
|
+
Foreground(normalFg),
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// NewInputModel creates a new input model
|
|
117
|
+
func NewInputModel(inputType InputType, title, placeholder, currentMode string) InputModel {
|
|
118
|
+
ta := textarea.New()
|
|
119
|
+
ta.Placeholder = placeholder
|
|
120
|
+
ta.Focus()
|
|
121
|
+
ta.CharLimit = 0
|
|
122
|
+
ta.ShowLineNumbers = false
|
|
123
|
+
ta.Prompt = "" // Remove prompt prefix (this is what adds the inner border!)
|
|
124
|
+
ta.SetHeight(5)
|
|
125
|
+
// Don't set width here - let WindowSizeMsg handle it
|
|
126
|
+
ta.SetWidth(INPUT_WIDTH)
|
|
127
|
+
|
|
128
|
+
// Configure keybindings like huh does:
|
|
129
|
+
// alt+enter and ctrl+j for newlines (textarea will handle these)
|
|
130
|
+
ta.KeyMap.InsertNewline.SetKeys("alt+enter", "ctrl+j")
|
|
131
|
+
|
|
132
|
+
// Apply huh-like styling
|
|
133
|
+
styles := newFieldStyles()
|
|
134
|
+
|
|
135
|
+
// Set cursor color based on mode
|
|
136
|
+
cursorColor := lipgloss.Color("3") // Yellow for plan
|
|
137
|
+
if currentMode == "act" {
|
|
138
|
+
cursorColor = lipgloss.Color("39") // Blue for act
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
ta.FocusedStyle.CursorLine = lipgloss.NewStyle() // No cursor line highlighting
|
|
142
|
+
ta.FocusedStyle.EndOfBuffer = lipgloss.NewStyle() // No end-of-buffer styling
|
|
143
|
+
ta.FocusedStyle.Placeholder = styles.placeholder
|
|
144
|
+
ta.FocusedStyle.Text = styles.textArea
|
|
145
|
+
ta.FocusedStyle.Prompt = lipgloss.NewStyle() // No prompt styling
|
|
146
|
+
ta.Cursor.Style = lipgloss.NewStyle().Foreground(cursorColor)
|
|
147
|
+
ta.Cursor.TextStyle = styles.textArea
|
|
148
|
+
|
|
149
|
+
m := InputModel{
|
|
150
|
+
textarea: ta,
|
|
151
|
+
inputType: inputType,
|
|
152
|
+
title: title,
|
|
153
|
+
placeholder: placeholder,
|
|
154
|
+
currentMode: currentMode,
|
|
155
|
+
width: 0, // Will be set by first WindowSizeMsg
|
|
156
|
+
styles: styles,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// For approval type, set up options
|
|
160
|
+
if inputType == InputTypeApproval {
|
|
161
|
+
m.approvalOptions = []string{
|
|
162
|
+
"Yes",
|
|
163
|
+
"Yes, and don't ask again for this task",
|
|
164
|
+
"No, with feedback",
|
|
165
|
+
}
|
|
166
|
+
m.selectedOption = 0
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return m
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Init initializes the model
|
|
173
|
+
func (m *InputModel) Init() tea.Cmd {
|
|
174
|
+
return textarea.Blink
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Update handles messages
|
|
178
|
+
func (m *InputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
179
|
+
var cmd tea.Cmd
|
|
180
|
+
|
|
181
|
+
switch msg := msg.(type) {
|
|
182
|
+
case editorFinishedMsg:
|
|
183
|
+
// External editor finished
|
|
184
|
+
if msg.err == nil && len(msg.content) > 0 {
|
|
185
|
+
m.textarea.SetValue(string(msg.content))
|
|
186
|
+
}
|
|
187
|
+
return m, nil
|
|
188
|
+
|
|
189
|
+
case SuspendInputMsg:
|
|
190
|
+
// Save current value and suspend
|
|
191
|
+
m.savedValue = m.textarea.Value()
|
|
192
|
+
m.suspended = true
|
|
193
|
+
return m, tea.ClearScreen
|
|
194
|
+
|
|
195
|
+
case ResumeInputMsg:
|
|
196
|
+
// Restore value and resume
|
|
197
|
+
m.textarea.SetValue(m.savedValue)
|
|
198
|
+
m.suspended = false
|
|
199
|
+
return m, nil
|
|
200
|
+
|
|
201
|
+
case ChangeInputTypeMsg:
|
|
202
|
+
// Change input type (e.g., from approval to feedback)
|
|
203
|
+
m.inputType = msg.InputType
|
|
204
|
+
m.title = msg.Title
|
|
205
|
+
m.placeholder = msg.Placeholder
|
|
206
|
+
m.textarea.Placeholder = msg.Placeholder
|
|
207
|
+
m.textarea.SetValue("")
|
|
208
|
+
m.textarea.Focus()
|
|
209
|
+
|
|
210
|
+
if msg.InputType == InputTypeApproval {
|
|
211
|
+
m.approvalOptions = []string{
|
|
212
|
+
"Yes",
|
|
213
|
+
"Yes, and don't ask again for this task",
|
|
214
|
+
"No, with feedback",
|
|
215
|
+
}
|
|
216
|
+
m.selectedOption = 0
|
|
217
|
+
}
|
|
218
|
+
return m, nil
|
|
219
|
+
|
|
220
|
+
default:
|
|
221
|
+
// Forward all other messages to textarea (including blink ticks)
|
|
222
|
+
if !m.suspended && (m.inputType == InputTypeMessage || m.inputType == InputTypeFeedback) {
|
|
223
|
+
m.textarea, cmd = m.textarea.Update(msg)
|
|
224
|
+
return m, cmd
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
case tea.KeyMsg:
|
|
228
|
+
if m.suspended {
|
|
229
|
+
return m, nil
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Handle keys for text input types (Message/Feedback)
|
|
233
|
+
if m.inputType == InputTypeMessage || m.inputType == InputTypeFeedback {
|
|
234
|
+
switch msg.String() {
|
|
235
|
+
case "ctrl+c":
|
|
236
|
+
return m, func() tea.Msg { return InputCancelMsg{} }
|
|
237
|
+
|
|
238
|
+
case "ctrl+e":
|
|
239
|
+
// Open external editor (like huh does)
|
|
240
|
+
return m, m.openEditor()
|
|
241
|
+
|
|
242
|
+
case "enter":
|
|
243
|
+
// Intercept enter for submit (textarea handles alt+enter and ctrl+j for newlines)
|
|
244
|
+
return m.handleSubmit()
|
|
245
|
+
|
|
246
|
+
case "up", "down", "left", "right":
|
|
247
|
+
// Let textarea handle navigation
|
|
248
|
+
m.textarea, cmd = m.textarea.Update(msg)
|
|
249
|
+
return m, cmd
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Pass all other keys to textarea (including alt+enter, ctrl+j for newlines)
|
|
253
|
+
m.textarea, cmd = m.textarea.Update(msg)
|
|
254
|
+
return m, cmd
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Handle keys for approval type
|
|
258
|
+
if m.inputType == InputTypeApproval {
|
|
259
|
+
switch msg.String() {
|
|
260
|
+
case "ctrl+c":
|
|
261
|
+
return m, func() tea.Msg { return InputCancelMsg{} }
|
|
262
|
+
|
|
263
|
+
case "enter":
|
|
264
|
+
return m.handleSubmit()
|
|
265
|
+
|
|
266
|
+
case "up":
|
|
267
|
+
if m.selectedOption > 0 {
|
|
268
|
+
m.selectedOption--
|
|
269
|
+
}
|
|
270
|
+
return m, nil
|
|
271
|
+
|
|
272
|
+
case "down":
|
|
273
|
+
if m.selectedOption < len(m.approvalOptions)-1 {
|
|
274
|
+
m.selectedOption++
|
|
275
|
+
}
|
|
276
|
+
return m, nil
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return m, nil
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// handleSubmit handles submission based on input type
|
|
285
|
+
func (m *InputModel) handleSubmit() (tea.Model, tea.Cmd) {
|
|
286
|
+
switch m.inputType {
|
|
287
|
+
case InputTypeMessage:
|
|
288
|
+
value := strings.TrimSpace(m.textarea.Value())
|
|
289
|
+
return m, func() tea.Msg {
|
|
290
|
+
return InputSubmitMsg{
|
|
291
|
+
Value: value,
|
|
292
|
+
InputType: InputTypeMessage,
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
case InputTypeApproval:
|
|
297
|
+
selected := m.approvalOptions[m.selectedOption]
|
|
298
|
+
approved := strings.HasPrefix(selected, "Yes")
|
|
299
|
+
needsFeedback := strings.Contains(selected, "feedback")
|
|
300
|
+
noAskAgain := strings.Contains(selected, "don't ask again")
|
|
301
|
+
|
|
302
|
+
if needsFeedback {
|
|
303
|
+
// Store the approval decision before switching to feedback input
|
|
304
|
+
m.pendingApproval = approved
|
|
305
|
+
// Switch to feedback input
|
|
306
|
+
return m, func() tea.Msg {
|
|
307
|
+
return ChangeInputTypeMsg{
|
|
308
|
+
InputType: InputTypeFeedback,
|
|
309
|
+
Title: "Your feedback",
|
|
310
|
+
Placeholder: "/plan or /act to switch modes\nctrl+e to open editor",
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return m, func() tea.Msg {
|
|
316
|
+
return InputSubmitMsg{
|
|
317
|
+
Value: "",
|
|
318
|
+
InputType: InputTypeApproval,
|
|
319
|
+
Approved: approved,
|
|
320
|
+
NeedsFeedback: false,
|
|
321
|
+
NoAskAgain: noAskAgain,
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
case InputTypeFeedback:
|
|
326
|
+
value := strings.TrimSpace(m.textarea.Value())
|
|
327
|
+
return m, func() tea.Msg {
|
|
328
|
+
return InputSubmitMsg{
|
|
329
|
+
Value: value,
|
|
330
|
+
InputType: InputTypeFeedback,
|
|
331
|
+
Approved: m.pendingApproval, // Pass the stored approval decision
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return m, nil
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// View renders the model
|
|
340
|
+
func (m *InputModel) View() string {
|
|
341
|
+
if m.suspended {
|
|
342
|
+
return ""
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
var parts []string
|
|
346
|
+
|
|
347
|
+
// Render title with mode indicator
|
|
348
|
+
yellow := lipgloss.Color("3")
|
|
349
|
+
blue := lipgloss.Color("39")
|
|
350
|
+
|
|
351
|
+
modeStyle := lipgloss.NewStyle().Bold(true)
|
|
352
|
+
if m.currentMode == "plan" {
|
|
353
|
+
modeStyle = modeStyle.Foreground(yellow)
|
|
354
|
+
} else {
|
|
355
|
+
modeStyle = modeStyle.Foreground(blue)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
modeIndicator := modeStyle.Render(fmt.Sprintf("[%s mode]", m.currentMode))
|
|
359
|
+
titleText := m.styles.title.Render(m.title)
|
|
360
|
+
fullTitle := fmt.Sprintf("%s %s", modeIndicator, titleText)
|
|
361
|
+
parts = append(parts, fullTitle)
|
|
362
|
+
|
|
363
|
+
// Render based on input type
|
|
364
|
+
switch m.inputType {
|
|
365
|
+
case InputTypeMessage, InputTypeFeedback:
|
|
366
|
+
parts = append(parts, m.textarea.View())
|
|
367
|
+
|
|
368
|
+
case InputTypeApproval:
|
|
369
|
+
var options []string
|
|
370
|
+
for i, option := range m.approvalOptions {
|
|
371
|
+
if i == m.selectedOption {
|
|
372
|
+
options = append(options, m.styles.selector.Render("")+m.styles.selectedOption.Render(option))
|
|
373
|
+
} else {
|
|
374
|
+
options = append(options, " "+m.styles.option.Render(option))
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
parts = append(parts, strings.Join(options, "\n"))
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Wrap everything in the base style with border
|
|
381
|
+
content := strings.Join(parts, "\n")
|
|
382
|
+
rendered := m.styles.base.Render(content)
|
|
383
|
+
|
|
384
|
+
// Add newline before the form (outside the border)
|
|
385
|
+
rendered = "\n" + rendered
|
|
386
|
+
|
|
387
|
+
// Track height for cleanup
|
|
388
|
+
m.lastHeight = lipgloss.Height(rendered)
|
|
389
|
+
|
|
390
|
+
return rendered
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ClearScreen returns the ANSI codes to clear the input from the terminal
|
|
394
|
+
// This is used when submitting to remove the form cleanly
|
|
395
|
+
func (m *InputModel) ClearScreen() string {
|
|
396
|
+
if m.lastHeight == 0 {
|
|
397
|
+
return ""
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Move cursor up by lastHeight lines and clear from cursor to end of screen
|
|
401
|
+
return fmt.Sprintf("\033[%dA\033[J", m.lastHeight)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Clone creates a deep copy of the InputModel with all state preserved
|
|
405
|
+
func (m *InputModel) Clone() *InputModel {
|
|
406
|
+
// Create new textarea with same configuration
|
|
407
|
+
ta := textarea.New()
|
|
408
|
+
ta.SetValue(m.textarea.Value())
|
|
409
|
+
ta.Placeholder = m.placeholder
|
|
410
|
+
ta.CharLimit = 0
|
|
411
|
+
ta.ShowLineNumbers = false
|
|
412
|
+
ta.Prompt = ""
|
|
413
|
+
ta.SetHeight(5)
|
|
414
|
+
ta.SetWidth(INPUT_WIDTH)
|
|
415
|
+
ta.Focus()
|
|
416
|
+
|
|
417
|
+
// Configure keybindings
|
|
418
|
+
ta.KeyMap.InsertNewline.SetKeys("alt+enter", "ctrl+j")
|
|
419
|
+
|
|
420
|
+
// Apply styles (including mode-based cursor color)
|
|
421
|
+
cursorColor := lipgloss.Color("3") // Yellow for plan
|
|
422
|
+
if m.currentMode == "act" {
|
|
423
|
+
cursorColor = lipgloss.Color("39") // Blue for act
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
ta.FocusedStyle.CursorLine = lipgloss.NewStyle()
|
|
427
|
+
ta.FocusedStyle.EndOfBuffer = lipgloss.NewStyle()
|
|
428
|
+
ta.FocusedStyle.Placeholder = m.styles.placeholder
|
|
429
|
+
ta.FocusedStyle.Text = m.styles.textArea
|
|
430
|
+
ta.FocusedStyle.Prompt = lipgloss.NewStyle()
|
|
431
|
+
ta.Cursor.Style = lipgloss.NewStyle().Foreground(cursorColor)
|
|
432
|
+
ta.Cursor.TextStyle = m.styles.textArea
|
|
433
|
+
|
|
434
|
+
// Create cloned model
|
|
435
|
+
clone := &InputModel{
|
|
436
|
+
textarea: ta,
|
|
437
|
+
suspended: false, // New program starts unsuspended
|
|
438
|
+
savedValue: m.savedValue,
|
|
439
|
+
inputType: m.inputType,
|
|
440
|
+
title: m.title,
|
|
441
|
+
placeholder: m.placeholder,
|
|
442
|
+
currentMode: m.currentMode,
|
|
443
|
+
width: m.width,
|
|
444
|
+
lastHeight: m.lastHeight,
|
|
445
|
+
approvalOptions: m.approvalOptions,
|
|
446
|
+
selectedOption: m.selectedOption,
|
|
447
|
+
pendingApproval: m.pendingApproval, // Preserve approval decision
|
|
448
|
+
styles: m.styles,
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return clone
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// openEditor opens an external editor for composing the message
|
|
455
|
+
func (m *InputModel) openEditor() tea.Cmd {
|
|
456
|
+
// Get editor from environment or use nano as default
|
|
457
|
+
editorCmd := "nano"
|
|
458
|
+
editorArgs := []string{}
|
|
459
|
+
|
|
460
|
+
if editor := os.Getenv("EDITOR"); editor != "" {
|
|
461
|
+
editorFields := strings.Fields(editor)
|
|
462
|
+
if len(editorFields) > 0 {
|
|
463
|
+
editorCmd = editorFields[0]
|
|
464
|
+
if len(editorFields) > 1 {
|
|
465
|
+
editorArgs = editorFields[1:]
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Create temp file with current content
|
|
471
|
+
tmpFile, err := os.CreateTemp(os.TempDir(), "*.md")
|
|
472
|
+
if err != nil {
|
|
473
|
+
return func() tea.Msg {
|
|
474
|
+
return editorFinishedMsg{err: err}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Write current textarea value to temp file
|
|
479
|
+
if err := os.WriteFile(tmpFile.Name(), []byte(m.textarea.Value()), 0o644); err != nil {
|
|
480
|
+
return func() tea.Msg {
|
|
481
|
+
return editorFinishedMsg{err: err}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Open the editor
|
|
486
|
+
cmd := exec.Command(editorCmd, append(editorArgs, tmpFile.Name())...)
|
|
487
|
+
return tea.ExecProcess(cmd, func(err error) tea.Msg {
|
|
488
|
+
content, readErr := os.ReadFile(tmpFile.Name())
|
|
489
|
+
_ = os.Remove(tmpFile.Name())
|
|
490
|
+
|
|
491
|
+
if readErr != nil {
|
|
492
|
+
return editorFinishedMsg{err: readErr}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return editorFinishedMsg{content: content, err: err}
|
|
496
|
+
})
|
|
497
|
+
}
|