@illuminis/comprism 0.1.4 → 0.1.6

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.
Files changed (116) hide show
  1. package/out/agent/command.js +5 -5
  2. package/out/agent/render.d.ts +39 -6
  3. package/out/agent/render.js +287 -28
  4. package/out/agent/session.d.ts +21 -1
  5. package/out/agent/session.js +81 -17
  6. package/out/commands/agents.js +3 -3
  7. package/out/commands/ask.js +20 -11
  8. package/out/commands/codemap.d.ts +1 -1
  9. package/out/commands/codemap.js +3 -3
  10. package/out/commands/commands-thin.js +4 -4
  11. package/out/commands/config.js +1 -1
  12. package/out/commands/cost.js +4 -4
  13. package/out/commands/hooks.js +1 -1
  14. package/out/commands/install.d.ts +1 -1
  15. package/out/commands/install.js +3 -2
  16. package/out/commands/instructions.js +1 -1
  17. package/out/commands/integrations.js +4 -4
  18. package/out/commands/keys.js +6 -6
  19. package/out/commands/login.js +97 -27
  20. package/out/commands/permissions.js +2 -2
  21. package/out/commands/plugins.js +3 -3
  22. package/out/commands/privacy.js +2 -2
  23. package/out/commands/repl.d.ts +4 -0
  24. package/out/commands/repl.js +563 -101
  25. package/out/commands/review.js +6 -6
  26. package/out/commands/settings.js +30 -123
  27. package/out/commands/skills.js +2 -2
  28. package/out/commands/unattended.js +6 -6
  29. package/out/commands/update.js +1 -1
  30. package/out/commands/welcome.d.ts +25 -1
  31. package/out/commands/welcome.js +65 -38
  32. package/out/commands/worktrees.d.ts +1 -1
  33. package/out/graph/sync.d.ts +1 -1
  34. package/out/graph/sync.js +6 -6
  35. package/out/lib/attach.js +4 -3
  36. package/out/lib/commandlist.d.ts +1 -1
  37. package/out/lib/commandlist.js +2 -2
  38. package/out/lib/config.d.ts +3 -2
  39. package/out/lib/config.js +7 -8
  40. package/out/lib/connection.js +1 -1
  41. package/out/lib/gateway.d.ts +9 -5
  42. package/out/lib/keyboard.d.ts +3 -0
  43. package/out/lib/keyboard.js +7 -0
  44. package/out/lib/lastlogin.d.ts +14 -0
  45. package/out/lib/lastlogin.js +105 -0
  46. package/out/lib/machine.js +1 -1
  47. package/out/lib/picker.d.ts +1 -0
  48. package/out/lib/picker.js +158 -0
  49. package/out/lib/project-ops.d.ts +1 -1
  50. package/out/lib/project-ops.js +4 -4
  51. package/out/lib/prompt.d.ts +3 -0
  52. package/out/lib/prompt.js +21 -0
  53. package/out/lib/queue.js +1 -1
  54. package/out/lib/readiness.d.ts +3 -30
  55. package/out/lib/readiness.js +13 -82
  56. package/out/lib/servicecommand.d.ts +14 -0
  57. package/out/lib/servicecommand.js +74 -0
  58. package/out/lib/sessions.js +3 -3
  59. package/out/lib/startcache.d.ts +13 -0
  60. package/out/lib/startcache.js +78 -0
  61. package/out/lib/ui.d.ts +11 -0
  62. package/out/lib/ui.js +28 -0
  63. package/out/lib/voice.d.ts +36 -0
  64. package/out/lib/voice.js +137 -2
  65. package/out/lib/words.d.ts +20 -0
  66. package/out/lib/words.js +27 -0
  67. package/out/machine/codemap/build.d.ts +45 -0
  68. package/out/machine/codemap/build.js +91 -0
  69. package/out/machine/codemap/facts.d.ts +47 -0
  70. package/out/machine/codemap/facts.js +12 -0
  71. package/out/machine/codemap/files.d.ts +45 -0
  72. package/out/machine/codemap/files.js +207 -0
  73. package/out/machine/codemap/read-locales.d.ts +29 -0
  74. package/out/machine/codemap/read-locales.js +246 -0
  75. package/out/machine/codemap/read-python.d.ts +11 -0
  76. package/out/machine/codemap/read-python.js +116 -0
  77. package/out/machine/codemap/read-typescript.d.ts +16 -0
  78. package/out/machine/codemap/read-typescript.js +292 -0
  79. package/out/machine/executor/browser.d.ts +14 -0
  80. package/out/machine/executor/browser.js +270 -0
  81. package/out/machine/executor/diagnostics.d.ts +2 -0
  82. package/out/machine/executor/diagnostics.js +181 -0
  83. package/out/machine/executor/documents.d.ts +40 -0
  84. package/out/machine/executor/documents.js +170 -0
  85. package/out/machine/executor/files.d.ts +2 -0
  86. package/out/machine/executor/files.js +590 -0
  87. package/out/machine/executor/git.d.ts +48 -0
  88. package/out/machine/executor/git.js +145 -0
  89. package/out/machine/executor/hooks.d.ts +51 -0
  90. package/out/machine/executor/hooks.js +154 -0
  91. package/out/machine/executor/index.d.ts +45 -0
  92. package/out/machine/executor/index.js +367 -0
  93. package/out/machine/executor/notebook.d.ts +2 -0
  94. package/out/machine/executor/notebook.js +147 -0
  95. package/out/machine/executor/paths.d.ts +20 -0
  96. package/out/machine/executor/paths.js +154 -0
  97. package/out/machine/executor/sandbox.d.ts +40 -0
  98. package/out/machine/executor/sandbox.js +299 -0
  99. package/out/machine/executor/shell.d.ts +86 -0
  100. package/out/machine/executor/shell.js +582 -0
  101. package/out/machine/executor/toolservers.d.ts +20 -0
  102. package/out/machine/executor/toolservers.js +189 -0
  103. package/out/machine/executor/worktree.d.ts +9 -0
  104. package/out/machine/executor/worktree.js +119 -0
  105. package/out/machine/folder/project.d.ts +28 -0
  106. package/out/machine/folder/project.js +114 -0
  107. package/out/machine/runtime/home.d.ts +2 -0
  108. package/out/machine/runtime/home.js +48 -0
  109. package/out/machine/runtime/needs.d.ts +30 -0
  110. package/out/machine/runtime/needs.js +89 -0
  111. package/out/machine/runtime/self.d.ts +23 -0
  112. package/out/machine/runtime/self.js +124 -0
  113. package/out/native/dictate +0 -0
  114. package/out/providers/index.js +2 -1
  115. package/out/thin.js +13 -12
  116. package/package.json +5 -4
