Skip to content

Commit 6d3fd9d

Browse files
committed
test message
1 parent fa40c2d commit 6d3fd9d

2 files changed

Lines changed: 109 additions & 2 deletions

File tree

cmd/iterate/repl.go

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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.
135139
func 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.
601655
func suggestAtFiles(prompt, repoPath string) {

internal/commands/session.go

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,14 @@ func registerSessionUtilityA(r *Registry) {
146146
Category: "session",
147147
Handler: cmdPair,
148148
})
149+
150+
r.Register(Command{
151+
Name: "/trim",
152+
Aliases: []string{},
153+
Description: "keep last N message pairs in context: /trim [n]",
154+
Category: "session",
155+
Handler: cmdTrim,
156+
})
149157
}
150158

151159
func registerSessionUtilityB(r *Registry) {
@@ -427,11 +435,56 @@ func cmdMulti(ctx Context) Result {
427435
return Result{Handled: true}
428436
}
429437
if ctx.REPL.StreamAndPrint != nil {
430-
ctx.REPL.StreamAndPrint(nil, ctx.Agent, text, ctx.RepoPath)
438+
ctx.REPL.StreamAndPrint(context.Background(), ctx.Agent, text, ctx.RepoPath)
431439
}
432440
return Result{Handled: true}
433441
}
434442

443+
// cmdTrim keeps the last N user+assistant message pairs, dropping older turns.
444+
// Unlike /compact-hard (which works in raw message count), /trim works in
445+
// conversation turns: each turn is one user message + one assistant reply.
446+
func cmdTrim(ctx Context) Result {
447+
if ctx.Agent == nil {
448+
PrintError("agent not available")
449+
return Result{Handled: true}
450+
}
451+
452+
turns := 5 // default: keep last 5 turns
453+
if ctx.HasArg(1) {
454+
if v, err := strconv.Atoi(ctx.Arg(1)); err == nil && v > 0 {
455+
turns = v
456+
} else {
457+
fmt.Println("Usage: /trim [n] — keep last n conversation turns (default 5)")
458+
return Result{Handled: true}
459+
}
460+
}
461+
462+
msgs := ctx.Agent.Messages
463+
if len(msgs) == 0 {
464+
fmt.Println("No messages in context.")
465+
return Result{Handled: true}
466+
}
467+
468+
// Count conversation turns from the end (each user+assistant pair = 1 turn).
469+
keep := turns * 2 // approximate: 2 messages per turn
470+
if keep >= len(msgs) {
471+
fmt.Printf("Context already has ≤%d turns (%d messages).\n", turns, len(msgs))
472+
return Result{Handled: true}
473+
}
474+
475+
// Align to a user-message boundary so we always start on a user turn.
476+
startIdx := len(msgs) - keep
477+
for startIdx > 0 && msgs[startIdx].Role != "user" {
478+
startIdx++
479+
}
480+
481+
before := len(msgs)
482+
ctx.Agent.Messages = msgs[startIdx:]
483+
fmt.Printf("%s✓ trimmed: %d → %d messages (%d turns kept)%s\n\n",
484+
ColorLime, before, len(ctx.Agent.Messages), turns, ColorReset)
485+
return Result{Handled: true}
486+
}
487+
435488
func cmdCompactHard(ctx Context) Result {
436489
if ctx.Agent == nil {
437490
PrintError("agent not available")

0 commit comments

Comments
 (0)