@gaia-ai/addon-gaia-ui 0.17.2 → 0.19.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 (42) hide show
  1. package/dist/src/Entity/ticket/Component/edit-form/index.js +9 -2
  2. package/dist/src/Entity/ticket/Component/sections/index.js +5 -3
  3. package/dist/src/Entity/ticket/Component/ticket-content/index.d.ts +17 -0
  4. package/dist/src/Entity/ticket/Component/ticket-content/index.js +60 -2
  5. package/dist/src/Entity/ticket/Component/ticket-content/model.d.ts +1 -1
  6. package/dist/src/Entity/ticket/Component/ticket-content/model.js +33 -5
  7. package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +6 -2
  8. package/dist/src/Entity/ticket/Component/ticket-detail/index.js +59 -4
  9. package/dist/src/Entity/ticket/Data/addons.d.ts +53 -0
  10. package/dist/src/Entity/ticket/Data/addons.js +35 -0
  11. package/dist/src/Entity/ticket/Data/detail.js +2 -0
  12. package/dist/src/Entity/ticket/Data/store.js +2 -0
  13. package/dist/src/Entity/ticket/Data/tickets.d.ts +13 -9
  14. package/dist/src/Entity/ticket/Data/tickets.js +12 -4
  15. package/dist/src/Entity/ticket/Data/write.d.ts +7 -0
  16. package/dist/src/Entity/ticket/Data/write.js +17 -0
  17. package/dist/src/Entity/ticket/Form/addon-action.d.ts +37 -0
  18. package/dist/src/Entity/ticket/Form/addon-action.js +80 -0
  19. package/dist/src/Entity/ticket/Form/create-data.d.ts +2 -0
  20. package/dist/src/Entity/ticket/Form/create-data.js +9 -1
  21. package/dist/src/Entity/ticket/Form/create.d.ts +5 -1
  22. package/dist/src/Entity/ticket/Form/create.js +3 -0
  23. package/dist/src/Entity/ticket/Form/edit-data.d.ts +2 -0
  24. package/dist/src/Entity/ticket/Form/edit-data.js +14 -1
  25. package/dist/src/Entity/ticket/Form/edit.d.ts +14 -1
  26. package/dist/src/Entity/ticket/Form/edit.js +31 -0
  27. package/dist/src/Entity/ticket/Form/reviewer-options.d.ts +10 -0
  28. package/dist/src/Entity/ticket/Form/reviewer-options.js +48 -0
  29. package/dist/src/Entity/ticket/Screen/conductor-tickets.d.ts +2 -2
  30. package/dist/src/Entity/ticket/Screen/create-ticket-form.js +4 -0
  31. package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +30 -1
  32. package/dist/src/Entity/ticket/Screen/ticket-addons.d.ts +94 -0
  33. package/dist/src/Entity/ticket/Screen/ticket-addons.js +492 -0
  34. package/dist/src/Entity/ticket/Screen/ticket-content-screen.d.ts +21 -0
  35. package/dist/src/Entity/ticket/Screen/ticket-content-screen.js +122 -8
  36. package/dist/src/Entity/ticket/Screen/ticket-detail.js +4 -2
  37. package/dist/src/Entity/ticket/Screen/tickets.d.ts +2 -2
  38. package/dist/src/Entity/ticket/Screen/user-tickets.d.ts +2 -2
  39. package/dist/src/Routing/routes.js +3 -0
  40. package/dist/src/Widget/multiselect.js +7 -3
  41. package/dist/src/types.d.ts +5 -0
  42. package/package.json +3 -2
