@cmodjs/core 4.0.3 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/docs/commands.md +44 -9
  2. package/docs/index.md +3 -3
  3. package/docs/install-steps.md +8 -8
  4. package/docs/jobs.md +2 -1
  5. package/docs/mod.md +1 -1
  6. package/docs/options.md +1 -1
  7. package/docs/permissions.md +1 -1
  8. package/docs/session.md +46 -13
  9. package/docs/testing.md +15 -11
  10. package/docs/ui.md +43 -11
  11. package/installer.d.ts +3 -2
  12. package/installer.d.ts.map +1 -1
  13. package/jobs/program.d.ts +1 -0
  14. package/jobs/program.d.ts.map +1 -1
  15. package/jobs/program.js +39 -23
  16. package/mod.d.ts +3 -2
  17. package/mod.d.ts.map +1 -1
  18. package/mod.js +1 -0
  19. package/package.json +1 -1
  20. package/records.d.ts +15 -1
  21. package/records.d.ts.map +1 -1
  22. package/records.js +39 -1
  23. package/register.d.ts.map +1 -1
  24. package/register.js +8 -5
  25. package/runtime/before.d.ts +6 -2
  26. package/runtime/before.d.ts.map +1 -1
  27. package/runtime/before.js +56 -18
  28. package/runtime/buttons.d.ts +10 -0
  29. package/runtime/buttons.d.ts.map +1 -0
  30. package/runtime/buttons.js +12 -0
  31. package/runtime/conversations.d.ts +3 -0
  32. package/runtime/conversations.d.ts.map +1 -1
  33. package/runtime/conversations.js +164 -27
  34. package/runtime/engine.d.ts +2 -0
  35. package/runtime/engine.d.ts.map +1 -1
  36. package/runtime/grants.d.ts +1 -0
  37. package/runtime/grants.d.ts.map +1 -1
  38. package/runtime/grants.js +5 -2
  39. package/runtime/hotkeys.d.ts.map +1 -1
  40. package/runtime/hotkeys.js +7 -4
  41. package/runtime/installer.d.ts +10 -6
  42. package/runtime/installer.d.ts.map +1 -1
  43. package/runtime/installer.js +106 -48
  44. package/runtime/lifecycle.d.ts +2 -2
  45. package/runtime/lifecycle.d.ts.map +1 -1
  46. package/runtime/lifecycle.js +144 -42
  47. package/runtime/pages.d.ts +3 -1
  48. package/runtime/pages.d.ts.map +1 -1
  49. package/runtime/pages.js +48 -14
  50. package/runtime/parts.d.ts +1 -0
  51. package/runtime/parts.d.ts.map +1 -1
  52. package/runtime/parts.js +6 -3
  53. package/runtime/programs.d.ts +1 -0
  54. package/runtime/programs.d.ts.map +1 -1
  55. package/runtime/programs.js +4 -1
  56. package/runtime/sessions.d.ts +10 -3
  57. package/runtime/sessions.d.ts.map +1 -1
  58. package/runtime/sessions.js +104 -59
  59. package/runtime/toaster.d.ts.map +1 -1
  60. package/runtime/toaster.js +23 -9
  61. package/runtime/ui.d.ts +22 -4
  62. package/runtime/ui.d.ts.map +1 -1
  63. package/runtime/ui.js +161 -59
  64. package/runtime/watches.d.ts.map +1 -1
  65. package/runtime/watches.js +18 -4
  66. package/runtime/widgets.d.ts +7 -5
  67. package/runtime/widgets.d.ts.map +1 -1
  68. package/runtime/widgets.js +31 -21
  69. package/session/context/turn/message/attached-file.d.ts +13 -0
  70. package/session/context/turn/message/attached-file.d.ts.map +1 -0
  71. package/session/context/turn/message/attached-file.js +21 -0
  72. package/session/context/turn/message/message.d.ts +3 -2
  73. package/session/context/turn/message/message.d.ts.map +1 -1
  74. package/session/context/turn/message/message.js +20 -6
  75. package/session/conversation.d.ts +15 -3
  76. package/session/conversation.d.ts.map +1 -1
  77. package/session/conversation.js +23 -4
  78. package/session/goal/task.d.ts +5 -0
  79. package/session/goal/task.d.ts.map +1 -1
  80. package/session/session.d.ts +36 -7
  81. package/session/session.d.ts.map +1 -1
  82. package/session/session.js +2 -1
  83. package/src/installer.ts +3 -2
  84. package/src/jobs/program.ts +41 -21
  85. package/src/mod.ts +3 -2
  86. package/src/records.ts +49 -1
  87. package/src/register.ts +11 -8
  88. package/src/runtime/before.ts +61 -22
  89. package/src/runtime/buttons.ts +22 -0
  90. package/src/runtime/conversations.ts +170 -34
  91. package/src/runtime/engine.ts +2 -0
  92. package/src/runtime/grants.ts +6 -2
  93. package/src/runtime/hotkeys.ts +7 -4
  94. package/src/runtime/installer.ts +147 -83
  95. package/src/runtime/lifecycle.ts +149 -48
  96. package/src/runtime/pages.ts +56 -12
  97. package/src/runtime/parts.ts +8 -4
  98. package/src/runtime/programs.ts +5 -1
  99. package/src/runtime/sessions.ts +106 -59
  100. package/src/runtime/toaster.ts +27 -9
  101. package/src/runtime/ui.ts +166 -62
  102. package/src/runtime/watches.ts +20 -5
  103. package/src/runtime/widgets.ts +35 -23
  104. package/src/session/context/turn/message/attached-file.ts +36 -0
  105. package/src/session/context/turn/message/message.ts +20 -7
  106. package/src/session/conversation.ts +31 -6
  107. package/src/session/goal/task.ts +6 -0
  108. package/src/session/session.ts +18 -7
  109. package/src/testing/fake-claude.ts +21 -6
  110. package/src/testing/fake-elements.ts +57 -8
  111. package/src/testing/fake-sessions.ts +72 -24
  112. package/src/testing.ts +114 -33
  113. package/src/ui/areas.ts +1 -0
  114. package/src/ui/components.ts +14 -8
  115. package/src/ui/elements.ts +6 -2
  116. package/src/ui/markdown.ts +3 -3
  117. package/src/ui/panes.ts +2 -2
  118. package/src/ui/styles.ts +6 -2
  119. package/testing/fake-claude.d.ts +3 -0
  120. package/testing/fake-claude.d.ts.map +1 -1
  121. package/testing/fake-claude.js +18 -5
  122. package/testing/fake-elements.d.ts.map +1 -1
  123. package/testing/fake-elements.js +52 -7
  124. package/testing/fake-sessions.d.ts +24 -10
  125. package/testing/fake-sessions.d.ts.map +1 -1
  126. package/testing/fake-sessions.js +66 -20
  127. package/testing.d.ts +8 -12
  128. package/testing.d.ts.map +1 -1
  129. package/testing.js +113 -27
  130. package/ui/areas.d.ts +4 -0
  131. package/ui/areas.d.ts.map +1 -1
  132. package/ui/components.d.ts +1 -0
  133. package/ui/components.d.ts.map +1 -1
  134. package/ui/components.js +11 -7
  135. package/ui/elements.d.ts +3 -0
  136. package/ui/elements.d.ts.map +1 -1
  137. package/ui/elements.js +6 -1
  138. package/ui/markdown.d.ts +2 -2
  139. package/ui/markdown.d.ts.map +1 -1
  140. package/ui/panes.d.ts +2 -1
  141. package/ui/panes.d.ts.map +1 -1
  142. package/ui/styles.d.ts +11 -2
  143. package/ui/styles.d.ts.map +1 -1
