Skip to content

Commit 2cf9ca6

Browse files
committed
fix typo
2 parents c419a55 + e4f2c9f commit 2cf9ca6

5 files changed

Lines changed: 288 additions & 25 deletions

File tree

internal/commands/agent.go

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,52 @@ func registerMany(r *Registry, category string, args ...interface{}) {
5353
}
5454

5555
func cmdModel(ctx Context) Result {
56-
if ctx.Provider != nil {
57-
PrintSuccess("current model: %s", ctx.Provider.Name())
58-
} else {
59-
fmt.Println("No provider configured.")
56+
fmt.Printf("%s── Current Model ───────────────────%s\n", ColorDim, ColorReset)
57+
if ctx.Provider == nil {
58+
fmt.Println(" No provider configured.")
59+
fmt.Printf("%s──────────────────────────────────%s\n\n", ColorDim, ColorReset)
60+
return Result{Handled: true}
6061
}
61-
fmt.Println("Use /provider <name> to switch provider.")
62+
63+
name := ctx.Provider.Name()
64+
fmt.Printf(" %sProvider:%s %s\n", ColorBold, ColorReset, name)
65+
66+
// Context window
67+
if cw, ok := ctx.Provider.(interface{ ContextWindow() int }); ok {
68+
w := cw.ContextWindow()
69+
if w >= 1_000_000 {
70+
fmt.Printf(" %sContext:%s %.1fM tokens\n", ColorBold, ColorReset, float64(w)/1_000_000)
71+
} else {
72+
fmt.Printf(" %sContext:%s %dk tokens\n", ColorBold, ColorReset, w/1_000)
73+
}
74+
}
75+
76+
// Thinking support
77+
if ctx.Thinking != nil {
78+
level := string(*ctx.Thinking)
79+
if level == "" || level == "off" {
80+
fmt.Printf(" %sThinking:%s off\n", ColorBold, ColorReset)
81+
} else {
82+
fmt.Printf(" %sThinking:%s %s\n", ColorBold, ColorReset, level)
83+
}
84+
}
85+
86+
// Session token usage
87+
if ctx.SessionInputTokens != nil && ctx.SessionOutputTokens != nil {
88+
total := *ctx.SessionInputTokens + *ctx.SessionOutputTokens
89+
if total > 0 {
90+
if ctx.ContextWindow != nil && *ctx.ContextWindow > 0 {
91+
pct := float64(total) * 100 / float64(*ctx.ContextWindow)
92+
fmt.Printf(" %sUsed:%s %d tokens (%.1f%%)\n", ColorBold, ColorReset, total, pct)
93+
} else {
94+
fmt.Printf(" %sUsed:%s %d tokens\n", ColorBold, ColorReset, total)
95+
}
96+
}
97+
}
98+
99+
fmt.Println()
100+
fmt.Printf(" Use %s/provider <name>%s to switch.\n", ColorBold, ColorReset)
101+
fmt.Printf("%s──────────────────────────────────%s\n\n", ColorDim, ColorReset)
62102
return Result{Handled: true}
63103
}
64104

internal/commands/files.go

Lines changed: 82 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,37 @@ import (
77
"os"
88
"os/exec"
99
"path/filepath"
10+
"regexp"
1011
"sort"
1112
"strings"
1213
"time"
1314

1415
iteragent "github.com/GrayCodeAI/iteragent"
1516
)
1617

18+
var (
19+
reHTMLTag = regexp.MustCompile(`<[^>]+>`)
20+
reHTMLComment = regexp.MustCompile(`<!--[\s\S]*?-->`)
21+
reScriptStyle = regexp.MustCompile(`(?i)<(script|style)[^>]*>[\s\S]*?</(script|style)>`)
22+
reMultiSpace = regexp.MustCompile(`[ \t]{2,}`)
23+
reMultiNewline = regexp.MustCompile(`\n{3,}`)
24+
)
25+
26+
// stripHTML removes HTML tags, strips scripts/styles, and normalises whitespace.
27+
func stripHTML(html string) string {
28+
s := reHTMLComment.ReplaceAllString(html, "")
29+
s = reScriptStyle.ReplaceAllString(s, "")
30+
s = reHTMLTag.ReplaceAllString(s, " ")
31+
s = strings.NewReplacer(
32+
"&amp;", "&", "&lt;", "<", "&gt;", ">",
33+
"&quot;", `"`, "&#39;", "'", "&nbsp;", " ",
34+
"&mdash;", "—", "&ndash;", "–",
35+
).Replace(s)
36+
s = reMultiSpace.ReplaceAllString(s, " ")
37+
s = reMultiNewline.ReplaceAllString(s, "\n\n")
38+
return strings.TrimSpace(s)
39+
}
40+
1741
// RegisterFileCommands adds file and search commands.
1842
func RegisterFileCommands(r *Registry) {
1943
registerFileContentCommands(r)
@@ -206,17 +230,23 @@ func cmdFind(ctx Context) Result {
206230

207231
func cmdWeb(ctx Context) Result {
208232
if !ctx.HasArg(1) {
209-
fmt.Println("Usage: /web <url>")
233+
fmt.Println("Usage: /web <url> [prompt]")
210234
return Result{Handled: true}
211235
}
212-
url := ctx.Arg(1)
213-
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
214-
url = "https://" + url
236+
rawURL := ctx.Arg(1)
237+
if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") {
238+
rawURL = "https://" + rawURL
215239
}
216-
fmt.Printf("%sfetching %s…%s\n", ColorDim, url, ColorReset)
217240

241+
PrintDim("fetching %s …", rawURL)
218242
client := &http.Client{Timeout: 15 * time.Second}
219-
resp, err := client.Get(url)
243+
req, err := http.NewRequest("GET", rawURL, nil)
244+
if err != nil {
245+
PrintError("invalid URL: %v", err)
246+
return Result{Handled: true}
247+
}
248+
req.Header.Set("User-Agent", "iterate-cli/1.0 (fetch)")
249+
resp, err := client.Do(req)
220250
if err != nil {
221251
PrintError("fetch failed: %v", err)
222252
return Result{Handled: true}
@@ -228,21 +258,58 @@ func cmdWeb(ctx Context) Result {
228258
return Result{Handled: true}
229259
}
230260

231-
body, err := io.ReadAll(io.LimitReader(resp.Body, 100*1024))
261+
const maxBodyBytes = 512 * 1024
262+
body, err := io.ReadAll(io.LimitReader(resp.Body, maxBodyBytes))
232263
if err != nil {
233264
PrintError("read failed: %v", err)
234265
return Result{Handled: true}
235266
}
236267

237-
if ctx.Agent != nil {
238-
content := fmt.Sprintf("Here is the content fetched from `%s`:\n\n%s", url, string(body))
239-
ctx.Agent.Messages = append(ctx.Agent.Messages, iteragent.Message{
240-
Role: "user",
241-
Content: content,
242-
})
243-
PrintSuccess("fetched %d bytes — injected into context", len(body))
268+
ct := resp.Header.Get("Content-Type")
269+
var text string
270+
if strings.Contains(ct, "text/html") || strings.Contains(ct, "xhtml") {
271+
text = stripHTML(string(body))
244272
} else {
245-
fmt.Println(string(body))
273+
text = string(body)
274+
}
275+
276+
const maxChars = 100_000
277+
truncated := false
278+
if len(text) > maxChars {
279+
text = text[:maxChars]
280+
truncated = true
281+
}
282+
283+
header := fmt.Sprintf("[Web content from %s]\n\n", rawURL)
284+
if truncated {
285+
header += "(truncated to first 100 000 characters)\n\n"
286+
}
287+
content := header + text
288+
289+
// Optional follow-up prompt after the URL.
290+
userPrompt := strings.TrimSpace(strings.TrimPrefix(ctx.Args(), ctx.Arg(1)))
291+
if userPrompt != "" {
292+
content = content + "\n\n" + userPrompt
293+
}
294+
295+
if ctx.Agent == nil {
296+
PrintError("no agent available")
297+
return Result{Handled: true}
298+
}
299+
300+
ctx.Agent.Messages = append(ctx.Agent.Messages, iteragent.Message{
301+
Role: "user",
302+
Content: content,
303+
})
304+
305+
suffix := ""
306+
if truncated {
307+
suffix = " (truncated)"
308+
}
309+
PrintSuccess("injected %d chars from %s%s", len(text), rawURL, suffix)
310+
311+
if userPrompt != "" && ctx.REPL.StreamAndPrint != nil {
312+
ctx.REPL.StreamAndPrint(nil, ctx.Agent, "", ctx.RepoPath)
246313
}
247314
return Result{Handled: true}
248315
}

internal/commands/utility.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ func registerUtilityContextCommands(r *Registry) {
4343
Category: "utility",
4444
Handler: cmdCompact,
4545
})
46+
4647
}
4748

4849
func registerUtilityConversationCommands(r *Registry) {

internal/ui/selector/selector_input.go

Lines changed: 117 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,15 @@ import (
1010
"github.com/GrayCodeAI/iterate/internal/ui"
1111
)
1212

13+
// inMultilineBlock is set when the user opens a triple-backtick block.
14+
// It is cleared when the closing ``` is entered or the input is cancelled.
15+
var inMultilineBlock bool
16+
1317
// ReadInput reads user input in raw mode.
14-
// Enter submits. Shift+Enter adds a newline. Up/Down arrow navigates history.
15-
// Left/Right arrows move the cursor. Delete removes the char under the cursor.
18+
// Enter submits. Ctrl+J inserts a literal newline (multiline input).
19+
// Starting a line with ``` enters block mode; another ``` closes it.
20+
// Up/Down arrow navigates history. Left/Right arrows move the cursor.
21+
// Delete removes the char under the cursor.
1622
// Returns (text, true) or ("", false) on Ctrl+C/EOF.
1723
func ReadInput() (string, bool) {
1824
fd := int(os.Stdin.Fd())
@@ -70,6 +76,44 @@ func ReadInput() (string, bool) {
7076
}
7177
}
7278

79+
// moveWordBackward moves cursorPos back to the start of the previous word and
80+
// emits the required backspace characters so the terminal cursor follows.
81+
func moveWordBackward(buf *[]byte, cursorPos *int) {
82+
i := *cursorPos
83+
// Skip trailing spaces.
84+
for i > 0 && (*buf)[i-1] == ' ' {
85+
i--
86+
}
87+
// Skip the word.
88+
for i > 0 && (*buf)[i-1] != ' ' {
89+
i--
90+
}
91+
delta := *cursorPos - i
92+
if delta > 0 {
93+
fmt.Printf("%s", strings.Repeat("\b", delta))
94+
*cursorPos = i
95+
}
96+
}
97+
98+
// moveWordForward moves cursorPos to the end of the next word and emits the
99+
// required character output so the terminal cursor follows.
100+
func moveWordForward(buf *[]byte, cursorPos *int) {
101+
i := *cursorPos
102+
n := len(*buf)
103+
// Skip leading spaces.
104+
for i < n && (*buf)[i] == ' ' {
105+
i++
106+
}
107+
// Skip the word.
108+
for i < n && (*buf)[i] != ' ' {
109+
i++
110+
}
111+
if i > *cursorPos {
112+
fmt.Printf("%s", string((*buf)[*cursorPos:i]))
113+
*cursorPos = i
114+
}
115+
}
116+
73117
// redrawFromCursor redraws buf[cursorPos:] on the terminal, appends erased
74118
// spaces to clear old characters, then repositions the cursor back to cursorPos.
75119
func redrawFromCursor(buf []byte, cursorPos, erased int) {
@@ -81,12 +125,25 @@ func redrawFromCursor(buf []byte, cursorPos, erased int) {
81125
// handleRawInput processes a single raw-mode key event and returns (done, text, ok).
82126
func handleRawInput(b []byte, n int, buf *[]byte, cursorPos *int, histSnapshot *[]string, histIdx *int, savedBuf *[]byte, killRing *string, replacePrompt func(string), fd int, oldState *term.State) (done bool, result string, ok bool) {
83127
switch {
84-
case b[0] == '\r' || b[0] == '\n':
128+
case b[0] == '\r':
85129
return handleLineSubmit(buf, cursorPos, histSnapshot, histIdx, savedBuf)
86130

131+
case b[0] == '\n': // Ctrl+J — insert literal newline (multiline input)
132+
*buf = append(append((*buf)[:*cursorPos:*cursorPos], '\n'), (*buf)[*cursorPos:]...)
133+
*cursorPos++
134+
fmt.Print("\r\n")
135+
fmt.Printf("%s ...%s ", ui.ColorDim, ui.ColorReset)
136+
// Reprint any text after the cursor on the new line.
137+
if *cursorPos < len(*buf) {
138+
fmt.Printf("%s", string((*buf)[*cursorPos:]))
139+
fmt.Printf("%s", strings.Repeat("\b", len(*buf)-*cursorPos))
140+
}
141+
return false, "", false
142+
87143
case b[0] == 3:
88144
// Two-stage Ctrl+C: first press clears the line, second press exits.
89-
if len(*buf) > 0 {
145+
if len(*buf) > 0 || inMultilineBlock {
146+
inMultilineBlock = false
90147
if *cursorPos > 0 {
91148
fmt.Printf("%s", strings.Repeat("\b", *cursorPos))
92149
}
@@ -115,6 +172,16 @@ func handleRawInput(b []byte, n int, buf *[]byte, cursorPos *int, histSnapshot *
115172
handleEscSeq(b, n, buf, cursorPos, histSnapshot, histIdx, savedBuf, replacePrompt)
116173
return false, "", false
117174

175+
case b[0] == 27 && n == 2 && b[1] == 'b':
176+
// Alt+Left (ESC b) — move backward one word.
177+
moveWordBackward(buf, cursorPos)
178+
return false, "", false
179+
180+
case b[0] == 27 && n == 2 && b[1] == 'f':
181+
// Alt+Right (ESC f) — move forward one word.
182+
moveWordForward(buf, cursorPos)
183+
return false, "", false
184+
118185
case b[0] == 27 && n == 1:
119186
return false, "", false
120187

@@ -259,10 +326,45 @@ func handleEscSeq(b []byte, n int, buf *[]byte, cursorPos *int, histSnapshot *[]
259326
}
260327
}
261328

262-
// handleLineSubmit processes Enter key, handling backslash continuation.
329+
// handleLineSubmit processes Enter key, handling backslash continuation and
330+
// triple-backtick multiline blocks.
263331
func handleLineSubmit(buf *[]byte, cursorPos *int, histSnapshot *[]string, histIdx *int, savedBuf *[]byte) (done bool, result string, ok bool) {
264332
fmt.Print("\r\n")
265333
text := string(*buf)
334+
trimmedText := strings.TrimSpace(text)
335+
336+
// Inside a triple-backtick block: ``` closes it, otherwise accumulate.
337+
if inMultilineBlock {
338+
if trimmedText == "```" {
339+
// Close the block — strip the closing fence and submit accumulated content.
340+
inMultilineBlock = false
341+
// Remove the closing ``` from the buffer tail.
342+
idx := strings.LastIndex(text, "```")
343+
if idx >= 0 {
344+
text = strings.TrimRight(text[:idx], "\n")
345+
}
346+
appendHistory(text)
347+
return true, text, true
348+
}
349+
// Continue accumulating.
350+
text = text + "\n"
351+
*buf = []byte(text)
352+
*cursorPos = len(*buf)
353+
fmt.Printf("%s ...%s ", ui.ColorDim, ui.ColorReset)
354+
return false, "", false
355+
}
356+
357+
// Opening triple-backtick block.
358+
if trimmedText == "```" {
359+
inMultilineBlock = true
360+
fmt.Printf("%s (multiline block — end with a line containing only ``` )%s\n", ui.ColorDim, ui.ColorReset)
361+
fmt.Printf("%s ...%s ", ui.ColorDim, ui.ColorReset)
362+
*buf = []byte{}
363+
*cursorPos = 0
364+
return false, "", false
365+
}
366+
367+
// Backslash continuation: "some text\" → append newline and keep reading.
266368
if strings.HasSuffix(strings.TrimRight(text, " "), "\\") {
267369
text = strings.TrimRight(text, " ")
268370
text = text[:len(text)-1] + "\n"
@@ -271,6 +373,16 @@ func handleLineSubmit(buf *[]byte, cursorPos *int, histSnapshot *[]string, histI
271373
fmt.Printf("%s ...%s ", ui.ColorDim, ui.ColorReset)
272374
return false, "", false
273375
}
376+
377+
// Already in backslash continuation (buf has embedded newlines from prior \-Enter)?
378+
if strings.Contains(text, "\n") {
379+
// Keep accumulating: no trailing \ means next Enter will submit.
380+
// (The user can end by pressing Enter without a trailing \.)
381+
text = strings.TrimSpace(text)
382+
appendHistory(text)
383+
return true, text, true
384+
}
385+
274386
text = strings.TrimSpace(text)
275387
appendHistory(text)
276388
return true, text, true

0 commit comments

Comments
 (0)