detached 0341161

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
137137 1. `$ZOT_HOME/AGENTS.md` (global, machine-wide instructions that apply to every project).
138138 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. 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.
141141
142142 For a general, non-project-specific instruction set, put your rules in `$ZOT_HOME/AGENTS.md`, for example:
143143
@@ -358,7 +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`.
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`.
362362 - **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).
363363 - **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.
364364 - **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 {
978978 // agents so they don't outlive their parent zot.
979979 defer swarmMgr.StopAll()
980980
981+ var startupSkills []*skills.Skill
982+ if r.SkillTool != nil {
983+ startupSkills = r.SkillTool.Skills()
984+ }
985+
981986 iv = modes.NewInteractive(modes.InteractiveConfig{
982987 Terminal: term,
983988 Theme: theme,
@@ -1007,6 +1012,8 @@ func runInteractive(ctx context.Context, args Args, version string) error {
10071012 MaxSteps: r.MaxSteps,
10081013 CWD: r.CWD,
10091014 StartupContextPaths: instructionContextPaths(r.ContextFiles),
1015+ StartupExtensionNames: startupExtensionNames(extMgr.All()),
1016+ StartupSkillNames: startupSkillNames(startupSkills),
10101017 ShowInstructionsAtStartup: initialCfg.ShowInstructionsAtStartup,
10111018 ZotHome: ZotHome(),
10121019 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 {
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.
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.
8787 ShowInstructionsAtStartup *bool `json:"show_instructions_at_startup,omitempty"`
8888
8989 // 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 above84+ // 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 @@
11 package agent
22
3+import (
4+ "sort"
5+
6+ "github.com/patriceckhart/zot/packages/agent/extensions"
7+ "github.com/patriceckhart/zot/packages/agent/skills"
8+)
9+
310 // instructionContextPaths returns the loaded instruction paths in effective
411 // prompt order for display in interactive startup metadata.
512 func instructionContextPaths(files []ContextFile) []string {
@@ -9,3 +16,27 @@ func instructionContextPaths(files []ContextFile) []string {
916 }
1017 return paths
1118 }
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 agent1 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 effective10 // 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 paths17 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 @@
11 package agent
22
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+)
410
511 func TestInstructionContextPathsPreserveLoadOrder(t *testing.T) {
612 files := []ContextFile{
@@ -18,3 +24,27 @@ func TestInstructionContextPathsPreserveLoadOrder(t *testing.T) {
1824 }
1925 }
2026 }
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 agent1 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 {
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
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
4446
45- // ShowInstructionsAtStartup controls whether StartupContextPaths are visible.
47+ // ShowInstructionsAtStartup controls whether startup resources are visible.
4648 // nil means the default, off.
4749 ShowInstructionsAtStartup *bool
4850
@@ -530,18 +532,24 @@ func NewInteractive(cfg InteractiveConfig) *Interactive {
530532 renderer := tui.NewRenderer(cfg.Terminal)
531533 renderer.SetTheme(cfg.Theme)
532534 startupContextPaths := []string(nil)
535+ startupExtensionNames := []string(nil)
536+ startupSkillNames := []string(nil)
533537 if cfg.ShowInstructionsAtStartup != nil && *cfg.ShowInstructionsAtStartup {
534538 startupContextPaths = append(startupContextPaths, cfg.StartupContextPaths...)
539+ startupExtensionNames = append(startupExtensionNames, cfg.StartupExtensionNames...)
540+ startupSkillNames = append(startupSkillNames, cfg.StartupSkillNames...)
535541 }
536542 i := &Interactive{
537543 cfg: cfg,
538544 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,
545553 },
546554 // Prompt is the standard half-block accent bar used by chat
547555 // speaker labels too, so the input gutter matches the rest
@@ -3219,8 +3227,8 @@ func (i *Interactive) openSettingsDialog() {
32193227 },
32203228 {
32213229 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",
32243232 value: showInstructions,
32253233 },
32263234 {
@@ -3602,10 +3610,14 @@ func (i *Interactive) applySettingToggle(key string, value bool) {
36023610 }
36033611 i.mu.Lock()
36043612 i.view.StartupContextPaths = nil
3613+ i.view.StartupExtensionNames = nil
3614+ i.view.StartupSkillNames = nil
36053615 if value {
36063616 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...)
36073619 }
3608- i.statusOK = "show loaded instructions at startup " + onOff(value)
3620+ i.statusOK = "show loaded resources at startup " + onOff(value)
36093621 i.statusErr = ""
36103622 i.mu.Unlock()
36113623 }
@@ -38,11 +38,13 @@ type InteractiveConfig struct {
38 MaxSteps int38 MaxSteps int
39 CWD string39 CWD string
40 40
41- // StartupContextPaths are loaded instruction file paths available to show41+ // 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 []string43+ 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 *bool49 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 chat554 // Prompt is the standard half-block accent bar used by chat
547 // speaker labels too, so the input gutter matches the rest555 // 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 = nil3612 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
22
33 import "testing"
44
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+ }
711
8- disabled := NewInteractive(InteractiveConfig{StartupContextPaths: paths})
12+ disabled := NewInteractive(cfg)
913 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+
1316 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+
1719 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)
2121
2222 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)
2938 }
3039 }
@@ -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 = nil13 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 := true22 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 {
123123 StreamingActive bool
124124 ToolCalls []ToolCallView // tool calls in flight or completed
125125
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
131133
132134 // liveBodyHigh tracks the tallest live-preview body height seen
133135 // per tool-call id, so a streaming edit/write/bash box never
@@ -327,19 +329,26 @@ func (v *View) renderErr(width int) []string {
327329 return out
328330 }
329331
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 {
334333 bodyWidth := width - 2
335334 if bodyWidth < 8 {
336335 bodyWidth = 8
337336 }
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, "")
341347 }
342- return append(out, "")
348+ appendSection("Context", v.StartupContextPaths)
349+ appendSection("Extensions", v.StartupExtensionNames)
350+ appendSection("Skills", v.StartupSkillNames)
351+ return out
343352 }
344353
345354 // BuildWithAnchors is like Build but additionally reports the first
@@ -415,7 +424,7 @@ func (v *View) BuildWithAnchors(width int) ([]string, []MessageAnchor) {
415424 }
416425
417426 out := make([]string, 0, total+16)
418- out = append(out, v.renderStartupContext(width)...)
427+ out = append(out, v.renderStartupResources(width)...)
419428 anchors := make([]MessageAnchor, 0, len(v.Messages))
420429 for idx := range v.Messages {
421430 anchors = append(anchors, MessageAnchor{MessageIdx: idx, Row: len(out)})
@@ -123,11 +123,13 @@ type View struct {
123 StreamingActive bool123 StreamingActive bool
124 ToolCalls []ToolCallView // tool calls in flight or completed124 ToolCalls []ToolCallView // tool calls in flight or completed
125 125
126- // StartupContextPaths lists host-loaded instruction files before the126+ // 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 []string128+ StartupContextPaths []string
129- StatusLine string129+ StartupExtensionNames []string
130- Err string130+ StartupSkillNames []string
131+ StatusLine string
132+ Err string
131 133
132 // liveBodyHigh tracks the tallest live-preview body height seen134 // liveBodyHigh tracks the tallest live-preview body height seen
133 // per tool-call id, so a streaming edit/write/bash box never135 // 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 out329 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 - 2333 bodyWidth := width - 2
335 if bodyWidth < 8 {334 if bodyWidth < 8 {
336 bodyWidth = 8335 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 first354 // 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 (
88 "github.com/patriceckhart/zot/packages/provider"
99 )
1010
11-func TestStartupContextRendersOnlyLoadedPaths(t *testing.T) {
11+func TestStartupResourcesRenderAsCompactSections(t *testing.T) {
1212 v := View{
1313 Theme: Dark,
1414 StartupContextPaths: []string{
1515 "/home/user/AGENTS.md",
1616 "/repo/AGENTS.md",
1717 },
18+ StartupExtensionNames: []string{"todo", "workspaces"},
19+ StartupSkillNames: []string{"review", "test"},
1820 }
1921
2022 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+ }
2431 }
2532 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)
2734 }
2835 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))
3037 }
3138 }
3239
@@ -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

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