@cmodjs/core 3.1.0 → 4.0.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 (200) hide show
  1. package/docs/commands.md +38 -21
  2. package/docs/dependencies.md +27 -20
  3. package/docs/index.md +21 -17
  4. package/docs/install-steps.md +37 -29
  5. package/docs/jobs.md +25 -52
  6. package/docs/mod.md +93 -65
  7. package/docs/options.md +2 -2
  8. package/docs/permissions.md +4 -4
  9. package/docs/session.md +14 -12
  10. package/docs/testing.md +55 -32
  11. package/docs/ui.md +255 -263
  12. package/hotkeys.d.ts +22 -0
  13. package/hotkeys.d.ts.map +1 -0
  14. package/hotkeys.js +26 -0
  15. package/installer.d.ts +1 -1
  16. package/installer.d.ts.map +1 -1
  17. package/installer.js +11 -6
  18. package/jobs/check.d.ts +2 -1
  19. package/jobs/check.d.ts.map +1 -1
  20. package/jobs/check.js +2 -2
  21. package/jobs/context.d.ts +2 -1
  22. package/jobs/context.d.ts.map +1 -1
  23. package/jobs/context.js +1 -1
  24. package/jobs/permissions.d.ts +2 -1
  25. package/jobs/permissions.d.ts.map +1 -1
  26. package/jobs/permissions.js +2 -2
  27. package/jobs/program.js +1 -1
  28. package/jobs/prompt.d.ts +2 -1
  29. package/jobs/prompt.d.ts.map +1 -1
  30. package/jobs/prompt.js +20 -13
  31. package/jobs/schedule.d.ts +2 -1
  32. package/jobs/schedule.d.ts.map +1 -1
  33. package/jobs/schedule.js +15 -1
  34. package/jobs/slash-command.d.ts +6 -2
  35. package/jobs/slash-command.d.ts.map +1 -1
  36. package/jobs/slash-command.js +6 -5
  37. package/jobs/tool.d.ts +6 -4
  38. package/jobs/tool.d.ts.map +1 -1
  39. package/jobs/tool.js +3 -3
  40. package/mod.d.ts +63 -42
  41. package/mod.d.ts.map +1 -1
  42. package/mod.js +11 -1
  43. package/package.json +1 -1
  44. package/records.d.ts +13 -8
  45. package/records.d.ts.map +1 -1
  46. package/records.js +41 -31
  47. package/register.d.ts.map +1 -1
  48. package/register.js +1 -2
  49. package/runtime/announce.d.ts +1 -1
  50. package/runtime/announce.d.ts.map +1 -1
  51. package/runtime/announce.js +6 -7
  52. package/runtime/before.d.ts +2 -2
  53. package/runtime/before.d.ts.map +1 -1
  54. package/runtime/before.js +1 -1
  55. package/runtime/claude.d.ts +0 -1
  56. package/runtime/claude.d.ts.map +1 -1
  57. package/runtime/conversations.d.ts +4 -1
  58. package/runtime/conversations.d.ts.map +1 -1
  59. package/runtime/conversations.js +42 -14
  60. package/runtime/dependencies.d.ts.map +1 -1
  61. package/runtime/dependencies.js +2 -1
  62. package/runtime/engine.d.ts +5 -0
  63. package/runtime/engine.d.ts.map +1 -1
  64. package/runtime/grants.js +2 -2
  65. package/runtime/hooks.d.ts +1 -1
  66. package/runtime/hooks.d.ts.map +1 -1
  67. package/runtime/hotkeys.d.ts +15 -0
  68. package/runtime/hotkeys.d.ts.map +1 -0
  69. package/runtime/hotkeys.js +58 -0
  70. package/runtime/installer.d.ts +5 -2
  71. package/runtime/installer.d.ts.map +1 -1
  72. package/runtime/installer.js +23 -12
  73. package/runtime/lifecycle.d.ts.map +1 -1
  74. package/runtime/lifecycle.js +190 -141
  75. package/runtime/options.js +2 -2
  76. package/runtime/pages.d.ts +28 -0
  77. package/runtime/pages.d.ts.map +1 -0
  78. package/runtime/pages.js +98 -0
  79. package/runtime/parts.d.ts +14 -0
  80. package/runtime/parts.d.ts.map +1 -0
  81. package/runtime/parts.js +59 -0
  82. package/runtime/permission-check.d.ts +2 -2
  83. package/runtime/permission-check.d.ts.map +1 -1
  84. package/runtime/programs.js +1 -1
  85. package/runtime/router.d.ts +2 -2
  86. package/runtime/router.d.ts.map +1 -1
  87. package/runtime/router.js +1 -1
  88. package/runtime/sessions.d.ts +10 -9
  89. package/runtime/sessions.d.ts.map +1 -1
  90. package/runtime/sessions.js +14 -5
  91. package/runtime/state.d.ts +2 -1
  92. package/runtime/state.d.ts.map +1 -1
  93. package/runtime/state.js +1 -0
  94. package/runtime/toaster.d.ts +1 -1
  95. package/runtime/toaster.d.ts.map +1 -1
  96. package/runtime/toaster.js +10 -4
  97. package/runtime/ui.d.ts +87 -5
  98. package/runtime/ui.d.ts.map +1 -1
  99. package/runtime/ui.js +374 -109
  100. package/runtime/watches.d.ts +9 -0
  101. package/runtime/watches.d.ts.map +1 -0
  102. package/runtime/watches.js +73 -0
  103. package/runtime/widgets.d.ts +48 -0
  104. package/runtime/widgets.d.ts.map +1 -0
  105. package/runtime/widgets.js +219 -0
  106. package/session/context/turn/message/message.d.ts.map +1 -1
  107. package/session/context/turn/message/message.js +7 -2
  108. package/session/session.d.ts +11 -5
  109. package/session/session.d.ts.map +1 -1
  110. package/src/hotkeys.ts +51 -0
  111. package/src/installer.ts +8 -4
  112. package/src/jobs/check.ts +4 -3
  113. package/src/jobs/context.ts +3 -2
  114. package/src/jobs/permissions.ts +4 -3
  115. package/src/jobs/program.ts +1 -1
  116. package/src/jobs/prompt.ts +23 -14
  117. package/src/jobs/schedule.ts +16 -2
  118. package/src/jobs/slash-command.ts +10 -7
  119. package/src/jobs/tool.ts +7 -5
  120. package/src/mod.ts +59 -47
  121. package/src/records.ts +46 -31
  122. package/src/register.ts +2 -3
  123. package/src/runtime/announce.ts +6 -7
  124. package/src/runtime/before.ts +4 -4
  125. package/src/runtime/claude.ts +0 -1
  126. package/src/runtime/conversations.ts +66 -20
  127. package/src/runtime/dependencies.ts +2 -1
  128. package/src/runtime/engine.ts +5 -0
  129. package/src/runtime/grants.ts +2 -2
  130. package/src/runtime/hooks.ts +1 -0
  131. package/src/runtime/hotkeys.ts +80 -0
  132. package/src/runtime/installer.ts +24 -12
  133. package/src/runtime/lifecycle.ts +189 -136
  134. package/src/runtime/options.ts +2 -2
  135. package/src/runtime/pages.ts +127 -0
  136. package/src/runtime/parts.ts +70 -0
  137. package/src/runtime/permission-check.ts +2 -2
  138. package/src/runtime/programs.ts +1 -1
  139. package/src/runtime/router.ts +3 -3
  140. package/src/runtime/sessions.ts +24 -15
  141. package/src/runtime/state.ts +3 -1
  142. package/src/runtime/toaster.ts +10 -4
  143. package/src/runtime/ui.ts +407 -115
  144. package/src/runtime/watches.ts +75 -0
  145. package/src/runtime/widgets.ts +287 -0
  146. package/src/session/context/turn/message/message.ts +7 -2
  147. package/src/session/session.ts +10 -6
  148. package/src/testing/fake-claude.ts +9 -6
  149. package/src/testing/fake-elements.ts +7 -4
  150. package/src/testing/fake-sessions.ts +6 -4
  151. package/src/testing.ts +203 -46
  152. package/src/ui/areas.ts +27 -0
  153. package/src/ui/elements.ts +35 -0
  154. package/src/ui/markdown.ts +18 -18
  155. package/src/ui/panes.ts +73 -0
  156. package/src/ui/styles.ts +53 -0
  157. package/src/utils/subscription.ts +1 -0
  158. package/testing/fake-claude.d.ts +0 -1
  159. package/testing/fake-claude.d.ts.map +1 -1
  160. package/testing/fake-claude.js +9 -5
  161. package/testing/fake-elements.js +7 -4
  162. package/testing/fake-sessions.d.ts +2 -1
  163. package/testing/fake-sessions.d.ts.map +1 -1
  164. package/testing/fake-sessions.js +1 -0
  165. package/testing.d.ts +21 -6
  166. package/testing.d.ts.map +1 -1
  167. package/testing.js +206 -45
  168. package/ui/areas.d.ts +29 -0
  169. package/ui/areas.d.ts.map +1 -0
  170. package/ui/areas.js +6 -0
  171. package/ui/elements.d.ts +3 -0
  172. package/ui/elements.d.ts.map +1 -1
  173. package/ui/elements.js +36 -0
  174. package/ui/markdown.d.ts +14 -14
  175. package/ui/markdown.d.ts.map +1 -1
  176. package/ui/markdown.js +11 -11
  177. package/ui/panes.d.ts +71 -0
  178. package/ui/panes.d.ts.map +1 -0
  179. package/ui/panes.js +16 -0
  180. package/ui/styles.d.ts +44 -0
  181. package/ui/styles.d.ts.map +1 -0
  182. package/ui/styles.js +24 -0
  183. package/utils/subscription.d.ts +4 -0
  184. package/utils/subscription.d.ts.map +1 -0
  185. package/jobs/status-line.d.ts +0 -6
  186. package/jobs/status-line.d.ts.map +0 -1
  187. package/jobs/status-line.js +0 -39
  188. package/src/jobs/status-line.ts +0 -44
  189. package/src/ui/define-pane.ts +0 -27
  190. package/src/ui/places.ts +0 -49
  191. package/src/utils/disposable.ts +0 -3
  192. package/ui/define-pane.d.ts +0 -15
  193. package/ui/define-pane.d.ts.map +0 -1
  194. package/ui/define-pane.js +0 -12
  195. package/ui/places.d.ts +0 -43
  196. package/ui/places.d.ts.map +0 -1
  197. package/ui/places.js +0 -31
  198. package/utils/disposable.d.ts +0 -4
  199. package/utils/disposable.d.ts.map +0 -1
  200. /package/utils/{disposable.js → subscription.js} +0 -0
