@cmodjs/core 1.5.0 → 1.5.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.
package/docs/ui.md CHANGED
@@ -411,7 +411,7 @@ export const indexer = defineMod({
411
411
 
412
412
  An error cmod catches for a mod, such as a hook that throws, shows as an error toast that stays, with its buttons on a line under the message:
413
413
 
414
- - `Retry` runs the failed step again, where running it again can work: an install, the mod's setup, or a program that stopped.
414
+ - `Retry` runs the failed step again, where running it again can work: an install, the mod's setup, or a program that stopped. The toast stays where it is, and the step's progress shows in place of its description, or of its buttons when it has none, until the try ends.
415
415
  - `Report` replaces the toast's buttons with the ways to tell the mod's author, with the keys on the first, and `Back` returns to the buttons. The same failure happening again keeps them open. `Ask Claude to report it` sends this session a prompt to file the report with the person's own tools, and Claude Code asks before each step. `Report it on GitHub (opens your browser)` opens a filled-in issue on the mod's `supportUrl` or `repository` in its `plugin.json`, or `Email the author (opens your mail app)` writes to its `author.email`. `Copy the error` copies the mod's name and version, the `@cmodjs/core` version, the message, the whole cause, and the stack.
416
416
  - `Hide` closes the toast and keeps the same failure hidden in later sessions. A new version of the mod, or a different failure, shows again.
417
417
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmodjs/core",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Build Claude Code mods: hooks, panes, permissions, checks, and an install that just works.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/records.d.ts CHANGED
@@ -72,7 +72,7 @@ export declare function marketplaceOf(root: string, name: string): string | unde
72
72
  export declare function updatesToTurnOn(marketplace: string | undefined, settings: unknown, known: unknown): string | undefined;
73
73
  export declare function consentPath(store: string): string;
74
74
  export declare function parseConsent(value: unknown, path: string): Record<string, string[]>;
75
- export declare const coreVersion = "1.5.0";
75
+ export declare const coreVersion = "1.5.1";
76
76
  export declare function oldestCmodFor(steps: Steps): string;
77
77
  export declare function keyWords(keys: Readonly<Record<string, string>> | undefined): string;
78
78
  export declare function scriptPaths(command: string): string[];
package/records.js CHANGED
@@ -220,7 +220,7 @@ function readKeys(keys) {
220
220
  }
221
221
  return { keys: commands };
222
222
  }
223
- export const coreVersion = '1.5.0';
223
+ export const coreVersion = '1.5.1';
224
224
  export function oldestCmodFor(steps) {
225
225
  if (steps.permissions?.some((item) => item === 'tasks' || item.startsWith('config:')) === true)
226
226
  return '1.0.0';
@@ -301,7 +301,7 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
301
301
  claude().ui.log(`${name} waits for consent to run ${event.install}. Run cmod install ${name} in a terminal.`);
302
302
  return;
303
303
  }
304
- installing('Waiting for your answer');
304
+ runtime?.toaster.toast.info('Installing', { id: installId, description: 'Waiting for your answer', duration: Infinity });
305
305
  const updates = await updatesToShow().catch((error) => {
306
306
  toastFailure({ id: 'cmod:updates', message: 'Could not read whether its marketplace updates automatically', error });
307
307
  return undefined;
@@ -1 +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,KAAK,EAAE,OAAO,EAAgB,SAAS,EAAE,MAAM,WAAW,CAAA;AAIxE,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,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,OAAO,CAAA;CAAE,CAAA;AAEhK,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAA;IAC/B,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,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,CAAA;CACnH,CAAA;AAsCD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,MAAM,GAAG,SAAS,GAAG,OAAO,CAwOtG"}
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,KAAK,EAAE,OAAO,EAAgB,SAAS,EAAE,MAAM,WAAW,CAAA;AAIxE,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,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,OAAO,CAAA;CAAE,CAAA;AAEhK,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAA;IAC/B,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,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,CAAA;CACnH,CAAA;AAuCD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,MAAM,GAAG,SAAS,GAAG,OAAO,CA6PtG"}
@@ -32,7 +32,7 @@ export function createToaster(claude, name, version) {
32
32
  let showsAll = false;
33
33
  let bandId;
34
34
  const changed = () => {
35
- const isLoading = [...entries.values()].some((entry) => entry.type === 'loading');
35
+ const isLoading = [...entries.values()].some((entry) => entry.type === 'loading' || entry.trying !== undefined);
36
36
  if (isLoading && spinning === undefined) {
37
37
  spinning = claude.clock.every(spinnerMs, () => {
38
38
  frame += 1;
@@ -75,6 +75,11 @@ export function createToaster(claude, name, version) {
75
75
  const put = (type, message, options, actions, hides) => {
76
76
  const id = options.id ?? (lastId += 1);
77
77
  const previous = entries.get(id);
78
+ if (previous?.trying !== undefined && type === 'loading') {
79
+ previous.trying = options.description ?? message;
80
+ changed();
81
+ return id;
82
+ }
78
83
  if (previous !== undefined)
79
84
  disarm(previous);
80
85
  const reporting = hides !== undefined && previous?.hides === hides ? previous.reporting : undefined;
@@ -149,7 +154,23 @@ export function createToaster(claude, name, version) {
149
154
  if ('deny' in focused)
150
155
  claude.ui.log(`${name}: the Report choices did not take the keys: ${String(focused.deny)}`, { to: 'debug' });
151
156
  };
152
- const actions = [...(retry === undefined ? [] : [{ label: 'Retry', onClick: retry }]), { label: 'Report', onClick: startReporting }];
157
+ const tryAgain = async () => {
158
+ const shown = entries.get(id);
159
+ if (shown === undefined || retry === undefined || shown.trying !== undefined)
160
+ return;
161
+ shown.trying = 'Starting';
162
+ changed();
163
+ try {
164
+ await retry();
165
+ }
166
+ finally {
167
+ if (entries.get(id) === shown) {
168
+ shown.trying = undefined;
169
+ changed();
170
+ }
171
+ }
172
+ };
173
+ const actions = [...(retry === undefined ? [] : [{ label: 'Retry', onClick: tryAgain }]), { label: 'Report', onClick: startReporting }];
153
174
  return put('error', message, { id, ...(description === undefined ? {} : { description }), duration: Infinity }, actions, hides);
154
175
  };
155
176
  const toggle = (entry) => () => {
@@ -160,14 +181,16 @@ export function createToaster(claude, name, version) {
160
181
  const row = (entry) => {
161
182
  const { description } = entry.options;
162
183
  const isText = typeof description === 'string';
163
- const icon = entry.type === 'loading' ? { glyph: spinner[frame % spinner.length], color: 'claude' } : icons[entry.type];
184
+ const icon = entry.type === 'loading' || entry.trying !== undefined ? { glyph: spinner[frame % spinner.length], color: 'claude' } : icons[entry.type];
164
185
  const message = isText ? Button({ plain: true, key: `toast:${entry.id}:open`, label: entry.message, onPress: toggle(entry) }) : Text({ children: entry.message });
165
186
  const details = !isText ? description : Text({ dimColor: true, wrap: entry.isOpen ? 'wrap' : 'truncate-end', children: entry.isOpen ? description : firstLine(description) });
166
187
  const fixed = (child) => Box({ flexShrink: 0, children: child });
167
188
  const close = Button({ plain: true, dimColor: true, key: `toast:${entry.id}:close`, label: entry.hides === undefined ? '✕' : 'Hide', onPress: () => hide(entry) });
168
189
  const actions = entry.actions.map((action, index) => Button({ plain: true, key: `toast:${entry.id}:${index === 0 ? 'action' : `action-${index}`}`, label: action.label, onPress: press(entry, action.onClick) }));
169
- const text = details === undefined ? [message] : [message, details];
170
- const under = entry.reporting === undefined ? [Box({ gap: 2, flexWrap: 'wrap', children: [...actions, close] })] : reportChoices(entry, entry.reporting);
190
+ const buttons = Box({ gap: 2, flexWrap: 'wrap', children: [...actions, close] });
191
+ const trying = entry.trying === undefined ? undefined : Box({ gap: 1, children: [Text({ dimColor: true, children: 'Trying again:' }), typeof entry.trying === 'string' ? Text({ dimColor: true, wrap: 'truncate-end', children: entry.trying }) : entry.trying] });
192
+ const text = details === undefined ? [message] : [message, trying ?? details];
193
+ const under = trying !== undefined && details === undefined ? [trying] : entry.reporting === undefined ? [buttons] : reportChoices(entry, entry.reporting);
171
194
  return Box({
172
195
  gap: 2,
173
196
  children: [
package/src/records.ts CHANGED
@@ -250,7 +250,7 @@ function readKeys(keys: unknown): Steps {
250
250
  return { keys: commands }
251
251
  }
252
252
 
253
- export const coreVersion = '1.5.0'
253
+ export const coreVersion = '1.5.1'
254
254
 
255
255
  export function oldestCmodFor(steps: Steps): string {
256
256
  if (steps.permissions?.some((item) => item === 'tasks' || item.startsWith('config:')) === true) return '1.0.0'
@@ -367,7 +367,7 @@ export function createLifecycle<State extends object, Declared extends Options =
367
367
  claude().ui.log(`${name} waits for consent to run ${event.install}. Run cmod install ${name} in a terminal.`)
368
368
  return
369
369
  }
370
- installing('Waiting for your answer')
370
+ runtime?.toaster.toast.info('Installing', { id: installId, description: 'Waiting for your answer', duration: Infinity })
371
371
  const updates = await updatesToShow().catch((error: unknown) => {
372
372
  toastFailure({ id: 'cmod:updates', message: 'Could not read whether its marketplace updates automatically', error })
373
373
  return undefined
@@ -32,6 +32,7 @@ type Entry = {
32
32
  readonly hides?: string
33
33
  isOpen: boolean
34
34
  reporting?: Reporting | undefined
35
+ trying?: string | RenderElement | undefined
35
36
  timer: Timer | undefined
36
37
  }
37
38
 
@@ -71,7 +72,7 @@ export function createToaster(claude: Claude, name: string, version: () => strin
71
72
  let bandId: string | undefined
72
73
 
73
74
  const changed = () => {
74
- const isLoading = [...entries.values()].some((entry) => entry.type === 'loading')
75
+ const isLoading = [...entries.values()].some((entry) => entry.type === 'loading' || entry.trying !== undefined)
75
76
  if (isLoading && spinning === undefined) {
76
77
  spinning = claude.clock.every(spinnerMs, () => {
77
78
  frame += 1
@@ -113,6 +114,11 @@ export function createToaster(claude: Claude, name: string, version: () => strin
113
114
  const put = (type: ToastType, message: string, options: ToastOptions, actions: readonly Action[], hides?: string): ToastId => {
114
115
  const id = options.id ?? (lastId += 1)
115
116
  const previous = entries.get(id)
117
+ if (previous?.trying !== undefined && type === 'loading') {
118
+ previous.trying = options.description ?? message
119
+ changed()
120
+ return id
121
+ }
116
122
  if (previous !== undefined) disarm(previous)
117
123
  const reporting = hides !== undefined && previous?.hides === hides ? previous.reporting : undefined
118
124
  const entry: Entry = { id, type, message, options, actions, ...(hides === undefined ? {} : { hides }), isOpen: previous?.isOpen ?? false, reporting, timer: undefined }
@@ -192,7 +198,21 @@ export function createToaster(claude: Claude, name: string, version: () => strin
192
198
  const focused = await claude.ui.focus({ requestId: bandId, key: `toast:${id}:ask-claude` })
193
199
  if ('deny' in focused) claude.ui.log(`${name}: the Report choices did not take the keys: ${String(focused.deny)}`, { to: 'debug' })
194
200
  }
195
- const actions = [...(retry === undefined ? [] : [{ label: 'Retry', onClick: retry }]), { label: 'Report', onClick: startReporting }]
201
+ const tryAgain = async () => {
202
+ const shown = entries.get(id)
203
+ if (shown === undefined || retry === undefined || shown.trying !== undefined) return
204
+ shown.trying = 'Starting'
205
+ changed()
206
+ try {
207
+ await retry()
208
+ } finally {
209
+ if (entries.get(id) === shown) {
210
+ shown.trying = undefined
211
+ changed()
212
+ }
213
+ }
214
+ }
215
+ const actions = [...(retry === undefined ? [] : [{ label: 'Retry', onClick: tryAgain }]), { label: 'Report', onClick: startReporting }]
196
216
  return put('error', message, { id, ...(description === undefined ? {} : { description }), duration: Infinity }, actions, hides)
197
217
  }
198
218
 
@@ -205,14 +225,16 @@ export function createToaster(claude: Claude, name: string, version: () => strin
205
225
  const row = (entry: Entry): RenderElement => {
206
226
  const { description } = entry.options
207
227
  const isText = typeof description === 'string'
208
- const icon = entry.type === 'loading' ? { glyph: spinner[frame % spinner.length] as string, color: 'claude' } : icons[entry.type]
228
+ const icon = entry.type === 'loading' || entry.trying !== undefined ? { glyph: spinner[frame % spinner.length] as string, color: 'claude' } : icons[entry.type]
209
229
  const message = isText ? Button({ plain: true, key: `toast:${entry.id}:open`, label: entry.message, onPress: toggle(entry) }) : Text({ children: entry.message })
210
230
  const details = !isText ? description : Text({ dimColor: true, wrap: entry.isOpen ? 'wrap' : 'truncate-end', children: entry.isOpen ? description : firstLine(description) })
211
231
  const fixed = (child: RenderElement) => Box({ flexShrink: 0, children: child })
212
232
  const close = Button({ plain: true, dimColor: true, key: `toast:${entry.id}:close`, label: entry.hides === undefined ? '✕' : 'Hide', onPress: () => hide(entry) })
213
233
  const actions = entry.actions.map((action, index) => Button({ plain: true, key: `toast:${entry.id}:${index === 0 ? 'action' : `action-${index}`}`, label: action.label, onPress: press(entry, action.onClick) }))
214
- const text = details === undefined ? [message] : [message, details]
215
- const under = entry.reporting === undefined ? [Box({ gap: 2, flexWrap: 'wrap', children: [...actions, close] })] : reportChoices(entry, entry.reporting)
234
+ const buttons = Box({ gap: 2, flexWrap: 'wrap', children: [...actions, close] })
235
+ const trying = entry.trying === undefined ? undefined : Box({ gap: 1, children: [Text({ dimColor: true, children: 'Trying again:' }), typeof entry.trying === 'string' ? Text({ dimColor: true, wrap: 'truncate-end', children: entry.trying }) : entry.trying] })
236
+ const text = details === undefined ? [message] : [message, trying ?? details]
237
+ const under = trying !== undefined && details === undefined ? [trying] : entry.reporting === undefined ? [buttons] : reportChoices(entry, entry.reporting)
216
238
  return Box({
217
239
  gap: 2,
218
240
  children: [