@@ -1,4 +1,4 @@
1
- import { consentPath, dataFolder, finishStepsMethod, grantFix, coreVersion, isAtLeast, marketplaceOf, parseConsent, pendingStepsMethod, parseEvent, permissionWords, readRecord, readSteps, recordPath, scriptsSha256, storeFolder, updatesToTurnOn } from '../records.js';
1
+ import { consentPath, dataFolder, finishStepsMethod, grantFix, coreVersion, isAtLeast, isProgramNewer, marketplaceOf, parseConsent, pendingStepsMethod, parseEvent, permissionWords, programPath, readRecord, readSteps, recordPath, scriptsSha256, storeFolder, updatesToTurnOn } from '../records.js';
2
2
  import { homePrefix, ifMissing, relativePath } from '../utils/paths.js';
3
3
  import { firstLine, formatExit, messageOf } from '../utils/text.js';
4
4
  import { beforeDeadline } from './deadline.js';
@@ -8,8 +8,8 @@ import { serveConversations } from './conversations.js';
8
8
  import { answerCall, answeringOwnCalls, cmodPluginName, dependencyCalls, withBareRefusals } from './dependencies.js';
9
9
  import { keepEngine } from './engine.js';
10
10
  import { classicFailure, classicHook, userSkillHook } from './hooks.js';
11
- import { checkedAnswer, checkingGrants, checkSettings, checksAnswers, checkWrite, isCovered, itemOf, passedDown, programAddresses } from './grants.js';
12
- import { cmodError, locatingPrograms, readLines } from './programs.js';
11
+ import { checkedAnswer, checkingGrants, checkRun, checkSettings, checksAnswers, checkWrite, isCovered, itemOf, passedDown, programAddresses } from './grants.js';
12
+ import { cmodError, locatingPrograms, readLines, spawnCmod } from './programs.js';
13
13
  import { createModFiles, resolvedPath } from './metadata.js';
14
14
  import { fitsOption } from '../options.js';
15
15
  import { createInstaller } from './installer.js';
@@ -35,6 +35,8 @@ const askingId = 'cmod:asking';
35
35
  const recordId = 'cmod:record';
36
36
  const installRetrySeconds = [2, 4, 8, 16, 32, 64, 128];
37
37
  const cmodCheckMs = 1000;
38
+ const recordCheckMs = 1000;
39
+ const drawsPerMs = 1000;
38
40
  const cmodWaitMs = 60_000;
39
41
  const cmodSlowCheckMs = 10_000;
40
42
  const hookMs = 10_000;
@@ -68,7 +70,8 @@ export async function readPlugin(claude) {
68
70
  if (version === undefined || record?.scriptsSha256 !== scripts)
69
71
  return { ...plugin, isInstalled: false, shouldRecord: false };
70
72
  const isInstalled = record.version === version || record.root === root;
71
- return { ...plugin, isInstalled, shouldRecord: isInstalled && record.version !== version };
73
+ const programModifiedMs = record.program === null ? undefined : (await claude.fs.stat(programPath(store, record.program, record.version)).catch(ifMissing(undefined)))?.mtimeMs;
74
+ return { ...plugin, isInstalled, shouldRecord: isInstalled && (record.version !== version || isProgramNewer(record, programModifiedMs)) };
72
75
  }
73
76
  async function filesBelow(claude, folder) {
74
77
  const entries = await claude.fs.list(folder);
@@ -121,6 +124,8 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
121
124
  let raise;
122
125
  let failure;
123
126
  let pluginOptions = options;
127
+ let drawCount = 0;
128
+ const nextDraw = () => (drawCount += 1);
124
129
  const installer = createInstaller(definition.name, () => claude(), {
125
130
  show: (open) => {
126
131
  if (phase !== 'active')
@@ -128,7 +133,7 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
128
133
  runtime?.toaster.toast.info('Installing', { id: askingId, description: 'Waiting for your answer', duration: Infinity, action: { label: 'Answer', onClick: open } });
129
134
  },
130
135
  hide: () => runtime?.toaster.toast.dismiss(askingId),
131
- });
136
+ }, nextDraw);
132
137
  let shouldRecord = false;
133
138
  let recording;
134
139
  let missedSessionStart;
@@ -147,6 +152,7 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
147
152
  failure ??= new Error(`${definition.name}: ${reason}${/[.!?]$/.test(reason) ? '' : '.'}${fix === undefined ? '' : ` ${fix}`}`, { cause: error });
148
153
  const again = retry === undefined ? {} : { retry: () => ((failure = undefined), stopRetrying(), retry().catch(report)) };
