feat: show loaded startup resources
- commit
- 0341161b2c423a7cd200dbe00b2cd4d639587af9
- author
- patriceckhart · · 2026-07-24 07:40 UTC
- parent
- 5561cbd
- signature
- unsigned
9 files · +162 -57
M README.md +2 -2
| @@ -137,7 +137,7 @@ zot discovers `AGENTS.md` files automatically at startup and loads them in this | |||
| 137 | 137 | 1. `$ZOT_HOME/AGENTS.md` (global, machine-wide instructions that apply to every project). | |
| 138 | 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. 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. | |
| 140 | + | All discovered files are appended to the prompt in that order, so a global baseline can be refined per project. To list loaded context paths, extensions, and user-installed skills above the interactive transcript, enable **show loaded resources at startup** in `/settings`. The setting is off by default, built-in skills are omitted, and the displayed lists are not sent to the model or stored in the session transcript. | |
| 141 | 141 | ||
| 142 | 142 | For a general, non-project-specific instruction set, put your rules in `$ZOT_HOME/AGENTS.md`, for example: | |
| 143 | 143 | ||
| @@ -358,7 +358,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o | |||
| 358 | 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 | 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 | 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 | + | - **show loaded resources at startup**: list loaded `AGENTS.md` paths, extensions, and user-installed skills in compact sections above the transcript. Built-in skills are omitted. Off by default. Changes apply immediately and persist to `config.json` as `show_instructions_at_startup`. | |
| 362 | 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). | |
| 363 | 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. | |
| 364 | 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). | |
| @@ -137,7 +137,7 @@ zot discovers `AGENTS.md` files automatically at startup and loads them in this | |||||
| 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 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. | 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. 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. | 140 | + | All discovered files are appended to the prompt in that order, so a global baseline can be refined per project. To list loaded context paths, extensions, and user-installed skills above the interactive transcript, enable **show loaded resources at startup** in `/settings`. The setting is off by default, built-in skills are omitted, and the displayed lists are 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,7 +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 | + | - **show loaded resources at startup**: list loaded `AGENTS.md` paths, extensions, and user-installed skills in compact sections above the transcript. Built-in skills are omitted. Off by default. Changes apply immediately and persist to `config.json` as `show_instructions_at_startup`. |
| 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 | - **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). | ||
| 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 | - **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. | ||
| 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). | 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/cli.go +7 -0
| @@ -978,6 +978,11 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 978 | 978 | // agents so they don't outlive their parent zot. | |
| 979 | 979 | defer swarmMgr.StopAll() | |
| 980 | 980 | ||
| 981 | + | var startupSkills []*skills.Skill | |
| 982 | + | if r.SkillTool != nil { | |
| 983 | + | startupSkills = r.SkillTool.Skills() | |
| 984 | + | } | |
| 985 | + | ||
| 981 | 986 | iv = modes.NewInteractive(modes.InteractiveConfig{ | |
| 982 | 987 | Terminal: term, | |
| 983 | 988 | Theme: theme, | |
| @@ -1007,6 +1012,8 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 1007 | 1012 | MaxSteps: r.MaxSteps, | |
| 1008 | 1013 | CWD: r.CWD, | |
| 1009 | 1014 | StartupContextPaths: instructionContextPaths(r.ContextFiles), | |
| 1015 | + | StartupExtensionNames: startupExtensionNames(extMgr.All()), | |
| 1016 | + | StartupSkillNames: startupSkillNames(startupSkills), | |
| 1010 | 1017 | ShowInstructionsAtStartup: initialCfg.ShowInstructionsAtStartup, | |
| 1011 | 1018 | ZotHome: ZotHome(), | |
| 1012 | 1019 | Version: version, | |
| @@ -978,6 +978,11 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 978 | // agents so they don't outlive their parent zot. | 978 | // agents so they don't outlive their parent zot. | ||
| 979 | defer swarmMgr.StopAll() | 979 | defer swarmMgr.StopAll() | ||
| 980 | 980 | ||||
| 981 | + | var startupSkills []*skills.Skill | |||
| 982 | + | if r.SkillTool != nil { | |||
| 983 | + | startupSkills = r.SkillTool.Skills() | |||
| 984 | + | } | |||
| 985 | + | ||||
| 981 | iv = modes.NewInteractive(modes.InteractiveConfig{ | 986 | iv = modes.NewInteractive(modes.InteractiveConfig{ | ||
| 982 | Terminal: term, | 987 | Terminal: term, | ||
| 983 | Theme: theme, | 988 | Theme: theme, | ||
| @@ -1007,6 +1012,8 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 1007 | MaxSteps: r.MaxSteps, | 1012 | MaxSteps: r.MaxSteps, | ||
| 1008 | CWD: r.CWD, | 1013 | CWD: r.CWD, | ||
| 1009 | StartupContextPaths: instructionContextPaths(r.ContextFiles), | 1014 | StartupContextPaths: instructionContextPaths(r.ContextFiles), | ||
| 1015 | + | StartupExtensionNames: startupExtensionNames(extMgr.All()), | |||
| 1016 | + | StartupSkillNames: startupSkillNames(startupSkills), | |||
| 1010 | ShowInstructionsAtStartup: initialCfg.ShowInstructionsAtStartup, | 1017 | ShowInstructionsAtStartup: initialCfg.ShowInstructionsAtStartup, | ||
| 1011 | ZotHome: ZotHome(), | 1018 | ZotHome: ZotHome(), | ||
| 1012 | Version: version, | 1019 | Version: version, | ||
M packages/agent/config.go +3 -3
| @@ -81,9 +81,9 @@ type Config struct { | |||
| 81 | 81 | // default; nil/missing means disabled. Toggle from /settings. | |
| 82 | 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. | |
| 84 | + | // ShowInstructionsAtStartup lists loaded context files, extensions, | |
| 85 | + | // and user-installed skills above the transcript. Off by default; | |
| 86 | + | // nil/missing means disabled. Toggle from /settings. | |
| 87 | 87 | ShowInstructionsAtStartup *bool `json:"show_instructions_at_startup,omitempty"` | |
| 88 | 88 | ||
| 89 | 89 | // TUIInputStyle controls the main input rendering. Supported values: | |
| @@ -81,9 +81,9 @@ 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 | 84 | + | // ShowInstructionsAtStartup lists loaded context files, extensions, |
| 85 | - | // the transcript. Off by default; nil/missing means disabled. | 85 | + | // and user-installed skills above the transcript. Off by default; |
| 86 | - | // Toggle from /settings. | 86 | + | // nil/missing means disabled. Toggle from /settings. |
| 87 | ShowInstructionsAtStartup *bool `json:"show_instructions_at_startup,omitempty"` | 87 | ShowInstructionsAtStartup *bool `json:"show_instructions_at_startup,omitempty"` | ||
| 88 | 88 | ||||
| 89 | // TUIInputStyle controls the main input rendering. Supported values: | 89 | // TUIInputStyle controls the main input rendering. Supported values: | ||
M packages/agent/instruction_context.go +31 -0
| @@ -1,5 +1,12 @@ | |||
| 1 | 1 | package agent | |
| 2 | 2 | ||
| 3 | + | import ( | |
| 4 | + | "sort" | |
| 5 | + | ||
| 6 | + | "github.com/patriceckhart/zot/packages/agent/extensions" | |
| 7 | + | "github.com/patriceckhart/zot/packages/agent/skills" | |
| 8 | + | ) | |
| 9 | + | ||
| 3 | 10 | // instructionContextPaths returns the loaded instruction paths in effective | |
| 4 | 11 | // prompt order for display in interactive startup metadata. | |
| 5 | 12 | func instructionContextPaths(files []ContextFile) []string { | |
| @@ -9,3 +16,27 @@ func instructionContextPaths(files []ContextFile) []string { | |||
| 9 | 16 | } | |
| 10 | 17 | return paths | |
| 11 | 18 | } | |
| 19 | + | ||
| 20 | + | func startupExtensionNames(exts []*extensions.Extension) []string { | |
| 21 | + | names := make([]string, 0, len(exts)) | |
| 22 | + | for _, ext := range exts { | |
| 23 | + | if ext == nil || !ext.Manifest.IsEnabled() || ext.Manifest.Name == "" { | |
| 24 | + | continue | |
| 25 | + | } | |
| 26 | + | names = append(names, ext.Manifest.Name) | |
| 27 | + | } | |
| 28 | + | sort.Strings(names) | |
| 29 | + | return names | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | func startupSkillNames(discovered []*skills.Skill) []string { | |
| 33 | + | visible := skills.VisibleSkills(discovered) | |
| 34 | + | names := make([]string, 0, len(visible)) | |
| 35 | + | for _, skill := range visible { | |
| 36 | + | if skill.Name != "" { | |
| 37 | + | names = append(names, skill.Name) | |
| 38 | + | } | |
| 39 | + | } | |
| 40 | + | sort.Strings(names) | |
| 41 | + | return names | |
| 42 | + | } | |
| @@ -1,5 +1,12 @@ | |||||
| 1 | package agent | 1 | package agent | ||
| 2 | 2 | ||||
| 3 | + | import ( | |||
| 4 | + | "sort" | |||
| 5 | + | ||||
| 6 | + | "github.com/patriceckhart/zot/packages/agent/extensions" | |||
| 7 | + | "github.com/patriceckhart/zot/packages/agent/skills" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 3 | // instructionContextPaths returns the loaded instruction paths in effective | 10 | // instructionContextPaths returns the loaded instruction paths in effective | ||
| 4 | // prompt order for display in interactive startup metadata. | 11 | // prompt order for display in interactive startup metadata. | ||
| 5 | func instructionContextPaths(files []ContextFile) []string { | 12 | func instructionContextPaths(files []ContextFile) []string { | ||
| @@ -9,3 +16,27 @@ func instructionContextPaths(files []ContextFile) []string { | |||||
| 9 | } | 16 | } | ||
| 10 | return paths | 17 | return paths | ||
| 11 | } | 18 | } | ||
| 19 | + | ||||
| 20 | + | func startupExtensionNames(exts []*extensions.Extension) []string { | |||
| 21 | + | names := make([]string, 0, len(exts)) | |||
| 22 | + | for _, ext := range exts { | |||
| 23 | + | if ext == nil || !ext.Manifest.IsEnabled() || ext.Manifest.Name == "" { | |||
| 24 | + | continue | |||
| 25 | + | } | |||
| 26 | + | names = append(names, ext.Manifest.Name) | |||
| 27 | + | } | |||
| 28 | + | sort.Strings(names) | |||
| 29 | + | return names | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | func startupSkillNames(discovered []*skills.Skill) []string { | |||
| 33 | + | visible := skills.VisibleSkills(discovered) | |||
| 34 | + | names := make([]string, 0, len(visible)) | |||
| 35 | + | for _, skill := range visible { | |||
| 36 | + | if skill.Name != "" { | |||
| 37 | + | names = append(names, skill.Name) | |||
| 38 | + | } | |||
| 39 | + | } | |||
| 40 | + | sort.Strings(names) | |||
| 41 | + | return names | |||
| 42 | + | } | |||
M packages/agent/instruction_context_test.go +31 -1
| @@ -1,6 +1,12 @@ | |||
| 1 | 1 | package agent | |
| 2 | 2 | ||
| 3 | - | import "testing" | |
| 3 | + | import ( | |
| 4 | + | "reflect" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/patriceckhart/zot/packages/agent/extensions" | |
| 8 | + | "github.com/patriceckhart/zot/packages/agent/skills" | |
| 9 | + | ) | |
| 4 | 10 | ||
| 5 | 11 | func TestInstructionContextPathsPreserveLoadOrder(t *testing.T) { | |
| 6 | 12 | files := []ContextFile{ | |
| @@ -18,3 +24,27 @@ func TestInstructionContextPathsPreserveLoadOrder(t *testing.T) { | |||
| 18 | 24 | } | |
| 19 | 25 | } | |
| 20 | 26 | } | |
| 27 | + | ||
| 28 | + | func TestStartupExtensionNamesAreSortedAndEnabled(t *testing.T) { | |
| 29 | + | disabled := false | |
| 30 | + | exts := []*extensions.Extension{ | |
| 31 | + | {Manifest: extensions.Manifest{Name: "zeta"}}, | |
| 32 | + | {Manifest: extensions.Manifest{Name: "disabled", Enabled: &disabled}}, | |
| 33 | + | {Manifest: extensions.Manifest{Name: "alpha"}}, | |
| 34 | + | nil, | |
| 35 | + | } | |
| 36 | + | if got, want := startupExtensionNames(exts), []string{"alpha", "zeta"}; !reflect.DeepEqual(got, want) { | |
| 37 | + | t.Fatalf("startupExtensionNames() = %v, want %v", got, want) | |
| 38 | + | } | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | func TestStartupSkillNamesExcludeBuiltins(t *testing.T) { | |
| 42 | + | discovered := []*skills.Skill{ | |
| 43 | + | {Name: "zeta"}, | |
| 44 | + | {Name: "internal", Builtin: true}, | |
| 45 | + | {Name: "alpha"}, | |
| 46 | + | } | |
| 47 | + | if got, want := startupSkillNames(discovered), []string{"alpha", "zeta"}; !reflect.DeepEqual(got, want) { | |
| 48 | + | t.Fatalf("startupSkillNames() = %v, want %v", got, want) | |
| 49 | + | } | |
| 50 | + | } | |
| @@ -1,6 +1,12 @@ | |||||
| 1 | package agent | 1 | package agent | ||
| 2 | 2 | ||||
| 3 | - | import "testing" | 3 | + | import ( |
| 4 | + | "reflect" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/patriceckhart/zot/packages/agent/extensions" | |||
| 8 | + | "github.com/patriceckhart/zot/packages/agent/skills" | |||
| 9 | + | ) | |||
| 4 | 10 | ||||
| 5 | func TestInstructionContextPathsPreserveLoadOrder(t *testing.T) { | 11 | func TestInstructionContextPathsPreserveLoadOrder(t *testing.T) { | ||
| 6 | files := []ContextFile{ | 12 | files := []ContextFile{ | ||
| @@ -18,3 +24,27 @@ func TestInstructionContextPathsPreserveLoadOrder(t *testing.T) { | |||||
| 18 | } | 24 | } | ||
| 19 | } | 25 | } | ||
| 20 | } | 26 | } | ||
| 27 | + | ||||
| 28 | + | func TestStartupExtensionNamesAreSortedAndEnabled(t *testing.T) { | |||
| 29 | + | disabled := false | |||
| 30 | + | exts := []*extensions.Extension{ | |||
| 31 | + | {Manifest: extensions.Manifest{Name: "zeta"}}, | |||
| 32 | + | {Manifest: extensions.Manifest{Name: "disabled", Enabled: &disabled}}, | |||
| 33 | + | {Manifest: extensions.Manifest{Name: "alpha"}}, | |||
| 34 | + | nil, | |||
| 35 | + | } | |||
| 36 | + | if got, want := startupExtensionNames(exts), []string{"alpha", "zeta"}; !reflect.DeepEqual(got, want) { | |||
| 37 | + | t.Fatalf("startupExtensionNames() = %v, want %v", got, want) | |||
| 38 | + | } | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | func TestStartupSkillNamesExcludeBuiltins(t *testing.T) { | |||
| 42 | + | discovered := []*skills.Skill{ | |||
| 43 | + | {Name: "zeta"}, | |||
| 44 | + | {Name: "internal", Builtin: true}, | |||
| 45 | + | {Name: "alpha"}, | |||
| 46 | + | } | |||
| 47 | + | if got, want := startupSkillNames(discovered), []string{"alpha", "zeta"}; !reflect.DeepEqual(got, want) { | |||
| 48 | + | t.Fatalf("startupSkillNames() = %v, want %v", got, want) | |||
| 49 | + | } | |||
| 50 | + | } | |||
M packages/agent/modes/interactive.go +25 -13
| @@ -38,11 +38,13 @@ type InteractiveConfig struct { | |||
| 38 | 38 | MaxSteps int | |
| 39 | 39 | 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 | |
| 41 | + | // Startup resource fields are loaded inputs available to list before the | |
| 42 | + | // transcript. They are never sent to the provider or persisted by the view. | |
| 43 | + | StartupContextPaths []string | |
| 44 | + | StartupExtensionNames []string | |
| 45 | + | StartupSkillNames []string | |
| 44 | 46 | ||
| 45 | - | // ShowInstructionsAtStartup controls whether StartupContextPaths are visible. | |
| 47 | + | // ShowInstructionsAtStartup controls whether startup resources are visible. | |
| 46 | 48 | // nil means the default, off. | |
| 47 | 49 | ShowInstructionsAtStartup *bool | |
| 48 | 50 | ||
| @@ -530,18 +532,24 @@ func NewInteractive(cfg InteractiveConfig) *Interactive { | |||
| 530 | 532 | renderer := tui.NewRenderer(cfg.Terminal) | |
| 531 | 533 | renderer.SetTheme(cfg.Theme) | |
| 532 | 534 | startupContextPaths := []string(nil) | |
| 535 | + | startupExtensionNames := []string(nil) | |
| 536 | + | startupSkillNames := []string(nil) | |
| 533 | 537 | if cfg.ShowInstructionsAtStartup != nil && *cfg.ShowInstructionsAtStartup { | |
| 534 | 538 | startupContextPaths = append(startupContextPaths, cfg.StartupContextPaths...) | |
| 539 | + | startupExtensionNames = append(startupExtensionNames, cfg.StartupExtensionNames...) | |
| 540 | + | startupSkillNames = append(startupSkillNames, cfg.StartupSkillNames...) | |
| 535 | 541 | } | |
| 536 | 542 | i := &Interactive{ | |
| 537 | 543 | cfg: cfg, | |
| 538 | 544 | view: &tui.View{ | |
| 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, | |
| 545 | + | Theme: cfg.Theme, | |
| 546 | + | ImageProto: effectiveImageProtocol(cfg.InlineImagesEnabled), | |
| 547 | + | FlatTools: cfg.FlatTools, | |
| 548 | + | CompactUser: cfg.CompactUser, | |
| 549 | + | CompactMode: cfg.CompactMode != nil && *cfg.CompactMode, | |
| 550 | + | StartupContextPaths: startupContextPaths, | |
| 551 | + | StartupExtensionNames: startupExtensionNames, | |
| 552 | + | StartupSkillNames: startupSkillNames, | |
| 545 | 553 | }, | |
| 546 | 554 | // Prompt is the standard half-block accent bar used by chat | |
| 547 | 555 | // speaker labels too, so the input gutter matches the rest | |
| @@ -3219,8 +3227,8 @@ func (i *Interactive) openSettingsDialog() { | |||
| 3219 | 3227 | }, | |
| 3220 | 3228 | { | |
| 3221 | 3229 | key: "show_instructions_at_startup", | |
| 3222 | - | label: "show loaded instructions at startup", | |
| 3223 | - | desc: "list loaded AGENTS.md file paths above the transcript", | |
| 3230 | + | label: "show loaded resources at startup", | |
| 3231 | + | desc: "list loaded context files, extensions, and user-installed skills above the transcript", | |
| 3224 | 3232 | value: showInstructions, | |
| 3225 | 3233 | }, | |
| 3226 | 3234 | { | |
| @@ -3602,10 +3610,14 @@ func (i *Interactive) applySettingToggle(key string, value bool) { | |||
| 3602 | 3610 | } | |
| 3603 | 3611 | i.mu.Lock() | |
| 3604 | 3612 | i.view.StartupContextPaths = nil | |
| 3613 | + | i.view.StartupExtensionNames = nil | |
| 3614 | + | i.view.StartupSkillNames = nil | |
| 3605 | 3615 | if value { | |
| 3606 | 3616 | i.view.StartupContextPaths = append(i.view.StartupContextPaths, i.cfg.StartupContextPaths...) | |
| 3617 | + | i.view.StartupExtensionNames = append(i.view.StartupExtensionNames, i.cfg.StartupExtensionNames...) | |
| 3618 | + | i.view.StartupSkillNames = append(i.view.StartupSkillNames, i.cfg.StartupSkillNames...) | |
| 3607 | 3619 | } | |
| 3608 | - | i.statusOK = "show loaded instructions at startup " + onOff(value) | |
| 3620 | + | i.statusOK = "show loaded resources at startup " + onOff(value) | |
| 3609 | 3621 | i.statusErr = "" | |
| 3610 | 3622 | i.mu.Unlock() | |
| 3611 | 3623 | } | |
| @@ -38,11 +38,13 @@ type InteractiveConfig struct { | |||||
| 38 | MaxSteps int | 38 | MaxSteps int | ||
| 39 | CWD string | 39 | CWD string | ||
| 40 | 40 | ||||
| 41 | - | // StartupContextPaths are loaded instruction file paths available to show | 41 | + | // Startup resource fields are loaded inputs available to list before the |
| 42 | - | // before the transcript. They are never sent to the provider or persisted. | 42 | + | // transcript. They are never sent to the provider or persisted by the view. |
| 43 | - | StartupContextPaths []string | 43 | + | StartupContextPaths []string |
| 44 | + | StartupExtensionNames []string | |||
| 45 | + | StartupSkillNames []string | |||
| 44 | 46 | ||||
| 45 | - | // ShowInstructionsAtStartup controls whether StartupContextPaths are visible. | 47 | + | // ShowInstructionsAtStartup controls whether startup resources are visible. |
| 46 | // nil means the default, off. | 48 | // nil means the default, off. | ||
| 47 | ShowInstructionsAtStartup *bool | 49 | ShowInstructionsAtStartup *bool | ||
| 48 | 50 | ||||
| @@ -530,18 +532,24 @@ func NewInteractive(cfg InteractiveConfig) *Interactive { | |||||
| 530 | renderer := tui.NewRenderer(cfg.Terminal) | 532 | renderer := tui.NewRenderer(cfg.Terminal) | ||
| 531 | renderer.SetTheme(cfg.Theme) | 533 | renderer.SetTheme(cfg.Theme) | ||
| 532 | startupContextPaths := []string(nil) | 534 | startupContextPaths := []string(nil) | ||
| 535 | + | startupExtensionNames := []string(nil) | |||
| 536 | + | startupSkillNames := []string(nil) | |||
| 533 | if cfg.ShowInstructionsAtStartup != nil && *cfg.ShowInstructionsAtStartup { | 537 | if cfg.ShowInstructionsAtStartup != nil && *cfg.ShowInstructionsAtStartup { | ||
| 534 | startupContextPaths = append(startupContextPaths, cfg.StartupContextPaths...) | 538 | startupContextPaths = append(startupContextPaths, cfg.StartupContextPaths...) | ||
| 539 | + | startupExtensionNames = append(startupExtensionNames, cfg.StartupExtensionNames...) | |||
| 540 | + | startupSkillNames = append(startupSkillNames, cfg.StartupSkillNames...) | |||
| 535 | } | 541 | } | ||
| 536 | i := &Interactive{ | 542 | i := &Interactive{ | ||
| 537 | cfg: cfg, | 543 | cfg: cfg, | ||
| 538 | view: &tui.View{ | 544 | view: &tui.View{ | ||
| 539 | - | Theme: cfg.Theme, | 545 | + | Theme: cfg.Theme, |
| 540 | - | ImageProto: effectiveImageProtocol(cfg.InlineImagesEnabled), | 546 | + | ImageProto: effectiveImageProtocol(cfg.InlineImagesEnabled), |
| 541 | - | FlatTools: cfg.FlatTools, | 547 | + | FlatTools: cfg.FlatTools, |
| 542 | - | CompactUser: cfg.CompactUser, | 548 | + | CompactUser: cfg.CompactUser, |
| 543 | - | CompactMode: cfg.CompactMode != nil && *cfg.CompactMode, | 549 | + | CompactMode: cfg.CompactMode != nil && *cfg.CompactMode, |
| 544 | - | StartupContextPaths: startupContextPaths, | 550 | + | StartupContextPaths: startupContextPaths, |
| 551 | + | StartupExtensionNames: startupExtensionNames, | |||
| 552 | + | StartupSkillNames: startupSkillNames, | |||
| 545 | }, | 553 | }, | ||
| 546 | // Prompt is the standard half-block accent bar used by chat | 554 | // Prompt is the standard half-block accent bar used by chat | ||
| 547 | // speaker labels too, so the input gutter matches the rest | 555 | // speaker labels too, so the input gutter matches the rest | ||
| @@ -3219,8 +3227,8 @@ func (i *Interactive) openSettingsDialog() { | |||||
| 3219 | }, | 3227 | }, | ||
| 3220 | { | 3228 | { | ||
| 3221 | key: "show_instructions_at_startup", | 3229 | key: "show_instructions_at_startup", | ||
| 3222 | - | label: "show loaded instructions at startup", | 3230 | + | label: "show loaded resources at startup", |
| 3223 | - | desc: "list loaded AGENTS.md file paths above the transcript", | 3231 | + | desc: "list loaded context files, extensions, and user-installed skills above the transcript", |
| 3224 | value: showInstructions, | 3232 | value: showInstructions, | ||
| 3225 | }, | 3233 | }, | ||
| 3226 | { | 3234 | { | ||
| @@ -3602,10 +3610,14 @@ func (i *Interactive) applySettingToggle(key string, value bool) { | |||||
| 3602 | } | 3610 | } | ||
| 3603 | i.mu.Lock() | 3611 | i.mu.Lock() | ||
| 3604 | i.view.StartupContextPaths = nil | 3612 | i.view.StartupContextPaths = nil | ||
| 3613 | + | i.view.StartupExtensionNames = nil | |||
| 3614 | + | i.view.StartupSkillNames = nil | |||
| 3605 | if value { | 3615 | if value { | ||
| 3606 | i.view.StartupContextPaths = append(i.view.StartupContextPaths, i.cfg.StartupContextPaths...) | 3616 | i.view.StartupContextPaths = append(i.view.StartupContextPaths, i.cfg.StartupContextPaths...) | ||
| 3617 | + | i.view.StartupExtensionNames = append(i.view.StartupExtensionNames, i.cfg.StartupExtensionNames...) | |||
| 3618 | + | i.view.StartupSkillNames = append(i.view.StartupSkillNames, i.cfg.StartupSkillNames...) | |||
| 3607 | } | 3619 | } | ||
| 3608 | - | i.statusOK = "show loaded instructions at startup " + onOff(value) | 3620 | + | i.statusOK = "show loaded resources at startup " + onOff(value) |
| 3609 | i.statusErr = "" | 3621 | i.statusErr = "" | ||
| 3610 | i.mu.Unlock() | 3622 | i.mu.Unlock() | ||
| 3611 | } | 3623 | } | ||
M packages/agent/modes/startup_instructions_test.go +27 -18
| @@ -2,29 +2,38 @@ package modes | |||
| 2 | 2 | ||
| 3 | 3 | import "testing" | |
| 4 | 4 | ||
| 5 | - | func TestStartupInstructionsAreOptIn(t *testing.T) { | |
| 6 | - | paths := []string{"/repo/AGENTS.md"} | |
| 5 | + | func TestStartupResourcesAreOptIn(t *testing.T) { | |
| 6 | + | cfg := InteractiveConfig{ | |
| 7 | + | StartupContextPaths: []string{"/repo/AGENTS.md"}, | |
| 8 | + | StartupExtensionNames: []string{"workspaces"}, | |
| 9 | + | StartupSkillNames: []string{"review"}, | |
| 10 | + | } | |
| 7 | 11 | ||
| 8 | - | disabled := NewInteractive(InteractiveConfig{StartupContextPaths: paths}) | |
| 12 | + | disabled := NewInteractive(cfg) | |
| 9 | 13 | disabled.rend = nil | |
| 10 | - | if len(disabled.view.StartupContextPaths) != 0 { | |
| 11 | - | t.Fatalf("default startup rendered %d instruction paths", len(disabled.view.StartupContextPaths)) | |
| 12 | - | } | |
| 14 | + | assertStartupResourceCounts(t, disabled, 0) | |
| 15 | + | ||
| 13 | 16 | 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 | + | assertStartupResourceCounts(t, disabled, 1) | |
| 18 | + | ||
| 17 | 19 | 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 | - | } | |
| 20 | + | assertStartupResourceCounts(t, disabled, 0) | |
| 21 | 21 | ||
| 22 | 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)) | |
| 23 | + | cfg.ShowInstructionsAtStartup = &enabledValue | |
| 24 | + | enabled := NewInteractive(cfg) | |
| 25 | + | assertStartupResourceCounts(t, enabled, 1) | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | func assertStartupResourceCounts(t *testing.T, i *Interactive, want int) { | |
| 29 | + | t.Helper() | |
| 30 | + | if got := len(i.view.StartupContextPaths); got != want { | |
| 31 | + | t.Fatalf("startup context count = %d, want %d", got, want) | |
| 32 | + | } | |
| 33 | + | if got := len(i.view.StartupExtensionNames); got != want { | |
| 34 | + | t.Fatalf("startup extension count = %d, want %d", got, want) | |
| 35 | + | } | |
| 36 | + | if got := len(i.view.StartupSkillNames); got != want { | |
| 37 | + | t.Fatalf("startup skill count = %d, want %d", got, want) | |
| 29 | 38 | } | |
| 30 | 39 | } | |
| @@ -2,29 +2,38 @@ package modes | |||||
| 2 | 2 | ||||
| 3 | import "testing" | 3 | import "testing" | ||
| 4 | 4 | ||||
| 5 | - | func TestStartupInstructionsAreOptIn(t *testing.T) { | 5 | + | func TestStartupResourcesAreOptIn(t *testing.T) { |
| 6 | - | paths := []string{"/repo/AGENTS.md"} | 6 | + | cfg := InteractiveConfig{ |
| 7 | + | StartupContextPaths: []string{"/repo/AGENTS.md"}, | |||
| 8 | + | StartupExtensionNames: []string{"workspaces"}, | |||
| 9 | + | StartupSkillNames: []string{"review"}, | |||
| 10 | + | } | |||
| 7 | 11 | ||||
| 8 | - | disabled := NewInteractive(InteractiveConfig{StartupContextPaths: paths}) | 12 | + | disabled := NewInteractive(cfg) |
| 9 | disabled.rend = nil | 13 | disabled.rend = nil | ||
| 10 | - | if len(disabled.view.StartupContextPaths) != 0 { | 14 | + | assertStartupResourceCounts(t, disabled, 0) |
| 11 | - | t.Fatalf("default startup rendered %d instruction paths", len(disabled.view.StartupContextPaths)) | 15 | + | |
| 12 | - | } | |||
| 13 | disabled.applySettingToggle("show_instructions_at_startup", true) | 16 | disabled.applySettingToggle("show_instructions_at_startup", true) | ||
| 14 | - | if len(disabled.view.StartupContextPaths) != 1 { | 17 | + | assertStartupResourceCounts(t, disabled, 1) |
| 15 | - | t.Fatalf("live toggle rendered %d instruction paths, want 1", len(disabled.view.StartupContextPaths)) | 18 | + | |
| 16 | - | } | |||
| 17 | disabled.applySettingToggle("show_instructions_at_startup", false) | 19 | disabled.applySettingToggle("show_instructions_at_startup", false) | ||
| 18 | - | if len(disabled.view.StartupContextPaths) != 0 { | 20 | + | assertStartupResourceCounts(t, disabled, 0) |
| 19 | - | t.Fatalf("disabled live toggle left %d instruction paths", len(disabled.view.StartupContextPaths)) | |||
| 20 | - | } | |||
| 21 | 21 | ||||
| 22 | enabledValue := true | 22 | enabledValue := true | ||
| 23 | - | enabled := NewInteractive(InteractiveConfig{ | 23 | + | cfg.ShowInstructionsAtStartup = &enabledValue |
| 24 | - | StartupContextPaths: paths, | 24 | + | enabled := NewInteractive(cfg) |
| 25 | - | ShowInstructionsAtStartup: &enabledValue, | 25 | + | assertStartupResourceCounts(t, enabled, 1) |
| 26 | - | }) | 26 | + | } |
| 27 | - | if len(enabled.view.StartupContextPaths) != 1 { | 27 | + | |
| 28 | - | t.Fatalf("enabled startup rendered %d instruction paths, want 1", len(enabled.view.StartupContextPaths)) | 28 | + | func assertStartupResourceCounts(t *testing.T, i *Interactive, want int) { |
| 29 | + | t.Helper() | |||
| 30 | + | if got := len(i.view.StartupContextPaths); got != want { | |||
| 31 | + | t.Fatalf("startup context count = %d, want %d", got, want) | |||
| 32 | + | } | |||
| 33 | + | if got := len(i.view.StartupExtensionNames); got != want { | |||
| 34 | + | t.Fatalf("startup extension count = %d, want %d", got, want) | |||
| 35 | + | } | |||
| 36 | + | if got := len(i.view.StartupSkillNames); got != want { | |||
| 37 | + | t.Fatalf("startup skill count = %d, want %d", got, want) | |||
| 29 | } | 38 | } | ||
| 30 | } | 39 | } | ||
M packages/tui/view.go +23 -14
| @@ -123,11 +123,13 @@ type View struct { | |||
| 123 | 123 | StreamingActive bool | |
| 124 | 124 | ToolCalls []ToolCallView // tool calls in flight or completed | |
| 125 | 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 | |
| 126 | + | // Startup resource fields list host-loaded inputs before the transcript | |
| 127 | + | // without creating provider messages or persisted entries. | |
| 128 | + | StartupContextPaths []string | |
| 129 | + | StartupExtensionNames []string | |
| 130 | + | StartupSkillNames []string | |
| 131 | + | StatusLine string | |
| 132 | + | Err string | |
| 131 | 133 | ||
| 132 | 134 | // liveBodyHigh tracks the tallest live-preview body height seen | |
| 133 | 135 | // per tool-call id, so a streaming edit/write/bash box never | |
| @@ -327,19 +329,26 @@ func (v *View) renderErr(width int) []string { | |||
| 327 | 329 | return out | |
| 328 | 330 | } | |
| 329 | 331 | ||
| 330 | - | func (v *View) renderStartupContext(width int) []string { | |
| 331 | - | if len(v.StartupContextPaths) == 0 { | |
| 332 | - | return nil | |
| 333 | - | } | |
| 332 | + | func (v *View) renderStartupResources(width int) []string { | |
| 334 | 333 | bodyWidth := width - 2 | |
| 335 | 334 | if bodyWidth < 8 { | |
| 336 | 335 | bodyWidth = 8 | |
| 337 | 336 | } | |
| 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)) | |
| 337 | + | var out []string | |
| 338 | + | appendSection := func(label string, items []string) { | |
| 339 | + | if len(items) == 0 { | |
| 340 | + | return | |
| 341 | + | } | |
| 342 | + | out = append(out, v.Theme.FG256(v.Theme.Accent, "["+label+"]")) | |
| 343 | + | for _, line := range wrapLine(strings.Join(items, ", "), bodyWidth, "") { | |
| 344 | + | out = append(out, v.Theme.FG256(v.Theme.Muted, " "+line)) | |
| 345 | + | } | |
| 346 | + | out = append(out, "") | |
| 341 | 347 | } | |
| 342 | - | return append(out, "") | |
| 348 | + | appendSection("Context", v.StartupContextPaths) | |
| 349 | + | appendSection("Extensions", v.StartupExtensionNames) | |
| 350 | + | appendSection("Skills", v.StartupSkillNames) | |
| 351 | + | return out | |
| 343 | 352 | } | |
| 344 | 353 | ||
| 345 | 354 | // BuildWithAnchors is like Build but additionally reports the first | |
| @@ -415,7 +424,7 @@ func (v *View) BuildWithAnchors(width int) ([]string, []MessageAnchor) { | |||
| 415 | 424 | } | |
| 416 | 425 | ||
| 417 | 426 | out := make([]string, 0, total+16) | |
| 418 | - | out = append(out, v.renderStartupContext(width)...) | |
| 427 | + | out = append(out, v.renderStartupResources(width)...) | |
| 419 | 428 | anchors := make([]MessageAnchor, 0, len(v.Messages)) | |
| 420 | 429 | for idx := range v.Messages { | |
| 421 | 430 | anchors = append(anchors, MessageAnchor{MessageIdx: idx, Row: len(out)}) | |
| @@ -123,11 +123,13 @@ type View struct { | |||||
| 123 | StreamingActive bool | 123 | StreamingActive bool | ||
| 124 | ToolCalls []ToolCallView // tool calls in flight or completed | 124 | ToolCalls []ToolCallView // tool calls in flight or completed | ||
| 125 | 125 | ||||
| 126 | - | // StartupContextPaths lists host-loaded instruction files before the | 126 | + | // Startup resource fields list host-loaded inputs before the transcript |
| 127 | - | // transcript without creating provider messages or persisted entries. | 127 | + | // without creating provider messages or persisted entries. |
| 128 | - | StartupContextPaths []string | 128 | + | StartupContextPaths []string |
| 129 | - | StatusLine string | 129 | + | StartupExtensionNames []string |
| 130 | - | Err string | 130 | + | StartupSkillNames []string |
| 131 | + | StatusLine string | |||
| 132 | + | Err string | |||
| 131 | 133 | ||||
| 132 | // liveBodyHigh tracks the tallest live-preview body height seen | 134 | // liveBodyHigh tracks the tallest live-preview body height seen | ||
| 133 | // per tool-call id, so a streaming edit/write/bash box never | 135 | // per tool-call id, so a streaming edit/write/bash box never | ||
| @@ -327,19 +329,26 @@ func (v *View) renderErr(width int) []string { | |||||
| 327 | return out | 329 | return out | ||
| 328 | } | 330 | } | ||
| 329 | 331 | ||||
| 330 | - | func (v *View) renderStartupContext(width int) []string { | 332 | + | func (v *View) renderStartupResources(width int) []string { |
| 331 | - | if len(v.StartupContextPaths) == 0 { | |||
| 332 | - | return nil | |||
| 333 | - | } | |||
| 334 | bodyWidth := width - 2 | 333 | bodyWidth := width - 2 | ||
| 335 | if bodyWidth < 8 { | 334 | if bodyWidth < 8 { | ||
| 336 | bodyWidth = 8 | 335 | bodyWidth = 8 | ||
| 337 | } | 336 | } | ||
| 338 | - | out := []string{v.Theme.FG256(v.Theme.Accent, "[Context]")} | 337 | + | var out []string |
| 339 | - | for _, line := range wrapLine(strings.Join(v.StartupContextPaths, ", "), bodyWidth, "") { | 338 | + | appendSection := func(label string, items []string) { |
| 340 | - | out = append(out, v.Theme.FG256(v.Theme.Muted, " "+line)) | 339 | + | if len(items) == 0 { |
| 340 | + | return | |||
| 341 | + | } | |||
| 342 | + | out = append(out, v.Theme.FG256(v.Theme.Accent, "["+label+"]")) | |||
| 343 | + | for _, line := range wrapLine(strings.Join(items, ", "), bodyWidth, "") { | |||
| 344 | + | out = append(out, v.Theme.FG256(v.Theme.Muted, " "+line)) | |||
| 345 | + | } | |||
| 346 | + | out = append(out, "") | |||
| 341 | } | 347 | } | ||
| 342 | - | return append(out, "") | 348 | + | appendSection("Context", v.StartupContextPaths) |
| 349 | + | appendSection("Extensions", v.StartupExtensionNames) | |||
| 350 | + | appendSection("Skills", v.StartupSkillNames) | |||
| 351 | + | return out | |||
| 343 | } | 352 | } | ||
| 344 | 353 | ||||
| 345 | // BuildWithAnchors is like Build but additionally reports the first | 354 | // BuildWithAnchors is like Build but additionally reports the first | ||
| @@ -415,7 +424,7 @@ func (v *View) BuildWithAnchors(width int) ([]string, []MessageAnchor) { | |||||
| 415 | } | 424 | } | ||
| 416 | 425 | ||||
| 417 | out := make([]string, 0, total+16) | 426 | out := make([]string, 0, total+16) | ||
| 418 | - | out = append(out, v.renderStartupContext(width)...) | 427 | + | out = append(out, v.renderStartupResources(width)...) |
| 419 | anchors := make([]MessageAnchor, 0, len(v.Messages)) | 428 | anchors := make([]MessageAnchor, 0, len(v.Messages)) | ||
| 420 | for idx := range v.Messages { | 429 | for idx := range v.Messages { | ||
| 421 | anchors = append(anchors, MessageAnchor{MessageIdx: idx, Row: len(out)}) | 430 | anchors = append(anchors, MessageAnchor{MessageIdx: idx, Row: len(out)}) | ||
M packages/tui/view_tool_overlay_test.go +13 -6
| @@ -8,25 +8,32 @@ import ( | |||
| 8 | 8 | "github.com/patriceckhart/zot/packages/provider" | |
| 9 | 9 | ) | |
| 10 | 10 | ||
| 11 | - | func TestStartupContextRendersOnlyLoadedPaths(t *testing.T) { | |
| 11 | + | func TestStartupResourcesRenderAsCompactSections(t *testing.T) { | |
| 12 | 12 | v := View{ | |
| 13 | 13 | Theme: Dark, | |
| 14 | 14 | StartupContextPaths: []string{ | |
| 15 | 15 | "/home/user/AGENTS.md", | |
| 16 | 16 | "/repo/AGENTS.md", | |
| 17 | 17 | }, | |
| 18 | + | StartupExtensionNames: []string{"todo", "workspaces"}, | |
| 19 | + | StartupSkillNames: []string{"review", "test"}, | |
| 18 | 20 | } | |
| 19 | 21 | ||
| 20 | 22 | 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) | |
| 23 | + | for _, want := range []string{ | |
| 24 | + | "[Context]", "/home/user/AGENTS.md, /repo/AGENTS.md", | |
| 25 | + | "[Extensions]", "todo, workspaces", | |
| 26 | + | "[Skills]", "review, test", | |
| 27 | + | } { | |
| 28 | + | if !strings.Contains(plain, want) { | |
| 29 | + | t.Fatalf("startup resources missing %q:\n%s", want, plain) | |
| 30 | + | } | |
| 24 | 31 | } | |
| 25 | 32 | if strings.ContainsAny(plain, boxGlyphs) || strings.Contains(plain, "read ") { | |
| 26 | - | t.Fatalf("startup instruction paths rendered as a tool call:\n%s", plain) | |
| 33 | + | t.Fatalf("startup resources rendered as a tool call:\n%s", plain) | |
| 27 | 34 | } | |
| 28 | 35 | if len(v.Messages) != 0 { | |
| 29 | - | t.Fatalf("startup context added %d transcript messages", len(v.Messages)) | |
| 36 | + | t.Fatalf("startup resources added %d transcript messages", len(v.Messages)) | |
| 30 | 37 | } | |
| 31 | 38 | } | |
| 32 | 39 | ||
| @@ -8,25 +8,32 @@ 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) { | 11 | + | func TestStartupResourcesRenderAsCompactSections(t *testing.T) { |
| 12 | v := View{ | 12 | v := View{ | ||
| 13 | Theme: Dark, | 13 | Theme: Dark, | ||
| 14 | StartupContextPaths: []string{ | 14 | StartupContextPaths: []string{ | ||
| 15 | "/home/user/AGENTS.md", | 15 | "/home/user/AGENTS.md", | ||
| 16 | "/repo/AGENTS.md", | 16 | "/repo/AGENTS.md", | ||
| 17 | }, | 17 | }, | ||
| 18 | + | StartupExtensionNames: []string{"todo", "workspaces"}, | |||
| 19 | + | StartupSkillNames: []string{"review", "test"}, | |||
| 18 | } | 20 | } | ||
| 19 | 21 | ||||
| 20 | plain := stripANSI(strings.Join(v.Build(80), "\n")) | 22 | plain := stripANSI(strings.Join(v.Build(80), "\n")) | ||
| 21 | - | if !strings.Contains(plain, "[Context]") || | 23 | + | for _, want := range []string{ |
| 22 | - | !strings.Contains(plain, "/home/user/AGENTS.md, /repo/AGENTS.md") { | 24 | + | "[Context]", "/home/user/AGENTS.md, /repo/AGENTS.md", |
| 23 | - | t.Fatalf("startup instruction paths were not rendered:\n%s", plain) | 25 | + | "[Extensions]", "todo, workspaces", |
| 26 | + | "[Skills]", "review, test", | |||
| 27 | + | } { | |||
| 28 | + | if !strings.Contains(plain, want) { | |||
| 29 | + | t.Fatalf("startup resources missing %q:\n%s", want, plain) | |||
| 30 | + | } | |||
| 24 | } | 31 | } | ||
| 25 | if strings.ContainsAny(plain, boxGlyphs) || strings.Contains(plain, "read ") { | 32 | if strings.ContainsAny(plain, boxGlyphs) || strings.Contains(plain, "read ") { | ||
| 26 | - | t.Fatalf("startup instruction paths rendered as a tool call:\n%s", plain) | 33 | + | t.Fatalf("startup resources rendered as a tool call:\n%s", plain) |
| 27 | } | 34 | } | ||
| 28 | if len(v.Messages) != 0 { | 35 | if len(v.Messages) != 0 { | ||
| 29 | - | t.Fatalf("startup context added %d transcript messages", len(v.Messages)) | 36 | + | t.Fatalf("startup resources added %d transcript messages", len(v.Messages)) |
| 30 | } | 37 | } | ||
| 31 | } | 38 | } | ||
| 32 | 39 | ||||