@@ -0,0 +1,492 @@
1
+ // The ticket's addon messages (GAIA-661): one entry per addon of the
2
+ // ticket's conductor in an addon bar (←/→), the selected addon's permitted
3
+ // messages as one list, Enter to send through the conductor inbox, and a
4
+ // modal form for a message with input. Definitions and states come from the
5
+ // server, so this tab needs no addon installed locally. Contract 1
6
+ // (views/actions/rows) is retired; only contract-2 messages render here.
7
+ import { actionItem, } from '../../../Component/footer-actions/index.js';
8
+ import { Markdown, markdownLines } from '../../../Component/markdown/index.js';
9
+ import { flattenSections } from '../../../Component/section-list/index.js';
10
+ import { copyThrough } from '../../../lib/clipboard.js';
11
+ import { ListCursor } from '../../../lib/cursor.js';
12
+ import { columnNode, messageTree } from '../../../lib/vnode.js';
13
+ import { AsyncModalScreen } from '../../../Screen/async-modal-screen.js';
14
+ import { AsyncScreen } from '../../../Screen/async-screen.js';
15
+ import { runRowAction } from '../../../Screen/row-action.js';
16
+ import { FIELDS_REGION, renderDeclaredFieldsTree, } from '../../../Widget/render.js';
17
+ import { listRowIds, listTableRows, listTableTree, } from '../../collection.js';
18
+ import { ticketEditAction, ticketHeaderTree, ticketTitleOf, } from '../Component/ticket-detail/index.js';
19
+ import { addonEntries, isOpen, readTicketAddons, sendAddonAction, } from '../Data/addons.js';
20
+ import { ticketInputContext } from '../Data/input.js';
21
+ import { readTicket } from '../Data/store.js';
22
+ import { addonForm, formFields, payloadFrom, } from '../Form/addon-action.js';
23
+ import { keyToIntent } from '../Form/keys.js';
24
+ /** Re-read cadence while a message of the shown addon is pending/running. */
25
+ const OPEN_POLL_MS = 3_000;
26
+ function ticketUuidOf(context, ticket) {
27
+ return ticket?.data?.id ?? context.params?.ticketId ?? '';
28
+ }
29
+ /** The table of one addon: label and an Enter-to-run hint per message. */
30
+ function tableFor() {
31
+ const columns = [
32
+ {
33
+ fields: [],
34
+ key: 'c0',
35
+ label: 'LABEL',
36
+ width: 28,
37
+ get: (l) => l.cells[0] ?? '',
38
+ },
39
+ {
40
+ fields: [],
41
+ key: 'actions',
42
+ label: 'ACTIONS',
43
+ width: 0,
44
+ get: (l) => l.cells[1] ?? '',
45
+ },
46
+ ];
47
+ return {
48
+ noun: 'addon',
49
+ columns,
50
+ emptyMessage: 'No data yet.',
51
+ };
52
+ }
53
+ function linesFor(entries) {
54
+ return entries.map((entry) => ({
55
+ id: entry.id,
56
+ cells: [
57
+ `▶ ${entry.message.label}`,
58
+ entry.message.input ? 'Enter: form' : 'Enter: send',
59
+ ],
60
+ }));
61
+ }
62
+ /** The addon bar: every addon, the selected one in brackets. */
63
+ function addonBar(model, selected) {
64
+ return model.addons
65
+ .map((a, i) => (i === selected ? `[${a.label}]` : a.label))
66
+ .join(' ');
67
+ }
68
+ /** The newest message of the addon, as one status line. */
69
+ function latestLine(addon) {
70
+ const latest = addon.latest;
71
+ if (!latest)
72
+ return undefined;
73
+ return `${latest.label}: ${latest.status}`;
74
+ }
75
+ /** The latest answer's Markdown body. */
76
+ function latestMarkdown(addon) {
77
+ return addon.latest?.result?.markdown;
78
+ }
79
+ /** The first safe http(s) link a Markdown answer carries, if any (`l` key). */
80
+ function firstHttpLink(markdown) {
81
+ // Prefer the URL inside markdown link syntax `[text](url)` first: a bare
82
+ // `\S+` scan greedily swallows the closing `)` and any wrapping brackets
83
+ // of a real `[443](https://host:443)` answer (GAIA-661).
84
+ return (markdown.match(/\]\((https?:[^)\s]+)\)/)?.[1] ??
85
+ markdown.match(/\bhttps?:\/\/\S+/)?.[0]?.replace(/[)\]."'`]+$/, ''));
86
+ }
87
+ export class TicketAddonsScreen extends AsyncScreen {
88
+ // No `tab` here (GAIA-661): this screen is the full send/poll surface, kept
89
+ // navigable from the Overview sidebar's inline card, never a tab a person
90
+ // sees — the tab bar never shows the word "addon".
91
+ static route = {
92
+ id: 'ticket.addons',
93
+ path: '/tickets/:ticketId/addons',
94
+ parent: 'project.tickets',
95
+ fallback: { routeId: 'my.work' },
96
+ context: ticketInputContext,
97
+ };
98
+ id = TicketAddonsScreen.route.id;
99
+ frameId = 'ticket';
100
+ cursor = new ListCursor();
101
+ selected = 0;
102
+ sending = new Set();
103
+ timer;
104
+ async load(context) {
105
+ const ticket = readTicket(context, this.readyValue?.ticket);
106
+ const client = context.services?.client;
107
+ // No control plane (a bare pipe): nothing to show rather than an error.
108
+ if (!client)
109
+ return { ticket, model: { conductor: null, addons: [] } };
110
+ return {
111
+ ticket,
112
+ model: await readTicketAddons(client, ticketUuidOf(context, ticket)),
113
+ };
114
+ }
115
+ didLoad(data, context) {
116
+ this.selected = Math.min(this.selected, Math.max(0, data.model.addons.length - 1));
117
+ this.clampCursor();
118
+ this.schedulePoll(context);
119
+ }
120
+ reload(context) {
121
+ void this.retry(context);
122
+ }
123
+ leave() {
124
+ this.stopPoll();
125
+ super.leave();
126
+ }
127
+ reset() {
128
+ this.stopPoll();
129
+ super.reset();
130
+ this.cursor.reset();
131
+ this.selected = 0;
132
+ this.sending = new Set();
133
+ this.toast = '';
134
+ }
135
+ /** Re-read every 3 s while the shown addon has an open message. */
136
+ schedulePoll(context) {
137
+ const addon = this.addon();
138
+ if (!addon || !isOpen(addon)) {
139
+ this.stopPoll();
140
+ return;
141
+ }
142
+ if (this.timer)
143
+ return;
144
+ this.timer = setInterval(() => {
145
+ if (context.isActive())
146
+ this.reload(context);
147
+ }, OPEN_POLL_MS);
148
+ this.timer.unref?.();
149
+ }
150
+ stopPoll() {
151
+ if (this.timer)
152
+ clearInterval(this.timer);
153
+ this.timer = undefined;
154
+ }
155
+ addon() {
156
+ return this.readyValue?.model.addons[this.selected];
157
+ }
158
+ entries() {
159
+ const addon = this.addon();
160
+ return addon ? addonEntries(addon) : [];
161
+ }
162
+ clampCursor() {
163
+ this.cursor.index = Math.max(0, Math.min(this.cursor.index, this.entries().length - 1));
164
+ }
165
+ renderLoading() {
166
+ return ['Loading addons…'];
167
+ }
168
+ lines() {
169
+ const addon = this.addon();
170
+ if (!addon)
171
+ return undefined;
172
+ const entries = this.entries();
173
+ const table = tableFor();
174
+ const lines = linesFor(entries).map((line) => this.sending.has(line.id)
175
+ ? { ...line, cells: [...line.cells.slice(0, -1), 'sending …'] }
176
+ : line);
177
+ return { table, lines };
178
+ }
179
+ renderReady(data, context) {
180
+ const view = this.lines();
181
+ if (!view)
182
+ return messageTree('addons', ['No addons for this ticket.']);
183
+ const addon = this.addon();
184
+ const markdown = addon ? latestMarkdown(addon) : undefined;
185
+ const children = [
186
+ messageTree('addons-bar', [addonBar(data.model, this.selected)]),
187
+ listTableTree(view.table, view.lines, undefined, {
188
+ selectedIndex: this.cursor.index,
189
+ }),
190
+ ];
191
+ if (markdown) {
192
+ children.push(Markdown({
193
+ id: 'addons-answer',
194
+ width: Math.max(16, (context.terminalColumns ?? 80) - 12),
195
+ source: markdown,
196
+ }));
197
+ }
198
+ return columnNode('addons', ...children);
199
+ }
200
+ renderNonInteractive(context) {
201
+ const notice = this.noticeRows(context);
202
+ if (notice)
203
+ return notice;
204
+ const data = this.readyValue;
205
+ const view = this.lines();
206
+ // A ticket report prints every tab; one without addons adds nothing.
207
+ if (!data || !view)
208
+ return [];
209
+ const addon = this.addon();
210
+ const markdown = addon ? latestMarkdown(addon) : undefined;
211
+ return flattenSections([
212
+ {
213
+ heading: addonBar(data.model, this.selected),
214
+ lines: [
215
+ ...listTableRows(view.table, view.lines, undefined),
216
+ ...(markdown ? markdownLines(markdown).map((l) => l.text) : []),
217
+ ],
218
+ },
219
+ ]).lines;
220
+ }
221
+ chrome(context) {
222
+ const ticket = this.readyValue?.ticket;
223
+ const { identifier, title } = ticketTitleOf(ticket ?? {});
224
+ const view = this.lines();
225
+ const count = view?.lines.length ?? 0;
226
+ return {
227
+ title: ticket?.data
228
+ ? `${identifier} · ${title}`
229
+ : (context.params?.ticketId ?? 'ticket'),
230
+ ...(ticket?.data
231
+ ? { header: ticketHeaderTree(ticket, identifier, title, []) }
232
+ : {}),
233
+ selectable: count,
234
+ selectedIndex: this.cursor.index,
235
+ cursorMode: count > 1 ? 'select' : 'scroll',
236
+ ...(view ? { rowIds: listRowIds(view.table, view.lines) } : {}),
237
+ };
238
+ }
239
+ actions() {
240
+ const many = (this.readyValue?.model.addons.length ?? 0) > 1;
241
+ return [
242
+ actionItem('detail-tab', 'Tab', 'section'),
243
+ actionItem('detail-edit', 'e', 'edit'),
244
+ ...(many ? [actionItem('addons-switch', '←→', 'addon')] : []),
245
+ actionItem('detail-arrows', '↑↓', 'select'),
246
+ actionItem('addons-enter', 'Enter', 'run'),
247
+ actionItem('addons-link', 'l', 'copy link'),
248
+ ];
249
+ }
250
+ notice() {
251
+ const addon = this.addon();
252
+ const offline = this.readyValue?.model.conductor?.status === 'offline';
253
+ return ([
254
+ offline
255
+ ? 'Conductor offline — messages are queued until it returns'
256
+ : '',
257
+ addon && addon.contract !== 2
258
+ ? `Addon contract ${addon.contract} is not supported`
259
+ : '',
260
+ addon
261
+ ? (latestLine(addon) ?? 'You have not sent a message to this addon.')
262
+ : '',
263
+ ]
264
+ .filter(Boolean)
265
+ .join(' · ') || undefined);
266
+ }
267
+ select(index, context) {
268
+ const next = Math.max(0, Math.min(index, this.entries().length - 1));
269
+ if (next === this.cursor.index)
270
+ return false;
271
+ this.cursor.index = next;
272
+ context.requestDraw();
273
+ return true;
274
+ }
275
+ activate(index, context) {
276
+ return this.run(index, context);
277
+ }
278
+ /** Runs an entry's message: sends it, or opens its form. */
279
+ run(index, context) {
280
+ const entry = this.entries()[index];
281
+ const addon = this.addon();
282
+ const data = this.readyValue;
283
+ if (!entry || !addon || !data)
284
+ return false;
285
+ const ticketUuid = ticketUuidOf(context, data.ticket);
286
+ const { message } = entry;
287
+ if (message.input) {
288
+ const state = {
289
+ ticketUuid,
290
+ addon: addon.name,
291
+ key: message.key,
292
+ label: message.label,
293
+ input: message.input,
294
+ };
295
+ context.navigate?.('ticket.addons.action', { ticketId: context.params?.ticketId ?? ticketUuid }, state);
296
+ return true;
297
+ }
298
+ const client = context.services?.client;
299
+ if (!client)
300
+ return false;
301
+ runRowAction({
302
+ rowId: entry.id,
303
+ busy: this.sending,
304
+ context,
305
+ perform: async () => {
306
+ await sendAddonAction(client, ticketUuid, `${addon.name}.${message.key}`, {});
307
+ return { message: `${message.label} sent` };
308
+ },
309
+ refresh: (ctx) => this.retry(ctx),
310
+ setStatus: (status) => {
311
+ this.toast = status;
312
+ },
313
+ captureLifecycle: () => this.captureLifecycle(context),
314
+ lifecycleIsCurrent: (guard, ctx) => this.lifecycleIsCurrent(guard, ctx),
315
+ statusIsCurrent: (guard, ctx) => this.actionStillCurrent(guard, ctx),
316
+ failPrefix: `${addon.name}.${message.key} failed`,
317
+ });
318
+ return true;
319
+ }
320
+ handleInput(input, context) {
321
+ if (super.handleInput(input, context))
322
+ return true;
323
+ const data = this.readyValue;
324
+ if (this.state !== 'ready' || !data)
325
+ return false;
326
+ const count = data.model.addons.length;
327
+ if ((input === 'left' || input === 'right') && count > 1) {
328
+ this.selected =
329
+ (this.selected + (input === 'right' ? 1 : -1) + count) % count;
330
+ this.cursor.reset();
331
+ this.schedulePoll(context);
332
+ context.requestDraw();
333
+ return true;
334
+ }
335
+ if (input === 'enter' || input === ' ')
336
+ return this.run(this.cursor.index, context);
337
+ if (input === 'l') {
338
+ const addon = this.addon();
339
+ const markdown = addon ? latestMarkdown(addon) : undefined;
340
+ const href = markdown ? firstHttpLink(markdown) : undefined;
341
+ this.toast = copyThrough(context.copyToClipboard, href, href ?? '', 'no link in the latest answer');
342
+ context.requestDraw();
343
+ return true;
344
+ }
345
+ const entries = this.entries().length;
346
+ if (entries > 1) {
347
+ const before = this.cursor.index;
348
+ if (this.cursor.handle(input, entries) && this.cursor.index !== before) {
349
+ context.requestDraw();
350
+ return true;
351
+ }
352
+ }
353
+ return false;
354
+ }
355
+ commandForInput(input) {
356
+ return this.state === 'ready' && input === 'e'
357
+ ? 'edit-assignment'
358
+ : undefined;
359
+ }
360
+ actionForInput(input, context) {
361
+ return this.commandForInput(input)
362
+ ? ticketEditAction(context, this.readyValue?.ticket)
363
+ : undefined;
364
+ }
365
+ receiveResult(result, context) {
366
+ const returned = result;
367
+ if (returned?.type === 'addon-action-sent') {
368
+ this.toast = returned.message ?? '';
369
+ this.reload(context);
370
+ }
371
+ }
372
+ }
373
+ /** The form of an action with input, built from its input subset. */
374
+ export class AddonActionFormScreen extends AsyncModalScreen {
375
+ static route = {
376
+ id: 'ticket.addons.action',
377
+ path: '/tickets/:ticketId/addons/action',
378
+ parent: 'ticket.addons',
379
+ presentation: 'modal',
380
+ };
381
+ id = AddonActionFormScreen.route.id;
382
+ target;
383
+ form;
384
+ values;
385
+ errors = {};
386
+ sending = false;
387
+ async load(context) {
388
+ const target = context.navigationState;
389
+ if (!target?.input)
390
+ throw new Error('No addon action to fill in');
391
+ this.target = target;
392
+ this.form = addonForm(target.input);
393
+ const options = {};
394
+ for (const field of formFields(target.input))
395
+ if (field.options)
396
+ options[field.id] = field.options;
397
+ return options;
398
+ }
399
+ didLoad(options) {
400
+ if (!this.form || !this.target)
401
+ return;
402
+ const prefill = {};
403
+ for (const [id, field] of Object.entries(this.target.input.properties)) {
404
+ if (field.type === 'boolean')
405
+ prefill[id] = field.default === true ? 'yes' : 'no';
406
+ else if (field.default !== undefined)
407
+ prefill[id] = String(field.default);
408
+ }
409
+ this.values = this.form.initial(options, prefill);
410
+ }
411
+ renderLoading() {
412
+ return ['Loading…'];
413
+ }
414
+ renderReady(options) {
415
+ if (!this.form || !this.values)
416
+ return columnNode(FIELDS_REGION);
417
+ return renderDeclaredFieldsTree(this.form.fields, this.values, options, {
418
+ width: 58,
419
+ errorFor: (field) => this.errors[field.id],
420
+ });
421
+ }
422
+ modalChrome() {
423
+ return { title: this.target?.label ?? 'Addon action' };
424
+ }
425
+ actions() {
426
+ return [
427
+ actionItem('addon-form-field', '↑↓', 'field'),
428
+ actionItem('addon-form-send', 'Ctrl+S', 'send'),
429
+ actionItem('addon-form-esc', 'Esc', 'cancel'),
430
+ ];
431
+ }
432
+ handleInput(input, context) {
433
+ if (super.handleInput(input, context))
434
+ return true;
435
+ const options = this.readyValue;
436
+ if (!options || !this.form || !this.values)
437
+ return false;
438
+ const intent = keyToIntent(input, this.form.activeField(this.values));
439
+ if (!intent)
440
+ return true;
441
+ if (intent.kind === 'cancel') {
442
+ context.back?.();
443
+ return true;
444
+ }
445
+ if (intent.kind === 'submit') {
446
+ void this.submit(context);
447
+ return true;
448
+ }
449
+ this.values = this.form.apply(this.values, options, intent);
450
+ context.requestDraw();
451
+ return true;
452
+ }
453
+ async submit(context) {
454
+ const target = this.target;
455
+ const client = context.services?.client;
456
+ if (!target || !client || !this.values || this.sending)
457
+ return;
458
+ const outcome = payloadFrom(target.input, this.values);
459
+ if ('errors' in outcome) {
460
+ this.errors = outcome.errors;
461
+ context.requestDraw();
462
+ return;
463
+ }
464
+ this.errors = {};
465
+ this.sending = true;
466
+ try {
467
+ await sendAddonAction(client, target.ticketUuid, `${target.addon}.${target.key}`, outcome.payload);
468
+ context.back?.({
469
+ type: 'addon-action-sent',
470
+ message: `${target.label} sent`,
471
+ });
472
+ }
473
+ catch (error) {
474
+ this.toast = error instanceof Error ? error.message : String(error);
475
+ context.requestDraw();
476
+ }
477
+ finally {
478
+ this.sending = false;
479
+ }
480
+ }
481
+ notice() {
482
+ return this.toast || undefined;
483
+ }
484
+ reset() {
485
+ this.target = undefined;
486
+ this.form = undefined;
487
+ this.values = undefined;
488
+ this.errors = {};
489
+ this.sending = false;
490
+ super.reset();
491
+ }
492
+ }
@@ -18,12 +18,16 @@ export declare abstract class TicketContentScreen extends AsyncScreen<TicketDeta
18
18
  private initializedKey;