@@ -80,7 +80,7 @@ const keyboard_1 = require("../lib/keyboard");
80
80
  const machine_1 = require("../lib/machine");
81
81
  const ui = __importStar(require("../lib/ui"));
82
82
  const render_1 = require("../agent/render");
83
- const shell_1 = require("../executor/shell");
83
+ const shell_1 = require("../machine/executor/shell");
84
84
  const voice = __importStar(require("../lib/voice"));
85
85
  const out = (s = '') => process.stdout.write(s + '\n');
86
86
  /**
@@ -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
@@ -180,7 +285,7 @@ function sessionWords() {
180
285
  return [...COMMANDS, 'project', 'folders', 'instructions', 'remember', 'detach', 'output', 'processes', 'stop', 'agents'];
181
286
  }
182
287
  async function refreshList() {
183
- const { resolveProject } = await Promise.resolve().then(() => __importStar(require('../lib/project')));
288
+ const { resolveProject } = await Promise.resolve().then(() => __importStar(require('../machine/folder/project')));
184
289
  const root = resolveProject(await sessionFolder(), place.added).root;
185
290
  const got = await (0, commandlist_1.fetchCommandList)(root, sessionWords());
186
291
  // A failed refresh keeps the list already held rather than emptying it.
@@ -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. */
@@ -250,11 +356,12 @@ async function showHelpers() {
250
356
  out();
251
357
  }
252
358
  /** The permission modes, in the order Shift+Tab moves through them (4.5). */
359
+ /** The modes Shift+Tab steps through, as the service last listed them. */
253
360
  const MODES = ['read_only', 'approve_writes', 'auto_edit', 'full_auto'];
254
361
  /** How the next job in this session runs: its mode and whether it plans first
255
362
  * (manual 4.1, 4.5). Changed by `mode`, Shift+Tab, `plan` and `plan off`, and
256
363
  * applied from the next job; the service still decides what each mode allows. */
257
- const steering_ = { mode: 'approve_writes', plan: false, pin: '', effort: '' };
364
+ const steering_ = { mode: 'approve_writes', plan: false, pin: '', effort: '', modes: MODES };
258
365
  /**
259
366
  * The line as a command, with a leading slash taken off.
260
367
  *
@@ -308,9 +415,9 @@ async function withTerminal(run) {
308
415
  * request. Returns nothing when the workspace cannot be reached: a list is not
309
416
  * worth stopping a session over.
310
417
  */
311
- async function earlierSessions(run) {
418
+ async function earlierSessions(run, limit = 10) {
312
419
  const { sessions } = await Promise.resolve().then(() => __importStar(require('../lib/sessions')));
313
- const reply = await sessions('list', { limit: 10 });
420
+ const reply = await sessions('list', { limit });
314
421
  if (!reply.ok || !reply.sessions?.sessions)
315
422
  return [];
316
423
  const rows = reply.sessions.sessions;
@@ -324,7 +431,7 @@ async function earlierSessions(run) {
324
431
  here.id = r.number;
325
432
  return;
326
433
  }
327
- found.push({ number: r.number, line: lines[i] ?? r.number });
434
+ found.push({ number: r.number, line: lines[i] ?? r.number, card: r.card });
328
435
  });
329
436
  return found;
330
437
  }
@@ -416,8 +523,100 @@ async function opening(how) {
416
523
  for (const line of reply.sessions.lines)
417
524
  out(line);
418
525
  out();
526
+ // What the session was about, before the next question goes in.
527
+ await showConversation(reply.sessions.session_id, false);
419
528
  return reopened(reply.sessions);
420
529
  }
