Skip to content

Commit c419a55

Browse files
committed
test message
1 parent a4d14f8 commit c419a55

15 files changed

Lines changed: 902 additions & 84 deletions

cmd/iterate/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ type iterConfig struct {
2727
MaxTokens int `json:"max_tokens,omitempty" toml:"max_tokens"`
2828
ThinkingLevel string `json:"thinking_level,omitempty" toml:"thinking_level"`
2929
CacheEnabled bool `json:"cache_enabled,omitempty" toml:"cache_enabled"`
30+
// Request timeout in seconds (0 = default 120s).
31+
RequestTimeout int `json:"request_timeout,omitempty" toml:"request_timeout"`
3032
// Glob-based allow/deny patterns for bash commands.
3133
AllowPatterns []string `json:"allow_patterns,omitempty" toml:"allow_patterns"`
3234
DenyPatterns []string `json:"deny_patterns,omitempty" toml:"deny_patterns"`

cmd/iterate/features_sessions.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ func saveSession(name string, messages []iteragent.Message) error {
6060
if err != nil {
6161
return err
6262
}
63+
// Write .bak backup of previous version before overwriting.
64+
if existing, err := os.ReadFile(path); err == nil {
65+
_ = atomicWriteFile(path+".bak", existing, 0o644)
66+
}
6367
return atomicWriteFile(path, data, 0o644)
6468
}
6569

@@ -71,7 +75,20 @@ func loadSession(name string) ([]iteragent.Message, error) {
7175
}
7276
var msgs []iteragent.Message
7377
if err := json.Unmarshal(data, &msgs); err != nil {
74-
return nil, err
78+
// Try .bak file if primary is corrupt.
79+
if bakData, bakErr := os.ReadFile(path + ".bak"); bakErr == nil {
80+
var bakMsgs []iteragent.Message
81+
if json.Unmarshal(bakData, &bakMsgs) == nil {
82+
return bakMsgs, nil
83+
}
84+
}
85+
return nil, fmt.Errorf("session file corrupt: %w", err)
86+
}
87+
// Integrity check: each message must have a non-empty role.
88+
for i, m := range msgs {
89+
if m.Role == "" {
90+
return nil, fmt.Errorf("session integrity check failed: message %d has empty role", i)
91+
}
7592
}
7693
return msgs, nil
7794
}

cmd/iterate/features_tools.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ import (
2424
// Tool wrappers wait for it to reach 0 before showing a prompt.
2525
var spinnerActive atomic.Int32
2626

27+
// streamingTokenCount is incremented for each token received during streaming.
28+
// The spinner reads this to display tok/s.
29+
var streamingTokenCount atomic.Int64
30+
2731
// deniedTools is the set of tools blocked in safe mode.
2832
var deniedTools = map[string]bool{
2933
"bash": true,

cmd/iterate/fileutil_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
)
8+
9+
func TestAtomicWriteFile_BasicWrite(t *testing.T) {
10+
dir := t.TempDir()
11+
path := filepath.Join(dir, "test.json")
12+
data := []byte(`{"key":"value"}`)
13+
14+
if err := atomicWriteFile(path, data, 0o644); err != nil {
15+
t.Fatalf("atomicWriteFile failed: %v", err)
16+
}
17+
18+
got, err := os.ReadFile(path)
19+
if err != nil {
20+
t.Fatalf("ReadFile failed: %v", err)
21+
}
22+
if string(got) != string(data) {
23+
t.Errorf("want %q, got %q", data, got)
24+
}
25+
}
26+
27+
func TestAtomicWriteFile_Overwrite(t *testing.T) {
28+
dir := t.TempDir()
29+
path := filepath.Join(dir, "test.json")
30+
31+
// Write initial content.
32+
if err := atomicWriteFile(path, []byte("original"), 0o644); err != nil {
33+
t.Fatalf("initial write failed: %v", err)
34+
}
35+
36+
// Overwrite with new content.
37+
if err := atomicWriteFile(path, []byte("updated"), 0o644); err != nil {
38+
t.Fatalf("overwrite failed: %v", err)
39+
}
40+
41+
got, _ := os.ReadFile(path)
42+
if string(got) != "updated" {
43+
t.Errorf("want %q, got %q", "updated", got)
44+
}
45+
}
46+
47+
func TestAtomicWriteFile_NoTempFilesLeft(t *testing.T) {
48+
dir := t.TempDir()
49+
path := filepath.Join(dir, "test.json")
50+
51+
if err := atomicWriteFile(path, []byte("data"), 0o644); err != nil {
52+
t.Fatalf("atomicWriteFile failed: %v", err)
53+
}
54+
55+
// No .tmp- files should remain in the directory.
56+
entries, _ := os.ReadDir(dir)
57+
for _, e := range entries {
58+
if e.Name() != "test.json" {
59+
t.Errorf("unexpected file left behind: %s", e.Name())
60+
}
61+
}
62+
}
63+
64+
func TestAtomicWriteFile_PermissionSet(t *testing.T) {
65+
dir := t.TempDir()
66+
path := filepath.Join(dir, "test.json")
67+
68+
if err := atomicWriteFile(path, []byte("data"), 0o600); err != nil {
69+
t.Fatalf("atomicWriteFile failed: %v", err)
70+
}
71+
72+
info, err := os.Stat(path)
73+
if err != nil {
74+
t.Fatalf("stat failed: %v", err)
75+
}
76+
if info.Mode().Perm() != 0o600 {
77+
t.Errorf("want perm 0600, got %04o", info.Mode().Perm())
78+
}
79+
}

cmd/iterate/provider.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66

77
iteragent "github.com/GrayCodeAI/iteragent"
8+
"github.com/GrayCodeAI/iterate/internal/ui/selector"
89
)
910

