reslop 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -1
- package/README.md +21 -3
- package/lib/agents/acp.js +309 -0
- package/lib/agents/agents.js +166 -370
- package/lib/agents/allow.js +141 -52
- package/lib/agents/allow.json +16 -13
- package/lib/agents/models.js +250 -376
- package/lib/agents/registry.js +437 -0
- package/lib/agents/run.js +234 -0
- package/lib/agents/transcript.js +143 -0
- package/lib/cli.js +2 -1
- package/lib/common/process.js +26 -1
- package/lib/dashboard/git.js +11 -1
- package/lib/dashboard/reports.js +80 -0
- package/lib/dashboard/watch.js +43 -8
- package/lib/dashboard/workspace.js +19 -20
- package/lib/dashboard/worktrees.js +145 -0
- package/lib/git/deps.js +29 -0
- package/lib/git/dir.js +50 -0
- package/lib/git/git.js +1 -1
- package/lib/git/linked.js +152 -0
- package/lib/git/local.js +179 -0
- package/lib/git/status.js +126 -0
- package/lib/git/worktree.js +137 -31
- package/lib/input/actions.js +83 -2
- package/lib/input/editor.js +9 -0
- package/lib/input/keys.js +39 -3
- package/lib/input/line-edit.js +8 -3
- package/lib/render/agents.js +64 -36
- package/lib/render/branches.js +55 -27
- package/lib/render/field.js +19 -5
- package/lib/render/footer.js +14 -4
- package/lib/render/frame.js +16 -2
- package/lib/render/log.js +5 -4
- package/lib/render/menu.js +103 -6
- package/lib/render/name-edit.js +31 -0
- package/lib/render/repos.js +52 -8
- package/lib/render/status.js +22 -4
- package/lib/report/render.js +4 -7
- package/lib/report/run.js +16 -23
- package/lib/review/agent-instructions.md +13 -8
- package/lib/review/format.js +0 -6
- package/lib/runs/process.js +3 -3
- package/lib/runs/runs.js +27 -4
- package/lib/session/agents/agents.js +227 -194
- package/lib/session/agents/away.js +48 -0
- package/lib/session/agents/job.js +23 -86
- package/lib/session/agents/jobs.js +14 -27
- package/lib/session/agents/options.js +45 -101
- package/lib/session/agents/plans.js +53 -0
- package/lib/session/agents/view.js +34 -0
- package/lib/session/branches.js +18 -2
- package/lib/session/compose.js +20 -6
- package/lib/session/dispatch.js +43 -3
- package/lib/session/editor-pointer.js +98 -0
- package/lib/session/files-pane.js +3 -0
- package/lib/session/import.js +12 -1
- package/lib/session/lifecycle.js +70 -10
- package/lib/session/navigation.js +5 -0
- package/lib/session/npm/history.js +3 -1
- package/lib/session/npm/npm.js +111 -13
- package/lib/session/ops.js +3 -0
- package/lib/session/pointer.js +304 -46
- package/lib/session/session.js +17 -2
- package/lib/session/tasks.js +1 -1
- package/lib/session/terminal.js +2 -2
- package/lib/session/text-menu.js +50 -0
- package/lib/session/view.js +19 -2
- package/lib/session/workspace.js +179 -38
- package/lib/session/worktrees.js +280 -0
- package/lib/term/select.js +2 -1
- package/lib/term/themes/dark.json +4 -0
- package/lib/term/themes/light.json +4 -0
- package/package.json +2 -2
package/lib/agents/models.js
CHANGED
|
@@ -1,47 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
const { stripAnsi } = require('../term/ansi.js');
|
|
3
|
+
const { isHashObject } = require('metautil');
|
|
5
4
|
const { trimText } = require('../common/utilities.js');
|
|
6
5
|
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
);
|
|
14
|
-
const CURSOR_ROW = /^([A-Za-z][A-Za-z0-9_./:+#-]*)\s+-\s+\S/;
|
|
15
|
-
const CURSOR_NOISE = /^(?:loading models|available models$|tip:|no models)/i;
|
|
16
|
-
const AVAILABLE_LINE = /^Available models:\s*(.+)$/i;
|
|
17
|
-
const WIDE_LABEL = /\b1M\b/;
|
|
18
|
-
const CONTEXT_TAG = /\[context=[^\]]*\]$/;
|
|
19
|
-
// list-models repeats one slug for every context, so 500k is unlabeled
|
|
20
|
-
const MODEL_CONTEXTS = {
|
|
21
|
-
'grok-4.7': ['256k', '500k'],
|
|
22
|
-
};
|
|
23
|
-
const JSON_KEYS = ['slug', 'id', 'model', 'modelID', 'name'];
|
|
24
|
-
const OPTION_LINE = /^\s*(?:-\w,\s*)?--[\w-]+/;
|
|
25
|
-
// (a, b), <a|b>, {a,b} or [possible values: a, b] in an option's help
|
|
26
|
-
const VALUE_LIST =
|
|
27
|
-
/[(<[{](?:[a-z ]+:)?\s*("?[\w-]+"?(?:\s*[,|]\s*"?[\w-]+"?)+)\s*[)>\]}]/i;
|
|
28
|
-
const QUOTED_NAME = /'([A-Za-z][\w.:-]*)'/g;
|
|
29
|
-
const FAST = '-fast';
|
|
30
|
-
const THINKING = '-thinking';
|
|
31
|
-
|
|
32
|
-
const EFFORT_LEVELS = [
|
|
33
|
-
'extra-high',
|
|
34
|
-
'xhigh',
|
|
35
|
-
'minimal',
|
|
36
|
-
'medium',
|
|
37
|
-
'none',
|
|
38
|
-
'high',
|
|
39
|
-
'max',
|
|
40
|
-
'low',
|
|
41
|
-
];
|
|
6
|
+
const EFFORT_KEYS = ['effort', 'reasoning_effort', 'reasoning'];
|
|
7
|
+
const MODEL_ID = /^([^[\]]+)\[(.*)\]$/;
|
|
8
|
+
const FAST_NAME = /fast/i;
|
|
9
|
+
const CONTEXT_NAME = /^context/i;
|
|
10
|
+
const FAST_VALUES = ['off', 'on'];
|
|
11
|
+
const SWITCHES = [FAST_VALUES, ['false', 'true']];
|
|
42
12
|
|
|
43
13
|
const EFFORT_RANK = [
|
|
44
|
-
'default',
|
|
45
14
|
'none',
|
|
46
15
|
'minimal',
|
|
47
16
|
'low',
|
|
@@ -50,411 +19,316 @@ const EFFORT_RANK = [
|
|
|
50
19
|
'xhigh',
|
|
51
20
|
'extra-high',
|
|
52
21
|
'max',
|
|
22
|
+
'ultra',
|
|
53
23
|
];
|
|
54
24
|
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
const line = raw.trim();
|
|
59
|
-
if (line) lines.push(line);
|
|
60
|
-
}
|
|
61
|
-
return lines;
|
|
25
|
+
const concrete = (name) => {
|
|
26
|
+
const text = trimText(name);
|
|
27
|
+
return text === 'default' ? '' : text;
|
|
62
28
|
};
|
|
63
29
|
|
|
64
|
-
const
|
|
65
|
-
const unique = new Set();
|
|
66
|
-
for (const raw of names) {
|
|
67
|
-
const name = trimText(raw);
|
|
68
|
-
if (name) unique.add(name);
|
|
69
|
-
}
|
|
70
|
-
return [...unique];
|
|
71
|
-
};
|
|
30
|
+
const unique = (values) => [...new Set(values.filter(Boolean))];
|
|
72
31
|
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (MODEL_ID.test(token) && !SKIP_TOKEN.test(token)) names.push(token);
|
|
78
|
-
}
|
|
79
|
-
return uniqueNames(names);
|
|
32
|
+
const switchValue = (value) => {
|
|
33
|
+
if (value === 'true' || value === 'on') return 'on';
|
|
34
|
+
if (value === 'false' || value === 'off') return 'off';
|
|
35
|
+
return '';
|
|
80
36
|
};
|
|
81
37
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
38
|
+
// name[key=value,...] or name[effort]: one model id carries its own settings
|
|
39
|
+
const parseModelId = (id) => {
|
|
40
|
+
const text = trimText(id);
|
|
41
|
+
const variant = { id: text, name: text, effort: '', context: '', fast: '' };
|
|
42
|
+
const match = MODEL_ID.exec(text);
|
|
43
|
+
if (!match) return variant;
|
|
44
|
+
variant.name = match[1].trim();
|
|
45
|
+
for (const part of match[2].split(',')) {
|
|
46
|
+
const [key, ...rest] = part.split('=');
|
|
47
|
+
const value = rest.join('=').trim();
|
|
48
|
+
const name = key.trim();
|
|
49
|
+
if (!rest.length) variant.effort ||= name;
|
|
50
|
+
else if (EFFORT_KEYS.includes(name)) variant.effort = value;
|
|
51
|
+
else if (name === 'context') variant.context = value;
|
|
52
|
+
else if (name === 'fast') variant.fast = switchValue(value);
|
|
90
53
|
}
|
|
91
|
-
return
|
|
54
|
+
return variant;
|
|
92
55
|
};
|
|
93
56
|
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
57
|
+
const optionValues = (option) => {
|
|
58
|
+
const values = [];
|
|
59
|
+
const collect = (items) => {
|
|
60
|
+
for (const item of Array.isArray(items) ? items : []) {
|
|
61
|
+
if (!isHashObject(item)) continue;
|
|
62
|
+
if (Array.isArray(item.options)) collect(item.options);
|
|
63
|
+
else if (typeof item.value === 'string') values.push(item.value);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
collect(option ? option.options : []);
|
|
67
|
+
return values;
|
|
101
68
|
};
|
|
102
69
|
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (at < 0) return '';
|
|
109
|
-
const block = [lines[at]];
|
|
110
|
-
for (const line of lines.slice(at + 1)) {
|
|
111
|
-
if (OPTION_LINE.test(line) || /^\S/.test(line)) break;
|
|
112
|
-
block.push(line.trim());
|
|
113
|
-
}
|
|
114
|
-
return block.join(' ');
|
|
70
|
+
const selects = (options) => {
|
|
71
|
+
const listed = Array.isArray(options) ? options : [];
|
|
72
|
+
return listed.filter(
|
|
73
|
+
(option) => isHashObject(option) && typeof option.id === 'string',
|
|
74
|
+
);
|
|
115
75
|
};
|
|
116
76
|
|
|
117
|
-
const
|
|
118
|
-
const listed =
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return uniqueNames(values.map((value) => value.replaceAll('"', '')));
|
|
77
|
+
const optionIn = (options, category) => {
|
|
78
|
+
const listed = selects(options);
|
|
79
|
+
const byCategory = listed.find((option) => option.category === category);
|
|
80
|
+
return byCategory ?? listed.find((option) => option.id === category) ?? null;
|
|
122
81
|
};
|
|
123
82
|
|
|
124
|
-
const
|
|
125
|
-
const help = optionHelp(text, '--model');
|
|
126
|
-
return uniqueNames([...help.matchAll(QUOTED_NAME)].map((hit) => hit[1]));
|
|
127
|
-
};
|
|
83
|
+
const optionsOf = (state) => (state ? state.configOptions : null);
|
|
128
84
|
|
|
129
|
-
const
|
|
130
|
-
const starts = [text.indexOf('{'), text.indexOf('[')].filter((at) => at >= 0);
|
|
131
|
-
return starts.length ? Math.min(...starts) : -1;
|
|
132
|
-
};
|
|
85
|
+
const optionOf = (state, category) => optionIn(optionsOf(state), category);
|
|
133
86
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (
|
|
87
|
+
// A switch is named by the agent: on and off, or true and false
|
|
88
|
+
const fastIn = (options) => {
|
|
89
|
+
for (const option of selects(options)) {
|
|
90
|
+
if (!FAST_NAME.test(option.id)) continue;
|
|
91
|
+
const values = optionValues(option);
|
|
92
|
+
const pair = SWITCHES.find((names) =>
|
|
93
|
+
names.every((value) => values.includes(value)),
|
|
94
|
+
);
|
|
95
|
+
if (pair) return { option, off: pair[0], on: pair[1] };
|
|
138
96
|
}
|
|
139
|
-
return
|
|
97
|
+
return null;
|
|
140
98
|
};
|
|
141
99
|
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
)
|
|
148
|
-
|
|
100
|
+
const contextIn = (options) =>
|
|
101
|
+
selects(options).find((option) => CONTEXT_NAME.test(option.id)) ?? null;
|
|
102
|
+
|
|
103
|
+
const listedModels = (state) => {
|
|
104
|
+
const models = state && isHashObject(state.models) ? state.models : {};
|
|
105
|
+
const listed = Array.isArray(models.availableModels)
|
|
106
|
+
? models.availableModels
|
|
107
|
+
: [];
|
|
108
|
+
const ids = listed.map((model) => (isHashObject(model) ? model.modelId : ''));
|
|
109
|
+
const current = trimText(models.currentModelId);
|
|
110
|
+
return { ids: ids.filter((id) => typeof id === 'string' && id), current };
|
|
149
111
|
};
|
|
150
112
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
113
|
+
const groupVariants = (ids) => {
|
|
114
|
+
const entries = new Map();
|
|
115
|
+
for (const id of ids) {
|
|
116
|
+
const variant = parseModelId(id);
|
|
117
|
+
if (!variant.name) continue;
|
|
118
|
+
if (!entries.has(variant.name)) {
|
|
119
|
+
entries.set(variant.name, { name: variant.name, variants: [] });
|
|
156
120
|
}
|
|
157
|
-
|
|
121
|
+
entries.get(variant.name).variants.push(variant);
|
|
158
122
|
}
|
|
159
|
-
|
|
160
|
-
const id = jsonModelId(value);
|
|
161
|
-
const shown = id && value.visibility !== 'hide';
|
|
162
|
-
if (shown) found.push({ id, efforts: jsonEfforts(value) });
|
|
163
|
-
if (value.models) collectJsonModels(value.models, found);
|
|
164
|
-
if (value.data) collectJsonModels(value.data, found);
|
|
165
|
-
return found;
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
const jsonModels = (text) => {
|
|
169
|
-
const start = jsonStart(text);
|
|
170
|
-
const data = start < 0 ? null : jsonParse(text.slice(start));
|
|
171
|
-
return collectJsonModels(data, []);
|
|
123
|
+
return entries;
|
|
172
124
|
};
|
|
173
125
|
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
return names.length ? uniqueNames(names) : parseNameList(plain);
|
|
126
|
+
const rankOf = (level) => {
|
|
127
|
+
const at = EFFORT_RANK.indexOf(level);
|
|
128
|
+
return at < 0 ? EFFORT_RANK.length : at;
|
|
178
129
|
};
|
|
179
130
|
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
131
|
+
const fitEffort = (levels, effort) => {
|
|
132
|
+
const want = concrete(effort);
|
|
133
|
+
if (!levels || !levels.length) return want;
|
|
134
|
+
if (levels.length === 1) return levels[0];
|
|
135
|
+
if (!want || levels.includes(want)) return want;
|
|
136
|
+
let best = levels[0];
|
|
137
|
+
for (const level of levels) {
|
|
138
|
+
const gap = Math.abs(rankOf(level) - rankOf(want));
|
|
139
|
+
if (gap < Math.abs(rankOf(best) - rankOf(want))) best = level;
|
|
185
140
|
}
|
|
186
|
-
return
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
// A full listing replaces the built-in names, a partial one extends them
|
|
190
|
-
const mergeModels = (fallback, listed, partial = false) => {
|
|
191
|
-
const names = listed ?? [];
|
|
192
|
-
const kept = partial || !names.length;
|
|
193
|
-
const known = kept && fallback ? fallback : [];
|
|
194
|
-
return uniqueNames(['default', ...names, ...known]);
|
|
141
|
+
return best;
|
|
195
142
|
};
|
|
196
143
|
|
|
197
|
-
const
|
|
198
|
-
if (!name.endsWith(suffix) || name.length <= suffix.length) return '';
|
|
199
|
-
return name.slice(0, -suffix.length);
|
|
200
|
-
};
|
|
144
|
+
const byRank = (left, right) => rankOf(left) - rankOf(right);
|
|
201
145
|
|
|
202
|
-
const
|
|
146
|
+
const variantValues = (entry, field) =>
|
|
147
|
+
entry ? unique(entry.variants.map((variant) => variant[field])) : [];
|
|
203
148
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
149
|
+
// What one agent session offers: its models and the settings of each
|
|
150
|
+
class ModelCatalog {
|
|
151
|
+
constructor(state) {
|
|
152
|
+
const option = optionOf(state, 'model');
|
|
153
|
+
const listed = listedModels(state);
|
|
154
|
+
const ids = option ? optionValues(option) : listed.ids;
|
|
155
|
+
const current = option ? option.currentValue : listed.current;
|
|
156
|
+
this.loaded = Boolean(state);
|
|
157
|
+
this.configId = option ? option.id : '';
|
|
158
|
+
this.entries = groupVariants(ids);
|
|
159
|
+
this.names = [...this.entries.keys()];
|
|
160
|
+
this.currentId = trimText(current);
|
|
161
|
+
this.current = parseModelId(this.currentId).name;
|
|
162
|
+
// The model list can name the efforts of each model the option leaves out
|
|
163
|
+
this.listed = option ? groupVariants(listed.ids) : new Map();
|
|
164
|
+
this.session = selects(optionsOf(state));
|
|
165
|
+
// An agent asked model by model has options that are not the session's
|
|
166
|
+
this.byModel = state && state.byModel instanceof Map ? state.byModel : null;
|
|
208
167
|
}
|
|
209
|
-
return null;
|
|
210
|
-
};
|
|
211
168
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (!unfast) return false;
|
|
215
|
-
const unthink = cutSuffix(unfast, THINKING);
|
|
216
|
-
return Boolean(effortTail(unfast) || (unthink && effortTail(unthink)));
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
const splitEffortModel = (id) => {
|
|
220
|
-
const name = trimText(id);
|
|
221
|
-
if (!name) return null;
|
|
222
|
-
const fast = hasFastTail(name);
|
|
223
|
-
let rest = fast ? cutSuffix(name, FAST) : name;
|
|
224
|
-
let thinking = '';
|
|
225
|
-
const unthink = cutSuffix(rest, THINKING);
|
|
226
|
-
if (unthink && effortTail(unthink)) {
|
|
227
|
-
rest = unthink;
|
|
228
|
-
thinking = 'after';
|
|
169
|
+
find(name) {
|
|
170
|
+
return this.entries.get(trimText(name)) ?? null;
|
|
229
171
|
}
|
|
230
|
-
const hit = effortTail(rest);
|
|
231
|
-
if (!hit) return null;
|
|
232
|
-
const before = cutSuffix(hit.base, THINKING);
|
|
233
|
-
if (before) thinking = 'before';
|
|
234
|
-
const base = before || hit.base;
|
|
235
|
-
return { base, effort: hit.level, thinking, fast, id: name };
|
|
236
|
-
};
|
|
237
172
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
};
|
|
173
|
+
described(entry) {
|
|
174
|
+
const { byModel } = this;
|
|
175
|
+
const variant = entry.variants.find((item) => byModel.has(item.id));
|
|
176
|
+
return variant ? byModel.get(variant.id) : null;
|
|
177
|
+
}
|
|
244
178
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (text && text !== 'default') sizes.push(text);
|
|
179
|
+
// The settings of a model are unknown until the agent was asked about it
|
|
180
|
+
unknown(name) {
|
|
181
|
+
const entry = this.find(name);
|
|
182
|
+
return Boolean(entry && this.byModel && !this.described(entry));
|
|
250
183
|
}
|
|
251
|
-
if (!sizes.length) return [];
|
|
252
|
-
const named = MODEL_CONTEXTS[contextFamily(model)];
|
|
253
|
-
if (named) return named.slice();
|
|
254
|
-
if (!trimText(model) || catalog.hasWide(wide, model)) return sizes;
|
|
255
|
-
return sizes.filter((size) => size !== '1m');
|
|
256
|
-
};
|
|
257
184
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
185
|
+
options(name) {
|
|
186
|
+
const entry = this.find(name);
|
|
187
|
+
if (!entry) return [];
|
|
188
|
+
if (!this.byModel) return this.session;
|
|
189
|
+
return this.described(entry) ?? [];
|
|
190
|
+
}
|
|
262
191
|
|
|
263
|
-
|
|
264
|
-
|
|
192
|
+
efforts(name) {
|
|
193
|
+
const own = variantValues(this.find(name), 'effort');
|
|
194
|
+
if (own.length) return own.sort(byRank);
|
|
195
|
+
const listed = variantValues(this.listed.get(trimText(name)), 'effort');
|
|
196
|
+
if (listed.length) return listed.sort(byRank);
|
|
197
|
+
const option = optionIn(this.options(name), 'thought_level');
|
|
198
|
+
return unique(optionValues(option).map(concrete));
|
|
199
|
+
}
|
|
265
200
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (!levels.includes(level)) levels.push(level);
|
|
201
|
+
contexts(name) {
|
|
202
|
+
const own = variantValues(this.find(name), 'context');
|
|
203
|
+
return own.length ? own : optionValues(contextIn(this.options(name)));
|
|
270
204
|
}
|
|
271
|
-
return levels;
|
|
272
|
-
};
|
|
273
205
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
ids: group.ids,
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
const plainEntry = (name) => ({
|
|
282
|
-
model: name,
|
|
283
|
-
encoded: false,
|
|
284
|
-
levels: [],
|
|
285
|
-
ids: new Map([['default', name]]),
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
const defaultGroup = (groups, name) => {
|
|
289
|
-
const direct = groups.get(`${name}\0\0`);
|
|
290
|
-
if (direct && !direct.ids.has('default')) return direct;
|
|
291
|
-
for (const group of groups.values()) {
|
|
292
|
-
if (group.model === name && !group.ids.has('default')) return group;
|
|
206
|
+
fasts(name) {
|
|
207
|
+
const own = variantValues(this.find(name), 'fast');
|
|
208
|
+
if (own.length) return own;
|
|
209
|
+
return fastIn(this.options(name)) ? FAST_VALUES : [];
|
|
293
210
|
}
|
|
294
|
-
return null;
|
|
295
|
-
};
|
|
296
211
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
212
|
+
variant(entry, picked) {
|
|
213
|
+
const effort = concrete(picked.effort);
|
|
214
|
+
const context = trimText(picked.context);
|
|
215
|
+
const fast = picked.fast === true ? 'on' : 'off';
|
|
216
|
+
let best = null;
|
|
217
|
+
let top = -1;
|
|
218
|
+
for (const variant of entry.variants) {
|
|
219
|
+
let score = variant.id === this.currentId ? 1 : 0;
|
|
220
|
+
if (effort && variant.effort === effort) score += 8;
|
|
221
|
+
if (context && variant.context === context) score += 4;
|
|
222
|
+
if (variant.fast === fast) score += 2;
|
|
223
|
+
if (score > top) [best, top] = [variant, score];
|
|
307
224
|
}
|
|
308
|
-
|
|
309
|
-
if (!groups.has(key)) {
|
|
310
|
-
groups.set(key, { model: modelLabel(part), ids: new Map() });
|
|
311
|
-
}
|
|
312
|
-
const group = groups.get(key);
|
|
313
|
-
if (!group.ids.has(part.effort)) group.ids.set(part.effort, name);
|
|
314
|
-
owners.set(name, group);
|
|
315
|
-
}
|
|
316
|
-
for (const name of plain) {
|
|
317
|
-
const group = defaultGroup(groups, name);
|
|
318
|
-
if (!group) continue;
|
|
319
|
-
group.ids.set('default', name);
|
|
320
|
-
owners.set(name, group);
|
|
225
|
+
return best;
|
|
321
226
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
227
|
+
|
|
228
|
+
// What a run of the model gets: the picked settings where it offers them
|
|
229
|
+
resolve(name, picked) {
|
|
230
|
+
const entry = this.find(name);
|
|
231
|
+
const variant = entry ? this.variant(entry, picked) : {};
|
|
232
|
+
const options = this.options(name);
|
|
233
|
+
const given = (option) => trimText(option ? option.currentValue : '');
|
|
234
|
+
const pick = (offered, want, option) =>
|
|
235
|
+
offered.includes(want) ? want : given(option);
|
|
236
|
+
const levels = this.efforts(name);
|
|
237
|
+
const effort = optionIn(options, 'thought_level');
|
|
238
|
+
const level = concrete(pick(levels, concrete(picked.effort), effort));
|
|
239
|
+
const sizes = this.contexts(name);
|
|
240
|
+
const size = pick(sizes, trimText(picked.context), contextIn(options));
|
|
241
|
+
const asked = fastIn(options) && picked.fast === true ? 'on' : 'off';
|
|
242
|
+
return {
|
|
243
|
+
id: variant.id ?? '',
|
|
244
|
+
effort: variant.effort || (levels.includes(level) ? level : ''),
|
|
245
|
+
context: variant.context || (sizes.includes(size) ? size : ''),
|
|
246
|
+
fast: variant.fast || asked,
|
|
247
|
+
};
|
|
330
248
|
}
|
|
331
|
-
|
|
332
|
-
};
|
|
249
|
+
}
|
|
333
250
|
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
if (!levels || !levels.length) return 'default';
|
|
337
|
-
if (levels.includes(want)) return want;
|
|
338
|
-
const named = levels.find((level) => {
|
|
339
|
-
const name = trimText(level);
|
|
340
|
-
return name && name !== 'default';
|
|
341
|
-
});
|
|
342
|
-
return named ?? levels[0];
|
|
343
|
-
};
|
|
251
|
+
const EMPTY_CATALOG = new ModelCatalog(null);
|
|
252
|
+
const catalogs = new WeakMap();
|
|
344
253
|
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
family: model,
|
|
350
|
-
levels: [],
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
const entryResolved = (entry, effort) => {
|
|
354
|
-
if (!entry.encoded) return plainResolved(entry.model, effort);
|
|
355
|
-
const level = fitEffort(entry.levels, effort);
|
|
356
|
-
return {
|
|
357
|
-
model: entry.ids.get(level) ?? entry.model,
|
|
358
|
-
encoded: true,
|
|
359
|
-
effort: level,
|
|
360
|
-
family: entry.model,
|
|
361
|
-
levels: entry.levels,
|
|
362
|
-
};
|
|
254
|
+
const catalogOf = (state) => {
|
|
255
|
+
if (!isHashObject(state)) return EMPTY_CATALOG;
|
|
256
|
+
if (!catalogs.has(state)) catalogs.set(state, new ModelCatalog(state));
|
|
257
|
+
return catalogs.get(state);
|
|
363
258
|
};
|
|
364
259
|
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
for (const entry of this.entries) {
|
|
372
|
-
if (!this.byName.has(entry.model)) this.byName.set(entry.model, entry);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
260
|
+
const setOption = (option, value) => {
|
|
261
|
+
if (!option || option.currentValue === value) return null;
|
|
262
|
+
if (!optionValues(option).includes(value)) return null;
|
|
263
|
+
const params = { configId: option.id, value };
|
|
264
|
+
return { method: 'session/set_config_option', params };
|
|
265
|
+
};
|
|
375
266
|
|
|
376
|
-
|
|
377
|
-
|
|
267
|
+
const modelRequest = (catalog, id) => {
|
|
268
|
+
if (!catalog.configId) {
|
|
269
|
+
return { method: 'session/set_model', params: { modelId: id } };
|
|
378
270
|
}
|
|
271
|
+
const params = { configId: catalog.configId, value: id };
|
|
272
|
+
return { method: 'session/set_config_option', params };
|
|
273
|
+
};
|
|
379
274
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
275
|
+
// An unlisted model keeps the one the agent already runs
|
|
276
|
+
const modelChange = (state, picked) => {
|
|
277
|
+
const catalog = catalogOf(state);
|
|
278
|
+
const name = catalog.find(picked.model) ? picked.model : catalog.current;
|
|
279
|
+
const { id } = catalog.resolve(name, picked);
|
|
280
|
+
if (!id || id === catalog.currentId) return null;
|
|
281
|
+
return modelRequest(catalog, id);
|
|
282
|
+
};
|
|
383
283
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const base = withoutFast(entry ? entry.model : trimText(name));
|
|
387
|
-
if (!base || base.startsWith('cursor-')) return null;
|
|
388
|
-
const found = this.find(`cursor-${base}`);
|
|
389
|
-
return found && found.encoded ? found : null;
|
|
390
|
-
}
|
|
284
|
+
const effortChange = (state, picked) =>
|
|
285
|
+
setOption(optionOf(state, 'thought_level'), concrete(picked.effort));
|
|
391
286
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const entry = this.find(choice.model);
|
|
395
|
-
const prefixed = this.prefixed(entry, choice.model);
|
|
396
|
-
if (prefixed) return entryResolved(prefixed, effort);
|
|
397
|
-
if (entry) return entryResolved(entry, effort);
|
|
398
|
-
const part = splitEffortModel(choice.model);
|
|
399
|
-
const family = part ? this.find(modelLabel(part)) : null;
|
|
400
|
-
if (!family) return plainResolved(choice.model, effort);
|
|
401
|
-
return entryResolved(family, effort === 'default' ? part.effort : effort);
|
|
402
|
-
}
|
|
287
|
+
const contextChange = (state, picked) =>
|
|
288
|
+
setOption(contextIn(optionsOf(state)), trimText(picked.context));
|
|
403
289
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const entry = this.find(other);
|
|
410
|
-
return entry ? entryResolved(entry, resolved.effort) : resolved;
|
|
411
|
-
}
|
|
290
|
+
const fastChange = (state, picked) => {
|
|
291
|
+
const fast = fastIn(optionsOf(state));
|
|
292
|
+
if (!fast) return null;
|
|
293
|
+
return setOption(fast.option, picked.fast === true ? fast.on : fast.off);
|
|
294
|
+
};
|
|
412
295
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
}
|
|
296
|
+
const modeChange = (state, mode) => {
|
|
297
|
+
if (!mode) return null;
|
|
298
|
+
const option = optionOf(state, 'mode');
|
|
299
|
+
if (option) return setOption(option, mode);
|
|
300
|
+
const modes = state && isHashObject(state.modes) ? state.modes : {};
|
|
301
|
+
const listed = Array.isArray(modes.availableModes)
|
|
302
|
+
? modes.availableModes
|
|
303
|
+
: [];
|
|
304
|
+
const offered = listed.some((item) => isHashObject(item) && item.id === mode);
|
|
305
|
+
if (!offered || modes.currentModeId === mode) return null;
|
|
306
|
+
return { method: 'session/set_mode', params: { modeId: mode } };
|
|
307
|
+
};
|
|
427
308
|
|
|
428
|
-
const
|
|
429
|
-
const catalogs = new WeakMap();
|
|
309
|
+
const emptyChoice = () => ({ model: '', effort: '', fast: false, context: '' });
|
|
430
310
|
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
311
|
+
const choiceOf = (picked) => {
|
|
312
|
+
const from = isHashObject(picked) ? picked : {};
|
|
313
|
+
return {
|
|
314
|
+
model: trimText(from.model),
|
|
315
|
+
effort: concrete(from.effort),
|
|
316
|
+
fast: from.fast === true,
|
|
317
|
+
context: trimText(from.context),
|
|
318
|
+
};
|
|
435
319
|
};
|
|
436
320
|
|
|
437
|
-
const emptyChoice = (row) => ({
|
|
438
|
-
model: row && row.models && row.models[0] ? row.models[0] : 'default',
|
|
439
|
-
effort: 'default',
|
|
440
|
-
extra: '',
|
|
441
|
-
fast: false,
|
|
442
|
-
context: '',
|
|
443
|
-
});
|
|
444
|
-
|
|
445
321
|
module.exports = {
|
|
322
|
+
concrete,
|
|
323
|
+
parseModelId,
|
|
446
324
|
catalogOf,
|
|
447
|
-
parseNameList,
|
|
448
|
-
parseCursorModels,
|
|
449
|
-
parseCursorWide,
|
|
450
|
-
parseJsonModels,
|
|
451
|
-
parseJsonEfforts,
|
|
452
|
-
parseHelpModels,
|
|
453
|
-
optionValues,
|
|
454
|
-
mergeModels,
|
|
455
|
-
contextMenu,
|
|
456
|
-
groupModels,
|
|
457
325
|
fitEffort,
|
|
458
|
-
|
|
326
|
+
modelRequest,
|
|
327
|
+
modelChange,
|
|
328
|
+
effortChange,
|
|
329
|
+
contextChange,
|
|
330
|
+
fastChange,
|
|
331
|
+
modeChange,
|
|
459
332
|
emptyChoice,
|
|
333
|
+
choiceOf,
|
|
460
334
|
};
|