530
+ /**
531
+ * A session's earlier questions and answers, read back (manual 8.4).
532
+ *
533
+ * On reopening, the last three exchanges, each answer cut to its first lines,
534
+ * so the screen says what the session was about without filling it. `history`
535
+ * shows every turn in full. Answers are drawn the way a new answer is: tables
536
+ * as tables, emphasis as emphasis.
537
+ */
538
+ async function showConversation(key, full) {
539
+ const { sessions } = await Promise.resolve().then(() => __importStar(require('../lib/sessions')));
540
+ const { plain } = await Promise.resolve().then(() => __importStar(require('../agent/render')));
541
+ const reply = await sessions('read', { key });
542
+ let turns = (reply.sessions?.turns) ?? [];
543
+ // A service released before it sent the turns still hands over the whole
544
+ // session on export, every request and answer in full. Read from there, so
545
+ // the answers are drawn with their headings and tables rather than as the
546
+ // one line summaries an older service lists.
547
+ if (reply.ok && turns.length === 0)
548
+ turns = await exportedTurns(key);
549
+ if (!reply.ok || turns.length === 0) {
550
+ if (full) {
551
+ out(ui.c.dim(' Nothing earlier in this session was kept to show.'));
552
+ out();
553
+ }
554
+ return;
555
+ }
556
+ const exchanges = turns.filter((t) => t.role === 'you').length;
557
+ let shown = turns;
558
+ if (!full) {
559
+ // From the third question from the end, so what is shown is whole exchanges.
560
+ let asked = 0;
561
+ let from = turns.length;
562
+ for (let i = turns.length - 1; i >= 0; i--) {
563
+ if (turns[i].role === 'you' && ++asked === 3) {
564
+ from = i;
565
+ break;
566
+ }
567
+ if (i === 0)
568
+ from = 0;
569
+ }
570
+ shown = turns.slice(from);
571
+ }
572
+ out(ui.c.dim(full ? ` The whole session, ${exchanges} question(s):`
573
+ : ` Earlier in this session${exchanges > 3 ? `, the last 3 of ${exchanges} questions` : ''}:`));
574
+ out();
575
+ for (const t of shown) {
576
+ if (t.role === 'you') {
577
+ out(` ${ui.c.bold(ui.c.blue('you'))} ${ui.c.bold(t.text.trim())}`);
578
+ out();
579
+ continue;
580
+ }
581
+ // Drawn exactly as a new answer is, rail and all, never indented further.
582
+ const lines = plain(t.text.trim()).split('\n');
583
+ const keep = full ? lines : lines.slice(0, openingCut(lines));
584
+ for (const l of keep)
585
+ out(l);
586
+ if (keep.length < lines.length)
587
+ out(ui.c.dim(` ... ${lines.length - keep.length} more lines. Type /history to read it all.`));
588
+ out();
589
+ }
590
+ }
591
+ /**
592
+ * Where to cut an earlier answer on reopening: the last paragraph break in its
593
+ * first dozen lines, so a table or a list is never shown half drawn. An answer
594
+ * with no break that early is cut at eight lines, as before.
595
+ */
596
+ function openingCut(lines) {
597
+ if (lines.length <= 12)
598
+ return lines.length;
599
+ // A break is the rail on its own; a blank row inside a table has more rules.
600
+ const isBreak = (l) => /^│?$/.test(l.replace(/\x1b\[[0-9;]*m/g, '').trim());
601
+ for (let i = 12; i >= 3; i--)
602
+ if (isBreak(lines[i]))
603
+ return i;
604
+ return 8;
605
+ }
606
+ /** A session's questions and answers in full, from the service's export. */
607
+ async function exportedTurns(key) {
608
+ const { sessions } = await Promise.resolve().then(() => __importStar(require('../lib/sessions')));
609
+ const reply = await sessions('export', { key, format: 'json' });
610
+ const doc = reply.sessions?.document;
611
+ const turns = [];
612
+ for (const job of doc?.work ?? []) {
613
+ if (job.request)
614
+ turns.push({ role: 'you', text: job.request });
615
+ if (job.answer)
616
+ turns.push({ role: 'agent', text: job.answer });
617
+ }
618
+ return turns;
619
+ }
421
620
  /** The command and skill list, as the service sent it (manual 6.4). */
422
621
  function printHelp() {
423
622
  out();
@@ -425,7 +624,7 @@ function printHelp() {
425
624
  out();
426
625
  drawList(listedItems());
427
626
  if (!listed?.ok) {
428
- out(ui.c.amber(' Your workspace could not be asked for its skills, so only this tool\'s own words are shown.'));
627
+ out(ui.c.amber(' Your organization could not be asked for its skills, so only this tool\'s own words are shown.'));
429
628
  out();
430
629
  }
431
630
  else if (listed.needsUpdate.length > 0) {
@@ -447,9 +646,42 @@ function printHelp() {
447
646
  /**
448
647
  * The folders this session works in (manual 2.1 and 2.3). `project <path>`
449
648
  * changes the folder for the rest of the session; `folders add <path>` allows
450
- * another. Neither changes the saved default, which is `comprism project`.
649
+ * another. Neither changes the saved default, which is `comprism workspace`.
650
+ */
651
+ const place = { folder: null, added: [], workspace: '' };
652
+ /**
653
+ * One command the service runs itself (web manual 13.2, 13.3). The service says
654
+ * what it means and writes every line; this answers whether a folder exists on
655
+ * this computer when asked, prints the lines, and carries out the one action it
656
+ * names. True when the session's workspace or folders changed; the request to
657
+ * run when a next step was chosen.
451
658
  */
452
- const place = { folder: null, added: [] };
659
+ async function serviceCommand(words) {
660
+ const { runServiceCommand } = await Promise.resolve().then(() => __importStar(require('../lib/servicecommand')));
661
+ const said = await runServiceCommand(words, { cwd: await sessionFolder(), current: place.workspace });
662
+ if (!said.reached) {
663
+ out(ui.fail(said.message || 'Your organization could not answer that.'));
664
+ return false;
665
+ }
666
+ for (const line of said.lines)
667
+ out(line);
668
+ const a = said.action;
669
+ if (!a)
670
+ return false;
671
+ if (a.run)
672
+ return a.run;
673
+ if (a.open_workspace)
674
+ place.workspace = a.open_workspace;
675
+ if (a.use_folder) {
676
+ place.folder = a.use_folder;
677
+ (await Promise.resolve().then(() => __importStar(require('../lib/project-ops')))).useSessionFolder(a.use_folder);
678
+ }
679
+ if (a.add_folder && !place.added.includes(a.add_folder))
680
+ place.added.push(a.add_folder);
681
+ if (a.remove_folder)
682
+ place.added = place.added.filter((f) => f !== a.remove_folder);
683
+ return true;
684
+ }
453
685
  async function sessionFolder() {
454
686
  if (place.folder)
455
687
  return place.folder;
@@ -457,14 +689,18 @@ async function sessionFolder() {
457
689
  return currentWorkspace().path;
458
690
  }
459
691
  /** The line this session begins with, as the service wrote it (manual 2.2). */
460
- async function showProjectLine() {
461
- const { resolveProject, asWire } = await Promise.resolve().then(() => __importStar(require('../lib/project')));
692
+ async function projectLine() {
693
+ const { resolveProject, asWire } = await Promise.resolve().then(() => __importStar(require('../machine/folder/project')));
462
694
  const { ask } = await Promise.resolve().then(() => __importStar(require('../lib/gateway')));
463
695
  const facts = resolveProject(await sessionFolder(), place.added);
464
696
  const reply = await ask({ intent: 'project', prompt: '', project: asWire(facts, 'approve_writes') });
465
697
  // Drawn as sent. When the service cannot answer, the folder alone, so the
466
698
  // person still knows where they are.
467
- out(ui.c.bold(reply.ok && reply.project_line ? reply.project_line : ` ${facts.root}`));
699
+ return reply.ok && reply.project_line ? reply.project_line : ` ${facts.root}`;
700
+ }
701
+ /** `asked` is the same line already on its way, when startup asked early. */
702
+ async function showProjectLine(asked) {
703
+ out(ui.c.bold(await (asked ?? projectLine())));
468
704
  out();
469
705
  }
470
706
  /** Ctrl+O's choice, kept for the next job too, and notes left over from the
@@ -483,7 +719,7 @@ async function contextOf(intent, jobId, keep = '', sessionKey) {
483
719
  intent, prompt: keep, ...(jobId ? { jobId } : {}), ...(sessionKey ? { spendSession: sessionKey } : {}),
484
720
  });
485
721
  if (!reply.ok || !reply.context) {
486
- out(ui.fail(reply.message || 'Your workspace could not answer that.'));
722
+ out(ui.fail(reply.message || 'Your organization could not answer that.'));
487
723
  }
488
724
  else {
489
725
  for (const line of reply.context.lines)
@@ -502,7 +738,7 @@ async function showTranscript(jobId) {
502
738
  const { ask } = await Promise.resolve().then(() => __importStar(require('../lib/gateway')));
503
739
  const reply = await ask({ intent: 'transcript', prompt: '', jobId });
504
740
  if (!reply.ok || !reply.transcript) {
505
- out(ui.fail(reply.message || 'The transcript could not be read from your workspace.'));
741
+ out(ui.fail(reply.message || 'The transcript could not be read from your organization.'));
506
742
  }
507
743
  else {
508
744
  for (const line of reply.transcript.lines)
@@ -758,10 +994,48 @@ async function runRepl(how = {}) {
758
994
  const finished = new Promise((resolve) => { done = resolve; });
759
995
  const conn = (0, connection_1.readConnection)();
760
996
  if (!conn?.url || !conn.workspaceKey) {
761
- await (0, welcome_1.showWelcome)();
997
+ await (0, welcome_1.showWelcome)(undefined, process.stdin.isTTY === true && !how.noSignIn);
998
+ // Signed out: sign in right here rather than naming another command to
999
+ // type. The last person is offered, so it is usually Enter and a password.
1000
+ if (process.stdin.isTTY === true && !how.noSignIn) {
1001
+ const { cmdLogin } = await Promise.resolve().then(() => __importStar(require('./login')));
1002
+ await cmdLogin(['--into-session']);
1003
+ const { closePrompt } = await Promise.resolve().then(() => __importStar(require('../lib/prompt')));
1004
+ closePrompt();
1005
+ if ((0, connection_1.readConnection)()?.workspaceKey)
1006
+ return runRepl({ ...how, noSignIn: true, noMark: true });
1007
+ }
762
1008
  return;
763
1009
  }
764
- await (0, welcome_1.showWelcome)();
1010
+ // Everything the opening screen needs is asked for AT ONCE, then drawn in
1011
+ // order as it arrives. Asked one after another it was five round trips end
1012
+ // to end, about fifteen seconds on a laptop before the prompt appeared.
1013
+ //
1014
+ // And what the service said LAST time is drawn at once, so the prompt is
1015
+ // ready in well under a second. The same questions are still asked; their
1016
+ // answers are kept for next time, and a change that matters is said as soon
1017
+ // as it arrives. A first start, or another workspace, waits as before.
1018
+ const { readStart, saveStart } = await Promise.resolve().then(() => __importStar(require('../lib/startcache')));
1019
+ const { session: whoAmI } = await Promise.resolve().then(() => __importStar(require('./welcome')));
1020
+ const known = readStart(conn.url);
1021
+ const here = await sessionFolder();
1022
+ const meAsked = whoAmI().then((me) => { if (me)
1023
+ saveStart(conn.url, { me }); return me; });
1024
+ const stateAsked = (0, keys_1.keyState)().then((s) => {
1025
+ if (s.reached)
1026
+ saveStart(conn.url, { hasKey: s.hasKey });
1027
+ return s;
1028
+ });
1029
+ const lineAsked = projectLine()
1030
+ .then((l) => { saveStart(conn.url, { lines: { [here]: l } }); return l; })
1031
+ .catch(() => '');
1032
+ const listAsked = refreshList()
1033
+ .then(() => { if (listed?.ok)
1034
+ saveStart(conn.url, { list: listed }); })
1035
+ .catch(() => undefined);
1036
+ if (known.list?.ok && !listed)
1037
+ listed = known.list;
1038
+ await (0, welcome_1.showWelcome)(known.me ? known.me : await meAsked, false, !how.noMark);
765
1039
  // A key, before anything else. With none there is nothing to bill and nothing
766
1040
  // to measure, so a prompt accepted here could only ever be refused later.
767
1041
  //
@@ -769,9 +1043,20 @@ async function runRepl(how = {}) {
769
1043
  // could not ask your account" is the whole point: on 17 September a person
770
1044
  // whose workspace held four working keys was shown the first message and sent
771
1045
  // to a form, because an unanswered question was read as an answer of none.
772
- const state = await (0, keys_1.keyState)();
1046
+ const state = known.hasKey === true ? { reached: true, hasKey: true } : await stateAsked;
1047
+ if (known.hasKey === true) {
1048
+ // Drawn from the last answer; the fresh one says so if the key has gone.
1049
+ void stateAsked.then((fresh) => {
1050
+ if (fresh.reached && !fresh.hasKey) {
1051
+ out();
1052
+ out(ui.c.amber(' Your account no longer has a provider key stored, so requests will be refused.'));
1053
+ out(` ${ui.c.dim('run')} ${ui.c.bold('keys')} ${ui.c.dim('to add one')}`);
1054
+ reader?.prompt(true);
1055
+ }
1056
+ });
1057
+ }
773
1058
  if (!state.reached) {
774
- out(ui.fail(`Your workspace could not be reached, so ${ui.c.bold('nothing is known')} about your keys.`));
1059
+ out(ui.fail(`Your organization could not be reached, so ${ui.c.bold('nothing is known')} about your keys.`));
775
1060
  out(` ${ui.c.dim(state.why)}`);
776
1061
  out(` ${ui.c.dim('check the address with')} ${ui.c.bold('comprism status')}${ui.c.dim(', or sign in again with')} ${ui.c.bold('comprism login')}`);
777
1062
  out();
@@ -808,6 +1093,14 @@ async function runRepl(how = {}) {
808
1093
  }
809
1094
  steering_.pin = said.pin ?? '';
810
1095
  steering_.effort = said.effort === 'normal' ? '' : said.effort;
1096
+ // Mode and plan are the service's too (CLI manual 4.1, 4.5); kept here only
1097
+ // to send with the next job and to draw the prompt.
1098
+ if (said.mode)
1099
+ steering_.mode = said.mode;
1100
+ if (typeof said.plan === 'boolean')
1101
+ steering_.plan = said.plan;
1102
+ if (said.modes?.length)
1103
+ steering_.modes = said.modes;
811
1104
  return said;
812
1105
  };
813
1106
  if (how.latest || how.resume || how.pick) {
@@ -821,7 +1114,8 @@ async function runRepl(how = {}) {
821
1114
  session = found;
822
1115
  }
823
1116
  else {
824
- await numbered(session);
1117
+ // The session's number arrives while the person is already typing.
1118
+ void numbered(session);
825
1119
  }
826
1120
  history.push(session);
827
1121
  // Ctrl-C leaves, and says what the session cost on the way out. A product
@@ -909,6 +1203,52 @@ async function runRepl(how = {}) {
909
1203
  * the person saw their sentence twice and could edit only the second copy.
910
1204
  */
911
1205
  const speak = async () => {
1206
+ // On a Mac, the words appear as they are spoken, the way the web app shows
1207
+ // them, from the Mac's own recogniser. Anywhere it cannot listen (no
1208
+ // helper, or permission not given) this falls through to recording and
1209
+ // having the server write it down when Enter is pressed.
1210
+ if (process.stdout.isTTY) {
1211
+ const live = voice.listenLive();
1212
+ if (live) {
1213
+ await new Promise((r) => setTimeout(r, 400));
1214
+ if (live.problem() && !live.words()) {
1215
+ await live.stop();
1216
+ out(ui.c.dim(` ${live.problem()}`));
1217
+ }
1218
+ else {
1219
+ out();
1220
+ out(ui.c.green(' listening...') + ui.c.dim(' say it, then press enter'));
1221
+ const draw = () => {
1222
+ const bars = Math.round(live.level() * 12);
1223
+ const meter = ui.c.green('█'.repeat(bars)) + ui.c.dim('░'.repeat(12 - bars));
1224
+ const room = Math.max(20, (process.stdout.columns || 100) - 20);
1225
+ const heard = live.words();
1226
+ const shown = heard.length > room ? `...${heard.slice(-(room - 3))}` : heard;
1227
+ process.stdout.write(`\r${CSI}2K ${meter} ${shown}`);
1228
+ };
1229
+ const ticking = setInterval(draw, 80);
1230
+ await nextLine();
1231
+ clearInterval(ticking);
1232
+ process.stdout.write(`${CSI}1A\r${CSI}2K`);
1233
+ const said = await live.stop();
1234
+ if (said === null) {
1235
+ out(ui.c.dim(` ${live.problem() || 'dictation stopped'}`));
1236
+ out();
1237
+ return prompt();
1238
+ }
1239
+ if (!said) {
1240
+ out(ui.c.dim(' no words in that one'));
1241
+ out();
1242
+ return prompt();
1243
+ }
1244
+ // Into the prompt, not sent: the person reads it, fixes what was
1245
+ // misheard, and presses Enter. Nothing is asked or charged until then.
1246
+ prompt();
1247
+ reader?.write(said);
1248
+ return;
1249
+ }
1250
+ }
1251
+ }
912
1252
  const short = voice.missing();
913
1253
  if (short) {
914
1254
  out();
@@ -927,7 +1267,45 @@ async function runRepl(how = {}) {
927
1267
  }
928
1268
  out();
929
1269
  out(ui.c.green(' listening...') + ui.c.dim(' say it, then press enter'));
1270
+ // What it hears, while it hears it: a meter that moves with the voice at
1271
+ // once, and the words so far, refreshed every few seconds. Without these
1272
+ // a person talks into a blank line with no idea whether anything is
1273
+ // arriving. The words so far are a preview; the whole recording is
1274
+ // written down again when they press Enter, and that is what they get.
1275
+ const tty = process.stdout.isTTY === true;
1276
+ let heard = '';
1277
+ let asking = false;
1278
+ let lastAsked = 0;
1279
+ const draw = () => {
1280
+ const bars = Math.round(voice.level(live.file) * 12);
1281
+ const meter = ui.c.green('█'.repeat(bars)) + ui.c.dim('░'.repeat(12 - bars));
1282
+ const room = Math.max(20, (process.stdout.columns || 100) - 20);
1283
+ const words = heard.length > room ? `...${heard.slice(-(room - 3))}` : heard;
1284
+ process.stdout.write(`\r${CSI}2K ${meter} ${words}`);
1285
+ };
1286
+ const preview = async () => {
1287
+ if (asking || Date.now() - lastAsked < 2500)
1288
+ return;
1289
+ const part = voice.soFar(live.file);
1290
+ if (!part)
1291
+ return;
1292
+ asking = true;
1293
+ lastAsked = Date.now();
1294
+ try {
1295
+ const got = await voice.transcribe(conn.url, conn.workspaceKey, part);
1296
+ if (got.text)
1297
+ heard = got.text.trim();
1298
+ }
1299
+ catch { /* a preview that fails leaves the last one showing */ }
1300
+ asking = false;
1301
+ };
1302
+ const ticking = tty ? setInterval(() => { draw(); void preview(); }, 120) : null;
930
1303
  await nextLine();
1304
+ if (ticking)
1305
+ clearInterval(ticking);
1306
+ // The line the typed Enter left behind is the meter's; clear it.
1307
+ if (tty)
1308
+ process.stdout.write(`${CSI}1A\r${CSI}2K`);
931
1309
  const { file, bytes } = await live.stop();
932
1310
  try {
933
1311
  if (bytes < 2000) {
@@ -1010,7 +1388,7 @@ async function runRepl(how = {}) {
1010
1388
  * a drag and drop, and the files wait for the question that follows.
1011
1389
  */
1012
1390
  const gather = async (typed) => {
1013
- const { resolveProject } = await Promise.resolve().then(() => __importStar(require('../lib/project')));
1391
+ const { resolveProject } = await Promise.resolve().then(() => __importStar(require('../machine/folder/project')));
1014
1392
  const here = await sessionFolder();
1015
1393
  const root = resolveProject(here, place.added).root;
1016
1394
  completion.root = root;
@@ -1168,17 +1546,20 @@ async function runRepl(how = {}) {
1168
1546
  reader.setPrompt(`${ui.c.bold(ui.c.blue('comprism'))} ${ui.c.dim('>')} `);
1169
1547
  reader.prompt();
1170
1548
  };
1171
- await showProjectLine();
1549
+ const lastLine = known.lines?.[here];
1550
+ await showProjectLine(lastLine ? Promise.resolve(lastLine) : lineAsked);
1172
1551
  // The command and skill list, and whether this company lets typed requests
1173
1552
  // be kept on this machine (manual 6.4, 6.5). Unknown keeps them for this
1174
- // session only and leaves any file alone.
1175
- await refreshList();
1553
+ // session only and leaves any file alone. The last list serves until the
1554
+ // fresh one lands.
1555
+ if (!known.list?.ok)
1556
+ await listAsked;
1176
1557
  // `config set history off` keeps nothing at all, not even for this session.
1177
1558
  const historyOff = (0, machine_1.machineSettings)().history === 'off';
1178
1559
  earlier.start(historyOff ? false : listed?.ok ? listed.keepHistory : null);
1179
1560
  if (historyOff)
1180
1561
  earlier.disable();
1181
- out(ui.c.dim(' Type what you want done, or help. Ctrl+C to leave.'));
1562
+ out(ui.c.dim(' Type what you want done. / lists commands and skills, voice dictates, Ctrl+C leaves.'));
1182
1563
  out();
1183
1564
  // One line at a time, in the order they arrived.
1184
1565
  //
@@ -1290,14 +1671,25 @@ async function runRepl(how = {}) {
1290
1671
  // Shift+Tab moves to the next permission mode (manual 4.5). Only at the
1291
1672
  // prompt: during a job the change would apply from the next job anyway, and
1292
1673
  // redrawing over a running job's output is unreadable.
1674
+ // The live list of commands and skills, redrawn after each key at the prompt,
1675
+ // once the reader has applied the key to the line.
1676
+ keys().on('keypress', (_s, key) => {
1677
+ if (working || answering || leaving || key?.name === 'return' || key?.name === 'tab')
1678
+ return;
1679
+ setImmediate(updatePopup);
1680
+ });
1293
1681
  keys().on('keypress', (_s, key) => {
1294
1682
  if (!key || key.name !== 'tab' || !key.shift || working || answering || leaving)
1295
1683
  return;
1296
- const next = MODES[(MODES.indexOf(steering_.mode) + 1) % MODES.length];
1297
- steering_.mode = next;
1684
+ // The next mode in the service's own list, set by the service.
1685
+ const modes = steering_.modes;
1686
+ const next = modes[(modes.indexOf(steering_.mode) + 1) % modes.length];
1298
1687
  out();
1299
- out(ui.c.dim(` mode ${ui.c.bold(next)}, from the next job`));
1300
- prompt();
1688
+ void steer({ mode: next }).then((said) => {
1689
+ for (const line of said?.lines ?? [])
1690
+ out(ui.c.dim(line));
1691
+ prompt();
1692
+ });
1301
1693
  });
1302
1694
  async function handle(line) {
1303
1695
  const typed = line.trim();
@@ -1323,38 +1715,27 @@ async function runRepl(how = {}) {
1323
1715
  out();
1324
1716
  return prompt();
1325
1717
  }
1326
- const where = /^(project|workspace)(?:\s+(.+))?$/i.exec(bare);
1327
- const addFolder = /^folders\s+add\s+(.+)$/i.exec(bare);
1328
- if (where || addFolder || /^folders$/i.test(bare)) {
1329
- const { checkFolder, resolveProject } = await Promise.resolve().then(() => __importStar(require('../lib/project')));
1330
- const named = where ? where[2] : addFolder ? addFolder[1] : undefined;
1331
- if (named) {
1332
- const found = checkFolder(named.trim());
1333
- if (!found.ok) {
1334
- out(ui.fail(found.why));
1335
- out();
1336
- return prompt();
1337
- }
1338
- if (where) {
1339
- place.folder = found.path;
1340
- (await Promise.resolve().then(() => __importStar(require('../lib/project-ops')))).useSessionFolder(found.path);
1341
- }
1342
- else if (!place.added.includes(found.path))
1343
- place.added.push(found.path);
1344
- }
1345
- if (/^folders$/i.test(bare)) {
1346
- const facts = resolveProject(await sessionFolder(), place.added);
1347
- out();
1348
- out(` ${ui.c.bold(facts.root)} ${ui.c.dim('project')}`);
1349
- for (const f of facts.added)
1350
- out(` ${ui.c.bold(f)} ${ui.c.dim('added for this session')}`);
1351
- out();
1352
- return prompt();
1353
- }
1718
+ // Commands the service runs itself (web manual 13.2, 13.3): any word the
1719
+ // service lists as its own, and `project`, the old name for `workspace`.
1720
+ // What they mean and every line printed are the service's; this sends what
1721
+ // was typed, answers whether a folder exists, and carries out the action.
1722
+ const head = (bare.split(/\s+/)[0] ?? '').toLowerCase();
1723
+ const { waitingNextSteps } = await Promise.resolve().then(() => __importStar(require('../agent/session')));
1724
+ const serviceWord = head === 'project'
1725
+ || (/^\d+$/.test(bare) && waitingNextSteps > 0)
1726
+ || (listed?.items ?? []).some((i) => i.source === 'service' && i.name === head);
1727
+ if (serviceWord) {
1728
+ const words = head === 'project' ? bare.replace(/^project\b/i, 'workspace') : bare;
1729
+ const acted = await serviceCommand(words);
1354
1730
  out();
1355
- await showProjectLine();
1356
- // Another folder has other skills.
1357
- await refreshList();
1731
+ // A next step chosen: it runs as the request it stands for.
1732
+ if (typeof acted === 'string')
1733
+ return handle(`work ${acted}`);
1734
+ if (acted) {
1735
+ await showProjectLine();
1736
+ // Another folder has other skills.
1737
+ await refreshList();
1738
+ }
1358
1739
  return prompt();
1359
1740
  }
1360
1741
  // `pin <model>` and `effort <level>` (manual 4.17, 4.18), kept by the
@@ -1367,18 +1748,35 @@ async function runRepl(how = {}) {
1367
1748
  out();
1368
1749
  return prompt();
1369
1750
  }
1751
+ // `optimize on` and `optimize off <model>` (manual 4.17): the same switch
1752
+ // as beside the model in the browser and VS Code. Off is the session pin.
1753
+ const optimizeTo = /^optimize\s+(on|off)(?:\s+(\S+))?$/i.exec(word);
1754
+ if (optimizeTo) {
1755
+ const on = optimizeTo[1].toLowerCase() === 'on';
1756
+ const hold = optimizeTo[2] ?? steering_.pin;
1757
+ if (on) {
1758
+ if (await steer({ pin: '' }))
1759
+ out(ui.c.dim(' Optimize is on: CompletionPrism picks the model for each step'));
1760
+ }
1761
+ else if (!hold) {
1762
+ out(ui.fail('Name the model to keep, such as `optimize off sonnet`.'));
1763
+ }
1764
+ else {
1765
+ const said = await steer({ pin: hold });
1766
+ if (said)
1767
+ 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.`));
1768
+ }
1769
+ out();
1770
+ return prompt();
1771
+ }
1370
1772
  const effortTo = /^effort\s+(\S+)$/.exec(word);
1371
1773
  if (effortTo) {
1372
- const level = effortTo[1];
1373
- if (!['low', 'medium', 'normal', 'high'].includes(level))
1374
- out(ui.fail('Effort is low, normal or high.'));
1375
- else {
1376
- const said = await steer({ effort: level === 'medium' ? 'normal' : level });
1377
- if (said) {
1378
- out(ui.c.dim(` effort ${ui.c.bold(said.effort)} for the next requests`));
1379
- if (said.effort_note)
1380
- out(ui.c.dim(` ${said.effort_note}`));
1381
- }
1774
+ // The service checks the word and says what is allowed.
1775
+ const said = await steer({ effort: effortTo[1] });
1776
+ if (said) {
1777
+ out(ui.c.dim(` effort ${ui.c.bold(said.effort)} for the next requests`));
1778
+ if (said.effort_note)
1779
+ out(ui.c.dim(` ${said.effort_note}`));
1382
1780
  }
1383
1781
  out();
1384
1782
  return prompt();
@@ -1386,20 +1784,16 @@ async function runRepl(how = {}) {
1386
1784
  // `mode <name>` and `plan off` (manual 4.1, 4.5).
1387
1785
  const modeTo = /^mode\s+(\S+)$/.exec(word);
1388
1786
  if (modeTo) {
1389
- const named = modeTo[1];
1390
- if (!MODES.includes(named)) {
1391
- out(ui.fail(`${named} is not a permission mode. One of: ${MODES.join(', ')}`));
1392
- }
1393
- else {
1394
- steering_.mode = named;
1395
- out(ui.c.dim(` mode ${ui.c.bold(named)}, from the next job`));
1396
- }
1787
+ const said = await steer({ mode: modeTo[1] });
1788
+ for (const line of said?.lines ?? [])
1789
+ out(ui.c.dim(line));
1397
1790
  out();
1398
1791
  return prompt();
1399
1792
  }
1400
1793
  if (word === 'plan off') {
1401
- steering_.plan = false;
1402
- out(ui.c.dim(' planning off: the next requests are carried out'));
1794
+ const said = await steer({ plan: false });
1795
+ for (const line of said?.lines ?? [])
1796
+ out(ui.c.dim(line));
1403
1797
  out();
1404
1798
  return prompt();
1405
1799
  }
@@ -1417,7 +1811,7 @@ async function runRepl(how = {}) {
1417
1811
  // `output 3`: a command's whole output, kept on this machine (5.12).
1418
1812
  const outputOf = /^output(?:\s+(\d+))?$/.exec(word);
1419
1813
  if (outputOf) {
1420
- const { keptOutput } = await Promise.resolve().then(() => __importStar(require('../executor/shell')));
1814
+ const { keptOutput } = await Promise.resolve().then(() => __importStar(require('../machine/executor/shell')));
1421
1815
  const kept = outputOf[1] ? keptOutput(Number(outputOf[1])) : undefined;
1422
1816
  if (!outputOf[1])
1423
1817
  out(ui.c.dim(' output N shows the whole output of command N, the number on its line.'));
@@ -1526,16 +1920,20 @@ async function runRepl(how = {}) {
1526
1920
  }
1527
1921
  if (COMMANDS.has(word)) {
1528
1922
  switch (word) {
1529
- case 'plan':
1530
- steering_.plan = true;
1531
- out(ui.c.dim(' planning on: the next requests are planned first and change nothing. `plan off` stops.'));
1923
+ case 'plan': {
1924
+ const said = await steer({ plan: true });
1925
+ for (const line of said?.lines ?? [])
1926
+ out(ui.c.dim(line));
1532
1927
  out();
1533
1928
  break;
1534
- case 'mode':
1535
- out(` ${ui.c.dim('mode')} ${ui.c.bold(steering_.mode)}${steering_.plan ? ui.c.dim(', planning first') : ''}`);
1536
- out(ui.c.dim(` one of ${MODES.join(', ')}. \`mode <name>\` or Shift+Tab changes it.`));
1929
+ }
1930
+ case 'mode': {
1931
+ const said = await steer({});
1932
+ for (const line of said?.lines ?? [])
1933
+ out(ui.c.dim(line));
1537
1934
  out();
1538
1935
  break;
1936
+ }
1539
1937
  case 'pin': {
1540
1938
  const said = await steer({});
1541
1939
  out(said?.pin
@@ -1544,6 +1942,14 @@ async function runRepl(how = {}) {
1544
1942
  out();
1545
1943
  break;
1546
1944
  }
1945
+ case 'optimize': {
1946
+ const said = await steer({});
1947
+ out(said?.pin
1948
+ ? ` ${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.')}`
1949
+ : ui.c.dim(' Optimize is on: CompletionPrism picks the model for each step. `optimize off <model>` keeps one.'));
1950
+ out();
1951
+ break;
1952
+ }
1547
1953
  case 'unpin':
1548
1954
  if (await steer({ pin: '' }))
1549
1955
  out(ui.c.dim(' unpinned: the service chooses the model for each step again'));
@@ -1649,6 +2055,46 @@ async function runRepl(how = {}) {
1649
2055
  }
1650
2056
  case 'sessions': {
1651
2057
  out();
2058
+ // What this account did before this window was opened, from the
2059
+ // service's record of sessions (manual 8.5). Kept numbered
2060
+ // separately so a number never means two different things.
2061
+ // Every session the service keeps a record of for this person, up to
2062
+ // its limit, when they can be moved through and picked by number; a
2063
+ // typed list stays short enough to read.
2064
+ const onTerminal = process.stdin.isTTY === true && process.stdout.isTTY === true;
2065
+ recent = await earlierSessions(history, onTerminal ? 200 : 10);
2066
+ // On a terminal, the list is picked from with the arrow keys; the
2067
+ // typed forms below still work everywhere, and in a pipe are the
2068
+ // only way.
2069
+ if (onTerminal && history.length + recent.length > 1) {
2070
+ // Names, dates and cost only. The ids are for typing a session
2071
+ // back, and nobody types in a list they move through with arrows.
2072
+ const noId = (line) => line.trimStart().replace(/^(?:srv|s)_[0-9a-f]+\s+/i, '');
2073
+ const rows = [
2074
+ ...history.map((s) => [
2075
+ `${s.title ?? 'this session'}${s === session ? ' (now)' : ''}`,
2076
+ `this window \u00b7 ${s.jobs.length} job(s) \u00b7 ${money(s.spentUsd)}`,
2077
+ ]),
2078
+ // The service's two lines where it sent them; its one line otherwise.
2079
+ ...recent.map((r) => (r.card?.length ? r.card : [noId(r.line)])),
2080
+ ];
2081
+ const { pick } = await Promise.resolve().then(() => __importStar(require('../lib/picker')));
2082
+ let choice = null;
2083
+ out(ui.c.dim(' choose a session:'));
2084
+ await withTerminal(async () => {
2085
+ choice = await pick(rows, 'Up and Down to choose, or type its number, Enter to open, Esc to stay here', history.indexOf(session), true);
2086
+ });
2087
+ out();
2088
+ if (choice === null)
2089
+ return prompt();
2090
+ const n = choice;
2091
+ if (n < history.length) {
2092
+ if (history[n] === session)
2093
+ return prompt();
2094
+ return handle(`sessions ${n + 1}`);
2095
+ }
2096
+ return handle(`sessions w${n - history.length + 1}`);
2097
+ }
1652
2098
  history.forEach((s, i) => {
1653
2099
  const here = s === session ? ui.c.green(' now') : ' ';
1654
2100
  // The NAME where our server has made one, and the id beside it
@@ -1658,10 +2104,6 @@ async function runRepl(how = {}) {
1658
2104
  out(`${here} ${String(i + 1).padStart(2)}. ${ui.c.bold(s.id)}`
1659
2105
  + ` ${s.jobs.length} job(s) ${money(s.spentUsd)}${named}`);
1660
2106
  });
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
2107
  if (recent.length > 0) {
1666
2108
  out();
1667
2109
  out(ui.c.dim(' earlier work on this account:'));
@@ -1685,17 +2127,10 @@ async function runRepl(how = {}) {
1685
2127
  // session the moment two are open, and this one is on the screen.
1686
2128
  case 'cost':
1687
2129
  case 'spend': {
1688
- // Nothing sent is nothing charged, and the service has no record of
1689
- // a session before its first job, so it would answer "no such
1690
- // session" to a person checking that a refusal cost them nothing
1691
- // (manual 6.1).
1692
- if (session.jobs.length === 0) {
1693
- out(` ${ui.c.dim('session')} ${ui.c.bold(session.id)} ${ui.c.dim('nothing has run yet, so it has cost')} $0.00`);
1694
- out();
1695
- break;
1696
- }
2130
+ // Nothing sent is nothing charged: the service answers even before
2131
+ // the first job, so a refusal can be checked as costing nothing (6.1).
1697
2132
  const { cmdCost } = await Promise.resolve().then(() => __importStar(require('./cost')));
1698
- await cmdCost([session.id]);
2133
+ await cmdCost([session.serverId || session.id]);
1699
2134
  break;
1700
2135
  }
1701
2136
  case 'model': {
@@ -1718,6 +2153,33 @@ async function runRepl(how = {}) {
1718
2153
  // Signing out ends the session, because everything after it would be
1719
2154
  // refused anyway. It used to answer "not available inside a session",
1720
2155
  // which left quitting and running a second command as the only way.
2156
+ case 'history':
2157
+ // Every earlier question and answer in the session being stood in.
2158
+ out();
2159
+ await showConversation(session.serverId, true);
2160
+ break;
2161
+ case 'login': {
2162
+ // Signing in again, or as somebody else, from inside a session. The
2163
+ // session was opened with the old sign in, so it ends and the next
2164
+ // `comprism` opens with the new one.
2165
+ const { cmdLogin } = await Promise.resolve().then(() => __importStar(require('./login')));
2166
+ const before = (0, connection_1.readConnection)()?.token;
2167
+ await withTerminal(async () => {
2168
+ // The Enter that sent `/login` is still in the terminal's input.
2169
+ // Given a moment it goes to the session, not to the email question.
2170
+ await new Promise((r) => setTimeout(r, 150));
2171
+ // Anything already typed but not yet read is thrown away, so the
2172
+ // first question waits for a real answer.
2173
+ while (process.stdin.read() !== null) { /* discard */ }
2174
+ await cmdLogin([]);
2175
+ });
2176
+ // Canceled or refused: nothing changed, so the session carries on.
2177
+ if ((0, connection_1.readConnection)()?.token === before)
2178
+ break;
2179
+ out(ui.c.dim(` type ${ui.c.bold('comprism')} to start a session with this sign in`));
2180
+ out();
2181
+ return leave();
2182
+ }
1721
2183
  case 'logout':
1722
2184
  case 'signout':
1723
2185
  case 'disconnect': {