1011
// resolveProviderConfig merges flag values with persisted config.
@@ -43,11 +44,13 @@ func resolveThinkingLevel(flagThinking string, cfg iterConfig) string {
4344
}
4445

4546
// initProvider creates an LLM provider from the given name and API key.
47+
// It also wires the provider's context window into the selector for display.
4648
func initProvider(providerName, apiKey string, logger *slog.Logger) (iteragent.Provider, error) {
4749
p, err := iteragent.NewProvider(providerName, apiKey)
4850
if err != nil {
4951
return nil, err
5052
}
5153
logger.Info("using provider", "name", p.Name())
54+
selector.ContextWindow = iteragent.ProviderContextWindow(p)
5255
return p, nil
5356
}

cmd/iterate/repl.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,19 @@ func makeAgent(p iteragent.Provider, repoPath string, thinking iteragent.Thinkin
6060
tools := wrapToolsWithPermissions(base)
6161
skills, _ := iteragent.LoadSkills([]string{filepath.Join(repoPath, "skills")})
6262
defaultTemp := float32(0.9)
63+
ctxCfg := iteragent.DefaultContextConfig()
64+
if cw := iteragent.ProviderContextWindow(p); cw > 0 {
65+
// Use 80% of the provider's actual context window as the compaction threshold.
66+
ctxCfg.MaxTokens = cw * 8 / 10
67+
}
6368
ag := iteragent.New(p, tools, logger).
6469
WithSystemPrompt(replSystemPrompt(repoPath)).
6570
WithSkillSet(skills).
6671
WithThinkingLevel(thinking).
6772
WithToolExecutionStrategy(iteragent.NewParallelStrategy()).
6873
WithHooks(replHooks()).
69-
WithTemperature(defaultTemp)
74+
WithTemperature(defaultTemp).
75+
WithContextConfig(ctxCfg)
7076
if rtConfig.Temperature != nil {
7177
ag = ag.WithTemperature(*rtConfig.Temperature)
7278
}
@@ -140,6 +146,7 @@ func setupSigintHandler() {
140146
func applyLoadedConfig(loadedCfg iterConfig, thinking iteragent.ThinkingLevel) iteragent.ThinkingLevel {
141147
cfg.SafeMode = loadedCfg.SafeMode
142148
cfg.NotifyEnabled = loadedCfg.Notify
149+
cfg.RequestTimeout = loadedCfg.RequestTimeout
143150
if loadedCfg.Theme != "" {
144151
if t, ok := themes[loadedCfg.Theme]; ok {
145152
applyTheme(t)
@@ -276,6 +283,7 @@ func handleModelProviderSwitch(line string, p *iteragent.Provider, thinking *ite
276283
closeProvider(*p)
277284
*p = newP
278285
os.Setenv("ITERATE_PROVIDER", providerName)
286+
selector.ContextWindow = iteragent.ProviderContextWindow(newP)
279287
_ = (*a).Close() // best-effort cleanup
280288
*a = makeAgent(*p, repoPath, *thinking, logger)
281289
fmt.Printf("%s✓ switched to %s%s\n\n", colorLime, (*p).Name(), colorReset)
@@ -293,13 +301,20 @@ func printSessionSummary(a *iteragent.Agent, repoPath string) {
293301
_ = saveSession("autosave", a.Messages) // best-effort cleanup
294302
}
295303
fmt.Println()
296-
fmt.Printf("%s session:%s %s%s%s %s·%s %s%d messages%s %s·%s %s%d tokens%s\n",
304+
costStr := ""
305+
if sess.CostUSD > 0 {
306+
costStr = fmt.Sprintf(" %s·%s %s%s%s",
307+
colorDim, colorReset,
308+
colorLime, formatSessionCost(sess.CostUSD), colorReset)
309+
}
310+
fmt.Printf("%s session:%s %s%s%s %s·%s %s%d messages%s %s·%s %s%d tokens%s%s\n",
297311
colorDim, colorReset,
298312
colorCyan, elapsed, colorReset,
299313
colorDim, colorReset,
300314
colorDim, sess.Messages, colorReset,
301315
colorDim, colorReset,
302-
colorPurple, sess.InputTokens+sess.OutputTokens, colorReset)
316+
colorPurple, sess.InputTokens+sess.OutputTokens, colorReset,
317+
costStr)
303318
if len(a.Messages) > 0 {
304319
fmt.Printf("%s autosaved · /load autosave to restore%s\n", colorDim, colorReset)
305320
}

cmd/iterate/repl_helpers.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,69 @@ func replSystemPrompt(repoPath string) string {
140140
base += "\n## Project Context (ITERATE.md)\n" + string(iterateMD)
141141
}
142142

143+
// Inject AGENTS.md if present (OpenAI Codex / Claude convention)
144+
if agentsMD, err := os.ReadFile(filepath.Join(repoPath, "AGENTS.md")); err == nil {
145+
base += "\n## Agent Instructions (AGENTS.md)\n" + string(agentsMD)
146+
}
147+
148+
// Inject detected project language/framework info
149+
if lang := detectProjectStack(repoPath); lang != "" {
150+
base += "\n## Project Stack\n" + lang + "\n"
151+
}
152+
143153
if index := buildRepoIndex(repoPath); index != "" {
144154
base += "\n## Repo structure\n```\n" + index + "\n```\n"
145155
}
146156
return base
147157
}
158+
159+
// detectProjectStack detects the primary language and frameworks used in the repo.
160+
func detectProjectStack(repoPath string) string {
161+
var tags []string
162+
163+
check := func(path string) bool {
164+
_, err := os.Stat(filepath.Join(repoPath, path))
165+
return err == nil
166+
}
167+
168+
// Go
169+
if check("go.mod") {
170+
tags = append(tags, "Go")
171+
}
172+
// Node / JS
173+
if check("package.json") {
174+
if check("tsconfig.json") {
175+
tags = append(tags, "TypeScript")
176+
} else {
177+
tags = append(tags, "JavaScript/Node.js")
178+
}
179+
if check("next.config.js") || check("next.config.ts") || check("next.config.mjs") {
180+
tags = append(tags, "Next.js")
181+
}
182+
}
183+
// Python
184+
if check("pyproject.toml") || check("requirements.txt") || check("setup.py") {
185+
tags = append(tags, "Python")
186+
}
187+
// Rust
188+
if check("Cargo.toml") {
189+
tags = append(tags, "Rust")
190+
}
191+
// Ruby
192+
if check("Gemfile") {
193+
tags = append(tags, "Ruby")
194+
}
195+
// Java / Kotlin
196+
if check("pom.xml") || check("build.gradle") || check("build.gradle.kts") {
197+
tags = append(tags, "JVM (Java/Kotlin)")
198+
}
199+
// Docker
200+
if check("Dockerfile") || check("docker-compose.yml") || check("docker-compose.yaml") {
201+
tags = append(tags, "Docker")
202+
}
203+
204+
if len(tags) == 0 {
205+
return ""
206+
}
207+
return strings.Join(tags, ", ")
208+
}

cmd/iterate/repl_models.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ func selectModel(currentThinking iteragent.ThinkingLevel) (iteragent.Provider, i
4545
return nil, currentThinking
4646
}
4747
os.Setenv("ITERATE_PROVIDER", providerName)
48+
selector.ContextWindow = iteragent.ProviderContextWindow(newP)
4849
return newP, currentThinking
4950
}
5051

@@ -74,6 +75,7 @@ func selectOllamaModel(currentThinking iteragent.ThinkingLevel) (iteragent.Provi
7475
fmt.Printf("%serror: %s%s\n\n", colorRed, err, colorReset)
7576
return nil, currentThinking
7677
}
78+
selector.ContextWindow = iteragent.ProviderContextWindow(p)
7779
return p, currentThinking
7880
}
7981

0 commit comments

Comments
 (0)