@cmodjs/core 0.2.3 → 1.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 (273) hide show
  1. package/docs/commands.md +58 -10
  2. package/docs/dependencies.md +12 -12
  3. package/docs/hooks.md +41 -28
  4. package/docs/index.md +29 -25
  5. package/docs/install-steps.md +9 -7
  6. package/docs/jobs.md +59 -111
  7. package/docs/mod.md +200 -174
  8. package/docs/options.md +2 -2
  9. package/docs/permissions.md +27 -16
  10. package/docs/session.md +248 -0
  11. package/docs/state.md +11 -7
  12. package/docs/testing.md +66 -23
  13. package/docs/ui.md +108 -76
  14. package/jobs/check.d.ts.map +1 -1
  15. package/jobs/check.js +5 -2
  16. package/jobs/context.d.ts +7 -8
  17. package/jobs/context.d.ts.map +1 -1
  18. package/jobs/context.js +23 -73
  19. package/jobs/permissions/decide-permission.d.ts +5 -3
  20. package/jobs/permissions/decide-permission.d.ts.map +1 -1
  21. package/jobs/permissions/decide-permission.js +6 -4
  22. package/jobs/permissions/find-project-scope.d.ts +1 -1
  23. package/jobs/permissions/find-project-scope.d.ts.map +1 -1
  24. package/jobs/permissions/find-project-scope.js +8 -11
  25. package/jobs/permissions/match-target.d.ts +2 -2
  26. package/jobs/permissions/match-target.d.ts.map +1 -1
  27. package/jobs/permissions.d.ts +1 -1
  28. package/jobs/permissions.d.ts.map +1 -1
  29. package/jobs/permissions.js +6 -4
  30. package/jobs/program.d.ts.map +1 -1
  31. package/jobs/program.js +8 -3
  32. package/jobs/prompt.d.ts +4 -2
  33. package/jobs/prompt.d.ts.map +1 -1
  34. package/jobs/prompt.js +16 -15
  35. package/jobs/schedule.d.ts +2 -2
  36. package/jobs/schedule.d.ts.map +1 -1
  37. package/jobs/schedule.js +9 -9
  38. package/jobs/slash-command.d.ts +4 -4
  39. package/jobs/slash-command.d.ts.map +1 -1
  40. package/jobs/slash-command.js +14 -9
  41. package/jobs/status-line.d.ts +1 -11
  42. package/jobs/status-line.d.ts.map +1 -1
  43. package/jobs/status-line.js +8 -12
  44. package/jobs/tool.d.ts +2 -2
  45. package/jobs/tool.d.ts.map +1 -1
  46. package/jobs/tool.js +15 -9
  47. package/mod.d.ts +154 -75
  48. package/mod.d.ts.map +1 -1
  49. package/mod.js +94 -3
  50. package/options.d.ts +2 -2
  51. package/options.js +19 -19
  52. package/package.json +3 -3
  53. package/records.d.ts +10 -8
  54. package/records.d.ts.map +1 -1
  55. package/records.js +28 -17
  56. package/register.d.ts.map +1 -1
  57. package/register.js +18 -6
  58. package/runtime/announce.d.ts +18 -0
  59. package/runtime/announce.d.ts.map +1 -0
  60. package/runtime/announce.js +66 -0
  61. package/runtime/bound.d.ts +4 -0
  62. package/runtime/bound.d.ts.map +1 -0
  63. package/runtime/bound.js +9 -0
  64. package/runtime/claude.d.ts +3 -3
  65. package/runtime/claude.d.ts.map +1 -1
  66. package/runtime/conversations.d.ts +16 -0
  67. package/runtime/conversations.d.ts.map +1 -0
  68. package/runtime/conversations.js +291 -0
  69. package/runtime/deadline.d.ts +0 -1
  70. package/runtime/deadline.d.ts.map +1 -1
  71. package/runtime/deadline.js +1 -1
  72. package/runtime/dependencies.d.ts +2 -0
  73. package/runtime/dependencies.d.ts.map +1 -1
  74. package/runtime/dependencies.js +9 -0
  75. package/runtime/engine.d.ts +17 -0
  76. package/runtime/engine.d.ts.map +1 -0
  77. package/runtime/engine.js +10 -0
  78. package/runtime/grants.d.ts +4 -0
  79. package/runtime/grants.d.ts.map +1 -1
  80. package/runtime/grants.js +63 -33
  81. package/runtime/hooks.d.ts +5 -4
  82. package/runtime/hooks.d.ts.map +1 -1
  83. package/runtime/hooks.js +10 -5
  84. package/runtime/installer.d.ts.map +1 -1
  85. package/runtime/installer.js +11 -9
  86. package/runtime/lifecycle.d.ts +3 -1
  87. package/runtime/lifecycle.d.ts.map +1 -1
  88. package/runtime/lifecycle.js +317 -186
  89. package/runtime/metadata.d.ts +3 -1
  90. package/runtime/metadata.d.ts.map +1 -1
  91. package/runtime/metadata.js +21 -11
  92. package/runtime/options.d.ts +3 -1
  93. package/runtime/options.d.ts.map +1 -1
  94. package/runtime/options.js +4 -4
  95. package/runtime/programs.d.ts +8 -0
  96. package/runtime/programs.d.ts.map +1 -1
  97. package/runtime/programs.js +39 -1
  98. package/runtime/router.d.ts +2 -1
  99. package/runtime/router.d.ts.map +1 -1
  100. package/runtime/router.js +11 -3
  101. package/runtime/sessions.d.ts +36 -0
  102. package/runtime/sessions.d.ts.map +1 -0
  103. package/runtime/sessions.js +235 -0
  104. package/runtime/state.d.ts +6 -2
  105. package/runtime/state.d.ts.map +1 -1
  106. package/runtime/state.js +46 -13
  107. package/runtime/toaster.d.ts +31 -0
  108. package/runtime/toaster.d.ts.map +1 -0
  109. package/runtime/toaster.js +176 -0
  110. package/runtime/ui.d.ts +6 -17
  111. package/runtime/ui.d.ts.map +1 -1
  112. package/runtime/ui.js +36 -112
  113. package/session/context/context.d.ts +10 -0
  114. package/session/context/context.d.ts.map +1 -0
  115. package/session/context/context.js +2 -0
  116. package/session/context/turn/message/document.d.ts +7 -0
  117. package/session/context/turn/message/document.d.ts.map +1 -0
  118. package/session/context/turn/message/document.js +1 -0
  119. package/session/context/turn/message/message.d.ts +54 -0
  120. package/session/context/turn/message/message.d.ts.map +1 -0
  121. package/session/context/turn/message/message.js +137 -0
  122. package/session/context/turn/message/note.d.ts +8 -0
  123. package/session/context/turn/message/note.d.ts.map +1 -0
  124. package/session/context/turn/message/note.js +1 -0
  125. package/session/context/turn/message/notice.d.ts +6 -0
  126. package/session/context/turn/message/notice.d.ts.map +1 -0
  127. package/session/context/turn/message/notice.js +1 -0
  128. package/session/context/turn/message/prompt.d.ts +8 -0
  129. package/session/context/turn/message/prompt.d.ts.map +1 -0
  130. package/session/context/turn/message/prompt.js +1 -0
  131. package/session/context/turn/message/reply.d.ts +6 -0
  132. package/session/context/turn/message/reply.d.ts.map +1 -0
  133. package/session/context/turn/message/reply.js +1 -0
  134. package/session/context/turn/message/sender.d.ts +14 -0
  135. package/session/context/turn/message/sender.d.ts.map +1 -0
  136. package/session/context/turn/message/sender.js +30 -0
  137. package/session/context/turn/message/skill.d.ts +7 -0
  138. package/session/context/turn/message/skill.d.ts.map +1 -0
  139. package/session/context/turn/message/skill.js +1 -0
  140. package/session/context/turn/message/tool-call.d.ts +9 -0
  141. package/session/context/turn/message/tool-call.d.ts.map +1 -0
  142. package/session/context/turn/message/tool-call.js +1 -0
  143. package/session/context/turn/message/tool-result.d.ts +8 -0
  144. package/session/context/turn/message/tool-result.d.ts.map +1 -0
  145. package/session/context/turn/message/tool-result.js +1 -0
  146. package/session/context/turn/turn.d.ts +12 -0
  147. package/session/context/turn/turn.d.ts.map +1 -0
  148. package/session/context/turn/turn.js +2 -0
  149. package/session/conversation.d.ts +64 -0
  150. package/session/conversation.d.ts.map +1 -0
  151. package/session/conversation.js +123 -0
  152. package/session/goal/goal.d.ts +10 -0
  153. package/session/goal/goal.d.ts.map +1 -0
  154. package/session/goal/goal.js +1 -0
  155. package/session/goal/task.d.ts +16 -0
  156. package/session/goal/task.d.ts.map +1 -0
  157. package/session/goal/task.js +3 -0
  158. package/session/session.d.ts +99 -0
  159. package/session/session.d.ts.map +1 -0
  160. package/session/session.js +1 -0
  161. package/src/jobs/check.ts +7 -4
  162. package/src/jobs/context.ts +24 -82
  163. package/src/jobs/permissions/decide-permission.ts +10 -8
  164. package/src/jobs/permissions/find-project-scope.ts +8 -14
  165. package/src/jobs/permissions/match-target.ts +3 -3
  166. package/src/jobs/permissions.ts +8 -6
  167. package/src/jobs/program.ts +7 -3
  168. package/src/jobs/prompt.ts +21 -24
  169. package/src/jobs/schedule.ts +14 -14
  170. package/src/jobs/slash-command.ts +20 -14
  171. package/src/jobs/status-line.ts +9 -21
  172. package/src/jobs/tool.ts +17 -11
  173. package/src/mod.ts +209 -97
  174. package/src/options.ts +22 -22
  175. package/src/records.ts +35 -23
  176. package/src/register.ts +18 -6
  177. package/src/runtime/announce.ts +74 -0
  178. package/src/runtime/bound.ts +12 -0
  179. package/src/runtime/claude.ts +5 -3
  180. package/src/runtime/conversations.ts +302 -0
  181. package/src/runtime/deadline.ts +2 -2
  182. package/src/runtime/dependencies.ts +12 -0
  183. package/src/runtime/engine.ts +22 -0
  184. package/src/runtime/grants.ts +66 -31
  185. package/src/runtime/hooks.ts +19 -10
  186. package/src/runtime/installer.ts +10 -9
  187. package/src/runtime/lifecycle.ts +313 -181
  188. package/src/runtime/metadata.ts +33 -11
  189. package/src/runtime/options.ts +19 -4
  190. package/src/runtime/programs.ts +40 -1
  191. package/src/runtime/router.ts +11 -4
  192. package/src/runtime/sessions.ts +242 -0
  193. package/src/runtime/state.ts +50 -14
  194. package/src/runtime/toaster.ts +218 -0
  195. package/src/runtime/ui.ts +42 -139
  196. package/src/session/context/context.ts +12 -0
  197. package/src/session/context/turn/message/document.ts +8 -0
  198. package/src/session/context/turn/message/message.ts +165 -0
  199. package/src/session/context/turn/message/note.ts +9 -0
  200. package/src/session/context/turn/message/notice.ts +7 -0
  201. package/src/session/context/turn/message/prompt.ts +9 -0
  202. package/src/session/context/turn/message/reply.ts +7 -0
  203. package/src/session/context/turn/message/sender.ts +38 -0
  204. package/src/session/context/turn/message/skill.ts +8 -0
  205. package/src/session/context/turn/message/tool-call.ts +10 -0
  206. package/src/session/context/turn/message/tool-result.ts +9 -0
  207. package/src/session/context/turn/turn.ts +14 -0
  208. package/src/session/conversation.ts +169 -0
  209. package/src/session/goal/goal.ts +9 -0
  210. package/src/session/goal/task.ts +19 -0
  211. package/src/session/session.ts +94 -0
  212. package/src/testing/fake-claude.ts +23 -18
  213. package/src/testing/fake-elements.ts +3 -2
  214. package/src/testing/fake-files.ts +1 -1
  215. package/src/testing/fake-sessions.ts +203 -0
  216. package/src/testing.ts +112 -33
  217. package/src/ui/components.ts +3 -3
  218. package/src/ui/elements.ts +22 -8
  219. package/src/ui/markdown.ts +22 -26
  220. package/src/ui/places.ts +49 -0
  221. package/src/utils/call-effects.ts +3 -3
  222. package/src/utils/changelog.ts +32 -0
  223. package/src/utils/matcher.ts +37 -0
  224. package/src/utils/metadata.ts +17 -15
  225. package/src/utils/parse-shell.ts +24 -16
  226. package/src/utils/paths.ts +15 -1
  227. package/src/utils/text.ts +4 -0
  228. package/testing/fake-claude.d.ts +4 -2
  229. package/testing/fake-claude.d.ts.map +1 -1
  230. package/testing/fake-claude.js +18 -12
  231. package/testing/fake-elements.js +4 -2
  232. package/testing/fake-files.js +1 -1
  233. package/testing/fake-sessions.d.ts +74 -0
  234. package/testing/fake-sessions.d.ts.map +1 -0
  235. package/testing/fake-sessions.js +170 -0
  236. package/testing.d.ts +33 -13
  237. package/testing.d.ts.map +1 -1
  238. package/testing.js +92 -21
  239. package/ui/components.d.ts +2 -3
  240. package/ui/components.d.ts.map +1 -1
  241. package/ui/components.js +3 -3
  242. package/ui/elements.d.ts.map +1 -1
  243. package/ui/elements.js +19 -9
  244. package/ui/markdown.d.ts +35 -33
  245. package/ui/markdown.d.ts.map +1 -1
  246. package/ui/markdown.js +14 -16
  247. package/ui/places.d.ts +43 -0
  248. package/ui/places.d.ts.map +1 -0
  249. package/ui/places.js +31 -0
  250. package/utils/call-effects.d.ts +1 -1
  251. package/utils/call-effects.d.ts.map +1 -1
  252. package/utils/call-effects.js +2 -2
  253. package/utils/changelog.d.ts +9 -0
  254. package/utils/changelog.d.ts.map +1 -0
  255. package/utils/changelog.js +29 -0
  256. package/utils/matcher.d.ts +13 -0
  257. package/utils/matcher.d.ts.map +1 -0
  258. package/utils/matcher.js +32 -0
  259. package/utils/metadata.d.ts +3 -3
  260. package/utils/metadata.js +18 -14
  261. package/utils/parse-shell.d.ts +1 -0
  262. package/utils/parse-shell.d.ts.map +1 -1
  263. package/utils/parse-shell.js +22 -13
  264. package/utils/paths.d.ts +2 -0
  265. package/utils/paths.d.ts.map +1 -1
  266. package/utils/paths.js +13 -1
  267. package/utils/text.d.ts +1 -0
  268. package/utils/text.d.ts.map +1 -1
  269. package/utils/text.js +3 -0
  270. package/src/ui/slots.ts +0 -36
  271. package/ui/slots.d.ts +0 -208
  272. package/ui/slots.d.ts.map +0 -1
  273. package/ui/slots.js +0 -19
