detached 5561cbd

feat: add optional startup instruction context

Fixes #100
commit
5561cbd8994c3fe8ef41ccfaf1c266fb6629d897
author
patriceckhart · · 2026-07-24 07:29 UTC
parent
5b23b1d
signature
unsigned
13 files · +259 -34
M README.md +3 -2
@@ -135,9 +135,9 @@ Use `AGENTS.md` to give zot standing instructions that layer **on top of** the d
135135 zot discovers `AGENTS.md` files automatically at startup and loads them in this order:
136136
137137 1. `$ZOT_HOME/AGENTS.md` (global, machine-wide instructions that apply to every project).
138-2. Every `AGENTS.md` from the top-most parent directory down to the current working directory. More specific (deeper) files may override earlier ones.
138+2. Every `AGENTS.md` from the filesystem root down to the current working directory. More specific (deeper) files may override earlier ones. This includes `~/AGENTS.md` when the working directory is inside your home directory.
139139
140-All discovered files are appended to the prompt in that order, so a global baseline can be refined per project.
140+All discovered files are appended to the prompt in that order, so a global baseline can be refined per project. To list their paths above the interactive transcript, enable **show loaded instructions at startup** in `/settings`. The setting is off by default, and the displayed path list is not sent to the model or stored in the session transcript.
141141
142142 For a general, non-project-specific instruction set, put your rules in `$ZOT_HOME/AGENTS.md`, for example:
143143
@@ -358,6 +358,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o
358358 - **auto-swarm** — let the main agent spawn background sub-agents in parallel via a built-in `swarm_spawn` tool. Off by default. When on, the tool is registered with the running agent, the system prompt gains a short addendum telling the model to delegate independent sub-tasks proactively, and zot watches every sub-agent the main agent spawns. As soon as the last sub-agent in a batch finishes its initial task, an `[auto-swarm update]` message is injected back into the chat with each agent's status / task / transcript tail, so the main agent can summarise the collective outcome. Flipping off mid-session removes the tool from the live agent and strips the addendum on the next turn — the model stops trying to delegate. See `/swarm` for the dashboard that lets you monitor, message, kill, or remove the spawned agents.
359359 - **jail new sessions by default**: start every new agent with tools confined to its working directory. Off by default. The setting applies to interactive, print, JSON, RPC, and background-agent runs, persists as `jail_by_default`, and immediately updates the current interactive session. `/jail` and `/unjail` remain session-scoped overrides and do not change this default.
360360 - **compact transcript rendering**: reduce visual chrome in the chat transcript. Tool calls render as a quiet header plus indented output instead of a bordered box, and sent messages render without padded background bubbles. Off by default. Changes apply immediately and persist to `config.json` as `compact_mode`.
361+- **show loaded instructions at startup**: list every loaded `AGENTS.md` file path above the transcript. Off by default. Changes apply immediately and persist to `config.json` as `show_instructions_at_startup`.
361362 - **TUI settings**: opens a sub-view for input layout and status placement. **Input style** can be `plain` (default prompt line), `lines` (separator lines above and below the input), or `block` (a user-bubble-style input block). **Status position** places model, usage, and working-directory information above or below the input. **Working spinner position** places the busy spinner above or below the input. Changes apply immediately and persist to `config.json` as `tui_input_style`, `tui_status_position`, and `tui_working_position` (`above_input` or `below_input` for the position fields).
362363 - **thinking level**: choose reasoning for supported models: off, minimum, low, medium, high, xhigh, or max. The `max` tier is opt-in and sent natively to GPT-5.6 and adaptive-thinking Claude models; unsupported backends clamp it to their highest accepted effort. The change is persisted to `config.json` and applied to the next model call.
363364 - **color theme** — choose the built-in auto/dark/light theme or any JSON theme discovered under `$ZOT_HOME/themes` or a loaded extension. Theme files can override any subset of UI colors, syntax colors, and spinner frames/messages. Changes apply immediately; if a selected theme file is deleted, zot resets to auto. See [docs/themes.md](docs/themes.md).
@@ -135,9 +135,9 @@ Use `AGENTS.md` to give zot standing instructions that layer **on top of** the d
135 zot discovers `AGENTS.md` files automatically at startup and loads them in this order:135 zot discovers `AGENTS.md` files automatically at startup and loads them in this order:
136 136
137 1. `$ZOT_HOME/AGENTS.md` (global, machine-wide instructions that apply to every project).137 1. `$ZOT_HOME/AGENTS.md` (global, machine-wide instructions that apply to every project).
138-2. Every `AGENTS.md` from the top-most parent directory down to the current working directory. More specific (deeper) files may override earlier ones.138+2. Every `AGENTS.md` from the filesystem root down to the current working directory. More specific (deeper) files may override earlier ones. This includes `~/AGENTS.md` when the working directory is inside your home directory.
139 139
140-All discovered files are appended to the prompt in that order, so a global baseline can be refined per project.140+All discovered files are appended to the prompt in that order, so a global baseline can be refined per project. To list their paths above the interactive transcript, enable **show loaded instructions at startup** in `/settings`. The setting is off by default, and the displayed path list is not sent to the model or stored in the session transcript.
141 141
142 For a general, non-project-specific instruction set, put your rules in `$ZOT_HOME/AGENTS.md`, for example:142 For a general, non-project-specific instruction set, put your rules in `$ZOT_HOME/AGENTS.md`, for example:
143 143
@@ -358,6 +358,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o
358 - **auto-swarm** — let the main agent spawn background sub-agents in parallel via a built-in `swarm_spawn` tool. Off by default. When on, the tool is registered with the running agent, the system prompt gains a short addendum telling the model to delegate independent sub-tasks proactively, and zot watches every sub-agent the main agent spawns. As soon as the last sub-agent in a batch finishes its initial task, an `[auto-swarm update]` message is injected back into the chat with each agent's status / task / transcript tail, so the main agent can summarise the collective outcome. Flipping off mid-session removes the tool from the live agent and strips the addendum on the next turn — the model stops trying to delegate. See `/swarm` for the dashboard that lets you monitor, message, kill, or remove the spawned agents.358 - **auto-swarm** — let the main agent spawn background sub-agents in parallel via a built-in `swarm_spawn` tool. Off by default. When on, the tool is registered with the running agent, the system prompt gains a short addendum telling the model to delegate independent sub-tasks proactively, and zot watches every sub-agent the main agent spawns. As soon as the last sub-agent in a batch finishes its initial task, an `[auto-swarm update]` message is injected back into the chat with each agent's status / task / transcript tail, so the main agent can summarise the collective outcome. Flipping off mid-session removes the tool from the live agent and strips the addendum on the next turn — the model stops trying to delegate. See `/swarm` for the dashboard that lets you monitor, message, kill, or remove the spawned agents.
359 - **jail new sessions by default**: start every new agent with tools confined to its working directory. Off by default. The setting applies to interactive, print, JSON, RPC, and background-agent runs, persists as `jail_by_default`, and immediately updates the current interactive session. `/jail` and `/unjail` remain session-scoped overrides and do not change this default.359 - **jail new sessions by default**: start every new agent with tools confined to its working directory. Off by default. The setting applies to interactive, print, JSON, RPC, and background-agent runs, persists as `jail_by_default`, and immediately updates the current interactive session. `/jail` and `/unjail` remain session-scoped overrides and do not change this default.
360 - **compact transcript rendering**: reduce visual chrome in the chat transcript. Tool calls render as a quiet header plus indented output instead of a bordered box, and sent messages render without padded background bubbles. Off by default. Changes apply immediately and persist to `config.json` as `compact_mode`.360 - **compact transcript rendering**: reduce visual chrome in the chat transcript. Tool calls render as a quiet header plus indented output instead of a bordered box, and sent messages render without padded background bubbles. Off by default. Changes apply immediately and persist to `config.json` as `compact_mode`.
361+- **show loaded instructions at startup**: list every loaded `AGENTS.md` file path above the transcript. Off by default. Changes apply immediately and persist to `config.json` as `show_instructions_at_startup`.
361 - **TUI settings**: opens a sub-view for input layout and status placement. **Input style** can be `plain` (default prompt line), `lines` (separator lines above and below the input), or `block` (a user-bubble-style input block). **Status position** places model, usage, and working-directory information above or below the input. **Working spinner position** places the busy spinner above or below the input. Changes apply immediately and persist to `config.json` as `tui_input_style`, `tui_status_position`, and `tui_working_position` (`above_input` or `below_input` for the position fields).362 - **TUI settings**: opens a sub-view for input layout and status placement. **Input style** can be `plain` (default prompt line), `lines` (separator lines above and below the input), or `block` (a user-bubble-style input block). **Status position** places model, usage, and working-directory information above or below the input. **Working spinner position** places the busy spinner above or below the input. Changes apply immediately and persist to `config.json` as `tui_input_style`, `tui_status_position`, and `tui_working_position` (`above_input` or `below_input` for the position fields).
362 - **thinking level**: choose reasoning for supported models: off, minimum, low, medium, high, xhigh, or max. The `max` tier is opt-in and sent natively to GPT-5.6 and adaptive-thinking Claude models; unsupported backends clamp it to their highest accepted effort. The change is persisted to `config.json` and applied to the next model call.363 - **thinking level**: choose reasoning for supported models: off, minimum, low, medium, high, xhigh, or max. The `max` tier is opt-in and sent natively to GPT-5.6 and adaptive-thinking Claude models; unsupported backends clamp it to their highest accepted effort. The change is persisted to `config.json` and applied to the next model call.
363 - **color theme** — choose the built-in auto/dark/light theme or any JSON theme discovered under `$ZOT_HOME/themes` or a loaded extension. Theme files can override any subset of UI colors, syntax colors, and spinner frames/messages. Changes apply immediately; if a selected theme file is deleted, zot resets to auto. See [docs/themes.md](docs/themes.md).364 - **color theme** — choose the built-in auto/dark/light theme or any JSON theme discovered under `$ZOT_HOME/themes` or a loaded extension. Theme files can override any subset of UI colors, syntax colors, and spinner frames/messages. Changes apply immediately; if a selected theme file is deleted, zot resets to auto. See [docs/themes.md](docs/themes.md).
M packages/agent/build.go +25 -11
@@ -15,6 +15,12 @@ import (
1515 "github.com/patriceckhart/zot/packages/provider"
1616 )
1717
18+// ContextFile is an instruction file loaded into the system prompt.
19+type ContextFile struct {
20+ Path string
21+ Content string
22+}
23+
1824 // Resolved is the effective configuration after merging CLI, config, defaults.
1925 type Resolved struct {
2026 Provider string
@@ -46,6 +52,12 @@ type Resolved struct {
4652 // discovered. Exposed so the tui can list / preview skills.
4753 SkillTool *skills.Tool
4854
55+ // ContextFiles records the AGENTS.md files appended to SystemPrompt,
56+ // in effective load order. Interactive mode uses this metadata to make
57+ // otherwise invisible startup context inspectable without adding fake
58+ // messages to the provider transcript.
59+ ContextFiles []ContextFile
60+
4961 // Bookkeeping for MergeExtensionTools. Captured at Resolve time
5062 // so the system prompt can be rebuilt later without re-running
5163 // resolve.
@@ -581,8 +593,9 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
581593
582594 summaries := toolSummaries(reg, args)
583595
596+ contextFiles := loadAgentsContext(args.CWD, ZotHome())
584597 append_ := append([]string(nil), args.AppendSystemPrompt...)
585- if agentsAddendum := readAgentsContext(args.CWD, ZotHome()); agentsAddendum != "" {
598+ if agentsAddendum := formatAgentsContext(contextFiles); agentsAddendum != "" {
586599 append_ = append(append_, agentsAddendum)
587600 }
588601 if skillAddendum != "" {
@@ -635,6 +648,7 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
635648 MaxOutput: resolvedModel.MaxOutput,
636649 Sandbox: sandbox,
637650 SkillTool: skillTool,
651+ ContextFiles: contextFiles,
638652 systemAppend: append_,
639653 systemCustom: custom,
640654 toolDescriptions: descMapFromSummaries(summaries),
@@ -658,16 +672,12 @@ func readUserSystemPrompt(zotHome string) string {
658672 return strings.TrimSpace(string(raw))
659673 }
660674
661-// readAgentsContext loads optional AGENTS.md instruction files. No
675+// loadAgentsContext loads optional AGENTS.md instruction files. No
662676 // default file is created or required: zot only includes files that
663677 // already exist. Global instructions ($ZOT_HOME/AGENTS.md) come first,
664-// followed by project instructions from the top-most parent down to cwd.
665-func readAgentsContext(cwd, zotHome string) string {
666- type contextFile struct {
667- path string
668- content string
669- }
670- var files []contextFile
678+// followed by project instructions from the filesystem root down to cwd.
679+func loadAgentsContext(cwd, zotHome string) []ContextFile {
680+ var files []ContextFile
671681 seen := map[string]bool{}
672682 add := func(path string) {
673683 if path == "" {
@@ -689,7 +699,7 @@ func readAgentsContext(cwd, zotHome string) string {
689699 return
690700 }
691701 seen[path] = true
692- files = append(files, contextFile{path: path, content: content})
702+ files = append(files, ContextFile{Path: path, Content: content})
693703 }
694704 addFirstFromDir := func(dir string) {
695705 if dir == "" {
@@ -724,13 +734,17 @@ func readAgentsContext(cwd, zotHome string) string {
724734 }
725735 }
726736
737+ return files
738+}
739+
740+func formatAgentsContext(files []ContextFile) string {
727741 if len(files) == 0 {
728742 return ""
729743 }
730744 var sb strings.Builder
731745 sb.WriteString("Project context instructions loaded from AGENTS.md files. Follow them when working in this repository. Later files are more specific and may override earlier ones.\n")
732746 for _, f := range files {
733- fmt.Fprintf(&sb, "\n## %s\n\n%s\n", f.path, f.content)
747+ fmt.Fprintf(&sb, "\n## %s\n\n%s\n", f.Path, f.Content)
734748 }
735749 return strings.TrimSpace(sb.String())
736750 }
@@ -15,6 +15,12 @@ import (
15 "github.com/patriceckhart/zot/packages/provider"15 "github.com/patriceckhart/zot/packages/provider"
16 )16 )
17 17
18+// ContextFile is an instruction file loaded into the system prompt.
19+type ContextFile struct {
20+ Path string
21+ Content string
22+}
23+
18 // Resolved is the effective configuration after merging CLI, config, defaults.24 // Resolved is the effective configuration after merging CLI, config, defaults.
19 type Resolved struct {25 type Resolved struct {
20 Provider string26 Provider string
@@ -46,6 +52,12 @@ type Resolved struct {
46 // discovered. Exposed so the tui can list / preview skills.52 // discovered. Exposed so the tui can list / preview skills.
47 SkillTool *skills.Tool53 SkillTool *skills.Tool
48 54
55+ // ContextFiles records the AGENTS.md files appended to SystemPrompt,
56+ // in effective load order. Interactive mode uses this metadata to make
57+ // otherwise invisible startup context inspectable without adding fake
58+ // messages to the provider transcript.
59+ ContextFiles []ContextFile
60+
49 // Bookkeeping for MergeExtensionTools. Captured at Resolve time61 // Bookkeeping for MergeExtensionTools. Captured at Resolve time
50 // so the system prompt can be rebuilt later without re-running62 // so the system prompt can be rebuilt later without re-running
51 // resolve.63 // resolve.
@@ -581,8 +593,9 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
581 593
582 summaries := toolSummaries(reg, args)594 summaries := toolSummaries(reg, args)
583 595
596+ contextFiles := loadAgentsContext(args.CWD, ZotHome())
584 append_ := append([]string(nil), args.AppendSystemPrompt...)597 append_ := append([]string(nil), args.AppendSystemPrompt...)
585- if agentsAddendum := readAgentsContext(args.CWD, ZotHome()); agentsAddendum != "" {598+ if agentsAddendum := formatAgentsContext(contextFiles); agentsAddendum != "" {
586 append_ = append(append_, agentsAddendum)599 append_ = append(append_, agentsAddendum)
587 }600 }
588 if skillAddendum != "" {601 if skillAddendum != "" {
@@ -635,6 +648,7 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
635 MaxOutput: resolvedModel.MaxOutput,648 MaxOutput: resolvedModel.MaxOutput,
636 Sandbox: sandbox,649 Sandbox: sandbox,
637 SkillTool: skillTool,650 SkillTool: skillTool,
651+ ContextFiles: contextFiles,
638 systemAppend: append_,652 systemAppend: append_,
639 systemCustom: custom,653 systemCustom: custom,
640 toolDescriptions: descMapFromSummaries(summaries),654 toolDescriptions: descMapFromSummaries(summaries),
@@ -658,16 +672,12 @@ func readUserSystemPrompt(zotHome string) string {
658 return strings.TrimSpace(string(raw))672 return strings.TrimSpace(string(raw))
659 }673 }
660 674
661-// readAgentsContext loads optional AGENTS.md instruction files. No675+// loadAgentsContext loads optional AGENTS.md instruction files. No
662 // default file is created or required: zot only includes files that676 // default file is created or required: zot only includes files that
663 // already exist. Global instructions ($ZOT_HOME/AGENTS.md) come first,677 // already exist. Global instructions ($ZOT_HOME/AGENTS.md) come first,
664-// followed by project instructions from the top-most parent down to cwd.678+// followed by project instructions from the filesystem root down to cwd.
665-func readAgentsContext(cwd, zotHome string) string {679+func loadAgentsContext(cwd, zotHome string) []ContextFile {
666- type contextFile struct {680+ var files []ContextFile
667- path string
668- content string
669- }
670- var files []contextFile
671 seen := map[string]bool{}681 seen := map[string]bool{}
672 add := func(path string) {682 add := func(path string) {
673 if path == "" {683 if path == "" {
@@ -689,7 +699,7 @@ func readAgentsContext(cwd, zotHome string) string {
689 return699 return
690 }700 }
691 seen[path] = true701 seen[path] = true
692- files = append(files, contextFile{path: path, content: content})702+ files = append(files, ContextFile{Path: path, Content: content})
693 }703 }
694 addFirstFromDir := func(dir string) {704 addFirstFromDir := func(dir string) {
695 if dir == "" {705 if dir == "" {
@@ -724,13 +734,17 @@ func readAgentsContext(cwd, zotHome string) string {
724 }734 }
725 }735 }
726 736
737+ return files
738+}
739+
740+func formatAgentsContext(files []ContextFile) string {
727 if len(files) == 0 {741 if len(files) == 0 {
728 return ""742 return ""
729 }743 }
730 var sb strings.Builder744 var sb strings.Builder
731 sb.WriteString("Project context instructions loaded from AGENTS.md files. Follow them when working in this repository. Later files are more specific and may override earlier ones.\n")745 sb.WriteString("Project context instructions loaded from AGENTS.md files. Follow them when working in this repository. Later files are more specific and may override earlier ones.\n")
732 for _, f := range files {746 for _, f := range files {
733- fmt.Fprintf(&sb, "\n## %s\n\n%s\n", f.path, f.content)747+ fmt.Fprintf(&sb, "\n## %s\n\n%s\n", f.Path, f.Content)
734 }748 }
735 return strings.TrimSpace(sb.String())749 return strings.TrimSpace(sb.String())
736 }750 }
M packages/agent/build_test.go +22 -4
@@ -31,10 +31,25 @@ func TestReadAgentsContextLoadsGlobalAndAncestors(t *testing.T) {
3131 t.Fatal(err)
3232 }
3333
34- got := readAgentsContext(nested, zotHome)
34+ files := loadAgentsContext(nested, zotHome)
35+ if len(files) != 3 {
36+ t.Fatalf("loaded %d context files, want 3: %#v", len(files), files)
37+ }
38+ wantPaths := []string{
39+ filepath.Join(zotHome, "AGENTS.md"),
40+ filepath.Join(project, "AGENTS.md"),
41+ filepath.Join(nested, "AGENTS.md"),
42+ }
43+ for idx, want := range wantPaths {
44+ if files[idx].Path != want {
45+ t.Fatalf("context file %d path = %q, want %q", idx, files[idx].Path, want)
46+ }
47+ }
48+
49+ got := formatAgentsContext(files)
3550 for _, want := range []string{"global rule", "repo rule", "app rule"} {
3651 if !strings.Contains(got, want) {
37- t.Fatalf("readAgentsContext missing %q in:\n%s", want, got)
52+ t.Fatalf("formatAgentsContext missing %q in:\n%s", want, got)
3853 }
3954 }
4055 if strings.Index(got, "global rule") > strings.Index(got, "repo rule") || strings.Index(got, "repo rule") > strings.Index(got, "app rule") {
@@ -43,8 +58,11 @@ func TestReadAgentsContextLoadsGlobalAndAncestors(t *testing.T) {
4358 }
4459
4560 func TestReadAgentsContextMissingFilesIsEmpty(t *testing.T) {
46- got := readAgentsContext(t.TempDir(), t.TempDir())
47- if got != "" {
61+ files := loadAgentsContext(t.TempDir(), t.TempDir())
62+ if len(files) != 0 {
63+ t.Fatalf("expected no context files, got %#v", files)
64+ }
65+ if got := formatAgentsContext(files); got != "" {
4866 t.Fatalf("expected no context, got %q", got)
4967 }
5068 }
@@ -31,10 +31,25 @@ func TestReadAgentsContextLoadsGlobalAndAncestors(t *testing.T) {
31 t.Fatal(err)31 t.Fatal(err)
32 }32 }
33 33
34- got := readAgentsContext(nested, zotHome)34+ files := loadAgentsContext(nested, zotHome)
35+ if len(files) != 3 {
36+ t.Fatalf("loaded %d context files, want 3: %#v", len(files), files)
37+ }
38+ wantPaths := []string{
39+ filepath.Join(zotHome, "AGENTS.md"),
40+ filepath.Join(project, "AGENTS.md"),
41+ filepath.Join(nested, "AGENTS.md"),
42+ }
43+ for idx, want := range wantPaths {
44+ if files[idx].Path != want {
45+ t.Fatalf("context file %d path = %q, want %q", idx, files[idx].Path, want)
46+ }
47+ }
48+
49+ got := formatAgentsContext(files)
35 for _, want := range []string{"global rule", "repo rule", "app rule"} {50 for _, want := range []string{"global rule", "repo rule", "app rule"} {
36 if !strings.Contains(got, want) {51 if !strings.Contains(got, want) {
37- t.Fatalf("readAgentsContext missing %q in:\n%s", want, got)52+ t.Fatalf("formatAgentsContext missing %q in:\n%s", want, got)
38 }53 }
39 }54 }
40 if strings.Index(got, "global rule") > strings.Index(got, "repo rule") || strings.Index(got, "repo rule") > strings.Index(got, "app rule") {55 if strings.Index(got, "global rule") > strings.Index(got, "repo rule") || strings.Index(got, "repo rule") > strings.Index(got, "app rule") {
@@ -43,8 +58,11 @@ func TestReadAgentsContextLoadsGlobalAndAncestors(t *testing.T) {
43 }58 }
44 59
45 func TestReadAgentsContextMissingFilesIsEmpty(t *testing.T) {60 func TestReadAgentsContextMissingFilesIsEmpty(t *testing.T) {
46- got := readAgentsContext(t.TempDir(), t.TempDir())61+ files := loadAgentsContext(t.TempDir(), t.TempDir())
47- if got != "" {62+ if len(files) != 0 {
63+ t.Fatalf("expected no context files, got %#v", files)
64+ }
65+ if got := formatAgentsContext(files); got != "" {
48 t.Fatalf("expected no context, got %q", got)66 t.Fatalf("expected no context, got %q", got)
49 }67 }
50 }68 }
M packages/agent/cli.go +4 -1
@@ -890,7 +890,8 @@ func runInteractive(ctx context.Context, args Args, version string) error {
890890
891891 // Push the new state into the running Interactive.
892892 if iv != nil {
893- iv.ApplyChangedCWD(newAg, newProvider, newModel, absPath)
893+ startupPaths := instructionContextPaths(loadAgentsContext(absPath, ZotHome()))
894+ iv.ApplyChangedCWDWithStartupContext(newAg, newProvider, newModel, absPath, startupPaths)
894895 }
895896
896897 // Re-scope the swarm dashboard to the new session.
@@ -1005,6 +1006,8 @@ func runInteractive(ctx context.Context, args Args, version string) error {
10051006 Tools: r.ToolRegistry,
10061007 MaxSteps: r.MaxSteps,
10071008 CWD: r.CWD,
1009+ StartupContextPaths: instructionContextPaths(r.ContextFiles),
1010+ ShowInstructionsAtStartup: initialCfg.ShowInstructionsAtStartup,
10081011 ZotHome: ZotHome(),
10091012 Version: version,
10101013 UpdateInfoChan: updateCh,
@@ -890,7 +890,8 @@ func runInteractive(ctx context.Context, args Args, version string) error {
890 890
891 // Push the new state into the running Interactive.891 // Push the new state into the running Interactive.
892 if iv != nil {892 if iv != nil {
893- iv.ApplyChangedCWD(newAg, newProvider, newModel, absPath)893+ startupPaths := instructionContextPaths(loadAgentsContext(absPath, ZotHome()))
894+ iv.ApplyChangedCWDWithStartupContext(newAg, newProvider, newModel, absPath, startupPaths)
894 }895 }
895 896
896 // Re-scope the swarm dashboard to the new session.897 // Re-scope the swarm dashboard to the new session.
@@ -1005,6 +1006,8 @@ func runInteractive(ctx context.Context, args Args, version string) error {
1005 Tools: r.ToolRegistry,1006 Tools: r.ToolRegistry,
1006 MaxSteps: r.MaxSteps,1007 MaxSteps: r.MaxSteps,
1007 CWD: r.CWD,1008 CWD: r.CWD,
1009+ StartupContextPaths: instructionContextPaths(r.ContextFiles),
1010+ ShowInstructionsAtStartup: initialCfg.ShowInstructionsAtStartup,
1008 ZotHome: ZotHome(),1011 ZotHome: ZotHome(),
1009 Version: version,1012 Version: version,
1010 UpdateInfoChan: updateCh,1013 UpdateInfoChan: updateCh,
M packages/agent/config.go +5 -0
@@ -81,6 +81,11 @@ type Config struct {
8181 // default; nil/missing means disabled. Toggle from /settings.
8282 CompactMode *bool `json:"compact_mode,omitempty"`
8383
84+ // ShowInstructionsAtStartup lists loaded AGENTS.md file paths above
85+ // the transcript. Off by default; nil/missing means disabled.
86+ // Toggle from /settings.
87+ ShowInstructionsAtStartup *bool `json:"show_instructions_at_startup,omitempty"`
88+
8489 // TUIInputStyle controls the main input rendering. Supported values:
8590 // "plain" (default), "lines", and "block".
8691 TUIInputStyle string `json:"tui_input_style,omitempty"`
@@ -81,6 +81,11 @@ type Config struct {
81 // default; nil/missing means disabled. Toggle from /settings.81 // default; nil/missing means disabled. Toggle from /settings.
82 CompactMode *bool `json:"compact_mode,omitempty"`82 CompactMode *bool `json:"compact_mode,omitempty"`
83 83
84+ // ShowInstructionsAtStartup lists loaded AGENTS.md file paths above
85+ // the transcript. Off by default; nil/missing means disabled.
86+ // Toggle from /settings.
87+ ShowInstructionsAtStartup *bool `json:"show_instructions_at_startup,omitempty"`
88+
84 // TUIInputStyle controls the main input rendering. Supported values:89 // TUIInputStyle controls the main input rendering. Supported values:
85 // "plain" (default), "lines", and "block".90 // "plain" (default), "lines", and "block".
86 TUIInputStyle string `json:"tui_input_style,omitempty"`91 TUIInputStyle string `json:"tui_input_style,omitempty"`
A packages/agent/instruction_context.go +11 -0
@@ -0,0 +1,11 @@
1+package agent
2+
3+// instructionContextPaths returns the loaded instruction paths in effective
4+// prompt order for display in interactive startup metadata.
5+func instructionContextPaths(files []ContextFile) []string {
6+ paths := make([]string, 0, len(files))
7+ for _, file := range files {
8+ paths = append(paths, file.Path)
9+ }
10+ return paths
11+}
@@ -0,0 +1,11 @@
1+package agent
2+
3+// instructionContextPaths returns the loaded instruction paths in effective
4+// prompt order for display in interactive startup metadata.
5+func instructionContextPaths(files []ContextFile) []string {
6+ paths := make([]string, 0, len(files))
7+ for _, file := range files {
8+ paths = append(paths, file.Path)
9+ }
10+ return paths
11+}
A packages/agent/instruction_context_test.go +20 -0
@@ -0,0 +1,20 @@
1+package agent
2+
3+import "testing"
4+
5+func TestInstructionContextPathsPreserveLoadOrder(t *testing.T) {
6+ files := []ContextFile{
7+ {Path: "/home/user/AGENTS.md", Content: "global rule"},
8+ {Path: "/repo/AGENTS.md", Content: "project rule"},
9+ }
10+
11+ paths := instructionContextPaths(files)
12+ if len(paths) != len(files) {
13+ t.Fatalf("got %d startup paths, want %d", len(paths), len(files))
14+ }
15+ for idx, path := range paths {
16+ if path != files[idx].Path {
17+ t.Fatalf("path %d = %q, want %q", idx, path, files[idx].Path)
18+ }
19+ }
20+}
@@ -0,0 +1,20 @@
1+package agent
2+
3+import "testing"
4+
5+func TestInstructionContextPathsPreserveLoadOrder(t *testing.T) {
6+ files := []ContextFile{
7+ {Path: "/home/user/AGENTS.md", Content: "global rule"},
8+ {Path: "/repo/AGENTS.md", Content: "project rule"},
9+ }
10+
11+ paths := instructionContextPaths(files)
12+ if len(paths) != len(files) {
13+ t.Fatalf("got %d startup paths, want %d", len(paths), len(files))
14+ }
15+ for idx, path := range paths {
16+ if path != files[idx].Path {
17+ t.Fatalf("path %d = %q, want %q", idx, path, files[idx].Path)
18+ }
19+ }
20+}
M packages/agent/modes/interactive.go +65 -14
@@ -38,6 +38,14 @@ type InteractiveConfig struct {
3838 MaxSteps int
3939 CWD string
4040
41+ // StartupContextPaths are loaded instruction file paths available to show
42+ // before the transcript. They are never sent to the provider or persisted.
43+ StartupContextPaths []string
44+
45+ // ShowInstructionsAtStartup controls whether StartupContextPaths are visible.
46+ // nil means the default, off.
47+ ShowInstructionsAtStartup *bool
48+
4149 // InlineImagesEnabled overrides terminal image rendering. nil means
4250 // auto-detect and render when supported; false disables; true uses
4351 // the detected protocol when available.
@@ -304,6 +312,10 @@ type SettingsStore interface {
304312 SetTheme(name string) error
305313 }
306314
315+type showInstructionsSettingsStore interface {
316+ SetShowInstructionsAtStartup(enabled bool) error
317+}
318+
307319 type Interactive struct {
308320 cfg InteractiveConfig
309321 view *tui.View
@@ -517,14 +529,19 @@ const resumeTailExpandStep = 80
517529 func NewInteractive(cfg InteractiveConfig) *Interactive {
518530 renderer := tui.NewRenderer(cfg.Terminal)
519531 renderer.SetTheme(cfg.Theme)
532+ startupContextPaths := []string(nil)
533+ if cfg.ShowInstructionsAtStartup != nil && *cfg.ShowInstructionsAtStartup {
534+ startupContextPaths = append(startupContextPaths, cfg.StartupContextPaths...)
535+ }
520536 i := &Interactive{
521537 cfg: cfg,
522538 view: &tui.View{
523- Theme: cfg.Theme,
524- ImageProto: effectiveImageProtocol(cfg.InlineImagesEnabled),
525- FlatTools: cfg.FlatTools,
526- CompactUser: cfg.CompactUser,
527- CompactMode: cfg.CompactMode != nil && *cfg.CompactMode,
539+ Theme: cfg.Theme,
540+ ImageProto: effectiveImageProtocol(cfg.InlineImagesEnabled),
541+ FlatTools: cfg.FlatTools,
542+ CompactUser: cfg.CompactUser,
543+ CompactMode: cfg.CompactMode != nil && *cfg.CompactMode,
544+ StartupContextPaths: startupContextPaths,
528545 },
529546 // Prompt is the standard half-block accent bar used by chat
530547 // speaker labels too, so the input gutter matches the rest
@@ -2836,19 +2853,27 @@ func (i *Interactive) Submit(text string) {
28362853 i.startTurn(i.runCtx, text)
28372854 }
28382855
2839-// ApplyChangedCWD is called by the host after a successful /cd hook.
2840-// The host has already rebuilt the agent and opened a fresh session
2841-// in the new cwd; this method swaps the fresh agent into the running
2842-// TUI, updates the displayed cwd, clears the transcript display
2843-// caches, and points the file picker at the new directory.
2844-//
2845-// The fresh agent's transcript is empty (new session) so the chat
2846-// view starts blank, matching what relaunching `zot --cwd <path>`
2847-// would show. Cost meters reset.
2856+// ApplyChangedCWD is called by hosts after a successful /cd hook that do
2857+// not provide startup context metadata.
28482858 func (i *Interactive) ApplyChangedCWD(ag *core.Agent, provider, model, cwd string) {
2859+ i.applyChangedCWD(ag, provider, model, cwd, nil)
2860+}
2861+
2862+// ApplyChangedCWDWithStartupContext swaps a rebuilt agent and its display-only
2863+// startup context into the running TUI after a successful /cd hook.
2864+func (i *Interactive) ApplyChangedCWDWithStartupContext(ag *core.Agent, provider, model, cwd string, startupContextPaths []string) {
2865+ i.applyChangedCWD(ag, provider, model, cwd, startupContextPaths)
2866+}
2867+
2868+func (i *Interactive) applyChangedCWD(ag *core.Agent, provider, model, cwd string, startupContextPaths []string) {
28492869 i.mu.Lock()
28502870 i.agent = ag
28512871 i.cfg.CWD = cwd
2872+ i.cfg.StartupContextPaths = append([]string(nil), startupContextPaths...)
2873+ i.view.StartupContextPaths = nil
2874+ if i.cfg.ShowInstructionsAtStartup != nil && *i.cfg.ShowInstructionsAtStartup {
2875+ i.view.StartupContextPaths = append(i.view.StartupContextPaths, startupContextPaths...)
2876+ }
28522877 // Re-report the working directory to the terminal so "new tab here"
28532878 // tracks the /cd change (OSC 7, see issue #38).
28542879 if i.cfg.Terminal != nil {
@@ -3062,6 +3087,7 @@ func (i *Interactive) openSettingsDialog() {
30623087 recursiveFiles := i.cfg.RecursiveFileSuggest != nil && *i.cfg.RecursiveFileSuggest
30633088 respectGitignore := i.cfg.RespectGitignore == nil || *i.cfg.RespectGitignore
30643089 compactMode := i.compactModeEnabled()
3090+ showInstructions := i.cfg.ShowInstructionsAtStartup != nil && *i.cfg.ShowInstructionsAtStartup
30653091 inputStyle := tui.NormalizeInputStyle(i.cfg.TUIInputStyle)
30663092 statusPosition := tui.NormalizeStatusPosition(i.cfg.TUIStatusPosition)
30673093 workingPosition := tui.NormalizeWorkingPosition(i.cfg.TUIWorkingPosition)
@@ -3191,6 +3217,12 @@ func (i *Interactive) openSettingsDialog() {
31913217 desc: "reduce visual chrome by rendering tool calls without boxes and sent messages without padded bubbles",
31923218 value: compactMode,
31933219 },
3220+ {
3221+ key: "show_instructions_at_startup",
3222+ label: "show loaded instructions at startup",
3223+ desc: "list loaded AGENTS.md file paths above the transcript",
3224+ value: showInstructions,
3225+ },
31943226 {
31953227 key: "tui_settings",
31963228 label: "tui settings",
@@ -3557,6 +3589,25 @@ func (i *Interactive) applySettingToggle(key string, value bool) {
35573589 i.statusOK = "compact transcript rendering " + onOff(value)
35583590 i.statusErr = ""
35593591 i.mu.Unlock()
3592+ case "show_instructions_at_startup":
3593+ val := value
3594+ i.cfg.ShowInstructionsAtStartup = &val
3595+ if store, ok := i.cfg.SettingsStore.(showInstructionsSettingsStore); ok {
3596+ if err := store.SetShowInstructionsAtStartup(value); err != nil {
3597+ i.mu.Lock()
3598+ i.statusErr = "settings: " + err.Error()
3599+ i.mu.Unlock()
3600+ return
3601+ }
3602+ }
3603+ i.mu.Lock()
3604+ i.view.StartupContextPaths = nil
3605+ if value {
3606+ i.view.StartupContextPaths = append(i.view.StartupContextPaths, i.cfg.StartupContextPaths...)
3607+ }
3608+ i.statusOK = "show loaded instructions at startup " + onOff(value)
3609+ i.statusErr = ""
3610+ i.mu.Unlock()
35603611 }
35613612 }
35623613
@@ -38,6 +38,14 @@ type InteractiveConfig struct {
38 MaxSteps int38 MaxSteps int
39 CWD string39 CWD string
40 40
41+ // StartupContextPaths are loaded instruction file paths available to show
42+ // before the transcript. They are never sent to the provider or persisted.
43+ StartupContextPaths []string
44+
45+ // ShowInstructionsAtStartup controls whether StartupContextPaths are visible.
46+ // nil means the default, off.
47+ ShowInstructionsAtStartup *bool
48+
41 // InlineImagesEnabled overrides terminal image rendering. nil means49 // InlineImagesEnabled overrides terminal image rendering. nil means
42 // auto-detect and render when supported; false disables; true uses50 // auto-detect and render when supported; false disables; true uses
43 // the detected protocol when available.51 // the detected protocol when available.
@@ -304,6 +312,10 @@ type SettingsStore interface {
304 SetTheme(name string) error312 SetTheme(name string) error
305 }313 }
306 314
315+type showInstructionsSettingsStore interface {
316+ SetShowInstructionsAtStartup(enabled bool) error
317+}
318+
307 type Interactive struct {319 type Interactive struct {
308 cfg InteractiveConfig320 cfg InteractiveConfig
309 view *tui.View321 view *tui.View
@@ -517,14 +529,19 @@ const resumeTailExpandStep = 80
517 func NewInteractive(cfg InteractiveConfig) *Interactive {529 func NewInteractive(cfg InteractiveConfig) *Interactive {
518 renderer := tui.NewRenderer(cfg.Terminal)530 renderer := tui.NewRenderer(cfg.Terminal)
519 renderer.SetTheme(cfg.Theme)531 renderer.SetTheme(cfg.Theme)
532+ startupContextPaths := []string(nil)
533+ if cfg.ShowInstructionsAtStartup != nil && *cfg.ShowInstructionsAtStartup {
534+ startupContextPaths = append(startupContextPaths, cfg.StartupContextPaths...)
535+ }
520 i := &Interactive{536 i := &Interactive{
521 cfg: cfg,537 cfg: cfg,
522 view: &tui.View{538 view: &tui.View{
523- Theme: cfg.Theme,539+ Theme: cfg.Theme,
524- ImageProto: effectiveImageProtocol(cfg.InlineImagesEnabled),540+ ImageProto: effectiveImageProtocol(cfg.InlineImagesEnabled),
525- FlatTools: cfg.FlatTools,541+ FlatTools: cfg.FlatTools,
526- CompactUser: cfg.CompactUser,542+ CompactUser: cfg.CompactUser,
527- CompactMode: cfg.CompactMode != nil && *cfg.CompactMode,543+ CompactMode: cfg.CompactMode != nil && *cfg.CompactMode,
544+ StartupContextPaths: startupContextPaths,
528 },545 },
529 // Prompt is the standard half-block accent bar used by chat546 // Prompt is the standard half-block accent bar used by chat
530 // speaker labels too, so the input gutter matches the rest547 // speaker labels too, so the input gutter matches the rest
@@ -2836,19 +2853,27 @@ func (i *Interactive) Submit(text string) {
2836 i.startTurn(i.runCtx, text)2853 i.startTurn(i.runCtx, text)
2837 }2854 }
2838 2855
2839-// ApplyChangedCWD is called by the host after a successful /cd hook.2856+// ApplyChangedCWD is called by hosts after a successful /cd hook that do
2840-// The host has already rebuilt the agent and opened a fresh session2857+// not provide startup context metadata.
2841-// in the new cwd; this method swaps the fresh agent into the running
2842-// TUI, updates the displayed cwd, clears the transcript display
2843-// caches, and points the file picker at the new directory.
2844-//
2845-// The fresh agent's transcript is empty (new session) so the chat
2846-// view starts blank, matching what relaunching `zot --cwd <path>`
2847-// would show. Cost meters reset.
2848 func (i *Interactive) ApplyChangedCWD(ag *core.Agent, provider, model, cwd string) {2858 func (i *Interactive) ApplyChangedCWD(ag *core.Agent, provider, model, cwd string) {
2859+ i.applyChangedCWD(ag, provider, model, cwd, nil)
2860+}
2861+
2862+// ApplyChangedCWDWithStartupContext swaps a rebuilt agent and its display-only
2863+// startup context into the running TUI after a successful /cd hook.
2864+func (i *Interactive) ApplyChangedCWDWithStartupContext(ag *core.Agent, provider, model, cwd string, startupContextPaths []string) {
2865+ i.applyChangedCWD(ag, provider, model, cwd, startupContextPaths)
2866+}
2867+
2868+func (i *Interactive) applyChangedCWD(ag *core.Agent, provider, model, cwd string, startupContextPaths []string) {
2849 i.mu.Lock()2869 i.mu.Lock()
2850 i.agent = ag2870 i.agent = ag
2851 i.cfg.CWD = cwd2871 i.cfg.CWD = cwd
2872+ i.cfg.StartupContextPaths = append([]string(nil), startupContextPaths...)
2873+ i.view.StartupContextPaths = nil
2874+ if i.cfg.ShowInstructionsAtStartup != nil && *i.cfg.ShowInstructionsAtStartup {
2875+ i.view.StartupContextPaths = append(i.view.StartupContextPaths, startupContextPaths...)
2876+ }
2852 // Re-report the working directory to the terminal so "new tab here"2877 // Re-report the working directory to the terminal so "new tab here"
2853 // tracks the /cd change (OSC 7, see issue #38).2878 // tracks the /cd change (OSC 7, see issue #38).
2854 if i.cfg.Terminal != nil {2879 if i.cfg.Terminal != nil {
@@ -3062,6 +3087,7 @@ func (i *Interactive) openSettingsDialog() {
3062 recursiveFiles := i.cfg.RecursiveFileSuggest != nil && *i.cfg.RecursiveFileSuggest3087 recursiveFiles := i.cfg.RecursiveFileSuggest != nil && *i.cfg.RecursiveFileSuggest
3063 respectGitignore := i.cfg.RespectGitignore == nil || *i.cfg.RespectGitignore3088 respectGitignore := i.cfg.RespectGitignore == nil || *i.cfg.RespectGitignore
3064 compactMode := i.compactModeEnabled()3089 compactMode := i.compactModeEnabled()
3090+ showInstructions := i.cfg.ShowInstructionsAtStartup != nil && *i.cfg.ShowInstructionsAtStartup
3065 inputStyle := tui.NormalizeInputStyle(i.cfg.TUIInputStyle)3091 inputStyle := tui.NormalizeInputStyle(i.cfg.TUIInputStyle)
3066 statusPosition := tui.NormalizeStatusPosition(i.cfg.TUIStatusPosition)3092 statusPosition := tui.NormalizeStatusPosition(i.cfg.TUIStatusPosition)
3067 workingPosition := tui.NormalizeWorkingPosition(i.cfg.TUIWorkingPosition)3093 workingPosition := tui.NormalizeWorkingPosition(i.cfg.TUIWorkingPosition)
@@ -3191,6 +3217,12 @@ func (i *Interactive) openSettingsDialog() {
3191 desc: "reduce visual chrome by rendering tool calls without boxes and sent messages without padded bubbles",3217 desc: "reduce visual chrome by rendering tool calls without boxes and sent messages without padded bubbles",
3192 value: compactMode,3218 value: compactMode,
3193 },3219 },
3220+ {
3221+ key: "show_instructions_at_startup",
3222+ label: "show loaded instructions at startup",
3223+ desc: "list loaded AGENTS.md file paths above the transcript",
3224+ value: showInstructions,
3225+ },
3194 {3226 {
3195 key: "tui_settings",3227 key: "tui_settings",
3196 label: "tui settings",3228 label: "tui settings",
@@ -3557,6 +3589,25 @@ func (i *Interactive) applySettingToggle(key string, value bool) {
3557 i.statusOK = "compact transcript rendering " + onOff(value)3589 i.statusOK = "compact transcript rendering " + onOff(value)
3558 i.statusErr = ""3590 i.statusErr = ""
3559 i.mu.Unlock()3591 i.mu.Unlock()
3592+ case "show_instructions_at_startup":
3593+ val := value
3594+ i.cfg.ShowInstructionsAtStartup = &val
3595+ if store, ok := i.cfg.SettingsStore.(showInstructionsSettingsStore); ok {
3596+ if err := store.SetShowInstructionsAtStartup(value); err != nil {
3597+ i.mu.Lock()
3598+ i.statusErr = "settings: " + err.Error()
3599+ i.mu.Unlock()
3600+ return
3601+ }
3602+ }
3603+ i.mu.Lock()
3604+ i.view.StartupContextPaths = nil
3605+ if value {
3606+ i.view.StartupContextPaths = append(i.view.StartupContextPaths, i.cfg.StartupContextPaths...)
3607+ }
3608+ i.statusOK = "show loaded instructions at startup " + onOff(value)
3609+ i.statusErr = ""
3610+ i.mu.Unlock()
3560 }3611 }
3561 }3612 }
3562 3613
A packages/agent/modes/startup_instructions_test.go +30 -0
@@ -0,0 +1,30 @@
1+package modes
2+
3+import "testing"
4+
5+func TestStartupInstructionsAreOptIn(t *testing.T) {
6+ paths := []string{"/repo/AGENTS.md"}
7+
8+ disabled := NewInteractive(InteractiveConfig{StartupContextPaths: paths})
9+ disabled.rend = nil
10+ if len(disabled.view.StartupContextPaths) != 0 {
11+ t.Fatalf("default startup rendered %d instruction paths", len(disabled.view.StartupContextPaths))
12+ }
13+ disabled.applySettingToggle("show_instructions_at_startup", true)
14+ if len(disabled.view.StartupContextPaths) != 1 {
15+ t.Fatalf("live toggle rendered %d instruction paths, want 1", len(disabled.view.StartupContextPaths))
16+ }
17+ disabled.applySettingToggle("show_instructions_at_startup", false)
18+ if len(disabled.view.StartupContextPaths) != 0 {
19+ t.Fatalf("disabled live toggle left %d instruction paths", len(disabled.view.StartupContextPaths))
20+ }
21+
22+ enabledValue := true
23+ enabled := NewInteractive(InteractiveConfig{
24+ StartupContextPaths: paths,
25+ ShowInstructionsAtStartup: &enabledValue,
26+ })
27+ if len(enabled.view.StartupContextPaths) != 1 {
28+ t.Fatalf("enabled startup rendered %d instruction paths, want 1", len(enabled.view.StartupContextPaths))
29+ }
30+}
@@ -0,0 +1,30 @@
1+package modes
2+
3+import "testing"
4+
5+func TestStartupInstructionsAreOptIn(t *testing.T) {
6+ paths := []string{"/repo/AGENTS.md"}
7+
8+ disabled := NewInteractive(InteractiveConfig{StartupContextPaths: paths})
9+ disabled.rend = nil
10+ if len(disabled.view.StartupContextPaths) != 0 {
11+ t.Fatalf("default startup rendered %d instruction paths", len(disabled.view.StartupContextPaths))
12+ }
13+ disabled.applySettingToggle("show_instructions_at_startup", true)
14+ if len(disabled.view.StartupContextPaths) != 1 {
15+ t.Fatalf("live toggle rendered %d instruction paths, want 1", len(disabled.view.StartupContextPaths))
16+ }
17+ disabled.applySettingToggle("show_instructions_at_startup", false)
18+ if len(disabled.view.StartupContextPaths) != 0 {
19+ t.Fatalf("disabled live toggle left %d instruction paths", len(disabled.view.StartupContextPaths))
20+ }
21+
22+ enabledValue := true
23+ enabled := NewInteractive(InteractiveConfig{
24+ StartupContextPaths: paths,
25+ ShowInstructionsAtStartup: &enabledValue,
26+ })
27+ if len(enabled.view.StartupContextPaths) != 1 {
28+ t.Fatalf("enabled startup rendered %d instruction paths, want 1", len(enabled.view.StartupContextPaths))
29+ }
30+}
M packages/agent/settings_store.go +9 -0
@@ -86,6 +86,15 @@ func (configSettingsStore) SetCompactMode(enabled bool) error {
8686 return SaveConfig(cfg)
8787 }
8888
89+func (configSettingsStore) SetShowInstructionsAtStartup(enabled bool) error {
90+ cfg, err := LoadConfig()
91+ if err != nil {
92+ return err
93+ }
94+ cfg.ShowInstructionsAtStartup = &enabled
95+ return SaveConfig(cfg)
96+}
97+
8998 func (configSettingsStore) SetTUIInputStyle(style string) error {
9099 cfg, err := LoadConfig()
91100 if err != nil {
@@ -86,6 +86,15 @@ func (configSettingsStore) SetCompactMode(enabled bool) error {
86 return SaveConfig(cfg)86 return SaveConfig(cfg)
87 }87 }
88 88
89+func (configSettingsStore) SetShowInstructionsAtStartup(enabled bool) error {
90+ cfg, err := LoadConfig()
91+ if err != nil {
92+ return err
93+ }
94+ cfg.ShowInstructionsAtStartup = &enabled
95+ return SaveConfig(cfg)
96+}
97+
89 func (configSettingsStore) SetTUIInputStyle(style string) error {98 func (configSettingsStore) SetTUIInputStyle(style string) error {
90 cfg, err := LoadConfig()99 cfg, err := LoadConfig()
91 if err != nil {100 if err != nil {
M packages/agent/settings_store_test.go +21 -0
@@ -2,6 +2,27 @@ package agent
22
33 import "testing"
44
5+func TestConfigSettingsStorePersistsShowInstructionsAtStartup(t *testing.T) {
6+ t.Setenv("ZOT_HOME", t.TempDir())
7+ if err := SaveConfig(Config{Theme: "dark"}); err != nil {
8+ t.Fatal(err)
9+ }
10+
11+ if err := (configSettingsStore{}).SetShowInstructionsAtStartup(true); err != nil {
12+ t.Fatal(err)
13+ }
14+ cfg, err := LoadConfig()
15+ if err != nil {
16+ t.Fatal(err)
17+ }
18+ if cfg.ShowInstructionsAtStartup == nil || !*cfg.ShowInstructionsAtStartup {
19+ t.Fatal("show_instructions_at_startup was not persisted as enabled")
20+ }
21+ if cfg.Theme != "dark" {
22+ t.Fatalf("unrelated config changed: theme = %q, want dark", cfg.Theme)
23+ }
24+}
25+
526 func TestConfigSettingsStorePersistsJailByDefault(t *testing.T) {
627 t.Setenv("ZOT_HOME", t.TempDir())
728 if err := SaveConfig(Config{Theme: "dark"}); err != nil {
@@ -2,6 +2,27 @@ package agent
2 2
3 import "testing"3 import "testing"
4 4
5+func TestConfigSettingsStorePersistsShowInstructionsAtStartup(t *testing.T) {
6+ t.Setenv("ZOT_HOME", t.TempDir())
7+ if err := SaveConfig(Config{Theme: "dark"}); err != nil {
8+ t.Fatal(err)
9+ }
10+
11+ if err := (configSettingsStore{}).SetShowInstructionsAtStartup(true); err != nil {
12+ t.Fatal(err)
13+ }
14+ cfg, err := LoadConfig()
15+ if err != nil {
16+ t.Fatal(err)
17+ }
18+ if cfg.ShowInstructionsAtStartup == nil || !*cfg.ShowInstructionsAtStartup {
19+ t.Fatal("show_instructions_at_startup was not persisted as enabled")
20+ }
21+ if cfg.Theme != "dark" {
22+ t.Fatalf("unrelated config changed: theme = %q, want dark", cfg.Theme)
23+ }
24+}
25+
5 func TestConfigSettingsStorePersistsJailByDefault(t *testing.T) {26 func TestConfigSettingsStorePersistsJailByDefault(t *testing.T) {
6 t.Setenv("ZOT_HOME", t.TempDir())27 t.Setenv("ZOT_HOME", t.TempDir())
7 if err := SaveConfig(Config{Theme: "dark"}); err != nil {28 if err := SaveConfig(Config{Theme: "dark"}); err != nil {
M packages/tui/view.go +22 -2
@@ -122,8 +122,12 @@ type View struct {
122122 Streaming string // current assistant text delta
123123 StreamingActive bool
124124 ToolCalls []ToolCallView // tool calls in flight or completed
125- StatusLine string
126- Err string
125+
126+ // StartupContextPaths lists host-loaded instruction files before the
127+ // transcript without creating provider messages or persisted entries.
128+ StartupContextPaths []string
129+ StatusLine string
130+ Err string
127131
128132 // liveBodyHigh tracks the tallest live-preview body height seen
129133 // per tool-call id, so a streaming edit/write/bash box never
@@ -323,6 +327,21 @@ func (v *View) renderErr(width int) []string {
323327 return out
324328 }
325329
330+func (v *View) renderStartupContext(width int) []string {
331+ if len(v.StartupContextPaths) == 0 {
332+ return nil
333+ }
334+ bodyWidth := width - 2
335+ if bodyWidth < 8 {
336+ bodyWidth = 8
337+ }
338+ out := []string{v.Theme.FG256(v.Theme.Accent, "[Context]")}
339+ for _, line := range wrapLine(strings.Join(v.StartupContextPaths, ", "), bodyWidth, "") {
340+ out = append(out, v.Theme.FG256(v.Theme.Muted, " "+line))
341+ }
342+ return append(out, "")
343+}
344+
326345 // BuildWithAnchors is like Build but additionally reports the first
327346 // row occupied by each message in v.Messages. Callers that need to
328347 // scroll to a specific turn (the /jump dialog) use the anchor slice
@@ -396,6 +415,7 @@ func (v *View) BuildWithAnchors(width int) ([]string, []MessageAnchor) {
396415 }
397416
398417 out := make([]string, 0, total+16)
418+ out = append(out, v.renderStartupContext(width)...)
399419 anchors := make([]MessageAnchor, 0, len(v.Messages))
400420 for idx := range v.Messages {
401421 anchors = append(anchors, MessageAnchor{MessageIdx: idx, Row: len(out)})
@@ -122,8 +122,12 @@ type View struct {
122 Streaming string // current assistant text delta122 Streaming string // current assistant text delta
123 StreamingActive bool123 StreamingActive bool
124 ToolCalls []ToolCallView // tool calls in flight or completed124 ToolCalls []ToolCallView // tool calls in flight or completed
125- StatusLine string125+
126- Err string126+ // StartupContextPaths lists host-loaded instruction files before the
127+ // transcript without creating provider messages or persisted entries.
128+ StartupContextPaths []string
129+ StatusLine string
130+ Err string
127 131
128 // liveBodyHigh tracks the tallest live-preview body height seen132 // liveBodyHigh tracks the tallest live-preview body height seen
129 // per tool-call id, so a streaming edit/write/bash box never133 // per tool-call id, so a streaming edit/write/bash box never
@@ -323,6 +327,21 @@ func (v *View) renderErr(width int) []string {
323 return out327 return out
324 }328 }
325 329
330+func (v *View) renderStartupContext(width int) []string {
331+ if len(v.StartupContextPaths) == 0 {
332+ return nil
333+ }
334+ bodyWidth := width - 2
335+ if bodyWidth < 8 {
336+ bodyWidth = 8
337+ }
338+ out := []string{v.Theme.FG256(v.Theme.Accent, "[Context]")}
339+ for _, line := range wrapLine(strings.Join(v.StartupContextPaths, ", "), bodyWidth, "") {
340+ out = append(out, v.Theme.FG256(v.Theme.Muted, " "+line))
341+ }
342+ return append(out, "")
343+}
344+
326 // BuildWithAnchors is like Build but additionally reports the first345 // BuildWithAnchors is like Build but additionally reports the first
327 // row occupied by each message in v.Messages. Callers that need to346 // row occupied by each message in v.Messages. Callers that need to
328 // scroll to a specific turn (the /jump dialog) use the anchor slice347 // scroll to a specific turn (the /jump dialog) use the anchor slice
@@ -396,6 +415,7 @@ func (v *View) BuildWithAnchors(width int) ([]string, []MessageAnchor) {
396 }415 }
397 416
398 out := make([]string, 0, total+16)417 out := make([]string, 0, total+16)
418+ out = append(out, v.renderStartupContext(width)...)
399 anchors := make([]MessageAnchor, 0, len(v.Messages))419 anchors := make([]MessageAnchor, 0, len(v.Messages))
400 for idx := range v.Messages {420 for idx := range v.Messages {
401 anchors = append(anchors, MessageAnchor{MessageIdx: idx, Row: len(out)})421 anchors = append(anchors, MessageAnchor{MessageIdx: idx, Row: len(out)})
M packages/tui/view_tool_overlay_test.go +22 -0
@@ -8,6 +8,28 @@ import (
88 "github.com/patriceckhart/zot/packages/provider"
99 )
1010
11+func TestStartupContextRendersOnlyLoadedPaths(t *testing.T) {
12+ v := View{
13+ Theme: Dark,
14+ StartupContextPaths: []string{
15+ "/home/user/AGENTS.md",
16+ "/repo/AGENTS.md",
17+ },
18+ }
19+
20+ plain := stripANSI(strings.Join(v.Build(80), "\n"))
21+ if !strings.Contains(plain, "[Context]") ||
22+ !strings.Contains(plain, "/home/user/AGENTS.md, /repo/AGENTS.md") {
23+ t.Fatalf("startup instruction paths were not rendered:\n%s", plain)
24+ }
25+ if strings.ContainsAny(plain, boxGlyphs) || strings.Contains(plain, "read ") {
26+ t.Fatalf("startup instruction paths rendered as a tool call:\n%s", plain)
27+ }
28+ if len(v.Messages) != 0 {
29+ t.Fatalf("startup context added %d transcript messages", len(v.Messages))
30+ }
31+}
32+
1133 func TestLiveToolOverlayRemainsAfterAssistantToolUse(t *testing.T) {
1234 args := json.RawMessage(`{"command":"sleep 1"}`)
1335 v := View{
@@ -8,6 +8,28 @@ import (
8 "github.com/patriceckhart/zot/packages/provider"8 "github.com/patriceckhart/zot/packages/provider"
9 )9 )
10 10
11+func TestStartupContextRendersOnlyLoadedPaths(t *testing.T) {
12+ v := View{
13+ Theme: Dark,
14+ StartupContextPaths: []string{
15+ "/home/user/AGENTS.md",
16+ "/repo/AGENTS.md",
17+ },
18+ }
19+
20+ plain := stripANSI(strings.Join(v.Build(80), "\n"))
21+ if !strings.Contains(plain, "[Context]") ||
22+ !strings.Contains(plain, "/home/user/AGENTS.md, /repo/AGENTS.md") {
23+ t.Fatalf("startup instruction paths were not rendered:\n%s", plain)
24+ }
25+ if strings.ContainsAny(plain, boxGlyphs) || strings.Contains(plain, "read ") {
26+ t.Fatalf("startup instruction paths rendered as a tool call:\n%s", plain)
27+ }
28+ if len(v.Messages) != 0 {
29+ t.Fatalf("startup context added %d transcript messages", len(v.Messages))
30+ }
31+}
32+
11 func TestLiveToolOverlayRemainsAfterAssistantToolUse(t *testing.T) {33 func TestLiveToolOverlayRemainsAfterAssistantToolUse(t *testing.T) {
12 args := json.RawMessage(`{"command":"sleep 1"}`)34 args := json.RawMessage(`{"command":"sleep 1"}`)
13 v := View{35 v := View{

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close