@cmodjs/core 3.1.0 → 4.0.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 (200) hide show
  1. package/docs/commands.md +38 -21
  2. package/docs/dependencies.md +27 -20
  3. package/docs/index.md +21 -17
  4. package/docs/install-steps.md +37 -29
  5. package/docs/jobs.md +25 -52
  6. package/docs/mod.md +93 -65
  7. package/docs/options.md +2 -2
  8. package/docs/permissions.md +4 -4
  9. package/docs/session.md +14 -12
  10. package/docs/testing.md +55 -32
  11. package/docs/ui.md +255 -263
  12. package/hotkeys.d.ts +22 -0
  13. package/hotkeys.d.ts.map +1 -0
  14. package/hotkeys.js +26 -0
  15. package/installer.d.ts +1 -1
  16. package/installer.d.ts.map +1 -1
  17. package/installer.js +11 -6
  18. package/jobs/check.d.ts +2 -1
  19. package/jobs/check.d.ts.map +1 -1
  20. package/jobs/check.js +2 -2
  21. package/jobs/context.d.ts +2 -1
  22. package/jobs/context.d.ts.map +1 -1
  23. package/jobs/context.js +1 -1
  24. package/jobs/permissions.d.ts +2 -1
  25. package/jobs/permissions.d.ts.map +1 -1
  26. package/jobs/permissions.js +2 -2
  27. package/jobs/program.js +1 -1
  28. package/jobs/prompt.d.ts +2 -1
  29. package/jobs/prompt.d.ts.map +1 -1
  30. package/jobs/prompt.js +20 -13
  31. package/jobs/schedule.d.ts +2 -1
  32. package/jobs/schedule.d.ts.map +1 -1
  33. package/jobs/schedule.js +15 -1
  34. package/jobs/slash-command.d.ts +6 -2
  35. package/jobs/slash-command.d.ts.map +1 -1
  36. package/jobs/slash-command.js +6 -5
  37. package/jobs/tool.d.ts +6 -4
  38. package/jobs/tool.d.ts.map +1 -1
  39. package/jobs/tool.js +3 -3
  40. package/mod.d.ts +63 -42
  41. package/mod.d.ts.map +1 -1
  42. package/mod.js +11 -1
  43. package/package.json +1 -1
  44. package/records.d.ts +13 -8
  45. package/records.d.ts.map +1 -1
  46. package/records.js +41 -31
  47. package/register.d.ts.map +1 -1
  48. package/register.js +1 -2
  49. package/runtime/announce.d.ts +1 -1
  50. package/runtime/announce.d.ts.map +1 -1
  51. package/runtime/announce.js +6 -7
  52. package/runtime/before.d.ts +2 -2
  53. package/runtime/before.d.ts.map +1 -1
  54. package/runtime/before.js +1 -1
  55. package/runtime/claude.d.ts +0 -1
  56. package/runtime/claude.d.ts.map +1 -1
  57. package/runtime/conversations.d.ts +4 -1
  58. package/runtime/conversations.d.ts.map +1 -1
  59. package/runtime/conversations.js +42 -14
  60. package/runtime/dependencies.d.ts.map +1 -1
  61. package/runtime/dependencies.js +2 -1
  62. package/runtime/engine.d.ts +5 -0
  63. package/runtime/engine.d.ts.map +1 -1
  64. package/runtime/grants.js +2 -2
  65. package/runtime/hooks.d.ts +1 -1
  66. package/runtime/hooks.d.ts.map +1 -1
  67. package/runtime/hotkeys.d.ts +15 -0
  68. package/runtime/hotkeys.d.ts.map +1 -0
  69. package/runtime/hotkeys.js +58 -0
  70. package/runtime/installer.d.ts +5 -2
  71. package/runtime/installer.d.ts.map +1 -1
  72. package/runtime/installer.js +23 -12
  73. package/runtime/lifecycle.d.ts.map +1 -1
  74. package/runtime/lifecycle.js +190 -141
  75. package/runtime/options.js +2 -2
  76. package/runtime/pages.d.ts +28 -0
  77. package/runtime/pages.d.ts.map +1 -0
  78. package/runtime/pages.js +98 -0
  79. package/runtime/parts.d.ts +14 -0
  80. package/runtime/parts.d.ts.map +1 -0
  81. package/runtime/parts.js +63 -0
  82. package/runtime/permission-check.d.ts +2 -2
  83. package/runtime/permission-check.d.ts.map +1 -1
  84. package/runtime/programs.js +1 -1
  85. package/runtime/router.d.ts +2 -2
  86. package/runtime/router.d.ts.map +1 -1
  87. package/runtime/router.js +1 -1
  88. package/runtime/sessions.d.ts +10 -9
  89. package/runtime/sessions.d.ts.map +1 -1
  90. package/runtime/sessions.js +14 -5
  91. package/runtime/state.d.ts +2 -1
  92. package/runtime/state.d.ts.map +1 -1
  93. package/runtime/state.js +1 -0
  94. package/runtime/toaster.d.ts +1 -1
  95. package/runtime/toaster.d.ts.map +1 -1
  96. package/runtime/toaster.js +10 -4
  97. package/runtime/ui.d.ts +87 -5
  98. package/runtime/ui.d.ts.map +1 -1
  99. package/runtime/ui.js +372 -109
  100. package/runtime/watches.d.ts +9 -0
  101. package/runtime/watches.d.ts.map +1 -0
  102. package/runtime/watches.js +73 -0
  103. package/runtime/widgets.d.ts +48 -0
  104. package/runtime/widgets.d.ts.map +1 -0
  105. package/runtime/widgets.js +219 -0
  106. package/session/context/turn/message/message.d.ts.map +1 -1
  107. package/session/context/turn/message/message.js +7 -2
  108. package/session/session.d.ts +11 -5
  109. package/session/session.d.ts.map +1 -1
  110. package/src/hotkeys.ts +51 -0
  111. package/src/installer.ts +8 -4
  112. package/src/jobs/check.ts +4 -3
  113. package/src/jobs/context.ts +3 -2
  114. package/src/jobs/permissions.ts +4 -3
  115. package/src/jobs/program.ts +1 -1
  116. package/src/jobs/prompt.ts +23 -14
  117. package/src/jobs/schedule.ts +16 -2
  118. package/src/jobs/slash-command.ts +10 -7
  119. package/src/jobs/tool.ts +7 -5
  120. package/src/mod.ts +59 -47
  121. package/src/records.ts +46 -31
  122. package/src/register.ts +2 -3
  123. package/src/runtime/announce.ts +6 -7
  124. package/src/runtime/before.ts +4 -4
  125. package/src/runtime/claude.ts +0 -1
  126. package/src/runtime/conversations.ts +66 -20
  127. package/src/runtime/dependencies.ts +2 -1
  128. package/src/runtime/engine.ts +5 -0
  129. package/src/runtime/grants.ts +2 -2
  130. package/src/runtime/hooks.ts +1 -0
  131. package/src/runtime/hotkeys.ts +80 -0
  132. package/src/runtime/installer.ts +24 -12
  133. package/src/runtime/lifecycle.ts +189 -136
  134. package/src/runtime/options.ts +2 -2
  135. package/src/runtime/pages.ts +127 -0
  136. package/src/runtime/parts.ts +72 -0
  137. package/src/runtime/permission-check.ts +2 -2
  138. package/src/runtime/programs.ts +1 -1
  139. package/src/runtime/router.ts +3 -3
  140. package/src/runtime/sessions.ts +24 -15
  141. package/src/runtime/state.ts +3 -1
  142. package/src/runtime/toaster.ts +10 -4
  143. package/src/runtime/ui.ts +405 -115
  144. package/src/runtime/watches.ts +75 -0
  145. package/src/runtime/widgets.ts +287 -0
  146. package/src/session/context/turn/message/message.ts +7 -2
  147. package/src/session/session.ts +10 -6
  148. package/src/testing/fake-claude.ts +9 -6
  149. package/src/testing/fake-elements.ts +7 -4
  150. package/src/testing/fake-sessions.ts +6 -4
  151. package/src/testing.ts +203 -46
  152. package/src/ui/areas.ts +27 -0
  153. package/src/ui/elements.ts +35 -0
  154. package/src/ui/markdown.ts +18 -18
  155. package/src/ui/panes.ts +73 -0
  156. package/src/ui/styles.ts +53 -0
  157. package/src/utils/subscription.ts +1 -0
  158. package/testing/fake-claude.d.ts +0 -1
  159. package/testing/fake-claude.d.ts.map +1 -1
  160. package/testing/fake-claude.js +9 -5
  161. package/testing/fake-elements.js +7 -4
  162. package/testing/fake-sessions.d.ts +2 -1
  163. package/testing/fake-sessions.d.ts.map +1 -1
  164. package/testing/fake-sessions.js +1 -0
  165. package/testing.d.ts +21 -6
  166. package/testing.d.ts.map +1 -1
  167. package/testing.js +206 -45
  168. package/ui/areas.d.ts +29 -0
  169. package/ui/areas.d.ts.map +1 -0
  170. package/ui/areas.js +6 -0
  171. package/ui/elements.d.ts +3 -0
  172. package/ui/elements.d.ts.map +1 -1
  173. package/ui/elements.js +36 -0
  174. package/ui/markdown.d.ts +14 -14
  175. package/ui/markdown.d.ts.map +1 -1
  176. package/ui/markdown.js +11 -11
  177. package/ui/panes.d.ts +71 -0
  178. package/ui/panes.d.ts.map +1 -0
  179. package/ui/panes.js +16 -0
  180. package/ui/styles.d.ts +44 -0
  181. package/ui/styles.d.ts.map +1 -0
  182. package/ui/styles.js +24 -0
  183. package/utils/subscription.d.ts +4 -0
  184. package/utils/subscription.d.ts.map +1 -0
  185. package/jobs/status-line.d.ts +0 -6
  186. package/jobs/status-line.d.ts.map +0 -1
  187. package/jobs/status-line.js +0 -39
  188. package/src/jobs/status-line.ts +0 -44
  189. package/src/ui/define-pane.ts +0 -27
  190. package/src/ui/places.ts +0 -49
  191. package/src/utils/disposable.ts +0 -3
  192. package/ui/define-pane.d.ts +0 -15
  193. package/ui/define-pane.d.ts.map +0 -1
  194. package/ui/define-pane.js +0 -12
  195. package/ui/places.d.ts +0 -43
  196. package/ui/places.d.ts.map +0 -1
  197. package/ui/places.js +0 -31
  198. package/utils/disposable.d.ts +0 -4
  199. package/utils/disposable.d.ts.map +0 -1
  200. /package/utils/{disposable.js → subscription.js} +0 -0
