@cmodjs/core 4.0.2 → 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 +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 +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,13 +1,17 @@
1
- import type { Args, Frozen, RenderElement, RenderPropsOf } from 'claude-code'
1
+ import type { RenderElement } from 'claude-code'
2
2
  import type { Step } from '../installer.js'
3
3
  import type { Mod } from '../mod.js'
4
4
  import type { Option } from '../options.js'
5
- import { permissionWords, secretFix } from '../records.js'
6
- import { Box, Button, drawWith, Input, Text } from '../ui/elements.js'
5
+ import { isObject, permissionWords, secretFix } from '../records.js'
6
+ import { Box, elementData, Input, Text, type Handler } from '../ui/elements.js'
7
+ import { filledButton } from './buttons.js'
8
+ import type { PageProps } from '../ui/panes.js'
7
9
  import { messageOf } from '../utils/text.js'
8
10
  import type { Claude } from './claude.js'
11
+ import { cmodPluginName, withBareRefusals } from './dependencies.js'
12
+ import { merged, partMethods, type DrawnPart, type PartPress, type PartsUpdate, type ShownPages } from './ui.js'
9
13
 
10
- export const installerPaneOf = (name: string): string => `installer-${name.replace(/[^A-Za-z0-9_-]/g, '_')}`.slice(0, 64)
14
+ export const installPage = 'cmod:install'
11
15
 