package/src/testing.ts CHANGED
@@ -20,7 +20,7 @@ import type {
20
20
  import type { CommandReply } from './jobs/slash-command.js'
21
21
  import type { ModDefinition } from './mod.js'
22
22
  import type { Options, OptionValues } from './options.js'
23
- import { readSteps, storeFolder, type Steps } from './records.js'
23
+ import { hotkeyCommand, readSteps, storeFolder, type Steps } from './records.js'
24
24
  import type { Claude } from './runtime/claude.js'
25
25
  import { answerCall, cmodPluginName, notInstalled } from './runtime/dependencies.js'
26
26
  import { closingContext, foldBefore, installBefore, withAnswer, type Answers } from './runtime/before.js'
@@ -47,14 +47,30 @@ import { elements, findElement, rowsOf } from './testing/fake-elements.js'
47
47
  import { fakeFiles } from './testing/fake-files.js'
48
48
  import { fakeSessions, type FakeSession, type FakeSessions, type MessageDraft } from './testing/fake-sessions.js'
49
49
  import { listed, messageOf } from './utils/text.js'
50
- import { componentOf, type ComponentPlace, type PlaceProps } from './ui/places.js'
50
+ import { componentOf, type Row, type StyleProps } from './ui/styles.js'
51
+ import { areas, TurnEnd as TurnEndArea, type Area } from './ui/areas.js'
52
+ import { drawWith, elementOf } from './ui/elements.js'
53
+ import { drawnParts } from './runtime/parts.js'
54
+ import { serveWidgets, type Widgets } from './runtime/widgets.js'
55
+ import { engineOf } from './runtime/engine.js'
56
+ import { pagesOf } from './runtime/pages.js'
57
+ import { partMethods, type DrawnPart, type PageRequest, type PartPress, type PartsUpdate } from './runtime/ui.js'
51
58
 
52
59
  export type { Fakes, TestCall } from './testing/fake-claude.js'
53
60
  export type { FakeSession, MessageDraft } from './testing/fake-sessions.js'
54
61
  export type { RoutedEvent } from './runtime/hooks.js'
55
62
  export type { ShownToast } from './runtime/toaster.js'
56
63
 
57
- export type Shown = ShownByClaude & { readonly toasts: readonly ShownToast[]; readonly sent: FakeSessions['sent'] }
64
+ export type ShownPart = {
65
+ readonly id: string
66
+ readonly params: unknown
67
+ lines(options?: { readonly columns?: number }): Promise<string[]>
68
+ press(key: string, link?: string): Promise<void>
69
+ input(key: string, text: string, type?: UiInputArgument['kind']): Promise<void>
70
+ select(key: string, value: string): Promise<void>
71
+ }
72
+
73
+ export type Shown = ShownByClaude & { readonly toasts: readonly ShownToast[]; readonly sent: FakeSessions['sent']; readonly pane: ShownPart; readonly page: ShownPart }
58
74
 
59
75
  export type TestOptions<State extends object, Declared extends Options = Options> = {
60
76
  readonly state?: { readonly [Lifetime in keyof State]?: Partial<State[Lifetime]> }
@@ -104,14 +120,14 @@ export type TestedMod<State extends object> = {
104
120
  compact(): Promise<void>
105
121
  startSession(start: { readonly source: 'startup' | 'resume' | 'clear' | 'fork'; readonly id?: string }): Promise<void>
106
122
  startTask(task: { readonly title: string; readonly agentType?: string; readonly model?: string; readonly openedBy?: string; readonly parent?: string }): Promise<string>
107
- lines(paneId: string): Promise<string[]>
108
- lines<P extends ComponentPlace>(place: P, props: Omit<PlaceProps<P>, 'Default'>, requestId?: string): Promise<string[]>
123
+ lines(area: Area, props?: { readonly columns?: number }): Promise<string[]>
124
+ lines<K extends Row>(kind: K, props: Omit<StyleProps<K>, 'Default'>, requestId?: string): Promise<string[]>
109
125
  type(line: string): Promise<CommandReply>
126
+ hotkey(name: string): Promise<void>
110
127
  callTool(name: string, input: Record<string, unknown>): Promise<EventResult<'tool.call'>>
111
- press(paneId: string, key: string, link?: string): Promise<void>
112
- input(paneId: string, key: string, text: string, type?: UiInputArgument['kind']): Promise<void>
113
- select(paneId: string, key: string, value: string): Promise<void>
128
+ press(area: Area, key: string): Promise<void>
114
129
  moveTo(projectRoot: string, cwd?: string): Promise<void>
130
+ readonly api: Readonly<Record<string, (input?: unknown) => Promise<unknown>>>
115
131
  readonly state: Readonly<State>
116
132
  readonly calls: readonly TestCall[]
117
133
  readonly fakes: Fakes
@@ -153,7 +169,56 @@ export function testMod<State extends object, Declared extends Options = Options
153
169
  },
154
170
  })