package/runtime/ui.js CHANGED
@@ -1,8 +1,18 @@
1
- import { Box, drawWith, Text } from '../ui/elements.js';
2
- import { componentOf, nameOf } from '../ui/places.js';
1
+ import { isObject } from '../records.js';
2
+ import { TurnEnd } from '../ui/areas.js';
3
+ import { Box, drawWith, elementData, Text } from '../ui/elements.js';
4
+ import { checkPane, checkPart, sizeKeys } from '../ui/panes.js';
5
+ import { componentOf, nameOf } from '../ui/styles.js';
3
6
  import { messageOf } from '../utils/text.js';
7
+ import { cmodPluginName } from './dependencies.js';
8
+ export const partMethods = {
9
+ list: 'cmod:parts',
10
+ draw: 'cmod:draw',
11
+ press: 'cmod:press',
12
+ shown: 'cmod:shown',
13
+ changed: 'cmod:partsChanged',
14
+ };
4
15
  const bullet = '⏺';
5
- const sizeKeys = ['columns', 'rows'];
6
16
  function paneSize(pane, state) {
7
17
  const size = {};
8
18
  for (const key of sizeKeys) {
@@ -65,7 +75,14 @@ function drawPieces(pieces, place) {
65
75
  return Box({ children: [Box({ minWidth: 2, children: Text({ color: 'text', children: bullet }) }), column(spaced)] });
66
76
  return column([opening, Box({ paddingLeft: 2, children: column(rest) })]);
67
77
  }
68
- export function createUi({ name, claude, router, toaster, report, announce, mod }) {
78
+ function merged(updates) {
79
+ const parts = updates.findLast((update) => update.parts !== undefined)?.parts;
80
+ const requests = updates.flatMap((update) => update.requests ?? []);
81
+ const isRedrawn = updates.some((update) => update.redraw === true);
82
+ return { ...(parts === undefined ? {} : { parts }), ...(isRedrawn ? { redraw: true } : {}), ...(requests.length === 0 ? {} : { requests }) };
83
+ }
84
+ const keptDrawInputs = 200;
85
+ export function createUi({ name, claude, router, report, announce, mod }) {
69
86
  const panes = new Map();
70
87
  const paneLogs = new Map();
71
88
  const openPanes = new Set();
@@ -81,16 +98,76 @@ export function createUi({ name, claude, router, toaster, report, announce, mod
81
98
  };
82
99
  const readPanes = () => void syncPanes().catch((error) => claude.ui.log(`${name} keeps its panes' shown and focused state from before: ${messageOf(error)}`, { to: 'debug' }));
83
100
  const requestedSizes = new Map();
84
- const renders = new Set();
101
+ const styles = new Set();
85
102
  const markdownRenders = new Map();
86
103
  let read;
87
104
  let isRouted = false;
88
105
  let isResizeQueued = false;
106
+ const widgets = new Map();
107
+ const widgetAreas = new Map();
108
+ const pages = new Map();
109
+ let shownPages = { open: [], shown: null, isFocused: false };
110
+ const draws = new Map();
111
+ const latestDraws = new Map();
112
+ const failedDraws = new Set();
113
+ let drawCount = 0;
114
+ let beforeStart = [];
115
+ let askedDepth = 0;
116
+ let unheldRequests = 0;
117
+ let collected;
118
+ let queued;
119
+ const partList = () => ({
120
+ widgets: [...widgets.values()].map(({ id, title, areas: listed }) => ({ id, title, areas: listed })),
121
+ pages: [...pages.values()].map(({ init: { id, title }, place }) => ({ id, title, place })),
122
+ });
123
+ const send = (update) => {
124
+ if (collected !== undefined)
125
+ return void collected.push(update);
126
+ if (beforeStart !== undefined)
127
+ return void beforeStart.push(update);
128
+ const isAsked = askedDepth > 0;
129
+ if (queued !== undefined && !isAsked)
130
+ return void queued.push(update);
131
+ const batch = [update];
132
+ if (!isAsked)
133
+ queued = batch;
134
+ const tell = () => {
135
+ if (queued === batch)
136
+ queued = undefined;
137
+ const input = { ...merged(batch), ...(isAsked ? { isAsked: true } : {}) };
138
+ return claude.cmod.call({ to: cmodPluginName, method: partMethods.changed, input }).then((answer) => {
139
+ if (isAsked && input.requests !== undefined && !(isObject(answer) && answer['isHeld'] === true))
140
+ unheldRequests += 1;
141
+ }, (error) => claude.ui.log(`${name}: the cmod plugin did not hear about its pages and widgets: ${messageOf(error)}`, { to: 'debug' }));
142
+ };
143
+ return isAsked ? tell() : Promise.resolve().then(tell);
144
+ };
145
+ const asked = async (run) => {
146
+ askedDepth += 1;
147
+ try {
148
+ return await run();
149
+ }
150
+ finally {
151
+ askedDepth -= 1;
152
+ }
153
+ };
154
+ const collect = async (run) => {
155
+ const outer = collected;
156
+ const mine = [];
157
+ collected = mine;
158
+ try {
159
+ await run();
160
+ }
161
+ finally {
162
+ collected = outer;
163
+ }
164
+ return merged(mine);
165
+ };
89
166
  const logOnce = (what) => {
90
167
  let hasLogged = false;
91
168
  return (failure) => {
92
169
  if (!hasLogged)
93
- report({ id: `render:${what}`, message: `The ${what} render failed`, description: failure });
170
+ report({ id: `draw:${what}`, message: `The ${what} failed`, description: failure });
94
171
  hasLogged = true;
95
172
  };
96
173
  };
@@ -162,32 +239,41 @@ export function createUi({ name, claude, router, toaster, report, announce, mod
162
239
  claude.ui.log(`${name}: the ${pane.title} pane kept its size: ${messageOf(error)}`, { to: 'debug' });
163
240
  }
164
241
  };
165
- const route = () => {
242
+ const handle = async (pane, handler, run) => {
243
+ try {
244
+ await run();
245
+ }
246
+ catch (error) {
247
+ report({ id: `pane:${pane.id}:${handler}`, message: `The ${pane.title} pane's ${handler} threw`, error });
248
+ }
249
+ };
250
+ const routePanes = () => {
166
251
  if (isRouted)
167
252
  return;
168
253
  isRouted = true;
169
254
  router.add('ui.render', (e, next) => {
170
- const pane = panes.get(e.requestId);
171
- if (pane === undefined || e.component !== 'Pane')
255
+ const added = panes.get(e.requestId);
256
+ if (added === undefined || e.component !== 'Pane')
172
257
  return next(e);
173
- openPanes.add(pane.id);
174
- paneStates.set(pane.id, { isShown: true, isFocused: e.props.isFocused });
258
+ const { init } = added;
259
+ openPanes.add(init.id);
260
+ paneStates.set(init.id, { isShown: true, isFocused: e.props.isFocused });
175
261
  readPanes();
176
262
  const table = claude.ui.resolve(e);
177
263
  try {
178
- return drawWith(table, () => pane.render(mod(), e.props), markdownIn(table));
264
+ return drawWith(table, () => init.render(mod(), { ...e.props, params: added.params }), markdownIn(table));
179
265
  }
180
266
  catch (error) {
181
- paneLogs.get(pane.id)?.(`threw: ${messageOf(error)}`);
182
- return drawWith(table, () => Text({ color: 'error', children: `The ${pane.title} pane could not draw: ${messageOf(error)}` }));
267
+ paneLogs.get(init.id)?.(`threw: ${messageOf(error)}`);
268
+ return drawWith(table, () => Text({ color: 'error', children: `The ${init.title} pane could not draw: ${messageOf(error)}` }));
183
269
  }
184
270
  });
185
271
  router.add('ui.scroll', async (e, next) => {
186
- const pane = panes.get(e.requestId);
272
+ const init = panes.get(e.requestId)?.init;
187
273
  const moved = await next(e);
188
- if (pane?.onScroll === undefined || e.component !== 'Pane' || moved.deny !== undefined)
274
+ if (init?.onScroll === undefined || e.component !== 'Pane' || moved.deny !== undefined)
189
275
  return moved;
190
- await handle(pane, 'onScroll', () => pane.onScroll?.(mod(), e));
276
+ await handle(init, 'onScroll', () => init.onScroll?.(mod(), e));
191
277
  return moved;
192
278
  });
193
279
  router.add('ui.close', async (e, next) => {
@@ -195,107 +281,284 @@ export function createUi({ name, claude, router, toaster, report, announce, mod
195
281
  paneStates.delete(e.id);
196
282
  readPanes();
197
283
  const closed = await next(e);
198
- const pane = panes.get(e.id);
199
- if (pane?.onClose !== undefined)
200
- await handle(pane, 'onClose', () => pane.onClose?.(mod(), e));
284
+ const init = panes.get(e.id)?.init;
285
+ if (init?.onClose !== undefined)
286
+ await handle(init, 'onClose', () => init.onClose?.(mod(), e));
201
287
  return closed;
202
288
  });
203
289
  };
204
- const handle = async (pane, handler, run) => {
290
+ const scrollIn = (id, target, options) => claude.ui.scroll({ to: target, in: id, ...(options?.block === undefined ? {} : { block: options.block }) }).then(({ deny }) => {
291
+ if (deny !== undefined)
292
+ throw new Error(`${name}: Claude Code did not scroll ${id}: ${deny}`);
293
+ });
294
+ const addPane = (init) => {
295
+ checkPane(name, init);
296
+ if (panes.has(init.id))
297
+ throw new Error(`${name}: the pane "${init.id}" is already added. Give each pane its own id.`);
298
+ const added = { init, params: undefined };
299
+ panes.set(init.id, added);
300
+ paneLogs.set(init.id, logOnce(`${init.title} pane`));
301
+ announce(`the ${init.title} pane`);
302
+ routePanes();
303
+ const sized = init;
304
+ const pane = {
305
+ id: init.id,
306
+ get isOpen() {
307
+ return openPanes.has(init.id);
308
+ },
309
+ get isShown() {
310
+ return paneStates.get(init.id)?.isShown ?? false;
311
+ },
312
+ get isFocused() {
313
+ return paneStates.get(init.id)?.isFocused ?? false;
314
+ },
315
+ get params() {
316
+ return added.params;
317
+ },
318
+ async open(...[options]) {
319
+ if (options !== undefined && 'params' in options)
320
+ added.params = options.params;
321
+ const { isPlaced } = await openAt(sized, paneSize(sized, mod().state), options?.focus);
322
+ if (isPlaced)
323
+ openPanes.add(init.id);
324
+ await syncPanes();
325
+ claude.ui.invalidate('ui.render');
326
+ },
327
+ async close() {
328
+ await claude.ui.close({ id: init.id });
329
+ openPanes.delete(init.id);
330
+ paneStates.delete(init.id);
331
+ await syncPanes();
332
+ },
333
+ toggle: (options) => (openPanes.has(init.id) ? pane.close() : pane.open(options)),
334
+ scrollTo: (target, options) => scrollIn(init.id, target, options),
335
+ remove() {
336
+ if (panes.get(init.id) !== added)
337
+ return;
338
+ panes.delete(init.id);
339
+ paneLogs.delete(init.id);
340
+ if (openPanes.has(init.id))
341
+ void claude.ui.close({ id: init.id }).catch((error) => claude.ui.log(`${name}: the ${init.title} pane stayed open: ${messageOf(error)}`, { to: 'debug' }));
342
+ openPanes.delete(init.id);
343
+ paneStates.delete(init.id);
344
+ },
345
+ };
346
+ return pane;
347
+ };
348
+ const pageEntry = (id) => shownPages.open.find((each) => each.id === id);
349
+ const addPage = (place, init) => {
350
+ checkPart(name, 'page', init.id, init.title);
351
+ if (pages.has(init.id))
352
+ throw new Error(`${name}: the page "${init.id}" is already added. Give each page its own id.`);
353
+ const added = { init, place, params: undefined };
354
+ pages.set(init.id, added);
355
+ announce(place === 'settings' ? `the ${init.title} page in Settings` : `the ${init.title} page in the Sidebar`);
356
+ void send({ parts: partList() });
357
+ const page = {
358
+ id: init.id,
359
+ get isOpen() {
360
+ return place === 'settings' ? shownPages.shown === init.id : pageEntry(init.id) !== undefined;
361
+ },
362
+ get isShown() {
363
+ return shownPages.shown === init.id;
364
+ },
365
+ get isFocused() {
366
+ return shownPages.shown === init.id && shownPages.isFocused;
367
+ },
368
+ get params() {
369
+ return (pageEntry(init.id)?.params ?? added.params);
370
+ },
371
+ async open(...[options]) {
372
+ if (options !== undefined && 'params' in options)
373
+ added.params = options.params;
374
+ await send({ requests: [{ open: init.id, ...(added.params === undefined ? {} : { params: added.params }), ...(options?.focus === true ? { focus: true } : {}) }] });
375
+ },
376
+ toggle: (options) => (page.isOpen ? page.close() : page.open(options)),
377
+ close: async () => {
378
+ await send({ requests: [{ close: init.id }] });
379
+ },
380
+ scrollTo: async (target, options) => {
381
+ await send({ requests: [{ scroll: init.id, to: target, ...(options?.block === undefined ? {} : { block: options.block }) }] });
382
+ },
383
+ remove() {
384
+ if (pages.get(init.id) !== added)
385
+ return;
386
+ pages.delete(init.id);
387
+ forgetDraws(`page:${init.id}`);
388
+ void send({ parts: partList(), requests: [{ close: init.id }] });
389
+ },
390
+ };
391
+ return page;
392
+ };
393
+ const addWidget = (init) => {
394
+ checkPart(name, 'widget', init.id, init.title);
395
+ if (widgets.has(init.id))
396
+ throw new Error(`${name}: the widget "${init.id}" is already added. Give each widget its own id.`);
397
+ widgets.set(init.id, init);
398
+ announce(`the ${init.title} widget`);
399
+ void send({ parts: partList() });
400
+ return {
401
+ id: init.id,
402
+ get area() {
403
+ return widgetAreas.get(init.id);
404
+ },
405
+ remove() {
406
+ if (widgets.get(init.id) !== init)
407
+ return;
408
+ widgets.delete(init.id);
409
+ widgetAreas.delete(init.id);
410
+ forgetDraws(`widget:${init.id}`);
411
+ void send({ parts: partList() });
412
+ },
413
+ };
414
+ };
415
+ const addStyle = ({ kind, render }) => {
416
+ const markdown = typeof kind === 'string' ? undefined : kind;
417
+ const component = componentOf(kind);
418
+ if (markdown === undefined && component === undefined)
419
+ throw new Error(`${name}: ${String(kind)} is not a kind styles.add draws, such as ToolCall or CodeBlock.`);
420
+ const feature = `the style of ${nameOf(kind)}`;
421
+ if (styles.has(feature))
422
+ throw new Error(`${name}: ${feature} is already added. Add one style per kind.`);
423
+ styles.add(feature);
424
+ announce(feature);
425
+ const Component = (props) => render(mod(), props);
426
+ const stop = (subscription) => () => {
427
+ subscription?.remove();
428
+ if (markdown !== undefined)
429
+ markdownRenders.delete(markdown.markdown);
430
+ styles.delete(feature);
431
+ claude.ui.invalidate('ui.render');
432
+ };
433
+ if (markdown !== undefined) {
434
+ markdownRenders.set(markdown.markdown, { Component, log: logOnce(`${nameOf(kind)} style`) });
435
+ routeMarkdown(markdown.createReader);
436
+ return { kind, remove: stop() };
437
+ }
438
+ const log = logOnce(`${nameOf(kind)} style`);
439
+ const drawing = router.add('ui.render', async (e, next) => {
440
+ if (e.component !== component)
441
+ return next(e);
442
+ const table = claude.ui.resolve(e);
443
+ const drawn = await drawComponent(table, (Default) => Component({ ...e.props, Default }), (given, isClaudesRow) => next({ ...e, props: pieceProps(e.props, given, isClaudesRow) }));
444
+ if ('failure' in drawn) {
445
+ log(drawn.failure);
446
+ return next(e);
447
+ }
448
+ if (e.component !== 'AssistantMessage')
449
+ return drawn.drawing;
450
+ return drawWith(table, () => drawPieces([drawn], e.props.isFirstOfReply ? 'opensReply' : 'inReply'));
451
+ });
452
+ return { kind, remove: stop(drawing) };
453
+ };
454
+ const forgetDraws = (key) => {
455
+ for (const kept of latestDraws.get(key)?.values() ?? [])
456
+ for (const draw of kept)
457
+ draws.delete(draw);
458
+ latestDraws.delete(key);
459
+ };
460
+ const drawPart = (key, title, input, render) => {
205
461
  try {
206
- await run();
462
+ const pressed = [];
463
+ const drawn = render();
464
+ const tree = drawn === undefined ? null : elementData(drawn, pressed);
465
+ failedDraws.delete(key);
466
+ drawCount += 1;
467
+ const inputs = latestDraws.get(key) ?? new Map();
468
+ latestDraws.set(key, inputs);
469
+ const drawnFor = JSON.stringify('turn' in input ? { ...input, turn: input.turn.prompt.id } : input);
470
+ const earlier = inputs.get(drawnFor) ?? [];
471
+ const kept = [...earlier, drawCount].slice(-2);
472
+ for (const draw of earlier)
473
+ if (!kept.includes(draw))
474
+ draws.delete(draw);
475
+ inputs.delete(drawnFor);
476
+ inputs.set(drawnFor, kept);
477
+ const [oldest] = inputs;
478
+ if (inputs.size > keptDrawInputs && oldest !== undefined) {
479
+ for (const draw of oldest[1])
480
+ draws.delete(draw);
481
+ inputs.delete(oldest[0]);
482
+ }
483
+ draws.set(drawCount, { id: key, handlers: pressed });
484
+ return { draw: drawCount, tree };
207
485
  }
208
486
  catch (error) {
209
- report({ id: `pane:${pane.id}:${handler}`, message: `The ${pane.title} pane's ${handler} threw`, error });
487
+ if (!failedDraws.has(key))
488
+ report({ id: key, message: `The ${title} could not draw`, error });
489
+ failedDraws.add(key);
490
+ return null;
491
+ }
492
+ };
493
+ const draw = (input) => {
494
+ if ('page' in input) {
495
+ const added = pages.get(input.page);
496
+ if (added === undefined)
497
+ return null;
498
+ return drawPart(`page:${input.page}`, `${added.init.title} page`, input, () => added.init.render(mod(), { params: input.params, isFocused: input.isFocused, columns: input.columns }));
210
499
  }
500
+ const widget = widgets.get(input.widget);
501
+ if (widget === undefined || !widget.areas.includes(input.area))
502
+ return null;
503
+ widgetAreas.set(widget.id, input.area);
504
+ const context = (input.area === TurnEnd && input.turn !== undefined ? { area: input.area, columns: input.columns, turn: input.turn } : { area: input.area, columns: input.columns });
505
+ return drawPart(`widget:${widget.id}`, `${widget.title} widget`, input, () => widget.render(mod(), context));
506
+ };
507
+ const press = async ({ draw: drawn, handler, args }) => {
508
+ const known = draws.get(drawn);
509
+ const run = known?.handlers[handler];
510
+ if (known === undefined || run === undefined)
511
+ throw new Error(`${name}: that button drew before the last redraw. Press the one it shows now.`);
512
+ return collect(async () => {
513
+ try {
514
+ await run(...args);
515
+ }
516
+ catch (error) {
517
+ report({ id: known.id, message: 'Its button failed', error });
518
+ }
519
+ });
211
520
  };
212
521
  return {
213
- ui: {
214
- pane(pane) {
215
- if (panes.has(pane.id))
216
- throw new Error(`${name}: the pane "${pane.id}" is already added. Give each pane its own id.`);
217
- panes.set(pane.id, pane);
218
- paneLogs.set(pane.id, logOnce(`${pane.title} pane`));
219
- announce(`the ${pane.title} pane`);
220
- route();
221
- const handle = {
222
- get isOpen() {
223
- return openPanes.has(pane.id);
224
- },
225
- get isShown() {
226
- return paneStates.get(pane.id)?.isShown ?? false;
227
- },
228
- get isFocused() {
229
- return paneStates.get(pane.id)?.isFocused ?? false;
230
- },
231
- async open(options) {
232
- const { isPlaced } = await openAt(pane, paneSize(pane, mod().state), options?.focus);
233
- if (isPlaced)
234
- openPanes.add(pane.id);
235
- await syncPanes();
236
- },
237
- async close() {
238
- await claude.ui.close({ id: pane.id });
239
- openPanes.delete(pane.id);
240
- paneStates.delete(pane.id);
241
- await syncPanes();
242
- },
243
- toggle: (options) => (openPanes.has(pane.id) ? handle.close() : handle.open(options)),
244
- };
245
- return handle;
522
+ panes: { add: addPane },
523
+ sidebar: { add: (init) => addPage('sidebar', init) },
524
+ settings: {
525
+ add: (init) => addPage('settings', init),
526
+ open: async () => {
527
+ await send({ requests: [{ settings: true }] });
246
528
  },
247
- render(place, Component) {
248
- const target = place;
249
- const markdown = typeof target === 'string' ? undefined : target;
250
- const component = componentOf(place);
251
- if (markdown === undefined && component === undefined)
252
- throw new Error(`${name}: ${String(place)} is not a place render draws, such as ToolCall or CodeBlock.`);
253
- const feature = `a render of ${nameOf(place)}`;
254
- if (renders.has(feature))
255
- throw new Error(`${name}: ${feature} is already added. Render each place once.`);
256
- renders.add(feature);
257
- announce(feature);
258
- if (markdown !== undefined) {
259
- const markdownType = markdown.markdown;
260
- markdownRenders.set(markdownType, { Component: Component, log: logOnce(nameOf(place)) });
261
- routeMarkdown(markdown.createReader);
262
- return {
263
- dispose() {
264
- markdownRenders.delete(markdownType);
265
- renders.delete(feature);
266
- claude.ui.invalidate('ui.render');
267
- },
268
- };
269
- }
270
- const log = logOnce(nameOf(place));
271
- const drawing = router.add('ui.render', async (e, next) => {
272
- if (e.component !== component)
273
- return next(e);
274
- const table = claude.ui.resolve(e);
275
- const drawn = await drawComponent(table, (Default) => Component({ ...e.props, Default }), (given, isClaudesRow) => next({ ...e, props: pieceProps(e.props, given, isClaudesRow) }));
276
- if ('failure' in drawn) {
277
- log(drawn.failure);
278
- return next(e);
279
- }
280
- if (e.component !== 'AssistantMessage')
281
- return drawn.drawing;
282
- return drawWith(table, () => drawPieces([drawn], e.props.isFirstOfReply ? 'opensReply' : 'inReply'));
283
- });
284
- return {
285
- dispose() {
286
- drawing.dispose();
287
- renders.delete(feature);
288
- claude.ui.invalidate('ui.render');
289
- },
290
- };
291
- },
292
- toast: toaster.toast,
293
- copy: toaster.copy,
294
- ask: (question, options) => claude.ui.ask(question, options),
295
- scroll: (args) => claude.ui.scroll(args),
529
+ },
530
+ widgets: { add: addWidget },
531
+ styles: { add: addStyle },
532
+ asked,
533
+ async answerInput(run, passOn) {
534
+ const before = unheldRequests;
535
+ const answer = await asked(run);
536
+ if (unheldRequests > before)
537
+ await passOn();
538
+ return answer;
539
+ },
540
+ answer(method, input) {
541
+ if (method === partMethods.list)
542
+ return Promise.resolve({ value: partList() });
543
+ if (method === partMethods.draw)
544
+ return Promise.resolve({ value: draw(input) });
545
+ if (method === partMethods.press)
546
+ return press(input).then((value) => ({ value }));
547
+ if (method === partMethods.shown) {
548
+ shownPages = input;
549
+ return Promise.resolve({ value: null });
550
+ }
551
+ return undefined;
552
+ },
553
+ started() {
554
+ const held = merged(beforeStart ?? []);
555
+ beforeStart = undefined;
556
+ void send({ ...held, parts: partList() });
296
557
  },
297
558
  changed() {
298
- if (panes.size === 0 && renders.size === 0)
559
+ if (widgets.size > 0 || pages.size > 0)
560
+ void send({ redraw: true });
561
+ if (panes.size === 0 && styles.size === 0)
299
562
  return;
300
563
  claude.ui.invalidate('ui.render');
301
564
  if (isResizeQueued)
@@ -303,9 +566,9 @@ export function createUi({ name, claude, router, toaster, report, announce, mod
303
566
  isResizeQueued = true;
304
567
  void Promise.resolve().then(() => {
305
568
  isResizeQueued = false;
306
- for (const pane of panes.values()) {
307
- if (openPanes.has(pane.id))
308
- void resize(pane);
569
+ for (const { init } of panes.values()) {
570
+ if (openPanes.has(init.id))
571
+ void resize(init);
309
572
  }
310
573
  });
311
574
  },
@@ -0,0 +1,9 @@
1
+ import type { FileChange } from '../mod.js';
2
+ import type { Subscription } from '../utils/subscription.js';
3
+ import type { Claude } from './claude.js';
4
+ import type { Failure } from './toaster.js';
5
+ export type Watches = {
6
+ add(paths: readonly string[], listener: (change: FileChange) => unknown): Subscription;
7
+ };
8
+ export declare function createWatches(claude: Claude, report: (failure: Failure) => void): Watches;
9
+ //# sourceMappingURL=watches.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,73 @@
1
+ import { ifMissing } from '../utils/paths.js';
2
+ const watchMs = 500;
3
+ const watchedFiles = 2000;
4
+ export function createWatches(claude, report) {
5
+ const watches = new Set();
6
+ let polling;
7
+ let isPolling = false;
8
+ const stampsOf = async (paths) => {
9
+ const stamps = new Map();
10
+ const visit = async (path) => {
11
+ const stat = await claude.fs.stat(path).catch(ifMissing(undefined));
12
+ if (stat === undefined)
13
+ return;
14
+ if (stat.kind !== 'dir') {
15
+ if (stamps.size === watchedFiles)
16
+ throw new Error(`fs.onChange watches at most ${watchedFiles} files, and ${paths.join(', ')} holds more, so it stopped. Watch a smaller folder.`);
17
+ return void stamps.set(path, stat.mtimeMs);
18
+ }
19
+ for (const entry of await claude.fs.list(path).catch(ifMissing([])))
20
+ await visit(`${path}/${entry.name}`);
21
+ };
22
+ for (const path of paths)
23
+ await visit(path);
24
+ return stamps;
25
+ };
26
+ const stop = (watch) => {
27
+ watches.delete(watch);
28
+ if (watches.size > 0)
29
+ return;
30
+ polling?.cancel();
31
+ polling = undefined;
32
+ };
33
+ const stampsOrStop = (watch) => stampsOf(watch.paths).catch((error) => {
34
+ stop(watch);
35
+ report({ id: 'fs:onChange', message: 'Could not read the files fs.onChange watches', error });
36
+ return undefined;
37
+ });
38
+ const poll = async () => {
39
+ if (isPolling)
40
+ return;
41
+ isPolling = true;
42
+ try {
43
+ for (const watch of watches) {
44
+ const now = await stampsOrStop(watch);
45
+ if (now === undefined)
46
+ continue;
47
+ const before = watch.seen;
48
+ watch.seen = now;
49
+ if (before === undefined)
50
+ continue;
51
+ const changes = [
52
+ ...[...now].filter(([path, at]) => before.has(path) && before.get(path) !== at).map(([path]) => ({ path, type: 'change' })),
53
+ ...[...now.keys()].filter((path) => !before.has(path)).map((path) => ({ path, type: 'add' })),
54
+ ...[...before.keys()].filter((path) => !now.has(path)).map((path) => ({ path, type: 'unlink' })),
55
+ ];
56
+ for (const change of changes)
57
+ await Promise.resolve().then(() => watch.listener(change)).catch((error) => report({ id: 'fs:onChange', message: 'The fs.onChange listener failed', error }));
58
+ }
59
+ }
60
+ finally {
61
+ isPolling = false;
62
+ }
63
+ };
64
+ return {
65
+ add(paths, listener) {
66
+ const watch = { paths, listener, seen: undefined };
67
+ watches.add(watch);
68
+ void stampsOrStop(watch).then((stamps) => void (watch.seen ??= stamps));
69
+ polling ??= claude.clock.every(watchMs, () => void poll());
70
+ return { remove: () => stop(watch) };
71
+ },
72
+ };
73
+ }