v0.3.94 e344aa8

skill_pins.go RawHistoryFolder

1package agent
2
3import (
4	"encoding/json"
5	"errors"
6	"fmt"
7	"io"
8	"os"
9	"path/filepath"
10	"slices"
11	"sort"
12	"sync"
13
14	"github.com/patriceckhart/zot/packages/agent/extensions"
15	"github.com/patriceckhart/zot/packages/agent/skills"
16)
17
18// userSkillSnapshot rediscovers user-facing skills with the same precedence
19// as agent construction. Built-in implementation skills are not pinnable.
20func userSkillSnapshot(args Args) []*skills.Skill {
21	if args.NoSkill {
22		return nil
23	}
24	userHome, _ := os.UserHomeDir()
25	var sources []skills.Source
26	if args.WithSkills {
27		sources = append(sources, skills.SearchSources(ZotHome(), args.CWD, userHome)...)
28	}
29	if !args.NoExt || len(args.Exts) > 0 {
30		extSources, _ := extensions.PlanSkillSources(ZotHome(), args.CWD, args.Exts, !args.NoExt)
31		sources = append(extSources, sources...)
32	}
33	list, _ := skills.DiscoverSources(sources, true)
34	return skills.VisibleSkills(list)
35}
36
37func preloadSkillPins(args Args, fresh bool, prompt string, diagnostics io.Writer) string {
38	if !fresh || args.NoSkill {
39		return prompt
40	}
41	pins, err := loadSkillPins(args.CWD)
42	if err != nil {
43		fmt.Fprintln(diagnostics, "warning:", err)
44		return prompt
45	}
46	selected, warnings := pins.Resolve(userSkillSnapshot(args))
47	for _, warning := range warnings {
48		fmt.Fprintln(diagnostics, "warning:", warning)
49	}
50	return skills.PreloadPrompt(selected, prompt)
51}
52
53// Keep preferences local, even for project pins. Never write into a checkout.
54type skillPinsFile struct {
55	Global   []string            `json:"global,omitempty"`
56	Projects map[string][]string `json:"projects,omitempty"`
57}
58
59var skillPinsMu sync.Mutex
60
61func skillPinProject(cwd string) (string, error) {
62	path, err := filepath.Abs(cwd)
63	if err != nil {
64		return "", err
65	}
66	// Existing symlink aliases of a directory share their project pins.
67	if resolved, err := filepath.EvalSymlinks(path); err == nil {
68		path = resolved
69	}
70	return filepath.Clean(path), nil
71}
72
73func readSkillPins() (skillPinsFile, error) {
74	var pins skillPinsFile
75	data, err := os.ReadFile(filepath.Join(ZotHome(), "skill-pins.json"))
76	if errors.Is(err, os.ErrNotExist) {
77		return pins, nil
78	}
79	if err != nil {
80		return pins, fmt.Errorf("read skill pins: %w", err)
81	}
82	// Do not echo malformed file contents in diagnostics.
83	if json.Unmarshal(data, &pins) != nil {
84		return skillPinsFile{}, fmt.Errorf("invalid skill-pins.json")
85	}
86	return pins, nil
87}
88
89func loadSkillPins(cwd string) (skills.Pins, error) {
90	skillPinsMu.Lock()
91	defer skillPinsMu.Unlock()
92	project, err := skillPinProject(cwd)
93	if err != nil {
94		return skills.Pins{}, err
95	}
96	pins, err := readSkillPins()
97	return skills.Pins{Global: pins.Global, Project: pins.Projects[project]}, err
98}
99
100func toggleSkillPin(cwd, name string, global bool) error {
101	if name == "" {
102		return fmt.Errorf("skill name is required")
103	}
104	skillPinsMu.Lock()
105	defer skillPinsMu.Unlock()
106	project, err := skillPinProject(cwd)
107	if err != nil {
108		return err
109	}
110	pins, err := readSkillPins()
111	if err != nil {
112		return err
113	}
114	toggle := func(names []string) []string {
115		if slices.Contains(names, name) {
116			return slices.DeleteFunc(names, func(n string) bool { return n == name })
117		}
118		names = append(names, name)
119		sort.Strings(names)
120		return names
121	}
122	if global {
123		pins.Global = toggle(pins.Global)
124	} else {
125		if pins.Projects == nil {
126			pins.Projects = make(map[string][]string)
127		}
128		pins.Projects[project] = toggle(pins.Projects[project])
129		if len(pins.Projects[project]) == 0 {
130			delete(pins.Projects, project)
131		}
132	}
133	data, err := json.MarshalIndent(pins, "", "  ")
134	if err != nil {
135		return err
136	}
137	if err := os.MkdirAll(ZotHome(), 0o700); err != nil {
138		return err
139	}
140	f, err := os.CreateTemp(ZotHome(), ".skill-pins-*")
141	if err != nil {
142		return err
143	}
144	defer os.Remove(f.Name())
145	if _, err := f.Write(append(data, '\n')); err != nil {
146		f.Close()
147		return err
148	}
149	if err := f.Close(); err != nil {
150		return err
151	}
152	if err := os.Rename(f.Name(), filepath.Join(ZotHome(), "skill-pins.json")); err != nil {
153		return fmt.Errorf("save skill pins: %w", err)
154	}
155	return nil
156}

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