155
171
  conversations.start(sessionId)
156
- fake.fakes.cmod.call = fakeDependencies(name, options.dependencies ?? {}, conversations)
172
+ const callMod = async (method: string, input: unknown, caller = cmodPluginName) => calledAnswer(caller, await lifecycle.route('cmod.call', { to: name, method, input, caller } as unknown as Frozen<Args<'cmod.call'>>, async () => ({ deny: notInstalled(name) })))
173
+ let declaredPages: ReturnType<typeof pagesOf> = []
174
+ let openPages: { readonly id: string; readonly params: unknown }[] = []
175
+ let shownPage: { readonly id: string; readonly params: unknown; readonly isFocused: boolean } | undefined
176
+ const tellShown = () => callMod(partMethods.shown, { open: openPages, shown: shownPage?.id ?? null, isFocused: shownPage?.isFocused ?? false })
177
+ const applyRequests = async (requests: readonly PageRequest[]) => {
178
+ for (const request of requests) {
179
+ if ('open' in request) {
180
+ const isSettings = declaredPages.some((page) => page.id === request.open && page.place === 'settings')
181
+ if (!isSettings) openPages = [...openPages.filter((page) => page.id !== request.open), { id: request.open, params: request.params ?? null }]
182
+ shownPage = { id: request.open, params: request.params ?? null, isFocused: request.focus === true }
183
+ } else if ('close' in request) {
184
+ openPages = openPages.filter((page) => page.id !== request.close)
185
+ if (shownPage?.id === request.close) shownPage = undefined
186
+ } else if ('settings' in request) {
187
+ const first = declaredPages.find((page) => page.place === 'settings')
188
+ shownPage = first === undefined ? undefined : { id: first.id, params: null, isFocused: true }
189
+ }
190
+ }
191
+ await tellShown()
192
+ }
193
+ let widgets: Promise<Widgets> | undefined
194
+ const updated = async ({ parts, redraw, requests }: PartsUpdate) => {
195
+ if (parts !== undefined) declaredPages = pagesOf(parts)
196
+ if (parts !== undefined || redraw !== undefined) (await widgetsOf()).update(name, { ...(parts === undefined ? {} : { parts }), ...(redraw === undefined ? {} : { redraw }) })
197
+ if (requests !== undefined && requests.length > 0) await applyRequests(requests)
198
+ }
199
+ const pressPart = (input: PartPress) => callMod(partMethods.press, input).then((update) => updated((update ?? {}) as PartsUpdate))
200
+ const servedWidgets = async (): Promise<Widgets> => {
201
+ const own = name === cmodPluginName && lifecycle.mod !== undefined ? engineOf(lifecycle.mod).widgets : undefined
202
+ if (own !== undefined) return own
203
+ const claude: Claude = { ...fake.claude, cmod: { call: async (input) => (input.to === name ? callMod(input.method, input.input) : fake.claude.cmod.call(input)) } }
204
+ const report = (_id: string, message: string, error: unknown) => fake.claude.ui.log(`cmod: ${message}: ${messageOf(error)}`, { to: 'debug' })
205
+ const press = (_mod: string, input: PartPress) => void pressPart(input).catch((error: unknown) => fake.claude.ui.log(`cmod: ${name} did not take the press: ${messageOf(error)}`, { to: 'debug' }))
206
+ return serveWidgets({
207
+ claude,
208
+ router: cmodPlugin,
209
+ store: storeFolder({ HOME: await fake.claude.env.home(), XDG_DATA_HOME: await fake.claude.env.dataHome() }),
210
+ report,
211
+ turnClosedBy: () => conversations.lastTurn(),
212
+ parts: drawnParts({ claude, report, press }),
213
+ })
214
+ }
215
+ const widgetsOf = () => (widgets ??= servedWidgets())
216
+ const dependencies = fakeDependencies(name, options.dependencies ?? {}, conversations)
217
+ fake.fakes.cmod.call = async (call) => {
218
+ if (call.to !== cmodPluginName || call.method !== partMethods.changed) return dependencies(call)
219
+ await updated(call.input as PartsUpdate)
220
+ return { isHeld: (call.input as PartsUpdate).isAsked === true }
221
+ }
157
222
  const appendRow = fake.claude.session.append
