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