@cmodjs/core 4.0.3 → 4.1.1

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 (143) hide show
  1. package/docs/commands.md +44 -9
  2. package/docs/index.md +3 -3
  3. package/docs/install-steps.md +8 -8
  4. package/docs/jobs.md +2 -1
  5. package/docs/mod.md +1 -1
  6. package/docs/options.md +1 -1
  7. package/docs/permissions.md +1 -1
  8. package/docs/session.md +46 -13
  9. package/docs/testing.md +15 -11
  10. package/docs/ui.md +43 -11
  11. package/installer.d.ts +3 -2
  12. package/installer.d.ts.map +1 -1
  13. package/jobs/program.d.ts +1 -0
  14. package/jobs/program.d.ts.map +1 -1
  15. package/jobs/program.js +39 -23
  16. package/mod.d.ts +3 -2
  17. package/mod.d.ts.map +1 -1
  18. package/mod.js +1 -0
  19. package/package.json +1 -1
  20. package/records.d.ts +15 -1
  21. package/records.d.ts.map +1 -1
  22. package/records.js +39 -1
  23. package/register.d.ts.map +1 -1
  24. package/register.js +8 -5
  25. package/runtime/before.d.ts +6 -2
  26. package/runtime/before.d.ts.map +1 -1
  27. package/runtime/before.js +56 -18
  28. package/runtime/buttons.d.ts +10 -0
  29. package/runtime/buttons.d.ts.map +1 -0
  30. package/runtime/buttons.js +12 -0
  31. package/runtime/conversations.d.ts +3 -0
  32. package/runtime/conversations.d.ts.map +1 -1
  33. package/runtime/conversations.js +164 -27
  34. package/runtime/engine.d.ts +2 -0
  35. package/runtime/engine.d.ts.map +1 -1
  36. package/runtime/grants.d.ts +1 -0
  37. package/runtime/grants.d.ts.map +1 -1
  38. package/runtime/grants.js +5 -2
  39. package/runtime/hotkeys.d.ts.map +1 -1
  40. package/runtime/hotkeys.js +7 -4
  41. package/runtime/installer.d.ts +10 -6
  42. package/runtime/installer.d.ts.map +1 -1
  43. package/runtime/installer.js +106 -48
  44. package/runtime/lifecycle.d.ts +2 -2
  45. package/runtime/lifecycle.d.ts.map +1 -1
  46. package/runtime/lifecycle.js +144 -42
  47. package/runtime/pages.d.ts +3 -1
  48. package/runtime/pages.d.ts.map +1 -1
  49. package/runtime/pages.js +48 -14
  50. package/runtime/parts.d.ts +1 -0
  51. package/runtime/parts.d.ts.map +1 -1
  52. package/runtime/parts.js +6 -3
  53. package/runtime/programs.d.ts +1 -0
  54. package/runtime/programs.d.ts.map +1 -1
  55. package/runtime/programs.js +4 -1
  56. package/runtime/sessions.d.ts +10 -3
  57. package/runtime/sessions.d.ts.map +1 -1
  58. package/runtime/sessions.js +104 -59
  59. package/runtime/toaster.d.ts.map +1 -1
  60. package/runtime/toaster.js +23 -9
  61. package/runtime/ui.d.ts +22 -4
  62. package/runtime/ui.d.ts.map +1 -1
  63. package/runtime/ui.js +161 -59
  64. package/runtime/watches.d.ts.map +1 -1
  65. package/runtime/watches.js +18 -4
  66. package/runtime/widgets.d.ts +7 -5
  67. package/runtime/widgets.d.ts.map +1 -1
  68. package/runtime/widgets.js +31 -21
  69. package/session/context/turn/message/attached-file.d.ts +13 -0
  70. package/session/context/turn/message/attached-file.d.ts.map +1 -0
  71. package/session/context/turn/message/attached-file.js +21 -0
  72. package/session/context/turn/message/message.d.ts +3 -2
  73. package/session/context/turn/message/message.d.ts.map +1 -1
  74. package/session/context/turn/message/message.js +20 -6
  75. package/session/conversation.d.ts +15 -3
  76. package/session/conversation.d.ts.map +1 -1
  77. package/session/conversation.js +23 -4
  78. package/session/goal/task.d.ts +5 -0
  79. package/session/goal/task.d.ts.map +1 -1
  80. package/session/session.d.ts +36 -7
  81. package/session/session.d.ts.map +1 -1
  82. package/session/session.js +2 -1
  83. package/src/installer.ts +3 -2
  84. package/src/jobs/program.ts +41 -21
  85. package/src/mod.ts +3 -2
  86. package/src/records.ts +49 -1
  87. package/src/register.ts +11 -8
  88. package/src/runtime/before.ts +61 -22
  89. package/src/runtime/buttons.ts +22 -0
  90. package/src/runtime/conversations.ts +170 -34
  91. package/src/runtime/engine.ts +2 -0
  92. package/src/runtime/grants.ts +6 -2
  93. package/src/runtime/hotkeys.ts +7 -4
  94. package/src/runtime/installer.ts +147 -83
  95. package/src/runtime/lifecycle.ts +149 -48
  96. package/src/runtime/pages.ts +56 -12
  97. package/src/runtime/parts.ts +8 -4
  98. package/src/runtime/programs.ts +5 -1
  99. package/src/runtime/sessions.ts +106 -59
  100. package/src/runtime/toaster.ts +27 -9
  101. package/src/runtime/ui.ts +166 -62
  102. package/src/runtime/watches.ts +20 -5
  103. package/src/runtime/widgets.ts +35 -23
  104. package/src/session/context/turn/message/attached-file.ts +36 -0
  105. package/src/session/context/turn/message/message.ts +20 -7
  106. package/src/session/conversation.ts +31 -6
  107. package/src/session/goal/task.ts +6 -0
  108. package/src/session/session.ts +18 -7
  109. package/src/testing/fake-claude.ts +21 -6
  110. package/src/testing/fake-elements.ts +57 -8
  111. package/src/testing/fake-sessions.ts +72 -24
  112. package/src/testing.ts +114 -33
  113. package/src/ui/areas.ts +1 -0
  114. package/src/ui/components.ts +14 -8
  115. package/src/ui/elements.ts +6 -2
  116. package/src/ui/markdown.ts +3 -3
  117. package/src/ui/panes.ts +2 -2
  118. package/src/ui/styles.ts +6 -2
  119. package/testing/fake-claude.d.ts +3 -0
  120. package/testing/fake-claude.d.ts.map +1 -1
  121. package/testing/fake-claude.js +18 -5
  122. package/testing/fake-elements.d.ts.map +1 -1
  123. package/testing/fake-elements.js +52 -7
  124. package/testing/fake-sessions.d.ts +24 -10
  125. package/testing/fake-sessions.d.ts.map +1 -1
  126. package/testing/fake-sessions.js +66 -20
  127. package/testing.d.ts +8 -12
  128. package/testing.d.ts.map +1 -1
  129. package/testing.js +113 -27
  130. package/ui/areas.d.ts +4 -0
  131. package/ui/areas.d.ts.map +1 -1
  132. package/ui/components.d.ts +1 -0
  133. package/ui/components.d.ts.map +1 -1
  134. package/ui/components.js +11 -7
  135. package/ui/elements.d.ts +3 -0
  136. package/ui/elements.d.ts.map +1 -1
  137. package/ui/elements.js +6 -1
  138. package/ui/markdown.d.ts +2 -2
  139. package/ui/markdown.d.ts.map +1 -1
  140. package/ui/panes.d.ts +2 -1
  141. package/ui/panes.d.ts.map +1 -1
  142. package/ui/styles.d.ts +11 -2
  143. package/ui/styles.d.ts.map +1 -1