158
223
  fake.claude.session.append = async (args) => {
159
224
  const added = await appendRow(args)
@@ -221,15 +286,25 @@ export function testMod<State extends object, Declared extends Options = Options
221
286
 
222
287
  const route = async <N extends RoutedEvent>(event: N, input: unknown, below: unknown): Promise<EventResult<N>> => {
223
288
  await start()
289
+ if (event === 'ui.render') {
290
+ await widgetsOf()
291
+ await fake.settle()
292
+ }
224
293
  return cmodPlugin.dispatch(event, input as Frozen<Args<N>>, (passed) => lifecycle.route(event, passed, async (e) => (below ?? claudeCodeAnswer(event, e)) as EventResult<N>))
225
294
  }
226
295
 
296
+ const runCommand = async (command: string, args: string): Promise<CommandRunResult> => {
297
+ await start()
298
+ const reached: Set<string> = new Set()
299
+ const ownRun = async (): Promise<CommandRunResult> => (reached.add(command), {})
300
+ const answer = await cmodPlugin.dispatch('command.run', { command, args, origin: { kind: 'composer' }, presentation }, (passed) => lifecycle.route('command.run', passed, ownRun))
301
+ if (reached.has(command) && answer.text === undefined) throw new Error(!fake.shown.commands.includes(command) ? `${name} has no slash command /${command}. Add it in setup with mod.use(slashCommand({ name: '${command}', … })).` : `${name}: /${command} reached Claude Code's own run of the command, so Claude Code shows "${name} registered /${command} but no command.run hook answered it". Answer it without calling next(e).`)
302
+ return answer
303
+ }
304
+
227
305
  const drawPane = async (paneId: string) => {
228
306
  await start()
229
- if (!fake.shown.openPanes.has(paneId)) {
230
- const opening = [...fake.shown.commands.map((command) => `tested.type('/${command}')`), 'pane.open() in the mod'].join(', or with ')
231
- throw new Error(`The pane "${paneId}" of ${name} is not open, and a user sees a pane only while it is open. Open it first with ${opening}.`)
232
- }
307
+ if (!fake.shown.openPanes.has(paneId)) throw new Error(`The pane "${paneId}" of ${name} closed, and a user sees a pane only while it is open.`)
233
308
  const opened = fake.calls.findLast((call) => call.call === 'ui.open' && (call.args[0] as PaneOpenArgs).id === paneId)?.args[0] as PaneOpenArgs | undefined
234
309
  const columns = opened?.columns ?? defaultColumns
235
310
  const isFocused = (await fake.claude.ui.panes()).some((open) => open.id === paneId && open.isFocused)
@@ -238,13 +313,92 @@ export function testMod<State extends object, Declared extends Options = Options
238
313
  return { drawing, columns }
239
314
  }
240
315
 
241
- const drawnElement = async (paneId: string, type: 'Button' | 'Input' | 'Select' | 'Markdown', key: string) => {
242
- const props = findElement((await drawPane(paneId)).drawing, type, key)
243
- if (props === undefined) throw new Error(`The pane "${paneId}" of ${name} draws no ${type} with the ${type === 'Button' ? 'key or label' : 'key'} "${key}".`)
244
- return props
316
+ const drawPage = async (pageId: string, columns = defaultColumns) => {
317
+ await start()
318
+ const page = shownPage
319
+ if (page?.id !== pageId) throw new Error(`The page "${pageId}" of ${name} no longer shows, and a user sees a page only while it shows.`)
320
+ const drawn = (await callMod(partMethods.draw, { page: pageId, params: page.params, columns, isFocused: page.isFocused })) as DrawnPart | null
321
+ await fake.settle()
322
+ const tree = drawn?.tree ?? null
323
+ const drawing = tree === null || drawn === null ? elements.Box({}) : (drawWith(elements, () => elementOf(tree, '', (handler, args) => void pressPart({ draw: drawn.draw, handler, args }))) as RenderElement)
324
+ return { drawing, columns }
325
+ }
326
+
327
+ const shownNow = (isPane: boolean): { readonly id: string; readonly params: unknown } => {
328
+ if (isPane) {
329
+ const id = [...fake.shown.openPanes].at(-1)
330
+ if (id === undefined) throw new Error(`No pane of ${name} shows. Open one with pane.open(), from a slash command, a hotkey, or setup.`)
331
+ return { id, params: undefined }
332
+ }
333
+ if (shownPage === undefined) throw new Error(`No page of ${name} shows in the Sidebar. Open one with page.open(), or Settings with mod.settings.open().`)
334
+ return shownPage
335
+ }
336
+
337
+ const shownPart = (isPane: boolean): ShownPart => {
338
+ const kind = isPane ? 'pane' : 'page'
339
+ const draw = async (columns?: number) => {
340
+ await start()
341
+ const { id } = shownNow(isPane)
342
+ return { id, ...(isPane ? await drawPane(id) : await drawPage(id, columns)) }
343
+ }
344
+ const drawnElement = async (type: 'Button' | 'Input' | 'Select' | 'Markdown', key: string) => {
345
+ const { id, drawing } = await draw()
346
+ const props = findElement(drawing, type, key)
347
+ if (props === undefined) throw new Error(`The ${kind} "${id}" of ${name} draws no ${type} with the ${type === 'Button' ? 'key or label' : 'key'} "${key}".`)
348
+ return { id, props }
349
+ }
350
+ const eventIn = (id: string, key: string) => ({ plugin: name, element: key, component: 'Pane' as const, requestId: id, surface: 'terminal' as const })
351
+ return {
352
+ get id() {
353
+ return shownNow(isPane).id
354
+ },
355
+ get params() {
356
+ return shownNow(isPane).params
357
+ },
358
+ lines: async (lineOptions) => {
359
+ const { drawing, columns } = await draw(lineOptions?.columns)
360
+ return rowsOf(drawing, columns).map((row) => row.trimEnd())
361
+ },
362
+ async press(key, link) {
363
+ const { id, props } = await drawnElement(link === undefined ? 'Button' : 'Markdown', key)
364
+ if (link !== undefined && props['onLinkPress'] === undefined) throw new Error(`The Markdown "${key}" in the ${kind} "${id}" of ${name} has no onLinkPress, so Claude Code opens its links itself.`)
365
+ const pressed: UiPressArgument = { ...eventIn(id, String(props['key'])), ...(link === undefined ? {} : { link: { href: link } }) }
366
+ const run = async (e: Frozen<UiPressArgument>) => {
367
+ if (e.link === undefined) await (props['onPress'] as (e: Frozen<UiPressArgument>) => unknown)(e)
368
+ else await (props['onLinkPress'] as (link: PressedLink, e: Frozen<UiPressArgument>) => unknown)(e.link, e)
369
+ return { element: e.element }
370
+ }
371
+ if (isPane) await lifecycle.route('ui.press', pressed, run)
372
+ else await run(pressed)
373
+ await fake.settle()
374
+ },
375
+ async input(key, text, type = 'submit') {
376
+ const { id, props } = await drawnElement('Input', key)
377
+ const typed: UiInputArgument = { ...eventIn(id, key), kind: type, value: text }
378
+ await (props[type === 'submit' ? 'onSubmit' : 'onInput'] as ((value: string, e: UiInputArgument) => unknown) | undefined)?.(text, typed)
379
+ await fake.settle()
380
+ },
381
+ async select(key, value) {
382
+ const { id, props } = await drawnElement('Select', key)
383
+ const choices = (props['options'] as readonly { readonly value: string }[]).map((option) => option.value)
384
+ if (!choices.includes(value)) throw new Error(`The Select "${key}" in the ${kind} "${id}" of ${name} has no option "${value}". Its options are ${choices.map((option) => `"${option}"`).join(', ')}.`)
385
+ const picked: UiSelectArgument = { ...eventIn(id, key), value }
386
+ await (props['onSelect'] as (value: string, e: UiSelectArgument) => unknown)(value, picked)
387
+ await fake.settle()
388
+ },
389
+ }
245
390
  }
246
391
 
247
- const eventIn = (paneId: string, key: string) => ({ plugin: name, element: key, component: 'Pane' as const, requestId: paneId, surface: 'terminal' as const })
392
+ const isArea = (target: string): target is Area => (areas as readonly string[]).includes(target)
393
+
394
+ const drawWidgets = async (area: Area, columns = defaultColumns) => {
395
+ await start()
396
+ const served = await widgetsOf()
397
+ await fake.settle()
398
+ const turn = area === TurnEndArea ? conversations.lastTurn() : undefined
399
+ if (area === TurnEndArea && turn?.closedAt === undefined) throw new Error(`${name} has no ended turn, so TurnEnd draws nothing. End one with tested.add({ type: Prompt, text: '…' }), then tested.endTurn().`)
400
+ return { drawing: elements.Box({ flexDirection: 'column', children: [...(await served.draw(area, elements, columns, turn))] }), columns }
401
+ }
248
402
 
249
403
  const drawSlot = async (component: RenderComponent, props: object, requestId = `${component}_${(slotDraws += 1)}`) => {
250
404
  await start()
@@ -363,20 +517,21 @@ export function testMod<State extends object, Declared extends Options = Options
363
517
  await start()
364
518
  return settled(conversations.startTask(task))
365
519
  },
366
- lines: async (target: string, props?: object, requestId?: string) => {
367
- const component = componentOf(target as ComponentPlace)
368
- if (props !== undefined && component === undefined) throw new Error(`lines takes a pane id, or a place render draws and its props, and ${target} is neither a place such as ToolCall nor called without props.`)
369
- const { drawing, columns } = props === undefined || component === undefined ? await drawPane(target) : await drawSlot(component, props, requestId)
520
+ lines: async (target: string, props?: { readonly columns?: number }, requestId?: string) => {
521
+ if (isArea(target) && (props === undefined || Object.keys(props).every((key) => key === 'columns'))) {
522
+ const { drawing, columns } = await drawWidgets(target, props?.columns)
523
+ return rowsOf(drawing, columns).map((row) => row.trimEnd())
524
+ }
525
+ const component = componentOf(target as Row)
526
+ if (props === undefined || component === undefined) throw new Error(`lines takes an area, such as AbovePrompt, or a row kind, such as ToolCall, with its props, and ${target} is neither. Read a pane with tested.shown.pane.lines() and a page with tested.shown.page.lines().`)
527
+ const { drawing, columns } = await drawSlot(component, props, requestId)
370
528
  return rowsOf(drawing, columns).map((row) => row.trimEnd())
371
529
  },
372
530
  async type(line) {
373
531
  const typed = /^\/(\S+)\s*(.*)$/s.exec(line)
374
532
  if (typed === null) throw new Error(`type takes a slash command the way the user types it, such as tested.type('/help'), and "${line}" does not start with /.`)
375
533
  const [, command = '', args = ''] = typed
376
- const unanswered: CommandRunResult = {}
377
- const answer = await route('command.run', { command, args, origin: { kind: 'composer' }, presentation }, unanswered)
378
- if (answer === unanswered) throw new Error(`${name} has no slash command /${command}. Add it in setup with mod.use(slashCommand({ name: '${command}', … })).`)
379
- return replyOf(answer)
534
+ return replyOf(await runCommand(command, args))
380
535
  },
381
536
  async callTool(toolName, input) {
382
537
  const tool = `mcp__${name}__${toolName}`
@@ -389,28 +544,28 @@ export function testMod<State extends object, Declared extends Options = Options
389
544
  if (answer === unanswered) throw new Error(`${name} has no tool ${toolName}. Add it in setup with mod.use(tool({ name: '${toolName}', … })).`)
390
545
  return answer
391
546
  },
392
- async press(paneId, key, link) {
393
- const props = await drawnElement(paneId, link === undefined ? 'Button' : 'Markdown', key)
394
- if (link !== undefined && props['onLinkPress'] === undefined) throw new Error(`The Markdown "${key}" in the pane "${paneId}" of ${name} has no onLinkPress, so Claude Code opens its links itself.`)
395
- const pressed: UiPressArgument = { ...eventIn(paneId, String(props['key'])), ...(link === undefined ? {} : { link: { href: link } }) }
396
- await lifecycle.route('ui.press', pressed, async (e) => {
397
- if (e.link === undefined) await (props['onPress'] as (e: UiPressArgument) => unknown)(e)
398
- else await (props['onLinkPress'] as (link: PressedLink, e: UiPressArgument) => unknown)(e.link, e)
399
- return { element: e.element }
400
- })
401
- },
402
- async input(paneId, key, text, type = 'submit') {
403
- const props = await drawnElement(paneId, 'Input', key)
404
- const typed: UiInputArgument = { ...eventIn(paneId, key), kind: type, value: text }
405
- await (props[type === 'submit' ? 'onSubmit' : 'onInput'] as ((value: string, e: UiInputArgument) => unknown) | undefined)?.(text, typed)
547
+ async hotkey(hotkeyName) {
548
+ if ((definition.hotkeys as Readonly<Record<string, unknown>> | undefined)?.[hotkeyName] === undefined) throw new Error(`${name} has no hotkey ${hotkeyName}. Declare it in defineMod: hotkeys: { ${hotkeyName}: { title: '…', default: 'ctrl+…' } }.`)
549
+ await runCommand(hotkeyCommand(name, hotkeyName), '')
550
+ await fake.settle()
406
551
  },
407
- async select(paneId, key, value) {
408
- const props = await drawnElement(paneId, 'Select', key)
409
- const options = (props['options'] as readonly { readonly value: string }[]).map((option) => option.value)
410
- if (!options.includes(value)) throw new Error(`The Select "${key}" in the pane "${paneId}" of ${name} has no option "${value}". Its options are ${options.map((option) => `"${option}"`).join(', ')}.`)
411
- const picked: UiSelectArgument = { ...eventIn(paneId, key), value }
412
- await (props['onSelect'] as (value: string, e: UiSelectArgument) => unknown)(value, picked)
552
+ async press(area: Area, key: string) {
553
+ const { drawing } = await drawWidgets(area)
554
+ const keys = [key, ...(await widgetsOf()).widgets().filter((widget) => widget.mod === name).map((widget) => `${widget.key}:${key}`)]
555
+ const button = keys.map((each) => findElement(drawing, 'Button', each)).find((found) => found !== undefined)
556
+ if (button === undefined) throw new Error(`The widgets of ${name} in ${area} draw no Button with the key or label "${key}".`)
557
+ await (button['onPress'] as () => unknown)()
558
+ await fake.settle()
413
559
  },
560
+ api: new Proxy({}, {
561
+ get: (_target, method) =>
562
+ method === 'then'
563
+ ? undefined
564
+ : async (input?: unknown) => {
565
+ await start()
566
+ return callMod(String(method), input ?? null, 'tester')
567
+ },
568
+ }),
414
569
  async moveTo(nextRoot, nextCwd = nextRoot) {
415
570
  await start()
416
571
  if (nextRoot !== projectRoot) {
@@ -438,6 +593,8 @@ export function testMod<State extends object, Declared extends Options = Options
438
593
  return lifecycle.toasts
439
594
  },
440
595
  sent: conversations.sent,
596
+ pane: shownPart(true),
597
+ page: shownPart(false),
441
598
  },
442
599
  }
443
600
  keepClaudeCode(tested, { fire })
@@ -0,0 +1,27 @@
1
+ import type { RenderElement } from 'claude-code'
2
+ import type { Mod, Turn } from '../mod.js'
3
+
4
+ export const AbovePrompt = 'abovePrompt'
5
+ export const BelowPrompt = 'belowPrompt'
6
+ export const Working = 'working'
7
+ export const TurnEnd = 'turnEnd'
8
+ export const Dashboard = 'dashboard'
9
+
10
+ export const areas = [AbovePrompt, BelowPrompt, Working, TurnEnd, Dashboard] as const
11
+
12
+ export type Area = (typeof areas)[number]
13
+
14
+ export type WidgetContext = { readonly area: typeof TurnEnd; readonly columns: number; readonly turn: Turn } | { readonly area: Exclude<Area, typeof TurnEnd>; readonly columns: number }
15
+
16
+ export type WidgetInit<State extends object = Record<never, never>> = {
17
+ readonly id: string
18
+ readonly title: string
19
+ readonly areas: readonly [Area, ...Area[]]
20
+ render(mod: Mod<State>, context: WidgetContext): RenderElement | undefined
21
+ }
22
+
23
+ export type Widget = {
24
+ readonly id: string
25
+ readonly area: Area | undefined
26
+ remove(): void
27
+ }
@@ -41,6 +41,39 @@ function built(current: Drawing, value: unknown): unknown {
41
41
  return build(current, name, props)
42
42
  }
43
43
 
44
+ const elementKey = '$cmodElement'
45
+
46
+ const handlerKey = '$cmodHandler'
47
+
48
+ export type Handler = (...args: unknown[]) => unknown
49
+
50
+ export function elementData(value: unknown, handlers: Handler[]): unknown {
51
+ if (typeof value === 'function') return { [handlerKey]: handlers.push(value as Handler) - 1 }
52
+ if (Array.isArray(value)) return value.map((each: unknown) => elementData(each, handlers))
53
+ if (typeof value !== 'object' || value === null) return value
54
+ if (unbuilt in value) {
55
+ const { name, props } = (value as Unbuilt)[unbuilt]
56
+ return { [elementKey]: name, props: elementData(props, handlers) }
57
+ }
58
+ if (Object.getPrototypeOf(value) !== Object.prototype) throw new Error('a widget draws only the elements of ui/elements.js and ui/components.js, text, numbers, and plain objects.')
59
+ return Object.fromEntries(Object.entries(value).map(([key, each]) => [key, elementData(each, handlers)]))
60
+ }
61
+
62
+ export function elementOf(data: unknown, keyPrefix: string, press: (handler: number, args: readonly unknown[]) => void): unknown {
63
+ if (Array.isArray(data)) return data.map((each: unknown) => elementOf(each, keyPrefix, press))
64
+ if (typeof data !== 'object' || data === null) return data
65
+ const fields = data as Readonly<Record<string, unknown>>
66
+ if (typeof fields[handlerKey] === 'number') {
67
+ const handler = fields[handlerKey]
68
+ return (...args: unknown[]) => press(handler, JSON.parse(JSON.stringify(args)) as unknown[])
69
+ }
70
+ const name = fields[elementKey]
71
+ if (name === undefined) return Object.fromEntries(Object.entries(fields).map(([key, each]) => [key, elementOf(each, keyPrefix, press)]))
72
+ if (typeof name !== 'string' || !Object.hasOwn(constructors, name)) throw new Error(`a widget drew ${String(name)}, which is not an element of ui/elements.js.`)
73
+ const props = elementOf(fields['props'], keyPrefix, press) as Record<string, unknown>
74
+ return constructors[name as keyof typeof constructors]((typeof props['key'] === 'string' ? { ...props, key: `${keyPrefix}${props['key']}` } : props) as never)
75
+ }
76
+
44
77
  export const Box = element('Box')
45
78
  export const Text = element('Text')
46
79
  export const Button = element('Button')
@@ -50,3 +83,5 @@ export const Markdown = element('Markdown')
50
83
  export const Input = element('Input')
51
84
  export const Select = element('Select')
52
85
  export const Image = element('Image')
86
+
87
+ const constructors = { Box, Text, Button, Link, Code, Markdown, Input, Select, Image }
@@ -1,8 +1,8 @@
1
1
  import type { Nodes, RootContent } from 'mdast'
2
2
  import { fromMarkdown, gfm, gfmFromMarkdown, toString } from '../vendor-markdown.js'
3
- import type { PlaceProps } from './places.js'
3
+ import type { StyleProps } from './styles.js'
4
4
 
5
- type Block = { readonly text: string; readonly source: string }
5
+ type BlockText = { readonly text: string; readonly source: string }
6
6
 
7
7
  type BlockFields = {
8
8
  heading: { readonly depth: 1 | 2 | 3 | 4 | 5 | 6 }
@@ -12,30 +12,30 @@ type BlockFields = {
12
12
  html: { readonly value: string }
13
13
  }
14
14
 
15
- export type BlockProps<Type extends string> = Type extends keyof BlockFields ? Block & BlockFields[Type] : Block
15
+ export type BlockProps<Type extends string> = Type extends keyof BlockFields ? BlockText & BlockFields[Type] : BlockText
16
16
 
17
- export type MarkdownPiece<Render> = { readonly text: string } | { readonly render: Render; readonly props: Block }
17
+ export type MarkdownPiece<Render> = { readonly text: string } | { readonly render: Render; readonly props: BlockText }
18
18
 
19
19
  export type MarkdownReader = <Render>(text: string, renderOf: (type: string) => Render | undefined, replyId?: string) => readonly MarkdownPiece<Render>[]
20
20
 
21
- export type MarkdownPlace<Type extends string = string> = {
21
+ export type Block<Type extends string = string> = {
22
22
  readonly name: string
23
23
  readonly markdown: Type
24
24
  createReader(): MarkdownReader
25
25
  }
26
26
 
27
- function markdownPlace<const Type extends string>(name: string, markdown: Type): MarkdownPlace<Type> {
27
+ function blockKind<const Type extends string>(name: string, markdown: Type): Block<Type> {
28
28
  return { name, markdown, createReader }
29
29
  }
30
30
 
31
- export const Heading = markdownPlace('Heading', 'heading')
32
- export const Paragraph = markdownPlace('Paragraph', 'paragraph')
33
- export const CodeBlock = markdownPlace('CodeBlock', 'code')
34
- export const BlockQuote = markdownPlace('BlockQuote', 'blockquote')
35
- export const List = markdownPlace('List', 'list')
36
- export const Table = markdownPlace('Table', 'table')
37
- export const ThematicBreak = markdownPlace('ThematicBreak', 'thematicBreak')
38
- export const HtmlBlock = markdownPlace('HtmlBlock', 'html')
31
+ export const Heading = blockKind('Heading', 'heading')
32
+ export const Paragraph = blockKind('Paragraph', 'paragraph')
33
+ export const CodeBlock = blockKind('CodeBlock', 'code')
34
+ export const BlockQuote = blockKind('BlockQuote', 'blockquote')
35
+ export const List = blockKind('List', 'list')
36
+ export const Table = blockKind('Table', 'table')
37
+ export const ThematicBreak = blockKind('ThematicBreak', 'thematicBreak')
38
+ export const HtmlBlock = blockKind('HtmlBlock', 'html')
39
39
 
40
40
  type Parsed = {
41
41
  readonly type: string
@@ -44,7 +44,7 @@ type Parsed = {
44
44
  readonly isClosed: boolean
45
45
  readonly canSpanBlankLines: boolean
46
46
  readonly hasDefinition: boolean
47
- readonly props: Block
47
+ readonly props: BlockText
48
48
  }
49
49
 
50
50
  type Reply = { readonly id: string; readonly text: string; readonly blocks: readonly Parsed[] }
@@ -163,7 +163,7 @@ function plainText(node: Nodes): string {
163
163
  return node.children.map(plainText).join(node.type === 'tableRow' ? '\t' : '\n')
164
164
  }
165
165
 
166
- export function markdownBlocks<P extends MarkdownPlace>(text: string, place: P): Omit<PlaceProps<P>, 'Default'>[] {
167
- const blocks = parsed(text, 0).filter((block) => block.type === place.markdown)
168
- return blocks.map((block) => block.props) as unknown as Omit<PlaceProps<P>, 'Default'>[]
166
+ export function markdownBlocks<K extends Block>(text: string, kind: K): Omit<StyleProps<K>, 'Default'>[] {
167
+ const blocks = parsed(text, 0).filter((block) => block.type === kind.markdown)
168
+ return blocks.map((block) => block.props) as unknown as Omit<StyleProps<K>, 'Default'>[]
169
169
  }
@@ -0,0 +1,73 @@
1
+ import type { Frozen, PaneCloseInput, RenderElement, RenderPropsOf, UiScrollInput } from 'claude-code'
2
+ import type { Mod } from '../mod.js'
3
+
4
+ export type ScrollTarget = 'start' | 'end' | { readonly key: string }
5
+
6
+ export type ScrollOptions = { readonly block?: 'start' | 'center' | 'end' | 'nearest' }
7
+
8
+ export type OpenOptions<Params> = { readonly focus?: true } & (undefined extends Params ? { readonly params?: Params } : { readonly params: Params })
9
+
10
+ export type PaneProps<Params> = Frozen<RenderPropsOf['Pane']> & { readonly params: Params }
11
+
12
+ export type PaneInit<State extends object = Record<never, never>, Params = undefined> = {
13
+ readonly id: string
14
+ readonly title: string
15
+ readonly columns?: number | ((state: State) => number | undefined)
16
+ readonly rows?: number | ((state: State) => number | undefined)
17
+ readonly closeOnEscape?: true
18
+ readonly holdToasts?: true
19
+ render(mod: Mod<State>, props: PaneProps<Params>): RenderElement
20
+ onScroll?(mod: Mod<State>, e: Frozen<UiScrollInput>): void | Promise<void>
21
+ onClose?(mod: Mod<State>, e: Frozen<PaneCloseInput>): void | Promise<void>
22
+ }
23
+
24
+ export type Pane<Params = undefined> = {
25
+ readonly id: string
26
+ readonly isOpen: boolean
27
+ readonly isShown: boolean
28
+ readonly isFocused: boolean
29
+ readonly params: Params | undefined
30
+ open(...options: undefined extends Params ? [options?: OpenOptions<Params>] : [options: OpenOptions<Params>]): Promise<void>
31
+ toggle(options?: { readonly focus?: true }): Promise<void>
32
+ close(): Promise<void>
33
+ scrollTo(target: ScrollTarget, options?: ScrollOptions): Promise<void>
34
+ remove(): void
35
+ }
36
+
37
+ export type PageProps<Params> = { readonly params: Params; readonly isFocused: boolean; readonly columns: number }
38
+
39
+ export type PageInit<State extends object = Record<never, never>, Params = undefined> = {
40
+ readonly id: string
41
+ readonly title: string
42
+ render(mod: Mod<State>, props: PageProps<Params>): RenderElement
43
+ }
44
+
45
+ export type Page<Params = undefined> = {
46
+ readonly id: string
47
+ readonly isOpen: boolean
48
+ readonly isShown: boolean
49
+ readonly isFocused: boolean
50
+ readonly params: Params | undefined
51
+ open(...options: undefined extends Params ? [options?: OpenOptions<Params>] : [options: OpenOptions<Params>]): Promise<void>
52
+ toggle(options?: { readonly focus?: true }): Promise<void>
53
+ close(): Promise<void>
54
+ scrollTo(target: ScrollTarget, options?: ScrollOptions): Promise<void>
55
+ remove(): void
56
+ }
57
+
58
+ const partId = /^[A-Za-z0-9_-]{1,64}$/
59
+
60
+ export const sizeKeys = ['columns', 'rows'] as const
61
+
62
+ export function checkPart(name: string, kind: string, id: string, title: string): void {
63
+ if (!partId.test(id)) throw new Error(`${name}: "${id}" is not a ${kind} id. Use 1 to 64 letters, digits, "_", or "-".`)
64
+ if (title.trim() === '') throw new Error(`${name}: the ${kind} "${id}" needs a title, the label its tab shows.`)
65
+ }
66
+
67
+ export function checkPane<State extends object, Params>(name: string, pane: PaneInit<State, Params>): void {
68
+ checkPart(name, 'pane', pane.id, pane.title)
69
+ for (const key of sizeKeys) {
70
+ const size = pane[key]
71
+ if (typeof size !== 'function' && size !== undefined && (!Number.isInteger(size) || size <= 0)) throw new Error(`${name}: the pane "${pane.id}" has ${key} ${size}. Use a whole number above 0, or leave it out.`)
72
+ }
73
+ }
@@ -0,0 +1,53 @@
1
+ import type { Frozen, RenderComponent, RenderElement, RenderPropsOf } from 'claude-code'
2
+ import type { Mod } from '../mod.js'
3
+ import { TurnEnd, Working } from './areas.js'
4
+ import type { Block, BlockProps } from './markdown.js'
5
+
6
+ export const ToolGroup = 'toolGroup'
7
+ export const ToolProgress = 'toolProgress'
8
+ export const CommandOutput = 'commandOutput'
9
+ export const AskUserQuestion = 'askUserQuestion'
10
+
11
+ const components = {
12
+ prompt: 'UserMessage',
13
+ reply: 'AssistantMessage',
14
+ toolCall: 'ToolUse',
15
+ toolResult: 'ToolResult',
16
+ notice: 'InfoNotice',
17
+ [ToolGroup]: 'ToolGroup',
18
+ [ToolProgress]: 'ToolProgress',
19
+ [CommandOutput]: 'CommandOutput',
20
+ [AskUserQuestion]: 'AskUserQuestion',
21
+ [Working]: 'Spinner',
22
+ [TurnEnd]: 'TurnDuration',
23
+ } as const satisfies Readonly<Record<string, Exclude<RenderComponent, 'Pane'>>>
24
+
25
+ export type Row = keyof typeof components
26
+
27
+ export type Kind = Row | Block
28
+
29
+ type RowProps = { readonly [R in Row]: RenderPropsOf[(typeof components)[R]] }
30
+
31
+ type PropsOf<K extends Kind> = K extends Row ? RowProps[K] : K extends Block<infer Type> ? BlockProps<Type> : never
32
+
33
+ type DefaultOf<K extends Kind> = K extends Row ? Partial<Omit<RowProps[K], 'onScreen'>> : { readonly source?: string }
34
+
35
+ export type StyleProps<K extends Kind> = Frozen<PropsOf<K>> & { readonly Default: (props: Frozen<DefaultOf<K>>) => RenderElement }
36
+
37
+ export type StyleInit<K extends Kind, State extends object = Record<never, never>> = {
38
+ readonly kind: K
39
+ render(mod: Mod<State>, props: StyleProps<K>): RenderElement
40
+ }
41
+
42
+ export type Style = {
43
+ readonly kind: Kind
44
+ remove(): void
45
+ }
46
+
47
+ export function componentOf(kind: Kind): Exclude<RenderComponent, 'Pane'> | undefined {
48
+ return typeof kind === 'string' && Object.hasOwn(components, kind) ? components[kind] : undefined
49
+ }
50
+
51
+ export function nameOf(kind: Kind): string {
52
+ return typeof kind === 'string' ? `${kind.charAt(0).toUpperCase()}${kind.slice(1)}` : kind.name
53
+ }
@@ -0,0 +1 @@
1
+ export type Subscription = { remove(): void }
@@ -73,7 +73,6 @@ export type Shown = {
73
73
  readonly prompts: string[];
74
74
  readonly logs: string[];
75
75
  readonly debug: string[];
76
- readonly statuses: (string | undefined)[];
77
76
  readonly openPanes: Set<string>;
78
77
  readonly commands: string[];
79
78
  readonly tools: string[];