149
154
  toastFailure({ id: installId, message: reason, ...(fix === undefined ? {} : { description: fix }), ...(error === undefined ? {} : { error }), ...again });
155
+ watchRecord();
150
156
  };
151
157
  let consented;
152
158
  const installAgain = () => install(consented);
@@ -255,29 +261,34 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
255
261
  };
256
262
  const runSteps = async (active) => {
257
263
  const steps = asksPerson ? (definition.installer ?? []) : [];
258
- for (const [index, step] of steps.entries()) {
259
- if (await isStepDone(active, step))
260
- continue;
261
- const isAnswered = (await claude().session.surfaces()).length > 0 && (await installer.step(active, step, index + 1, steps.length));
262
- if (!isAnswered) {
263
- runtime?.toaster.toast.warning(steps.length - index === 1 ? 'Needs one more step' : `Needs ${steps.length - index} more steps`, {
264
- id: 'cmod:steps',
265
- description: `Starting with ${step.title}.`,
266
- duration: Infinity,
267
- action: { label: 'Finish', onClick: () => runSteps(active) },
268
- });
269
- break;
264
+ try {
265
+ for (const [index, step] of steps.entries()) {
266
+ if (await isStepDone(active, step))
267
+ continue;
268
+ const isAnswered = (await claude().session.surfaces()).length > 0 && (await installer.step(active, step, index + 1, steps.length));
269
+ if (!isAnswered) {
270
+ runtime?.toaster.toast.warning(steps.length - index === 1 ? 'Needs one more step' : `Needs ${steps.length - index} more steps`, {
271
+ id: 'cmod:steps',
272
+ description: `Starting with ${step.title}.`,
273
+ duration: Infinity,
274
+ action: { label: 'Finish', onClick: () => runSteps(active) },
275
+ });
276
+ break;
277
+ }
270
278
  }
271
279
  }
272
- await installer.close();
280
+ finally {
281
+ await installer.close();
282
+ }
273
283
  };
274
284
  const activate = async () => {
275
285
  if (runtime === undefined || plugin === undefined)
276
286
  return;
277
287
  const activeRouter = createRouter();
278
- const active = await createMod(definition, { ...runtime, router: activeRouter, dataFolder: dataFolder(plugin.store, plugin.name), store: plugin.store, steps: plugin.steps, granted, refreshGrants, permissionRoutes, options: pluginOptions }).catch(async (error) => {
288
+ const active = await createMod(definition, { ...runtime, router: activeRouter, dataFolder: dataFolder(plugin.store, plugin.name), store: plugin.store, steps: plugin.steps, granted, refreshGrants, permissionRoutes, options: pluginOptions, nextDraw }).catch(async (error) => {
279
289
  if (error instanceof MissingOptions && (await askOptions(error)))
280
290
  return undefined;
291
+ await installer.close();
281
292
  fail(messageOf(error), error instanceof MissingOptions ? `Retry asks for ${error.keys.length === 1 ? 'it' : 'them'}.` : undefined, begin, error);
282
293
  throw error;
283
294
  });
@@ -289,7 +300,10 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
289
300
  raise = active.raise;
290
301
  router = activeRouter;
291
302
  phase = 'active';
303
+ stopWatchingRecord();
292
304
  runtime.claude.ui.invalidate('ui.render');
305
+ if (definition.name === cmodPluginName)
306
+ runtime.claude.ui.invalidate('command.describe');
293
307
  void runSteps(active.mod).catch((error) => toastFailure({ id: 'cmod:steps', message: 'Its installer stopped', error }));
294
308
  const missed = missedSessionStart;
295
309
  missedSessionStart = undefined;
@@ -343,9 +357,15 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
343
357
  toastFailure({ id: 'cmod:updates', message: 'Could not read whether its marketplace updates automatically', error });
344
358
  return undefined;
345
359
  });
346
- const isAccepted = await installer.consent({ ...event, updates });
360
+ const installedElsewhere = new Promise((resolve) => (settleAsked = () => resolve('installed')));
361
+ watchRecord();
362
+ const answer = await Promise.race([installer.consent({ ...event, program: plugin?.steps.program ?? '', updates }), installedElsewhere]).finally(() => {
363
+ settleAsked = undefined;
364
+ });
347
365
  await installer.close();
348
- if (isAccepted) {
366
+ if (answer === 'installed')
367
+ return startInstalled();
368
+ if (answer) {
349
369
  consented = event.sha256;
350
370
  return install(consented);
351
371
  }
@@ -480,8 +500,8 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
480
500
  };
481
501
  let recordMs = 0;
482
502
  let readStarted;
483
- const recover = async () => {
484
- if (plugin === undefined || readStarted === undefined || (phase !== 'failed' && phase !== 'declined'))
503
+ const isNewlyInstalled = async () => {
504
+ if (plugin === undefined || readStarted === undefined)
485
505
  return false;
486
506
  const modifiedMs = await stampOf(recordPath(plugin.store, plugin.name));
487
507
  if (modifiedMs === recordMs)
@@ -491,12 +511,40 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
491
511
  if (!current.isInstalled)
492
512
  return false;
493
513
  plugin = current;
514
+ return true;
515
+ };
516
+ const startInstalled = async () => {
494
517
  failure = undefined;
495
518
  activation = undefined;
496
519
  await refreshGrants();
497
520
  await finish();
521
+ };
522
+ let settleAsked;
523
+ const recover = async () => {
524
+ if ((settleAsked === undefined && phase !== 'failed' && phase !== 'declined') || !(await isNewlyInstalled()))
525
+ return false;
526
+ if (settleAsked === undefined)
527
+ await startInstalled();
528
+ else
529
+ settleAsked();
498
530
  return true;
499
531
  };
532
+ let recordCheck;
533
+ let checking;
534
+ const watchRecord = () => {
535
+ recordCheck ??= claude().clock.every(recordCheckMs, () => {
536
+ checking ??= recover()
537
+ .then(() => undefined)
538
+ .catch((error) => claude().ui.log(`${definition.name} could not read whether another cmod installed it: ${messageOf(error)}`, { to: 'debug' }))
539
+ .finally(() => {
540
+ checking = undefined;
541
+ });
542
+ });
543
+ };
544
+ const stopWatchingRecord = () => {
545
+ recordCheck?.cancel();
546
+ recordCheck = undefined;
547
+ };
500
548
  const refreshGrants = async () => {
501
549
  if (plugin === undefined || plugin.name === cmodPluginName)
502
550
  return;
@@ -579,6 +627,7 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
579
627
  runtime = { claude: programs.claude, toaster: createToaster(claudeCalls, definition.name, () => plugin?.version, () => openSettings('permissions')) };
580
628
  readStarted = read;
581
629
  try {
630
+ drawCount = (await claudeCalls.clock.now()) * drawsPerMs;
582
631
  await programs.refresh();
583
632
  await begin().catch(report);
584
633
  }
@@ -589,10 +638,16 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
589
638
  async route(event, e, next) {
590
639
  if (imports !== undefined)
591
640
  bind(imports);
592
- if (event === 'ui.render' && installer.draws(e))
593
- return installer.draw(e);
594
- if (event === 'ui.close')
595
- installer.closed(e.id);
641
+ const asked = e;
642
+ if (event === 'cmod.call' && asked.to === definition.name && asked.caller === cmodPluginName) {
643
+ if (asked.method === partMethods.shown)
644
+ installer.hear(asked.input);
645
+ const answered = installer.answer(asked.method, asked.input);
646
+ if (answered !== undefined)
647
+ return (await answered.catch((error) => ({ deny: `${definition.name}: ${messageOf(error)}` })));
648
+ if (asked.method === partMethods.shown && phase !== 'active')
649
+ return { value: null };
650
+ }
596
651
  if (event === 'classic.SessionStart') {
597
652
  missedSessionStart = undefined;
598
653
  hasHeardSessionStart = true;
@@ -605,10 +660,11 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
605
660
  record();
606
661
  if (event === 'classic.UserPromptSubmit' || event === 'classic.SessionStart')
607
662
  await recover().catch((error) => claude().ui.log(`${definition.name} stays stopped: ${messageOf(error)}`, { to: 'debug' }));
608
- const isCallToThisMod = event === 'cmod.call' && e.to === definition.name;
663
+ const tool = e.tool;
664
+ const isCallToThisMod = (event === 'cmod.call' && e.to === definition.name) || (event === 'tool.call' && typeof tool === 'string' && tool.startsWith(`mcp__${definition.name}__`));
609
665
  if (event === 'cmod.call' && !isCallToThisMod)
610
666
  return next(e);
611
- await (event === 'classic.SessionStart' || isCallToThisMod ? holdUntilStarted() : whenActive());
667
+ await (event === 'classic.SessionStart' || event === 'session.append' || isCallToThisMod ? holdUntilStarted() : whenActive());
612
668
  if (event === 'classic.SessionStart' && phase !== 'active') {
613
669
  missedSessionStart = e;
614
670
  return next(e);
@@ -621,8 +677,11 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
621
677
  if (event === 'cmod.call' && call.to === definition.name && mod !== undefined) {
622
678
  if (call.method === pendingStepsMethod)
623
679
  return { value: await pendingSteps(mod) };
624
- if (call.method === finishStepsMethod)
625
- return (await runSteps(mod), { value: null });
680
+ if (call.method === finishStepsMethod) {
681
+ const active = mod;
682
+ const opened = await installer.collect(() => Promise.race([runSteps(active).catch((error) => void toastFailure({ id: 'cmod:steps', message: 'Could not finish the setup', error })), installer.shown()]));
683
+ return { value: opened };
684
+ }
626
685
  }
627
686
  if (event === 'classic.UserPromptSubmit' || event === 'classic.SessionStart')
628
687
  await refreshGrants().catch((error) => claude().ui.log(`${definition.name} keeps its grants from before: ${messageOf(error)}`, { to: 'debug' }));
@@ -638,7 +697,7 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
638
697
  return answer;
639
698
  return runtime.toaster.draw((await answer), runtime.claude.ui.resolve(render), render.props, render.requestId);
640
699
  },
641
- async caught(event, e, next, { kind, message }) {
700
+ async caught(event, e, next, { error: { kind, message }, called }) {
642
701
  const call = e;
643
702
  const isAsk = event === 'cmod.call' && call.to === definition.name && beforeAskOf(call) !== undefined;
644
703
  const reasons = { timeout: `it ran past the ${hookMs / 1000} s Claude Code gives a hook`, throw: message ?? 'it threw' };
@@ -646,7 +705,9 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
646
705
  if (reason === undefined)
647
706
  return isAsk ? { value: null } : next(e);
648
707
  const error = new Error(reason);
649
- toastFailure({ id: `stopped:${event}`, message: `Claude Code stopped its ${event.replace(/^classic\./, '')} hook`, error });
708
+ const failedBeneath = called && kind === 'throw' ? await next(e).then(() => undefined, messageOf) : undefined;
709
+ if (failedBeneath === undefined || failedBeneath !== message)
710
+ toastFailure({ id: `stopped:${event}`, message: `Claude Code stopped its ${event.replace(/^classic\./, '')} hook`, error });
650
711
  return router.dispatchFailure(event, e, error, next);
651
712
  },
652
713
  };
@@ -689,7 +750,13 @@ async function createMod(definition, runtime) {
689
750
  let parentAsked;
690
751
  let hasEarlierPrompts = true;
691
752
  let agentType;
692
- const area = createUi({ name: definition.name, claude, router, report, announce, mod: () => mod });
753
+ let parts;
754
+ const borrowed = (styler, cached, input) => {
755
+ const name = `The markdown style of ${styler}`;
756
+ void parts?.fill(styler, cached, input, name)?.then(() => claude.ui.invalidate('ui.render'));
757
+ return parts?.element(styler, cached, `${styler}/markdown:`, name, `markdown:${styler}`);
758
+ };
759
+ const area = createUi({ name: definition.name, claude, router, report, announce, mod: () => mod, borrowed, nextDraw: runtime.nextDraw });
693
760
  const askParent = async () => {
694
761
  if (definition.name === cmodPluginName)
695
762
  return undefined;
@@ -968,9 +1035,24 @@ async function createMod(definition, runtime) {
968
1035
  const conversations = definition.name === cmodPluginName ? serveConversations({ claude, router, store: runtime.store, project: () => modState.root, report: (id, message, error) => report({ id, message, error }), clear: (id) => toaster.toast.dismiss(id) }) : undefined;
969
1036
  let pages;
970
1037
  const reportPart = (id, message, error) => report({ id, message, error });
971
- const parts = drawnParts({ claude, report: reportPart, press: (to, input) => pages?.press(to, input) });
972
- const widgets = conversations === undefined ? undefined : serveWidgets({ claude, router, store: runtime.store, report: reportPart, turnClosedBy: (rowId) => conversations.turnClosedBy(rowId), parts });
973
- pages = widgets === undefined ? undefined : servePages({ claude, router, report: reportPart, widgets, parts });
1038
+ const pressElsewhere = (to, input) => void claude.cmod.call({ to, method: partMethods.press, input }).catch((error) => {
1039
+ parts?.forget(to);
1040
+ claude.ui.invalidate('ui.render');
1041
+ reportPart(`press:${to}`, `${to} did not take the press`, error);
1042
+ });
1043
+ parts = drawnParts({ claude, report: reportPart, press: (to, input) => (pages === undefined ? pressElsewhere(to, input) : pages.press(to, input)) });
1044
+ const widgets = conversations === undefined ? undefined : serveWidgets({ claude, router, store: runtime.store, report: reportPart, turnClosedBy: (rowId) => conversations.turnClosedBy(rowId), parts, markdownIn: area.markdownIn });
1045
+ pages = widgets === undefined ? undefined : servePages({ claude, router, report: reportPart, widgets, parts, tellStylers: area.restyled });
1046
+ const tellColor = (to, told) => {
1047
+ if (told.session === '' || to === cmodPluginName)
1048
+ return;
1049
+ claude.clock.after(0, () => void claude.cmod.call({ to, method: conversationMethods.sessionColor, input: told }).catch((error) => claude.ui.log(`cmod: ${to} did not hear the session's color: ${messageOf(error)}`, { to: 'debug' })));
1050
+ };
1051
+ conversations?.onColor((told) => {
1052
+ client.recolor(told);
1053
+ for (const { mod: to } of pages?.declared() ?? [])
1054
+ tellColor(to, told);
1055
+ });
974
1056
  router.add('cmod.call', async (e, next) => {
975
1057
  if (e.to !== definition.name)
976
1058
  return next(e);
@@ -982,17 +1064,24 @@ async function createMod(definition, runtime) {
982
1064
  client.changed(changedSession, change, isInProject !== false);
983
1065
  return { value: null };
984
1066
  }
1067
+ if (e.method === conversationMethods.sessionColor && call.caller === cmodPluginName) {
1068
+ client.recolor(e.input);
1069
+ return { value: null };
1070
+ }
985
1071
  const ask = beforeAskOf(call);
986
1072
  if (ask !== undefined)
987
1073
  return { value: (await client.answerBefore(ask.session, ask.event, ask.change, ask.isInProject)) ?? null };
988
1074
  if (e.method === partMethods.changed && call.caller !== undefined && pages !== undefined) {
989
- return { value: { isHeld: pages.update(call.caller, e.input) } };
1075
+ const update = e.input;
1076
+ if (update.parts !== undefined && conversations !== undefined)
1077
+ tellColor(call.caller, conversations.color());
1078
+ return { value: { isHeld: pages.update(call.caller, update), stylers: pages.stylers() } };
990
1079
  }
991
- if (call.caller === cmodPluginName) {
1080
+ if (call.caller !== undefined) {
992
1081
  const turn = e.method === partMethods.draw ? e.input.turn : undefined;
993
- const answered = area.answer(e.method, turn === undefined ? e.input : { ...e.input, turn: client.turn(turn) });
1082
+ const answered = area.answer(e.method, turn === undefined ? e.input : { ...e.input, turn: client.turn(turn) }, call.caller);
994
1083
  if (answered !== undefined)
995
- return answered;
1084
+ return answered.catch((error) => ({ deny: `${definition.name}: ${messageOf(error)}` }));
996
1085
  }
997
1086
  const served = await conversations?.answer(call);
998
1087
  return served ?? answerCall(definition.name, api, e);
@@ -1003,7 +1092,20 @@ async function createMod(definition, runtime) {
1003
1092
  return ask === undefined ? { deny: `${definition.name}: ${error.message}` } : { value: client.failedBefore(ask.session, ask.event, ask.change, ask.isInProject, error) ?? null };
1004
1093
  });
1005
1094
  const agents = toolCalls(claude, router);
1006
- keepEngine(mod, { claude: checked, router, toolCalls: agents, report, hasPermissionCheck: () => runtime.permissionRoutes() !== undefined, answerInput: area.answerInput, ...(widgets === undefined ? {} : { widgets }), ...(pages === undefined ? {} : { pages }) });
1095
+ keepEngine(mod, {
1096
+ claude: checked,
1097
+ router,
1098
+ toolCalls: agents,
1099
+ report,
1100
+ hasPermissionCheck: () => runtime.permissionRoutes() !== undefined,
1101
+ answerInput: area.answerInput,
1102
+ spawnCmod: async (args, program) => {
1103
+ await checkRun(grants, 'process.spawn', program.argv);
1104
+ return spawnCmod(claude, args, session, program.env);
1105
+ },
1106
+ ...(widgets === undefined ? {} : { widgets }),
1107
+ ...(pages === undefined ? {} : { pages }),
1108
+ });
1007
1109
  const names = new Set();
1008
1110
  const reserveName = (type, name, taken) => {
1009
1111
  const key = `${type}:${name}`;
@@ -11,6 +11,7 @@ export type ModPages = {
11
11
  export type PageApply = (mod: string, requests: readonly PageRequest[]) => Promise<void>;
12
12
  export type Pages = {
13
13
  declared(): readonly ModPages[];
14
+ stylers(): readonly string[];
14
15
  element(mod: string, page: string, params: unknown, columns: number, isFocused: boolean): RenderElement | undefined;
15
16
  press(mod: string, press: PartPress): void;
16
17
  update(mod: string, update: PartsUpdate): boolean;
@@ -18,11 +19,12 @@ export type Pages = {
18
19
  apply(listener: PageApply): void;
19
20
  };
20
21
  export declare function pagesOf(input: unknown): PageDeclaration[];
21
- export declare function servePages({ claude, router, report, widgets, parts }: {
22
+ export declare function servePages({ claude, router, report, widgets, parts, tellStylers, }: {
22
23
  readonly claude: Claude;
23
24
  readonly router: Router;
24
25
  readonly report: (id: string, message: string, error: unknown) => void;
25
26
  readonly widgets: Pick<Widgets, 'update'>;
26
27
  readonly parts: DrawnParts;
28
+ readonly tellStylers: (stylers: readonly string[]) => void;
27
29
  }): Pages;
28
30
  //# sourceMappingURL=pages.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../src/runtime/pages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAGhD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAe,KAAK,eAAe,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,SAAS,CAAA;AAChI,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAI3C,MAAM,MAAM,QAAQ,GAAG;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,eAAe,EAAE,CAAA;CAAE,CAAA;AAE3F,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,WAAW,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AAExF,MAAM,MAAM,KAAK,GAAG;IAClB,QAAQ,IAAI,SAAS,QAAQ,EAAE,CAAA;IAC/B,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,aAAa,GAAG,SAAS,CAAA;IACnH,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,IAAI,CAAA;IAC1C,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAA;IACjD,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAAA;IAC1C,KAAK,CAAC,QAAQ,EAAE,SAAS,GAAG,IAAI,CAAA;CACjC,CAAA;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,EAAE,CAGzD;AAED,wBAAgB,UAAU,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAA;CAAE,GAAG,KAAK,CAiGjR"}
1
+ {"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../src/runtime/pages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAIhD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAA2B,KAAK,eAAe,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,SAAS,CAAA;AAC5I,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAI3C,MAAM,MAAM,QAAQ,GAAG;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,eAAe,EAAE,CAAA;CAAE,CAAA;AAE3F,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,WAAW,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AAExF,MAAM,MAAM,KAAK,GAAG;IAClB,QAAQ,IAAI,SAAS,QAAQ,EAAE,CAAA;IAC/B,OAAO,IAAI,SAAS,MAAM,EAAE,CAAA;IAC5B,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,aAAa,GAAG,SAAS,CAAA;IACnH,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,IAAI,CAAA;IAC1C,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAA;IACjD,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAAA;IAC1C,KAAK,CAAC,QAAQ,EAAE,SAAS,GAAG,IAAI,CAAA;CACjC,CAAA;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,EAAE,CAGzD;AAED,wBAAgB,UAAU,CAAC,EACzB,MAAM,EACN,MAAM,EACN,MAAM,EACN,OAAO,EACP,KAAK,EACL,WAAW,GACZ,EAAE;IACD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IACtE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;IACzC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAA;IAC1B,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAA;CAC3D,GAAG,KAAK,CA4HR"}
package/runtime/pages.js CHANGED
@@ -1,13 +1,16 @@
1
1
  import { isObject } from '../records.js';
2
+ import { Text } from '../ui/elements.js';
2
3
  import { messageOf } from '../utils/text.js';
3
- import { partMethods } from './ui.js';
4
+ import { cmodPluginName } from './dependencies.js';
5
+ import { partMethods, stalePress } from './ui.js';
4
6
  const askedWaitMs = 1000;
5
7
  export function pagesOf(input) {
6
8
  const listed = isObject(input) && Array.isArray(input['pages']) ? input['pages'] : [];
7
9
  return listed.filter(isObject).flatMap((each) => (typeof each['id'] === 'string' && typeof each['title'] === 'string' && (each['place'] === 'sidebar' || each['place'] === 'settings') ? [{ id: each['id'], title: each['title'], place: each['place'] }] : []));
8
10
  }
9
- export function servePages({ claude, router, report, widgets, parts }) {
11
+ export function servePages({ claude, router, report, widgets, parts, tellStylers, }) {
10
12
  const declared = new Map();
13
+ const stylers = new Set();
11
14
  const asking = new Map();
12
15
  const pressing = new Set();
13
16
  let waiting = [];
@@ -20,16 +23,18 @@ export function servePages({ claude, router, report, widgets, parts }) {
20
23
  for (const { mod, requests } of taken)
21
24
  await run(mod, requests);
22
25
  };
23
- const settle = async () => {
24
- while (pressing.size > 0)
25
- await Promise.allSettled(pressing);
26
+ const settle = async (earlier) => {
27
+ const own = () => [...pressing].filter((pending) => !earlier.has(pending));
28
+ for (let pending = own(); pending.length > 0; pending = own())
29
+ await Promise.allSettled(pending);
26
30
  await takeWaiting();
27
31
  };
28
32
  const wrapped = async (next) => {
29
33
  inputDepth += 1;
34
+ const earlier = new Set(pressing);
30
35
  try {
31
36
  const answer = await next();
32
- await settle();
37
+ await settle(earlier);
33
38
  return answer;
34
39
  }
35
40
  finally {
@@ -47,11 +52,27 @@ export function servePages({ claude, router, report, widgets, parts }) {
47
52
  asking.set(mod, pending);
48
53
  return pending;
49
54
  };
55
+ const restyle = (mod, isStyler) => {
56
+ if (!isStyler && !stylers.delete(mod))
57
+ return;
58
+ if (isStyler)
59
+ stylers.add(mod);
60
+ parts.forget(mod);
61
+ const listed = [...stylers];
62
+ tellStylers(listed);
63
+ for (const told of declared.keys()) {
64
+ if (told !== cmodPluginName)
65
+ void claude.cmod.call({ to: told, method: partMethods.stylers, input: listed }).catch((error) => claude.ui.log(`cmod: ${told} did not hear which mods style markdown: ${messageOf(error)}`, { to: 'debug' }));
66
+ }
67
+ };
50
68
  const update = (mod, { parts: listing, redraw, requests, isAsked }) => {
51
69
  if (listing !== undefined)
52
70
  declared.set(mod, pagesOf(listing));
53
- if (listing !== undefined || redraw === true)
54
- widgets.update(mod, { ...(listing === undefined ? {} : { parts: listing }), ...(redraw === undefined ? {} : { redraw }) });
71
+ if (listing !== undefined)
72
+ restyle(mod, isObject(listing) && listing['stylesMarkdown'] === true);
73
+ const isRedrawn = redraw === true || requests?.some((request) => 'open' in request) === true;
74
+ if (listing !== undefined || isRedrawn)
75
+ widgets.update(mod, { ...(listing === undefined ? {} : { parts: listing }), ...(isRedrawn ? { redraw: true } : {}) });
55
76
  if (requests === undefined || requests.length === 0)
56
77
  return false;
57
78
  if (isAsked !== true)
@@ -68,28 +89,41 @@ export function servePages({ claude, router, report, widgets, parts }) {
68
89
  .then((answer) => void update(mod, (isObject(answer) ? answer : {})), (error) => {
69
90
  parts.forget(mod);
70
91
  claude.ui.invalidate('ui.render');
71
- report(`press:${mod}`, `${mod} did not take the press`, error);
92
+ if (messageOf(error).includes(stalePress))
93
+ claude.ui.log(`cmod: ${mod} draws its page again, because the button pressed was from an older drawing:${messageOf(error)}`, { to: 'debug' });
94
+ else
95
+ report(`press:${mod}`, `${mod} did not take the press`, error);
72
96
  })
73
97
  .finally(() => pressing.delete(pending));
74
98
  pressing.add(pending);
75
99
  if (inputDepth === 0)
76
100
  void pending.then(takeWaiting);
77
101
  };
78
- const cacheKey = (page, params, columns, isFocused) => `${page}\u0000${JSON.stringify(params ?? null)}\u0000${columns}\u0000${isFocused}`;
102
+ const lastDrawn = new Map();
103
+ const plain = (data) => JSON.parse(JSON.stringify(data));
79
104
  return {
80
105
  declared: () => [...declared].map(([mod, pages]) => ({ mod, pages })),
106
+ stylers: () => [...stylers],
81
107
  element(mod, page, params, columns, isFocused) {
82
108
  if (!declared.has(mod))
83
109
  void ask(mod);
84
- const cached = cacheKey(page, params, columns, isFocused);
110
+ const given = JSON.stringify(params ?? null);
111
+ const cached = `${page}\u0000${given}\u0000${columns}\u0000${isFocused}`;
85
112
  const name = `The ${page} page of ${mod}`;
86
- void parts.fill(mod, cached, { page, params: params ?? null, columns, isFocused }, name)?.then(() => claude.ui.invalidate('ui.render'));
87
- return parts.element(mod, cached, `${mod}/${page}:`, name, `page:${mod}/${page}`);
113
+ const keyPrefix = `${mod}/${page}:`;
114
+ void parts.fill(mod, cached, { page, params: plain(params ?? null), columns, isFocused }, name)?.then(() => claude.ui.invalidate('ui.render'));
115
+ const failure = parts.failure(mod, cached);
116
+ const drawnAt = (key) => parts.element(mod, key, keyPrefix, name, `page:${mod}/${page}`);
117
+ const last = lastDrawn.get(keyPrefix);
118
+ const drawn = drawnAt(cached);
119
+ if (drawn !== undefined)
120
+ lastDrawn.set(keyPrefix, { params: given, cached });
121
+ return drawn ?? (last?.params === given ? drawnAt(last.cached) : undefined) ?? (failure === undefined ? undefined : Text({ color: 'error', children: `${name} could not draw: ${failure}` }));
88
122
  },
89
123
  press,
90
124
  update,
91
125
  tell(mod, shown) {
92
- void claude.cmod.call({ to: mod, method: partMethods.shown, input: shown }).catch((error) => claude.ui.log(`cmod: ${mod} did not hear which of its pages show: ${messageOf(error)}`, { to: 'debug' }));
126
+ void claude.cmod.call({ to: mod, method: partMethods.shown, input: plain(shown) }).catch((error) => claude.ui.log(`cmod: ${mod} did not hear which of its pages show: ${messageOf(error)}`, { to: 'debug' }));
93
127
  },
94
128
  apply(listener) {
95
129
  applyTo = listener;
@@ -4,6 +4,7 @@ import { type PartPress } from './ui.js';
4
4
  export type DrawnParts = {
5
5
  fill(mod: string, cached: string, input: Readonly<Record<string, unknown>>, name: string): Promise<void> | undefined;
6
6
  element(mod: string, cached: string, keyPrefix: string, name: string, reportId: string): RenderElement | undefined;
7
+ failure(mod: string, cached: string): string | undefined;
7
8
  forget(mod: string): void;
8
9
  };
9
10
  export declare function drawnParts({ claude, report, press }: {
@@ -1 +1 @@
1
- {"version":3,"file":"parts.d.ts","sourceRoot":"","sources":["../src/runtime/parts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAGhD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAA+B,KAAK,SAAS,EAAE,MAAM,SAAS,CAAA;AAErE,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS,CAAA;IACpH,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAAA;IAClH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B,CAAA;AAMD,wBAAgB,UAAU,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,KAAK,IAAI,CAAA;CAAE,GAAG,UAAU,CAqD9N"}
1
+ {"version":3,"file":"parts.d.ts","sourceRoot":"","sources":["../src/runtime/parts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAGhD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAA+B,KAAK,SAAS,EAAE,MAAM,SAAS,CAAA;AAErE,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS,CAAA;IACpH,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAAA;IAClH,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IACxD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B,CAAA;AAMD,wBAAgB,UAAU,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,KAAK,IAAI,CAAA;CAAE,GAAG,UAAU,CAwD9N"}
package/runtime/parts.js CHANGED
@@ -14,9 +14,9 @@ export function drawnParts({ claude, report, press }) {
14
14
  const generation = generationOf(mod);
15
15
  if (drawn.get(key)?.generation === generation)
16
16
  return undefined;
17
- const keep = (data) => {
17
+ const keep = (data, failure) => {
18
18
  drawn.delete(key);
19
- drawn.set(key, { data, generation });
19
+ drawn.set(key, { data, ...(failure === undefined ? {} : { failure }), generation });
20
20
  const [oldest] = drawn.keys();
21
21
  if (drawn.size > keptDrawings && oldest !== undefined)
22
22
  drawn.delete(oldest);
@@ -28,7 +28,7 @@ export function drawnParts({ claude, report, press }) {
28
28
  .call({ to: mod, method: partMethods.draw, input })
29
29
  .then((data) => keep((data ?? null)), (error) => {
30
30
  claude.ui.log(`cmod: ${name} drew nothing: ${messageOf(error)}`, { to: 'debug' });
31
- keep(null);
31
+ keep(drawn.get(key)?.data ?? null, messageOf(error));
32
32
  })
33
33
  .finally(() => drawing.delete(key));
34
34
  drawing.set(key, pending);
@@ -52,6 +52,9 @@ export function drawnParts({ claude, report, press }) {
52
52
  return undefined;
53
53
  }
54
54
  },
55
+ failure(mod, cached) {
56
+ return drawn.get(keyOf(mod, cached))?.failure;
57
+ },
55
58
  forget(mod) {
56
59
  generations.set(mod, generationOf(mod) + 1);
57
60
  },
@@ -10,6 +10,7 @@ export declare function readLines(stream: HookStream<ProcessSpawnChunk, ProcessS
10
10
  code: number | null;
11
11
  lastError: string;
12
12
  }>;
13
+ export declare function spawnCmod(claude: Claude, args: readonly string[], session: string, env?: Readonly<Record<string, string>>): HookStream<ProcessSpawnChunk, ProcessSpawnResult>;
13
14
  export declare function runCmod(claude: Claude, args: readonly string[], session: string): Promise<string>;
14
15
  export declare function locatingPrograms(claude: Claude): Programs;
15
16
  //# sourceMappingURL=programs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"programs.d.ts","sourceRoot":"","sources":["../src/runtime/programs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAIpF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,MAAM,MAAM,QAAQ,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAA;AAE5E,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG3D;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAI7E;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,UAAU,CAAC,iBAAiB,EAAE,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAgB9K;AAED,wBAAsB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAKvG;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAsBzD"}
1
+ {"version":3,"file":"programs.d.ts","sourceRoot":"","sources":["../src/runtime/programs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAIpF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,MAAM,MAAM,QAAQ,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAA;AAE5E,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG3D;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAI7E;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,UAAU,CAAC,iBAAiB,EAAE,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAgB9K;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM,GAAG,UAAU,CAAC,iBAAiB,EAAE,kBAAkB,CAAC,CAEjL;AAED,wBAAsB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAKvG;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAsBzD"}
@@ -30,9 +30,12 @@ export async function readLines(stream, onLine) {
30
30
  onLine(pending);
31
31
  return { code: piece.value.code, lastError };
32
32
  }
33
+ export function spawnCmod(claude, args, session, env = {}) {
34
+ return claude.process.spawn({ argv: ['cmod', ...args], env: { ...env, CLAUDE_CODE_SESSION_ID: session } });
35
+ }
33
36
  export async function runCmod(claude, args, session) {
34
37
  const lines = [];
35
- const { code, lastError } = await readLines(claude.process.spawn({ argv: ['cmod', ...args], env: { CLAUDE_CODE_SESSION_ID: session } }), (line) => void lines.push(line));
38
+ const { code, lastError } = await readLines(spawnCmod(claude, args, session), (line) => void lines.push(line));
36
39
  if (code !== 0)
37
40
  throw new Error(cmodError(lastError, args[0]) || `cmod ${args.slice(0, 2).join(' ')} ${formatExit(code, '')}.`);
38
41
  return lines.join('\n');
@@ -30,8 +30,14 @@ export declare const conversationMethods: {
30
30
  readonly act: 'cmod:act';
31
31
  readonly changed: 'cmod:changed';
32
32
  readonly before: 'cmod:before';
33
+ readonly sessionColor: 'cmod:sessionColor';
33
34
  };
34
- export type Act = 'send' | 'startTask' | 'callTool' | 'usage' | 'surfaces' | 'abort';
35
+ export type SessionColor = {
36
+ readonly session: string;
37
+ readonly color?: string;
38
+ };
39
+ export declare function pickedColor(args: string): string | undefined;
40
+ export type Act = 'send' | 'startTask' | 'fork' | 'clone' | 'startBackgroundTask' | 'callTool' | 'usage' | 'surfaces' | 'abort';
35
41
  export declare const beforePermission: (event: BeforeEvent) => string;
36
42
  export declare const isAct: (action: string) => action is Act;
37
43
  export declare function toolPermission(tool: string): string | undefined;
@@ -55,7 +61,8 @@ export type SessionsClient = {
55
61
  listen(method: string, args: readonly unknown[]): Subscription;
56
62
  turn(data: TurnData): Turn;
57
63
  /** An empty title keeps the title the session already has. */
58
- follow(current: Omit<SessionData, 'isRunning' | 'lastAt'>): void;
64
+ follow(current: Omit<SessionData, 'isRunning' | 'lastAt' | 'color'>): void;
65
+ recolor(told: SessionColor): void;
59
66
  };
60
67
  export declare function usageOf(claude: Pick<Claude, 'session'>): Promise<Usage>;
61
68
  export declare function payloadOf(change: SessionChange): unknown;
@@ -90,7 +97,7 @@ export type RouteOn = {
90
97
  export declare const recordOf: (value: unknown) => Readonly<Record<string, unknown>>;
91
98
  export declare const defined: (fields: HookFields) => HookFields;
92
99
  type ProcessFieldsName = Exclude<ProcessName, 'PermissionCheck'>;
93
- export declare function spawnTask(claude: Pick<Claude, 'agent'>, task: TaskStartArgs): Promise<TaskData>;
100
+ export declare function spawnTask(claude: Pick<Claude, 'agent'>, task: TaskStartArgs, forkedFrom?: string): Promise<TaskData>;
94
101
  export declare function processPayloadOf(named: ProcessFieldsName, input: unknown): HookFields;
95
102
  export declare function resultOf(where: Pick<Place, 'session' | 'context' | 'turn'>, call: ToolCallArgs & {
96
103
  readonly cwd: string;