@cmodjs/core 3.1.0 → 4.0.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 (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 +59 -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 +374 -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 +70 -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 +407 -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
@@ -1 +1 @@
1
- {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../src/runtime/ui.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAc,GAAG,EAAc,MAAM,WAAW,CAAA;AAM5D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAgFpD,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,MAAM,IAAI;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;IAC3C,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IAC5C,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,KAAK,CAAC,CAAA;CAC/B,CAAA;AAED,MAAM,MAAM,MAAM,CAAC,KAAK,SAAS,MAAM,IAAI;IACzC,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAA;IAC7C,OAAO,IAAI,IAAI,CAAA;IACf,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAC9B,CAAA;AAED,wBAAgB,QAAQ,CAAC,KAAK,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,SAAS,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CA8OxI"}
1
+ {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../src/runtime/ui.ts"],"names":[],"mappings":"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"}
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,78 @@ 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 collecting = 0;
119
+ let queued;
120
+ const partList = () => ({
121
+ widgets: [...widgets.values()].map(({ id, title, areas: listed }) => ({ id, title, areas: listed })),
122
+ pages: [...pages.values()].map(({ init: { id, title }, place }) => ({ id, title, place })),
123
+ });
124
+ const send = (update) => {
125
+ if (collected !== undefined)
126
+ return void collected.push(update);
127
+ if (beforeStart !== undefined)
128
+ return void beforeStart.push(update);
129
+ const isAsked = askedDepth > 0;
130
+ if (queued !== undefined && !isAsked)
131
+ return void queued.push(update);
132
+ const batch = [update];
133
+ if (!isAsked)
134
+ queued = batch;
135
+ const tell = () => {
136
+ if (queued === batch)
137
+ queued = undefined;
138
+ const input = { ...merged(batch), ...(isAsked ? { isAsked: true } : {}) };
139
+ return claude.cmod.call({ to: cmodPluginName, method: partMethods.changed, input }).then((answer) => {
140
+ if (isAsked && input.requests !== undefined && !(isObject(answer) && answer['isHeld'] === true))
141
+ unheldRequests += 1;
142
+ }, (error) => claude.ui.log(`${name}: the cmod plugin did not hear about its pages and widgets: ${messageOf(error)}`, { to: 'debug' }));
143
+ };
144
+ return isAsked ? tell() : Promise.resolve().then(tell);
145
+ };
146
+ const asked = async (run) => {
147
+ askedDepth += 1;
148
+ try {
149
+ return await run();
150
+ }
151
+ finally {
152
+ askedDepth -= 1;
153
+ }
154
+ };
155
+ const collect = async (run) => {
156
+ collecting += 1;
157
+ collected ??= [];
158
+ try {
159
+ await run();
160
+ }
161
+ finally {
162
+ collecting -= 1;
163
+ }
164
+ const mine = collected;
165
+ collected = collecting === 0 ? undefined : [];
166
+ return merged(mine);
167
+ };
89
168
  const logOnce = (what) => {
90
169
  let hasLogged = false;
91
170
  return (failure) => {
92
171
  if (!hasLogged)
93
- report({ id: `render:${what}`, message: `The ${what} render failed`, description: failure });
172
+ report({ id: `draw:${what}`, message: `The ${what} failed`, description: failure });
94
173
  hasLogged = true;
95
174
  };
96
175
  };
@@ -162,32 +241,41 @@ export function createUi({ name, claude, router, toaster, report, announce, mod
162
241
  claude.ui.log(`${name}: the ${pane.title} pane kept its size: ${messageOf(error)}`, { to: 'debug' });
163
242
  }
164
243
  };
165
- const route = () => {
244
+ const handle = async (pane, handler, run) => {
245
+ try {
246
+ await run();
247
+ }
248
+ catch (error) {
249
+ report({ id: `pane:${pane.id}:${handler}`, message: `The ${pane.title} pane's ${handler} threw`, error });
250
+ }
251
+ };
252
+ const routePanes = () => {
166
253
  if (isRouted)
167
254
  return;
168
255
  isRouted = true;
169
256
  router.add('ui.render', (e, next) => {
170
- const pane = panes.get(e.requestId);
171
- if (pane === undefined || e.component !== 'Pane')
257
+ const added = panes.get(e.requestId);
258
+ if (added === undefined || e.component !== 'Pane')
172
259
  return next(e);
173
- openPanes.add(pane.id);
174
- paneStates.set(pane.id, { isShown: true, isFocused: e.props.isFocused });
260
+ const { init } = added;
261
+ openPanes.add(init.id);
262
+ paneStates.set(init.id, { isShown: true, isFocused: e.props.isFocused });
175
263
  readPanes();
176
264
  const table = claude.ui.resolve(e);
177
265
  try {
178
- return drawWith(table, () => pane.render(mod(), e.props), markdownIn(table));
266
+ return drawWith(table, () => init.render(mod(), { ...e.props, params: added.params }), markdownIn(table));
179
267
  }
180
268
  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)}` }));
269
+ paneLogs.get(init.id)?.(`threw: ${messageOf(error)}`);
270
+ return drawWith(table, () => Text({ color: 'error', children: `The ${init.title} pane could not draw: ${messageOf(error)}` }));
183
271
  }
184
272
  });
185
273
  router.add('ui.scroll', async (e, next) => {
186
- const pane = panes.get(e.requestId);
274
+ const init = panes.get(e.requestId)?.init;
187
275
  const moved = await next(e);
188
- if (pane?.onScroll === undefined || e.component !== 'Pane' || moved.deny !== undefined)
276
+ if (init?.onScroll === undefined || e.component !== 'Pane' || moved.deny !== undefined)
189
277
  return moved;
190
- await handle(pane, 'onScroll', () => pane.onScroll?.(mod(), e));
278
+ await handle(init, 'onScroll', () => init.onScroll?.(mod(), e));
191
279
  return moved;
192
280
  });
193
281
  router.add('ui.close', async (e, next) => {
@@ -195,107 +283,284 @@ export function createUi({ name, claude, router, toaster, report, announce, mod
195
283
  paneStates.delete(e.id);
196
284
  readPanes();
197
285
  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));
286
+ const init = panes.get(e.id)?.init;
287
+ if (init?.onClose !== undefined)
288
+ await handle(init, 'onClose', () => init.onClose?.(mod(), e));
201
289
  return closed;
202
290
  });
203
291
  };
204
- const handle = async (pane, handler, run) => {
292
+ const scrollIn = (id, target, options) => claude.ui.scroll({ to: target, in: id, ...(options?.block === undefined ? {} : { block: options.block }) }).then(({ deny }) => {
293
+ if (deny !== undefined)
294
+ throw new Error(`${name}: Claude Code did not scroll ${id}: ${deny}`);
295
+ });
296
+ const addPane = (init) => {
297
+ checkPane(name, init);
298
+ if (panes.has(init.id))
299
+ throw new Error(`${name}: the pane "${init.id}" is already added. Give each pane its own id.`);
300
+ const added = { init, params: undefined };
301
+ panes.set(init.id, added);
302
+ paneLogs.set(init.id, logOnce(`${init.title} pane`));
303
+ announce(`the ${init.title} pane`);
304
+ routePanes();
305
+ const sized = init;
306
+ const pane = {
307
+ id: init.id,
308
+ get isOpen() {
309
+ return openPanes.has(init.id);
310
+ },
311
+ get isShown() {
312
+ return paneStates.get(init.id)?.isShown ?? false;
313
+ },
314
+ get isFocused() {
315
+ return paneStates.get(init.id)?.isFocused ?? false;
316
+ },
317
+ get params() {
318
+ return added.params;
319
+ },
320
+ async open(...[options]) {
321
+ if (options !== undefined && 'params' in options)
322
+ added.params = options.params;
323
+ const { isPlaced } = await openAt(sized, paneSize(sized, mod().state), options?.focus);
324
+ if (isPlaced)
325
+ openPanes.add(init.id);
326
+ await syncPanes();
327
+ claude.ui.invalidate('ui.render');
328
+ },
329
+ async close() {
330
+ await claude.ui.close({ id: init.id });
331
+ openPanes.delete(init.id);
332
+ paneStates.delete(init.id);
333
+ await syncPanes();
334
+ },
335
+ toggle: (options) => (openPanes.has(init.id) ? pane.close() : pane.open(options)),
336
+ scrollTo: (target, options) => scrollIn(init.id, target, options),
337
+ remove() {
338
+ if (panes.get(init.id) !== added)
339
+ return;
340
+ panes.delete(init.id);
341
+ paneLogs.delete(init.id);
342
+ if (openPanes.has(init.id))
343
+ void claude.ui.close({ id: init.id }).catch((error) => claude.ui.log(`${name}: the ${init.title} pane stayed open: ${messageOf(error)}`, { to: 'debug' }));
344
+ openPanes.delete(init.id);
345
+ paneStates.delete(init.id);
346
+ },
347
+ };
348
+ return pane;
349
+ };
350
+ const pageEntry = (id) => shownPages.open.find((each) => each.id === id);
351
+ const addPage = (place, init) => {
352
+ checkPart(name, 'page', init.id, init.title);
353
+ if (pages.has(init.id))
354
+ throw new Error(`${name}: the page "${init.id}" is already added. Give each page its own id.`);
355
+ const added = { init, place, params: undefined };
356
+ pages.set(init.id, added);
357
+ announce(place === 'settings' ? `the ${init.title} page in Settings` : `the ${init.title} page in the Sidebar`);
358
+ void send({ parts: partList() });
359
+ const page = {
360
+ id: init.id,
361
+ get isOpen() {
362
+ return place === 'settings' ? shownPages.shown === init.id : pageEntry(init.id) !== undefined;
363
+ },
364
+ get isShown() {
365
+ return shownPages.shown === init.id;
366
+ },
367
+ get isFocused() {
368
+ return shownPages.shown === init.id && shownPages.isFocused;
369
+ },
370
+ get params() {
371
+ return (pageEntry(init.id)?.params ?? added.params);
372
+ },
373
+ async open(...[options]) {
374
+ if (options !== undefined && 'params' in options)
375
+ added.params = options.params;
376
+ await send({ requests: [{ open: init.id, ...(added.params === undefined ? {} : { params: added.params }), ...(options?.focus === true ? { focus: true } : {}) }] });
377
+ },
378
+ toggle: (options) => (page.isOpen ? page.close() : page.open(options)),
379
+ close: async () => {
380
+ await send({ requests: [{ close: init.id }] });
381
+ },
382
+ scrollTo: async (target, options) => {
383
+ await send({ requests: [{ scroll: init.id, to: target, ...(options?.block === undefined ? {} : { block: options.block }) }] });
384
+ },
385
+ remove() {
386
+ if (pages.get(init.id) !== added)
387
+ return;
388
+ pages.delete(init.id);
389
+ forgetDraws(`page:${init.id}`);
390
+ void send({ parts: partList(), requests: [{ close: init.id }] });
391
+ },
392
+ };
393
+ return page;
394
+ };
395
+ const addWidget = (init) => {
396
+ checkPart(name, 'widget', init.id, init.title);
397
+ if (widgets.has(init.id))
398
+ throw new Error(`${name}: the widget "${init.id}" is already added. Give each widget its own id.`);
399
+ widgets.set(init.id, init);
400
+ announce(`the ${init.title} widget`);
401
+ void send({ parts: partList() });
402
+ return {
403
+ id: init.id,
404
+ get area() {
405
+ return widgetAreas.get(init.id);
406
+ },
407
+ remove() {
408
+ if (widgets.get(init.id) !== init)
409
+ return;
410
+ widgets.delete(init.id);
411
+ widgetAreas.delete(init.id);
412
+ forgetDraws(`widget:${init.id}`);
413
+ void send({ parts: partList() });
414
+ },
415
+ };
416
+ };
417
+ const addStyle = ({ kind, render }) => {
418
+ const markdown = typeof kind === 'string' ? undefined : kind;
419
+ const component = componentOf(kind);
420
+ if (markdown === undefined && component === undefined)
421
+ throw new Error(`${name}: ${String(kind)} is not a kind styles.add draws, such as ToolCall or CodeBlock.`);
422
+ const feature = `the style of ${nameOf(kind)}`;
423
+ if (styles.has(feature))
424
+ throw new Error(`${name}: ${feature} is already added. Add one style per kind.`);
425
+ styles.add(feature);
426
+ announce(feature);
427
+ const Component = (props) => render(mod(), props);
428
+ const stop = (subscription) => () => {
429
+ subscription?.remove();
430
+ if (markdown !== undefined)
431
+ markdownRenders.delete(markdown.markdown);
432
+ styles.delete(feature);
433
+ claude.ui.invalidate('ui.render');
434
+ };
435
+ if (markdown !== undefined) {
436
+ markdownRenders.set(markdown.markdown, { Component, log: logOnce(`${nameOf(kind)} style`) });
437
+ routeMarkdown(markdown.createReader);
438
+ return { kind, remove: stop() };
439
+ }
440
+ const log = logOnce(`${nameOf(kind)} style`);
441
+ const drawing = router.add('ui.render', async (e, next) => {
442
+ if (e.component !== component)
443
+ return next(e);
444
+ 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) }));
446
+ if ('failure' in drawn) {
447
+ log(drawn.failure);
448
+ return next(e);
449
+ }
450
+ if (e.component !== 'AssistantMessage')
451
+ return drawn.drawing;
452
+ return drawWith(table, () => drawPieces([drawn], e.props.isFirstOfReply ? 'opensReply' : 'inReply'));
453
+ });
454
+ return { kind, remove: stop(drawing) };
455
+ };
456
+ const forgetDraws = (key) => {
457
+ for (const kept of latestDraws.get(key)?.values() ?? [])
458
+ for (const draw of kept)
459
+ draws.delete(draw);
460
+ latestDraws.delete(key);
461
+ };
462
+ const drawPart = (key, title, input, render) => {
205
463
  try {
206
- await run();
464
+ const pressed = [];
465
+ const drawn = render();
466
+ const tree = drawn === undefined ? null : elementData(drawn, pressed);
467
+ failedDraws.delete(key);
468
+ drawCount += 1;
469
+ const inputs = latestDraws.get(key) ?? new Map();
470
+ latestDraws.set(key, inputs);
471
+ 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);
474
+ for (const draw of earlier)
475
+ if (!kept.includes(draw))
476
+ draws.delete(draw);
477
+ inputs.delete(drawnFor);
478
+ inputs.set(drawnFor, kept);
479
+ const [oldest] = inputs;
480
+ if (inputs.size > keptDrawInputs && oldest !== undefined) {
481
+ for (const draw of oldest[1])
482
+ draws.delete(draw);
483
+ inputs.delete(oldest[0]);
484
+ }
485
+ draws.set(drawCount, { id: key, handlers: pressed });
486
+ return { draw: drawCount, tree };
207
487
  }
208
488
  catch (error) {
209
- report({ id: `pane:${pane.id}:${handler}`, message: `The ${pane.title} pane's ${handler} threw`, error });
489
+ if (!failedDraws.has(key))
490
+ report({ id: key, message: `The ${title} could not draw`, error });
491
+ failedDraws.add(key);
492
+ return null;
493
+ }
494
+ };
495
+ const draw = (input) => {
496
+ if ('page' in input) {
497
+ const added = pages.get(input.page);
498
+ if (added === undefined)
499
+ return null;
500
+ return drawPart(`page:${input.page}`, `${added.init.title} page`, input, () => added.init.render(mod(), { params: input.params, isFocused: input.isFocused, columns: input.columns }));
210
501
  }
502
+ const widget = widgets.get(input.widget);
503
+ if (widget === undefined || !widget.areas.includes(input.area))
504
+ return null;
505
+ widgetAreas.set(widget.id, input.area);
506
+ const context = (input.area === TurnEnd && input.turn !== undefined ? { area: input.area, columns: input.columns, turn: input.turn } : { area: input.area, columns: input.columns });
507
+ return drawPart(`widget:${widget.id}`, `${widget.title} widget`, input, () => widget.render(mod(), context));
508
+ };
509
+ const press = async ({ draw: drawn, handler, args }) => {
510
+ const known = draws.get(drawn);
511
+ const run = known?.handlers[handler];
512
+ 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 () => {
515
+ try {
516
+ await run(...args);
517
+ }
518
+ catch (error) {
519
+ report({ id: known.id, message: 'Its button failed', error });
520
+ }
521
+ });
211
522
  };
212
523
  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;
524
+ panes: { add: addPane },
525
+ sidebar: { add: (init) => addPage('sidebar', init) },
526
+ settings: {
527
+ add: (init) => addPage('settings', init),
528
+ open: async () => {
529
+ await send({ requests: [{ settings: true }] });
246
530
  },
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),
531
+ },
532
+ widgets: { add: addWidget },
533
+ styles: { add: addStyle },
534
+ asked,
535
+ async answerInput(run, passOn) {
536
+ const before = unheldRequests;
537
+ const answer = await asked(run);
538
+ if (unheldRequests > before)
539
+ await passOn();
540
+ return answer;
541
+ },
542
+ answer(method, input) {
543
+ if (method === partMethods.list)
544
+ return Promise.resolve({ value: partList() });
545
+ if (method === partMethods.draw)
546
+ return Promise.resolve({ value: draw(input) });
547
+ if (method === partMethods.press)
548
+ return press(input).then((value) => ({ value }));
549
+ if (method === partMethods.shown) {
550
+ shownPages = input;
551
+ return Promise.resolve({ value: null });
552
+ }
553
+ return undefined;
554
+ },
555
+ started() {
556
+ const held = merged(beforeStart ?? []);
557
+ beforeStart = undefined;
558
+ void send({ ...held, parts: partList() });
296
559
  },
297
560
  changed() {
298
- if (panes.size === 0 && renders.size === 0)
561
+ if (widgets.size > 0 || pages.size > 0)
562
+ void send({ redraw: true });
563
+ if (panes.size === 0 && styles.size === 0)
299
564
  return;
300
565
  claude.ui.invalidate('ui.render');
301
566
  if (isResizeQueued)
@@ -303,9 +568,9 @@ export function createUi({ name, claude, router, toaster, report, announce, mod
303
568
  isResizeQueued = true;
304
569
  void Promise.resolve().then(() => {
305
570
  isResizeQueued = false;
306
- for (const pane of panes.values()) {
307
- if (openPanes.has(pane.id))
308
- void resize(pane);
571
+ for (const { init } of panes.values()) {
572
+ if (openPanes.has(init.id))
573
+ void resize(init);
309
574
  }
310
575
  });
311
576
  },
@@ -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
+ }