reslop 0.3.1 → 0.3.3
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 +28 -1
- package/README.md +30 -4
- package/lib/agents/acp.js +309 -0
- package/lib/agents/agents.js +274 -438
- package/lib/agents/allow.js +141 -52
- package/lib/agents/allow.json +16 -14
- package/lib/agents/models.js +250 -383
- package/lib/agents/registry.js +437 -0
- package/lib/agents/run.js +234 -0
- package/lib/agents/script.js +93 -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/dir.js +50 -0
- 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 +113 -36
- package/lib/input/actions.js +83 -5
- package/lib/input/keys.js +30 -1
- package/lib/input/line-edit.js +5 -1
- package/lib/render/agents.js +84 -34
- package/lib/render/branches.js +55 -27
- package/lib/render/field.js +19 -5
- package/lib/render/footer.js +14 -6
- package/lib/render/frame.js +16 -2
- 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/run.js +16 -23
- package/lib/review/agent-instructions.md +2 -2
- package/lib/runs/process.js +3 -3
- package/lib/runs/runs.js +27 -4
- package/lib/session/agents/agents.js +220 -205
- 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 -1
- package/lib/session/compose.js +13 -5
- package/lib/session/dispatch.js +43 -4
- package/lib/session/editor-pointer.js +98 -0
- package/lib/session/files-pane.js +3 -0
- package/lib/session/lifecycle.js +67 -10
- package/lib/session/navigation.js +5 -0
- package/lib/session/npm/history.js +2 -1
- package/lib/session/npm/npm.js +89 -12
- package/lib/session/ops.js +3 -0
- package/lib/session/pointer.js +304 -46
- package/lib/session/session.js +15 -2
- 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/themes/dark.json +4 -0
- package/lib/term/themes/light.json +4 -0
- package/package.json +2 -2
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { saveRunFiles } = require('./jobs.js');
|
|
4
|
+
|
|
5
|
+
const isRunning = (job) => job.status === 'running';
|
|
6
|
+
|
|
7
|
+
// Runs that go on in a worktree which is not shown. Each list is saved in
|
|
8
|
+
// the logs of its own directory and is taken back when that one is shown
|
|
9
|
+
class AwayRuns {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.lists = new Map();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
park(root, jobs) {
|
|
15
|
+
if (root && jobs.some(isRunning)) this.lists.set(root, jobs);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
take(root) {
|
|
19
|
+
const jobs = this.lists.get(root);
|
|
20
|
+
this.lists.delete(root);
|
|
21
|
+
return jobs;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
jobs() {
|
|
25
|
+
return [...this.lists.values()].flat();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
save() {
|
|
29
|
+
for (const [root, jobs] of this.lists) {
|
|
30
|
+
saveRunFiles(root, jobs);
|
|
31
|
+
if (!jobs.some(isRunning)) this.lists.delete(root);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The count of runs in work by directory, with the list that is shown
|
|
36
|
+
counts(root, jobs) {
|
|
37
|
+
const lists = [...this.lists.entries()];
|
|
38
|
+
if (root) lists.push([root, jobs]);
|
|
39
|
+
const live = new Map();
|
|
40
|
+
for (const [dir, list] of lists) {
|
|
41
|
+
const count = list.filter(isRunning).length;
|
|
42
|
+
if (count) live.set(dir, count);
|
|
43
|
+
}
|
|
44
|
+
return live;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { AwayRuns };
|
|
@@ -4,33 +4,12 @@ const path = require('node:path');
|
|
|
4
4
|
const { isHashObject } = require('metautil');
|
|
5
5
|
const { pad2, withNotice, readText } = require('../../common/utilities.js');
|
|
6
6
|
const { LOG_DIR } = require('../../common/files.js');
|
|
7
|
-
const {
|
|
7
|
+
const { choiceOf } = require('../../agents/models.js');
|
|
8
8
|
|
|
9
9
|
const RUN_KEEP = 40;
|
|
10
10
|
|
|
11
11
|
const OUTPUT_KEEP = 100000;
|
|
12
12
|
|
|
13
|
-
const UUID = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';
|
|
14
|
-
|
|
15
|
-
const SESSION_FLAG = '--(?:resume|conversation)';
|
|
16
|
-
|
|
17
|
-
const SESSION_MARK = new RegExp(`${SESSION_FLAG}[= ](${UUID})`, 'gi');
|
|
18
|
-
|
|
19
|
-
const FILES_MARK = /(\d[\d,]*)\s+files?\s+(?:edited|changed|modified)/gi;
|
|
20
|
-
|
|
21
|
-
const tokenPattern = (name) => {
|
|
22
|
-
const word = `${name}[_ -]?tokens?`;
|
|
23
|
-
const json = `"${name}_tokens"`;
|
|
24
|
-
const body = `(?:${word}|${json})\\s*[:=]\\s*"?(\\d[\\d,]*)`;
|
|
25
|
-
return new RegExp(body, 'gi');
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const TOTAL_TOKEN = tokenPattern('total');
|
|
29
|
-
|
|
30
|
-
const INPUT_TOKEN = tokenPattern('input');
|
|
31
|
-
|
|
32
|
-
const OUTPUT_TOKEN = tokenPattern('output');
|
|
33
|
-
|
|
34
13
|
const exitLabel = (status) => {
|
|
35
14
|
if (status === null || status === undefined || status === '') {
|
|
36
15
|
return 'stopped';
|
|
@@ -70,43 +49,6 @@ const countField = (value) => {
|
|
|
70
49
|
return countOf(value);
|
|
71
50
|
};
|
|
72
51
|
|
|
73
|
-
const lastCount = (text, re) => {
|
|
74
|
-
re.lastIndex = 0;
|
|
75
|
-
let found = null;
|
|
76
|
-
for (const match of `${text ?? ''}`.matchAll(re)) {
|
|
77
|
-
const count = countOf(match[1]);
|
|
78
|
-
if (count !== null) found = count;
|
|
79
|
-
}
|
|
80
|
-
return found;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const sessionOf = (text) => {
|
|
84
|
-
SESSION_MARK.lastIndex = 0;
|
|
85
|
-
let found = '';
|
|
86
|
-
for (const match of `${text ?? ''}`.matchAll(SESSION_MARK)) found = match[1];
|
|
87
|
-
return found;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const tokensOf = (text) => {
|
|
91
|
-
const total = lastCount(text, TOTAL_TOKEN);
|
|
92
|
-
if (total !== null) return total;
|
|
93
|
-
const input = lastCount(text, INPUT_TOKEN);
|
|
94
|
-
const output = lastCount(text, OUTPUT_TOKEN);
|
|
95
|
-
if (input === null && output === null) return null;
|
|
96
|
-
return (input ?? 0) + (output ?? 0);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const runStats = (text) => {
|
|
100
|
-
const stats = {};
|
|
101
|
-
const session = sessionOf(text);
|
|
102
|
-
const files = lastCount(text, FILES_MARK);
|
|
103
|
-
const tokens = tokensOf(text);
|
|
104
|
-
if (session) stats.session = session;
|
|
105
|
-
if (files !== null) stats.files = files;
|
|
106
|
-
if (tokens !== null) stats.tokens = tokens;
|
|
107
|
-
return stats;
|
|
108
|
-
};
|
|
109
|
-
|
|
110
52
|
const restoredState = (item) => {
|
|
111
53
|
if (item.running === true || item.status === 'running') {
|
|
112
54
|
return { status: 'running', exit: null };
|
|
@@ -139,24 +81,17 @@ const applyElapsed = (job, item) => {
|
|
|
139
81
|
const readLog = (root, name) =>
|
|
140
82
|
root && name ? readText(path.join(root, LOG_DIR, name)) : '';
|
|
141
83
|
|
|
142
|
-
// An agent with a denial mark exits 0 having done nothing: silent or refused
|
|
143
|
-
const fruitless = (job, text) => {
|
|
144
|
-
if (job.action !== 'run') return false;
|
|
145
|
-
const spec = agentSpec(job.cliId);
|
|
146
|
-
if (!spec || !spec.denied) return false;
|
|
147
|
-
return !text.trim() || spec.denied.test(text);
|
|
148
|
-
};
|
|
149
|
-
|
|
150
84
|
class AgentJob {
|
|
151
85
|
constructor(fields) {
|
|
152
86
|
this.id = fields.id;
|
|
153
87
|
this.cliId = fields.cliId;
|
|
154
88
|
this.name = fields.name;
|
|
155
89
|
this.model = fields.model;
|
|
156
|
-
this.
|
|
157
|
-
this.
|
|
90
|
+
this.choice = choiceOf(fields.choice ?? { model: fields.model });
|
|
91
|
+
this.resume = textOf(fields.resume);
|
|
158
92
|
this.command = fields.command;
|
|
159
93
|
this.plan = fields.plan;
|
|
94
|
+
this.title = textOf(fields.title);
|
|
160
95
|
this.action = fields.action;
|
|
161
96
|
this.status = fields.status;
|
|
162
97
|
this.exit = fields.exit;
|
|
@@ -182,10 +117,11 @@ class AgentJob {
|
|
|
182
117
|
cliId: launch.id,
|
|
183
118
|
name: launch.name,
|
|
184
119
|
model,
|
|
185
|
-
|
|
186
|
-
|
|
120
|
+
choice: launch.choice,
|
|
121
|
+
resume: launch.session,
|
|
187
122
|
command: launch.command,
|
|
188
123
|
plan: launch.plan,
|
|
124
|
+
title: textOf(launch.title),
|
|
189
125
|
action: launch.kind,
|
|
190
126
|
status: 'running',
|
|
191
127
|
exit: null,
|
|
@@ -205,10 +141,11 @@ class AgentJob {
|
|
|
205
141
|
cliId: textOf(item.cliId),
|
|
206
142
|
name: textOf(item.name),
|
|
207
143
|
model: textOf(item.model),
|
|
208
|
-
|
|
209
|
-
|
|
144
|
+
choice: item.choice,
|
|
145
|
+
resume: item.resume,
|
|
210
146
|
command: textOf(item.command),
|
|
211
147
|
plan: textOf(item.plan),
|
|
148
|
+
title: textOf(item.title),
|
|
212
149
|
action: textOf(item.action, 'run'),
|
|
213
150
|
status: state.status,
|
|
214
151
|
exit: state.exit,
|
|
@@ -235,8 +172,12 @@ class AgentJob {
|
|
|
235
172
|
return elapsedLabel(this, now);
|
|
236
173
|
}
|
|
237
174
|
|
|
238
|
-
|
|
239
|
-
|
|
175
|
+
// The session is known as soon as it opens, long before the run ends
|
|
176
|
+
captureStats(given) {
|
|
177
|
+
const { child } = this;
|
|
178
|
+
const live = child && typeof child.stats === 'function';
|
|
179
|
+
const stats = given ?? (live ? child.stats() : null);
|
|
180
|
+
if (!stats) return;
|
|
240
181
|
if (stats.session) this.session = stats.session;
|
|
241
182
|
if (typeof stats.files === 'number') this.files = stats.files;
|
|
242
183
|
if (typeof stats.tokens === 'number') this.tokens = stats.tokens;
|
|
@@ -254,8 +195,7 @@ class AgentJob {
|
|
|
254
195
|
cliId: this.cliId,
|
|
255
196
|
name: this.name,
|
|
256
197
|
model: this.model,
|
|
257
|
-
|
|
258
|
-
args: this.args,
|
|
198
|
+
choice: this.choice,
|
|
259
199
|
command: this.command,
|
|
260
200
|
plan: this.plan,
|
|
261
201
|
action: this.action,
|
|
@@ -263,8 +203,10 @@ class AgentJob {
|
|
|
263
203
|
elapsed: elapsedSeconds(this),
|
|
264
204
|
startedAt: this.startedAt,
|
|
265
205
|
};
|
|
206
|
+
if (this.title) saved.title = this.title;
|
|
266
207
|
if (this.logName) saved.log = this.logName;
|
|
267
208
|
if (this.rawName) saved.raw = this.rawName;
|
|
209
|
+
if (this.resume) saved.resume = this.resume;
|
|
268
210
|
if (this.session) saved.session = this.session;
|
|
269
211
|
if (typeof this.files === 'number') saved.files = this.files;
|
|
270
212
|
return saved;
|
|
@@ -284,13 +226,8 @@ class AgentJob {
|
|
|
284
226
|
}
|
|
285
227
|
|
|
286
228
|
finish(result) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (status === 0 && fruitless(this, text)) {
|
|
290
|
-
status = 1;
|
|
291
|
-
if (!text.trim()) text = withNotice(text, 'Agent exited without output');
|
|
292
|
-
}
|
|
293
|
-
this.close(exitLabel(status), status, text);
|
|
229
|
+
this.captureStats(result.stats);
|
|
230
|
+
this.close(exitLabel(result.status), result.status, result.text ?? '');
|
|
294
231
|
}
|
|
295
232
|
}
|
|
296
233
|
|
|
@@ -300,7 +237,8 @@ const restoreJobs = (saved) => {
|
|
|
300
237
|
const jobs = [];
|
|
301
238
|
let nextId = 1;
|
|
302
239
|
for (const item of Array.isArray(saved) ? saved : []) {
|
|
303
|
-
|
|
240
|
+
// Signing in was a run of its own before agents were reached over ACP
|
|
241
|
+
if (!isHashObject(item) || item.action === 'login') continue;
|
|
304
242
|
const job = AgentJob.restore(item, nextId);
|
|
305
243
|
nextId = Math.max(nextId, job.id + 1);
|
|
306
244
|
jobs.push(job);
|
|
@@ -313,7 +251,6 @@ module.exports = {
|
|
|
313
251
|
OUTPUT_KEEP,
|
|
314
252
|
elapsedLabel,
|
|
315
253
|
textOf,
|
|
316
|
-
runStats,
|
|
317
254
|
readLog,
|
|
318
255
|
launchJob,
|
|
319
256
|
restoreJobs,
|
|
@@ -8,7 +8,6 @@ const { logText, nextLogFile } = require('../../runs/logs.js');
|
|
|
8
8
|
const jobPart = require('./job.js');
|
|
9
9
|
const { OUTPUT_KEEP, readLog, textOf } = jobPart;
|
|
10
10
|
const { elapsedLabel, RUN_KEEP, restoreJobs, launchJob } = jobPart;
|
|
11
|
-
const { runStats } = jobPart;
|
|
12
11
|
const prefsPart = require('../prefs.js');
|
|
13
12
|
const { planPath, readPrefs, writePrefs } = prefsPart;
|
|
14
13
|
const { writeStored } = prefsPart;
|
|
@@ -104,19 +103,6 @@ const runBlock = (root, job) => {
|
|
|
104
103
|
return `${stem}\n${body}`;
|
|
105
104
|
};
|
|
106
105
|
|
|
107
|
-
const replayLaunch = (job) => ({
|
|
108
|
-
ok: true,
|
|
109
|
-
cmd: job.cmd,
|
|
110
|
-
args: job.args.slice(),
|
|
111
|
-
prompt: '',
|
|
112
|
-
plan: job.plan,
|
|
113
|
-
id: job.cliId,
|
|
114
|
-
name: job.name,
|
|
115
|
-
kind: job.action || 'run',
|
|
116
|
-
command: job.command,
|
|
117
|
-
session: job.session,
|
|
118
|
-
});
|
|
119
|
-
|
|
120
106
|
const pruneAgent = (root, jobs) => {
|
|
121
107
|
const keep = new Set();
|
|
122
108
|
for (const job of jobs) {
|
|
@@ -174,10 +160,12 @@ const sessionEntry = (prev, job) => {
|
|
|
174
160
|
const prior = isHashObject(prev) ? prev : {};
|
|
175
161
|
const entry = { cli: job.cliId || prior.cli || '' };
|
|
176
162
|
const name = stemOf(job.logName) || prior.name || '';
|
|
163
|
+
const title = textOf(job.title) || textOf(prior.title);
|
|
177
164
|
const startedAt = Number(job.startedAt) || Number(prior.startedAt) || 0;
|
|
178
165
|
const files = typeof job.files === 'number' ? job.files : prior.files;
|
|
179
166
|
const tokens = typeof job.tokens === 'number' ? job.tokens : prior.tokens;
|
|
180
167
|
if (name) entry.name = name;
|
|
168
|
+
if (title) entry.title = title;
|
|
181
169
|
if (startedAt) entry.startedAt = startedAt;
|
|
182
170
|
if (typeof files === 'number') entry.files = files;
|
|
183
171
|
if (typeof tokens === 'number') entry.tokens = tokens;
|
|
@@ -194,15 +182,17 @@ const replacedStems = (jobs) => {
|
|
|
194
182
|
return stems;
|
|
195
183
|
};
|
|
196
184
|
|
|
197
|
-
const rememberSession = (rows, id, stem, at) => {
|
|
198
|
-
const blank = { id, name: '', startedAt: 0 };
|
|
185
|
+
const rememberSession = (rows, id, stem, at, title) => {
|
|
186
|
+
const blank = { id, name: '', title: '', startedAt: 0 };
|
|
199
187
|
const prev = rows.get(id) || blank;
|
|
200
188
|
if (at >= prev.startedAt) {
|
|
201
189
|
prev.startedAt = at;
|
|
202
190
|
if (stem) prev.name = stem;
|
|
191
|
+
if (title) prev.title = title;
|
|
203
192
|
} else if (!prev.name && stem) {
|
|
204
193
|
prev.name = stem;
|
|
205
194
|
}
|
|
195
|
+
if (!prev.title && title) prev.title = title;
|
|
206
196
|
rows.set(id, prev);
|
|
207
197
|
};
|
|
208
198
|
|
|
@@ -214,16 +204,17 @@ const listSessions = (root, jobs, cliId) => {
|
|
|
214
204
|
if (hidden.has(id)) continue;
|
|
215
205
|
if (!isHashObject(entry) || entry.cli !== cliId) continue;
|
|
216
206
|
const name = textOf(entry.name);
|
|
207
|
+
const title = textOf(entry.title);
|
|
217
208
|
const startedAt = Number(entry.startedAt) || 0;
|
|
218
|
-
rows.set(id, { id, name, startedAt });
|
|
209
|
+
rows.set(id, { id, name, title, startedAt });
|
|
219
210
|
}
|
|
220
211
|
for (const job of jobs) {
|
|
221
|
-
if (job.cliId !== cliId
|
|
212
|
+
if (job.cliId !== cliId) continue;
|
|
222
213
|
const id = sessionKey(job);
|
|
223
214
|
if (!id) continue;
|
|
224
215
|
const stem = stemOf(job.logName);
|
|
225
216
|
const at = Number(job.startedAt) || 0;
|
|
226
|
-
rememberSession(rows, id, stem, at);
|
|
217
|
+
rememberSession(rows, id, stem, at, textOf(job.title));
|
|
227
218
|
}
|
|
228
219
|
const list = [...rows.values()];
|
|
229
220
|
list.sort((left, right) => {
|
|
@@ -240,7 +231,7 @@ const panelSessions = (root, jobs, cliId) => {
|
|
|
240
231
|
for (const session of found) {
|
|
241
232
|
rows.push({
|
|
242
233
|
id: session.id,
|
|
243
|
-
name: session.name || session.id,
|
|
234
|
+
name: session.title || session.name || session.id,
|
|
244
235
|
session: true,
|
|
245
236
|
});
|
|
246
237
|
}
|
|
@@ -251,7 +242,6 @@ const runningJobs = (jobs, cliId) => {
|
|
|
251
242
|
const runs = [];
|
|
252
243
|
for (const job of jobs) {
|
|
253
244
|
if (job.cliId !== cliId || job.status !== 'running') continue;
|
|
254
|
-
if (job.action === 'login') continue;
|
|
255
245
|
runs.push(job);
|
|
256
246
|
}
|
|
257
247
|
return runs;
|
|
@@ -260,15 +250,15 @@ const runningJobs = (jobs, cliId) => {
|
|
|
260
250
|
const liveRun = (job, progress) => {
|
|
261
251
|
const stem = stemOf(job.logName);
|
|
262
252
|
const id = sessionKey(job);
|
|
263
|
-
const
|
|
253
|
+
const caption = job.title || stem || job.command || job.name;
|
|
264
254
|
return {
|
|
265
255
|
id,
|
|
266
256
|
jobId: job.id,
|
|
267
|
-
name:
|
|
257
|
+
name: caption,
|
|
268
258
|
status: job.status,
|
|
269
259
|
elapsed: elapsedLabel(job),
|
|
270
260
|
progress,
|
|
271
|
-
command,
|
|
261
|
+
command: caption,
|
|
272
262
|
};
|
|
273
263
|
};
|
|
274
264
|
|
|
@@ -295,7 +285,6 @@ const withLiveRuns = (saved, running, progressOf) => {
|
|
|
295
285
|
const mergeSessions = (saved, jobs) => {
|
|
296
286
|
const sessions = { ...saved };
|
|
297
287
|
for (const job of jobs) {
|
|
298
|
-
if (job.action === 'login') continue;
|
|
299
288
|
const stem = stemOf(job.logName);
|
|
300
289
|
const id = textOf(job.session);
|
|
301
290
|
const done = job.status !== 'running';
|
|
@@ -341,12 +330,10 @@ module.exports = {
|
|
|
341
330
|
elapsedLabel,
|
|
342
331
|
launchJob,
|
|
343
332
|
restoreJobs,
|
|
344
|
-
runStats,
|
|
345
333
|
assignLog,
|
|
346
334
|
stemOf,
|
|
347
335
|
orderedSessions,
|
|
348
336
|
runBlock,
|
|
349
|
-
replayLaunch,
|
|
350
337
|
panelSessions,
|
|
351
338
|
runningJobs,
|
|
352
339
|
withLiveRuns,
|
|
@@ -4,92 +4,57 @@ const { isHashObject } = require('metautil');
|
|
|
4
4
|
const { trimText } = require('../../common/utilities.js');
|
|
5
5
|
|
|
6
6
|
const models = require('../../agents/models.js');
|
|
7
|
-
const { catalogOf, fitEffort,
|
|
7
|
+
const { catalogOf, fitEffort, concrete } = models;
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
const concrete = (name) => {
|
|
12
|
-
const text = trimText(name);
|
|
13
|
-
return text === 'default' ? '' : text;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const menuNames = (catalog) => {
|
|
17
|
-
const names = catalog.entries.map((entry) => concrete(entry.model));
|
|
18
|
-
const known = new Set(names);
|
|
19
|
-
const shown = names.filter((name) => {
|
|
20
|
-
const base = withoutFast(name);
|
|
21
|
-
return name && (name === base || !known.has(base));
|
|
22
|
-
});
|
|
23
|
-
return [...new Set(shown)];
|
|
24
|
-
};
|
|
9
|
+
const STORED_FIELDS = ['model', 'effort', 'context'];
|
|
25
10
|
|
|
11
|
+
// The choices one agent row offers, from what its session lists
|
|
26
12
|
class AgentOptions {
|
|
27
13
|
constructor(row) {
|
|
28
14
|
this.row = row;
|
|
29
|
-
this.catalog = catalogOf(row.
|
|
30
|
-
this.names =
|
|
15
|
+
this.catalog = catalogOf(row.catalog);
|
|
16
|
+
this.names = this.catalog.names;
|
|
31
17
|
}
|
|
32
18
|
|
|
19
|
+
// A saved model stays as it is until the agent has listed its own
|
|
33
20
|
shown(model) {
|
|
34
|
-
const saved =
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (this.names.includes(saved)) return saved;
|
|
38
|
-
return this.names[0] || '';
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// What the cli lists wins over the levels the spec falls back to
|
|
42
|
-
levels(model) {
|
|
43
|
-
const resolved = this.catalog.resolve({ model, effort: 'default' });
|
|
44
|
-
if (resolved.encoded) return resolved.levels;
|
|
45
|
-
const { modelEfforts, listedEfforts, spec } = this.row;
|
|
46
|
-
const own = modelEfforts ? modelEfforts.get(resolved.model) : null;
|
|
47
|
-
const listed = own ?? listedEfforts;
|
|
48
|
-
return listed ? ['default', ...listed] : spec.efforts;
|
|
21
|
+
const saved = trimText(model);
|
|
22
|
+
if (!this.catalog.loaded || this.catalog.find(saved)) return saved;
|
|
23
|
+
return this.catalog.current || this.names[0] || '';
|
|
49
24
|
}
|
|
50
25
|
|
|
51
26
|
efforts(model) {
|
|
52
|
-
return this.
|
|
27
|
+
return this.catalog.efforts(this.shown(model));
|
|
53
28
|
}
|
|
54
29
|
|
|
55
30
|
contexts(model) {
|
|
56
|
-
return
|
|
57
|
-
model,
|
|
58
|
-
this.row.wide,
|
|
59
|
-
this.catalog,
|
|
60
|
-
this.row.spec.contexts,
|
|
61
|
-
);
|
|
31
|
+
return this.catalog.contexts(this.shown(model));
|
|
62
32
|
}
|
|
63
33
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return Boolean(base) && this.catalog.find(`${base}-fast`) !== null;
|
|
34
|
+
fasts(model) {
|
|
35
|
+
return this.catalog.fasts(this.shown(model));
|
|
67
36
|
}
|
|
68
37
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
38
|
+
// Before the agent has listed anything the saved choice is all there is
|
|
39
|
+
settings(picked) {
|
|
40
|
+
if (this.catalog.loaded) {
|
|
41
|
+
return this.catalog.resolve(this.shown(picked.model), picked);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
effort: concrete(picked.effort),
|
|
45
|
+
context: trimText(picked.context),
|
|
46
|
+
fast: picked.fast === true ? 'on' : 'off',
|
|
47
|
+
};
|
|
78
48
|
}
|
|
79
49
|
|
|
80
|
-
align(picked
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
picked.effort = fitEffort(this.levels(basis), picked.effort);
|
|
84
|
-
picked.context = this.fitContext(shown, picked.context);
|
|
85
|
-
const lost = this.catalog.hasFast() && !this.hasFast(shown);
|
|
86
|
-
if (picked.fast && lost) picked.fast = false;
|
|
87
|
-
return shown;
|
|
50
|
+
align(picked) {
|
|
51
|
+
if (!this.catalog.loaded) return;
|
|
52
|
+
picked.effort = fitEffort(this.efforts(picked.model), picked.effort);
|
|
88
53
|
}
|
|
89
54
|
|
|
90
|
-
normalize(picked
|
|
91
|
-
|
|
92
|
-
return this.
|
|
55
|
+
normalize(picked) {
|
|
56
|
+
this.align(picked);
|
|
57
|
+
return this.shown(picked.model);
|
|
93
58
|
}
|
|
94
59
|
|
|
95
60
|
preferEffort(picked, level) {
|
|
@@ -99,55 +64,34 @@ class AgentOptions {
|
|
|
99
64
|
|
|
100
65
|
pickModel(picked, model) {
|
|
101
66
|
picked.model = model;
|
|
102
|
-
this.align(picked
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
pickContext(picked, size) {
|
|
106
|
-
picked.context = size;
|
|
107
|
-
const offers = (model) => this.contexts(model).includes(size);
|
|
108
|
-
if (!offers(this.shown(picked.model))) {
|
|
109
|
-
picked.model = this.firstModel(offers) || picked.model;
|
|
110
|
-
}
|
|
111
|
-
const model = this.shown(picked.model);
|
|
112
|
-
this.align(picked, model);
|
|
113
|
-
if (offers(model)) picked.context = size;
|
|
67
|
+
this.align(picked);
|
|
114
68
|
}
|
|
115
69
|
|
|
116
70
|
toggleFast(picked) {
|
|
71
|
+
if (this.fasts(picked.model).length < 2) return false;
|
|
117
72
|
picked.fast = picked.fast !== true;
|
|
118
|
-
|
|
119
|
-
const missing = this.catalog.hasFast() && !this.hasFast(shown);
|
|
120
|
-
if (picked.fast && missing) {
|
|
121
|
-
const next = this.firstModel((name) => this.hasFast(name));
|
|
122
|
-
if (next) picked.model = next;
|
|
123
|
-
else picked.fast = false;
|
|
124
|
-
}
|
|
125
|
-
this.align(picked, this.shown(picked.model));
|
|
73
|
+
return true;
|
|
126
74
|
}
|
|
127
75
|
}
|
|
128
76
|
|
|
129
|
-
const
|
|
130
|
-
spec.savesExtra ? [...RESTORED_FIELDS, 'extra'] : RESTORED_FIELDS;
|
|
131
|
-
|
|
132
|
-
const restoreChoice = (picked, saved, spec) => {
|
|
133
|
-
if (!isHashObject(saved)) return;
|
|
134
|
-
for (const field of restoredFields(spec)) {
|
|
135
|
-
const value = concrete(saved[field]);
|
|
136
|
-
if (value) picked[field] = value;
|
|
137
|
-
}
|
|
138
|
-
if (saved.fast === true) picked.fast = true;
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const storedChoice = (picked, spec) => {
|
|
77
|
+
const storedChoice = (picked) => {
|
|
142
78
|
const fields = [
|
|
143
|
-
['model',
|
|
79
|
+
['model', trimText(picked.model)],
|
|
144
80
|
['effort', concrete(picked.effort)],
|
|
145
|
-
['extra', spec.savesExtra ? concrete(picked.extra) : ''],
|
|
146
81
|
['fast', picked.fast === true],
|
|
147
|
-
['context',
|
|
82
|
+
['context', trimText(picked.context)],
|
|
148
83
|
];
|
|
149
84
|
const stored = Object.fromEntries(fields.filter((field) => field[1]));
|
|
150
85
|
return Object.keys(stored).length ? stored : null;
|
|
151
86
|
};
|
|
152
87
|
|
|
153
|
-
|
|
88
|
+
const restoreChoice = (picked, saved) => {
|
|
89
|
+
if (!isHashObject(saved)) return;
|
|
90
|
+
const stored = storedChoice(saved) ?? {};
|
|
91
|
+
for (const field of STORED_FIELDS) {
|
|
92
|
+
if (stored[field]) picked[field] = stored[field];
|
|
93
|
+
}
|
|
94
|
+
if (stored.fast) picked.fast = true;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
module.exports = { AgentOptions, restoreChoice, storedChoice };
|
|
@@ -8,6 +8,57 @@ const review = require('../../review/review.js');
|
|
|
8
8
|
const { parseReview, reviewProgress, frontmatterStatus } = review;
|
|
9
9
|
const { trimText } = require('../../common/utilities.js');
|
|
10
10
|
|
|
11
|
+
const FRONTMATTER = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/;
|
|
12
|
+
|
|
13
|
+
const HEADING = /^#{1,3}\s+(.+)$/;
|
|
14
|
+
|
|
15
|
+
const TASK = /^- \[[ xX]\]\s+(.+)$/;
|
|
16
|
+
|
|
17
|
+
const DATE_TOKEN = /\d{4}-\d{2}-\d{2}(?:-\d+)?/;
|
|
18
|
+
|
|
19
|
+
const ITEM_PREFIX = /^F\d+:\s*/;
|
|
20
|
+
|
|
21
|
+
const CAPTION_MAX = 72;
|
|
22
|
+
|
|
23
|
+
const clipCaption = (text) => {
|
|
24
|
+
const line = text.replace(/\s+/g, ' ').trim();
|
|
25
|
+
if (line.length <= CAPTION_MAX) return line;
|
|
26
|
+
return `${line.slice(0, CAPTION_MAX - 1)}…`;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const captionFromPlan = (text) => {
|
|
30
|
+
const body = `${text ?? ''}`.replace(FRONTMATTER, '');
|
|
31
|
+
let heading = '';
|
|
32
|
+
let task = '';
|
|
33
|
+
for (const raw of body.split('\n')) {
|
|
34
|
+
const line = raw.trim();
|
|
35
|
+
if (!heading) {
|
|
36
|
+
const head = HEADING.exec(line);
|
|
37
|
+
if (head) heading = head[1].trim();
|
|
38
|
+
}
|
|
39
|
+
if (!task) {
|
|
40
|
+
const item = TASK.exec(line);
|
|
41
|
+
if (item) task = item[1].replace(ITEM_PREFIX, '').trim();
|
|
42
|
+
}
|
|
43
|
+
if (heading && task) break;
|
|
44
|
+
}
|
|
45
|
+
if (heading && !DATE_TOKEN.test(heading)) return clipCaption(heading);
|
|
46
|
+
if (task) return clipCaption(task);
|
|
47
|
+
return '';
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const planCaption = (root, plan) => {
|
|
51
|
+
const rel = `${plan ?? ''}`.trim();
|
|
52
|
+
if (!rel || !root) return '';
|
|
53
|
+
const file = path.isAbsolute(rel) ? rel : path.join(root, rel);
|
|
54
|
+
try {
|
|
55
|
+
return captionFromPlan(fs.readFileSync(file, 'utf8'));
|
|
56
|
+
} catch {
|
|
57
|
+
// missing plan
|
|
58
|
+
return '';
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
11
62
|
const planKey = (value) => {
|
|
12
63
|
const text = trimText(value);
|
|
13
64
|
if (!text) return '';
|
|
@@ -75,6 +126,8 @@ const alignPlanLabels = (parts) => {
|
|
|
75
126
|
};
|
|
76
127
|
|
|
77
128
|
module.exports = {
|
|
129
|
+
captionFromPlan,
|
|
130
|
+
planCaption,
|
|
78
131
|
planKey,
|
|
79
132
|
planProgress,
|
|
80
133
|
planPart,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const NO_PICK = {
|
|
4
|
+
agentPick: '',
|
|
5
|
+
agentMenu: null,
|
|
6
|
+
agentQuery: '',
|
|
7
|
+
agentEditor: null,
|
|
8
|
+
agentMenuOff: [],
|
|
9
|
+
agentPickCursor: 0,
|
|
10
|
+
agentMenuScroll: 0,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const NO_LIST = {
|
|
14
|
+
agents: null,
|
|
15
|
+
agentInstallPackage: '',
|
|
16
|
+
agentRuns: [],
|
|
17
|
+
agentRunCursor: 0,
|
|
18
|
+
agentRunScroll: 0,
|
|
19
|
+
agentFocus: 'cli',
|
|
20
|
+
agentCanStart: false,
|
|
21
|
+
agentPlanCount: 0,
|
|
22
|
+
agentView: false,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const NO_JOB = {
|
|
26
|
+
agentOutput: '',
|
|
27
|
+
agentCommand: '',
|
|
28
|
+
agentElapsed: '',
|
|
29
|
+
agentProgress: '',
|
|
30
|
+
agentRunning: false,
|
|
31
|
+
agentStatus: '',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
module.exports = { NO_PICK, NO_LIST, NO_JOB };
|