feat(tui-go): @ file-reference picker
Typing `@` at a word boundary in the chat input (mid-word `@` stays literal, e.g. emails) opens a workspace file picker: it requests file.list for the session (cached per session), filters as you type, and enter inserts `@path ` at the cursor — back to typing. Renders as a transparent overlay, windowed around the selection. file.list is classified non-rendering in the render-matrix guard. Tests cover token-boundary detection, ref insertion, and filtering. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -124,6 +124,8 @@ func (m Model) renderOverlay(base string) string {
|
||||
return m.center(m.ideasModal())
|
||||
case OverlaySessions:
|
||||
return m.center(m.sessionsModal())
|
||||
case OverlayFiles:
|
||||
return m.center(m.filesModal())
|
||||
}
|
||||
return base
|
||||
}
|
||||
@@ -520,6 +522,63 @@ func (m Model) paletteModal() string {
|
||||
return m.center(modal)
|
||||
}
|
||||
|
||||
// filesModal is the `@` file-reference picker: a filter line over the session workspace's
|
||||
// paths, windowed around the selection; enter inserts `@path` into the chat input.
|
||||
func (m Model) filesModal() string {
|
||||
t := m.theme
|
||||
w := m.modalWidth()
|
||||
files := m.filteredFiles()
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(m.titleLine("@ file reference") + "\n\n")
|
||||
|
||||
caret := mbg(t, " ", t.P.BgPanel)
|
||||
if m.caretVisible() {
|
||||
caret = lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.BgPanel).Render("▏")
|
||||
}
|
||||
prompt := lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.BgPanel).Render("@ ")
|
||||
if m.fileFilter == "" {
|
||||
b.WriteString(prompt + caret + mbg(t, "type to filter files…", t.P.Faint) + "\n\n")
|
||||
} else {
|
||||
b.WriteString(prompt + mbg(t, m.fileFilter, t.P.FgStrong) + caret + "\n\n")
|
||||
}
|
||||
|
||||
switch {
|
||||
case m.filesLoading:
|
||||
b.WriteString(mbg(t, " loading…", t.P.Faint) + "\n")
|
||||
case len(m.files) == 0:
|
||||
b.WriteString(mbg(t, " no workspace files (is a workspace bound?)", t.P.Faint) + "\n")
|
||||
case len(files) == 0:
|
||||
b.WriteString(mbg(t, " no matching files", t.P.Faint) + "\n")
|
||||
default:
|
||||
const maxRows = 12
|
||||
start := 0
|
||||
if m.fileIndex >= maxRows {
|
||||
start = m.fileIndex - maxRows + 1
|
||||
}
|
||||
end := start + maxRows
|
||||
if end > len(files) {
|
||||
end = len(files)
|
||||
}
|
||||
for i := start; i < end; i++ {
|
||||
marker := mbg(t, " ", t.P.BgPanel)
|
||||
fg := t.P.Fg
|
||||
if i == m.fileIndex {
|
||||
marker = lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.BgPanel).Render("▌ ")
|
||||
fg = t.P.FgStrong
|
||||
}
|
||||
b.WriteString(marker +
|
||||
lipgloss.NewStyle().Foreground(fg).Background(t.P.BgPanel).Render(clip(files[i], w-6)) + "\n")
|
||||
}
|
||||
if len(files) > maxRows {
|
||||
b.WriteString(mbg(t, " "+itoa(len(files))+" matches", t.P.Faint) + "\n")
|
||||
}
|
||||
}
|
||||
b.WriteString("\n" + modalHints(t, [][2]string{{"↑↓", "select"}, {"enter", "insert"}, {"esc", "close"}}))
|
||||
modal := t.Overlay.Width(w).Render(b.String())
|
||||
return m.center(modal)
|
||||
}
|
||||
|
||||
func (m Model) toolPaletteModal() string {
|
||||
t := m.theme
|
||||
w := m.modalWidth()
|
||||
|
||||
Reference in New Issue
Block a user