skill_pins.go RawHistoryFolder
1package modes
2
3import (
4 "slices"
5
6 "github.com/patriceckhart/zot/packages/agent/skills"
7 "github.com/patriceckhart/zot/packages/tui"
8)
9
10// SetPinnedSkillsPending arms preloading for a fresh conversation or disarms
11// it after a session load. Changing models must not reset this state.
12func (i *Interactive) SetPinnedSkillsPending(pending bool) {
13 i.mu.Lock()
14 i.pinnedSkillsPending = pending && i.cfg.LoadSkillPins != nil
15 i.pinnedSkills = nil
16 i.mu.Unlock()
17 if pending {
18 i.refreshSkillPins()
19 }
20 i.invalidate()
21}
22
23func (i *Interactive) refreshSkillPins() {
24 i.mu.Lock()
25 defer i.mu.Unlock()
26 if i.cfg.LoadSkillPins == nil {
27 return
28 }
29 pins, err := i.cfg.LoadSkillPins(i.cfg.CWD)
30 i.skillsDialog.pins = pins
31 i.skillsDialog.canPin = i.cfg.ToggleSkillPin != nil
32 var warnings []string
33 if err != nil {
34 warnings = append(warnings, err.Error())
35 }
36 if i.pinnedSkillsPending {
37 var available []*skills.Skill
38 if i.cfg.SkillSnapshot != nil {
39 available = i.cfg.SkillSnapshot()
40 }
41 var missing []string
42 i.pinnedSkills, missing = pins.Resolve(available)
43 warnings = append(warnings, missing...)
44 }
45 for _, warning := range warnings {
46 if !slices.Contains(i.reloadErrors, warning) {
47 i.reloadErrors = append(i.reloadErrors, warning)
48 }
49 }
50}
51
52func (i *Interactive) handleSkillPinKey(k tui.Key) bool {
53 if k.Kind != tui.KeyRune || (k.Rune != 'p' && k.Rune != 'g') {
54 return false
55 }
56 i.mu.Lock()
57 d := i.skillsDialog
58 if !d.Active() || d.viewing != nil || len(d.skills) == 0 || i.cfg.ToggleSkillPin == nil {
59 i.mu.Unlock()
60 return false
61 }
62 err := i.cfg.ToggleSkillPin(i.cfg.CWD, d.skills[d.cursor].Name, k.Rune == 'g')
63 if err != nil {
64 i.statusErr = "save skill pin: " + err.Error()
65 i.statusOK = ""
66 } else {
67 i.statusErr = ""
68 i.statusOK = "skill pins updated (apply on the next fresh conversation or /clear)"
69 }
70 i.mu.Unlock()
71 if err == nil {
72 i.refreshSkillPins()
73 }
74 return true
75}
76
77// Caller holds i.mu. Pending skills are folded into the real user message so
78// normal event delivery and persistence include their bodies exactly once.
79func (i *Interactive) preloadPinnedPromptLocked(prompt string) string {
80 if !i.pinnedSkillsPending {
81 return prompt
82 }
83 i.pinnedSkillsPending = false
84 result := skills.PreloadPrompt(i.pinnedSkills, prompt)
85 i.pinnedSkills = nil
86 return result
87}
88
89// Caller holds i.mu. Render a read-only notice, never editor placeholders.
90func (i *Interactive) pinnedSkillsNotice(cols int) []string {
91 if !i.pinnedSkillsPending || len(i.pinnedSkills) == 0 || cols < 4 {
92 return nil
93 }
94 names := make([]string, 0, len(i.pinnedSkills))
95 for _, s := range i.pinnedSkills {
96 name := s.Name
97 project := slices.Contains(i.skillsDialog.pins.Project, s.Name)
98 global := slices.Contains(i.skillsDialog.pins.Global, s.Name)
99 switch {
100 case project && global:
101 name += " (project + global)"
102 case project:
103 name += " (project)"
104 case global:
105 name += " (global)"
106 }
107 names = append(names, name)
108 }
109 return tui.RenderPinnedSkills(i.cfg.Theme, names, cols)
110}