package/runtime/ui.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { isObject } from '../records.js';
2
2
  import { TurnEnd } from '../ui/areas.js';
3
- import { Box, drawWith, elementData, Text } from '../ui/elements.js';
3
+ import { Box, drawWith, elementData, Markdown, Text } from '../ui/elements.js';
4
4
  import { checkPane, checkPart, sizeKeys } from '../ui/panes.js';
5
5
  import { componentOf, nameOf } from '../ui/styles.js';
6
6
  import { messageOf } from '../utils/text.js';
@@ -11,7 +11,16 @@ export const partMethods = {
11
11
  press: 'cmod:press',
12
12
  shown: 'cmod:shown',
13
13
  changed: 'cmod:partsChanged',
14
+ stylers: 'cmod:markdownStylers',
14
15
  };
16
+ export const stalePress = 'that button drew before the last redraw. Press the one it shows now.';
17
+ const unmeasuredColumns = 80;
18
+ export function viewportColumns(e) {
19
+ return e.viewport?.columns ?? unmeasuredColumns;
20
+ }
21
+ export function styledAt(e, columns = viewportColumns(e)) {
22
+ return { surface: e.surface, columns: Math.max(columns, 0) };
23
+ }
15
24
  const bullet = '⏺';
16
25
  function paneSize(pane, state) {
17
26
  const size = {};
@@ -75,32 +84,39 @@ function drawPieces(pieces, place) {
75
84
  return Box({ children: [Box({ minWidth: 2, children: Text({ color: 'text', children: bullet }) }), column(spaced)] });
76
85
  return column([opening, Box({ paddingLeft: 2, children: column(rest) })]);
77
86
  }
78
- function merged(updates) {
87
+ export function merged(updates) {
79
88
  const parts = updates.findLast((update) => update.parts !== undefined)?.parts;
80
89
  const requests = updates.flatMap((update) => update.requests ?? []);
81
90
  const isRedrawn = updates.some((update) => update.redraw === true);
82
91
  return { ...(parts === undefined ? {} : { parts }), ...(isRedrawn ? { redraw: true } : {}), ...(requests.length === 0 ? {} : { requests }) };
83
92
  }
84
93
  const keptDrawInputs = 200;
85
- export function createUi({ name, claude, router, report, announce, mod }) {
94
+ export function createUi({ name, claude, router, report, announce, mod, borrowed, nextDraw }) {
86
95
  const panes = new Map();
87
96
  const paneLogs = new Map();
88
97
  const openPanes = new Set();
89
98
  const paneStates = new Map();
90
99
  const syncPanes = async () => {
91
- for (const open of await claude.ui.panes()) {
92
- if (!panes.has(open.id))
93
- continue;
94
- if (open.isPlaced)
95
- openPanes.add(open.id);
96
- paneStates.set(open.id, { isShown: open.isShown, isFocused: open.isFocused });
100
+ const listed = new Map((await claude.ui.panes()).map((open) => [open.id, open]));
101
+ for (const id of panes.keys()) {
102
+ const open = listed.get(id);
103
+ if (open?.isPlaced === true)
104
+ openPanes.add(id);
105
+ else
106
+ openPanes.delete(id);
107
+ if (open === undefined)
108
+ paneStates.delete(id);
109
+ else
110
+ paneStates.set(id, { isShown: open.isShown, isFocused: open.isFocused });
97
111
  }
98
112
  };
99
- const readPanes = () => void syncPanes().catch((error) => claude.ui.log(`${name} keeps its panes' shown and focused state from before: ${messageOf(error)}`, { to: 'debug' }));
113
+ const readPanes = () => syncPanes().catch((error) => claude.ui.log(`${name} keeps its panes' open, shown, and focused state from before: ${messageOf(error)}`, { to: 'debug' }));
100
114
  const requestedSizes = new Map();
101
115
  const styles = new Set();
102
116
  const markdownRenders = new Map();
103
117
  let read;
118
+ let stylers = [];
119
+ let stylersSeen = 0;
104
120
  let isRouted = false;
105
121
  let isResizeQueued = false;
106
122
  const widgets = new Map();
@@ -110,17 +126,55 @@ export function createUi({ name, claude, router, report, announce, mod }) {
110
126
  const draws = new Map();
111
127
  const latestDraws = new Map();
112
128
  const failedDraws = new Set();
113
- let drawCount = 0;
114
129
  let beforeStart = [];
115
130
  let askedDepth = 0;
131
+ const askedCalls = new Set();
116
132
  let unheldRequests = 0;
117
133
  let collected;
118
134
  let collecting = 0;
119
135
  let queued;
120
136
  const partList = () => ({
121
- widgets: [...widgets.values()].map(({ id, title, areas: listed }) => ({ id, title, areas: listed })),
137
+ widgets: [...widgets.values()].map(({ id, title, areas: listed, place }) => ({ id, title, areas: listed, ...(place === undefined ? {} : { place }) })),
122
138
  pages: [...pages.values()].map(({ init: { id, title }, place }) => ({ id, title, place })),
139
+ ...(markdownRenders.size > 0 ? { stylesMarkdown: true } : {}),
123
140
  });
141
+ const hear = (listed, isRestyled) => {
142
+ if (!Array.isArray(listed))
143
+ return;
144
+ const heard = listed.filter((styler) => typeof styler === 'string' && styler !== name);
145
+ if (!isRestyled && heard.join('\0') === stylers.join('\0'))
146
+ return;
147
+ stylers = heard;
148
+ stylersSeen += 1;
149
+ if (name === cmodPluginName || panes.size > 0)
150
+ claude.ui.invalidate('ui.render');
151
+ };
152
+ const treeOf = (render, handlers) => {
153
+ const drawn = render();
154
+ return drawn === undefined ? null : elementData(drawn, handlers);
155
+ };
156
+ const isAnyDrawingChanged = () => {
157
+ for (const [key, inputs] of latestDraws) {
158
+ if (key === 'markdown')
159
+ continue;
160
+ for (const sent of inputs.values()) {
161
+ const handlers = [];
162
+ try {
163
+ if (JSON.stringify(treeOf(sent.render, handlers)) !== sent.tree)
164
+ return true;
165
+ }
166
+ catch (error) {
167
+ claude.ui.log(`${name}: the ${key} drawing threw, so the cmod plugin asks for it again: ${messageOf(error)}`, { to: 'debug' });
168
+ return true;
169
+ }
170
+ const latest = sent.draws.at(-1);
171
+ if (latest !== undefined && draws.has(latest))
172
+ draws.set(latest, { id: key, handlers });
173
+ }
174
+ }
175
+ return false;
176
+ };
177
+ const withNeededRedraw = ({ redraw, ...update }) => (redraw === true && isAnyDrawingChanged() ? { ...update, redraw } : update);
124
178
  const send = (update) => {
125
179
  if (collected !== undefined)
126
180
  return void collected.push(update);
@@ -132,21 +186,33 @@ export function createUi({ name, claude, router, report, announce, mod }) {
132
186
  const batch = [update];
133
187
  if (!isAsked)
134
188
  queued = batch;
135
- const tell = () => {
189
+ const tell = async () => {
136
190
  if (queued === batch)
137
191
  queued = undefined;
138
- const input = { ...merged(batch), ...(isAsked ? { isAsked: true } : {}) };
192
+ const needed = withNeededRedraw(merged(batch));
193
+ if (Object.keys(needed).length === 0)
194
+ return;
195
+ const input = { ...needed, ...(isAsked ? { isAsked: true } : {}) };
139
196
  return claude.cmod.call({ to: cmodPluginName, method: partMethods.changed, input }).then((answer) => {
140
197
  if (isAsked && input.requests !== undefined && !(isObject(answer) && answer['isHeld'] === true))
141
198
  unheldRequests += 1;
199
+ if (isObject(answer) && name !== cmodPluginName)
200
+ hear(answer['stylers'], false);
142
201
  }, (error) => claude.ui.log(`${name}: the cmod plugin did not hear about its pages and widgets: ${messageOf(error)}`, { to: 'debug' }));
143
202
  };
144
- return isAsked ? tell() : Promise.resolve().then(tell);
203
+ if (!isAsked)
204
+ return Promise.resolve().then(tell);
205
+ const told = tell().finally(() => askedCalls.delete(told));
206
+ askedCalls.add(told);
207
+ return told;
145
208
  };
146
209
  const asked = async (run) => {
147
210
  askedDepth += 1;
148
211
  try {
149
- return await run();
212
+ const value = await run();
213
+ while (askedCalls.size > 0)
214
+ await Promise.allSettled(askedCalls);
215
+ return value;
150
216
  }
151
217
  finally {
152
218
  askedDepth -= 1;
@@ -163,7 +229,7 @@ export function createUi({ name, claude, router, report, announce, mod }) {
163
229
  }
164
230
  const mine = collected;
165
231
  collected = collecting === 0 ? undefined : [];
166
- return merged(mine);
232
+ return withNeededRedraw(merged(mine));
167
233
  };
168
234
  const logOnce = (what) => {
169
235
  let hasLogged = false;
@@ -174,30 +240,49 @@ export function createUi({ name, claude, router, report, announce, mod }) {
174
240
  };
175
241
  };
176
242
  const renderOf = (type) => markdownRenders.get(type);
177
- const markdownIn = (table) => {
243
+ const drawnBlock = (piece, at, drawClaudes, table) => {
244
+ if ('text' in piece)
245
+ return drawClaudes(piece.text);
246
+ const { render, props: block } = piece;
247
+ const draw = () => render.Component({ ...block, ...at, Default: ({ source = block.source }) => drawClaudes(source) });
248
+ try {
249
+ return table === undefined ? draw() : drawWith(table, draw);
250
+ }
251
+ catch (error) {
252
+ render.log(threw(error));
253
+ return drawClaudes(block.source);
254
+ }
255
+ };
256
+ const othersFrom = (table, at, from) => (props) => {
257
+ for (const [index, styler] of stylers.entries()) {
258
+ if (index < from)
259
+ continue;
260
+ const cached = `${stylersSeen}\u0000${at.surface}\u0000${at.columns}\u0000${props.text}`;
261
+ const drawn = drawWith(table, () => borrowed(styler, cached, { markdown: props.text, ...at }), othersFrom(table, at, index + 1));
262
+ if (drawn !== undefined)
263
+ return drawn;
264
+ }
265
+ return table.Markdown(props);
266
+ };
267
+ const markdownIn = (table, at) => {
178
268
  const reader = read;
269
+ const others = othersFrom(table, at, 0);
179
270
  if (reader === undefined)
180
- return undefined;
271
+ return stylers.length === 0 ? undefined : others;
181
272
  return (props) => {
182
273
  const pieces = reader(props.text, renderOf);
183
274
  if (pieces.every((piece) => 'text' in piece))
184
- return table.Markdown(props);
185
- const drawClaudes = (text) => table.Markdown({ ...props, text });
186
- const drawings = pieces.map((piece) => {
187
- if ('text' in piece)
188
- return drawClaudes(piece.text);
189
- const { render, props: block } = piece;
190
- try {
191
- return drawWith(table, () => render.Component({ ...block, Default: ({ source = block.source }) => drawClaudes(source) }));
192
- }
193
- catch (error) {
194
- render.log(threw(error));
195
- return drawClaudes(block.source);
196
- }
197
- });
275
+ return others(props);
276
+ const drawings = pieces.map((piece) => drawnBlock(piece, at, (text) => others({ ...props, text }), table));
198
277
  return drawPieces(drawings.map((drawing) => ({ drawing, isClaudesRow: false })), 'pane');
199
278
  };
200
279
  };
280
+ const markdownDrawing = (text, at) => {
281
+ const pieces = read === undefined ? [] : read(text, renderOf);
282
+ if (pieces.every((piece) => 'text' in piece))
283
+ return Markdown({ text });
284
+ return drawPieces(pieces.map((piece) => ({ drawing: drawnBlock(piece, at, (source) => Markdown({ text: source })), isClaudesRow: false })), 'pane');
285
+ };
201
286
  const routeMarkdown = (createReader) => {
202
287
  if (read !== undefined)
203
288
  return;
@@ -215,7 +300,7 @@ export function createUi({ name, claude, router, report, announce, mod }) {
215
300
  if ('text' in piece)
216
301
  return { drawing: await drawClaudes(piece.text, index === 0), isClaudesRow: true };
217
302
  const { render, props: block } = piece;
218
- const component = await drawComponent(table, (Default) => render.Component({ ...block, Default }), ({ source = block.source }, isClaudesRow) => drawClaudes(source, index === 0 && isClaudesRow));
303
+ const component = await drawComponent(table, (Default) => render.Component({ ...block, ...styledAt(e), id: e.requestId, Default }), ({ source = block.source }, isClaudesRow) => drawClaudes(source, index === 0 && isClaudesRow));
219
304
  if (!('failure' in component))
220
305
  return component;
221
306
  render.log(component.failure);
@@ -260,10 +345,10 @@ export function createUi({ name, claude, router, report, announce, mod }) {
260
345
  const { init } = added;
261
346
  openPanes.add(init.id);
262
347
  paneStates.set(init.id, { isShown: true, isFocused: e.props.isFocused });
263
- readPanes();
348
+ void readPanes();
264
349
  const table = claude.ui.resolve(e);
265
350
  try {
266
- return drawWith(table, () => init.render(mod(), { ...e.props, params: added.params }), markdownIn(table));
351
+ return drawWith(table, () => init.render(mod(), { ...e.props, surface: e.surface, params: added.params }), markdownIn(table, styledAt(e, e.props.bodyColumns)));
267
352
  }
268
353
  catch (error) {
269
354
  paneLogs.get(init.id)?.(`threw: ${messageOf(error)}`);
@@ -281,8 +366,8 @@ export function createUi({ name, claude, router, report, announce, mod }) {
281
366
  router.add('ui.close', async (e, next) => {
282
367
  openPanes.delete(e.id);
283
368
  paneStates.delete(e.id);
284
- readPanes();
285
369
  const closed = await next(e);
370
+ await readPanes();
286
371
  const init = panes.get(e.id)?.init;
287
372
  if (init?.onClose !== undefined)
288
373
  await handle(init, 'onClose', () => init.onClose?.(mod(), e));
@@ -427,14 +512,15 @@ export function createUi({ name, claude, router, report, announce, mod }) {
427
512
  const Component = (props) => render(mod(), props);
428
513
  const stop = (subscription) => () => {
429
514
  subscription?.remove();
430
- if (markdown !== undefined)
431
- markdownRenders.delete(markdown.markdown);
515
+ if (markdown !== undefined && markdownRenders.delete(markdown.markdown))
516
+ void send({ parts: partList() });
432
517
  styles.delete(feature);
433
518
  claude.ui.invalidate('ui.render');
434
519
  };
435
520
  if (markdown !== undefined) {
436
521
  markdownRenders.set(markdown.markdown, { Component, log: logOnce(`${nameOf(kind)} style`) });
437
522
  routeMarkdown(markdown.createReader);
523
+ void send({ parts: partList() });
438
524
  return { kind, remove: stop() };
439
525
  }
440
526
  const log = logOnce(`${nameOf(kind)} style`);
@@ -442,7 +528,7 @@ export function createUi({ name, claude, router, report, announce, mod }) {
442
528
  if (e.component !== component)
443
529
  return next(e);
444
530
  const table = claude.ui.resolve(e);
445
- const drawn = await drawComponent(table, (Default) => Component({ ...e.props, Default }), (given, isClaudesRow) => next({ ...e, props: pieceProps(e.props, given, isClaudesRow) }));
531
+ const drawn = await drawComponent(table, (Default) => Component({ ...e.props, ...styledAt(e), id: e.requestId, Default }), (given, isClaudesRow) => next({ ...e, props: pieceProps(e.props, given, isClaudesRow) }));
446
532
  if ('failure' in drawn) {
447
533
  log(drawn.failure);
448
534
  return next(e);
@@ -455,48 +541,49 @@ export function createUi({ name, claude, router, report, announce, mod }) {
455
541
  };
456
542
  const forgetDraws = (key) => {
457
543
  for (const kept of latestDraws.get(key)?.values() ?? [])
458
- for (const draw of kept)
544
+ for (const draw of kept.draws)
459
545
  draws.delete(draw);
460
546
  latestDraws.delete(key);
461
547
  };
462
548
  const drawPart = (key, title, input, render) => {
463
549
  try {
464
550
  const pressed = [];
465
- const drawn = render();
466
- const tree = drawn === undefined ? null : elementData(drawn, pressed);
551
+ const tree = treeOf(render, pressed);
467
552
  failedDraws.delete(key);
468
- drawCount += 1;
553
+ const number = nextDraw();
469
554
  const inputs = latestDraws.get(key) ?? new Map();
470
555
  latestDraws.set(key, inputs);
471
556
  const drawnFor = JSON.stringify('turn' in input ? { ...input, turn: input.turn.prompt.id } : input);
472
- const earlier = inputs.get(drawnFor) ?? [];
473
- const kept = [...earlier, drawCount].slice(-2);
557
+ const earlier = inputs.get(drawnFor)?.draws ?? [];
558
+ const kept = [...earlier, number].slice(-2);
474
559
  for (const draw of earlier)
475
560
  if (!kept.includes(draw))
476
561
  draws.delete(draw);
477
562
  inputs.delete(drawnFor);
478
- inputs.set(drawnFor, kept);
563
+ inputs.set(drawnFor, { draws: kept, tree: JSON.stringify(tree), render });
479
564
  const [oldest] = inputs;
480
565
  if (inputs.size > keptDrawInputs && oldest !== undefined) {
481
- for (const draw of oldest[1])
566
+ for (const draw of oldest[1].draws)
482
567
  draws.delete(draw);
483
568
  inputs.delete(oldest[0]);
484
569
  }
485
- draws.set(drawCount, { id: key, handlers: pressed });
486
- return { draw: drawCount, tree };
570
+ draws.set(number, { id: key, handlers: pressed });
571
+ return { draw: number, tree };
487
572
  }
488
573
  catch (error) {
489
574
  if (!failedDraws.has(key))
490
575
  report({ id: key, message: `The ${title} could not draw`, error });
491
576
  failedDraws.add(key);
492
- return null;
577
+ throw error;
493
578
  }
494
579
  };
495
580
  const draw = (input) => {
581
+ if ('markdown' in input)
582
+ return drawPart('markdown', 'markdown style', input, () => markdownDrawing(input.markdown, { surface: input.surface, columns: input.columns }));
496
583
  if ('page' in input) {
497
584
  const added = pages.get(input.page);
498
585
  if (added === undefined)
499
- return null;
586
+ throw new Error(`${name} has no ${input.page} page.`);
500
587
  return drawPart(`page:${input.page}`, `${added.init.title} page`, input, () => added.init.render(mod(), { params: input.params, isFocused: input.isFocused, columns: input.columns }));
501
588
  }
502
589
  const widget = widgets.get(input.widget);
@@ -506,19 +593,25 @@ export function createUi({ name, claude, router, report, announce, mod }) {
506
593
  const context = (input.area === TurnEnd && input.turn !== undefined ? { area: input.area, columns: input.columns, turn: input.turn } : { area: input.area, columns: input.columns });
507
594
  return drawPart(`widget:${widget.id}`, `${widget.title} widget`, input, () => widget.render(mod(), context));
508
595
  };
509
- const press = async ({ draw: drawn, handler, args }) => {
596
+ const press = async ({ draw: drawn, handler, args }, caller) => {
510
597
  const known = draws.get(drawn);
511
598
  const run = known?.handlers[handler];
512
599
  if (known === undefined || run === undefined)
513
- throw new Error(`${name}: that button drew before the last redraw. Press the one it shows now.`);
514
- return collect(async () => {
600
+ throw new Error(`${name}: ${stalePress}`);
601
+ if (caller !== cmodPluginName && known.id !== 'markdown')
602
+ throw new Error(`${name}: ${caller} pressed a button only the cmod plugin draws.`);
603
+ const pressed = async () => {
515
604
  try {
516
605
  await run(...args);
517
606
  }
518
607
  catch (error) {
519
608
  report({ id: known.id, message: 'Its button failed', error });
520
609
  }
521
- });
610
+ };
611
+ if (caller === cmodPluginName)
612
+ return collect(pressed);
613
+ await pressed();
614
+ return null;
522
615
  };
523
616
  return {
524
617
  panes: { add: addPane },
@@ -539,19 +632,28 @@ export function createUi({ name, claude, router, report, announce, mod }) {
539
632
  await passOn();
540
633
  return answer;
541
634
  },
542
- answer(method, input) {
635
+ answer(method, input, caller) {
636
+ const isMarkdown = method === partMethods.draw && isObject(input) && typeof input['markdown'] === 'string';
637
+ if (caller !== cmodPluginName && !isMarkdown && method !== partMethods.press)
638
+ return undefined;
543
639
  if (method === partMethods.list)
544
640
  return Promise.resolve({ value: partList() });
545
641
  if (method === partMethods.draw)
546
- return Promise.resolve({ value: draw(input) });
642
+ return Promise.resolve().then(() => ({ value: draw(input) }));
547
643
  if (method === partMethods.press)
548
- return press(input).then((value) => ({ value }));
644
+ return press(input, caller).then((value) => ({ value }));
549
645
  if (method === partMethods.shown) {
550
646
  shownPages = input;
551
647
  return Promise.resolve({ value: null });
552
648
  }
649
+ if (method === partMethods.stylers) {
650
+ hear(input, true);
651
+ return Promise.resolve({ value: null });
652
+ }
553
653
  return undefined;
554
654
  },
655
+ markdownIn,
656
+ restyled: (stylers) => hear(stylers, true),
555
657
  started() {
556
658
  const held = merged(beforeStart ?? []);
557
659
  beforeStart = undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"watches.d.ts","sourceRoot":"","sources":["../src/runtime/watches.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAE3C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C,MAAM,MAAM,OAAO,GAAG;IAAE,GAAG,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,GAAG,YAAY,CAAA;CAAE,CAAA;AAQhH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CA4DzF"}
1
+ {"version":3,"file":"watches.d.ts","sourceRoot":"","sources":["../src/runtime/watches.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAE3C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C,MAAM,MAAM,OAAO,GAAG;IAAE,GAAG,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,GAAG,YAAY,CAAA;CAAE,CAAA;AAUhH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAyEzF"}
@@ -1,12 +1,17 @@
1
1
  import { ifMissing } from '../utils/paths.js';
2
2
  const watchMs = 500;
3
+ const missingEvery = 4;
3
4
  const watchedFiles = 2000;
4
5
  export function createWatches(claude, report) {
5
6
  const watches = new Set();
6
7
  let polling;
7
8
  let isPolling = false;
8
- const stampsOf = async (paths) => {
9
+ let ticks = 0;
10
+ const stampsOf = async (watch) => {
11
+ const { paths } = watch;
9
12
  const stamps = new Map();
13
+ const missing = new Set();
14
+ const skipped = ticks % missingEvery === 0 ? new Set() : watch.missing;
10
15
  const visit = async (path) => {
11
16
  const stat = await claude.fs.stat(path).catch(ifMissing(undefined));
12
17
  if (stat === undefined)
@@ -19,8 +24,16 @@ export function createWatches(claude, report) {
19
24
  for (const entry of await claude.fs.list(path).catch(ifMissing([])))
20
25
  await visit(`${path}/${entry.name}`);
21
26
  };
22
- for (const path of paths)
27
+ for (const path of paths) {
28
+ if (skipped.has(path)) {
29
+ missing.add(path);
30
+ continue;
31
+ }
23
32
  await visit(path);
33
+ if (![...stamps.keys()].some((seen) => seen === path || seen.startsWith(`${path}/`)))
34
+ missing.add(path);
35
+ }
36
+ watch.missing = missing;
24
37
  return stamps;
25
38
  };
26
39
  const stop = (watch) => {
@@ -30,7 +43,7 @@ export function createWatches(claude, report) {
30
43
  polling?.cancel();
31
44
  polling = undefined;
32
45
  };
33
- const stampsOrStop = (watch) => stampsOf(watch.paths).catch((error) => {
46
+ const stampsOrStop = (watch) => stampsOf(watch).catch((error) => {
34
47
  stop(watch);
35
48
  report({ id: 'fs:onChange', message: 'Could not read the files fs.onChange watches', error });
36
49
  return undefined;
@@ -39,6 +52,7 @@ export function createWatches(claude, report) {
39
52
  if (isPolling)
40
53
  return;
41
54
  isPolling = true;
55
+ ticks += 1;
42
56
  try {
43
57
  for (const watch of watches) {
44
58
  const now = await stampsOrStop(watch);
@@ -63,7 +77,7 @@ export function createWatches(claude, report) {
63
77
  };
64
78
  return {
65
79
  add(paths, listener) {
66
- const watch = { paths, listener, seen: undefined };
80
+ const watch = { paths, listener, seen: undefined, missing: new Set() };
67
81
  watches.add(watch);
68
82
  void stampsOrStop(watch).then((stamps) => void (watch.seen ??= stamps));
69
83
  polling ??= claude.clock.every(watchMs, () => void poll());
@@ -1,10 +1,11 @@
1
- import type { ElementTable, RenderElement } from 'claude-code';
1
+ import type { ElementTable, MarkdownProps, RenderElement } from 'claude-code';
2
2
  import type { TurnData } from '../session/conversation.js';
3
3
  import { type Area } from '../ui/areas.js';
4
+ import type { StyledAt } from '../ui/styles.js';
4
5
  import type { Claude } from './claude.js';
5
6
  import type { DrawnParts } from './parts.js';
6
7
  import type { Router } from './router.js';
7
- import { type PartsUpdate } from './ui.js';
8
+ import { type PartsUpdate, type WidgetDeclaration } from './ui.js';
8
9
  export type WidgetRow = {
9
10
  readonly left: readonly string[];
10
11
  readonly right: readonly string[];
@@ -18,7 +19,7 @@ export type PlacedWidget = {
18
19
  readonly mod: string;
19
20
  readonly title: string;
20
21
  readonly areas: readonly Area[];
21
- };
22
+ } & Pick<WidgetDeclaration, 'place'>;
22
23
  export type Move = 'up' | 'down' | 'left' | 'right';
23
24
  export declare const claudeHint = "claude/hint";
24
25
  export declare const claudeModes = "claude/modes";
@@ -31,18 +32,19 @@ export type Widgets = {
31
32
  reload(): Promise<void>;
32
33
  recheck(): Promise<void>;
33
34
  rows(area: Area, columns: number): RenderElement[];
34
- draw(area: Area, table: ElementTable, columns: number, turn?: TurnData): Promise<RenderElement[]>;
35
+ draw(area: Area, table: ElementTable, at: StyledAt, turn?: TurnData): Promise<RenderElement[]>;
35
36
  };
36
37
  export declare function layoutOf(value: unknown): WidgetLayout;
37
38
  export declare function placedIn(layout: WidgetLayout, key: string): Area | undefined;
38
39
  export declare function toggled(layout: WidgetLayout, key: string): WidgetLayout;
39
40
  export declare function moved(layout: WidgetLayout, widget: PlacedWidget, move: Move): WidgetLayout;
40
- export declare function serveWidgets({ claude, router, store, report, turnClosedBy, parts, }: {
41
+ export declare function serveWidgets({ claude, router, store, report, turnClosedBy, parts, markdownIn, }: {
41
42
  readonly claude: Claude;
42
43
  readonly router: Router;
43
44
  readonly store: string;
44
45
  readonly report: (id: string, message: string, error: unknown) => void;
45
46
  readonly turnClosedBy: (rowId: string) => TurnData | undefined;
46
47
  readonly parts: DrawnParts;
48
+ readonly markdownIn: (table: ElementTable, at: StyledAt) => ((props: MarkdownProps) => RenderElement) | undefined;
47
49
  }): Widgets;
48
50
  //# sourceMappingURL=widgets.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"widgets.d.ts","sourceRoot":"","sources":["../src/runtime/widgets.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"widgets.d.ts","sourceRoot":"","sources":["../src/runtime/widgets.ts"],"names":[],"mappings":"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"}