mirror of
https://github.com/patriceckhart/zot.git
synced 2026-06-26 21:36:31 +02:00
feat(/study): accept an optional file or directory argument
/study previously hard-coded the prompt to 'the current directory'. It now takes an optional path - typed, drag-dropped, or selected via the @ file picker - and tailors the prompt to whatever was passed, distinguishing files from directories via os.Stat and rendering paths under cwd as relative for readability. With no argument, behaviour is unchanged. Examples: /study -> current directory (old behaviour) /study internal -> directory internal /study [dir:internal/] -> directory internal (via @-picker) /study cmd/zot/main.go -> file cmd/zot/main.go /study [file:cmd/zot/main.go] -> file cmd/zot/main.go (via @-picker)
This commit is contained in:
parent
e0c933ad7e
commit
81c913aef9
4 changed files with 133 additions and 7 deletions
|
|
@ -190,7 +190,7 @@ Type `/` in the TUI to open the autocomplete popup. Available commands:
|
|||
| `/swarm` | Spawn, monitor, and chat with background subagents. Each runs in parallel with your main session and shares its working directory. |
|
||||
| `/skills` | List discovered skills (SKILL.md files) and preview their bodies. |
|
||||
| `/compact` | Summarize the transcript into one message to free up context. |
|
||||
| `/study` | Run the canned prompt "Read and understand everything in the current directory." so the agent has full project context before you start asking targeted questions. |
|
||||
| `/study` | Run the canned prompt "Read and understand everything in the current directory." so the agent has full project context before you start asking targeted questions. Pass a path — typed, drag-dropped, or selected via `@` — to target a specific file or directory instead: `/study [dir:internal/]`, `/study cmd/zot/main.go`. |
|
||||
| `/jail` | Confine tools to the current directory. |
|
||||
| `/unjail` | Allow tools to touch paths outside again. |
|
||||
| `/reload-ext` | Hot-reload all extensions (re-read manifests, respawn subprocesses, rebuild tool registry). |
|
||||
|
|
|
|||
|
|
@ -2362,6 +2362,43 @@ func (i *Interactive) applySettingToggle(key string, value bool) {
|
|||
}
|
||||
}
|
||||
|
||||
// buildStudyPrompt returns the canned prompt the /study command
|
||||
// submits to the agent.
|
||||
//
|
||||
// With no argument, /study targets the current directory — the
|
||||
// historical behaviour. With an argument, /study targets that path
|
||||
// instead; either a directory ("read every file in here") or a
|
||||
// single file ("read this file"). The argument can be:
|
||||
//
|
||||
// - a relative path (resolved against cwd)
|
||||
// - an absolute path
|
||||
// - an @-picker chip, which has already been expanded to an
|
||||
// absolute path by expandFileChips before runSlash sees it
|
||||
//
|
||||
// The path is stat'd to pick the right wording ("directory" vs
|
||||
// "file"). If the path doesn't exist, we still build a sensible
|
||||
// prompt rather than erroring — the agent will surface the
|
||||
// missing-file failure itself when it tries to read it, which is
|
||||
// more useful than a refusal here.
|
||||
func buildStudyPrompt(arg, cwd string) string {
|
||||
arg = strings.TrimSpace(arg)
|
||||
if arg == "" {
|
||||
return "Read and understand everything in the current directory."
|
||||
}
|
||||
abs := arg
|
||||
if !filepath.IsAbs(abs) {
|
||||
abs = filepath.Join(cwd, abs)
|
||||
}
|
||||
display := arg
|
||||
if rel, err := filepath.Rel(cwd, abs); err == nil && !strings.HasPrefix(rel, "..") {
|
||||
display = rel
|
||||
}
|
||||
if info, err := os.Stat(abs); err == nil && !info.IsDir() {
|
||||
return "Read and understand the file " + display + "."
|
||||
}
|
||||
return "Read and understand everything in the directory " + display + "."
|
||||
}
|
||||
|
||||
func (i *Interactive) runSlash(ctx context.Context, cmd string) (done bool) {
|
||||
parts := strings.Fields(cmd)
|
||||
switch parts[0] {
|
||||
|
|
@ -2432,11 +2469,16 @@ func (i *Interactive) runSlash(ctx context.Context, cmd string) (done bool) {
|
|||
i.runCompact(ctx, false)
|
||||
case "/study":
|
||||
// Canned prompt that tells the agent to read every file
|
||||
// in the current directory so its later turns have the
|
||||
// whole project in context. Dispatched through the normal
|
||||
// queue-or-start path so it behaves identically to
|
||||
// typing the prompt by hand.
|
||||
const studyPrompt = "Read and understand everything in the current directory."
|
||||
// in some target so its later turns have the whole thing
|
||||
// in context. With no argument, the target is the current
|
||||
// directory. With an argument, the target is whatever the
|
||||
// user passed — typed by hand, drag-dropped, or selected
|
||||
// via the @ file picker (which is why we accept both files
|
||||
// and directories; the @-picker chips for both have already
|
||||
// been expanded to absolute paths by expandFileChips above).
|
||||
// Dispatched through the normal queue-or-start path so it
|
||||
// behaves identically to typing the prompt by hand.
|
||||
studyPrompt := buildStudyPrompt(strings.TrimSpace(strings.TrimPrefix(cmd, parts[0])), i.cfg.CWD)
|
||||
i.mu.Lock()
|
||||
busy := i.busy
|
||||
ag := i.agent
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ var slashCatalog = []slashCommand{
|
|||
{Name: "/session", Desc: "export the current session to a .zotsession file, or import one"},
|
||||
{Name: "/jump", Desc: "scroll the chat to a previous turn (or /jump <text>)"},
|
||||
{Name: "/compact", Desc: "summarize and replace the transcript to free up context"},
|
||||
{Name: "/study", Desc: "read every file in the current directory so the agent has full project context"},
|
||||
{Name: "/study", Desc: "read every file in the cwd (or a passed file/dir) so the agent has full context"},
|
||||
{Name: "/btw", Desc: "side-chat that doesn't add to the main thread (saves tokens)"},
|
||||
{Name: "/jail", Desc: "confine tools to the current directory"},
|
||||
{Name: "/unjail", Desc: "allow tools to touch paths outside this directory"},
|
||||
|
|
|
|||
84
internal/agent/modes/study_test.go
Normal file
84
internal/agent/modes/study_test.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package modes
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildStudyPrompt(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
subdir := filepath.Join(tmp, "internal")
|
||||
if err := os.Mkdir(subdir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
file := filepath.Join(tmp, "main.go")
|
||||
if err := os.WriteFile(file, []byte("package main\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
arg string
|
||||
cwd string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty arg keeps the original cwd prompt",
|
||||
arg: "",
|
||||
cwd: tmp,
|
||||
want: "Read and understand everything in the current directory.",
|
||||
},
|
||||
{
|
||||
name: "relative dir becomes a directory prompt",
|
||||
arg: "internal",
|
||||
cwd: tmp,
|
||||
want: "Read and understand everything in the directory internal.",
|
||||
},
|
||||
{
|
||||
name: "absolute dir under cwd is shown as a relative path",
|
||||
arg: subdir,
|
||||
cwd: tmp,
|
||||
want: "Read and understand everything in the directory internal.",
|
||||
},
|
||||
{
|
||||
name: "relative file becomes a file prompt",
|
||||
arg: "main.go",
|
||||
cwd: tmp,
|
||||
want: "Read and understand the file main.go.",
|
||||
},
|
||||
{
|
||||
name: "absolute file under cwd is shown as a relative path",
|
||||
arg: file,
|
||||
cwd: tmp,
|
||||
want: "Read and understand the file main.go.",
|
||||
},
|
||||
{
|
||||
name: "missing path falls back to the directory phrasing",
|
||||
arg: "does-not-exist",
|
||||
cwd: tmp,
|
||||
want: "Read and understand everything in the directory does-not-exist.",
|
||||
},
|
||||
{
|
||||
name: "absolute path outside cwd keeps its absolute form",
|
||||
arg: subdir,
|
||||
cwd: filepath.Join(tmp, "elsewhere"),
|
||||
want: "Read and understand everything in the directory " + subdir + ".",
|
||||
},
|
||||
{
|
||||
name: "leading and trailing whitespace are stripped",
|
||||
arg: " main.go ",
|
||||
cwd: tmp,
|
||||
want: "Read and understand the file main.go.",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := buildStudyPrompt(tc.arg, tc.cwd)
|
||||
if got != tc.want {
|
||||
t.Fatalf("buildStudyPrompt(%q, %q)\n got: %q\n want: %q", tc.arg, tc.cwd, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue