@cmodjs/core 4.0.3 → 4.1.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 (141) hide show
  1. package/docs/commands.md +43 -8
  2. package/docs/index.md +3 -3
  3. package/docs/install-steps.md +6 -6
  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 +44 -13
  9. package/docs/testing.md +14 -10
  10. package/docs/ui.md +42 -10
  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 +5 -2
  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.map +1 -1
  32. package/runtime/conversations.js +134 -25
  33. package/runtime/engine.d.ts +2 -0
  34. package/runtime/engine.d.ts.map +1 -1
  35. package/runtime/grants.d.ts +1 -0
  36. package/runtime/grants.d.ts.map +1 -1
  37. package/runtime/grants.js +5 -2
  38. package/runtime/hotkeys.d.ts.map +1 -1
  39. package/runtime/hotkeys.js +7 -4
  40. package/runtime/installer.d.ts +9 -6
  41. package/runtime/installer.d.ts.map +1 -1
  42. package/runtime/installer.js +104 -47
  43. package/runtime/lifecycle.d.ts.map +1 -1
  44. package/runtime/lifecycle.js +121 -40
  45. package/runtime/pages.d.ts +3 -1
  46. package/runtime/pages.d.ts.map +1 -1
  47. package/runtime/pages.js +34 -10
  48. package/runtime/parts.d.ts +1 -0
  49. package/runtime/parts.d.ts.map +1 -1
  50. package/runtime/parts.js +6 -3
  51. package/runtime/programs.d.ts +1 -0
  52. package/runtime/programs.d.ts.map +1 -1
  53. package/runtime/programs.js +4 -1
  54. package/runtime/sessions.d.ts +2 -2
  55. package/runtime/sessions.d.ts.map +1 -1
  56. package/runtime/sessions.js +88 -58
  57. package/runtime/toaster.d.ts.map +1 -1
  58. package/runtime/toaster.js +23 -9
  59. package/runtime/ui.d.ts +21 -4
  60. package/runtime/ui.d.ts.map +1 -1
  61. package/runtime/ui.js +158 -58
  62. package/runtime/watches.d.ts.map +1 -1
  63. package/runtime/watches.js +18 -4
  64. package/runtime/widgets.d.ts +7 -5
  65. package/runtime/widgets.d.ts.map +1 -1
  66. package/runtime/widgets.js +29 -20
  67. package/session/context/turn/message/attached-file.d.ts +13 -0
  68. package/session/context/turn/message/attached-file.d.ts.map +1 -0
  69. package/session/context/turn/message/attached-file.js +21 -0
  70. package/session/context/turn/message/message.d.ts +3 -2
  71. package/session/context/turn/message/message.d.ts.map +1 -1
  72. package/session/context/turn/message/message.js +20 -6
  73. package/session/conversation.d.ts +14 -3
  74. package/session/conversation.d.ts.map +1 -1
  75. package/session/conversation.js +23 -4
  76. package/session/goal/task.d.ts +5 -0
  77. package/session/goal/task.d.ts.map +1 -1
  78. package/session/session.d.ts +35 -7
  79. package/session/session.d.ts.map +1 -1
  80. package/session/session.js +2 -1
  81. package/src/installer.ts +3 -2
  82. package/src/jobs/program.ts +41 -21
  83. package/src/mod.ts +3 -2
  84. package/src/records.ts +49 -1
  85. package/src/register.ts +9 -6
  86. package/src/runtime/before.ts +61 -22
  87. package/src/runtime/buttons.ts +22 -0
  88. package/src/runtime/conversations.ts +138 -32
  89. package/src/runtime/engine.ts +2 -0
  90. package/src/runtime/grants.ts +6 -2
  91. package/src/runtime/hotkeys.ts +7 -4
  92. package/src/runtime/installer.ts +144 -82
  93. package/src/runtime/lifecycle.ts +128 -43
  94. package/src/runtime/pages.ts +45 -8
  95. package/src/runtime/parts.ts +8 -4
  96. package/src/runtime/programs.ts +5 -1
  97. package/src/runtime/sessions.ts +83 -57
  98. package/src/runtime/toaster.ts +27 -9
  99. package/src/runtime/ui.ts +163 -61
  100. package/src/runtime/watches.ts +20 -5
  101. package/src/runtime/widgets.ts +34 -22
  102. package/src/session/context/turn/message/attached-file.ts +36 -0
  103. package/src/session/context/turn/message/message.ts +20 -7
  104. package/src/session/conversation.ts +30 -6
  105. package/src/session/goal/task.ts +6 -0
  106. package/src/session/session.ts +17 -7
  107. package/src/testing/fake-claude.ts +21 -6
  108. package/src/testing/fake-elements.ts +51 -8
  109. package/src/testing/fake-sessions.ts +60 -23
  110. package/src/testing.ts +86 -28
  111. package/src/ui/areas.ts +1 -0
  112. package/src/ui/components.ts +14 -8
  113. package/src/ui/elements.ts +6 -2
  114. package/src/ui/markdown.ts +3 -3
  115. package/src/ui/panes.ts +2 -2
  116. package/src/ui/styles.ts +6 -2
  117. package/testing/fake-claude.d.ts +3 -0
  118. package/testing/fake-claude.d.ts.map +1 -1
  119. package/testing/fake-claude.js +18 -5
  120. package/testing/fake-elements.d.ts.map +1 -1
  121. package/testing/fake-elements.js +46 -7
  122. package/testing/fake-sessions.d.ts +21 -10
  123. package/testing/fake-sessions.d.ts.map +1 -1
  124. package/testing/fake-sessions.js +58 -20
  125. package/testing.d.ts +8 -12
  126. package/testing.d.ts.map +1 -1
  127. package/testing.js +82 -21
  128. package/ui/areas.d.ts +4 -0
  129. package/ui/areas.d.ts.map +1 -1
  130. package/ui/components.d.ts +1 -0
  131. package/ui/components.d.ts.map +1 -1
  132. package/ui/components.js +11 -7
  133. package/ui/elements.d.ts +3 -0
  134. package/ui/elements.d.ts.map +1 -1
  135. package/ui/elements.js +6 -1
  136. package/ui/markdown.d.ts +2 -2
  137. package/ui/markdown.d.ts.map +1 -1
  138. package/ui/panes.d.ts +2 -1
  139. package/ui/panes.d.ts.map +1 -1
  140. package/ui/styles.d.ts +11 -2
  141. package/ui/styles.d.ts.map +1 -1
@@ -1,9 +1,9 @@
1
- import type { Args, Frozen, RenderElement } from 'claude-code';
2
1
  import type { Step } from '../installer.js';
3
2
  import type { Mod } from '../mod.js';
4
3
  import type { Option } from '../options.js';
5
4
  import type { Claude } from './claude.js';
6
- export declare const installerPaneOf: (name: string) => string;
5
+ import { type PartsUpdate, type ShownPages } from './ui.js';
6
+ export declare const installPage = "cmod:install";
7
7
  export type Consent = {
8
8
  readonly install: string;
9
9
  readonly uninstall: string;
@@ -12,16 +12,19 @@ export type Consent = {
12
12
  readonly updates?: string | undefined;
13
13
  };
14
14
  export type Installer = {
15
- draws(e: Frozen<Args<'ui.render'>>): boolean;
16
- draw(e: Frozen<Args<'ui.render'>>): RenderElement;
17
- closed(id: string): void;
15
+ answer(method: string, input: unknown): Promise<{
16
+ readonly value: unknown;
17
+ }> | undefined;
18
+ hear(shown: ShownPages): void;
18
19
  consent(consent: Consent): Promise<boolean>;
19
20
  options(missing: readonly (readonly [string, Option])[], save: (key: string, value: string) => Promise<string | undefined>): Promise<boolean>;
20
21
  step<State extends object>(mod: Mod<State>, step: Step<State>, index: number, total: number): Promise<boolean>;
21
22
  close(): Promise<void>;
23
+ shown(): Promise<void>;
24
+ collect(run: () => Promise<unknown>): Promise<PartsUpdate>;
22
25
  };
23
26
  export declare function createInstaller(name: string, claude: () => Claude, waiting: {
24
27
  show(open: () => Promise<void>): void;
25
28
  hide(): void;
26
- }): Installer;
29
+ }, nextDraw: () => number): Installer;
27
30
  //# sourceMappingURL=installer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/runtime/installer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,EAAiB,MAAM,aAAa,CAAA;AAC7E,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAC3C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AACpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAI3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,eAAO,MAAM,eAAe,SAAU,MAAM,KAAG,MAA0E,CAAA;AAEzH,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACtC,CAAA;AAOD,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,OAAO,CAAA;IAC5C,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,aAAa,CAAA;IACjD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC3C,OAAO,CAAC,OAAO,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC7I,IAAI,CAAC,KAAK,SAAS,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC9G,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB,CAAA;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,MAAM,EAAE,OAAO,EAAE;IAAE,IAAI,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAAC,IAAI,IAAI,IAAI,CAAA;CAAE,GAAG,SAAS,CAoJ/I"}
1
+ {"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/runtime/installer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAC3C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AACpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAM3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,OAAO,EAAuD,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,SAAS,CAAA;AAEhH,eAAO,MAAM,WAAW,iBAAiB,CAAA;AAEzC,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACtC,CAAA;AAOD,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,GAAG,SAAS,CAAA;IACxF,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAA;IAC7B,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC3C,OAAO,CAAC,OAAO,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC7I,IAAI,CAAC,KAAK,SAAS,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC9G,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,OAAO,CAAC,GAAG,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;CAC3D,CAAA;AAID,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,MAAM,EAAE,OAAO,EAAE;IAAE,IAAI,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAAC,IAAI,IAAI,IAAI,CAAA;CAAE,EAAE,QAAQ,EAAE,MAAM,MAAM,GAAG,SAAS,CA2MvK"}
@@ -1,48 +1,108 @@
1
- import { permissionWords, secretFix } from '../records.js';
2
- import { Box, Button, drawWith, Input, Text } from '../ui/elements.js';
1
+ import { isObject, permissionWords, secretFix } from '../records.js';
2
+ import { Box, elementData, Input, Text } from '../ui/elements.js';
3
+ import { filledButton } from './buttons.js';
3
4
  import { messageOf } from '../utils/text.js';
4
- export const installerPaneOf = (name) => `installer-${name.replace(/[^A-Za-z0-9_-]/g, '_')}`.slice(0, 64);
5
- export function createInstaller(name, claude, waiting) {
6
- const paneId = installerPaneOf(name);
7
- let page;
5
+ import { cmodPluginName, withBareRefusals } from './dependencies.js';
6
+ import { merged, partMethods } from './ui.js';
7
+ export const installPage = 'cmod:install';
8
+ const keptDraws = 8;
9
+ export function createInstaller(name, claude, waiting, nextDraw) {
10
+ let question;
8
11
  let isOpen = false;
9
- const open = async () => {
10
- isOpen = true;
11
- await claude().ui.open({ id: paneId, title: `Install ${name}`, focus: true, holdToasts: true, closeOnEscape: true });
12
+ let isSeen = false;
13
+ let waitingForShow = [];
14
+ let collected;
15
+ const draws = new Map();
16
+ const send = async (update) => {
17
+ if (collected !== undefined)
18
+ return void collected.push(update);
19
+ await withBareRefusals(claude()).cmod.call({ to: cmodPluginName, method: partMethods.changed, input: update });
12
20
  };
21
+ const log = (what) => (error) => claude().ui.log(`${name}: ${what}: ${messageOf(error)}`, { to: 'debug' });
22
+ const open = (isAsked) => send({ requests: [{ open: installPage, focus: true }], ...(isAsked ? { isAsked: true } : {}) });
13
23
  const show = async (build, cancelled) => {
14
- page?.cancel();
24
+ question?.cancel();
15
25
  const answer = new Promise((resolve) => {
26
+ let isAnswered = false;
16
27
  const done = (value) => {
17
- if (page === shown)
18
- page = undefined;
28
+ if (isAnswered)
29
+ return;
30
+ isAnswered = true;
19
31
  waiting.hide();
20
- resolve(value);
32
+ claude().clock.after(0, () => resolve(value));
21
33
  };
22
- const shown = { ...build(done), cancel: () => done(cancelled) };
23
- page = shown;
34
+ question = { ...build(done), cancel: () => done(cancelled) };
24
35
  });
25
- waiting.show(open);
26
- if (!isOpen)
27
- await open().catch((error) => {
36
+ waiting.show(() => open(true));
37
+ if (isOpen) {
38
+ await send({ redraw: true }).catch(log('the installer did not redraw'));
39
+ }
40
+ else {
41
+ isOpen = true;
42
+ await open(false).catch((error) => {
43
+ isOpen = false;
28
44
  waiting.hide();
29
45
  throw error;
30
46
  });
31
- claude().ui.invalidate('ui.render');
47
+ }
48
+ for (const resolve of waitingForShow)
49
+ resolve();
50
+ waitingForShow = [];
32
51
  return answer;
33
52
  };
34
- const frame = (title, body, actions) => Box({ flexDirection: 'column', gap: 1, paddingX: 1, children: [Text({ bold: true, children: title }), ...body, Box({ gap: 3, children: actions })] });
35
- const bullet = (text) => Text({ children: [Text({ color: 'claude', children: '◆ ' }), text] });
53
+ const redraw = () => void send({ redraw: true }).catch(log('the installer did not redraw'));
54
+ const actions = (...buttons) => Box({ gap: 2, flexShrink: 0, children: buttons });
55
+ const dialog = (title, ...children) => Box({ flexDirection: 'column', gap: 1, children: [title, ...children] });
56
+ const bullet = (text) => Box({ gap: 1, children: [Text({ dimColor: true, children: '•' }), Text({ wrap: 'wrap', children: text })] });
57
+ const group = (heading, items) => Box({ flexDirection: 'column', children: [Text({ bold: true, children: heading }), ...items.map(bullet)] });
58
+ const draw = (columns, isFocused) => {
59
+ if (question === undefined && !isOpen)
60
+ claude().clock.after(0, () => void send({ requests: [{ close: installPage }] }).catch(log('a leftover install page stayed open')));
61
+ const handlers = [];
62
+ const tree = elementData(question?.draw({ params: undefined, isFocused, columns }) ?? Text({ dimColor: true, children: `${name} needs nothing from you now.` }), handlers);
63
+ const drawn = nextDraw();
64
+ draws.set(drawn, handlers);
65
+ for (const kept of draws.keys())
66
+ if (draws.size > keptDraws)
67
+ draws.delete(kept);
68
+ return { draw: drawn, tree };
69
+ };
70
+ const collect = async (run) => {
71
+ const outer = collected;
72
+ const mine = [];
73
+ collected = mine;
74
+ try {
75
+ await run();
76
+ }
77
+ finally {
78
+ collected = outer;
79
+ }
80
+ return merged(mine);
81
+ };
82
+ const press = async ({ draw: drawn, handler, args }) => {
83
+ const run = draws.get(drawn)?.[handler];
84
+ if (run === undefined)
85
+ throw new Error(`${name}: that button drew before the last redraw. Press the one it shows now.`);
86
+ return collect(() => run(...args));
87
+ };
36
88
  return {
37
- draws: (e) => e.component === 'Pane' && e.requestId === paneId,
38
- draw(e) {
39
- return drawWith(claude().ui.resolve(e), () => page?.draw(e.props) ?? Text({ dimColor: true, children: `${name} is installing.` }));
89
+ answer(method, input) {
90
+ if (method === partMethods.draw && isObject(input) && input['page'] === installPage)
91
+ return Promise.resolve({ value: draw(Number(input['columns']), input['isFocused'] === true) });
92
+ if (method === partMethods.press && isObject(input) && typeof input['draw'] === 'number' && draws.has(input['draw']))
93
+ return press(input).then((value) => ({ value }));
94
+ return undefined;
40
95
  },
41
- closed(id) {
42
- if (id !== paneId)
96
+ hear(shown) {
97
+ if (shown.open.some((each) => each.id === installPage)) {
98
+ isSeen = isOpen;
99
+ return;
100
+ }
101
+ if (!isSeen)
43
102
  return;
44
103
  isOpen = false;
45
- page?.cancel();
104
+ isSeen = false;
105
+ question?.cancel();
46
106
  },
47
107
  consent({ install, uninstall, hotkeys, permissions, updates }) {
48
108
  const changes = [
@@ -52,24 +112,17 @@ export function createInstaller(name, claude, waiting) {
52
112
  ...(updates === undefined ? [] : [`Update ${name} automatically from the ${updates} marketplace`]),
53
113
  ];
54
114
  return show((answer) => ({
55
- draw: () => frame(`${name} wants to:`, [
56
- ...(permissions.length === 0 ? [] : [Box({ flexDirection: 'column', children: permissions.map((item) => bullet(permissionWords(item))) })]),
57
- ...(changes.length === 0 ? [] : [Text({ children: permissions.length === 0 ? 'Change your computer:' : 'and change your computer:' }), Box({ flexDirection: 'column', children: changes.map(bullet) })]),
58
- Text({ dimColor: true, children: 'You can turn each permission off later in Settings.' }),
59
- ], [Button({ key: 'not-now', hotkey: 'n', label: 'Not now', onPress: () => answer(false) }), Button({ key: 'accept', hotkey: 'a', label: 'Accept', onPress: () => answer(true) })]),
115
+ draw: () => dialog(Text({ bold: true, children: `Install ${name}?` }), ...(permissions.length === 0 ? [] : [group(`${name} wants to:`, permissions.map(permissionWords))]), ...(changes.length === 0 ? [] : [group(permissions.length === 0 ? `${name} wants to change your computer:` : 'and change your computer:', changes)]), Text({ dimColor: true, wrap: 'wrap', children: 'You can turn each permission off later in Settings.' }), actions(filledButton({ key: 'accept', hotkey: 'a', variant: 'primary', label: 'Accept', onPress: () => answer(true) }), filledButton({ key: 'not-now', hotkey: 'n', label: 'Not now', onPress: () => answer(false) }))),
60
116
  }), false);
61
117
  },
62
118
  options(missing, save) {
63
119
  const saved = new Map();
64
120
  const failures = new Map();
65
121
  return show((answer) => ({
66
- draw: () => frame(`${name} needs ${missing.length === 1 ? 'one setting' : `${missing.length} settings`}:`, missing.map(([key, option]) => {
67
- const label = Text({ children: [Text({ bold: true, children: option.title }), option.description === '' ? '' : Text({ dimColor: true, children: ` ${option.description}` })] });
122
+ draw: () => dialog(Text({ bold: true, children: `Set up ${name}` }), Text({ children: `${name} needs ${missing.length === 1 ? 'one setting' : `${missing.length} settings`}:` }), ...missing.map(([key, option]) => {
123
+ const label = Box({ flexDirection: 'column', children: [Text({ bold: true, children: option.title }), ...(option.description === '' ? [] : [Text({ dimColor: true, wrap: 'wrap', children: option.description })])] });
68
124
  if (option.type === 'secret') {
69
- return Box({
70
- flexDirection: 'column',
71
- children: [label, Text({ dimColor: true, children: `${secretFix} Then press Next.` }), Button({ key: `secret:${key}`, label: 'Open /config', onPress: () => void claude().command.run({ command: 'config' }) })],
72
- });
125
+ return Box({ flexDirection: 'column', children: [label, Text({ dimColor: true, wrap: 'wrap', children: `${secretFix} Then press Next.` }), filledButton({ key: `secret:${key}`, label: 'Open /config', onPress: () => void claude().command.run({ command: 'config' }) })] });
73
126
  }
74
127
  const failure = failures.get(key);
75
128
  const field = Input({
@@ -88,11 +141,11 @@ export function createInstaller(name, claude, waiting) {
88
141
  failures.set(key, refused);
89
142
  })
90
143
  .catch((error) => failures.set(key, messageOf(error)))
91
- .finally(() => claude().ui.invalidate('ui.render')),
144
+ .finally(redraw),
92
145
  });
93
- const status = failure !== undefined ? Text({ color: 'error', children: failure }) : saved.has(key) ? Text({ color: 'success', children: '✔ saved' }) : Text({ children: '' });
94
- return Box({ flexDirection: 'column', children: [label, field, status] });
95
- }), [Button({ key: 'not-now', hotkey: 'n', label: 'Not now', onPress: () => answer(false) }), Button({ key: 'next', hotkey: 'x', label: 'Next', onPress: () => answer(true) })]),
146
+ const status = failure !== undefined ? [Text({ color: 'error', wrap: 'wrap', children: failure })] : saved.has(key) ? [Text({ color: 'success', children: '✓ saved' })] : [];
147
+ return Box({ flexDirection: 'column', children: [label, field, ...status] });
148
+ }), actions(filledButton({ key: 'next', hotkey: 'x', variant: 'primary', label: 'Next', onPress: () => answer(true) }), filledButton({ key: 'not-now', hotkey: 'n', label: 'Not now', onPress: () => answer(false) }))),
96
149
  }), false);
97
150
  },
98
151
  step(mod, step, index, total) {
@@ -104,7 +157,7 @@ export function createInstaller(name, claude, waiting) {
104
157
  body = step.render(mod, props);
105
158
  }
106
159
  catch (error) {
107
- body = Text({ color: 'error', children: `The ${step.title} step could not draw: ${messageOf(error)}` });
160
+ body = Text({ color: 'error', wrap: 'wrap', children: `The ${step.title} step could not draw: ${messageOf(error)}` });
108
161
  }
109
162
  const next = async () => {
110
163
  try {
@@ -115,18 +168,22 @@ export function createInstaller(name, claude, waiting) {
115
168
  catch (error) {
116
169
  notice = { color: 'error', text: `The ${step.title} step's isDone failed: ${messageOf(error)}` };
117
170
  }
118
- claude().ui.invalidate('ui.render');
171
+ await send({ redraw: true });
119
172
  };
120
- return frame(`${step.title} (${index} of ${total})`, [body, ...(notice === undefined ? [] : [Text({ color: notice.color, children: notice.text })])], [Button({ key: 'not-now', hotkey: 'n', label: 'Not now', onPress: () => answer(false) }), Button({ key: 'next', hotkey: 'x', label: index === total ? 'Finish' : 'Next', onPress: () => void next() })]);
173
+ return dialog(Text({ children: [Text({ bold: true, children: step.title }), Text({ dimColor: true, children: ` (${index} of ${total})` })] }), body, ...(notice === undefined ? [] : [Text({ color: notice.color, wrap: 'wrap', children: notice.text })]), actions(filledButton({ key: 'next', hotkey: 'x', variant: 'primary', label: index === total ? 'Finish' : 'Next', onPress: () => void next() }), filledButton({ key: 'not-now', hotkey: 'n', label: 'Not now', onPress: () => answer(false) })));
121
174
  },
122
175
  }), false);
123
176
  },
124
177
  async close() {
125
- page = undefined;
178
+ question?.cancel();
179
+ question = undefined;
126
180
  if (!isOpen)
127
181
  return;
128
182
  isOpen = false;
129
- await claude().ui.close({ id: paneId });
183
+ isSeen = false;
184
+ await send({ requests: [{ close: installPage }] }).catch(log('the install page stayed open'));
130
185
  },
186
+ shown: () => new Promise((resolve) => void waitingForShow.push(resolve)),
187
+ collect,
131
188
  };
132
189
  }
@@ -1 +1 @@
1
- {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/runtime/lifecycle.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/runtime/lifecycle.ts"],"names":[],"mappings":"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"}
@@ -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,6 +300,7 @@ 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');
293
305
  void runSteps(active.mod).catch((error) => toastFailure({ id: 'cmod:steps', message: 'Its installer stopped', error }));
294
306
  const missed = missedSessionStart;
@@ -343,9 +355,15 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
343
355
  toastFailure({ id: 'cmod:updates', message: 'Could not read whether its marketplace updates automatically', error });
344
356
  return undefined;
345
357
  });
346
- const isAccepted = await installer.consent({ ...event, updates });
358
+ const installedElsewhere = new Promise((resolve) => (settleAsked = () => resolve('installed')));
359
+ watchRecord();
360
+ const answer = await Promise.race([installer.consent({ ...event, updates }), installedElsewhere]).finally(() => {
361
+ settleAsked = undefined;
362
+ });
347
363
  await installer.close();
348
- if (isAccepted) {
364
+ if (answer === 'installed')
365
+ return startInstalled();
366
+ if (answer) {
349
367
  consented = event.sha256;
350
368
  return install(consented);
351
369
  }
@@ -480,8 +498,8 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
480
498
  };
481
499
  let recordMs = 0;
482
500
  let readStarted;
483
- const recover = async () => {
484
- if (plugin === undefined || readStarted === undefined || (phase !== 'failed' && phase !== 'declined'))
501
+ const isNewlyInstalled = async () => {
502
+ if (plugin === undefined || readStarted === undefined)
485
503
  return false;
486
504
  const modifiedMs = await stampOf(recordPath(plugin.store, plugin.name));
487
505
  if (modifiedMs === recordMs)
@@ -491,12 +509,40 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
491
509
  if (!current.isInstalled)
492
510
  return false;
493
511
  plugin = current;
512
+ return true;
513
+ };
514
+ const startInstalled = async () => {
494
515
  failure = undefined;
495
516
  activation = undefined;
496
517
  await refreshGrants();
497
518
  await finish();
519
+ };
520
+ let settleAsked;
521
+ const recover = async () => {
522
+ if ((settleAsked === undefined && phase !== 'failed' && phase !== 'declined') || !(await isNewlyInstalled()))
523
+ return false;
524
+ if (settleAsked === undefined)
525
+ await startInstalled();
526
+ else
527
+ settleAsked();
498
528
  return true;
499
529
  };
530
+ let recordCheck;
531
+ let checking;
532
+ const watchRecord = () => {
533
+ recordCheck ??= claude().clock.every(recordCheckMs, () => {
534
+ checking ??= recover()
535
+ .then(() => undefined)
536
+ .catch((error) => claude().ui.log(`${definition.name} could not read whether another cmod installed it: ${messageOf(error)}`, { to: 'debug' }))
537
+ .finally(() => {
538
+ checking = undefined;
539
+ });
540
+ });
541
+ };
542
+ const stopWatchingRecord = () => {
543
+ recordCheck?.cancel();
544
+ recordCheck = undefined;
545
+ };
500
546
  const refreshGrants = async () => {
501
547
  if (plugin === undefined || plugin.name === cmodPluginName)
502
548
  return;
@@ -579,6 +625,7 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
579
625
  runtime = { claude: programs.claude, toaster: createToaster(claudeCalls, definition.name, () => plugin?.version, () => openSettings('permissions')) };
580
626
  readStarted = read;
581
627
  try {
628
+ drawCount = (await claudeCalls.clock.now()) * drawsPerMs;
582
629
  await programs.refresh();
583
630
  await begin().catch(report);
584
631
  }
@@ -589,10 +636,16 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
589
636
  async route(event, e, next) {
590
637
  if (imports !== undefined)
591
638
  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);
639
+ const asked = e;
640
+ if (event === 'cmod.call' && asked.to === definition.name && asked.caller === cmodPluginName) {
641
+ if (asked.method === partMethods.shown)
642
+ installer.hear(asked.input);
643
+ const answered = installer.answer(asked.method, asked.input);
644
+ if (answered !== undefined)
645
+ return (await answered.catch((error) => ({ deny: `${definition.name}: ${messageOf(error)}` })));
646
+ if (asked.method === partMethods.shown && phase !== 'active')
647
+ return { value: null };
648
+ }
596
649
  if (event === 'classic.SessionStart') {
597
650
  missedSessionStart = undefined;
598
651
  hasHeardSessionStart = true;
@@ -605,10 +658,11 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
605
658
  record();
606
659
  if (event === 'classic.UserPromptSubmit' || event === 'classic.SessionStart')
607
660
  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;
661
+ const tool = e.tool;
662
+ const isCallToThisMod = (event === 'cmod.call' && e.to === definition.name) || (event === 'tool.call' && typeof tool === 'string' && tool.startsWith(`mcp__${definition.name}__`));
609
663
  if (event === 'cmod.call' && !isCallToThisMod)
610
664
  return next(e);
611
- await (event === 'classic.SessionStart' || isCallToThisMod ? holdUntilStarted() : whenActive());
665
+ await (event === 'classic.SessionStart' || event === 'session.append' || isCallToThisMod ? holdUntilStarted() : whenActive());
612
666
  if (event === 'classic.SessionStart' && phase !== 'active') {
613
667
  missedSessionStart = e;
614
668
  return next(e);
@@ -621,8 +675,11 @@ export function createLifecycle(definition, permissionRoutes = () => undefined,
621
675
  if (event === 'cmod.call' && call.to === definition.name && mod !== undefined) {
622
676
  if (call.method === pendingStepsMethod)
623
677
  return { value: await pendingSteps(mod) };
624
- if (call.method === finishStepsMethod)
625
- return (await runSteps(mod), { value: null });
678
+ if (call.method === finishStepsMethod) {
679
+ const active = mod;
680
+ 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()]));
681
+ return { value: opened };
682
+ }
626
683
  }
627
684
  if (event === 'classic.UserPromptSubmit' || event === 'classic.SessionStart')
628
685
  await refreshGrants().catch((error) => claude().ui.log(`${definition.name} keeps its grants from before: ${messageOf(error)}`, { to: 'debug' }));
@@ -689,7 +746,13 @@ async function createMod(definition, runtime) {
689
746
  let parentAsked;
690
747
  let hasEarlierPrompts = true;
691
748
  let agentType;
692
- const area = createUi({ name: definition.name, claude, router, report, announce, mod: () => mod });
749
+ let parts;
750
+ const borrowed = (styler, cached, input) => {
751
+ const name = `The markdown style of ${styler}`;
752
+ void parts?.fill(styler, cached, input, name)?.then(() => claude.ui.invalidate('ui.render'));
753
+ return parts?.element(styler, cached, `${styler}/markdown:`, name, `markdown:${styler}`);
754
+ };
755
+ const area = createUi({ name: definition.name, claude, router, report, announce, mod: () => mod, borrowed, nextDraw: runtime.nextDraw });
693
756
  const askParent = async () => {
694
757
  if (definition.name === cmodPluginName)
695
758
  return undefined;
@@ -968,9 +1031,14 @@ async function createMod(definition, runtime) {
968
1031
  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
1032
  let pages;
970
1033
  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 });
1034
+ const pressElsewhere = (to, input) => void claude.cmod.call({ to, method: partMethods.press, input }).catch((error) => {
1035
+ parts?.forget(to);
1036
+ claude.ui.invalidate('ui.render');
1037
+ reportPart(`press:${to}`, `${to} did not take the press`, error);
1038
+ });
1039
+ parts = drawnParts({ claude, report: reportPart, press: (to, input) => (pages === undefined ? pressElsewhere(to, input) : pages.press(to, input)) });
1040
+ const widgets = conversations === undefined ? undefined : serveWidgets({ claude, router, store: runtime.store, report: reportPart, turnClosedBy: (rowId) => conversations.turnClosedBy(rowId), parts, markdownIn: area.markdownIn });
1041
+ pages = widgets === undefined ? undefined : servePages({ claude, router, report: reportPart, widgets, parts, tellStylers: area.restyled });
974
1042
  router.add('cmod.call', async (e, next) => {
975
1043
  if (e.to !== definition.name)
976
1044
  return next(e);
@@ -986,13 +1054,13 @@ async function createMod(definition, runtime) {
986
1054
  if (ask !== undefined)
987
1055
  return { value: (await client.answerBefore(ask.session, ask.event, ask.change, ask.isInProject)) ?? null };
988
1056
  if (e.method === partMethods.changed && call.caller !== undefined && pages !== undefined) {
989
- return { value: { isHeld: pages.update(call.caller, e.input) } };
1057
+ return { value: { isHeld: pages.update(call.caller, e.input), stylers: pages.stylers() } };
990
1058
  }
991
- if (call.caller === cmodPluginName) {
1059
+ if (call.caller !== undefined) {
992
1060
  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) });
1061
+ const answered = area.answer(e.method, turn === undefined ? e.input : { ...e.input, turn: client.turn(turn) }, call.caller);
994
1062
  if (answered !== undefined)
995
- return answered;
1063
+ return answered.catch((error) => ({ deny: `${definition.name}: ${messageOf(error)}` }));
996
1064
  }
997
1065
  const served = await conversations?.answer(call);
998
1066
  return served ?? answerCall(definition.name, api, e);
@@ -1003,7 +1071,20 @@ async function createMod(definition, runtime) {
1003
1071
  return ask === undefined ? { deny: `${definition.name}: ${error.message}` } : { value: client.failedBefore(ask.session, ask.event, ask.change, ask.isInProject, error) ?? null };
1004
1072
  });
1005
1073
  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 }) });
1074
+ keepEngine(mod, {
1075
+ claude: checked,
1076
+ router,
1077
+ toolCalls: agents,
1078
+ report,
1079
+ hasPermissionCheck: () => runtime.permissionRoutes() !== undefined,
1080
+ answerInput: area.answerInput,
1081
+ spawnCmod: async (args, program) => {
1082
+ await checkRun(grants, 'process.spawn', program.argv);
1083
+ return spawnCmod(claude, args, session, program.env);
1084
+ },
1085
+ ...(widgets === undefined ? {} : { widgets }),
1086
+ ...(pages === undefined ? {} : { pages }),
1087
+ });
1007
1088
  const names = new Set();
1008
1089
  const reserveName = (type, name, taken) => {
1009
1090
  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,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,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,CAqHR"}