Skip to content

Commit 2092390

Browse files
committed
test message
1 parent 28e0671 commit 2092390

3 files changed

Lines changed: 266 additions & 9 deletions

File tree

cmd/iterate/repl.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ func runREPL(ctx context.Context, p iteragent.Provider, repoPath string, thinkin
237237
continue
238238
}
239239

240+
suggestAtFiles(line, repoPath)
240241
streamAndPrint(ctx, a, line, repoPath)
241242
}
242243

@@ -534,3 +535,76 @@ func buildCommandContext(repoPath, line string, parts []string, p iteragent.Prov
534535
},
535536
}
536537
}
538+
539+
// suggestAtFiles scans the prompt for words that match repo filenames without
540+
// an @-prefix already present, and prints a one-line dim hint.
541+
func suggestAtFiles(prompt, repoPath string) {
542+
const maxScan = 500
543+
const minWordLen = 4
544+
545+
// Skip if the prompt already uses @-references.
546+
if strings.Contains(prompt, "@") {
547+
return
548+
}
549+
550+
words := strings.Fields(prompt)
551+
var candidates []string
552+
seen := make(map[string]bool)
553+
for _, w := range words {
554+
// Strip trailing punctuation.
555+
w = strings.TrimRight(w, ".,;:!?\"')")
556+
if len(w) < minWordLen {
557+
continue
558+
}
559+
if seen[w] {
560+
continue
561+
}
562+
seen[w] = true
563+
candidates = append(candidates, w)
564+
}
565+
if len(candidates) == 0 {
566+
return
567+
}
568+
569+
// Build a quick filename index (base names only, limited scan).
570+
fileIndex := make(map[string]string) // base → rel path
571+
count := 0
572+
_ = filepath.Walk(repoPath, func(path string, info os.FileInfo, err error) error {
573+
if err != nil || info.IsDir() {
574+
if info != nil && info.IsDir() {
575+
n := info.Name()
576+
if n == ".git" || n == "node_modules" || n == "vendor" || n == "dist" {
577+
return filepath.SkipDir
578+
}
579+
}
580+
return nil
581+
}
582+
if count >= maxScan {
583+
return filepath.SkipDir
584+
}
585+
rel, _ := filepath.Rel(repoPath, path)
586+
base := filepath.Base(rel)
587+
// index both the base and the rel path
588+
fileIndex[strings.ToLower(base)] = rel
589+
count++
590+
return nil
591+
})
592+
593+
var hints []string
594+
for _, word := range candidates {
595+
lower := strings.ToLower(word)
596+
for base, rel := range fileIndex {
597+
if strings.Contains(base, lower) {
598+
hints = append(hints, "@"+rel)
599+
break
600+
}
601+
}
602+
if len(hints) >= 3 {
603+
break
604+
}
605+
}
606+
607+
if len(hints) > 0 {
608+
fmt.Printf("%s hint: use %s to include file context%s\n", colorDim, strings.Join(hints, ", "), colorReset)
609+
}
610+
}

internal/commands/session.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package commands
22

33
import (
44
"bufio"
5+
"context"
56
"encoding/json"
67
"fmt"
78
"os"
@@ -179,6 +180,14 @@ func registerSessionUtilityB(r *Registry) {
179180
Category: "session",
180181
Handler: cmdAuditLog,
181182
})
183+
184+
r.Register(Command{
185+
Name: "/replay",
186+
Aliases: []string{},
187+
Description: "replay a saved session step-by-step [name]",
188+
Category: "session",
189+
Handler: cmdReplay,
190+
})
182191
}
183192