12
16
  export type Consent = {
13
17
  readonly install: string
@@ -17,65 +21,122 @@ export type Consent = {
17
21
  readonly updates?: string | undefined
18
22
  }
19
23
 
20
- type Page = {
21
- draw(props: Frozen<RenderPropsOf['Pane']>): RenderElement
24
+ type Question = {
25
+ draw(props: PageProps<undefined>): RenderElement
22
26
  cancel(): void
23
27
  }
24
28
 
25
29
  export type Installer = {
26
- draws(e: Frozen<Args<'ui.render'>>): boolean
27
- draw(e: Frozen<Args<'ui.render'>>): RenderElement
28
- closed(id: string): void
30
+ answer(method: string, input: unknown): Promise<{ readonly value: unknown }> | undefined
31
+ hear(shown: ShownPages): void
29
32
  consent(consent: Consent): Promise<boolean>
30
33
  options(missing: readonly (readonly [string, Option])[], save: (key: string, value: string) => Promise<string | undefined>): Promise<boolean>
31
34
  step<State extends object>(mod: Mod<State>, step: Step<State>, index: number, total: number): Promise<boolean>
32
35
  close(): Promise<void>
36
+ shown(): Promise<void>
37
+ collect(run: () => Promise<unknown>): Promise<PartsUpdate>
33
38
  }
34
39
 
35
- export function createInstaller(name: string, claude: () => Claude, waiting: { show(open: () => Promise<void>): void; hide(): void }): Installer {
36
- const paneId = installerPaneOf(name)
37
- let page: Page | undefined
40
+ const keptDraws = 8
41
+
42
+ export function createInstaller(name: string, claude: () => Claude, waiting: { show(open: () => Promise<void>): void; hide(): void }, nextDraw: () => number): Installer {
43
+ let question: Question | undefined
38
44
  let isOpen = false
45
+ let isSeen = false
46
+ let waitingForShow: (() => void)[] = []
47
+ let collected: PartsUpdate[] | undefined
48
+ const draws = new Map<number, readonly Handler[]>()
39
49
 
40
- const open = async () => {
41
- isOpen = true
42
- await claude().ui.open({ id: paneId, title: `Install ${name}`, focus: true, holdToasts: true, closeOnEscape: true })
50
+ const send = async (update: PartsUpdate) => {
51
+ if (collected !== undefined) return void collected.push(update)
52
+ await withBareRefusals(claude()).cmod.call({ to: cmodPluginName, method: partMethods.changed, input: update })
43
53
  }
44
54
 
45
- const show = async <Answer>(build: (answer: (value: Answer) => void) => Omit<Page, 'cancel'>, cancelled: Answer): Promise<Answer> => {
46
- page?.cancel()
55
+ const log = (what: string) => (error: unknown) => claude().ui.log(`${name}: ${what}: ${messageOf(error)}`, { to: 'debug' })
56
+
57
+ const open = (isAsked: boolean) => send({ requests: [{ open: installPage, focus: true }], ...(isAsked ? { isAsked: true } : {}) })
58
+
59
+ const show = async <Answer>(build: (answer: (value: Answer) => void) => Omit<Question, 'cancel'>, cancelled: Answer): Promise<Answer> => {
60
+ question?.cancel()
47
61
  const answer = new Promise<Answer>((resolve) => {
62
+ let isAnswered = false
48
63
  const done = (value: Answer) => {
49
- if (page === shown) page = undefined
64
+ if (isAnswered) return
65
+ isAnswered = true
50
66
  waiting.hide()
51
- resolve(value)
67
+ claude().clock.after(0, () => resolve(value))
52
68
  }
53
- const shown: Page = { ...build(done), cancel: () => done(cancelled) }
54
- page = shown
55
- })
56
- waiting.show(open)
57
- if (!isOpen) await open().catch((error: unknown) => {
58
- waiting.hide()
59
- throw error
69
+ question = { ...build(done), cancel: () => done(cancelled) }
60
70
  })
61
- claude().ui.invalidate('ui.render')
71
+ waiting.show(() => open(true))
72
+ if (isOpen) {
73
+ await send({ redraw: true }).catch(log('the installer did not redraw'))
74
+ } else {
75
+ isOpen = true
76
+ await open(false).catch((error: unknown) => {
77
+ isOpen = false
78
+ waiting.hide()
79
+ throw error
80
+ })
81
+ }
82
+ for (const resolve of waitingForShow) resolve()
83
+ waitingForShow = []
62
84
  return answer
63
85
  }
64
86
 
65
- const frame = (title: string, body: readonly RenderElement[], actions: readonly RenderElement[]) =>
66
- Box({ flexDirection: 'column', gap: 1, paddingX: 1, children: [Text({ bold: true, children: title }), ...body, Box({ gap: 3, children: actions })] })
87
+ const redraw = () => void send({ redraw: true }).catch(log('the installer did not redraw'))
88
+
89
+ const actions = (...buttons: readonly RenderElement[]) => Box({ gap: 2, flexShrink: 0, children: buttons })
90
+
91
+ const dialog = (title: RenderElement, ...children: readonly RenderElement[]) => Box({ flexDirection: 'column', gap: 1, children: [title, ...children] })
92
+
93
+ const bullet = (text: string) => Box({ gap: 1, children: [Text({ dimColor: true, children: '•' }), Text({ wrap: 'wrap', children: text })] })
67
94
 
68
- const bullet = (text: string) => Text({ children: [Text({ color: 'claude', children: '◆ ' }), text] })
95
+ const group = (heading: string, items: readonly string[]) => Box({ flexDirection: 'column', children: [Text({ bold: true, children: heading }), ...items.map(bullet)] })
96
+
97
+ const draw = (columns: number, isFocused: boolean): DrawnPart => {
98
+ if (question === undefined && !isOpen) claude().clock.after(0, () => void send({ requests: [{ close: installPage }] }).catch(log('a leftover install page stayed open')))
99
+ const handlers: Handler[] = []
100
+ const tree = elementData(question?.draw({ params: undefined, isFocused, columns }) ?? Text({ dimColor: true, children: `${name} needs nothing from you now.` }), handlers)
101
+ const drawn = nextDraw()
102
+ draws.set(drawn, handlers)
103
+ for (const kept of draws.keys()) if (draws.size > keptDraws) draws.delete(kept)
104
+ return { draw: drawn, tree }
105
+ }
106
+
107
+ const collect = async (run: () => unknown): Promise<PartsUpdate> => {
108
+ const outer = collected
109
+ const mine: PartsUpdate[] = []
110
+ collected = mine
111
+ try {
112
+ await run()
113
+ } finally {
114
+ collected = outer
115
+ }
116
+ return merged(mine)
117
+ }
118
+
119
+ const press = async ({ draw: drawn, handler, args }: PartPress): Promise<PartsUpdate> => {
120
+ const run = draws.get(drawn)?.[handler]
121
+ if (run === undefined) throw new Error(`${name}: that button drew before the last redraw. Press the one it shows now.`)
122
+ return collect(() => run(...args))
123
+ }
69
124
 
70
125
  return {
71
- draws: (e) => e.component === 'Pane' && e.requestId === paneId,
72
- draw(e) {
73
- return drawWith(claude().ui.resolve(e), () => page?.draw(e.props as Frozen<RenderPropsOf['Pane']>) ?? Text({ dimColor: true, children: `${name} is installing.` }))
126
+ answer(method, input) {
127
+ if (method === partMethods.draw && isObject(input) && input['page'] === installPage) return Promise.resolve({ value: draw(Number(input['columns']), input['isFocused'] === true) })
128
+ if (method === partMethods.press && isObject(input) && typeof input['draw'] === 'number' && draws.has(input['draw'])) return press(input as PartPress).then((value) => ({ value }))
129
+ return undefined
74
130
  },
75
- closed(id) {
76
- if (id !== paneId) return
131
+ hear(shown) {
132
+ if (shown.open.some((each) => each.id === installPage)) {
133
+ isSeen = isOpen
134
+ return
135
+ }
136
+ if (!isSeen) return
77
137
  isOpen = false
78
- page?.cancel()
138
+ isSeen = false
139
+ question?.cancel()
79
140
  },
80
141
  consent({ install, uninstall, hotkeys, permissions, updates }) {
81
142
  const changes = [
@@ -87,14 +148,12 @@ export function createInstaller(name: string, claude: () => Claude, waiting: { s
87
148
  return show<boolean>(
88
149
  (answer) => ({
89
150
  draw: () =>
90
- frame(
91
- `${name} wants to:`,
92
- [
93
- ...(permissions.length === 0 ? [] : [Box({ flexDirection: 'column', children: permissions.map((item) => bullet(permissionWords(item))) })]),
94
- ...(changes.length === 0 ? [] : [Text({ children: permissions.length === 0 ? 'Change your computer:' : 'and change your computer:' }), Box({ flexDirection: 'column', children: changes.map(bullet) })]),
95
- Text({ dimColor: true, children: 'You can turn each permission off later in Settings.' }),
96
- ],
97
- [Button({ key: 'not-now', hotkey: 'n', label: 'Not now', onPress: () => answer(false) }), Button({ key: 'accept', hotkey: 'a', label: 'Accept', onPress: () => answer(true) })],
151
+ dialog(
152
+ Text({ bold: true, children: `Install ${name}?` }),
153
+ ...(permissions.length === 0 ? [] : [group(`${name} wants to:`, permissions.map(permissionWords))]),
154
+ ...(changes.length === 0 ? [] : [group(permissions.length === 0 ? `${name} wants to change your computer:` : 'and change your computer:', changes)]),
155
+ Text({ dimColor: true, wrap: 'wrap', children: 'You can turn each permission off later in Settings.' }),
156
+ 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) })),
98
157
  ),
99
158
  }),
100
159
  false,
@@ -106,38 +165,36 @@ export function createInstaller(name: string, claude: () => Claude, waiting: { s
106
165
  return show<boolean>(
107
166
  (answer) => ({
108
167
  draw: () =>
109
- frame(
110
- `${name} needs ${missing.length === 1 ? 'one setting' : `${missing.length} settings`}:`,
111
- missing.map(([key, option]) => {
112
- const label = Text({ children: [Text({ bold: true, children: option.title }), option.description === '' ? '' : Text({ dimColor: true, children: ` ${option.description}` })] })
113
- if (option.type === 'secret') {
114
- return Box({
115
- flexDirection: 'column',
116
- children: [label, Text({ dimColor: true, children: `${secretFix} Then press Next.` }), Button({ key: `secret:${key}`, label: 'Open /config', onPress: () => void claude().command.run({ command: 'config' }) })],
168
+ dialog(
169
+ Text({ bold: true, children: `Set up ${name}` }),
170
+ Text({ children: `${name} needs ${missing.length === 1 ? 'one setting' : `${missing.length} settings`}:` }),
171
+ ...missing.map(([key, option]) => {
172
+ const label = Box({ flexDirection: 'column', children: [Text({ bold: true, children: option.title }), ...(option.description === '' ? [] : [Text({ dimColor: true, wrap: 'wrap', children: option.description })])] })
173
+ if (option.type === 'secret') {
174
+ 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' }) })] })
175
+ }
176
+ const failure = failures.get(key)
177
+ const field = Input({
178
+ key: `option:${key}`,
179
+ label: 'Value',
180
+ placeholder: option.type === 'choice' ? (option.choices ?? []).join(', ') : option.type === 'toggle' ? 'true or false' : option.type === 'list' ? 'comma-separated' : '',
181
+ value: saved.get(key) ?? '',
182
+ submitLabel: 'save',
183
+ onSubmit: (value: string) =>
184
+ void save(key, value)
185
+ .then((refused) => {
186
+ if (refused === undefined) {
187
+ saved.set(key, value)
188
+ failures.delete(key)
189
+ } else failures.set(key, refused)
190
+ })
191
+ .catch((error: unknown) => failures.set(key, messageOf(error)))
192
+ .finally(redraw),
117
193
  })
118
- }
119
- const failure = failures.get(key)
120
- const field = Input({
121
- key: `option:${key}`,
122
- label: 'Value',
123
- placeholder: option.type === 'choice' ? (option.choices ?? []).join(', ') : option.type === 'toggle' ? 'true or false' : option.type === 'list' ? 'comma-separated' : '',
124
- value: saved.get(key) ?? '',
125
- submitLabel: 'save',
126
- onSubmit: (value: string) =>
127
- void save(key, value)
128
- .then((refused) => {
129
- if (refused === undefined) {
130
- saved.set(key, value)
131
- failures.delete(key)
132
- } else failures.set(key, refused)
133
- })
134
- .catch((error: unknown) => failures.set(key, messageOf(error)))
135
- .finally(() => claude().ui.invalidate('ui.render')),
136
- })
137
- const status = failure !== undefined ? Text({ color: 'error', children: failure }) : saved.has(key) ? Text({ color: 'success', children: '✔ saved' }) : Text({ children: '' })
138
- return Box({ flexDirection: 'column', children: [label, field, status] })
194
+ const status = failure !== undefined ? [Text({ color: 'error', wrap: 'wrap', children: failure })] : saved.has(key) ? [Text({ color: 'success', children: '✓ saved' })] : []
195
+ return Box({ flexDirection: 'column', children: [label, field, ...status] })
139
196
  }),
140
- [Button({ key: 'not-now', hotkey: 'n', label: 'Not now', onPress: () => answer(false) }), Button({ key: 'next', hotkey: 'x', label: 'Next', onPress: () => answer(true) })],
197
+ 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) })),
141
198
  ),
142
199
  }),
143
200
  false,
@@ -152,7 +209,7 @@ export function createInstaller(name: string, claude: () => Claude, waiting: { s
152
209
  try {
153
210
  body = step.render(mod, props)
154
211
  } catch (error) {
155
- body = Text({ color: 'error', children: `The ${step.title} step could not draw: ${messageOf(error)}` })
212
+ body = Text({ color: 'error', wrap: 'wrap', children: `The ${step.title} step could not draw: ${messageOf(error)}` })
156
213
  }
157
214
  const next = async () => {
158
215
  try {
@@ -161,12 +218,13 @@ export function createInstaller(name: string, claude: () => Claude, waiting: { s
161
218
  } catch (error) {
162
219
  notice = { color: 'error', text: `The ${step.title} step's isDone failed: ${messageOf(error)}` }
163
220
  }
164
- claude().ui.invalidate('ui.render')
221
+ await send({ redraw: true })
165
222
  }
166
- return frame(
167
- `${step.title} (${index} of ${total})`,
168
- [body, ...(notice === undefined ? [] : [Text({ color: notice.color, children: notice.text })])],
169
- [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() })],
223
+ return dialog(
224
+ Text({ children: [Text({ bold: true, children: step.title }), Text({ dimColor: true, children: ` (${index} of ${total})` })] }),
225
+ body,
226
+ ...(notice === undefined ? [] : [Text({ color: notice.color, wrap: 'wrap', children: notice.text })]),
227
+ 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) })),
170
228
  )
171
229
  },
172
230
  }),
@@ -174,10 +232,14 @@ export function createInstaller(name: string, claude: () => Claude, waiting: { s
174
232
  )
175
233
  },
176
234
  async close() {
177
- page = undefined
235
+ question?.cancel()
236
+ question = undefined
178
237
  if (!isOpen) return
179
238
  isOpen = false
180
- await claude().ui.close({ id: paneId })
239
+ isSeen = false
240
+ await send({ requests: [{ close: installPage }] }).catch(log('the install page stayed open'))
181
241
  },
242
+ shown: () => new Promise<void>((resolve) => void waitingForShow.push(resolve)),
243
+ collect,
182
244
  }
183
245
  }