@@ -131,19 +131,40 @@ func replHooks() iteragent.AgentHooks {
131131 }
132132}
133133
134+ // ctrlCExitCh is closed when the user presses Ctrl+C while idle (no active request).
135+ // The REPL loop listens on this channel to exit cleanly.
136+ var ctrlCExitCh = make (chan struct {}, 1 )
137+
134138// initREPL loads config, applies theme, sets up signal handling and runtime state.
135139func setupSigintHandler () {
136140 sigCh := make (chan os.Signal , 1 )
137141 signal .Notify (sigCh , syscall .SIGINT )
138142 go func () {
143+ var lastIdle time.Time
139144 for range sigCh {
140145 if sess .RequestCancel != nil {
146+ // Active request — cancel it.
141147 sess .RequestCancel ()
142- // Snapshot colors under read lock — applyTheme writes these from the main goroutine.
143148 colorMu .RLock ()
144149 y , r := colorYellow , colorReset
145150 colorMu .RUnlock ()
146151 fmt .Printf ("\r \033 [K%s[cancelled]%s\n " , y , r )
152+ lastIdle = time.Time {} // reset idle timer after cancel
153+ } else {
154+ // Idle — double-Ctrl+C to exit.
155+ now := time .Now ()
156+ if ! lastIdle .IsZero () && now .Sub (lastIdle ) < 2 * time .Second {
157+ select {
158+ case ctrlCExitCh <- struct {}{}:
159+ default :
160+ }
161+ } else {
162+ lastIdle = now
163+ colorMu .RLock ()
164+ d , r := colorDim , colorReset
165+ colorMu .RUnlock ()
166+ fmt .Printf ("\r \033 [K%s(press Ctrl+C again to exit)%s\n " , d , r )
167+ }
147168 }
148169 }
149170 }()
@@ -221,8 +242,20 @@ func runREPL(ctx context.Context, p iteragent.Provider, repoPath string, thinkin
221242 }
222243
223244 for {
245+ // Check for double-Ctrl+C exit signal before blocking on ReadInput.
246+ select {
247+ case <- ctrlCExitCh :
248+ return
249+ default :
250+ }
251+
224252 line , ok := selector .ReadInput ()
225253 if ! ok {
254+ // Check if this was a double-Ctrl+C exit.
255+ select {
256+ case <- ctrlCExitCh :
257+ default :
258+ }
226259 break
227260 }
228261 if line == "" {
@@ -495,6 +528,7 @@ func buildCommandContext(repoPath, line string, parts []string, p iteragent.Prov
495528 StreamAndPrint : streamAndPrint ,
496529 RunShell : runShell ,
497530 PromptLine : selector .PromptLine ,
531+ ReadMultiLine : readMultiLine ,
498532 Undo : performUndo ,
499533 BuildRepoMap : func (rp string , refresh bool ) string {
500534 if refresh {
@@ -596,6 +630,26 @@ func buildAtFileIndex(repoPath string) map[string]string {
596630 return idx
597631}
598632
633+ // readMultiLine reads multi-line input from the user one line at a time.
634+ // A blank line submits; Ctrl+C cancels. Returns (text, true) or ("", false).
635+ func readMultiLine () (string , bool ) {
636+ fmt .Printf ("%s Multi-line mode — press Enter on a blank line to submit, Ctrl+C to cancel%s\n \n " ,
637+ colorDim , colorReset )
638+
639+ var lines []string
640+ for {
641+ line , ok := selector .PromptLine (" ···" )
642+ if ! ok {
643+ return "" , false
644+ }
645+ if line == "" && len (lines ) > 0 {
646+ break
647+ }
648+ lines = append (lines , line )
649+ }
650+ return strings .Join (lines , "\n " ), true
651+ }
652+
599653// suggestAtFiles scans the prompt for words that match repo filenames without
600654// an @-prefix already present, and prints a one-line dim hint.
601655func suggestAtFiles (prompt , repoPath string ) {
0 commit comments