184193
func cmdQuit(ctx Context) Result {
@@ -658,3 +667,78 @@ func cmdHistorySearch(ctx Context) Result {
658667
fmt.Printf("%s──────────────────────────────────%s\n\n", ColorDim, ColorReset)
659668
return Result{Handled: true}
660669
}
670+
671+
// cmdReplay replays a saved session step-by-step, re-sending each user message.
672+
func cmdReplay(ctx Context) Result {
673+
if ctx.Session.LoadSession == nil || ctx.Session.ListSessions == nil {
674+
PrintError("session replay not available")
675+
return Result{Handled: true}
676+
}
677+
678+
var name string
679+
if ctx.HasArg(1) {
680+
name = ctx.Arg(1)
681+
} else {
682+
sessions := ctx.Session.ListSessions()
683+
if len(sessions) == 0 {
684+
fmt.Println("No saved sessions.")
685+
return Result{Handled: true}
686+
}
687+
if ctx.Session.SelectItem != nil {
688+
var ok bool
689+
name, ok = ctx.Session.SelectItem("Replay session", sessions)
690+
if !ok {
691+
return Result{Handled: true}
692+
}
693+
} else {
694+
name = sessions[len(sessions)-1]
695+
}
696+
}
697+
698+
msgs, err := ctx.Session.LoadSession(name)
699+
if err != nil {
700+
PrintError("failed to load session %q: %v", name, err)
701+
return Result{Handled: true}
702+
}
703+
704+
// Collect user turns only.
705+
var userMsgs []iteragent.Message
706+
for _, m := range msgs {
707+
if m.Role == "user" {
708+
userMsgs = append(userMsgs, m)
709+
}
710+
}
711+
if len(userMsgs) == 0 {
712+
fmt.Println("No user messages to replay.")
713+
return Result{Handled: true}
714+
}
715+
716+
fmt.Printf("%s── Replaying %q — %d user turns ──%s\n\n", ColorDim, name, len(userMsgs), ColorReset)
717+
718+
if ctx.Agent != nil {
719+
ctx.Agent.Reset()
720+
}
721+
722+
for i, msg := range userMsgs {
723+
content := msg.Content
724+
if len(content) > 120 {
725+
content = content[:120] + "…"
726+
}
727+
fmt.Printf("%s[step %d/%d]%s %s%s%s\n\n", ColorDim, i+1, len(userMsgs), ColorReset, ColorYellow, content, ColorReset)
728+
729+
if ctx.REPL.PromptLine != nil {
730+
answer, ok := ctx.REPL.PromptLine(" [enter] continue [q] quit > ")
731+
if !ok || strings.ToLower(strings.TrimSpace(answer)) == "q" {
732+
fmt.Printf("\n%s[replay aborted]%s\n\n", ColorDim, ColorReset)
733+
return Result{Handled: true}
734+
}
735+
}
736+
737+
if ctx.REPL.StreamAndPrint != nil && ctx.Agent != nil {
738+
ctx.REPL.StreamAndPrint(context.Background(), ctx.Agent, msg.Content, ctx.RepoPath)
739+
}
740+
}
741+
742+
fmt.Printf("\n%s[replay complete]%s\n\n", ColorLime, ColorReset)
743+
return Result{Handled: true}
744+
}

internal/commands/utility.go

Lines changed: 108 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -125,20 +125,88 @@ func registerUtilityActionCommands(r *Registry) {
125125
}
126126

127127
func cmdContext(ctx Context) Result {
128-
fmt.Printf("%s── Context ─────────────────────────%s\n", ColorDim, ColorReset)
129-
if ctx.Agent != nil {
130-
fmt.Printf(" Messages: %d\n", len(ctx.Agent.Messages))
131-
}
128+
const barWidth = 24
129+
130+
inTok := 0
131+
outTok := 0
132+
cacheRd := 0
133+
cacheWr := 0
132134
if ctx.SessionInputTokens != nil {
133-
fmt.Printf(" Session input: ~%d tokens\n", *ctx.SessionInputTokens)
135+
inTok = *ctx.SessionInputTokens
134136
}
135137
if ctx.SessionOutputTokens != nil {
136-
fmt.Printf(" Session output: ~%d tokens\n", *ctx.SessionOutputTokens)
138+
outTok = *ctx.SessionOutputTokens
139+
}
140+
if ctx.SessionCacheRead != nil {
141+
cacheRd = *ctx.SessionCacheRead
142+
}
143+
if ctx.SessionCacheWrite != nil {
144+
cacheWr = *ctx.SessionCacheWrite
145+
}
146+
147+
windowSize := 200000 // default assumption
148+
if ctx.ContextWindow != nil && *ctx.ContextWindow > 0 {
149+
windowSize = *ctx.ContextWindow
150+
}
151+
152+
used := inTok + outTok
153+
pct := 0.0
154+
if windowSize > 0 {
155+
pct = float64(used) / float64(windowSize)
156+
if pct > 1.0 {
157+
pct = 1.0
158+
}
159+
}
160+
161+
filled := int(pct * float64(barWidth))
162+
if filled > barWidth {
163+
filled = barWidth
137164
}
138-
fmt.Printf("%s──────────────────────────────────%s\n\n", ColorDim, ColorReset)
165+
bar := strings.Repeat("█", filled) + strings.Repeat("░", barWidth-filled)
166+
167+
barColor := ColorLime
168+
if pct >= 0.85 {
169+
barColor = ColorRed
170+
} else if pct >= 0.6 {
171+
barColor = ColorYellow
172+
}
173+
174+
msgs := 0
175+
if ctx.Agent != nil {
176+
msgs = len(ctx.Agent.Messages)
177+
}
178+
179+
fmt.Printf("%s── Context Window ────────────────────────────%s\n", ColorDim, ColorReset)
180+
fmt.Printf(" %s[%s%s%s]%s %s%.0f%%%s (%s / %s tokens)\n",
181+
ColorDim, barColor, bar, ColorDim, ColorReset,
182+
ColorBold, pct*100, ColorReset,
183+
formatTokenCount(used), formatTokenCount(windowSize))
184+
fmt.Println()
185+
fmt.Printf(" %-18s %s%s%s\n", "Input tokens:", ColorCyan, formatTokenCount(inTok), ColorReset)
186+
fmt.Printf(" %-18s %s%s%s\n", "Output tokens:", ColorCyan, formatTokenCount(outTok), ColorReset)
187+
if cacheRd > 0 || cacheWr > 0 {
188+
fmt.Printf(" %-18s %s%s read%s / %s%s write%s\n",
189+
"Cache:",
190+
ColorDim, formatTokenCount(cacheRd), ColorReset,
191+
ColorDim, formatTokenCount(cacheWr), ColorReset)
192+
}
193+
fmt.Printf(" %-18s %d\n", "Messages:", msgs)
194+
fmt.Printf("%s──────────────────────────────────────────────%s\n\n", ColorDim, ColorReset)
139195
return Result{Handled: true}
140196
}
141197

198+
// formatTokenCount formats a token count as "42k" or "1.2M" for readability.
199+
func formatTokenCount(n int) string {
200+
switch {
201+
case n >= 1_000_000:
202+
return fmt.Sprintf("%.1fM", float64(n)/1_000_000)
203+
case n >= 1_000:
204+
return fmt.Sprintf("%.1fk", float64(n)/1_000)
205+
default:
206+
return fmt.Sprintf("%d", n)
207+
}
208+
}
209+
142210
func cmdExport(ctx Context) Result {
143211
if ctx.Agent == nil || len(ctx.Agent.Messages) == 0 {
144212
PrintError("no conversation to export")
@@ -369,24 +437,55 @@ func savePins(repoPath string, pins []iteragent.Message) {
369437
}
370438

371439
func cmdPin(ctx Context) Result {
440+
// /pin <text> — pin arbitrary text as a persistent user message.
441+
if ctx.HasArg(1) {
442+
text := ctx.Args()
443+
pins := loadPins(ctx.RepoPath)
444+
pins = append(pins, iteragent.Message{Role: "user", Content: text})
445+
savePins(ctx.RepoPath, pins)
446+
PrintSuccess("pinned: %q — will survive /compact", truncate(text, 60))
447+
return Result{Handled: true}
448+
}
449+
// /pin with no args — pin the last message in the conversation.
372450
if ctx.Agent == nil || len(ctx.Agent.Messages) == 0 {
373-
PrintError("no messages to pin")
451+
PrintError("no messages to pin; use /pin <text> to pin arbitrary text")
374452
return Result{Handled: true}
375453
}
376454
last := ctx.Agent.Messages[len(ctx.Agent.Messages)-1]
377455
pins := loadPins(ctx.RepoPath)
378456
pins = append(pins, last)
379457
savePins(ctx.RepoPath, pins)
380-
PrintSuccess("message pinned — will survive /compact")
458+
PrintSuccess("last message pinned — will survive /compact")
381459
return Result{Handled: true}
382460
}
383461

384462
func cmdUnpin(ctx Context) Result {
463+
// /unpin <n> — remove nth pin (1-indexed); no arg clears all.
464+
pins := loadPins(ctx.RepoPath)
465+
if ctx.HasArg(1) {
466+
var n int
467+
fmt.Sscanf(ctx.Arg(1), "%d", &n)
468+
if n < 1 || n > len(pins) {
469+
PrintError("pin index out of range (1–%d)", len(pins))
470+
return Result{Handled: true}
471+
}
472+
pins = append(pins[:n-1], pins[n:]...)
473+
savePins(ctx.RepoPath, pins)
474+
PrintSuccess("pin #%d removed (%d remaining)", n, len(pins))
475+
return Result{Handled: true}
476+
}
385477
savePins(ctx.RepoPath, nil)
386478
PrintSuccess("all pins cleared")
387479
return Result{Handled: true}
388480
}
389481

482+
func truncate(s string, n int) string {
483+
if len(s) <= n {
484+
return s
485+
}
486+
return s[:n] + "…"
487+
}
488+
390489
func cmdRewind(ctx Context) Result {
391490
n := 1
392491
if ctx.HasArg(1) {

0 commit comments

Comments
 (0)