@sigil0/looking-glass 0.1.0

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 (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +572 -0
  3. package/dist/agents/coordinator.d.ts +21 -0
  4. package/dist/agents/coordinator.d.ts.map +1 -0
  5. package/dist/agents/coordinator.js +171 -0
  6. package/dist/agents/coordinator.js.map +1 -0
  7. package/dist/app.d.ts +44 -0
  8. package/dist/app.d.ts.map +1 -0
  9. package/dist/app.js +215 -0
  10. package/dist/app.js.map +1 -0
  11. package/dist/cli.d.ts +3 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +419 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/config.d.ts +4 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/config.js +132 -0
  18. package/dist/config.js.map +1 -0
  19. package/dist/engine/context.d.ts +6 -0
  20. package/dist/engine/context.d.ts.map +1 -0
  21. package/dist/engine/context.js +65 -0
  22. package/dist/engine/context.js.map +1 -0
  23. package/dist/engine/engine.d.ts +74 -0
  24. package/dist/engine/engine.d.ts.map +1 -0
  25. package/dist/engine/engine.js +573 -0
  26. package/dist/engine/engine.js.map +1 -0
  27. package/dist/engine/types.d.ts +35 -0
  28. package/dist/engine/types.d.ts.map +1 -0
  29. package/dist/engine/types.js +2 -0
  30. package/dist/engine/types.js.map +1 -0
  31. package/dist/instructions.d.ts +8 -0
  32. package/dist/instructions.d.ts.map +1 -0
  33. package/dist/instructions.js +60 -0
  34. package/dist/instructions.js.map +1 -0
  35. package/dist/model/codex-lb.d.ts +141 -0
  36. package/dist/model/codex-lb.d.ts.map +1 -0
  37. package/dist/model/codex-lb.js +471 -0
  38. package/dist/model/codex-lb.js.map +1 -0
  39. package/dist/paths.d.ts +7 -0
  40. package/dist/paths.d.ts.map +1 -0
  41. package/dist/paths.js +41 -0
  42. package/dist/paths.js.map +1 -0
  43. package/dist/scheduler/daemon.d.ts +40 -0
  44. package/dist/scheduler/daemon.d.ts.map +1 -0
  45. package/dist/scheduler/daemon.js +189 -0
  46. package/dist/scheduler/daemon.js.map +1 -0
  47. package/dist/scheduler/index.d.ts +8 -0
  48. package/dist/scheduler/index.d.ts.map +1 -0
  49. package/dist/scheduler/index.js +6 -0
  50. package/dist/scheduler/index.js.map +1 -0
  51. package/dist/scheduler/runner.d.ts +18 -0
  52. package/dist/scheduler/runner.d.ts.map +1 -0
  53. package/dist/scheduler/runner.js +173 -0
  54. package/dist/scheduler/runner.js.map +1 -0
  55. package/dist/scheduler/schedule.d.ts +7 -0
  56. package/dist/scheduler/schedule.d.ts.map +1 -0
  57. package/dist/scheduler/schedule.js +94 -0
  58. package/dist/scheduler/schedule.js.map +1 -0
  59. package/dist/scheduler/store.d.ts +55 -0
  60. package/dist/scheduler/store.d.ts.map +1 -0
  61. package/dist/scheduler/store.js +1203 -0
  62. package/dist/scheduler/store.js.map +1 -0
  63. package/dist/scheduler/systemd.d.ts +7 -0
  64. package/dist/scheduler/systemd.d.ts.map +1 -0
  65. package/dist/scheduler/systemd.js +97 -0
  66. package/dist/scheduler/systemd.js.map +1 -0
  67. package/dist/scheduler/types.d.ts +145 -0
  68. package/dist/scheduler/types.d.ts.map +1 -0
  69. package/dist/scheduler/types.js +7 -0
  70. package/dist/scheduler/types.js.map +1 -0
  71. package/dist/security.d.ts +6 -0
  72. package/dist/security.d.ts.map +1 -0
  73. package/dist/security.js +27 -0
  74. package/dist/security.js.map +1 -0
  75. package/dist/storage/artifact-store.d.ts +19 -0
  76. package/dist/storage/artifact-store.d.ts.map +1 -0
  77. package/dist/storage/artifact-store.js +47 -0
  78. package/dist/storage/artifact-store.js.map +1 -0
  79. package/dist/storage/database.d.ts +5 -0
  80. package/dist/storage/database.d.ts.map +1 -0
  81. package/dist/storage/database.js +395 -0
  82. package/dist/storage/database.js.map +1 -0
  83. package/dist/storage/session-store.d.ts +114 -0
  84. package/dist/storage/session-store.d.ts.map +1 -0
  85. package/dist/storage/session-store.js +731 -0
  86. package/dist/storage/session-store.js.map +1 -0
  87. package/dist/tools/agents.d.ts +14 -0
  88. package/dist/tools/agents.d.ts.map +1 -0
  89. package/dist/tools/agents.js +35 -0
  90. package/dist/tools/agents.js.map +1 -0
  91. package/dist/tools/apply-patch.d.ts +7 -0
  92. package/dist/tools/apply-patch.d.ts.map +1 -0
  93. package/dist/tools/apply-patch.js +429 -0
  94. package/dist/tools/apply-patch.js.map +1 -0
  95. package/dist/tools/ask.d.ts +8 -0
  96. package/dist/tools/ask.d.ts.map +1 -0
  97. package/dist/tools/ask.js +27 -0
  98. package/dist/tools/ask.js.map +1 -0
  99. package/dist/tools/bash.d.ts +9 -0
  100. package/dist/tools/bash.d.ts.map +1 -0
  101. package/dist/tools/bash.js +98 -0
  102. package/dist/tools/bash.js.map +1 -0
  103. package/dist/tools/index.d.ts +9 -0
  104. package/dist/tools/index.d.ts.map +1 -0
  105. package/dist/tools/index.js +34 -0
  106. package/dist/tools/index.js.map +1 -0
  107. package/dist/tools/paths.d.ts +3 -0
  108. package/dist/tools/paths.d.ts.map +1 -0
  109. package/dist/tools/paths.js +34 -0
  110. package/dist/tools/paths.js.map +1 -0
  111. package/dist/tools/process.d.ts +21 -0
  112. package/dist/tools/process.d.ts.map +1 -0
  113. package/dist/tools/process.js +104 -0
  114. package/dist/tools/process.js.map +1 -0
  115. package/dist/tools/read.d.ts +9 -0
  116. package/dist/tools/read.d.ts.map +1 -0
  117. package/dist/tools/read.js +95 -0
  118. package/dist/tools/read.js.map +1 -0
  119. package/dist/tools/registry.d.ts +19 -0
  120. package/dist/tools/registry.d.ts.map +1 -0
  121. package/dist/tools/registry.js +188 -0
  122. package/dist/tools/registry.js.map +1 -0
  123. package/dist/tools/safety.d.ts +9 -0
  124. package/dist/tools/safety.d.ts.map +1 -0
  125. package/dist/tools/safety.js +199 -0
  126. package/dist/tools/safety.js.map +1 -0
  127. package/dist/tools/schedule.d.ts +4 -0
  128. package/dist/tools/schedule.d.ts.map +1 -0
  129. package/dist/tools/schedule.js +191 -0
  130. package/dist/tools/schedule.js.map +1 -0
  131. package/dist/tools/search.d.ts +16 -0
  132. package/dist/tools/search.d.ts.map +1 -0
  133. package/dist/tools/search.js +75 -0
  134. package/dist/tools/search.js.map +1 -0
  135. package/dist/tools/types.d.ts +54 -0
  136. package/dist/tools/types.d.ts.map +1 -0
  137. package/dist/tools/types.js +10 -0
  138. package/dist/tools/types.js.map +1 -0
  139. package/dist/types.d.ts +90 -0
  140. package/dist/types.d.ts.map +1 -0
  141. package/dist/types.js +2 -0
  142. package/dist/types.js.map +1 -0
  143. package/dist/ui/stdio.d.ts +5 -0
  144. package/dist/ui/stdio.d.ts.map +1 -0
  145. package/dist/ui/stdio.js +65 -0
  146. package/dist/ui/stdio.js.map +1 -0
  147. package/dist/ui/tui.d.ts +93 -0
  148. package/dist/ui/tui.d.ts.map +1 -0
  149. package/dist/ui/tui.js +2246 -0
  150. package/dist/ui/tui.js.map +1 -0
  151. package/package.json +53 -0
package/dist/ui/tui.js ADDED
@@ -0,0 +1,2246 @@
1
+ import { spawn } from "node:child_process";
2
+ import { platform } from "node:os";
3
+ import { stdin, stdout } from "node:process";
4
+ import { stripVTControlCharacters } from "node:util";
5
+ import { CombinedAutocompleteProvider, Container, Editor, Input, Key, Markdown, ProcessTerminal, SelectList, TUI, matchesKey, sliceByColumn, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
6
+ import { projectContext } from "../engine/context.js";
7
+ import { initialDue } from "../scheduler/schedule.js";
8
+ import { isWithin } from "../tools/paths.js";
9
+ const MIN_COLUMNS = 32;
10
+ const MIN_ROWS = 12;
11
+ const STREAM_RENDER_MS = 40;
12
+ const INBOX_POLL_MS = 2_000;
13
+ const MODEL_AVAILABILITY_RETRY_MS = 30_000;
14
+ const MAX_TOOL_PREVIEW = 320;
15
+ const ACTIVITY_RENDER_MS = 1_000;
16
+ const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
17
+ const DELETE_SELECTION_PREFIX = "\0delete:";
18
+ const style = (open, close = 39) => (text) => `\x1b[${open}m${text}\x1b[${close}m`;
19
+ const bold = style(1, 22);
20
+ const dim = style(2, 22);
21
+ const red = style(31);
22
+ const green = style(32);
23
+ const yellow = style(33);
24
+ const blue = style(34);
25
+ const magenta = style(35);
26
+ const cyan = style(36);
27
+ function tonalLine(line, width, background) {
28
+ const base = `\x1b[38;5;252;48;5;${background}m`;
29
+ const content = pad(line, width).replace(/\x1b\[(?:0|39|49)m/g, (reset) => `${reset}${base}`);
30
+ return `${base}${content}\x1b[39;49m`;
31
+ }
32
+ const selectTheme = {
33
+ selectedPrefix: cyan,
34
+ selectedText: (text) => `\x1b[7m${text}\x1b[27m`,
35
+ description: dim,
36
+ scrollInfo: dim,
37
+ noMatch: dim,
38
+ };
39
+ const editorTheme = {
40
+ borderColor: cyan,
41
+ selectList: selectTheme,
42
+ };
43
+ const markdownTheme = {
44
+ heading: (text) => bold(cyan(text)),
45
+ link: blue,
46
+ linkUrl: dim,
47
+ code: yellow,
48
+ codeBlock: (text) => text,
49
+ codeBlockBorder: dim,
50
+ quote: dim,
51
+ quoteBorder: cyan,
52
+ hr: dim,
53
+ listBullet: cyan,
54
+ bold,
55
+ italic: (text) => `\x1b[3m${text}\x1b[23m`,
56
+ strikethrough: (text) => `\x1b[9m${text}\x1b[29m`,
57
+ underline: (text) => `\x1b[4m${text}\x1b[24m`,
58
+ codeBlockIndent: " ",
59
+ };
60
+ function errorMessage(error) {
61
+ return error instanceof Error ? error.message : String(error);
62
+ }
63
+ function isRecord(value) {
64
+ return value !== null && typeof value === "object" && !Array.isArray(value);
65
+ }
66
+ function displaySafe(text) {
67
+ return text
68
+ .replace(/\r\n?/g, "\n")
69
+ .replace(/\t/g, " ")
70
+ .replace(/[\x00-\x08\x0b-\x1f\x7f]/g, (character) => {
71
+ return `\\x${character.charCodeAt(0).toString(16).padStart(2, "0")}`;
72
+ });
73
+ }
74
+ function oneLine(text, limit = MAX_TOOL_PREVIEW) {
75
+ const compact = displaySafe(text).replace(/\s+/g, " ").trim();
76
+ return compact.length > limit ? `${compact.slice(0, Math.max(0, limit - 3))}...` : compact;
77
+ }
78
+ export function contextUsageLabel(inputTokens, contextWindow) {
79
+ if (contextWindow <= 0)
80
+ return "ctx:?";
81
+ const percent = Math.max(0, Math.min(999, Math.round((inputTokens / contextWindow) * 100)));
82
+ return `ctx:${percent}%`;
83
+ }
84
+ export function sessionMetadataLine(session, approval, contextUsage, width = Number.POSITIVE_INFINITY) {
85
+ const narrow = Number.isFinite(width) && width < 100;
86
+ const middle = [
87
+ session.reasoningEffort,
88
+ contextUsage,
89
+ `agents:${session.agentsEnabled ? "on" : "off"}`,
90
+ approval,
91
+ `${narrow ? "p" : "persist"}:${session.persistent ? "on" : "off"}`,
92
+ ];
93
+ const separators = 3 * (middle.length + 1);
94
+ const available = Number.isFinite(width)
95
+ ? Math.max(8, Math.floor(width) - middle.reduce((sum, value) => sum + value.length, 0) - separators)
96
+ : session.model.length + session.title.length;
97
+ let modelWidth = Math.max(4, Math.floor(available * 0.6));
98
+ let titleWidth = Math.max(4, available - modelWidth);
99
+ if (session.model.length < modelWidth) {
100
+ titleWidth += modelWidth - session.model.length;
101
+ modelWidth = session.model.length;
102
+ }
103
+ else if (session.title.length < titleWidth) {
104
+ modelWidth += titleWidth - session.title.length;
105
+ titleWidth = session.title.length;
106
+ }
107
+ return [
108
+ oneLine(session.model, modelWidth),
109
+ ...middle,
110
+ oneLine(session.title, titleWidth),
111
+ ].join(" | ");
112
+ }
113
+ export function activityLine(active, status, frame, scrollOffset) {
114
+ const activity = active ? `${status}${".".repeat((frame % 3) + 1)}` : "Ready";
115
+ return scrollOffset > 0 ? `${activity} | scroll:+${scrollOffset}` : activity;
116
+ }
117
+ function orderedSelection(anchor, focus) {
118
+ if (anchor.row < focus.row || (anchor.row === focus.row && anchor.column <= focus.column)) {
119
+ return [anchor, focus];
120
+ }
121
+ return [focus, anchor];
122
+ }
123
+ function stripTerminalFormatting(text) {
124
+ return stripVTControlCharacters(text.replace(/\x1b_[\s\S]*?(?:\x07|\x1b\\)/g, ""));
125
+ }
126
+ function snapColumn(line, column, direction) {
127
+ const text = stripTerminalFormatting(line);
128
+ const segments = graphemeSegmenter.segment(text);
129
+ let current = 0;
130
+ for (const { segment } of segments) {
131
+ const next = current + visibleWidth(segment);
132
+ if (column > current && column < next)
133
+ return direction === "floor" ? current : next;
134
+ if (column <= current)
135
+ return current;
136
+ current = next;
137
+ }
138
+ return current;
139
+ }
140
+ export function selectedScreenText(lines, anchor, focus) {
141
+ if (lines.length === 0)
142
+ return "";
143
+ const [start, end] = orderedSelection(anchor, focus);
144
+ const firstRow = Math.max(0, Math.min(lines.length - 1, start.row));
145
+ const lastRow = Math.max(firstRow, Math.min(lines.length - 1, end.row));
146
+ const selected = [];
147
+ for (let row = firstRow; row <= lastRow; row += 1) {
148
+ const line = lines[row] ?? "";
149
+ const width = visibleWidth(line);
150
+ const from = row === firstRow ? snapColumn(line, Math.max(0, Math.min(width, start.column)), "floor") : 0;
151
+ const to = row === lastRow
152
+ ? snapColumn(line, Math.max(from, Math.min(width, end.column + 1)), "ceil")
153
+ : width;
154
+ selected.push(stripTerminalFormatting(sliceByColumn(line, from, Math.max(0, to - from))).trimEnd());
155
+ }
156
+ return selected.join("\n");
157
+ }
158
+ function jsonPreview(value) {
159
+ try {
160
+ return oneLine(JSON.stringify(value));
161
+ }
162
+ catch {
163
+ return oneLine(String(value));
164
+ }
165
+ }
166
+ function fit(line, width) {
167
+ return truncateToWidth(line, Math.max(1, width), "");
168
+ }
169
+ function pad(line, width) {
170
+ const fitted = fit(line, width);
171
+ return fitted + " ".repeat(Math.max(0, width - visibleWidth(fitted)));
172
+ }
173
+ function wrap(text, width) {
174
+ return wrapTextWithAnsi(displaySafe(text), Math.max(1, width));
175
+ }
176
+ function renderFrame(title, body, width) {
177
+ const frameWidth = Math.max(1, width);
178
+ if (frameWidth < 4)
179
+ return body.map((line) => fit(line, frameWidth));
180
+ const innerWidth = frameWidth - 2;
181
+ const border = `+${"-".repeat(innerWidth)}+`;
182
+ return [
183
+ border,
184
+ `|${pad(` ${bold(displaySafe(title))}`, innerWidth)}|`,
185
+ `|${"-".repeat(innerWidth)}|`,
186
+ ...body.map((line) => `|${pad(line, innerWidth)}|`),
187
+ border,
188
+ ].map((line) => fit(line, frameWidth));
189
+ }
190
+ class AlternateScreenTerminal extends ProcessTerminal {
191
+ active = false;
192
+ alternate = false;
193
+ get isActive() {
194
+ return this.active;
195
+ }
196
+ copy(text) {
197
+ if (!text)
198
+ return;
199
+ const sequence = `\x1b]52;c;${Buffer.from(text).toString("base64")}\x07`;
200
+ if (process.env.TMUX)
201
+ this.write(`\x1bPtmux;\x1b${sequence}\x1b\\`);
202
+ else if (process.env.STY)
203
+ this.write(`\x1bP${sequence}\x1b\\`);
204
+ else
205
+ this.write(sequence);
206
+ let command;
207
+ let args = [];
208
+ if (platform() === "darwin")
209
+ command = "pbcopy";
210
+ else if (platform() === "win32")
211
+ command = "clip.exe";
212
+ else if (process.env.WAYLAND_DISPLAY)
213
+ command = "wl-copy";
214
+ else if (process.env.DISPLAY) {
215
+ command = "xclip";
216
+ args = ["-selection", "clipboard"];
217
+ }
218
+ if (!command)
219
+ return;
220
+ const child = spawn(command, args, { stdio: ["pipe", "ignore", "ignore"] });
221
+ child.on("error", () => { });
222
+ child.stdin?.on("error", () => { });
223
+ child.stdin?.end(text);
224
+ }
225
+ start(onInput, onResize) {
226
+ if (this.active)
227
+ return;
228
+ try {
229
+ super.start(onInput, onResize);
230
+ this.active = true;
231
+ this.write("\x1b[?1049h\x1b[?1000h\x1b[?1002h\x1b[?1006h\x1b[2J\x1b[H");
232
+ this.alternate = true;
233
+ }
234
+ catch (error) {
235
+ try {
236
+ super.stop();
237
+ }
238
+ finally {
239
+ if (this.alternate)
240
+ this.write("\x1b[?1006l\x1b[?1002l\x1b[?1000l\x1b[?1049l");
241
+ this.active = false;
242
+ this.alternate = false;
243
+ }
244
+ throw error;
245
+ }
246
+ }
247
+ async drainInput(maxMs, idleMs) {
248
+ if (!this.active)
249
+ return;
250
+ await super.drainInput(maxMs, idleMs);
251
+ }
252
+ stop() {
253
+ if (!this.active && !this.alternate)
254
+ return;
255
+ try {
256
+ if (this.active)
257
+ super.stop();
258
+ }
259
+ finally {
260
+ if (this.alternate)
261
+ this.write("\x1b[0m\x1b[?25h\x1b[?1006l\x1b[?1002l\x1b[?1000l\x1b[?1049l");
262
+ this.active = false;
263
+ this.alternate = false;
264
+ }
265
+ }
266
+ }
267
+ class SafeAutocompleteProvider extends CombinedAutocompleteProvider {
268
+ async getSuggestions(lines, cursorLine, cursorCol, options) {
269
+ try {
270
+ return await super.getSuggestions(lines, cursorLine, cursorCol, options);
271
+ }
272
+ catch {
273
+ return null;
274
+ }
275
+ }
276
+ }
277
+ class TurnEditor extends Editor {
278
+ enabled = true;
279
+ setEnabled(enabled) {
280
+ this.enabled = enabled;
281
+ this.disableSubmit = !enabled;
282
+ this.borderColor = enabled ? editorTheme.borderColor : dim;
283
+ }
284
+ handleInput(data) {
285
+ if (this.enabled)
286
+ super.handleInput(data);
287
+ }
288
+ }
289
+ export class UserMessage {
290
+ text;
291
+ constructor(text) {
292
+ this.text = text;
293
+ }
294
+ invalidate() { }
295
+ render(width) {
296
+ const contentWidth = Math.max(1, width - 2);
297
+ const lines = [bold(blue("You")), ...wrap(this.text, contentWidth).map((line) => ` ${line}`)];
298
+ return [...lines.map((line) => tonalLine(line, width, 17)), ""];
299
+ }
300
+ }
301
+ export class AssistantMessage {
302
+ markdown;
303
+ text;
304
+ constructor(text = "") {
305
+ this.text = displaySafe(text);
306
+ this.markdown = new Markdown(this.text, 2, 0, markdownTheme);
307
+ }
308
+ get empty() {
309
+ return this.text.length === 0;
310
+ }
311
+ setText(text) {
312
+ this.text = displaySafe(text);
313
+ this.markdown.setText(this.text);
314
+ }
315
+ invalidate() {
316
+ this.markdown.invalidate();
317
+ }
318
+ render(width) {
319
+ if (this.empty)
320
+ return [];
321
+ return [...this.markdown.render(width).map((line) => tonalLine(line, width, 234)), ""];
322
+ }
323
+ }
324
+ class StreamingAssistant extends AssistantMessage {
325
+ requestRender;
326
+ raw = "";
327
+ timer = null;
328
+ constructor(requestRender) {
329
+ super();
330
+ this.requestRender = requestRender;
331
+ }
332
+ append(delta) {
333
+ this.raw += delta;
334
+ if (this.timer)
335
+ return;
336
+ this.timer = setTimeout(() => {
337
+ this.timer = null;
338
+ this.setText(this.raw);
339
+ this.requestRender();
340
+ }, STREAM_RENDER_MS);
341
+ }
342
+ flush() {
343
+ if (this.timer) {
344
+ clearTimeout(this.timer);
345
+ this.timer = null;
346
+ }
347
+ this.setText(this.raw);
348
+ this.requestRender();
349
+ }
350
+ dispose() {
351
+ if (this.timer)
352
+ clearTimeout(this.timer);
353
+ this.timer = null;
354
+ }
355
+ }
356
+ class ReasoningSummary {
357
+ text;
358
+ constructor(text = "") {
359
+ this.text = displaySafe(text);
360
+ }
361
+ setText(text) {
362
+ this.text = displaySafe(text);
363
+ }
364
+ invalidate() { }
365
+ render(width) {
366
+ if (!this.text)
367
+ return [];
368
+ const contentWidth = Math.max(1, width - 2);
369
+ return [dim("Reasoning summary"), ...wrap(this.text, contentWidth).map((line) => ` ${dim(line)}`), ""]
370
+ .map((line) => fit(line, width));
371
+ }
372
+ }
373
+ class StreamingReasoning extends ReasoningSummary {
374
+ requestRender;
375
+ raw = "";
376
+ timer = null;
377
+ constructor(requestRender) {
378
+ super();
379
+ this.requestRender = requestRender;
380
+ }
381
+ append(delta) {
382
+ this.raw += delta;
383
+ if (this.timer)
384
+ return;
385
+ this.timer = setTimeout(() => {
386
+ this.timer = null;
387
+ this.setText(this.raw);
388
+ this.requestRender();
389
+ }, STREAM_RENDER_MS);
390
+ }
391
+ flush() {
392
+ if (this.timer)
393
+ clearTimeout(this.timer);
394
+ this.timer = null;
395
+ this.setText(this.raw);
396
+ this.requestRender();
397
+ }
398
+ dispose() {
399
+ if (this.timer)
400
+ clearTimeout(this.timer);
401
+ this.timer = null;
402
+ }
403
+ }
404
+ export class ToolCard {
405
+ callId;
406
+ name;
407
+ summary;
408
+ output = null;
409
+ failed = false;
410
+ finished = false;
411
+ constructor(callId, name, summary) {
412
+ this.callId = callId;
413
+ this.name = name;
414
+ this.summary = summary;
415
+ }
416
+ finish(output, failed) {
417
+ this.output = oneLine(output) || "No output";
418
+ this.failed = failed;
419
+ this.finished = true;
420
+ }
421
+ progress(output) {
422
+ if (!this.finished)
423
+ this.output = oneLine(output) || null;
424
+ }
425
+ invalidate() { }
426
+ render(width) {
427
+ const state = this.finished ? (this.failed ? red("failed") : green("done")) : yellow("running");
428
+ const header = `${dim("tool")} ${cyan(oneLine(this.name))} [${state}] ${oneLine(this.summary)}`;
429
+ const lines = [fit(header, width)];
430
+ if (this.output)
431
+ lines.push(fit(` ${dim(this.output)}`, width));
432
+ return [...lines.map((line) => tonalLine(line, width, 236)), ""];
433
+ }
434
+ }
435
+ class Notice {
436
+ label;
437
+ text;
438
+ color;
439
+ constructor(label, text, color = magenta) {
440
+ this.label = label;
441
+ this.text = text;
442
+ this.color = color;
443
+ }
444
+ invalidate() { }
445
+ render(width) {
446
+ const prefix = `${this.color(`[${displaySafe(this.label)}]`)} `;
447
+ const prefixWidth = visibleWidth(prefix);
448
+ const contentWidth = Math.max(1, width - prefixWidth);
449
+ const lines = wrap(this.text, contentWidth);
450
+ const first = lines[0] ?? "";
451
+ return [
452
+ fit(prefix + first, width),
453
+ ...lines.slice(1).map((line) => fit(`${" ".repeat(prefixWidth)}${line}`, width)),
454
+ "",
455
+ ];
456
+ }
457
+ }
458
+ class FullHeightRoot extends Container {
459
+ terminal;
460
+ editor;
461
+ activityText;
462
+ metadataText;
463
+ entries = [];
464
+ transcriptCache = null;
465
+ transcriptCacheWidth = 0;
466
+ scrollFromBottom = 0;
467
+ lastTranscriptLines = 0;
468
+ lastWidth = 0;
469
+ viewportRows = 1;
470
+ maxScroll = 0;
471
+ selectionAnchor = null;
472
+ selectionFocus = null;
473
+ lastFrame = [];
474
+ selectableRows = 0;
475
+ constructor(terminal, editor, activityText, metadataText) {
476
+ super();
477
+ this.terminal = terminal;
478
+ this.editor = editor;
479
+ this.activityText = activityText;
480
+ this.metadataText = metadataText;
481
+ this.addChild(editor);
482
+ }
483
+ addEntry(component) {
484
+ this.entries.push(component);
485
+ this.invalidateTranscript();
486
+ }
487
+ invalidateTranscript() {
488
+ this.transcriptCache = null;
489
+ }
490
+ clearTranscript() {
491
+ for (const entry of this.entries)
492
+ entry.invalidate();
493
+ this.entries.length = 0;
494
+ this.invalidateTranscript();
495
+ this.scrollFromBottom = 0;
496
+ this.lastTranscriptLines = 0;
497
+ }
498
+ scrollPage(direction) {
499
+ const amount = Math.max(1, this.viewportRows - 2);
500
+ this.scrollFromBottom = direction < 0
501
+ ? Math.min(this.maxScroll, this.scrollFromBottom + amount)
502
+ : Math.max(0, this.scrollFromBottom - amount);
503
+ }
504
+ scrollLines(lines) {
505
+ this.scrollFromBottom = Math.max(0, Math.min(this.maxScroll, this.scrollFromBottom + lines));
506
+ }
507
+ get scrollOffset() {
508
+ return this.scrollFromBottom;
509
+ }
510
+ startSelection(point) {
511
+ if (point.row < 0 || point.row >= this.selectableRows)
512
+ return false;
513
+ this.selectionAnchor = this.clampPoint(point);
514
+ this.selectionFocus = this.selectionAnchor;
515
+ return true;
516
+ }
517
+ updateSelection(point) {
518
+ if (!this.selectionAnchor)
519
+ return false;
520
+ this.selectionFocus = this.clampPoint(point);
521
+ return true;
522
+ }
523
+ selectionText() {
524
+ if (!this.selectionAnchor || !this.selectionFocus)
525
+ return "";
526
+ return selectedScreenText(this.lastFrame, this.selectionAnchor, this.selectionFocus);
527
+ }
528
+ clearSelection() {
529
+ this.selectionAnchor = null;
530
+ this.selectionFocus = null;
531
+ }
532
+ clampPoint(point) {
533
+ return {
534
+ column: Math.max(0, Math.min(this.terminal.columns - 1, point.column)),
535
+ row: Math.max(0, Math.min(Math.max(0, this.selectableRows - 1), point.row)),
536
+ };
537
+ }
538
+ renderSelection(frame) {
539
+ this.lastFrame = frame;
540
+ if (!this.selectionAnchor || !this.selectionFocus)
541
+ return frame;
542
+ const [start, end] = orderedSelection(this.selectionAnchor, this.selectionFocus);
543
+ return frame.map((line, row) => {
544
+ if (row < start.row || row > end.row)
545
+ return line;
546
+ const width = visibleWidth(line);
547
+ const from = row === start.row
548
+ ? snapColumn(line, Math.max(0, Math.min(width, start.column)), "floor")
549
+ : 0;
550
+ const to = row === end.row
551
+ ? snapColumn(line, Math.max(from, Math.min(width, end.column + 1)), "ceil")
552
+ : width;
553
+ if (to <= from)
554
+ return line;
555
+ const before = sliceByColumn(line, 0, from);
556
+ const selected = stripVTControlCharacters(sliceByColumn(line, from, to - from));
557
+ const after = sliceByColumn(line, to, Math.max(0, width - to));
558
+ return `${before}\x1b[7m${selected}\x1b[27m${after}`;
559
+ });
560
+ }
561
+ invalidate() {
562
+ this.editor.invalidate();
563
+ for (const entry of this.entries)
564
+ entry.invalidate();
565
+ this.invalidateTranscript();
566
+ }
567
+ render(width) {
568
+ const height = Math.max(1, this.terminal.rows);
569
+ const safeWidth = Math.max(1, width);
570
+ if (safeWidth < MIN_COLUMNS || height < MIN_ROWS) {
571
+ this.selectableRows = 0;
572
+ const lines = Array.from({ length: height }).fill("");
573
+ const message = `Terminal too small (${safeWidth}x${height}); need ${MIN_COLUMNS}x${MIN_ROWS}`;
574
+ lines[Math.floor(height / 2)] = fit(yellow(message), safeWidth);
575
+ return this.renderSelection(lines);
576
+ }
577
+ let editorLines = this.editor.render(safeWidth).map((line) => fit(line, safeWidth));
578
+ const maxEditorRows = Math.max(3, height - 4);
579
+ if (editorLines.length > maxEditorRows)
580
+ editorLines = editorLines.slice(0, maxEditorRows);
581
+ this.viewportRows = Math.max(0, height - editorLines.length - 2);
582
+ if (!this.transcriptCache || this.transcriptCacheWidth !== safeWidth) {
583
+ this.transcriptCache = this.entries.flatMap((entry) => entry.render(safeWidth).map((line) => fit(line, safeWidth)));
584
+ this.transcriptCacheWidth = safeWidth;
585
+ }
586
+ const transcript = this.transcriptCache;
587
+ if (this.lastWidth === safeWidth && this.scrollFromBottom > 0 && transcript.length > this.lastTranscriptLines) {
588
+ this.scrollFromBottom += transcript.length - this.lastTranscriptLines;
589
+ }
590
+ this.lastWidth = safeWidth;
591
+ this.lastTranscriptLines = transcript.length;
592
+ this.maxScroll = Math.max(0, transcript.length - this.viewportRows);
593
+ this.scrollFromBottom = Math.min(this.scrollFromBottom, this.maxScroll);
594
+ const end = Math.max(0, transcript.length - this.scrollFromBottom);
595
+ const start = Math.max(0, end - this.viewportRows);
596
+ const visibleTranscript = transcript.slice(start, end);
597
+ const topPadding = Array.from({
598
+ length: Math.max(0, this.viewportRows - visibleTranscript.length),
599
+ }).fill("");
600
+ this.selectableRows = topPadding.length + visibleTranscript.length;
601
+ const activity = fit(dim(` ${oneLine(this.activityText(), 10_000)}`), safeWidth);
602
+ const metadata = fit(dim(` ${oneLine(this.metadataText(Math.max(1, safeWidth - 1)), 10_000)}`), safeWidth);
603
+ return this.renderSelection([...topPadding, ...visibleTranscript, activity, ...editorLines, metadata]);
604
+ }
605
+ }
606
+ class SelectorModal {
607
+ prompt;
608
+ list;
609
+ onSelect;
610
+ onDelete;
611
+ onCancel;
612
+ constructor(title, items, maxVisible, prompt) {
613
+ this.prompt = prompt;
614
+ this.title = title;
615
+ const safeItems = items.map((item) => ({
616
+ value: item.value,
617
+ label: oneLine(item.label, 500),
618
+ ...(item.description ? { description: oneLine(item.description, 1_000) } : {}),
619
+ }));
620
+ this.list = new SelectList(safeItems, maxVisible, selectTheme, {
621
+ minPrimaryColumnWidth: 12,
622
+ maxPrimaryColumnWidth: 34,
623
+ });
624
+ this.list.onSelect = (item) => this.onSelect?.(item.value);
625
+ this.list.onCancel = () => this.onCancel?.();
626
+ }
627
+ title;
628
+ invalidate() {
629
+ this.list.invalidate();
630
+ }
631
+ handleInput(data) {
632
+ if (this.onDelete && matchesKey(data, Key.delete)) {
633
+ const selected = this.list.getSelectedItem();
634
+ if (selected)
635
+ this.onDelete(selected.value);
636
+ return;
637
+ }
638
+ this.list.handleInput(data);
639
+ }
640
+ render(width) {
641
+ const prompt = this.prompt ? [...wrap(this.prompt, Math.max(1, width - 4)), ""] : [];
642
+ return renderFrame(this.title, [
643
+ ...prompt,
644
+ ...this.list.render(Math.max(1, width - 2)),
645
+ dim(` Enter select${this.onDelete ? " | Delete remove" : ""} | Esc cancel`),
646
+ ], width);
647
+ }
648
+ }
649
+ export class ApprovalModal {
650
+ terminal;
651
+ request;
652
+ onDone;
653
+ selected = "deny";
654
+ scrollOffset = 0;
655
+ pageSize = 1;
656
+ maxScroll = 0;
657
+ renderedWidth = 1;
658
+ renderedHeight = 1;
659
+ hitboxes = new Map();
660
+ pressed = null;
661
+ constructor(terminal, request) {
662
+ this.terminal = terminal;
663
+ this.request = request;
664
+ }
665
+ invalidate() { }
666
+ actions() {
667
+ return this.request.canAlwaysApprove ? ["deny", "once", "always"] : ["deny", "once"];
668
+ }
669
+ handleInput(data) {
670
+ if (matchesKey(data, Key.left) || matchesKey(data, Key.right) || matchesKey(data, Key.tab)) {
671
+ const actions = this.actions();
672
+ const current = actions.indexOf(this.selected);
673
+ const direction = matchesKey(data, Key.left) ? -1 : 1;
674
+ this.selected = actions[(current + direction + actions.length) % actions.length] ?? "deny";
675
+ }
676
+ else if (matchesKey(data, Key.up)) {
677
+ this.scrollOffset = Math.max(0, this.scrollOffset - 1);
678
+ }
679
+ else if (matchesKey(data, Key.down)) {
680
+ this.scrollOffset = Math.min(this.maxScroll, this.scrollOffset + 1);
681
+ }
682
+ else if (matchesKey(data, Key.pageUp)) {
683
+ this.scrollOffset = Math.max(0, this.scrollOffset - this.pageSize);
684
+ }
685
+ else if (matchesKey(data, Key.pageDown)) {
686
+ this.scrollOffset = Math.min(this.maxScroll, this.scrollOffset + this.pageSize);
687
+ }
688
+ else if (matchesKey(data, Key.enter)) {
689
+ this.onDone?.(this.selected);
690
+ }
691
+ else if (matchesKey(data, "y")) {
692
+ this.onDone?.("once");
693
+ }
694
+ else if (this.request.canAlwaysApprove && matchesKey(data, "a")) {
695
+ this.onDone?.("always");
696
+ }
697
+ else if (matchesKey(data, "n") || matchesKey(data, Key.escape)) {
698
+ this.onDone?.("deny");
699
+ }
700
+ }
701
+ handleMouse(event) {
702
+ if (event.action === "wheel_up" || event.action === "wheel_down") {
703
+ this.scrollOffset = Math.max(0, Math.min(this.maxScroll, this.scrollOffset + (event.action === "wheel_up" ? -1 : 1)));
704
+ return true;
705
+ }
706
+ if (event.button !== 0 && event.action !== "release")
707
+ return false;
708
+ const target = this.actionAt(event);
709
+ if (event.action === "press") {
710
+ this.pressed = target;
711
+ this.selected = target ?? "deny";
712
+ return Boolean(target);
713
+ }
714
+ if (event.action === "drag") {
715
+ return Boolean(this.pressed);
716
+ }
717
+ if (event.action === "release") {
718
+ const pressed = this.pressed;
719
+ this.pressed = null;
720
+ if (pressed && target === pressed)
721
+ this.onDone?.(pressed);
722
+ else
723
+ this.selected = "deny";
724
+ return Boolean(pressed || target);
725
+ }
726
+ return false;
727
+ }
728
+ actionAt(event) {
729
+ const margin = 1;
730
+ const availableWidth = Math.max(1, this.terminal.columns - margin * 2);
731
+ const availableHeight = Math.max(1, this.terminal.rows - margin * 2);
732
+ const overlayColumn = margin + Math.floor((availableWidth - this.renderedWidth) / 2);
733
+ const overlayRow = margin + Math.floor((availableHeight - this.renderedHeight) / 2);
734
+ const row = event.row - overlayRow;
735
+ const column = event.column - overlayColumn;
736
+ for (const [action, hitbox] of this.hitboxes) {
737
+ if (row === hitbox.row && column >= hitbox.range[0] && column < hitbox.range[1])
738
+ return action;
739
+ }
740
+ return null;
741
+ }
742
+ render(width) {
743
+ const contentWidth = Math.max(1, width - 4);
744
+ const risk = this.request.risk === "critical"
745
+ ? bold(red(`[ ${this.request.risk.toUpperCase()} ]`))
746
+ : bold(yellow(`[ ${this.request.risk.toUpperCase()} ]`));
747
+ const exactContent = [
748
+ `${risk} ${bold(displaySafe(this.request.tool))}`,
749
+ "",
750
+ bold(cyan("Action")),
751
+ ...wrap(this.request.summary, contentWidth),
752
+ "",
753
+ bold(cyan("Command / details")),
754
+ ...wrap(this.request.details, contentWidth).map(dim),
755
+ ...(this.request.canAlwaysApprove ? [
756
+ "",
757
+ ...wrap("Always approve remembers the displayed scope for this session, including agents and scheduled work.", contentWidth).map(yellow),
758
+ ] : []),
759
+ ];
760
+ const actionDefinitions = [
761
+ { action: "deny", label: "[ Deny ]" },
762
+ { action: "once", label: "[ Approve once ]" },
763
+ ...(this.request.canAlwaysApprove ? [{ action: "always", label: "[ Always approve ]" }] : []),
764
+ ];
765
+ const gap = 4;
766
+ const innerWidth = Math.max(1, width - 2);
767
+ const actionsWidth = actionDefinitions.reduce((total, item) => total + item.label.length, 0)
768
+ + gap * (actionDefinitions.length - 1);
769
+ const horizontalActions = innerWidth >= actionsWidth;
770
+ const styleAction = ({ action, label }) => {
771
+ if (this.selected !== action) {
772
+ if (action === "deny")
773
+ return red(label);
774
+ if (action === "always")
775
+ return yellow(label);
776
+ return green(label);
777
+ }
778
+ if (action === "deny")
779
+ return `\x1b[41;97;1m${label}\x1b[0m`;
780
+ if (action === "always")
781
+ return `\x1b[43;30;1m${label}\x1b[0m`;
782
+ return `\x1b[42;30;1m${label}\x1b[0m`;
783
+ };
784
+ const starts = new Map();
785
+ const actionLines = horizontalActions
786
+ ? (() => {
787
+ let column = Math.max(0, Math.floor((innerWidth - actionsWidth) / 2));
788
+ const parts = [" ".repeat(column)];
789
+ for (const item of actionDefinitions) {
790
+ starts.set(item.action, column);
791
+ parts.push(styleAction(item));
792
+ column += item.label.length;
793
+ if (item !== actionDefinitions.at(-1)) {
794
+ parts.push(" ".repeat(gap));
795
+ column += gap;
796
+ }
797
+ }
798
+ return [parts.join("")];
799
+ })()
800
+ : actionDefinitions.map((item) => {
801
+ const padding = Math.max(0, Math.floor((innerWidth - item.label.length) / 2));
802
+ starts.set(item.action, padding);
803
+ return `${" ".repeat(padding)}${styleAction(item)}`;
804
+ });
805
+ const maximumHeight = Math.max(9, Math.min(this.terminal.rows - 2, Math.floor(this.terminal.rows * 0.8)));
806
+ const showHint = maximumHeight - 4 - 1 - actionLines.length >= 2;
807
+ const fixedBodyLines = 1 + actionLines.length + (showHint ? 1 : 0);
808
+ this.pageSize = Math.max(1, maximumHeight - 4 - fixedBodyLines);
809
+ this.maxScroll = Math.max(0, exactContent.length - this.pageSize);
810
+ this.scrollOffset = Math.min(this.scrollOffset, this.maxScroll);
811
+ const visible = exactContent.slice(this.scrollOffset, this.scrollOffset + this.pageSize);
812
+ const scroll = this.maxScroll > 0
813
+ ? dim(` ${this.scrollOffset + 1}-${Math.min(exactContent.length, this.scrollOffset + this.pageSize)}/${exactContent.length} | scroll for more`)
814
+ : "";
815
+ const body = [
816
+ ...visible,
817
+ scroll,
818
+ ...actionLines,
819
+ ...(showHint ? [dim(` Click an action | Left/Right or Tab | Enter${this.request.canAlwaysApprove ? " | A always" : ""}`)] : []),
820
+ ];
821
+ const lines = renderFrame("Confirm command", body, width);
822
+ this.renderedWidth = width;
823
+ this.renderedHeight = lines.length;
824
+ const firstActionRow = 3 + visible.length + 1;
825
+ this.hitboxes.clear();
826
+ actionDefinitions.forEach((item, index) => {
827
+ const start = 1 + (starts.get(item.action) ?? 0);
828
+ this.hitboxes.set(item.action, {
829
+ row: firstActionRow + (horizontalActions ? 0 : index),
830
+ range: [start, start + item.label.length],
831
+ });
832
+ });
833
+ return lines;
834
+ }
835
+ }
836
+ class QuestionInputModal {
837
+ terminal;
838
+ question;
839
+ input = new Input();
840
+ onDone;
841
+ constructor(terminal, question) {
842
+ this.terminal = terminal;
843
+ this.question = question;
844
+ this.input.onSubmit = (answer) => this.onDone?.(answer);
845
+ this.input.onEscape = () => this.onDone?.("");
846
+ }
847
+ get focused() {
848
+ return this.input.focused;
849
+ }
850
+ set focused(value) {
851
+ this.input.focused = value;
852
+ }
853
+ invalidate() {
854
+ this.input.invalidate();
855
+ }
856
+ handleInput(data) {
857
+ this.input.handleInput(data);
858
+ }
859
+ render(width) {
860
+ const contentWidth = Math.max(1, width - 4);
861
+ const questionLines = wrap(this.question, contentWidth);
862
+ const maximumQuestionRows = Math.max(1, Math.floor(this.terminal.rows * 0.6) - 6);
863
+ const visibleQuestion = questionLines.slice(0, maximumQuestionRows);
864
+ if (questionLines.length > visibleQuestion.length)
865
+ visibleQuestion.push(dim("... question continues beyond this viewport"));
866
+ const inputLine = this.input.render(Math.max(1, contentWidth - 2))[0] ?? "";
867
+ return renderFrame("Question", [...visibleQuestion, "", `> ${inputLine}`], width);
868
+ }
869
+ }
870
+ function userText(event) {
871
+ if (!isRecord(event.payload) || !isRecord(event.payload.item))
872
+ return null;
873
+ const content = event.payload.item.content;
874
+ if (typeof content === "string")
875
+ return content;
876
+ if (!Array.isArray(content))
877
+ return null;
878
+ const parts = content.flatMap((part) => {
879
+ if (!isRecord(part) || part.type !== "input_text" || typeof part.text !== "string")
880
+ return [];
881
+ return [part.text];
882
+ });
883
+ return parts.length > 0 ? parts.join("\n") : null;
884
+ }
885
+ function assistantText(event) {
886
+ if (!isRecord(event.payload) || !isRecord(event.payload.response))
887
+ return null;
888
+ const output = event.payload.response.output;
889
+ if (!Array.isArray(output))
890
+ return null;
891
+ const messages = output.flatMap((item) => {
892
+ if (!isRecord(item) || item.type !== "message" || item.role !== "assistant" || !Array.isArray(item.content))
893
+ return [];
894
+ const parts = item.content.flatMap((part) => {
895
+ if (!isRecord(part) || part.type !== "output_text" || typeof part.text !== "string")
896
+ return [];
897
+ return [part.text];
898
+ });
899
+ return parts.length > 0 ? [parts.join("")] : [];
900
+ });
901
+ return messages.length > 0 ? messages.join("\n\n") : null;
902
+ }
903
+ function reasoningSummary(event) {
904
+ if (!isRecord(event.payload) || !isRecord(event.payload.response))
905
+ return null;
906
+ const output = event.payload.response.output;
907
+ if (!Array.isArray(output))
908
+ return null;
909
+ const summaries = output.flatMap((item) => {
910
+ if (!isRecord(item) || item.type !== "reasoning" || !Array.isArray(item.summary))
911
+ return [];
912
+ return item.summary.flatMap((part) => {
913
+ if (!isRecord(part) || part.type !== "summary_text" || typeof part.text !== "string")
914
+ return [];
915
+ return [part.text];
916
+ });
917
+ });
918
+ return summaries.length > 0 ? summaries.join("\n") : null;
919
+ }
920
+ function schedulerJobLine(job) {
921
+ const state = job.blockedReason ? `blocked: ${job.blockedReason}` : (job.enabled ? "enabled" : "disabled");
922
+ const action = job.kind === "reminder" ? job.message : job.kind === "session_prompt" ? job.prompt : job.command;
923
+ const due = job.nextDue === null ? "no next run" : new Date(job.nextDue).toISOString();
924
+ return `${job.id.slice(0, 8)} ${job.kind} ${state} | ${job.scheduleKind}:${job.schedule} | ${due} | ${oneLine(action ?? "")}`;
925
+ }
926
+ function schedulerJobDetails(job) {
927
+ const action = job.kind === "reminder" ? job.message : job.kind === "session_prompt" ? job.prompt : job.command;
928
+ return [
929
+ `ID: ${job.id}`,
930
+ `Kind: ${job.kind}`,
931
+ `State: ${job.blockedReason ?? (job.enabled ? "enabled" : "disabled")}`,
932
+ `Schedule: ${job.scheduleKind}:${job.schedule} (${job.timezone})`,
933
+ job.cwd ? `Working directory: ${job.cwd}` : "",
934
+ `Exact action:\n${action ?? ""}`,
935
+ ].filter(Boolean).join("\n");
936
+ }
937
+ export function parseSessionSchedule(argument) {
938
+ const trimmed = argument.trim();
939
+ if (trimmed.startsWith("once ")) {
940
+ const match = /^once\s+(\S+)\s+([\s\S]+)$/.exec(trimmed);
941
+ if (!match?.[1] || !match[2]?.trim())
942
+ throw new Error("Usage: /schedule once <ISO timestamp> <prompt>");
943
+ return { scheduleKind: "once", schedule: match[1], prompt: match[2].trim() };
944
+ }
945
+ if (trimmed.startsWith("cron ")) {
946
+ const rest = trimmed.slice(5).trim();
947
+ if (rest.startsWith('"') || rest.startsWith("'")) {
948
+ const quote = rest[0];
949
+ const end = rest.indexOf(quote, 1);
950
+ if (end < 0)
951
+ throw new Error("Cron expression is missing its closing quote");
952
+ const schedule = rest.slice(1, end).trim();
953
+ const prompt = rest.slice(end + 1).trim();
954
+ if (!schedule || !prompt)
955
+ throw new Error('Usage: /schedule cron "<five fields>" <prompt>');
956
+ return { scheduleKind: "cron", schedule, prompt };
957
+ }
958
+ const tokens = rest.split(/\s+/);
959
+ if (tokens.length < 6)
960
+ throw new Error('Usage: /schedule cron "<five fields>" <prompt>');
961
+ return { scheduleKind: "cron", schedule: tokens.slice(0, 5).join(" "), prompt: tokens.slice(5).join(" ") };
962
+ }
963
+ throw new Error('Usage: /schedule once <ISO> <prompt> or /schedule cron "<five fields>" <prompt>');
964
+ }
965
+ export function parseTerminalMouse(data) {
966
+ const match = /^\x1b\[<(\d+);(\d+);(\d+)([mM])$/.exec(data);
967
+ if (!match?.[1] || !match[2] || !match[3] || !match[4])
968
+ return null;
969
+ const code = Number(match[1]);
970
+ const column = Math.max(0, Number(match[2]) - 1);
971
+ const row = Math.max(0, Number(match[3]) - 1);
972
+ const button = code & 3;
973
+ const motion = (code & 32) !== 0;
974
+ const wheel = (code & 64) !== 0;
975
+ const action = wheel
976
+ ? ["wheel_up", "wheel_down", "wheel_left", "wheel_right"][button]
977
+ : match[4] === "m" || button === 3
978
+ ? "release"
979
+ : motion
980
+ ? "drag"
981
+ : "press";
982
+ return {
983
+ action,
984
+ button,
985
+ column,
986
+ row,
987
+ shift: (code & 4) !== 0,
988
+ alt: (code & 8) !== 0,
989
+ ctrl: (code & 16) !== 0,
990
+ };
991
+ }
992
+ export function mouseWheelDelta(data) {
993
+ const mouse = parseTerminalMouse(data);
994
+ if (mouse?.action === "wheel_up")
995
+ return 3;
996
+ if (mouse?.action === "wheel_down")
997
+ return -3;
998
+ return 0;
999
+ }
1000
+ export function inboxLine(item, job, sessionTitle) {
1001
+ const source = job?.kind === "session_prompt"
1002
+ ? `session:${oneLine(sessionTitle ?? job.sessionId ?? "unknown", 60)}`
1003
+ : job?.kind ?? item.kind;
1004
+ return `#${item.id} ${source} ${item.occurrence.state} | ${new Date(item.createdAt).toISOString()} | ${oneLine(item.message)}`;
1005
+ }
1006
+ export function shouldAutoDisplayInbox(job, currentSessionId) {
1007
+ return job?.kind !== "session_prompt" || job.sessionId === currentSessionId;
1008
+ }
1009
+ export function markInboxItemsRead(items, acknowledge) {
1010
+ let count = 0;
1011
+ for (const item of items)
1012
+ if (acknowledge(item.id))
1013
+ count += 1;
1014
+ return count;
1015
+ }
1016
+ function inboxIdentity(item) {
1017
+ return `${item.jobId}:${item.occurrenceId}`;
1018
+ }
1019
+ export async function initialTuiSession(app, initialSessionId) {
1020
+ return initialSessionId
1021
+ ? app.currentOrNewSession(initialSessionId)
1022
+ : app.createSession();
1023
+ }
1024
+ /** Run the interactive Looking Glass chat interface until the user exits. */
1025
+ export async function runTui(app, initialSessionId) {
1026
+ if (!stdin.isTTY || !stdout.isTTY)
1027
+ throw new Error("Interactive TUI requires a TTY");
1028
+ let session = await initialTuiSession(app, initialSessionId);
1029
+ const terminal = new AlternateScreenTerminal();
1030
+ const tui = new TUI(terminal, false);
1031
+ const toolCards = new Map();
1032
+ const streaming = new Set();
1033
+ const streamingReasoning = new Set();
1034
+ const seenInbox = new Set();
1035
+ const suppressedInbox = new Map();
1036
+ const pendingTasks = new Set();
1037
+ let activeModal = null;
1038
+ let activeController = null;
1039
+ let engineStatus = "Ready";
1040
+ let activeOperation = false;
1041
+ let submissionBusy = false;
1042
+ let stopping = false;
1043
+ let pollErrorShown = false;
1044
+ let pollTimer = null;
1045
+ let activityTimer = null;
1046
+ let activityFrame = 0;
1047
+ let selectionDragging = false;
1048
+ let contextInputTokens = 0;
1049
+ let contextWindow = 0;
1050
+ let unavailableModelKey = null;
1051
+ let nextModelAvailabilityCheckAt = 0;
1052
+ let resolveStopped = null;
1053
+ const stopped = new Promise((resolve) => {
1054
+ resolveStopped = resolve;
1055
+ });
1056
+ const activityText = () => activityLine(activeOperation, engineStatus, activityFrame, root.scrollOffset);
1057
+ const metadataText = (width) => sessionMetadataLine(session, session.approvalMode, contextUsageLabel(contextInputTokens, contextWindow), width);
1058
+ const editor = new TurnEditor(tui, editorTheme, { paddingX: 1, autocompleteMaxVisible: 7 });
1059
+ const root = new FullHeightRoot(terminal, editor, activityText, metadataText);
1060
+ tui.addChild(root);
1061
+ tui.setFocus(editor);
1062
+ tui.setClearOnShrink(true);
1063
+ const requestRender = () => {
1064
+ if (!stopping)
1065
+ tui.requestRender();
1066
+ };
1067
+ const requestTranscriptRender = () => {
1068
+ root.invalidateTranscript();
1069
+ requestRender();
1070
+ };
1071
+ const refreshContextWindow = async () => {
1072
+ const sessionId = session.id;
1073
+ const provider = session.provider;
1074
+ const modelId = session.model;
1075
+ try {
1076
+ const model = await app.catalogModel(modelId, provider);
1077
+ if (session.id === sessionId && session.provider === provider && session.model === modelId) {
1078
+ contextWindow = model.contextWindow;
1079
+ if (unavailableModelKey === `${provider}:${modelId}`) {
1080
+ addNotice("model available", `${provider}:${modelId} is available again.`, green);
1081
+ }
1082
+ unavailableModelKey = null;
1083
+ nextModelAvailabilityCheckAt = 0;
1084
+ requestRender();
1085
+ }
1086
+ }
1087
+ catch (error) {
1088
+ if (session.id === sessionId && session.provider === provider && session.model === modelId) {
1089
+ contextWindow = 0;
1090
+ const key = `${provider}:${modelId}`;
1091
+ if (unavailableModelKey !== key) {
1092
+ addNotice("model unavailable", `${key} could not be reached: ${errorMessage(error)}. Input and session commands remain available; use /model to switch models.`, yellow);
1093
+ unavailableModelKey = key;
1094
+ }
1095
+ nextModelAvailabilityCheckAt = Date.now() + MODEL_AVAILABILITY_RETRY_MS;
1096
+ requestRender();
1097
+ }
1098
+ }
1099
+ };
1100
+ const refreshContextUsage = () => {
1101
+ const latestUsage = app.sessions.latestResponseUsage(session.id);
1102
+ const checkpoint = app.sessions.latestCheckpoint(session.id);
1103
+ if (latestUsage && (!checkpoint || latestUsage.sequence > checkpoint.throughSequence)) {
1104
+ contextInputTokens = latestUsage.inputTokens;
1105
+ return;
1106
+ }
1107
+ contextInputTokens = Math.ceil(JSON.stringify(projectContext(app.sessions, session.id).input).length / 4);
1108
+ };
1109
+ const add = (component) => {
1110
+ root.addEntry(component);
1111
+ requestRender();
1112
+ };
1113
+ const addNotice = (label, text, color) => {
1114
+ add(new Notice(label, text, color));
1115
+ };
1116
+ const schedulerInboxLine = (item) => {
1117
+ const job = app.scheduler.getJob(item.jobId);
1118
+ const sessionTitle = job?.sessionId ? app.sessions.get(job.sessionId)?.title : undefined;
1119
+ return inboxLine(item, job, sessionTitle);
1120
+ };
1121
+ const refreshSession = () => {
1122
+ session = app.sessions.get(session.id) ?? session;
1123
+ };
1124
+ const restoreEditor = () => {
1125
+ if (stopping)
1126
+ return;
1127
+ tui.setFocus(editor);
1128
+ requestRender();
1129
+ };
1130
+ const settleModal = (handle, modalToken, resolve, value) => {
1131
+ if (activeModal !== modalToken)
1132
+ return;
1133
+ activeModal = null;
1134
+ handle.hide();
1135
+ restoreEditor();
1136
+ resolve(value);
1137
+ };
1138
+ const selectValue = (title, items, prompt, options = {}) => {
1139
+ if (stopping || items.length === 0)
1140
+ return Promise.resolve(null);
1141
+ return new Promise((resolve) => {
1142
+ let settled = false;
1143
+ const modal = new SelectorModal(title, items, Math.max(3, Math.min(10, Math.floor(terminal.rows * 0.8) - 6)), prompt);
1144
+ const handle = tui.showOverlay(modal, { width: "80%", maxHeight: "80%", margin: 1 });
1145
+ const finish = (value) => {
1146
+ if (settled)
1147
+ return;
1148
+ settled = true;
1149
+ settleModal(handle, token, resolve, value);
1150
+ };
1151
+ const token = { cancel: () => finish(null) };
1152
+ activeModal = token;
1153
+ modal.onSelect = (value) => finish(value);
1154
+ if (options.deletable)
1155
+ modal.onDelete = (value) => finish(`${DELETE_SELECTION_PREFIX}${value}`);
1156
+ modal.onCancel = () => finish(null);
1157
+ });
1158
+ };
1159
+ const interaction = {
1160
+ approve(request) {
1161
+ if (stopping)
1162
+ return Promise.resolve("deny");
1163
+ return new Promise((resolve) => {
1164
+ let settled = false;
1165
+ const modal = new ApprovalModal(terminal, request);
1166
+ const handle = tui.showOverlay(modal, { width: "85%", maxHeight: "80%", margin: 1 });
1167
+ const finish = (decision) => {
1168
+ if (settled)
1169
+ return;
1170
+ settled = true;
1171
+ settleModal(handle, token, resolve, decision);
1172
+ };
1173
+ const token = {
1174
+ cancel: () => finish("deny"),
1175
+ mouse: (event) => modal.handleMouse(event),
1176
+ };
1177
+ activeModal = token;
1178
+ modal.onDone = finish;
1179
+ });
1180
+ },
1181
+ ask(request) {
1182
+ if (stopping)
1183
+ return Promise.resolve("");
1184
+ if (request.options && request.options.length > 0) {
1185
+ return selectValue("Question", request.options.map((option) => ({ value: option, label: displaySafe(option) })), request.question).then((answer) => answer ?? "");
1186
+ }
1187
+ return new Promise((resolve) => {
1188
+ let settled = false;
1189
+ const modal = new QuestionInputModal(terminal, request.question);
1190
+ const handle = tui.showOverlay(modal, { width: "80%", maxHeight: "80%", margin: 1 });
1191
+ const finish = (answer) => {
1192
+ if (settled)
1193
+ return;
1194
+ settled = true;
1195
+ settleModal(handle, token, resolve, answer);
1196
+ };
1197
+ const token = { cancel: () => finish("") };
1198
+ activeModal = token;
1199
+ modal.onDone = finish;
1200
+ });
1201
+ },
1202
+ };
1203
+ const loadSessionEvents = () => {
1204
+ root.clearTranscript();
1205
+ toolCards.clear();
1206
+ for (const event of app.sessions.events(session.id)) {
1207
+ if (event.kind === "user") {
1208
+ const text = userText(event);
1209
+ if (text)
1210
+ root.addEntry(new UserMessage(text));
1211
+ continue;
1212
+ }
1213
+ if (event.kind === "response") {
1214
+ const summary = reasoningSummary(event);
1215
+ if (session.showReasoning && summary)
1216
+ root.addEntry(new ReasoningSummary(summary));
1217
+ const text = assistantText(event);
1218
+ if (text)
1219
+ root.addEntry(new AssistantMessage(text));
1220
+ continue;
1221
+ }
1222
+ if (event.kind === "tool_started" && isRecord(event.payload)) {
1223
+ const callId = typeof event.payload.callId === "string" ? event.payload.callId : `event-${event.sequence}`;
1224
+ const name = typeof event.payload.name === "string" ? event.payload.name : "tool";
1225
+ const summary = "arguments" in event.payload ? jsonPreview(event.payload.arguments) : name;
1226
+ const card = new ToolCard(callId, name, summary);
1227
+ toolCards.set(callId, card);
1228
+ root.addEntry(card);
1229
+ continue;
1230
+ }
1231
+ if ((event.kind === "tool_result" || event.kind === "tool_denied") && isRecord(event.payload)) {
1232
+ const callId = typeof event.payload.callId === "string" ? event.payload.callId : `event-${event.sequence}`;
1233
+ const name = typeof event.payload.name === "string" ? event.payload.name : "tool";
1234
+ const output = typeof event.payload.output === "string" ? event.payload.output : "No recorded output";
1235
+ let card = toolCards.get(callId);
1236
+ if (!card) {
1237
+ card = new ToolCard(callId, name, name);
1238
+ toolCards.set(callId, card);
1239
+ root.addEntry(card);
1240
+ }
1241
+ card.finish(output, event.kind === "tool_denied" || /^Tool (?:argument )?error:/i.test(output));
1242
+ continue;
1243
+ }
1244
+ if (event.kind === "error" && isRecord(event.payload) && typeof event.payload.message === "string") {
1245
+ root.addEntry(new Notice("error", event.payload.message, red));
1246
+ }
1247
+ }
1248
+ refreshContextUsage();
1249
+ requestRender();
1250
+ };
1251
+ const callbacks = () => {
1252
+ let currentAssistant = null;
1253
+ let currentReasoning = null;
1254
+ return {
1255
+ onResponseStart(round) {
1256
+ currentAssistant?.flush();
1257
+ currentReasoning?.flush();
1258
+ currentReasoning = new StreamingReasoning(requestTranscriptRender);
1259
+ streamingReasoning.add(currentReasoning);
1260
+ if (session.showReasoning)
1261
+ add(currentReasoning);
1262
+ currentAssistant = new StreamingAssistant(requestTranscriptRender);
1263
+ streaming.add(currentAssistant);
1264
+ add(currentAssistant);
1265
+ if (round > 0)
1266
+ engineStatus = `Response round ${round + 1}`;
1267
+ },
1268
+ onTextDelta(delta) {
1269
+ if (!currentAssistant) {
1270
+ currentAssistant = new StreamingAssistant(requestTranscriptRender);
1271
+ streaming.add(currentAssistant);
1272
+ add(currentAssistant);
1273
+ }
1274
+ currentAssistant.append(delta);
1275
+ },
1276
+ onReasoningDelta(delta) {
1277
+ engineStatus = "Reasoning";
1278
+ currentReasoning?.append(delta);
1279
+ },
1280
+ onStatus(status) {
1281
+ engineStatus = displaySafe(status);
1282
+ requestRender();
1283
+ },
1284
+ onWarning(message) {
1285
+ addNotice("warning", message, yellow);
1286
+ },
1287
+ onToolStart(notice) {
1288
+ currentAssistant?.flush();
1289
+ const card = new ToolCard(notice.callId, notice.name, notice.summary);
1290
+ toolCards.set(notice.callId, card);
1291
+ add(card);
1292
+ },
1293
+ onToolProgress(notice) {
1294
+ const card = toolCards.get(notice.callId);
1295
+ if (card && notice.output)
1296
+ card.progress(notice.output);
1297
+ requestTranscriptRender();
1298
+ },
1299
+ onToolFinish(notice) {
1300
+ let card = toolCards.get(notice.callId);
1301
+ if (!card) {
1302
+ card = new ToolCard(notice.callId, notice.name, notice.summary);
1303
+ toolCards.set(notice.callId, card);
1304
+ add(card);
1305
+ }
1306
+ card.finish(notice.output ?? "No output", notice.failed ?? false);
1307
+ requestTranscriptRender();
1308
+ },
1309
+ };
1310
+ };
1311
+ const beginOperation = (status) => {
1312
+ activeOperation = true;
1313
+ activeController = new AbortController();
1314
+ engineStatus = status;
1315
+ editor.setEnabled(false);
1316
+ tui.setFocus(null);
1317
+ requestRender();
1318
+ return activeController;
1319
+ };
1320
+ const endOperation = () => {
1321
+ activeController = null;
1322
+ activeOperation = false;
1323
+ engineStatus = "Ready";
1324
+ editor.setEnabled(true);
1325
+ refreshSession();
1326
+ restoreEditor();
1327
+ };
1328
+ const runTurn = async (text) => {
1329
+ const controller = beginOperation("Thinking");
1330
+ try {
1331
+ const model = await app.catalogModel(session.model, session.provider, controller.signal);
1332
+ contextWindow = model.contextWindow;
1333
+ if (controller.signal.aborted || stopping)
1334
+ return;
1335
+ add(new UserMessage(text));
1336
+ const turn = app.engine.turn(session.id, text, {
1337
+ signal: controller.signal,
1338
+ interaction,
1339
+ callbacks: callbacks(),
1340
+ modelInfo: model,
1341
+ });
1342
+ refreshSession();
1343
+ requestRender();
1344
+ await turn;
1345
+ refreshContextUsage();
1346
+ }
1347
+ catch (error) {
1348
+ if (controller.signal.aborted)
1349
+ addNotice("cancelled", "Active operation cancelled.", yellow);
1350
+ else
1351
+ addNotice("error", errorMessage(error), red);
1352
+ }
1353
+ finally {
1354
+ for (const assistant of streaming)
1355
+ assistant.flush();
1356
+ for (const reasoning of streamingReasoning)
1357
+ reasoning.flush();
1358
+ streaming.clear();
1359
+ streamingReasoning.clear();
1360
+ refreshContextUsage();
1361
+ endOperation();
1362
+ }
1363
+ };
1364
+ const runCompact = async () => {
1365
+ const controller = beginOperation("Compacting context");
1366
+ try {
1367
+ await app.engine.compactNow(session.id, { signal: controller.signal, callbacks: callbacks() });
1368
+ refreshContextUsage();
1369
+ if (!controller.signal.aborted)
1370
+ addNotice("compact", "Conversation context compacted.", green);
1371
+ }
1372
+ catch (error) {
1373
+ if (controller.signal.aborted)
1374
+ addNotice("cancelled", "Compaction cancelled.", yellow);
1375
+ else
1376
+ addNotice("error", errorMessage(error), red);
1377
+ }
1378
+ finally {
1379
+ endOperation();
1380
+ }
1381
+ };
1382
+ const switchSession = async (id) => {
1383
+ const previousSessionId = session.id;
1384
+ session = await app.currentOrNewSession(id);
1385
+ if (previousSessionId !== session.id)
1386
+ app.sessions.deleteIfEmpty(previousSessionId);
1387
+ for (const [identity, ownerSessionId] of suppressedInbox) {
1388
+ if (ownerSessionId === session.id)
1389
+ suppressedInbox.delete(identity);
1390
+ }
1391
+ contextWindow = 0;
1392
+ unavailableModelKey = null;
1393
+ nextModelAvailabilityCheckAt = 0;
1394
+ loadSessionEvents();
1395
+ trackTask(refreshContextWindow());
1396
+ };
1397
+ const setModel = (model) => {
1398
+ const effort = model.reasoningEfforts.includes(session.reasoningEffort)
1399
+ ? session.reasoningEffort
1400
+ : (model.reasoningEfforts.includes(model.defaultReasoningEffort)
1401
+ ? model.defaultReasoningEffort
1402
+ : model.reasoningEfforts[0]);
1403
+ if (!effort)
1404
+ throw new Error(`Model ${model.id} does not advertise a reasoning effort`);
1405
+ session = app.sessions.updateSettings(session.id, {
1406
+ provider: model.provider,
1407
+ model: model.id,
1408
+ reasoningEffort: effort,
1409
+ fast: session.fast && model.supportsFast,
1410
+ });
1411
+ contextWindow = model.contextWindow;
1412
+ unavailableModelKey = null;
1413
+ nextModelAvailabilityCheckAt = 0;
1414
+ refreshContextUsage();
1415
+ addNotice("model", `Using ${model.name} (${model.provider}:${model.id}); response anchor reset.`, cyan);
1416
+ };
1417
+ const setAgentModel = (model) => {
1418
+ const effort = model.reasoningEfforts.includes(session.agentReasoningEffort)
1419
+ ? session.agentReasoningEffort
1420
+ : (model.reasoningEfforts.includes(model.defaultReasoningEffort)
1421
+ ? model.defaultReasoningEffort
1422
+ : model.reasoningEfforts[0]);
1423
+ if (!effort)
1424
+ throw new Error(`Model ${model.id} does not advertise a reasoning effort`);
1425
+ session = app.sessions.updateSettings(session.id, {
1426
+ agentProvider: model.provider,
1427
+ agentModel: model.id,
1428
+ agentReasoningEffort: effort,
1429
+ });
1430
+ addNotice("agent model", `Agents will use ${model.name} (${model.provider}:${model.id}) with ${effort} reasoning.`, cyan);
1431
+ };
1432
+ const showInbox = () => {
1433
+ const inbox = app.scheduler.listInbox({ unackedOnly: true, limit: 10_000 });
1434
+ if (inbox.length === 0) {
1435
+ addNotice("inbox", "No unread scheduler items.", cyan);
1436
+ return;
1437
+ }
1438
+ if (inbox.some((item) => {
1439
+ const job = app.scheduler.getJob(item.jobId);
1440
+ return job?.kind === "session_prompt" && job.sessionId === session.id;
1441
+ }))
1442
+ loadSessionEvents();
1443
+ addNotice("inbox", `${inbox.length} scheduler item${inbox.length === 1 ? "" : "s"}:`, cyan);
1444
+ for (const item of inbox.slice().reverse()) {
1445
+ const identity = inboxIdentity(item);
1446
+ seenInbox.add(identity);
1447
+ suppressedInbox.delete(identity);
1448
+ addNotice("inbox", schedulerInboxLine(item), magenta);
1449
+ }
1450
+ const marked = markInboxItemsRead(inbox, (id) => app.scheduler.ackInbox(id));
1451
+ addNotice("inbox", `${marked} item${marked === 1 ? "" : "s"} marked read.`, green);
1452
+ };
1453
+ const setPersistence = (enabled) => {
1454
+ session = app.sessions.updateSettings(session.id, { persistent: enabled });
1455
+ const jobs = app.scheduler.setSessionJobsEnabled(session.id, enabled);
1456
+ addNotice("session", `Persistence ${enabled ? "enabled" : "disabled"}; ${jobs.length} session schedule${jobs.length === 1 ? "" : "s"} ${enabled ? "enabled where resumable" : "paused"}.`, enabled ? green : yellow);
1457
+ };
1458
+ const createSessionSchedule = async (argument) => {
1459
+ if (!argument) {
1460
+ addNotice("schedule", [
1461
+ "Create a scheduled AI turn in this session:",
1462
+ "/schedule once 2026-07-20T12:00:00Z Check system inventory",
1463
+ '/schedule cron "0 9 * * *" Review service health and act on problems',
1464
+ "The session must be persistent. Scheduled AI turns inherit this session's permissions and cannot request interactive approval.",
1465
+ ].join("\n"), cyan);
1466
+ return;
1467
+ }
1468
+ const parsed = parseSessionSchedule(argument);
1469
+ initialDue(parsed.scheduleKind, parsed.schedule, app.config.scheduler.timezone, Date.now());
1470
+ if (!session.persistent) {
1471
+ const enable = await selectValue("Enable persistent session", [
1472
+ { value: "yes", label: "Enable persistence", description: "Allow scheduled model/tool turns" },
1473
+ { value: "no", label: "Cancel" },
1474
+ ], "Scheduled prompts require an explicitly persistent session.");
1475
+ if (enable !== "yes" || stopping)
1476
+ return;
1477
+ setPersistence(true);
1478
+ }
1479
+ const job = app.scheduler.createSessionPrompt({
1480
+ sessionId: session.id,
1481
+ prompt: parsed.prompt,
1482
+ scheduleKind: parsed.scheduleKind,
1483
+ schedule: parsed.schedule,
1484
+ timezone: app.config.scheduler.timezone,
1485
+ startGraceMs: app.config.scheduler.commandStartGraceMs,
1486
+ outputBytes: app.config.scheduler.commandOutputBytes,
1487
+ });
1488
+ addNotice("schedule", `Created ${schedulerJobLine(job)}`, green);
1489
+ };
1490
+ const showCronBrowser = async (sessionOnly = false) => {
1491
+ const jobs = sessionOnly
1492
+ ? app.scheduler.listJobsForSession(session.id)
1493
+ : app.scheduler.listJobs().filter((job) => {
1494
+ if (job.kind === "reminder")
1495
+ return true;
1496
+ if (job.kind === "session_prompt")
1497
+ return job.sessionId === session.id;
1498
+ return job.cwd !== null && isWithin(app.workspace, job.cwd);
1499
+ });
1500
+ if (jobs.length === 0) {
1501
+ addNotice("cron", sessionOnly
1502
+ ? "This session has no schedules. Use /schedule to create one."
1503
+ : "No scheduled jobs. Use /schedule for session prompts or ask Looking Glass to create a reminder/command.", cyan);
1504
+ return;
1505
+ }
1506
+ const selectedId = await selectValue(sessionOnly ? "Session schedules" : "Schedules", jobs.map((job) => ({
1507
+ value: job.id,
1508
+ label: `${job.enabled ? "[on]" : "[off]"} ${job.kind} ${oneLine(job.prompt ?? job.message ?? job.command ?? "", 48)}`,
1509
+ description: `${job.scheduleKind}:${job.schedule} | ${job.nextDue ? new Date(job.nextDue).toISOString() : "no next run"}`,
1510
+ })), "Select a schedule to manage it.");
1511
+ if (!selectedId || stopping)
1512
+ return;
1513
+ const job = app.scheduler.getJob(selectedId);
1514
+ if (!job)
1515
+ throw new Error(`Schedule not found: ${selectedId}`);
1516
+ const actions = [
1517
+ job.enabled
1518
+ ? { value: "pause", label: "Pause", description: "Disable future runs" }
1519
+ : { value: "resume", label: "Resume", description: "Enable future runs" },
1520
+ ...(job.enabled ? [{ value: "run", label: "Run now", description: "Queue one immediate occurrence" }] : []),
1521
+ ...(job.blockedReason?.includes("unknown outcome")
1522
+ ? [{ value: "resolve", label: "Resolve unknown", description: "Acknowledge uncertain prior outcome" }]
1523
+ : []),
1524
+ { value: "delete", label: "Delete", description: "Disable and hide this schedule" },
1525
+ ];
1526
+ const action = await selectValue("Schedule action", actions, schedulerJobLine(job));
1527
+ if (!action || stopping)
1528
+ return;
1529
+ if (action === "run" || action === "resume" || action === "resolve") {
1530
+ const title = action === "run" ? "Run schedule now" : action === "resume" ? "Resume schedule" : "Resolve unknown outcome";
1531
+ const label = action === "run" ? "Run exact action" : action === "resume" ? "Resume exact schedule" : "Resolve and resume";
1532
+ const confirmed = await selectValue(title, [
1533
+ { value: "yes", label },
1534
+ { value: "no", label: "Cancel" },
1535
+ ], schedulerJobDetails(job));
1536
+ if (confirmed !== "yes")
1537
+ return;
1538
+ }
1539
+ let updated = null;
1540
+ if (action === "pause")
1541
+ updated = app.scheduler.pauseJob(job.id);
1542
+ else if (action === "resume")
1543
+ updated = app.scheduler.resumeJob(job.id);
1544
+ else if (action === "resolve")
1545
+ updated = app.scheduler.acknowledgeUnknown(job.id);
1546
+ else if (action === "run") {
1547
+ const run = app.scheduler.runNow(job.id);
1548
+ addNotice("cron", `Queued occurrence ${run.id} (${run.state}).`, green);
1549
+ return;
1550
+ }
1551
+ else if (action === "delete") {
1552
+ const confirmed = await selectValue("Delete schedule", [
1553
+ { value: "yes", label: "Delete schedule" },
1554
+ { value: "no", label: "Cancel" },
1555
+ ], schedulerJobDetails(job));
1556
+ if (confirmed !== "yes")
1557
+ return;
1558
+ updated = app.scheduler.deleteJob(job.id);
1559
+ }
1560
+ if (updated)
1561
+ addNotice("cron", schedulerJobLine(updated), action === "delete" ? yellow : green);
1562
+ };
1563
+ const showCommandApprovals = async () => {
1564
+ while (!stopping) {
1565
+ const approvals = app.sessions.listCommandApprovals(session.id);
1566
+ if (approvals.length === 0) {
1567
+ addNotice("approvals", "No actions are always approved for this session.", cyan);
1568
+ return;
1569
+ }
1570
+ const selected = await selectValue("Always-approved actions", approvals.map((approval) => {
1571
+ let command = approval.signature;
1572
+ let detail = new Date(approval.approvedAt).toISOString();
1573
+ try {
1574
+ const parsed = JSON.parse(approval.signature);
1575
+ if (Array.isArray(parsed) && parsed[0] === "bash-exec" && typeof parsed[2] === "string") {
1576
+ command = parsed[2];
1577
+ detail = `${String(parsed[3])} | ${String(parsed[4])}ms | ${detail}`;
1578
+ }
1579
+ else if (Array.isArray(parsed) && parsed[0] === "tool-action" && typeof parsed[2] === "string") {
1580
+ const args = parsed[3];
1581
+ command = parsed[2] === "bash" && typeof args?.command === "string"
1582
+ ? args.command
1583
+ : `${parsed[2]}: ${JSON.stringify(args ?? {})}`;
1584
+ }
1585
+ }
1586
+ catch {
1587
+ // Retain the raw signature for forward-compatible records.
1588
+ }
1589
+ return {
1590
+ value: approval.signature,
1591
+ label: oneLine(command, 240),
1592
+ description: detail,
1593
+ };
1594
+ }), "Delete/Supr revokes the highlighted action approval.", { deletable: true });
1595
+ if (!selected || stopping)
1596
+ return;
1597
+ if (!selected.startsWith(DELETE_SELECTION_PREFIX))
1598
+ return;
1599
+ const signature = selected.slice(DELETE_SELECTION_PREFIX.length);
1600
+ if (app.sessions.revokeCommandApproval(session.id, signature)) {
1601
+ addNotice("approvals", "Always approval revoked for the selected action.", yellow);
1602
+ }
1603
+ }
1604
+ };
1605
+ const showSessionMenu = async () => {
1606
+ const jobs = app.scheduler.listJobsForSession(session.id);
1607
+ const approvals = app.sessions.listCommandApprovals(session.id);
1608
+ const action = await selectValue("Session", [
1609
+ {
1610
+ value: "persist",
1611
+ label: session.persistent ? "Disable persistence" : "Enable persistence",
1612
+ description: `${jobs.length} schedule${jobs.length === 1 ? "" : "s"}`,
1613
+ },
1614
+ { value: "rename", label: "Rename session", description: session.title },
1615
+ { value: "schedules", label: "Manage schedules", description: `${jobs.length} attached` },
1616
+ { value: "approvals", label: "Manage always approvals", description: `${approvals.length} registered` },
1617
+ {
1618
+ value: "thinking",
1619
+ label: session.showReasoning ? "Hide reasoning summaries" : "Show reasoning summaries",
1620
+ },
1621
+ ], `${session.id}\n${session.model}`);
1622
+ if (!action || stopping)
1623
+ return;
1624
+ if (action === "persist")
1625
+ setPersistence(!session.persistent);
1626
+ else if (action === "schedules")
1627
+ await showCronBrowser(true);
1628
+ else if (action === "approvals")
1629
+ await showCommandApprovals();
1630
+ else if (action === "thinking") {
1631
+ session = app.sessions.updateSettings(session.id, { showReasoning: !session.showReasoning });
1632
+ loadSessionEvents();
1633
+ }
1634
+ else if (action === "rename") {
1635
+ const title = await interaction.ask({ question: "New session title" });
1636
+ if (title.trim()) {
1637
+ session = app.sessions.rename(session.id, title);
1638
+ addNotice("session", `Renamed to ${session.title}.`, green);
1639
+ }
1640
+ }
1641
+ };
1642
+ const showSessionBrowser = async () => {
1643
+ while (!stopping) {
1644
+ const sessions = app.sessions.listWithMessages(app.workspace, 100)
1645
+ .filter((item) => app.hasProvider(item.provider));
1646
+ const selected = await selectValue("Sessions", sessions.map((item) => ({
1647
+ value: item.id,
1648
+ label: `${item.persistent ? "[persistent] " : ""}${item.title}`,
1649
+ description: `${app.scheduler.listJobsForSession(item.id).length} schedules | ${item.model} | ${new Date(item.updatedAt).toISOString()} | ${item.id.slice(0, 8)}`,
1650
+ })), "Enter switches session. Delete/Supr removes the highlighted session.", { deletable: true });
1651
+ if (!selected || stopping)
1652
+ return;
1653
+ if (!selected.startsWith(DELETE_SELECTION_PREFIX)) {
1654
+ await switchSession(selected);
1655
+ return;
1656
+ }
1657
+ const id = selected.slice(DELETE_SELECTION_PREFIX.length);
1658
+ const target = app.sessions.get(id);
1659
+ if (!target) {
1660
+ addNotice("session", "The selected session no longer exists.", yellow);
1661
+ continue;
1662
+ }
1663
+ const summary = app.sessions.deletionSummary(id);
1664
+ const attached = [
1665
+ `${summary.events} transcript event${summary.events === 1 ? "" : "s"}`,
1666
+ `${summary.toolCalls} tool call${summary.toolCalls === 1 ? "" : "s"}`,
1667
+ `${summary.checkpoints} checkpoint${summary.checkpoints === 1 ? "" : "s"}`,
1668
+ `${summary.schedules} schedule${summary.schedules === 1 ? "" : "s"}`,
1669
+ `${summary.occurrences} scheduled occurrence${summary.occurrences === 1 ? "" : "s"}`,
1670
+ `${summary.inboxItems} inbox item${summary.inboxItems === 1 ? "" : "s"}`,
1671
+ `${summary.commandApprovals} always approval${summary.commandApprovals === 1 ? "" : "s"}`,
1672
+ ].join(" | ");
1673
+ const confirmed = await selectValue("Delete session", [
1674
+ { value: "yes", label: "Delete permanently", description: "This cannot be undone" },
1675
+ { value: "no", label: "Cancel" },
1676
+ ], [
1677
+ target.title,
1678
+ target.id,
1679
+ attached,
1680
+ `${summary.retainedArtifacts} artifact file${summary.retainedArtifacts === 1 ? "" : "s"} will be retained without the session link.`,
1681
+ ].join("\n"));
1682
+ if (confirmed !== "yes" || stopping)
1683
+ continue;
1684
+ const deletingCurrent = target.id === session.id;
1685
+ let deleted = summary;
1686
+ try {
1687
+ deleted = app.sessions.delete(target.id, summary);
1688
+ }
1689
+ catch (error) {
1690
+ if (errorMessage(error).includes("changed after deletion was confirmed")) {
1691
+ addNotice("session", errorMessage(error), yellow);
1692
+ continue;
1693
+ }
1694
+ throw error;
1695
+ }
1696
+ if (deletingCurrent) {
1697
+ const replacement = app.sessions.create({
1698
+ workspace: target.workspace,
1699
+ provider: target.provider,
1700
+ model: target.model,
1701
+ reasoningEffort: target.reasoningEffort,
1702
+ agentProvider: target.agentProvider,
1703
+ agentModel: target.agentModel,
1704
+ agentReasoningEffort: target.agentReasoningEffort,
1705
+ verbosity: target.verbosity,
1706
+ fast: target.fast,
1707
+ approvalMode: target.approvalMode,
1708
+ });
1709
+ await switchSession(replacement.id);
1710
+ }
1711
+ addNotice("session", `Deleted ${target.title}: ${deleted.events} events, ${deleted.schedules} schedules, and ${deleted.commandApprovals} always approvals removed; ${deleted.retainedArtifacts} artifacts retained.`, yellow);
1712
+ if (deletingCurrent)
1713
+ return;
1714
+ }
1715
+ };
1716
+ const handleCommand = async (input, signal) => {
1717
+ const match = /^\/(\S+)(?:\s+([\s\S]*))?$/.exec(input.trim());
1718
+ if (!match)
1719
+ throw new Error("Invalid slash command");
1720
+ const command = match[1]?.toLowerCase() ?? "";
1721
+ const argument = match[2]?.trim() ?? "";
1722
+ if (command === "exit") {
1723
+ requestStop();
1724
+ return;
1725
+ }
1726
+ if (command === "new") {
1727
+ const created = await app.createSession(signal);
1728
+ if (!stopping)
1729
+ await switchSession(created.id);
1730
+ return;
1731
+ }
1732
+ if (command === "sessions") {
1733
+ if (argument) {
1734
+ await switchSession(argument);
1735
+ return;
1736
+ }
1737
+ await showSessionBrowser();
1738
+ return;
1739
+ }
1740
+ if (command === "session") {
1741
+ await showSessionMenu();
1742
+ return;
1743
+ }
1744
+ if (command === "persist") {
1745
+ let enabled;
1746
+ if (!argument)
1747
+ enabled = !session.persistent;
1748
+ else if (["on", "true", "1"].includes(argument.toLowerCase()))
1749
+ enabled = true;
1750
+ else if (["off", "false", "0"].includes(argument.toLowerCase()))
1751
+ enabled = false;
1752
+ else
1753
+ throw new Error("/persist accepts on or off");
1754
+ setPersistence(enabled);
1755
+ return;
1756
+ }
1757
+ if (command === "model") {
1758
+ const models = await app.models(false, signal);
1759
+ if (stopping)
1760
+ return;
1761
+ let id = argument;
1762
+ if (!id) {
1763
+ id = await selectValue("Models", models.map((model) => ({
1764
+ value: `${model.provider}:${model.id}`,
1765
+ label: `[${model.provider}] ${model.name}`,
1766
+ description: `${model.id} | context ${model.contextWindow.toLocaleString()}${model.supportsFast ? " | fast" : ""}`,
1767
+ }))) ?? "";
1768
+ }
1769
+ if (!id || stopping)
1770
+ return;
1771
+ const model = models.find((candidate) => `${candidate.provider}:${candidate.id}` === id)
1772
+ ?? models.find((candidate) => candidate.provider === session.provider && candidate.id === id);
1773
+ if (!model)
1774
+ throw new Error(`Model is not available: ${id}`);
1775
+ setModel(model);
1776
+ return;
1777
+ }
1778
+ if (command === "agents") {
1779
+ let enabled;
1780
+ if (!argument)
1781
+ enabled = !session.agentsEnabled;
1782
+ else if (["on", "true", "1"].includes(argument.toLowerCase()))
1783
+ enabled = true;
1784
+ else if (["off", "false", "0"].includes(argument.toLowerCase()))
1785
+ enabled = false;
1786
+ else
1787
+ throw new Error("/agents accepts on or off");
1788
+ session = app.sessions.updateSettings(session.id, { agentsEnabled: enabled });
1789
+ addNotice("agents", `Agents ${enabled ? "enabled" : "disabled"} for this session.`, cyan);
1790
+ return;
1791
+ }
1792
+ if (command === "agentmodel") {
1793
+ const models = await app.models(false, signal);
1794
+ if (stopping)
1795
+ return;
1796
+ let id = argument;
1797
+ if (!id) {
1798
+ id = await selectValue("Agent models", models.map((model) => ({
1799
+ value: `${model.provider}:${model.id}`,
1800
+ label: `[${model.provider}] ${model.name}`,
1801
+ description: `${model.id} | context ${model.contextWindow.toLocaleString()}`,
1802
+ }))) ?? "";
1803
+ }
1804
+ if (!id || stopping)
1805
+ return;
1806
+ const model = models.find((candidate) => `${candidate.provider}:${candidate.id}` === id)
1807
+ ?? models.find((candidate) => candidate.provider === session.agentProvider && candidate.id === id);
1808
+ if (!model)
1809
+ throw new Error(`Agent model is not available: ${id}`);
1810
+ setAgentModel(model);
1811
+ return;
1812
+ }
1813
+ if (command === "reasoning") {
1814
+ const model = await app.catalogModel(session.model, session.provider, signal);
1815
+ if (stopping)
1816
+ return;
1817
+ let effort = argument;
1818
+ if (!effort) {
1819
+ effort = await selectValue("Reasoning effort", model.reasoningEfforts.map((value) => ({
1820
+ value,
1821
+ label: value,
1822
+ description: value === session.reasoningEffort ? "current" : "",
1823
+ }))) ?? "";
1824
+ }
1825
+ const supported = model.reasoningEfforts.find((value) => value === effort);
1826
+ if (!supported)
1827
+ throw new Error(`${model.id} does not support reasoning effort '${effort}'`);
1828
+ session = app.sessions.updateSettings(session.id, { reasoningEffort: supported });
1829
+ addNotice("reasoning", `Reasoning effort set to ${supported}.`, cyan);
1830
+ return;
1831
+ }
1832
+ if (command === "agentreasoning") {
1833
+ const model = await app.catalogModel(session.agentModel, session.agentProvider, signal);
1834
+ if (stopping)
1835
+ return;
1836
+ let effort = argument;
1837
+ if (!effort) {
1838
+ effort = await selectValue("Agent reasoning effort", model.reasoningEfforts.map((value) => ({
1839
+ value,
1840
+ label: value,
1841
+ description: value === session.agentReasoningEffort ? "current" : "",
1842
+ }))) ?? "";
1843
+ }
1844
+ if (!effort || stopping)
1845
+ return;
1846
+ const supported = model.reasoningEfforts.find((value) => value === effort);
1847
+ if (!supported)
1848
+ throw new Error(`${model.id} does not support agent reasoning effort '${effort}'`);
1849
+ session = app.sessions.updateSettings(session.id, { agentReasoningEffort: supported });
1850
+ addNotice("agent reasoning", `Agent reasoning effort set to ${supported}.`, cyan);
1851
+ return;
1852
+ }
1853
+ if (command === "thinking" || command === "show-reasoning") {
1854
+ let enabled;
1855
+ if (!argument)
1856
+ enabled = !session.showReasoning;
1857
+ else if (["on", "true", "1"].includes(argument.toLowerCase()))
1858
+ enabled = true;
1859
+ else if (["off", "false", "0"].includes(argument.toLowerCase()))
1860
+ enabled = false;
1861
+ else
1862
+ throw new Error(`/${command} accepts on or off`);
1863
+ session = app.sessions.updateSettings(session.id, { showReasoning: enabled });
1864
+ loadSessionEvents();
1865
+ addNotice("reasoning", `Reasoning summaries ${enabled ? "shown" : "hidden"}.`, cyan);
1866
+ return;
1867
+ }
1868
+ if (command === "fast") {
1869
+ const model = await app.catalogModel(session.model, session.provider, signal);
1870
+ if (stopping)
1871
+ return;
1872
+ let enabled;
1873
+ if (!argument)
1874
+ enabled = !session.fast;
1875
+ else if (["on", "true", "1"].includes(argument.toLowerCase()))
1876
+ enabled = true;
1877
+ else if (["off", "false", "0"].includes(argument.toLowerCase()))
1878
+ enabled = false;
1879
+ else
1880
+ throw new Error("/fast accepts on or off");
1881
+ if (enabled && !model.supportsFast)
1882
+ throw new Error(`${model.id} does not support fast service`);
1883
+ session = app.sessions.updateSettings(session.id, { fast: enabled });
1884
+ addNotice("fast", `Fast service ${enabled ? "enabled" : "disabled"}.`, cyan);
1885
+ return;
1886
+ }
1887
+ if (command === "compact") {
1888
+ await runCompact();
1889
+ return;
1890
+ }
1891
+ if (command === "permissions") {
1892
+ const modes = ["review", "code", "unrestricted"];
1893
+ let mode;
1894
+ if (argument) {
1895
+ const selected = modes.find((candidate) => candidate === argument.toLowerCase());
1896
+ if (!selected)
1897
+ throw new Error("/permissions accepts review, code, or unrestricted");
1898
+ mode = selected;
1899
+ }
1900
+ else {
1901
+ const current = modes.indexOf(session.approvalMode);
1902
+ mode = modes[(current + 1) % modes.length] ?? "review";
1903
+ }
1904
+ session = app.sessions.updateSettings(session.id, { approvalMode: mode });
1905
+ addNotice("permissions", mode === "unrestricted"
1906
+ ? "Approval mode set to unrestricted for this session. Tool confirmations are disabled."
1907
+ : `Approval mode set to ${mode} for this session. Critical actions require approval or a remembered matching scope.`, yellow);
1908
+ return;
1909
+ }
1910
+ if (command === "cron") {
1911
+ await showCronBrowser(argument.toLowerCase() === "session");
1912
+ return;
1913
+ }
1914
+ if (command === "schedule") {
1915
+ await createSessionSchedule(argument);
1916
+ return;
1917
+ }
1918
+ if (command === "inbox") {
1919
+ showInbox();
1920
+ return;
1921
+ }
1922
+ throw new Error(`Unknown command: /${command}`);
1923
+ };
1924
+ const handleSubmission = async (text) => {
1925
+ if (!text || submissionBusy || activeOperation || stopping)
1926
+ return;
1927
+ submissionBusy = true;
1928
+ editor.setEnabled(false);
1929
+ editor.addToHistory(text);
1930
+ requestRender();
1931
+ try {
1932
+ if (text.startsWith("/")) {
1933
+ const controller = new AbortController();
1934
+ activeController = controller;
1935
+ try {
1936
+ await handleCommand(text, controller.signal);
1937
+ }
1938
+ finally {
1939
+ if (activeController === controller)
1940
+ activeController = null;
1941
+ }
1942
+ }
1943
+ else
1944
+ await runTurn(text);
1945
+ }
1946
+ catch (error) {
1947
+ if (!stopping)
1948
+ addNotice("error", errorMessage(error), red);
1949
+ }
1950
+ finally {
1951
+ submissionBusy = false;
1952
+ if (!activeOperation && !stopping) {
1953
+ editor.setEnabled(true);
1954
+ restoreEditor();
1955
+ }
1956
+ }
1957
+ };
1958
+ const slashCommands = [
1959
+ { name: "new", description: "Start a new session" },
1960
+ { name: "session", description: "Manage current session" },
1961
+ {
1962
+ name: "persist",
1963
+ description: "Enable or disable persistent scheduled turns",
1964
+ argumentHint: "[on|off]",
1965
+ getArgumentCompletions: (prefix) => ["on", "off"]
1966
+ .filter((value) => value.startsWith(prefix))
1967
+ .map((value) => ({ value, label: value })),
1968
+ },
1969
+ {
1970
+ name: "sessions",
1971
+ description: "Switch session",
1972
+ argumentHint: "[id]",
1973
+ getArgumentCompletions: (prefix) => app.sessions.listWithMessages(app.workspace, 100)
1974
+ .filter((item) => app.hasProvider(item.provider))
1975
+ .filter((item) => item.id.startsWith(prefix))
1976
+ .map((item) => ({ value: item.id, label: item.title, description: item.id })),
1977
+ },
1978
+ {
1979
+ name: "model",
1980
+ description: "Select model",
1981
+ argumentHint: "[id]",
1982
+ getArgumentCompletions: async (prefix) => (await app.models())
1983
+ .filter((model) => `${model.provider}:${model.id}`.toLowerCase().includes(prefix.toLowerCase()))
1984
+ .map((model) => ({
1985
+ value: `${model.provider}:${model.id}`,
1986
+ label: `[${model.provider}] ${model.name}`,
1987
+ description: model.id,
1988
+ })),
1989
+ },
1990
+ {
1991
+ name: "agents",
1992
+ description: "Enable or disable agent delegation",
1993
+ argumentHint: "[on|off]",
1994
+ getArgumentCompletions: (prefix) => ["on", "off"]
1995
+ .filter((value) => value.startsWith(prefix))
1996
+ .map((value) => ({ value, label: value })),
1997
+ },
1998
+ {
1999
+ name: "agentmodel",
2000
+ description: "Select the model used by spawned agents",
2001
+ argumentHint: "[id]",
2002
+ getArgumentCompletions: async (prefix) => (await app.models())
2003
+ .filter((model) => `${model.provider}:${model.id}`.toLowerCase().includes(prefix.toLowerCase()))
2004
+ .map((model) => ({
2005
+ value: `${model.provider}:${model.id}`,
2006
+ label: `[${model.provider}] ${model.name}`,
2007
+ description: model.id,
2008
+ })),
2009
+ },
2010
+ {
2011
+ name: "reasoning",
2012
+ description: "Select a supported reasoning effort",
2013
+ argumentHint: "[effort]",
2014
+ getArgumentCompletions: async (prefix) => (await app.catalogModel(session.model, session.provider)).reasoningEfforts
2015
+ .filter((effort) => effort.startsWith(prefix))
2016
+ .map((effort) => ({ value: effort, label: effort })),
2017
+ },
2018
+ {
2019
+ name: "agentreasoning",
2020
+ description: "Select reasoning effort for spawned agents",
2021
+ argumentHint: "[effort]",
2022
+ getArgumentCompletions: async (prefix) => (await app.catalogModel(session.agentModel, session.agentProvider)).reasoningEfforts
2023
+ .filter((effort) => effort.startsWith(prefix))
2024
+ .map((effort) => ({ value: effort, label: effort })),
2025
+ },
2026
+ {
2027
+ name: "thinking",
2028
+ description: "Show or hide reasoning summaries",
2029
+ argumentHint: "[on|off]",
2030
+ getArgumentCompletions: (prefix) => ["on", "off"]
2031
+ .filter((value) => value.startsWith(prefix))
2032
+ .map((value) => ({ value, label: value })),
2033
+ },
2034
+ {
2035
+ name: "fast",
2036
+ description: "Toggle fast service",
2037
+ argumentHint: "[on|off]",
2038
+ getArgumentCompletions: (prefix) => ["on", "off"]
2039
+ .filter((value) => value.startsWith(prefix))
2040
+ .map((value) => ({ value, label: value })),
2041
+ },
2042
+ { name: "compact", description: "Compact conversation context" },
2043
+ {
2044
+ name: "permissions",
2045
+ description: "Set approval mode",
2046
+ argumentHint: "[review|code|unrestricted]",
2047
+ getArgumentCompletions: (prefix) => ["review", "code", "unrestricted"]
2048
+ .filter((value) => value.startsWith(prefix))
2049
+ .map((value) => ({ value, label: value })),
2050
+ },
2051
+ { name: "schedule", description: "Create a scheduled prompt in this session", argumentHint: "[once|cron] ..." },
2052
+ { name: "cron", description: "Browse and manage schedules", argumentHint: "[session]" },
2053
+ { name: "inbox", description: "Read all unread scheduler items" },
2054
+ { name: "exit", description: "Exit Looking Glass" },
2055
+ ];
2056
+ editor.setAutocompleteProvider(new SafeAutocompleteProvider(slashCommands, app.workspace));
2057
+ const trackTask = (task) => {
2058
+ pendingTasks.add(task);
2059
+ void task.then(() => pendingTasks.delete(task), () => pendingTasks.delete(task));
2060
+ };
2061
+ editor.onSubmit = (text) => trackTask(handleSubmission(text));
2062
+ const cancelActiveInteraction = () => {
2063
+ activeController?.abort();
2064
+ activeModal?.cancel();
2065
+ activeModal = null;
2066
+ };
2067
+ function requestStop() {
2068
+ if (stopping)
2069
+ return;
2070
+ stopping = true;
2071
+ cancelActiveInteraction();
2072
+ if (pollTimer)
2073
+ clearInterval(pollTimer);
2074
+ if (activityTimer)
2075
+ clearInterval(activityTimer);
2076
+ pollTimer = null;
2077
+ activityTimer = null;
2078
+ resolveStopped?.();
2079
+ }
2080
+ const onSignal = () => requestStop();
2081
+ const pollInbox = () => {
2082
+ if (stopping)
2083
+ return;
2084
+ try {
2085
+ if (unavailableModelKey && Date.now() >= nextModelAvailabilityCheckAt && !activeOperation) {
2086
+ nextModelAvailabilityCheckAt = Date.now() + MODEL_AVAILABILITY_RETRY_MS;
2087
+ trackTask(refreshContextWindow());
2088
+ }
2089
+ const latestSession = app.sessions.get(session.id);
2090
+ if (latestSession && latestSession.updatedAt > session.updatedAt && !activeOperation) {
2091
+ const modelChanged = latestSession.provider !== session.provider || latestSession.model !== session.model;
2092
+ session = latestSession;
2093
+ if (modelChanged) {
2094
+ contextWindow = 0;
2095
+ trackTask(refreshContextWindow());
2096
+ }
2097
+ loadSessionEvents();
2098
+ }
2099
+ const unread = app.scheduler.listInbox({ unackedOnly: true, limit: 200 });
2100
+ const unseen = unread.filter((item) => {
2101
+ const identity = inboxIdentity(item);
2102
+ return !seenInbox.has(identity) && !suppressedInbox.has(identity);
2103
+ }).reverse();
2104
+ const refreshCurrent = unseen.some((item) => {
2105
+ const job = app.scheduler.getJob(item.jobId);
2106
+ return job?.kind === "session_prompt" && job.sessionId === session.id;
2107
+ });
2108
+ if (refreshCurrent && !activeOperation)
2109
+ loadSessionEvents();
2110
+ for (const item of unseen) {
2111
+ const job = app.scheduler.getJob(item.jobId);
2112
+ if (!shouldAutoDisplayInbox(job, session.id)) {
2113
+ if (job?.kind === "session_prompt" && job.sessionId) {
2114
+ suppressedInbox.set(inboxIdentity(item), job.sessionId);
2115
+ }
2116
+ continue;
2117
+ }
2118
+ if (job?.kind === "session_prompt" && job.sessionId === session.id) {
2119
+ if (activeOperation)
2120
+ continue;
2121
+ }
2122
+ seenInbox.add(inboxIdentity(item));
2123
+ addNotice("scheduler", schedulerInboxLine(item), magenta);
2124
+ }
2125
+ pollErrorShown = false;
2126
+ }
2127
+ catch (error) {
2128
+ if (!pollErrorShown)
2129
+ addNotice("scheduler error", errorMessage(error), red);
2130
+ pollErrorShown = true;
2131
+ }
2132
+ };
2133
+ tui.addInputListener((data) => {
2134
+ if (matchesKey(data, Key.ctrl("c"))) {
2135
+ root.clearSelection();
2136
+ selectionDragging = false;
2137
+ if (activeOperation) {
2138
+ activeController?.abort();
2139
+ activeModal?.cancel();
2140
+ }
2141
+ else {
2142
+ requestStop();
2143
+ }
2144
+ return { consume: true };
2145
+ }
2146
+ if (terminal.columns < MIN_COLUMNS || terminal.rows < MIN_ROWS)
2147
+ return { consume: true };
2148
+ const mouse = parseTerminalMouse(data);
2149
+ if (mouse) {
2150
+ if (activeModal) {
2151
+ root.clearSelection();
2152
+ selectionDragging = false;
2153
+ activeModal.mouse?.(mouse);
2154
+ requestRender();
2155
+ return { consume: true };
2156
+ }
2157
+ if (mouse.action === "wheel_up" || mouse.action === "wheel_down") {
2158
+ root.clearSelection();
2159
+ selectionDragging = false;
2160
+ root.scrollLines(mouse.action === "wheel_up" ? 3 : -3);
2161
+ requestRender();
2162
+ return { consume: true };
2163
+ }
2164
+ if (mouse.action === "press" && mouse.button === 0) {
2165
+ root.startSelection(mouse);
2166
+ selectionDragging = false;
2167
+ requestRender();
2168
+ return { consume: true };
2169
+ }
2170
+ if (mouse.action === "drag" && mouse.button === 0) {
2171
+ selectionDragging = root.updateSelection(mouse);
2172
+ requestRender();
2173
+ return { consume: true };
2174
+ }
2175
+ if (mouse.action === "release") {
2176
+ root.updateSelection(mouse);
2177
+ if (selectionDragging)
2178
+ terminal.copy(root.selectionText());
2179
+ root.clearSelection();
2180
+ selectionDragging = false;
2181
+ requestRender();
2182
+ return { consume: true };
2183
+ }
2184
+ return { consume: true };
2185
+ }
2186
+ if (activeModal)
2187
+ return undefined;
2188
+ if (matchesKey(data, Key.ctrl("pageUp")) || matchesKey(data, Key.pageUp)) {
2189
+ root.scrollPage(-1);
2190
+ requestRender();
2191
+ return { consume: true };
2192
+ }
2193
+ if (matchesKey(data, Key.ctrl("pageDown")) || matchesKey(data, Key.pageDown)) {
2194
+ root.scrollPage(1);
2195
+ requestRender();
2196
+ return { consume: true };
2197
+ }
2198
+ return undefined;
2199
+ });
2200
+ loadSessionEvents();
2201
+ process.once("SIGINT", onSignal);
2202
+ process.once("SIGTERM", onSignal);
2203
+ let started = false;
2204
+ try {
2205
+ started = true;
2206
+ tui.start();
2207
+ trackTask(refreshContextWindow());
2208
+ pollInbox();
2209
+ pollTimer = setInterval(pollInbox, INBOX_POLL_MS);
2210
+ activityTimer = setInterval(() => {
2211
+ if (!activeOperation || stopping)
2212
+ return;
2213
+ activityFrame += 1;
2214
+ requestRender();
2215
+ }, ACTIVITY_RENDER_MS);
2216
+ await stopped;
2217
+ }
2218
+ finally {
2219
+ stopping = true;
2220
+ process.removeListener("SIGINT", onSignal);
2221
+ process.removeListener("SIGTERM", onSignal);
2222
+ if (pollTimer)
2223
+ clearInterval(pollTimer);
2224
+ if (activityTimer)
2225
+ clearInterval(activityTimer);
2226
+ cancelActiveInteraction();
2227
+ for (const assistant of streaming)
2228
+ assistant.dispose();
2229
+ for (const reasoning of streamingReasoning)
2230
+ reasoning.dispose();
2231
+ if (started) {
2232
+ try {
2233
+ await terminal.drainInput();
2234
+ }
2235
+ finally {
2236
+ if (terminal.isActive)
2237
+ tui.stop();
2238
+ else
2239
+ terminal.stop();
2240
+ }
2241
+ }
2242
+ await Promise.allSettled([...pendingTasks]);
2243
+ app.sessions.deleteIfEmpty(session.id);
2244
+ }
2245
+ }
2246
+ //# sourceMappingURL=tui.js.map