返回 DeepSeek-Reasonix
slash_cache.go
根目录 / internal / cli / slash_cache.go
1 package cli
2
3 import (
4 "strings"
5
6 "reasonix/internal/control"
7 )
8
9 // slashCompletionCache memoizes the two expensive completion snapshots: the
10 // slash catalog (commands, skills, prompts) and the arg-completion data
11 // (config models/providers, plugin state, MCP names, memory refs). Assembling
12 // the arg data runs several config loads plus a plugin-state read per keystroke
13 // otherwise, which reads as visible input lag on cold Linux caches (#9503).
14 type slashCompletionCache struct {
15 items []compItem
16 // argData is the memoized snapshot; argBuilt separates "not built" from a
17 // snapshot taken while modelRef was still "".
18 argData control.ArgData
19 argModel string
20 // argContext identifies the structured command whose editing session owns
21 // argData. It survives an empty filter result but is cleared by an explicit
22 // dismissal, input reset, invalidation, or a different command.
23 argContext string
24 argBuilt bool
25 }
26
27 func (m *chatTUI) ensureSlashCache() *slashCompletionCache {
28 if m.slashCache == nil {
29 m.slashCache = &slashCompletionCache{}
30 }
31 return m.slashCache
32 }
33
34 // slashItems returns the cached slash catalog. Rebuilds only after
35 // invalidateSlashCatalog — never on ordinary keystrokes.
36 func (m *chatTUI) slashItems() []compItem {
37 if c := m.slashCache; c != nil && c.items != nil {
38 return c.items
39 }
40 // Immutable snapshot so keystroke filtering never mutates shared state.
41 items := m.buildSlashCatalog()
42 out := make([]compItem, len(items))
43 copy(out, items)
44 m.ensureSlashCache().items = out
45 return out
46 }
47
48 // slashArgDataSnapshot returns the memoized arg-completion data. It rebuilds
49 // when the active model changed or the cache was invalidated; filtering stays
50 // in-memory, so keystrokes inside an open arg popup cost no I/O.
51 func (m *chatTUI) slashArgDataSnapshot() control.ArgData {
52 if c := m.slashCache; c != nil && c.argBuilt && c.argModel == m.modelRef {
53 return c.argData
54 }
55 c := m.ensureSlashCache()
56 c.argData = m.slashArgData()
57 c.argModel = m.modelRef
58 c.argBuilt = true
59 return c.argData
60 }
61
62 func (m *chatTUI) cachedSlashArgItems(line string) ([]control.SlashItem, int, bool) {
63 context := ""
64 if end := strings.IndexAny(line, " \t"); end >= 0 {
65 context = line[:end]
66 }
67 usedData := false
68 items, from, applies := control.SlashArgItemsLazy(line, func() control.ArgData {
69 usedData = true
70 m.prepareSlashArgSnapshot(context)
71 return m.slashArgDataSnapshot()
72 })
73 if !usedData {
74 m.endSlashArgSnapshot()
75 }
76 return items, from, applies
77 }
78
79 // prepareSlashArgSnapshot keeps one stable snapshot for a structured argument
80 // editing session. Candidate filtering may temporarily hide the popup, so the
81 // command context—not menu visibility—owns the generation boundary.
82 func (m *chatTUI) prepareSlashArgSnapshot(context string) {
83 c := m.ensureSlashCache()
84 if context != "" && c.argContext == context {
85 return
86 }
87 c.clearArgData()
88 c.argContext = context
89 }
90
91 func (c *slashCompletionCache) clearArgData() {
92 c.argData = control.ArgData{}
93 c.argModel = ""
94 c.argBuilt = false
95 }
96
97 func (m *chatTUI) endSlashArgSnapshot() {
98 if c := m.slashCache; c != nil {
99 c.clearArgData()
100 c.argContext = ""
101 }
102 }
103
104 func (m *chatTUI) endSlashArgSnapshotForKey(key string) string {
105 switch key {
106 case "esc", "ctrl+c", "super+c", "meta+c", "ctrl+enter", "enter":
107 m.endSlashArgSnapshot()
108 }
109 return key
110 }
111
112 func (m *chatTUI) resetComposerInput() {
113 m.input.Reset()
114 m.endSlashArgSnapshot()
115 }
116
117 // invalidateSlashCatalog drops the cached catalog and arg data so the next
118 // slashItems/slashArgDataSnapshot call rebuilds them. Call from model switch,
119 // skill rescan, /reload-cmd, and any path that mutates
120 // commands/skills/host/extension actions.
121 func (m *chatTUI) invalidateSlashCatalog() {
122 m.slashCache = nil
123 }
124
124 lines GO