@illuminis/comprism 0.1.4 → 0.1.5
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/out/agent/command.js +1 -1
- package/out/agent/render.d.ts +39 -6
- package/out/agent/render.js +287 -28
- package/out/agent/session.d.ts +15 -0
- package/out/agent/session.js +64 -9
- package/out/commands/ask.js +12 -3
- package/out/commands/login.js +63 -9
- package/out/commands/repl.d.ts +4 -0
- package/out/commands/repl.js +456 -18
- package/out/commands/welcome.d.ts +25 -1
- package/out/commands/welcome.js +71 -35
- package/out/lib/gateway.d.ts +4 -0
- package/out/lib/keyboard.d.ts +3 -0
- package/out/lib/keyboard.js +7 -0
- package/out/lib/lastlogin.d.ts +14 -0
- package/out/lib/lastlogin.js +105 -0
- package/out/lib/picker.d.ts +1 -0
- package/out/lib/picker.js +158 -0
- package/out/lib/prompt.d.ts +3 -0
- package/out/lib/prompt.js +21 -0
- package/out/lib/startcache.d.ts +13 -0
- package/out/lib/startcache.js +78 -0
- package/out/lib/ui.d.ts +8 -0
- package/out/lib/ui.js +34 -0
- package/out/lib/voice.d.ts +36 -0
- package/out/lib/voice.js +136 -1
- package/out/native/dictate +0 -0
- package/out/thin.js +2 -2
- package/package.json +3 -2
package/out/commands/repl.js
CHANGED
|
@@ -127,6 +127,13 @@ function takeTerminal() {
|
|
|
127
127
|
completer: complete,
|
|
128
128
|
});
|
|
129
129
|
keyboard.bind(reader);
|
|
130
|
+
// Enter has already moved the cursor onto the first line of the live list,
|
|
131
|
+
// so clearing from here down removes it before anything else prints.
|
|
132
|
+
reader.on('line', () => {
|
|
133
|
+
if (popupShown)
|
|
134
|
+
process.stdout.write(`${CSI}J`);
|
|
135
|
+
popupShown = 0;
|
|
136
|
+
});
|
|
130
137
|
reader.on('line', onLine);
|
|
131
138
|
reader.on('SIGINT', () => interrupt());
|
|
132
139
|
}
|
|
@@ -136,6 +143,7 @@ function takeTerminal() {
|
|
|
136
143
|
* Anywhere else it offers nothing, so it never rewrites a sentence.
|
|
137
144
|
*/
|
|
138
145
|
function complete(line, done) {
|
|
146
|
+
clearPopup();
|
|
139
147
|
const slash = /^\/(\S*)$/.exec(line);
|
|
140
148
|
if (!slash) {
|
|
141
149
|
done(null, attach.completeAt(line, () => {
|
|
@@ -172,6 +180,103 @@ function complete(line, done) {
|
|
|
172
180
|
edit.cursor = edit.line.length;
|
|
173
181
|
reader.prompt(true);
|
|
174
182
|
}
|
|
183
|
+
const CSI = `${String.fromCharCode(27)}[`;
|
|
184
|
+
const SAVE = `${String.fromCharCode(27)}7`;
|
|
185
|
+
const RESTORE = `${String.fromCharCode(27)}8`;
|
|
186
|
+
/** How many lines the live list under the prompt holds now, 0 when none. */
|
|
187
|
+
let popupShown = 0;
|
|
188
|
+
const POPUP_ROWS = 8;
|
|
189
|
+
/** What the list under the prompt holds, and which of it is marked (-1: none). */
|
|
190
|
+
let popupHits = [];
|
|
191
|
+
let popupAt = -1;
|
|
192
|
+
/**
|
|
193
|
+
* Up and Down move through the list under the prompt; Enter runs the marked
|
|
194
|
+
* command, Tab fills it in to add to. With nothing marked, or no list showing,
|
|
195
|
+
* every key does what it always did (Up and Down recall earlier requests).
|
|
196
|
+
*/
|
|
197
|
+
keyboard.onMenuKey = (key) => {
|
|
198
|
+
if (!popupShown || !reader || popupHits.length === 0)
|
|
199
|
+
return false;
|
|
200
|
+
const edit = reader;
|
|
201
|
+
if (key === 'up' || key === 'down') {
|
|
202
|
+
popupAt = key === 'down' ? Math.min(popupHits.length - 1, popupAt + 1) : Math.max(-1, popupAt - 1);
|
|
203
|
+
updatePopup();
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
if (popupAt < 0)
|
|
207
|
+
return false;
|
|
208
|
+
const chosen = `/${popupHits[popupAt].name}`;
|
|
209
|
+
if (key === 'tab') {
|
|
210
|
+
edit.line = `${chosen} `;
|
|
211
|
+
edit.cursor = edit.line.length;
|
|
212
|
+
edit._refreshLine();
|
|
213
|
+
clearPopup();
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
// Enter: the marked command replaces what was typed, then Enter goes on.
|
|
217
|
+
edit.line = chosen;
|
|
218
|
+
edit.cursor = chosen.length;
|
|
219
|
+
popupAt = -1;
|
|
220
|
+
return false;
|
|
221
|
+
};
|
|
222
|
+
/** Takes the live list away, leaving the prompt and the cursor where they are. */
|
|
223
|
+
function clearPopup() {
|
|
224
|
+
if (!popupShown || !process.stdout.isTTY)
|
|
225
|
+
return;
|
|
226
|
+
// Each row the list used, blanked; never a clear to the end of the screen.
|
|
227
|
+
process.stdout.write(`${SAVE}${`${CSI}1E${CSI}2K`.repeat(popupShown)}${RESTORE}`);
|
|
228
|
+
popupShown = 0;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* The commands and skills that match what is typed, shown under the prompt
|
|
232
|
+
* while it is typed (manual 6.4). The same list Tab and `help` offer, from the
|
|
233
|
+
* service, so a skill added on the server appears here with no new build.
|
|
234
|
+
* Drawn only while the line is `/` and a word; anything else clears it.
|
|
235
|
+
*/
|
|
236
|
+
function updatePopup() {
|
|
237
|
+
if (!reader || !process.stdout.isTTY)
|
|
238
|
+
return;
|
|
239
|
+
const line = reader.line ?? '';
|
|
240
|
+
const slash = /^\/(\S*)$/.exec(line);
|
|
241
|
+
const typed = slash ? slash[1].toLowerCase() : '';
|
|
242
|
+
const hits = slash ? listedItems().filter((i) => i.name.startsWith(typed)) : [];
|
|
243
|
+
// A different set of matches starts with nothing marked, so Enter still
|
|
244
|
+
// sends what was typed until Down is pressed.
|
|
245
|
+
if (hits.map((h) => h.name).join() !== popupHits.map((h) => h.name).join())
|
|
246
|
+
popupAt = -1;
|
|
247
|
+
popupHits = hits;
|
|
248
|
+
// Shown for a whole word too, so the list stays in sight while it is chosen.
|
|
249
|
+
if (!hits.length) {
|
|
250
|
+
clearPopup();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const first = Math.max(0, Math.min(popupAt - POPUP_ROWS + 1, hits.length - POPUP_ROWS));
|
|
254
|
+
const shown = hits.slice(first, first + POPUP_ROWS);
|
|
255
|
+
const width = Math.min(18, Math.max(...shown.map((i) => i.name.length)) + 1);
|
|
256
|
+
const room = Math.max(20, (process.stdout.columns || 100) - width - 8);
|
|
257
|
+
const rows = shown.map((i, k) => {
|
|
258
|
+
const from = i.source === 'session' ? '' : ` ${i.source}`;
|
|
259
|
+
const words = `${i.description}${from}`;
|
|
260
|
+
const name = `/${i.name}`.padEnd(width + 1);
|
|
261
|
+
const marked = first + k === popupAt;
|
|
262
|
+
return `${marked ? ui.c.blue('\u25B8 ') : ' '}${marked ? ui.c.bold(ui.c.blue(name)) : ui.c.bold(name)} `
|
|
263
|
+
+ ui.c.dim(words.length > room ? `${words.slice(0, room - 3)}...` : words);
|
|
264
|
+
});
|
|
265
|
+
if (hits.length > shown.length)
|
|
266
|
+
rows.push(ui.c.dim(` ${hits.length - shown.length} more, Down to see them`));
|
|
267
|
+
const col = reader.getCursorPos().cols;
|
|
268
|
+
// A shorter list than last time blanks the rows it no longer uses, rather
|
|
269
|
+
// than clearing to the end of the screen: on the bottom row there is no line
|
|
270
|
+
// below to move to, and that clear wiped the list's own last row.
|
|
271
|
+
const height = rows.length;
|
|
272
|
+
while (rows.length < popupShown)
|
|
273
|
+
rows.push('');
|
|
274
|
+
// Room is made first, so a prompt on the bottom row scrolls up rather than
|
|
275
|
+
// the list running off the screen, and the cursor comes back to its column.
|
|
276
|
+
process.stdout.write(`${'\n'.repeat(rows.length)}${CSI}${rows.length}A${CSI}${col + 1}G${SAVE}`
|
|
277
|
+
+ rows.map((r) => `${CSI}1E${CSI}2K${r}`).join('') + RESTORE);
|
|
278
|
+
popupShown = Math.max(height, 1);
|
|
279
|
+
}
|
|
175
280
|
/** The service's list for this session, once fetched (manual 6.4). */
|
|
176
281
|
let listed = null;
|
|
177
282
|
/** The session words this tool runs. Sent to the service, which lists only
|
|
@@ -214,9 +319,10 @@ let interrupt = () => leaveSession();
|
|
|
214
319
|
/** Words that are commands. Anything else typed is work. */
|
|
215
320
|
const COMMANDS = new Set([
|
|
216
321
|
'new', 'sessions', 'branch', 'remote', 'model', 'providers', 'keys', 'status',
|
|
217
|
-
'voice', 'help', 'exit', 'quit', 'clear', 'login', 'logout', 'signout',
|
|
322
|
+
'voice', 'help', 'exit', 'quit', 'clear', 'login', 'logout', 'signout', 'history',
|
|
218
323
|
'disconnect', 'cost', 'spend', 'attached', 'paste', 'plan', 'todo', 'mode',
|
|
219
324
|
'queue', 'transcript', 'edit', 'context', 'compact', 'why', 'unpin', 'pin', 'effort',
|
|
325
|
+
'optimize',
|
|
220
326
|
]);
|
|
221
327
|
/** `processes` and `stop pN` (manual 5.13): what this session started, and
|
|
222
328
|
* stopping one by its number. Works while a job runs, too. True if handled. */
|
|
@@ -308,9 +414,9 @@ async function withTerminal(run) {
|
|
|
308
414
|
* request. Returns nothing when the workspace cannot be reached: a list is not
|
|
309
415
|
* worth stopping a session over.
|
|
310
416
|
*/
|
|
311
|
-
async function earlierSessions(run) {
|
|
417
|
+
async function earlierSessions(run, limit = 10) {
|
|
312
418
|
const { sessions } = await Promise.resolve().then(() => __importStar(require('../lib/sessions')));
|
|
313
|
-
const reply = await sessions('list', { limit
|
|
419
|
+
const reply = await sessions('list', { limit });
|
|
314
420
|
if (!reply.ok || !reply.sessions?.sessions)
|
|
315
421
|
return [];
|
|
316
422
|
const rows = reply.sessions.sessions;
|
|
@@ -324,7 +430,7 @@ async function earlierSessions(run) {
|
|
|
324
430
|
here.id = r.number;
|
|
325
431
|
return;
|
|
326
432
|
}
|
|
327
|
-
found.push({ number: r.number, line: lines[i] ?? r.number });
|
|
433
|
+
found.push({ number: r.number, line: lines[i] ?? r.number, card: r.card });
|
|
328
434
|
});
|
|
329
435
|
return found;
|
|
330
436
|
}
|
|
@@ -416,8 +522,100 @@ async function opening(how) {
|
|
|
416
522
|
for (const line of reply.sessions.lines)
|
|
417
523
|
out(line);
|
|
418
524
|
out();
|
|
525
|
+
// What the session was about, before the next question goes in.
|
|
526
|
+
await showConversation(reply.sessions.session_id, false);
|
|
419
527
|
return reopened(reply.sessions);
|
|
420
528
|
}
|
|
529
|
+
/**
|
|
530
|
+
* A session's earlier questions and answers, read back (manual 8.4).
|
|
531
|
+
*
|
|
532
|
+
* On reopening, the last three exchanges, each answer cut to its first lines,
|
|
533
|
+
* so the screen says what the session was about without filling it. `history`
|
|
534
|
+
* shows every turn in full. Answers are drawn the way a new answer is: tables
|
|
535
|
+
* as tables, emphasis as emphasis.
|
|
536
|
+
*/
|
|
537
|
+
async function showConversation(key, full) {
|
|
538
|
+
const { sessions } = await Promise.resolve().then(() => __importStar(require('../lib/sessions')));
|
|
539
|
+
const { plain } = await Promise.resolve().then(() => __importStar(require('../agent/render')));
|
|
540
|
+
const reply = await sessions('read', { key });
|
|
541
|
+
let turns = (reply.sessions?.turns) ?? [];
|
|
542
|
+
// A service released before it sent the turns still hands over the whole
|
|
543
|
+
// session on export, every request and answer in full. Read from there, so
|
|
544
|
+
// the answers are drawn with their headings and tables rather than as the
|
|
545
|
+
// one line summaries an older service lists.
|
|
546
|
+
if (reply.ok && turns.length === 0)
|
|
547
|
+
turns = await exportedTurns(key);
|
|
548
|
+
if (!reply.ok || turns.length === 0) {
|
|
549
|
+
if (full) {
|
|
550
|
+
out(ui.c.dim(' Nothing earlier in this session was kept to show.'));
|
|
551
|
+
out();
|
|
552
|
+
}
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const exchanges = turns.filter((t) => t.role === 'you').length;
|
|
556
|
+
let shown = turns;
|
|
557
|
+
if (!full) {
|
|
558
|
+
// From the third question from the end, so what is shown is whole exchanges.
|
|
559
|
+
let asked = 0;
|
|
560
|
+
let from = turns.length;
|
|
561
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
562
|
+
if (turns[i].role === 'you' && ++asked === 3) {
|
|
563
|
+
from = i;
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
if (i === 0)
|
|
567
|
+
from = 0;
|
|
568
|
+
}
|
|
569
|
+
shown = turns.slice(from);
|
|
570
|
+
}
|
|
571
|
+
out(ui.c.dim(full ? ` The whole session, ${exchanges} question(s):`
|
|
572
|
+
: ` Earlier in this session${exchanges > 3 ? `, the last 3 of ${exchanges} questions` : ''}:`));
|
|
573
|
+
out();
|
|
574
|
+
for (const t of shown) {
|
|
575
|
+
if (t.role === 'you') {
|
|
576
|
+
out(` ${ui.c.bold(ui.c.blue('you'))} ${ui.c.bold(t.text.trim())}`);
|
|
577
|
+
out();
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
// Drawn exactly as a new answer is, rail and all, never indented further.
|
|
581
|
+
const lines = plain(t.text.trim()).split('\n');
|
|
582
|
+
const keep = full ? lines : lines.slice(0, openingCut(lines));
|
|
583
|
+
for (const l of keep)
|
|
584
|
+
out(l);
|
|
585
|
+
if (keep.length < lines.length)
|
|
586
|
+
out(ui.c.dim(` ... ${lines.length - keep.length} more lines. Type /history to read it all.`));
|
|
587
|
+
out();
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Where to cut an earlier answer on reopening: the last paragraph break in its
|
|
592
|
+
* first dozen lines, so a table or a list is never shown half drawn. An answer
|
|
593
|
+
* with no break that early is cut at eight lines, as before.
|
|
594
|
+
*/
|
|
595
|
+
function openingCut(lines) {
|
|
596
|
+
if (lines.length <= 12)
|
|
597
|
+
return lines.length;
|
|
598
|
+
// A break is the rail on its own; a blank row inside a table has more rules.
|
|
599
|
+
const isBreak = (l) => /^│?$/.test(l.replace(/\x1b\[[0-9;]*m/g, '').trim());
|
|
600
|
+
for (let i = 12; i >= 3; i--)
|
|
601
|
+
if (isBreak(lines[i]))
|
|
602
|
+
return i;
|
|
603
|
+
return 8;
|
|
604
|
+
}
|
|
605
|
+
/** A session's questions and answers in full, from the service's export. */
|
|
606
|
+
async function exportedTurns(key) {
|
|
607
|
+
const { sessions } = await Promise.resolve().then(() => __importStar(require('../lib/sessions')));
|
|
608
|
+
const reply = await sessions('export', { key, format: 'json' });
|
|
609
|
+
const doc = reply.sessions?.document;
|
|
610
|
+
const turns = [];
|
|
611
|
+
for (const job of doc?.work ?? []) {
|
|
612
|
+
if (job.request)
|
|
613
|
+
turns.push({ role: 'you', text: job.request });
|
|
614
|
+
if (job.answer)
|
|
615
|
+
turns.push({ role: 'agent', text: job.answer });
|
|
616
|
+
}
|
|
617
|
+
return turns;
|
|
618
|
+
}
|
|
421
619
|
/** The command and skill list, as the service sent it (manual 6.4). */
|
|
422
620
|
function printHelp() {
|
|
423
621
|
out();
|
|
@@ -457,14 +655,18 @@ async function sessionFolder() {
|
|
|
457
655
|
return currentWorkspace().path;
|
|
458
656
|
}
|
|
459
657
|
/** The line this session begins with, as the service wrote it (manual 2.2). */
|
|
460
|
-
async function
|
|
658
|
+
async function projectLine() {
|
|
461
659
|
const { resolveProject, asWire } = await Promise.resolve().then(() => __importStar(require('../lib/project')));
|
|
462
660
|
const { ask } = await Promise.resolve().then(() => __importStar(require('../lib/gateway')));
|
|
463
661
|
const facts = resolveProject(await sessionFolder(), place.added);
|
|
464
662
|
const reply = await ask({ intent: 'project', prompt: '', project: asWire(facts, 'approve_writes') });
|
|
465
663
|
// Drawn as sent. When the service cannot answer, the folder alone, so the
|
|
466
664
|
// person still knows where they are.
|
|
467
|
-
|
|
665
|
+
return reply.ok && reply.project_line ? reply.project_line : ` ${facts.root}`;
|
|
666
|
+
}
|
|
667
|
+
/** `asked` is the same line already on its way, when startup asked early. */
|
|
668
|
+
async function showProjectLine(asked) {
|
|
669
|
+
out(ui.c.bold(await (asked ?? projectLine())));
|
|
468
670
|
out();
|
|
469
671
|
}
|
|
470
672
|
/** Ctrl+O's choice, kept for the next job too, and notes left over from the
|
|
@@ -758,10 +960,48 @@ async function runRepl(how = {}) {
|
|
|
758
960
|
const finished = new Promise((resolve) => { done = resolve; });
|
|
759
961
|
const conn = (0, connection_1.readConnection)();
|
|
760
962
|
if (!conn?.url || !conn.workspaceKey) {
|
|
761
|
-
await (0, welcome_1.showWelcome)();
|
|
963
|
+
await (0, welcome_1.showWelcome)(undefined, process.stdin.isTTY === true && !how.noSignIn);
|
|
964
|
+
// Signed out: sign in right here rather than naming another command to
|
|
965
|
+
// type. The last person is offered, so it is usually Enter and a password.
|
|
966
|
+
if (process.stdin.isTTY === true && !how.noSignIn) {
|
|
967
|
+
const { cmdLogin } = await Promise.resolve().then(() => __importStar(require('./login')));
|
|
968
|
+
await cmdLogin(['--into-session']);
|
|
969
|
+
const { closePrompt } = await Promise.resolve().then(() => __importStar(require('../lib/prompt')));
|
|
970
|
+
closePrompt();
|
|
971
|
+
if ((0, connection_1.readConnection)()?.workspaceKey)
|
|
972
|
+
return runRepl({ ...how, noSignIn: true, noMark: true });
|
|
973
|
+
}
|
|
762
974
|
return;
|
|
763
975
|
}
|
|
764
|
-
|
|
976
|
+
// Everything the opening screen needs is asked for AT ONCE, then drawn in
|
|
977
|
+
// order as it arrives. Asked one after another it was five round trips end
|
|
978
|
+
// to end, about fifteen seconds on a laptop before the prompt appeared.
|
|
979
|
+
//
|
|
980
|
+
// And what the service said LAST time is drawn at once, so the prompt is
|
|
981
|
+
// ready in well under a second. The same questions are still asked; their
|
|
982
|
+
// answers are kept for next time, and a change that matters is said as soon
|
|
983
|
+
// as it arrives. A first start, or another workspace, waits as before.
|
|
984
|
+
const { readStart, saveStart } = await Promise.resolve().then(() => __importStar(require('../lib/startcache')));
|
|
985
|
+
const { session: whoAmI } = await Promise.resolve().then(() => __importStar(require('./welcome')));
|
|
986
|
+
const known = readStart(conn.url);
|
|
987
|
+
const here = await sessionFolder();
|
|
988
|
+
const meAsked = whoAmI().then((me) => { if (me)
|
|
989
|
+
saveStart(conn.url, { me }); return me; });
|
|
990
|
+
const stateAsked = (0, keys_1.keyState)().then((s) => {
|
|
991
|
+
if (s.reached)
|
|
992
|
+
saveStart(conn.url, { hasKey: s.hasKey });
|
|
993
|
+
return s;
|
|
994
|
+
});
|
|
995
|
+
const lineAsked = projectLine()
|
|
996
|
+
.then((l) => { saveStart(conn.url, { lines: { [here]: l } }); return l; })
|
|
997
|
+
.catch(() => '');
|
|
998
|
+
const listAsked = refreshList()
|
|
999
|
+
.then(() => { if (listed?.ok)
|
|
1000
|
+
saveStart(conn.url, { list: listed }); })
|
|
1001
|
+
.catch(() => undefined);
|
|
1002
|
+
if (known.list?.ok && !listed)
|
|
1003
|
+
listed = known.list;
|
|
1004
|
+
await (0, welcome_1.showWelcome)(known.me ? known.me : await meAsked, false, !how.noMark);
|
|
765
1005
|
// A key, before anything else. With none there is nothing to bill and nothing
|
|
766
1006
|
// to measure, so a prompt accepted here could only ever be refused later.
|
|
767
1007
|
//
|
|
@@ -769,7 +1009,18 @@ async function runRepl(how = {}) {
|
|
|
769
1009
|
// could not ask your account" is the whole point: on 17 September a person
|
|
770
1010
|
// whose workspace held four working keys was shown the first message and sent
|
|
771
1011
|
// to a form, because an unanswered question was read as an answer of none.
|
|
772
|
-
const state =
|
|
1012
|
+
const state = known.hasKey === true ? { reached: true, hasKey: true } : await stateAsked;
|
|
1013
|
+
if (known.hasKey === true) {
|
|
1014
|
+
// Drawn from the last answer; the fresh one says so if the key has gone.
|
|
1015
|
+
void stateAsked.then((fresh) => {
|
|
1016
|
+
if (fresh.reached && !fresh.hasKey) {
|
|
1017
|
+
out();
|
|
1018
|
+
out(ui.c.amber(' Your account no longer has a provider key stored, so requests will be refused.'));
|
|
1019
|
+
out(` ${ui.c.dim('run')} ${ui.c.bold('keys')} ${ui.c.dim('to add one')}`);
|
|
1020
|
+
reader?.prompt(true);
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
773
1024
|
if (!state.reached) {
|
|
774
1025
|
out(ui.fail(`Your workspace could not be reached, so ${ui.c.bold('nothing is known')} about your keys.`));
|
|
775
1026
|
out(` ${ui.c.dim(state.why)}`);
|
|
@@ -821,7 +1072,8 @@ async function runRepl(how = {}) {
|
|
|
821
1072
|
session = found;
|
|
822
1073
|
}
|
|
823
1074
|
else {
|
|
824
|
-
|
|
1075
|
+
// The session's number arrives while the person is already typing.
|
|
1076
|
+
void numbered(session);
|
|
825
1077
|
}
|
|
826
1078
|
history.push(session);
|
|
827
1079
|
// Ctrl-C leaves, and says what the session cost on the way out. A product
|
|
@@ -909,6 +1161,52 @@ async function runRepl(how = {}) {
|
|
|
909
1161
|
* the person saw their sentence twice and could edit only the second copy.
|
|
910
1162
|
*/
|
|
911
1163
|
const speak = async () => {
|
|
1164
|
+
// On a Mac, the words appear as they are spoken, the way the web app shows
|
|
1165
|
+
// them, from the Mac's own recogniser. Anywhere it cannot listen (no
|
|
1166
|
+
// helper, or permission not given) this falls through to recording and
|
|
1167
|
+
// having the server write it down when Enter is pressed.
|
|
1168
|
+
if (process.stdout.isTTY) {
|
|
1169
|
+
const live = voice.listenLive();
|
|
1170
|
+
if (live) {
|
|
1171
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
1172
|
+
if (live.problem() && !live.words()) {
|
|
1173
|
+
await live.stop();
|
|
1174
|
+
out(ui.c.dim(` ${live.problem()}`));
|
|
1175
|
+
}
|
|
1176
|
+
else {
|
|
1177
|
+
out();
|
|
1178
|
+
out(ui.c.green(' listening...') + ui.c.dim(' say it, then press enter'));
|
|
1179
|
+
const draw = () => {
|
|
1180
|
+
const bars = Math.round(live.level() * 12);
|
|
1181
|
+
const meter = ui.c.green('█'.repeat(bars)) + ui.c.dim('░'.repeat(12 - bars));
|
|
1182
|
+
const room = Math.max(20, (process.stdout.columns || 100) - 20);
|
|
1183
|
+
const heard = live.words();
|
|
1184
|
+
const shown = heard.length > room ? `...${heard.slice(-(room - 3))}` : heard;
|
|
1185
|
+
process.stdout.write(`\r${CSI}2K ${meter} ${shown}`);
|
|
1186
|
+
};
|
|
1187
|
+
const ticking = setInterval(draw, 80);
|
|
1188
|
+
await nextLine();
|
|
1189
|
+
clearInterval(ticking);
|
|
1190
|
+
process.stdout.write(`${CSI}1A\r${CSI}2K`);
|
|
1191
|
+
const said = await live.stop();
|
|
1192
|
+
if (said === null) {
|
|
1193
|
+
out(ui.c.dim(` ${live.problem() || 'dictation stopped'}`));
|
|
1194
|
+
out();
|
|
1195
|
+
return prompt();
|
|
1196
|
+
}
|
|
1197
|
+
if (!said) {
|
|
1198
|
+
out(ui.c.dim(' no words in that one'));
|
|
1199
|
+
out();
|
|
1200
|
+
return prompt();
|
|
1201
|
+
}
|
|
1202
|
+
// Into the prompt, not sent: the person reads it, fixes what was
|
|
1203
|
+
// misheard, and presses Enter. Nothing is asked or charged until then.
|
|
1204
|
+
prompt();
|
|
1205
|
+
reader?.write(said);
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
912
1210
|
const short = voice.missing();
|
|
913
1211
|
if (short) {
|
|
914
1212
|
out();
|
|
@@ -927,7 +1225,45 @@ async function runRepl(how = {}) {
|
|
|
927
1225
|
}
|
|
928
1226
|
out();
|
|
929
1227
|
out(ui.c.green(' listening...') + ui.c.dim(' say it, then press enter'));
|
|
1228
|
+
// What it hears, while it hears it: a meter that moves with the voice at
|
|
1229
|
+
// once, and the words so far, refreshed every few seconds. Without these
|
|
1230
|
+
// a person talks into a blank line with no idea whether anything is
|
|
1231
|
+
// arriving. The words so far are a preview; the whole recording is
|
|
1232
|
+
// written down again when they press Enter, and that is what they get.
|
|
1233
|
+
const tty = process.stdout.isTTY === true;
|
|
1234
|
+
let heard = '';
|
|
1235
|
+
let asking = false;
|
|
1236
|
+
let lastAsked = 0;
|
|
1237
|
+
const draw = () => {
|
|
1238
|
+
const bars = Math.round(voice.level(live.file) * 12);
|
|
1239
|
+
const meter = ui.c.green('█'.repeat(bars)) + ui.c.dim('░'.repeat(12 - bars));
|
|
1240
|
+
const room = Math.max(20, (process.stdout.columns || 100) - 20);
|
|
1241
|
+
const words = heard.length > room ? `...${heard.slice(-(room - 3))}` : heard;
|
|
1242
|
+
process.stdout.write(`\r${CSI}2K ${meter} ${words}`);
|
|
1243
|
+
};
|
|
1244
|
+
const preview = async () => {
|
|
1245
|
+
if (asking || Date.now() - lastAsked < 2500)
|
|
1246
|
+
return;
|
|
1247
|
+
const part = voice.soFar(live.file);
|
|
1248
|
+
if (!part)
|
|
1249
|
+
return;
|
|
1250
|
+
asking = true;
|
|
1251
|
+
lastAsked = Date.now();
|
|
1252
|
+
try {
|
|
1253
|
+
const got = await voice.transcribe(conn.url, conn.workspaceKey, part);
|
|
1254
|
+
if (got.text)
|
|
1255
|
+
heard = got.text.trim();
|
|
1256
|
+
}
|
|
1257
|
+
catch { /* a preview that fails leaves the last one showing */ }
|
|
1258
|
+
asking = false;
|
|
1259
|
+
};
|
|
1260
|
+
const ticking = tty ? setInterval(() => { draw(); void preview(); }, 120) : null;
|
|
930
1261
|
await nextLine();
|
|
1262
|
+
if (ticking)
|
|
1263
|
+
clearInterval(ticking);
|
|
1264
|
+
// The line the typed Enter left behind is the meter's; clear it.
|
|
1265
|
+
if (tty)
|
|
1266
|
+
process.stdout.write(`${CSI}1A\r${CSI}2K`);
|
|
931
1267
|
const { file, bytes } = await live.stop();
|
|
932
1268
|
try {
|
|
933
1269
|
if (bytes < 2000) {
|
|
@@ -1168,17 +1504,20 @@ async function runRepl(how = {}) {
|
|
|
1168
1504
|
reader.setPrompt(`${ui.c.bold(ui.c.blue('comprism'))} ${ui.c.dim('>')} `);
|
|
1169
1505
|
reader.prompt();
|
|
1170
1506
|
};
|
|
1171
|
-
|
|
1507
|
+
const lastLine = known.lines?.[here];
|
|
1508
|
+
await showProjectLine(lastLine ? Promise.resolve(lastLine) : lineAsked);
|
|
1172
1509
|
// The command and skill list, and whether this company lets typed requests
|
|
1173
1510
|
// be kept on this machine (manual 6.4, 6.5). Unknown keeps them for this
|
|
1174
|
-
// session only and leaves any file alone.
|
|
1175
|
-
|
|
1511
|
+
// session only and leaves any file alone. The last list serves until the
|
|
1512
|
+
// fresh one lands.
|
|
1513
|
+
if (!known.list?.ok)
|
|
1514
|
+
await listAsked;
|
|
1176
1515
|
// `config set history off` keeps nothing at all, not even for this session.
|
|
1177
1516
|
const historyOff = (0, machine_1.machineSettings)().history === 'off';
|
|
1178
1517
|
earlier.start(historyOff ? false : listed?.ok ? listed.keepHistory : null);
|
|
1179
1518
|
if (historyOff)
|
|
1180
1519
|
earlier.disable();
|
|
1181
|
-
out(ui.c.dim(' Type what you want done,
|
|
1520
|
+
out(ui.c.dim(' Type what you want done. / lists commands and skills, voice dictates, Ctrl+C leaves.'));
|
|
1182
1521
|
out();
|
|
1183
1522
|
// One line at a time, in the order they arrived.
|
|
1184
1523
|
//
|
|
@@ -1290,6 +1629,13 @@ async function runRepl(how = {}) {
|
|
|
1290
1629
|
// Shift+Tab moves to the next permission mode (manual 4.5). Only at the
|
|
1291
1630
|
// prompt: during a job the change would apply from the next job anyway, and
|
|
1292
1631
|
// redrawing over a running job's output is unreadable.
|
|
1632
|
+
// The live list of commands and skills, redrawn after each key at the prompt,
|
|
1633
|
+
// once the reader has applied the key to the line.
|
|
1634
|
+
keys().on('keypress', (_s, key) => {
|
|
1635
|
+
if (working || answering || leaving || key?.name === 'return' || key?.name === 'tab')
|
|
1636
|
+
return;
|
|
1637
|
+
setImmediate(updatePopup);
|
|
1638
|
+
});
|
|
1293
1639
|
keys().on('keypress', (_s, key) => {
|
|
1294
1640
|
if (!key || key.name !== 'tab' || !key.shift || working || answering || leaving)
|
|
1295
1641
|
return;
|
|
@@ -1367,6 +1713,27 @@ async function runRepl(how = {}) {
|
|
|
1367
1713
|
out();
|
|
1368
1714
|
return prompt();
|
|
1369
1715
|
}
|
|
1716
|
+
// `optimize on` and `optimize off <model>` (manual 4.17): the same switch
|
|
1717
|
+
// as beside the model in the browser and VS Code. Off is the session pin.
|
|
1718
|
+
const optimizeTo = /^optimize\s+(on|off)(?:\s+(\S+))?$/i.exec(word);
|
|
1719
|
+
if (optimizeTo) {
|
|
1720
|
+
const on = optimizeTo[1].toLowerCase() === 'on';
|
|
1721
|
+
const hold = optimizeTo[2] ?? steering_.pin;
|
|
1722
|
+
if (on) {
|
|
1723
|
+
if (await steer({ pin: '' }))
|
|
1724
|
+
out(ui.c.dim(' Optimize is on: CompletionPrism picks the model for each step'));
|
|
1725
|
+
}
|
|
1726
|
+
else if (!hold) {
|
|
1727
|
+
out(ui.fail('Name the model to keep, such as `optimize off sonnet`.'));
|
|
1728
|
+
}
|
|
1729
|
+
else {
|
|
1730
|
+
const said = await steer({ pin: hold });
|
|
1731
|
+
if (said)
|
|
1732
|
+
out(ui.c.dim(` Optimize is off: ${ui.c.bold(said.pin_label ?? hold)} answers every request. Work is not optimized. \`optimize on\` hands the choice back.`));
|
|
1733
|
+
}
|
|
1734
|
+
out();
|
|
1735
|
+
return prompt();
|
|
1736
|
+
}
|
|
1370
1737
|
const effortTo = /^effort\s+(\S+)$/.exec(word);
|
|
1371
1738
|
if (effortTo) {
|
|
1372
1739
|
const level = effortTo[1];
|
|
@@ -1544,6 +1911,14 @@ async function runRepl(how = {}) {
|
|
|
1544
1911
|
out();
|
|
1545
1912
|
break;
|
|
1546
1913
|
}
|
|
1914
|
+
case 'optimize': {
|
|
1915
|
+
const said = await steer({});
|
|
1916
|
+
out(said?.pin
|
|
1917
|
+
? ` ${ui.c.dim('Optimize is off:')} ${ui.c.bold(said.pin_label ?? said.pin)} ${ui.c.dim('answers every request. `optimize on` turns it back on.')}`
|
|
1918
|
+
: ui.c.dim(' Optimize is on: CompletionPrism picks the model for each step. `optimize off <model>` keeps one.'));
|
|
1919
|
+
out();
|
|
1920
|
+
break;
|
|
1921
|
+
}
|
|
1547
1922
|
case 'unpin':
|
|
1548
1923
|
if (await steer({ pin: '' }))
|
|
1549
1924
|
out(ui.c.dim(' unpinned: the service chooses the model for each step again'));
|
|
@@ -1649,6 +2024,46 @@ async function runRepl(how = {}) {
|
|
|
1649
2024
|
}
|
|
1650
2025
|
case 'sessions': {
|
|
1651
2026
|
out();
|
|
2027
|
+
// What this account did before this window was opened, from the
|
|
2028
|
+
// service's record of sessions (manual 8.5). Kept numbered
|
|
2029
|
+
// separately so a number never means two different things.
|
|
2030
|
+
// Every session the service keeps a record of for this person, up to
|
|
2031
|
+
// its limit, when they can be moved through and picked by number; a
|
|
2032
|
+
// typed list stays short enough to read.
|
|
2033
|
+
const onTerminal = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
2034
|
+
recent = await earlierSessions(history, onTerminal ? 200 : 10);
|
|
2035
|
+
// On a terminal, the list is picked from with the arrow keys; the
|
|
2036
|
+
// typed forms below still work everywhere, and in a pipe are the
|
|
2037
|
+
// only way.
|
|
2038
|
+
if (onTerminal && history.length + recent.length > 1) {
|
|
2039
|
+
// Names, dates and cost only. The ids are for typing a session
|
|
2040
|
+
// back, and nobody types in a list they move through with arrows.
|
|
2041
|
+
const noId = (line) => line.trimStart().replace(/^(?:srv|s)_[0-9a-f]+\s+/i, '');
|
|
2042
|
+
const rows = [
|
|
2043
|
+
...history.map((s) => [
|
|
2044
|
+
`${s.title ?? 'this session'}${s === session ? ' (now)' : ''}`,
|
|
2045
|
+
`this window \u00b7 ${s.jobs.length} job(s) \u00b7 ${money(s.spentUsd)}`,
|
|
2046
|
+
]),
|
|
2047
|
+
// The service's two lines where it sent them; its one line otherwise.
|
|
2048
|
+
...recent.map((r) => (r.card?.length ? r.card : [noId(r.line)])),
|
|
2049
|
+
];
|
|
2050
|
+
const { pick } = await Promise.resolve().then(() => __importStar(require('../lib/picker')));
|
|
2051
|
+
let choice = null;
|
|
2052
|
+
out(ui.c.dim(' choose a session:'));
|
|
2053
|
+
await withTerminal(async () => {
|
|
2054
|
+
choice = await pick(rows, 'Up and Down to choose, or type its number, Enter to open, Esc to stay here', history.indexOf(session), true);
|
|
2055
|
+
});
|
|
2056
|
+
out();
|
|
2057
|
+
if (choice === null)
|
|
2058
|
+
return prompt();
|
|
2059
|
+
const n = choice;
|
|
2060
|
+
if (n < history.length) {
|
|
2061
|
+
if (history[n] === session)
|
|
2062
|
+
return prompt();
|
|
2063
|
+
return handle(`sessions ${n + 1}`);
|
|
2064
|
+
}
|
|
2065
|
+
return handle(`sessions w${n - history.length + 1}`);
|
|
2066
|
+
}
|
|
1652
2067
|
history.forEach((s, i) => {
|
|
1653
2068
|
const here = s === session ? ui.c.green(' now') : ' ';
|
|
1654
2069
|
// The NAME where our server has made one, and the id beside it
|
|
@@ -1658,10 +2073,6 @@ async function runRepl(how = {}) {
|
|
|
1658
2073
|
out(`${here} ${String(i + 1).padStart(2)}. ${ui.c.bold(s.id)}`
|
|
1659
2074
|
+ ` ${s.jobs.length} job(s) ${money(s.spentUsd)}${named}`);
|
|
1660
2075
|
});
|
|
1661
|
-
// And what this account did before this window was opened, from
|
|
1662
|
-
// the service's record of sessions (manual 8.5). Kept numbered
|
|
1663
|
-
// separately so a number never means two different things.
|
|
1664
|
-
recent = await earlierSessions(history);
|
|
1665
2076
|
if (recent.length > 0) {
|
|
1666
2077
|
out();
|
|
1667
2078
|
out(ui.c.dim(' earlier work on this account:'));
|
|
@@ -1718,6 +2129,33 @@ async function runRepl(how = {}) {
|
|
|
1718
2129
|
// Signing out ends the session, because everything after it would be
|
|
1719
2130
|
// refused anyway. It used to answer "not available inside a session",
|
|
1720
2131
|
// which left quitting and running a second command as the only way.
|
|
2132
|
+
case 'history':
|
|
2133
|
+
// Every earlier question and answer in the session being stood in.
|
|
2134
|
+
out();
|
|
2135
|
+
await showConversation(session.serverId, true);
|
|
2136
|
+
break;
|
|
2137
|
+
case 'login': {
|
|
2138
|
+
// Signing in again, or as somebody else, from inside a session. The
|
|
2139
|
+
// session was opened with the old sign in, so it ends and the next
|
|
2140
|
+
// `comprism` opens with the new one.
|
|
2141
|
+
const { cmdLogin } = await Promise.resolve().then(() => __importStar(require('./login')));
|
|
2142
|
+
const before = (0, connection_1.readConnection)()?.token;
|
|
2143
|
+
await withTerminal(async () => {
|
|
2144
|
+
// The Enter that sent `/login` is still in the terminal's input.
|
|
2145
|
+
// Given a moment it goes to the session, not to the email question.
|
|
2146
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
2147
|
+
// Anything already typed but not yet read is thrown away, so the
|
|
2148
|
+
// first question waits for a real answer.
|
|
2149
|
+
while (process.stdin.read() !== null) { /* discard */ }
|
|
2150
|
+
await cmdLogin([]);
|
|
2151
|
+
});
|
|
2152
|
+
// Canceled or refused: nothing changed, so the session carries on.
|
|
2153
|
+
if ((0, connection_1.readConnection)()?.token === before)
|
|
2154
|
+
break;
|
|
2155
|
+
out(ui.c.dim(` type ${ui.c.bold('comprism')} to start a session with this sign in`));
|
|
2156
|
+
out();
|
|
2157
|
+
return leave();
|
|
2158
|
+
}
|
|
1721
2159
|
case 'logout':
|
|
1722
2160
|
case 'signout':
|
|
1723
2161
|
case 'disconnect': {
|
|
@@ -1 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
import type { SessionOut } from '../lib/gateway';
|
|
2
|
+
/**
|
|
3
|
+
* Who this machine is signed in as, and everything else the header prints.
|
|
4
|
+
*
|
|
5
|
+
* ── One call, to the one service ──────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* This used to call the platform's own `whoami` for the identity, and the
|
|
8
|
+
* header then had no way at all to say how many vendor keys the account held.
|
|
9
|
+
* Two questions, two addresses, and the header could show a person facts that
|
|
10
|
+
* disagreed with each other. On 17 September 2026 it did exactly that: "User
|
|
11
|
+
* unknown" beside a company name, and a session that then announced no provider
|
|
12
|
+
* key was stored while four were stored and active.
|
|
13
|
+
*
|
|
14
|
+
* Every line below now comes from one request to the single service, so the
|
|
15
|
+
* header either shows a consistent picture or says plainly that it could not
|
|
16
|
+
* reach the workspace. There is no third state in which half of it is true.
|
|
17
|
+
*
|
|
18
|
+
* Null rather than an error when it cannot be reached. A header is the wrong
|
|
19
|
+
* place to fail: somebody on a plane still gets the local facts, and the lines
|
|
20
|
+
* needing the server say so.
|
|
21
|
+
*/
|
|
22
|
+
export declare function session(): Promise<SessionOut | null>;
|
|
23
|
+
/** `known` is who this machine is, when the caller already has it (the last
|
|
24
|
+
* answer, drawn at once while a fresh one is asked for). */
|
|
25
|
+
export declare function showWelcome(known?: SessionOut | null, signingIn?: boolean, withMark?: boolean): Promise<void>;
|