19
19
  protected labels: AssignmentLabels | undefined;
20
20
  private status;
21
+ private readonly freezeBusy;
21
22
  private releaseStore;
22
23
  private expanded;
23
24
  protected viewMode: UiViewMode;
24
25
  /** Visit-local: the sidebar pane and Planning document tab last chosen. */
25
26
  private sidebarTab;
26
27
  private documentKey;
28
+ /** Overview only (GAIA-661); a stale load after navigating away is dropped. */
29
+ private addonsPreview;
30
+ private addonsPreviewGeneration;
27
31
  private modelMemo;
28
32
  compact(): boolean;
29
33
  get document(): TicketDetailDocument | undefined;
@@ -37,6 +41,8 @@ export declare abstract class TicketContentScreen extends AsyncScreen<TicketDeta
37
41
  contentProps(context: ScreenContext): TicketContentProps;
38
42
  reload(context: ScreenContext): void;
39
43
  enter(context: ScreenContext): void;
44
+ /** The inline card's data (GAIA-661); Overview only, loaded once per visit. */
45
+ private loadAddonsPreview;
40
46
  leave(): void;
41
47
  protected load(context: ScreenContext): Promise<TicketDetailDocument>;
42
48
  protected didLoad(document: TicketDetailDocument, context: ScreenContext): void;
@@ -45,8 +51,23 @@ export declare abstract class TicketContentScreen extends AsyncScreen<TicketDeta
45
51
  };
46
52
  restoreState(token: unknown): void;
47
53
  setLabels(labels: AssignmentLabels | undefined): void;
54
+ /**
55
+ * The model's selectable keys, plus the inline addons card's row (GAIA-661
56
+ * D1) when the Overview preview has a message worth reaching: a Tab/arrow
57
+ * visit stops there and Enter opens the full send/poll screen, the same
58
+ * keyboard reach the Details/Stats/Related sidebar card already has via its
59
+ * own (Technical details) row.
60
+ */
61
+ private selectableKeys;
62
+ private openAddons;
48
63
  select(index: number, context: ScreenContext): boolean;
49
64
  activate(index: number, context: ScreenContext): boolean;
65
+ private isFrozen;
66
+ /**
67
+ * PATCH `frozen` through the write client, then re-read on the session
68
+ * client so the ladder and document agree with the server.
69
+ */
70
+ private toggleFrozen;
50
71
  handleInput(input: string, context: ScreenContext): boolean;
51
72
  commandForInput(input: string): string | undefined;
52
73
  actionForInput(input: string, context: ScreenContext): ScreenAction | undefined;