package/runtime/state.js CHANGED
@@ -1,13 +1,21 @@
1
- import { messageOf } from '../utils/text.js';
2
1
  const lifetimes = ['memory', 'session', 'project', 'global'];
3
- const keptPerValue = 20;
2
+ const keptProjects = 20;
4
3
  const listeners = new WeakMap();
5
4
  export function onStateChange(state, listener) {
6
5
  const own = listeners.get(state) ?? new Set();
7
6
  own.add(listener);
8
7
  listeners.set(state, own);
9
8
  }
10
- export function createState({ name, initial, session, root, claude, changed: redraw }) {
9
+ export async function sessionsWith(claude, name, key, value) {
10
+ const prefix = `${name}.${key}.sessions.`;
11
+ const found = [];
12
+ for (const stored of (await claude.store.keys()).filter((each) => each.startsWith(prefix))) {
13
+ if ((await claude.store.get(stored)) === value)
14
+ found.push(stored.slice(prefix.length));
15
+ }
16
+ return found;
17
+ }
18
+ export function createState({ name, initial, session, root, claude, report, changed: redraw, forkParent }) {
11
19
  const changed = () => {
12
20
  redraw();
13
21
  for (const listener of listeners.get(state) ?? [])
@@ -20,21 +28,36 @@ export function createState({ name, initial, session, root, claude, changed: red
20
28
  let turn = Promise.resolve();
21
29
  const inTurn = (task) => {
22
30
  const run = turn.then(task);
23
- turn = run.catch(() => undefined);
31
+ turn = Promise.allSettled([run]);
24
32
  return run;
25
33
  };
26
34
  const ownersKey = (lifetime, key) => `${name}.${key}.${lifetime === 'project' ? 'projects' : 'sessions'}.`;
27
35
  const storeKey = (lifetime, key, owner) => (lifetime === 'global' ? `${name}.${key}` : `${ownersKey(lifetime, key)}${owner}`);
28
36
  const read = (lifetime, key, owner) => claude.store.get(storeKey(lifetime, key, owner));
29
37
  const write = async (lifetime, key, value, owner) => {
30
- if (lifetime === 'global')
38
+ if (lifetime !== 'project')
31
39
  return claude.store.set(storeKey(lifetime, key, owner), value);
32
40
  await claude.store.delete(storeKey(lifetime, key, owner));
33
41
  await claude.store.set(storeKey(lifetime, key, owner), value);
34
42
  const owners = (await claude.store.keys()).filter((stored) => stored.startsWith(ownersKey(lifetime, key)));
35
- for (const oldest of owners.slice(0, -keptPerValue))
43
+ for (const oldest of owners.slice(0, -keptProjects))
36
44
  await claude.store.delete(oldest);
37
45
  };
46
+ const transcriptsKey = `${name}.$transcripts.`;
47
+ const forgetEndedSessions = async (current) => {
48
+ const stored = await claude.store.keys();
49
+ for (const recorded of stored.filter((key) => key.startsWith(transcriptsKey))) {
50
+ const owner = recorded.slice(transcriptsKey.length);
51
+ if (owner === current)
52
+ continue;
53
+ const transcript = await claude.store.get(recorded);
54
+ if (typeof transcript === 'string' && (await claude.fs.exists(transcript)))
55
+ continue;
56
+ for (const value of stored.filter((key) => key.startsWith(`${name}.`) && key.endsWith(`.sessions.${owner}`)))
57
+ await claude.store.delete(value);
58
+ await claude.store.delete(recorded);
59
+ }
60
+ };
38
61
  const loadGroups = async (only, next) => {
39
62
  const loaded = [];
40
63
  for (const lifetime of only) {
@@ -47,13 +70,14 @@ export function createState({ name, initial, session, root, claude, changed: red
47
70
  current[lifetime][key] = saved === undefined ? declared[lifetime][key] : frozen(name, `${lifetime}.${key}`, saved);
48
71
  changed();
49
72
  };
50
- const copySession = async (nextSession) => {
73
+ const copySession = async (from, to) => {
51
74
  for (const key of Object.keys(declared.session)) {
52
- const saved = await read('session', key, sessionId);
75
+ const saved = await read('session', key, from);
53
76
  if (saved !== undefined)
54
- await write('session', key, saved, nextSession);
77
+ await write('session', key, saved, to);
55
78
  }
56
79
  };
80
+ const hasSaved = async (owner) => (await claude.store.keys()).some((key) => key.startsWith(`${name}.`) && key.endsWith(`.sessions.${owner}`));
57
81
  const group = (lifetime) => new Proxy(current[lifetime], {
58
82
  set(target, key, value) {
59
83
  if (typeof key !== 'string')
@@ -66,7 +90,7 @@ export function createState({ name, initial, session, root, claude, changed: red
66
90
  if (lifetime !== 'memory') {
67
91
  const owner = lifetime === 'project' ? projectRoot : sessionId;
68
92
  inTurn(() => write(lifetime, key, value, owner)).catch((error) => {
69
- claude.ui.log(`${name} could not keep state.${lifetime}.${key}: ${messageOf(error)}. Keep only JSON data in mod.state.`);
93
+ report({ id: `state:${lifetime}.${key}`, message: `Could not keep state.${lifetime}.${key}. Keep only JSON data in mod.state`, error });
70
94
  });
71
95
  }
72
96
  changed();
@@ -80,13 +104,22 @@ export function createState({ name, initial, session, root, claude, changed: red
80
104
  return projectRoot;
81
105
  },
82
106
  changed,
83
- load: () => inTurn(() => loadGroups(lifetimes, { root: projectRoot, session: sessionId })),
107
+ load: () => inTurn(async () => {
108
+ if (Object.keys(declared.session).length > 0 && !(await hasSaved(sessionId))) {
109
+ const parent = await forkParent();
110
+ if (parent !== undefined)
111
+ await copySession(parent, sessionId);
112
+ }
113
+ return loadGroups(lifetimes, { root: projectRoot, session: sessionId });
114
+ }),
84
115
  moveTo: (nextRoot) => inTurn(async () => (nextRoot === projectRoot ? undefined : loadGroups(['project'], { root: nextRoot, session: sessionId }))),
85
- switchSession: (nextSession, source) => inTurn(async () => {
116
+ switchSession: (nextSession, source, transcript) => inTurn(async () => {
117
+ await claude.store.set(`${transcriptsKey}${nextSession}`, transcript);
118
+ await forgetEndedSessions(nextSession);
86
119
  if (nextSession === sessionId)
87
120
  return;
88
121
  if (source === 'fork')
89
- await copySession(nextSession);
122
+ await copySession(sessionId, nextSession);
90
123
  return loadGroups(['memory', 'session'], { root: projectRoot, session: nextSession });
91
124
  }),
92
125
  };
@@ -0,0 +1,31 @@
1
+ import type { ElementTable, RenderElement, RenderPropsOf } from 'claude-code';
2
+ import type { Mod, Toast, ToastId, ToastType } from '../mod.js';
3
+ import type { Claude } from './claude.js';
4
+ export type ShownToast = {
5
+ readonly type: ToastType;
6
+ readonly message: string;
7
+ readonly description?: string;
8
+ };
9
+ export type Failure = {
10
+ readonly id: string;
11
+ readonly message: string;
12
+ readonly error?: unknown;
13
+ readonly description?: string;
14
+ };
15
+ export type Toaster = {
16
+ readonly toast: Toast;
17
+ copy(text: string): Promise<void>;
18
+ readonly shown: readonly ShownToast[];
19
+ draw(drawn: RenderElement, table: ElementTable, band: RenderPropsOf['AbovePrompt']): RenderElement;
20
+ };
21
+ export declare function createToaster(claude: Claude, name: string): Toaster;
22
+ export type FailureTarget = {
23
+ readonly name: string;
24
+ readonly ui: Pick<Mod['ui'], 'toast' | 'copy'>;
25
+ readonly claude: {
26
+ readonly plugin: Claude['plugin'];
27
+ readonly fs: Pick<Claude['fs'], 'read'>;
28
+ };
29
+ };
30
+ export declare function showFailure({ name, ui, claude }: FailureTarget, { id, message, error, description }: Failure): ToastId;
31
+ //# sourceMappingURL=toaster.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toaster.d.ts","sourceRoot":"","sources":["../src/runtime/toaster.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAS,MAAM,aAAa,CAAA;AACpF,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAgB,SAAS,EAAE,MAAM,WAAW,CAAA;AAI7E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,MAAM,MAAM,UAAU,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9G,MAAM,MAAM,OAAO,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEhI,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAA;IACrC,IAAI,CAAC,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,CAAC,aAAa,CAAC,GAAG,aAAa,CAAA;CACnG,CAAA;AAuBD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAmJnE;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,CAAA;IAC9C,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;QAAC,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAA;KAAE,CAAA;CAChG,CAAA;AAED,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,WAA6B,EAAE,EAAE,OAAO,GAAG,OAAO,CAcxI"}
@@ -0,0 +1,176 @@
1
+ import { Box, Button, drawWith, Text } from '../ui/elements.js';
2
+ import { coreVersion } from '../records.js';
3
+ import { firstLine, messageOf } from '../utils/text.js';
4
+ const defaultDuration = 4000;
5
+ const visibleToasts = 3;
6
+ const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
7
+ const spinnerMs = 100;
8
+ const icons = {
9
+ default: { glyph: ' ' },
10
+ success: { glyph: '✓', color: 'success' },
11
+ info: { glyph: 'ℹ', color: 'suggestion' },
12
+ warning: { glyph: '⚠', color: 'warning' },
13
+ error: { glyph: '✗', color: 'error' },
14
+ };
15
+ export function createToaster(claude, name) {
16
+ const entries = new Map();
17
+ let lastId = 0;
18
+ let frame = 0;
19
+ let spinning;
20
+ let isHeld = false;
21
+ let showsAll = false;
22
+ const changed = () => {
23
+ const isLoading = [...entries.values()].some((entry) => entry.type === 'loading');
24
+ if (isLoading && spinning === undefined) {
25
+ spinning = claude.clock.every(spinnerMs, () => {
26
+ frame += 1;
27
+ claude.ui.invalidate('ui.render');
28
+ });
29
+ }
30
+ if (!isLoading && spinning !== undefined) {
31
+ spinning.cancel();
32
+ spinning = undefined;
33
+ }
34
+ if (entries.size <= visibleToasts)
35
+ showsAll = false;
36
+ claude.ui.invalidate('ui.render');
37
+ };
38
+ const disarm = (entry) => {
39
+ entry.timer?.cancel();
40
+ entry.timer = undefined;
41
+ };
42
+ const arm = (entry) => {
43
+ const duration = entry.options.duration ?? defaultDuration;
44
+ if (entry.timer !== undefined || entry.type === 'loading' || entry.isOpen || !Number.isFinite(duration))
45
+ return;
46
+ entry.timer = claude.clock.after(duration, () => {
47
+ entry.timer = undefined;
48
+ if (isHeld)
49
+ arm(entry);
50
+ else
51
+ dismiss(entry.id);
52
+ });
53
+ };
54
+ const dismiss = (id) => {
55
+ for (const entry of entries.values()) {
56
+ if (id !== undefined && entry.id !== id)
57
+ continue;
58
+ disarm(entry);
59
+ entries.delete(entry.id);
60
+ }
61
+ changed();
62
+ };
63
+ const show = (type) => (message, options = {}) => {
64
+ const id = options.id ?? (lastId += 1);
65
+ const previous = entries.get(id);
66
+ if (previous !== undefined)
67
+ disarm(previous);
68
+ const entry = { id, type, message, options, isOpen: previous?.isOpen ?? false, timer: undefined };
69
+ if (type === 'error' && previous?.message !== message)
70
+ claude.ui.log(`${name}: ${message}${typeof options.description === 'string' ? `: ${options.description}` : ''}`, { to: 'debug' });
71
+ entries.set(id, entry);
72
+ changed();
73
+ return id;
74
+ };
75
+ const toast = Object.assign(show('default'), {
76
+ success: show('success'),
77
+ info: show('info'),
78
+ warning: show('warning'),
79
+ error: show('error'),
80
+ loading: show('loading'),
81
+ dismiss,
82
+ promise(task, messages) {
83
+ const id = show('loading')(messages.loading);
84
+ const settled = Promise.resolve()
85
+ .then(() => (typeof task === 'function' ? task() : task))
86
+ .then((value) => {
87
+ show('success')(typeof messages.success === 'function' ? messages.success(value) : messages.success, { id });
88
+ return value;
89
+ }, (error) => {
90
+ show('error')(typeof messages.error === 'function' ? messages.error(error) : messages.error, { id });
91
+ throw error;
92
+ });
93
+ settled.catch(() => undefined);
94
+ return { unwrap: () => settled };
95
+ },
96
+ });
97
+ const copy = async (text) => {
98
+ const copied = await claude.ui.copy({ text });
99
+ if (!copied.isCopied)
100
+ throw new Error(copied.reason);
101
+ };
102
+ const press = (entry, run) => () => void Promise.resolve()
103
+ .then(run)
104
+ .catch((error) => showFailure({ name, claude, ui: { toast, copy } }, { id: `press:${entry.id}`, message: `${entry.message}: the press failed`, error }));
105
+ const toggle = (entry) => () => {
106
+ entry.isOpen = !entry.isOpen;
107
+ disarm(entry);
108
+ changed();
109
+ };
110
+ const row = (entry) => {
111
+ const { description, action } = entry.options;
112
+ const isText = typeof description === 'string';
113
+ const icon = entry.type === 'loading' ? { glyph: spinner[frame % spinner.length], color: 'claude' } : icons[entry.type];
114
+ const message = isText ? Button({ plain: true, key: `toast:${entry.id}:open`, label: entry.message, onPress: toggle(entry) }) : Text({ children: entry.message });
115
+ const details = !isText ? description : Text({ dimColor: true, wrap: entry.isOpen ? 'wrap' : 'truncate-end', children: entry.isOpen ? description : firstLine(description) });
116
+ const fixed = (child) => Box({ flexShrink: 0, children: child });
117
+ return Box({
118
+ gap: 2,
119
+ children: [
120
+ fixed(Text({ ...(icon.color === undefined ? {} : { color: icon.color }), children: icon.glyph })),
121
+ fixed(Text({ dimColor: true, children: name })),
122
+ Box({ flexDirection: 'column', flexGrow: 1, flexShrink: 1, children: details === undefined ? [message] : [message, details] }),
123
+ ...(action === undefined ? [] : [fixed(Button({ plain: true, key: `toast:${entry.id}:action`, label: action.label, onPress: press(entry, action.onClick) }))]),
124
+ fixed(Button({ plain: true, dimColor: true, key: `toast:${entry.id}:close`, label: '✕', onPress: () => dismiss(entry.id) })),
125
+ ],
126
+ });
127
+ };
128
+ return {
129
+ toast,
130
+ copy,
131
+ get shown() {
132
+ return [...entries.values()].map(({ type, message, options }) => ({ type, message, ...(typeof options.description === 'string' ? { description: options.description } : {}) }));
133
+ },
134
+ draw(drawn, table, band) {
135
+ isHeld = band.hasSurvey === true;
136
+ if (isHeld || entries.size === 0)
137
+ return drawn;
138
+ const all = [...entries.values()];
139
+ const hidden = showsAll ? 0 : Math.max(all.length - visibleToasts, 0);
140
+ for (const entry of all.slice(hidden))
141
+ arm(entry);
142
+ const more = Button({
143
+ plain: true,
144
+ dimColor: true,
145
+ key: 'toasts:more',
146
+ label: `+${hidden} more`,
147
+ onPress: () => {
148
+ showsAll = true;
149
+ changed();
150
+ },
151
+ });
152
+ return drawWith(table, () => Box({ flexDirection: 'column', children: [drawn, ...(hidden > 0 ? [more] : []), ...all.slice(hidden).map(row)] }));
153
+ },
154
+ };
155
+ }
156
+ export function showFailure({ name, ui, claude }, { id, message, error, description = describe(error) }) {
157
+ const stack = error instanceof Error && error.stack !== undefined ? `\n\n${error.stack}` : '';
158
+ const show = (label) => ui.toast.error(message, { id, description, duration: Infinity, action: { label, onClick: copy } });
159
+ const copy = async () => {
160
+ const modVersion = await claude.fs
161
+ .read(`${claude.plugin.root}/.claude-plugin/plugin.json`)
162
+ .then((text) => String(JSON.parse(text).version))
163
+ .catch((failure) => `unknown (${messageOf(failure)})`);
164
+ return ui.copy(`${name} ${modVersion}, @cmodjs/core ${coreVersion}\n${message}\n${description}${stack}`).then(() => show('Copied'), (failure) => show(`Copy failed: ${messageOf(failure)}`));
165
+ };
166
+ return show('Copy');
167
+ }
168
+ function describe(error) {
169
+ const messages = [];
170
+ for (let cause = error; cause !== undefined; cause = cause instanceof Error ? cause.cause : undefined) {
171
+ const text = messageOf(cause);
172
+ if (!messages.some((earlier) => earlier.includes(text) || text.includes(earlier)))
173
+ messages.push(text);
174
+ }
175
+ return messages.join('\n');
176
+ }
package/runtime/ui.d.ts CHANGED
@@ -1,31 +1,20 @@
1
- import type { ElementTable, RenderElement } from 'claude-code';
2
- import type { Mod, ProgressStep } from '../mod.js';
1
+ import type { Mod } from '../mod.js';
3
2
  import type { Claude } from './claude.js';
4
3
  import type { Router } from './router.js';
5
- export type ProgressLine = {
6
- report(step: ProgressStep): void;
7
- wait(text: string): void;
8
- fail(reason: string, fix: string): void;
9
- end(): void;
10
- };
11
- export type Progress = {
12
- readonly isShown: boolean;
13
- start(title: string): ProgressLine;
14
- draw(drawn: RenderElement, table: ElementTable, columns: number | undefined): RenderElement;
15
- };
16
- export declare function createProgress(claude: Claude): Progress;
4
+ import type { Failure, Toaster } from './toaster.js';
17
5
  export type UiOptions<State extends object> = {
18
6
  readonly name: string;
19
7
  readonly claude: Claude;
20
8
  readonly router: Router;
21
- readonly progress: Progress;
9
+ readonly toaster: Toaster;
10
+ readonly report: (failure: Failure) => void;
22
11
  readonly announce: (feature: string) => void;
23
12
  readonly mod: () => Mod<State>;
24
13
  };
25
14
  export type UiArea<State extends object> = {
26
- readonly ui: Mod<State>['ui'];
15
+ readonly ui: Omit<Mod<State>['ui'], 'notice'>;
27
16
  changed(): void;
28
17
  restorePanes(): Promise<void>;
29
18
  };
30
- export declare function createUi<State extends object>({ name, claude, router, progress, announce, mod }: UiOptions<State>): UiArea<State>;
19
+ export declare function createUi<State extends object>({ name, claude, router, toaster, report, announce, mod }: UiOptions<State>): UiArea<State>;
31
20
  //# sourceMappingURL=ui.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../src/runtime/ui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAA+B,aAAa,EAAS,MAAM,aAAa,CAAA;AAClG,OAAO,KAAK,EAAE,GAAG,EAAc,YAAY,EAAE,MAAM,WAAW,CAAA;AAO9D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAAA;IAChC,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IACvC,GAAG,IAAI,IAAI,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,CAAA;IAClC,IAAI,CAAC,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,aAAa,CAAA;CAC5F,CAAA;AAkBD,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAqDvD;AAsGD,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,QAAQ,EAAE,QAAQ,CAAA;IAC3B,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,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAA;IAC7B,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,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,SAAS,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAmNjI"}
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,CA2NxI"}
package/runtime/ui.js CHANGED
@@ -1,92 +1,8 @@
1
- import { ProgressBar } from '../ui/components.js';
2
1
  import { Box, drawWith, Text } from '../ui/elements.js';
2
+ import { componentOf, nameOf } from '../ui/places.js';
3
3
  import { messageOf } from '../utils/text.js';
4
- const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
5
- const spinnerMs = 100;
6
4
  const bullet = '⏺';
7
- const widestBar = 30;
8
- const narrowestBar = 10;
9
5
  const sizeKeys = ['columns', 'rows'];
10
- export function createProgress(claude) {
11
- const lines = [];
12
- let frame = 0;
13
- let timer;
14
- const changed = () => {
15
- const isSpinning = lines.some((line) => line.failure === undefined && line.waiting === undefined);
16
- if (isSpinning && timer === undefined) {
17
- timer = claude.clock.every(spinnerMs, () => {
18
- frame += 1;
19
- claude.ui.invalidate('ui.render');
20
- });
21
- }
22
- if (!isSpinning && timer !== undefined) {
23
- timer.cancel();
24
- timer = undefined;
25
- }
26
- claude.ui.invalidate('ui.render');
27
- };
28
- return {
29
- get isShown() {
30
- return lines.length > 0;
31
- },
32
- start(title) {
33
- const line = { title, step: undefined, waiting: undefined, failure: undefined };
34
- lines.push(line);
35
- changed();
36
- return {
37
- report(step) {
38
- line.step = step;
39
- line.waiting = undefined;
40
- changed();
41
- },
42
- wait(text) {
43
- line.waiting = text;
44
- changed();
45
- },
46
- fail(reason, fix) {
47
- line.failure = { reason, fix };
48
- changed();
49
- },
50
- end() {
51
- const index = lines.indexOf(line);
52
- if (index >= 0)
53
- lines.splice(index, 1);
54
- changed();
55
- },
56
- };
57
- },
58
- draw(drawn, table, columns) {
59
- return drawWith(table, () => Box({ flexDirection: 'column', children: [drawn, ...lines.map((line) => drawLine(line, frame, columns))] }));
60
- },
61
- };
62
- }
63
- function drawLine(line, frame, columns) {
64
- const title = Text({ bold: true, children: line.title });
65
- if (line.failure !== undefined) {
66
- return Box({
67
- flexDirection: 'column',
68
- children: [
69
- Box({ children: [Box({ minWidth: 2, children: Text({ color: 'error', children: '✗' }) }), Text({ children: [title, ' ', Text({ color: 'error', children: line.failure.reason })] })] }),
70
- Box({ paddingLeft: 2, children: Text({ dimColor: true, children: line.failure.fix }) }),
71
- ],
72
- });
73
- }
74
- if (line.waiting !== undefined) {
75
- return Text({ wrap: 'truncate-end', children: [Text({ color: 'warning', children: '◌' }), ' ', title, ' ', Text({ dimColor: true, children: line.waiting })] });
76
- }
77
- const glyph = Text({ color: 'claude', children: spinner[frame % spinner.length] });
78
- if (line.step === undefined)
79
- return Text({ wrap: 'truncate-end', children: [glyph, ' ', title, Text({ dimColor: true, children: '…' })] });
80
- const { done, total, label = '' } = line.step;
81
- const width = barWidth(columns, `${line.title}${done}/${total}${label}`.length);
82
- return Text({ wrap: 'truncate-end', children: [glyph, ' ', title, ' ', ProgressBar({ done, total, width }), ...(label === '' ? [] : [' ', label])] });
83
- }
84
- function barWidth(columns, textLength) {
85
- if (columns === undefined)
86
- return widestBar;
87
- const spacing = 2 + 2 + 2 + 2;
88
- return Math.max(narrowestBar, Math.min(widestBar, columns - textLength - spacing));
89
- }
90
6
  function paneSize(pane, state) {
91
7
  const size = {};
92
8
  for (const key of sizeKeys) {
@@ -149,7 +65,7 @@ function drawPieces(pieces, place) {
149
65
  return Box({ children: [Box({ minWidth: 2, children: Text({ color: 'text', children: bullet }) }), column(spaced)] });
150
66
  return column([opening, Box({ paddingLeft: 2, children: column(rest) })]);
151
67
  }
152
- export function createUi({ name, claude, router, progress, announce, mod }) {
68
+ export function createUi({ name, claude, router, toaster, report, announce, mod }) {
153
69
  const panes = new Map();
154
70
  const paneLogs = new Map();
155
71
  const openPanes = new Set();
@@ -163,7 +79,7 @@ export function createUi({ name, claude, router, progress, announce, mod }) {
163
79
  let hasLogged = false;
164
80
  return (failure) => {
165
81
  if (!hasLogged)
166
- claude.ui.log(`${name}: the ${what} render ${failure}`);
82
+ report({ id: `render:${what}`, message: `The ${what} render failed`, description: failure });
167
83
  hasLogged = true;
168
84
  };
169
85
  };
@@ -192,10 +108,10 @@ export function createUi({ name, claude, router, progress, announce, mod }) {
192
108
  return drawPieces(drawings.map((drawing) => ({ drawing, isClaudesRow: false })), 'pane');
193
109
  };
194
110
  };
195
- const routeMarkdown = (kind) => {
111
+ const routeMarkdown = (createReader) => {
196
112
  if (read !== undefined)
197
113
  return;
198
- const reader = kind.createReader();
114
+ const reader = createReader();
199
115
  read = reader;
200
116
  router.add('ui.render', async (e, next) => {
201
117
  if (e.component !== 'AssistantMessage')
@@ -275,7 +191,7 @@ export function createUi({ name, claude, router, progress, announce, mod }) {
275
191
  await run();
276
192
  }
277
193
  catch (error) {
278
- claude.ui.log(`${name}: the ${pane.title} pane's ${handler} threw: ${messageOf(error)}`);
194
+ report({ id: `pane:${pane.id}:${handler}`, message: `The ${pane.title} pane's ${handler} threw`, error });
279
195
  }
280
196
  };
281
197
  return {
@@ -304,22 +220,31 @@ export function createUi({ name, claude, router, progress, announce, mod }) {
304
220
  };
305
221
  return handle;
306
222
  },
307
- render(slot, Component) {
308
- const place = slot;
309
- const feature = 'markdown' in place ? `a render of markdown ${place.markdown.name}` : `a render of ${place.component}`;
223
+ render(place, Component) {
224
+ const target = place;
225
+ const markdown = typeof target === 'string' ? undefined : target;
226
+ const component = componentOf(place);
227
+ if (markdown === undefined && component === undefined)
228
+ throw new Error(`${name}: ${String(place)} is not a place render draws, such as ToolCall or CodeBlock.`);
229
+ const feature = `a render of ${nameOf(place)}`;
310
230
  if (renders.has(feature))
311
- throw new Error(`${name}: ${feature} is already added. Render each slot once.`);
231
+ throw new Error(`${name}: ${feature} is already added. Render each place once.`);
312
232
  renders.add(feature);
313
233
  announce(feature);
314
- if ('markdown' in place) {
315
- const kind = place.markdown;
316
- markdownRenders.set(kind.type, { Component: Component, log: logOnce(`markdown ${kind.name}`) });
317
- routeMarkdown(kind);
318
- return;
234
+ if (markdown !== undefined) {
235
+ const markdownType = markdown.markdown;
236
+ markdownRenders.set(markdownType, { Component: Component, log: logOnce(nameOf(place)) });
237
+ routeMarkdown(markdown.createReader);
238
+ return {
239
+ dispose() {
240
+ markdownRenders.delete(markdownType);
241
+ renders.delete(feature);
242
+ claude.ui.invalidate('ui.render');
243
+ },
244
+ };
319
245
  }
320
- const { component } = place;
321
- const log = logOnce(component);
322
- router.add('ui.render', async (e, next) => {
246
+ const log = logOnce(nameOf(place));
247
+ const drawing = router.add('ui.render', async (e, next) => {
323
248
  if (e.component !== component)
324
249
  return next(e);
325
250
  const table = claude.ui.resolve(e);
@@ -332,17 +257,16 @@ export function createUi({ name, claude, router, progress, announce, mod }) {
332
257
  return drawn.drawing;
333
258
  return drawWith(table, () => drawPieces([drawn], e.props.isFirstOfReply ? 'opensReply' : 'inReply'));
334
259
  });
260
+ return {
261
+ dispose() {
262
+ drawing.dispose();
263
+ renders.delete(feature);
264
+ claude.ui.invalidate('ui.render');
265
+ },
266
+ };
335
267
  },
336
- toast: (text, options) => claude.ui.toast(text, options),
337
- async progress(title, task) {
338
- const line = progress.start(title);
339
- try {
340
- return await task((step) => line.report(step));
341
- }
342
- finally {
343
- line.end();
344
- }
345
- },
268
+ toast: toaster.toast,
269
+ copy: toaster.copy,
346
270
  ask: (question, options) => claude.ui.ask(question, options),
347
271
  scroll: (args) => claude.ui.scroll(args),
348
272
  },
@@ -0,0 +1,10 @@
1
+ import type { Turn } from './turn/turn.js';
2
+ export declare const ContextStart = "contextStart";
3
+ export declare const ContextEnd = "contextEnd";
4
+ export type Context = {
5
+ readonly number: number;
6
+ readonly openedAt: string;
7
+ readonly closedAt?: string;
8
+ turns(): Promise<Turn[]>;
9
+ };
10
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/session/context/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAA;AAE1C,eAAO,MAAM,YAAY,iBAAiB,CAAA;AAE1C,eAAO,MAAM,UAAU,eAAe,CAAA;AAEtC,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,KAAK,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,CAAA;CACzB,CAAA"}
@@ -0,0 +1,2 @@
1
+ export const ContextStart = 'contextStart';
2
+ export const ContextEnd = 'contextEnd';
@@ -0,0 +1,7 @@
1
+ import type { MessageOf } from './message.js';
2
+ export declare const Document = "document";
3
+ export type Document = MessageOf<typeof Document> & {
4
+ readonly path: string;
5
+ readonly text: string;
6
+ };
7
+ //# sourceMappingURL=document.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../../../../src/session/context/turn/message/document.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAE7C,eAAO,MAAM,QAAQ,aAAa,CAAA;AAElC,MAAM,MAAM,QAAQ,GAAG,SAAS,CAAC,OAAO,QAAQ,CAAC,GAAG;IAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB,CAAA"}
@@ -0,0 +1 @@
1
+ export const Document = 'document';