@cmodjs/core 3.0.0 → 4.0.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 (201) hide show
  1. package/docs/commands.md +39 -22
  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 +94 -66
  7. package/docs/options.md +2 -2
  8. package/docs/permissions.md +7 -5
  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 +14 -8
  45. package/records.d.ts.map +1 -1
  46. package/records.js +44 -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 +63 -0
  82. package/runtime/permission-check.d.ts +2 -2
  83. package/runtime/permission-check.d.ts.map +1 -1
  84. package/runtime/permission-check.js +12 -4
  85. package/runtime/programs.js +1 -1
  86. package/runtime/router.d.ts +2 -2
  87. package/runtime/router.d.ts.map +1 -1
  88. package/runtime/router.js +1 -1
  89. package/runtime/sessions.d.ts +10 -9
  90. package/runtime/sessions.d.ts.map +1 -1
  91. package/runtime/sessions.js +14 -5
  92. package/runtime/state.d.ts +2 -1
  93. package/runtime/state.d.ts.map +1 -1
  94. package/runtime/state.js +1 -0
  95. package/runtime/toaster.d.ts +1 -1
  96. package/runtime/toaster.d.ts.map +1 -1
  97. package/runtime/toaster.js +10 -4
  98. package/runtime/ui.d.ts +87 -5
  99. package/runtime/ui.d.ts.map +1 -1
  100. package/runtime/ui.js +372 -109
  101. package/runtime/watches.d.ts +9 -0
  102. package/runtime/watches.d.ts.map +1 -0
  103. package/runtime/watches.js +73 -0
  104. package/runtime/widgets.d.ts +48 -0
  105. package/runtime/widgets.d.ts.map +1 -0
  106. package/runtime/widgets.js +219 -0
  107. package/session/context/turn/message/message.d.ts.map +1 -1
  108. package/session/context/turn/message/message.js +7 -2
  109. package/session/session.d.ts +11 -5
  110. package/session/session.d.ts.map +1 -1
  111. package/src/hotkeys.ts +51 -0
  112. package/src/installer.ts +8 -4
  113. package/src/jobs/check.ts +4 -3
  114. package/src/jobs/context.ts +3 -2
  115. package/src/jobs/permissions.ts +4 -3
  116. package/src/jobs/program.ts +1 -1
  117. package/src/jobs/prompt.ts +23 -14
  118. package/src/jobs/schedule.ts +16 -2
  119. package/src/jobs/slash-command.ts +10 -7
  120. package/src/jobs/tool.ts +7 -5
  121. package/src/mod.ts +59 -47
  122. package/src/records.ts +50 -31
  123. package/src/register.ts +2 -3
  124. package/src/runtime/announce.ts +6 -7
  125. package/src/runtime/before.ts +4 -4
  126. package/src/runtime/claude.ts +0 -1
  127. package/src/runtime/conversations.ts +66 -20
  128. package/src/runtime/dependencies.ts +2 -1
  129. package/src/runtime/engine.ts +5 -0
  130. package/src/runtime/grants.ts +2 -2
  131. package/src/runtime/hooks.ts +1 -0
  132. package/src/runtime/hotkeys.ts +80 -0
  133. package/src/runtime/installer.ts +24 -12
  134. package/src/runtime/lifecycle.ts +189 -136
  135. package/src/runtime/options.ts +2 -2
  136. package/src/runtime/pages.ts +127 -0
  137. package/src/runtime/parts.ts +72 -0
  138. package/src/runtime/permission-check.ts +13 -5
  139. package/src/runtime/programs.ts +1 -1
  140. package/src/runtime/router.ts +3 -3
  141. package/src/runtime/sessions.ts +24 -15
  142. package/src/runtime/state.ts +3 -1
  143. package/src/runtime/toaster.ts +10 -4
  144. package/src/runtime/ui.ts +405 -115
  145. package/src/runtime/watches.ts +75 -0
  146. package/src/runtime/widgets.ts +287 -0
  147. package/src/session/context/turn/message/message.ts +7 -2
  148. package/src/session/session.ts +10 -6
  149. package/src/testing/fake-claude.ts +9 -6
  150. package/src/testing/fake-elements.ts +7 -4
  151. package/src/testing/fake-sessions.ts +6 -4
  152. package/src/testing.ts +203 -46
  153. package/src/ui/areas.ts +27 -0
  154. package/src/ui/elements.ts +35 -0
  155. package/src/ui/markdown.ts +18 -18
  156. package/src/ui/panes.ts +73 -0
  157. package/src/ui/styles.ts +53 -0
  158. package/src/utils/subscription.ts +1 -0
  159. package/testing/fake-claude.d.ts +0 -1
  160. package/testing/fake-claude.d.ts.map +1 -1
  161. package/testing/fake-claude.js +9 -5
  162. package/testing/fake-elements.js +7 -4
  163. package/testing/fake-sessions.d.ts +2 -1
  164. package/testing/fake-sessions.d.ts.map +1 -1
  165. package/testing/fake-sessions.js +1 -0
  166. package/testing.d.ts +21 -6
  167. package/testing.d.ts.map +1 -1
  168. package/testing.js +206 -45
  169. package/ui/areas.d.ts +29 -0
  170. package/ui/areas.d.ts.map +1 -0
  171. package/ui/areas.js +6 -0
  172. package/ui/elements.d.ts +3 -0
  173. package/ui/elements.d.ts.map +1 -1
  174. package/ui/elements.js +36 -0
  175. package/ui/markdown.d.ts +14 -14
  176. package/ui/markdown.d.ts.map +1 -1
  177. package/ui/markdown.js +11 -11
  178. package/ui/panes.d.ts +71 -0
  179. package/ui/panes.d.ts.map +1 -0
  180. package/ui/panes.js +16 -0
  181. package/ui/styles.d.ts +44 -0
  182. package/ui/styles.d.ts.map +1 -0
  183. package/ui/styles.js +24 -0
  184. package/utils/subscription.d.ts +4 -0
  185. package/utils/subscription.d.ts.map +1 -0
  186. package/jobs/status-line.d.ts +0 -6
  187. package/jobs/status-line.d.ts.map +0 -1
  188. package/jobs/status-line.js +0 -39
  189. package/src/jobs/status-line.ts +0 -44
  190. package/src/ui/define-pane.ts +0 -27
  191. package/src/ui/places.ts +0 -49
  192. package/src/utils/disposable.ts +0 -3
  193. package/ui/define-pane.d.ts +0 -15
  194. package/ui/define-pane.d.ts.map +0 -1
  195. package/ui/define-pane.js +0 -12
  196. package/ui/places.d.ts +0 -43
  197. package/ui/places.d.ts.map +0 -1
  198. package/ui/places.js +0 -31
  199. package/utils/disposable.d.ts +0 -4
  200. package/utils/disposable.d.ts.map +0 -1
  201. /package/utils/{disposable.js → subscription.js} +0 -0
package/docs/ui.md CHANGED
@@ -1,34 +1,39 @@
1
1
  # UI
2
2
 
3
- A mod draws in four ways: its own panes, the rows Claude Code draws (places), the blocks of Claude's replies (markdown places), and short messages: toasts, questions, and notices. All of it lives on `mod.ui`.
3
+ A mod draws in five ways: pages in the Sidebar and in Settings, panes of its own, widgets the person places around the prompt and on the Dashboard, styles that change how Claude Code draws a row or a block of a reply, and short messages: toasts and questions. Each part is added with one `add` call in `setup`, and each call returns the part, with `remove()` to take it back.
4
4
 
5
5
  ```ts
6
- mod.ui.pane(pane: Pane<State>): PaneHandle
7
- mod.ui.render<P extends Place>(place: P, Component: (props: PlaceProps<P>) => RenderElement): Disposable
8
- mod.ui.toast: Toast
9
- mod.ui.copy(text: string): Promise<void>
10
- mod.ui.ask(question: string, options?: readonly string[] | AskOptions): Promise<string>
11
- mod.ui.scroll(args: UiScrollArgs): Promise<UiScrollResult>
12
- mod.ui.notice(callId: string, text: string | undefined): void
6
+ mod.sidebar.add<Params>(page: PageInit<State, Params>): Page<Params>
7
+ mod.settings.add<Params>(page: PageInit<State, Params>): Page<Params>
8
+ mod.settings.open(): Promise<void>
9
+ mod.panes.add<Params>(pane: PaneInit<State, Params>): Pane<Params>
10
+ mod.widgets.add(widget: WidgetInit<State>): Widget
11
+ mod.styles.add<K extends Kind>(style: StyleInit<K, State>): Style
12
+ mod.toast: Toast
13
+ mod.ask(question: string, options?: readonly string[] | AskOptions): Promise<string>
13
14
  ```
14
15
 
16
+ `mod.js` exports the types: `PageInit`, `Page`, `PageProps`, `PaneInit`, `Pane`, `PaneProps`, `OpenOptions`, `ScrollTarget`, `ScrollOptions`, `WidgetInit`, `Widget`, `WidgetContext`, `Area`, `StyleInit`, `Style`, `StyleProps`, `Kind`, and `Row`.
17
+
15
18
  Claude Mod Manager (cmod) draws the mod again whenever a value in `mod.state` changes. A render reads `mod.state` and returns what to show. It never draws by hand.
16
19
 
20
+ ## Every message the person acts on carries a button
21
+
22
+ A mod never asks the person to type a command. When a toast, a page, or a pane needs the person to do something, it draws a `Button` that does it, or that opens the place where they do it, such as `mod.settings.open()` or a page's `open()`. cmod follows the same rule: a failure caused by a permission the person turned off carries a `Settings` button that opens the mod's Permissions, and a secret option carries an `Open /config` button.
23
+
17
24
  ## Elements
18
25
 
19
26
  `ui/elements.js` exports the elements a render draws with. Each takes the props Claude Code types in the `claude-code` module:
20
27
 
21
- | Element | Props type | What it draws |
22
- | --- | --- | --- |
23
- | `Box` | `BoxProps` | A flex container: `flexDirection`, `gap`, `padding`, `margin`, `minWidth`, and the rest |
24
- | `Text` | `TextProps` | Text: `bold`, `dimColor`, `color`, `wrap`, and the rest |
25
- | `Button` | `ButtonProps` | A button: `label` or one string child, `onPress`, `hotkey`, `key`, `variant` |
26
- | `Link` | `LinkProps` | A link |
27
- | `Code` | `CodeProps` | Highlighted code |
28
- | `Markdown` | `MarkdownProps` | Markdown text, as Claude Code draws a reply |
29
- | `Input` | `InputProps` | A text input |
30
- | `Select` | `SelectProps` | A choice of options |
31
- | `Image` | `ImageProps` | An image, with `alt` text |
28
+ - `Box`, with `BoxProps`, draws a flex container: `flexDirection`, `gap`, `padding`, `margin`, `minWidth`, and the rest.
29
+ - `Text`, with `TextProps`, draws text: `bold`, `dimColor`, `color`, `wrap`, and the rest.
30
+ - `Button`, with `ButtonProps`, draws a button: `label` or one string child, `onPress`, `hotkey`, `key`, `variant`.
31
+ - `Link`, with `LinkProps`, draws a link.
32
+ - `Code`, with `CodeProps`, draws highlighted code.
33
+ - `Markdown`, with `MarkdownProps`, draws markdown text, as Claude Code draws a reply.
34
+ - `Input`, with `InputProps`, draws a text input.
35
+ - `Select`, with `SelectProps`, draws a choice of options.
36
+ - `Image`, with `ImageProps`, draws an image, with `alt` text.
32
37
 
33
38
  Write them as JSX in a `.tsx` file. The template's `tsconfig.json` sets `jsx: react` with `jsxFactory: h` and `jsxFragmentFactory: Fragment`. Claude Code declares `h` and `Fragment` as globals, and `testMod` supplies them in tests. Never declare, import, or name anything `h` or `Fragment` in a file that writes JSX. A component is a function that returns a `RenderElement`:
34
39
 
@@ -52,7 +57,7 @@ An element built outside a render, at module load or in a hook, holds its props
52
57
 
53
58
  ## Components
54
59
 
55
- `ui/components.js` exports controls built from the elements and Claude Code's theme colors, so they follow the person's theme and look the same in every mod. Each is a component like the ones a mod writes: a function of props, used in a pane's `render` or a place's component.
60
+ `ui/components.js` exports controls built from the elements and Claude Code's theme colors, so they follow the person's theme and look the same in every mod. Each is a component like the ones a mod writes: a function of props, used in a render.
56
61
 
57
62
  ```ts
58
63
  Tabs(props: { tabs: readonly { key: string; label: string }[]; selected: string; onSelect(key: string): void; children? })
@@ -66,8 +71,8 @@ Pagination(props: { page: number; pages: number; onPage(page: number): void })
66
71
  Progress(props: { value: number; width?: number })
67
72
  ```
68
73
 
69
- - `Tabs` draws a row of tabs over `children`. The selected tab's number is in the accent color and its label bold and underlined. Each other tab is a `Button` whose hotkey is its number, so a pane that holds the keyboard switches tabs by number, Tab, or a click. A hotkey is one digit, so only the first nine tabs have a number.
70
- - `Split` lays its children side by side in equal widths. `Panel` frames one part of a pane in a rounded border with a bold title.
74
+ - `Tabs` draws a row of tabs over `children`. The selected tab's number is in the accent color and its label bold and underlined. Each other tab is a `Button` whose hotkey is its number, so a part that holds the keyboard switches tabs by number, Tab, or a click. A hotkey is one digit, so only the first nine tabs have a number.
75
+ - `Split` lays its children side by side in equal widths. `Panel` frames one part of a page or pane in a rounded border with a bold title.
71
76
  - `Toggle` draws `[x] label` or `[ ] label` as a `Button`. Enter or a click calls `onChange` with the other value.
72
77
  - `Tooltip` shows `text` in an inverted card above its children while the pointer rests on them. The card opens upward, because Claude Code paints a card over the rows drawn before it and under the rows drawn after it. It has no border, so a one-line card needs one free row above its children. The pointer alone opens it, so put nothing only a tooltip says.
73
78
  - `Help` draws a `?` that shows `text` the same way. A press of `?` keeps the card open until the next press, so the keyboard reaches it too. Keep `isOpen` in `mod.state`.
@@ -76,284 +81,298 @@ Progress(props: { value: number; width?: number })
76
81
  - `Progress` fills `value` percent of `width` columns, 24 unless given. Put the count or a label beside it in a `Text`, or pass it as a loading toast's `description`.
77
82
  - `Tabs`, `Toggle`, `Help`, and `Pagination` hold no state of their own. Keep their value in `mod.state` and set it in the callback, as in React's controlled components.
78
83
 
79
- A pane opened with `focus`, `closeOnEscape`, and `holdToasts` is a dialog. Claude Code shows it as a tab of the dock, beside the mod's other panes, and Escape closes it:
84
+ ## The Sidebar and its pages
85
+
86
+ The Sidebar is one pane the cmod plugin draws beside the conversation for every mod. Its tabs are the Dashboard, the pages mods opened, and Settings. The person shows or hides it with `ctrl+q`, switches tabs with `0` for the Dashboard, `1` to `8` for the pages, and `9` for Settings, and closes a page with the `×` on its tab. They change both combos in Settings, under cmod, Hotkeys.
87
+
88
+ A page is a part of a mod that draws in the Sidebar. Put each page in its own file in `src/pages/`:
80
89
 
81
90
  ```tsx
82
- import { definePane } from '../../node_modules/@cmodjs/core/ui/define-pane.js'
83
- import { Dialog } from '../../node_modules/@cmodjs/core/ui/components.js'
84
- import { Button, Text } from '../../node_modules/@cmodjs/core/ui/elements.js'
91
+ import type { PageInit } from '../../node_modules/@cmodjs/core/mod.js'
92
+ import { Box, Button, Text } from '../../node_modules/@cmodjs/core/ui/elements.js'
85
93
 
86
- export const confirmPane = definePane<{ session: { deleting: boolean } }>({
87
- id: 'delete-branch',
88
- title: 'Delete branch',
89
- rows: 9,
90
- closeOnEscape: true,
91
- holdToasts: true,
94
+ export type NotesState = { global: { notes: readonly string[] } }
95
+
96
+ export const notesPage: PageInit<NotesState> = {
97
+ id: 'notes',
98
+ title: 'Notes',
92
99
  render: (mod) => (
93
- <Dialog
94
- title="Delete the branch feature/kit?"
95
- actions={[
96
- <Button variant="primary" autoFocus label="Keep it" onPress={() => void mod.ui.toast('Kept feature/kit')} />,
97
- <Button label="Delete" onPress={() => void (mod.state.session.deleting = true)} />,
98
- ]}
99
- >
100
- <Text dimColor wrap="wrap">Its 3 commits are not on main. Deleting it removes them from this machine.</Text>
101
- </Dialog>
100
+ <Box flexDirection="column" gap={1}>
101
+ {mod.state.global.notes.map((note) => <Text>{note}</Text>)}
102
+ <Button label="Clear" onPress={() => { mod.state.global.notes = [] }} />
103
+ </Box>
102
104
  ),
103
- })
105
+ }
104
106
  ```
105
107
 
106
- Open it with `mod.ui.pane(confirmPane).open({ focus: true })`. It draws as:
107
-
108
- ```text
109
- Widgets Delete branch ✕
110
- Delete the branch feature/kit?
111
-
112
- Its 3 commits are not on main. Deleting it removes them from
113
- this machine.
108
+ ```ts
109
+ type PageInit<State, Params = undefined> = {
110
+ readonly id: string
111
+ readonly title: string
112
+ render(mod: Mod<State>, props: { readonly params: Params; readonly isFocused: boolean; readonly columns: number }): RenderElement
113
+ }
114
114
 
115
- [ Keep it ] [ Delete ]
115
+ type Page<Params = undefined> = {
116
+ readonly id: string
117
+ readonly isOpen: boolean
118
+ readonly isShown: boolean
119
+ readonly isFocused: boolean
120
+ readonly params: Params | undefined
121
+ open(options?: { readonly params?: Params; readonly focus?: true }): Promise<void>
122
+ toggle(options?: { readonly focus?: true }): Promise<void>
123
+ close(): Promise<void>
124
+ scrollTo(target: 'start' | 'end' | { readonly key: string }, options?: { readonly block?: 'start' | 'center' | 'end' | 'nearest' }): Promise<void>
125
+ remove(): void
126
+ }
116
127
  ```
117
128
 
118
- A pane with `Tabs`, a `Tooltip`, a `Progress` beside its count, `Pagination`, and a `Toggle` with `Help` draws as:
119
-
120
- ```text
121
- 1: Controls 2: Split 3: Files
122
-
123
- 3 files read
124
-
125
- ██████████████░░░░░░░░░░ 7/12
129
+ - `id` is 1 to 64 letters, digits, `_`, or `-`, unique among the mod's pages. `title` labels the page's tab. Either one missing throws `<mod>: "<id>" is not a page id.` or `<mod>: the page "<id>" needs a title, the label its tab shows.`
130
+ - `mod.sidebar.add(page)` adds the page and returns it. Adding two pages with one id throws `<mod>: the page "<id>" is already added. Give each page its own id.`
131
+ - `open` shows the page as a tab of the Sidebar, and shows the Sidebar. `params` is what the page shows, such as a run's number, and the page reads it as `props.params`. A page whose `Params` is not `undefined` must be given `params`. `{ focus: true }` gives it the keyboard.
132
+ - `toggle` closes an open page and opens a closed one. `close` takes its tab away, and the Sidebar hides once no page is left.
133
+ - `isOpen` is true while the page has a tab, `isShown` while its tab is the one on screen, and `isFocused` while it has the keyboard.
134
+ - `scrollTo` brings an element the page drew with that `key` into view, or the page's start or end.
135
+ - `props.columns` is the width of the Sidebar.
136
+ - A page opened from what the person did, such as a slash command they typed, a hotkey, or a `Button` they pressed, shows at any terminal width. A page opened from anything else, such as `setup`, a timer, or a listener, shows only on a terminal 144 columns wide. Below that it waits until the person opens the Sidebar.
137
+ - A page draws only the elements of `ui/elements.js` and `ui/components.js`, text, numbers, and plain objects, because the cmod plugin draws it. A `Button`'s `onPress` runs in the mod.
138
+ - A `render` that throws draws nothing, and cmod shows the error toast `The <title> page could not draw` once, until the page draws again.
126
139
 
127
- p: Previous 2 of 5 n: Next
140
+ A hotkey is the usual way in:
128
141
 
129
- [x] Show hidden files ?
130
- ```
131
-
132
- With the pointer on `3 files read` and `Help` open, the two cards cover the rows above them:
133
-
134
- ```text
135
- 1: Controls 2: Split 3: Files
136
- Files Claude read this session
137
- 3 files read
142
+ ```tsx
143
+ import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
144
+ import { notesPage, type NotesState } from './pages/notes.js'
138
145
 
139
- ██████████████░░░░░░░░░░ 7/12
146
+ const initialState: NotesState = { global: { notes: [] } }
140
147
 
141
- p: Previous 2 of 5 n Hidden files start with a dot,
142
- such as .env.
143
- [x] Show hidden files ?
148
+ export const notes = defineMod({
149
+ name: 'notes',
150
+ state: initialState,
151
+ hotkeys: {
152
+ toggle: { title: 'Show or hide the notes', default: 'alt+shift+n' },
153
+ },
154
+ setup(mod) {
155
+ const page = mod.sidebar.add(notesPage)
156
+ mod.hotkeys.toggle.onPress(() => page.toggle({ focus: true }))
157
+ },
158
+ })
144
159
  ```
145
160
 
146
- and its Split tab, two `Panel`s in a `Split`, as:
147
-
148
- ```text
149
- 1: Controls 2: Split 3: Files
161
+ ### Settings pages
150
162
 
151
- ╭─────────────────────────────────╮ ╭────────────────────────────────╮
152
- │ Changed files │ │ src/mod.tsx │
153
- │ src/mod.tsx │ │ + mod.ui.pane(widgetsPane) │
154
- │ src/state.ts │ │ - mod.ui.pane(oldPane) │
155
- ╰─────────────────────────────────╯ ╰────────────────────────────────╯
156
- ```
163
+ `mod.settings.add(page)` adds a page to the mod's entry in Settings, beside the Options, Permissions, Hotkeys, and Changelog cmod draws for every mod. It takes the same `PageInit` and returns the same `Page`. `mod.settings.open()` shows Settings at the mod's entry. `/mods` and `meta+s` open Settings too.
157
164
 
158
165
  ## Panes
159
166
 
160
- A pane is a framed region the mod opens. Claude Code docks it beside the transcript in fullscreen, or shows it above the prompt.
161
-
162
- ### definePane
167
+ A pane is a framed region of the mod's own, outside the Sidebar. Claude Code docks it beside the transcript in fullscreen, or shows it above the prompt. Use a page unless the part needs its own size, its own Escape, or to hold back toasts, as a dialog does.
163
168
 
164
169
  ```ts
165
- definePane<State>(pane: Pane<State>): Pane<State>
166
-
167
- type Pane<State> = {
170
+ type PaneInit<State, Params = undefined> = {
168
171
  readonly id: string
169
172
  readonly title: string
170
173
  readonly columns?: number | ((state: State) => number | undefined)
171
174
  readonly rows?: number | ((state: State) => number | undefined)
172
175
  readonly closeOnEscape?: true
173
176
  readonly holdToasts?: true
174
- render(mod: Mod<State>, props: Frozen<RenderPropsOf['Pane']>): RenderElement
177
+ render(mod: Mod<State>, props: PaneProps<Params>): RenderElement
175
178
  onScroll?(mod: Mod<State>, e: Frozen<UiScrollInput>): void | Promise<void>
176
179
  onClose?(mod: Mod<State>, e: Frozen<PaneCloseInput>): void | Promise<void>
177
180
  }
178
181
  ```
179
182
 
180
- - `id` is 1 to 64 letters, digits, `_`, or `-`. Another id throws `definePane: "<id>" is not a pane id.`
183
+ `mod.panes.add(pane)` adds the pane and returns a `Pane`, with the same members as a `Page`.
184
+
185
+ - `id` is 1 to 64 letters, digits, `_`, or `-`. Another id throws `<mod>: "<id>" is not a pane id.`
181
186
  - `title` labels the pane's tab while more than one pane is open.
182
- - `columns` and `rows` ask for a size. A number must be a whole number above 0, or `definePane` throws. A function reads the state and returns the size, or `undefined` for Claude Code's default. When the state changes the size, cmod resizes an open pane. A function that returns a bad size keeps an open pane at its size and logs the pane's title to the debug log. `open`, and `toggle` on a closed pane, reject with that size instead.
183
- - `closeOnEscape` closes the pane when the person presses Escape while it holds the keyboard, as their own close does. Left out, Escape gives the keyboard back to the prompt and the pane stays.
187
+ - `columns` and `rows` ask for a size. A number must be a whole number above 0, or `mod.panes.add` throws. A function reads the state and returns the size, or `undefined` for Claude Code's default. When the state changes the size, cmod resizes an open pane.
188
+ - `closeOnEscape` closes the pane when the person presses Escape while it holds the keyboard. Left out, Escape gives the keyboard back to the prompt and the pane stays.
184
189
  - `holdToasts` holds every toast back while the pane is shown, the toasts of other plugins and of Claude Code included. Use it for a pane the person answers and closes, never for one that stays open.
185
- - `render` draws the pane's body. `props` holds `title`, `isFocused`, `bodyColumns`, `placement` (`'dock'` or `'inline'`), `scroll`, the body's window over a taller drawing, and `view`, which transcript is on screen beside the pane.
186
- - `onScroll` runs after this pane's window moved, by the person's wheel or keys or by `mod.ui.scroll`. `e` holds the new `offset`, the move's `by`, `bodyRows`, `contentRows`, and `origin`. Another pane's scroll and the band's never reach it, and a scroll Claude Code refused does not either.
187
- - `onClose` runs after the pane closed, by the person or by `close()`. `e.origin.kind` is `'person'` or `'plugin'`.
188
- - A handler that throws logs `<mod>: the <title> pane's <handler> threw: <error>`.
190
+ - `render` draws the pane's body. `props` holds `params`, `title`, `isFocused`, `bodyColumns`, `placement` (`'dock'` or `'inline'`), `scroll`, and `view`, which transcript is on screen beside the pane.
191
+ - `onScroll` runs after this pane's window moved. `onClose` runs after the pane closed, by the person or by `close()`, and `e.origin.kind` is `'person'` or `'plugin'`. A handler that throws shows the error toast `The <title> pane's <handler> threw`.
192
+ - `open` asks Claude Code to show the pane, and `isOpen` turns true once Claude Code places it. A pane opened from what the person did shows at any width, and one opened from anything else only at 144 columns, or 110 for a pane the person opened before.
193
+ - Adding two panes with one id throws `<mod>: the pane "<id>" is already added. Give each pane its own id.`
194
+ - A `render` that throws draws `The <title> pane could not draw: <error>` in the pane.
189
195
 
190
- Put each pane in its own file in `src/panes/`.
196
+ A pane with `closeOnEscape` and `holdToasts`, opened with `focus`, is a dialog:
191
197
 
192
198
  ```tsx
193
- import { definePane } from '../../node_modules/@cmodjs/core/ui/define-pane.js'
194
- import { Box, Button, Text } from '../../node_modules/@cmodjs/core/ui/elements.js'
195
-
196
- export type NotesState = { global: { notes: readonly string[] } }
199
+ import type { PaneInit } from '../../node_modules/@cmodjs/core/mod.js'
200
+ import { Dialog } from '../../node_modules/@cmodjs/core/ui/components.js'
201
+ import { Button, Text } from '../../node_modules/@cmodjs/core/ui/elements.js'
197
202
 
198
- export const notesPane = definePane<NotesState>({
199
- id: 'notes',
200
- title: 'Notes',
201
- columns: (state) => (state.global.notes.length > 10 ? 40 : 24),
203
+ export const confirmPane: PaneInit<{ session: { deleting: boolean } }> = {
204
+ id: 'delete-branch',
205
+ title: 'Delete branch',
206
+ rows: 9,
207
+ closeOnEscape: true,
208
+ holdToasts: true,
202
209
  render: (mod) => (
203
- <Box flexDirection="column" gap={1}>
204
- {mod.state.global.notes.map((note) => <Text>{note}</Text>)}
205
- <Button label="Clear" onPress={() => { mod.state.global.notes = [] }} />
206
- </Box>
210
+ <Dialog
211
+ title="Delete the branch feature/kit?"
212
+ actions={[
213
+ <Button variant="primary" autoFocus label="Keep it" onPress={() => void mod.toast('Kept feature/kit')} />,
214
+ <Button label="Delete" onPress={() => void (mod.state.session.deleting = true)} />,
215
+ ]}
216
+ >
217
+ <Text dimColor wrap="wrap">Its 3 commits are not on main. Deleting it removes them from this machine.</Text>
218
+ </Dialog>
207
219
  ),
208
- })
220
+ }
209
221
  ```
210
222
 
211
- ### mod.ui.pane
223
+ Open it with `mod.panes.add(confirmPane).open({ focus: true })`. It draws as:
212
224
 
213
- `mod.ui.pane(pane)` adds the pane and returns its handle:
225
+ ```text
226
+ Delete branch ✕
227
+ Delete the branch feature/kit?
228
+
229
+ Its 3 commits are not on main. Deleting it removes them from
230
+ this machine.
231
+
232
+ [ Keep it ] [ Delete ]
233
+ ```
234
+
235
+ ## Widgets
236
+
237
+ A widget is a small piece of a mod, such as the branch, a CI status, or a cost, that the person places. The mod says which areas a widget can draw in, and the person decides where it draws, in which row, on which side, or whether it draws at all.
214
238
 
215
239
  ```ts
216
- type PaneHandle = {
217
- open(options?: { readonly focus?: true }): Promise<void>
218
- close(): Promise<void>
219
- toggle(options?: { readonly focus?: true }): Promise<void>
220
- readonly isOpen: boolean
221
- readonly isShown: boolean
222
- readonly isFocused: boolean
240
+ type WidgetInit<State> = {
241
+ readonly id: string
242
+ readonly title: string
243
+ readonly areas: readonly [Area, ...Area[]]
244
+ render(mod: Mod<State>, context: WidgetContext): RenderElement | undefined
223
245
  }
246
+
247
+ type WidgetContext =
248
+ | { readonly area: typeof TurnEnd; readonly columns: number; readonly turn: Turn }
249
+ | { readonly area: Exclude<Area, typeof TurnEnd>; readonly columns: number }
250
+
251
+ type Widget = { readonly id: string; readonly area: Area | undefined; remove(): void }
224
252
  ```
225
253
 
226
- - `isShown` is true while the pane is the one Claude Code shows. Claude Code shows one pane at a time and the rest wait as tabs behind it.
227
- - `isFocused` is true while the person has given the pane the keyboard.
228
- - `open` asks Claude Code to show the pane. `isOpen` turns true once Claude Code places it. Claude Code may hold a pane back, and then `isOpen` stays false.
229
- - `{ focus: true }` asks Claude Code to give the pane the keyboard as it opens. Claude Code gives it only while the prompt is empty and holds the keyboard, so typed text, a dialog, or another pane the person is using keeps it. cmod sends `focus` only on that one open, never when it resizes the pane.
230
- - `mod.ui.pane` only adds the pane. cmod opens it only when the mod calls `open` or `toggle`, and Claude Code shows a pane only once the plugin has opened it. So give the person a way in, such as a slash command whose `reply` calls `pane.toggle()`.
231
- - A pane opened from what the person did, such as a slash command they typed or a `Button` they pressed, shows at any terminal width. A pane opened from anything else, such as `setup`, a timer, or `onSessionStart`, shows only on a terminal 144 columns wide, or 110 for a pane the person opened before. Below that it waits undrawn until the person opens it or the terminal widens.
232
- - `close` closes it. The person closes a pane too, and `isOpen` follows.
233
- - `toggle` closes an open pane and opens a closed one.
234
- - When the mod starts, `isOpen` is true for each of its panes Claude Code already shows.
235
- - Adding two panes with one id throws `<mod>: the pane "<id>" is already added. Give each pane its own id.`
236
- - A `render` that throws draws `The <title> pane could not draw: <error>` in the pane, and cmod logs `<mod>: the <title> pane render threw: <error>` once.
237
- - The line cmod logs when the mod first starts names the pane by its `title`, as in `the Notes pane`.
254
+ `mod.js` exports the five areas:
238
255
 
239
- ```tsx
240
- import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
241
- import { slashCommand } from '../node_modules/@cmodjs/core/jobs/slash-command.js'
242
- import { notesPane, type NotesState } from './panes/notes.js'
256
+ - `AbovePrompt` draws above the prompt, in the band where toasts draw. Nothing draws there while Claude Code shows a survey.
257
+ - `BelowPrompt` draws below the prompt, under Claude Code's hint line.
258
+ - `Working` draws under the spinner, while Claude works.
259
+ - `TurnEnd` draws under the line that closes each turn, in the transcript.
260
+ - `Dashboard` draws on the Sidebar's Dashboard tab.
243
261
 
244
- const initialState: NotesState = { global: { notes: [] } }
262
+ - `id` is 1 to 64 letters, digits, `_`, or `-`, unique in the mod. `title` names the widget in Settings, under cmod, Widgets.
263
+ - `areas` lists where the person may place the widget. A new widget first draws in the first row of its first area, on the left. `area` on the returned `Widget` is where the person placed it.
264
+ - `render` returns the widget's elements, or `undefined` to draw nothing now. `columns` is the width of the row it draws in.
265
+ - In `TurnEnd`, `context.turn` is the [turn](session.md#turn) that closed, so a widget there reads its answer, failure, or usage.
266
+ - A widget draws only the elements of `ui/elements.js` and `ui/components.js`, text, numbers, and plain objects. A `Button`'s `onPress` runs in the mod.
267
+ - A `render` that throws draws nothing, and cmod shows the error toast `The <title> widget could not draw` once, until the widget draws again. The widgets beside it still draw.
268
+ - Adding two widgets with one id throws `<mod>: the widget "<id>" is already added. Give each widget its own id.`
245
269
 
246
- export const notes = defineMod({
247
- name: 'notes',
248
- state: initialState,
249
- setup(mod) {
250
- const pane = mod.ui.pane(notesPane)
251
- mod.use(slashCommand({ name: 'notes', description: 'Show or hide the notes', reply: () => pane.toggle({ focus: true }) }))
252
- },
253
- })
270
+ ```tsx
271
+ import { BelowPrompt, Dashboard, type WidgetInit } from '../../node_modules/@cmodjs/core/mod.js'
272
+ import { Text } from '../../node_modules/@cmodjs/core/ui/elements.js'
273
+
274
+ export const promptsWidget: WidgetInit<{ session: { prompts: number } }> = {
275
+ id: 'prompts',
276
+ title: 'Prompt count',
277
+ areas: [BelowPrompt, Dashboard],
278
+ render: (mod) => <Text dimColor>{`Prompts: ${mod.state.session.prompts}`}</Text>,
279
+ }
254
280
  ```
255
281
 
256
- ## Places: change a row Claude Code draws
257
-
258
- `mod.ui.render(place, Component)` draws in place of one of Claude Code's own rows. `mod.js` exports each place as a constant. A message type is a place too, so `Reply` is where Claude Code draws a reply:
259
-
260
- | Place | What Claude Code draws there |
261
- | --- | --- |
262
- | `Reply` | One text block of Claude's reply |
263
- | `Prompt` | A prompt row: the person's prompt, a task notification, or another session's message |
264
- | `ToolCall` | A tool call's row |
265
- | `ToolResult` | A tool call's result |
266
- | `Notice` | A dim status line under the logo |
267
- | `ToolGroup` | The condensed row of a group of tool calls |
268
- | `ToolProgress` | A tool's progress line |
269
- | `CommandOutput` | A slash command's output |
270
- | `AskUserQuestion` | The dialog the AskUserQuestion tool opens |
271
- | `Spinner` | The spinner line while Claude works |
272
- | `TurnDuration` | The line that closes a turn |
273
- | `SessionMode` | The session's modes |
274
- | `PromptHint` | The prompt's hint line, such as `? for shortcuts` |
275
- | `AbovePrompt` | The band above the prompt, where surveys draw |
276
-
277
- `Component` gets `PlaceProps<P>`: the props Claude Code draws that row with, plus `Default`.
278
-
279
- - `Default` draws what Claude Code, and the renders of other mods, would draw. Pass it props to change what it draws, such as `<Default hint="…" />`. Return `<Default />` to change nothing.
280
- - A render adds to what it wraps. In `AbovePrompt`, drawing `<Default />` and a line under it keeps the lines of Claude Code and other mods.
281
- - A render must draw the same for the same props. One that calls `Default` a different number of times on a second draw falls back to Claude Code's drawing.
282
- - A render that throws, or draws differently on a second pass, falls back to Claude Code's drawing, and cmod shows the error toast `The <Place> render failed` once, with the reason under it.
283
- - A mod renders each place once. A second `mod.ui.render` of one place throws `<mod>: a render of <Place> is already added. Render each place once.`
284
- - `render` returns a `Disposable`. `dispose()` takes the render back, and Claude Code draws the row as before ([mod.md](mod.md#disposable)).
285
- - `ToolCall` changes a call's own row. A group of calls shows the `ToolGroup` row, which Claude Code builds from the stored message, so a mod that hides a tool's input also sets `isExpanded` on `ToolGroup` to unfold the group into `ToolCall` rows.
286
- - No place covers the permission dialog. `mod.ui.notice` adds a line under it instead ([Notices](#notices)).
282
+ ### Where the person places widgets
283
+
284
+ The person arranges every mod's widgets in Settings, under cmod, Widgets. They pick a widget, then move it with `h`, `j`, `k`, and `l`, or the arrow buttons, and hide or show it with `x`.
285
+
286
+ - Each area holds rows, and each row has a left side and a right side.
287
+ - Moving a widget down from the last row gives it a row of its own, and moving it down again, or up from the first row, takes it to the next area it can draw in.
288
+ - A hidden widget keeps its place and draws nothing.
289
+ - Claude Code's own hint and mode labels, below the prompt, show there too. The person can hide them, and never move them.
290
+ - The layout is one file for every session, `widgets.json` in the cmod store, so every running session follows a change at once.
291
+
292
+ ## Styles: change how Claude Code draws
293
+
294
+ `mod.styles.add({ kind, render })` draws in place of one kind of row Claude Code draws, or one kind of markdown block in Claude's replies. It returns a `Style` with `remove()`, after which Claude Code draws that kind as before.
295
+
296
+ The rows a style changes:
297
+
298
+ - `Prompt`: a prompt row, the person's prompt, a task notification, or another session's message.
299
+ - `Reply`: one text block of Claude's reply.
300
+ - `ToolCall`: a tool call's row.
301
+ - `ToolResult`: a tool call's result.
302
+ - `Notice`: a dim status line under the logo.
303
+ - `ToolGroup`: the condensed row of a group of tool calls.
304
+ - `ToolProgress`: a tool's progress line.
305
+ - `CommandOutput`: a slash command's output.
306
+ - `AskUserQuestion`: the dialog the AskUserQuestion tool opens.
307
+ - `Working`: the spinner line while Claude works.
308
+ - `TurnEnd`: the line that closes a turn.
309
+
310
+ `render` gets the mod and `StyleProps<K>`: the props Claude Code draws that row with, plus `Default`.
311
+
312
+ - `Default` draws what Claude Code, and the styles of other mods, would draw. Pass it props to change what it draws, such as `<Default word="Baked" />`. Return `<Default />` to change nothing.
313
+ - A style adds to what it wraps. In `Working`, drawing `<Default />` and a line under it keeps the lines of Claude Code and other mods.
314
+ - A style must draw the same for the same props. One that throws, or calls `Default` a different number of times on a second draw, falls back to Claude Code's drawing, and cmod shows the error toast `The <Kind> style failed` once, with the reason under it.
315
+ - A mod adds one style per kind. A second throws `<mod>: the style of <Kind> is already added. Add one style per kind.`
316
+ - `ToolCall` changes a call's own row. A group of calls shows the `ToolGroup` row, so a mod that hides a tool's input also sets `isExpanded` on `ToolGroup` to unfold the group into `ToolCall` rows.
287
317
 
288
318
  ```tsx
289
- import { AbovePrompt, defineMod, PromptHint, sessions } from '../node_modules/@cmodjs/core/mod.js'
319
+ import { defineMod, sessions, TurnEnd } from '../node_modules/@cmodjs/core/mod.js'
290
320
  import { Box, Text } from '../node_modules/@cmodjs/core/ui/elements.js'
291
321
 
292
- export const hints = defineMod({
293
- name: 'hints',
294
- state: { session: { prompts: 0 } },
322
+ export const turns = defineMod({
323
+ name: 'turns',
324
+ state: { session: { turns: 0 } },
295
325
  setup(mod) {
296
- mod.ui.render(PromptHint, ({ hint, Default }) => <Default hint={hint.toUpperCase()} />)
297
- mod.ui.render(AbovePrompt, ({ hasSurvey, Default }) =>
298
- hasSurvey ? <Default /> : (
326
+ mod.styles.add({
327
+ kind: TurnEnd,
328
+ render: (current, { Default }) => (
299
329
  <Box flexDirection="column">
300
330
  <Default />
301
- <Text dimColor>{`Prompts: ${mod.state.session.prompts}`}</Text>
331
+ <Text dimColor>{`Turn ${current.state.session.turns}`}</Text>
302
332
  </Box>
303
- ))
304
- sessions.current.onPrompt(() => {
305
- mod.state.session.prompts += 1
333
+ ),
334
+ })
335
+ sessions.current.onTurnEnd(() => {
336
+ mod.state.session.turns += 1
306
337
  })
307
338
  },
308
339
  })
309
340
  ```
310
341
 
311
- `Place` is the type of every place, and `PlaceProps` the props of one. Type a component's props as `PlaceProps<typeof PromptHint>`.
342
+ ### Markdown blocks
312
343
 
313
- ## Markdown places: change the blocks of a reply
344
+ Each kind of markdown block of a reply is a kind of its own. `ui/markdown.js` exports them, apart from the rest, because it brings the markdown reader, about 230 KB that takes some 18 ms to load. A mod that imports none of them starts without it.
314
345
 
315
- Each markdown block of a reply is a place of its own. `ui/markdown.js` exports these places, apart from the rest, because it brings the markdown reader, about 230 KB that takes some 18 ms to load. A mod that imports none of them starts without it.
346
+ - `Heading`: a heading, with `depth`, 1 to 6.
347
+ - `Paragraph`: a paragraph.
348
+ - `CodeBlock`: a code block, with `lang`, `meta`, and `value`.
349
+ - `BlockQuote`: a block quote.
350
+ - `List`: a list, with `ordered`, `start`, and `spread`.
351
+ - `Table`: a table, with `align`.
352
+ - `ThematicBreak`: a `---` line.
353
+ - `HtmlBlock`: an HTML block, with `value`.
316
354
 
317
- | Place | Block | Props beyond `text` and `source` |
318
- | --- | --- | --- |
319
- | `Heading` | A heading | `depth`, 1 to 6 |
320
- | `Paragraph` | A paragraph | none |
321
- | `CodeBlock` | A code block | `lang`, `meta`, `value` |
322
- | `BlockQuote` | A block quote | none |
323
- | `List` | A list | `ordered`, `start`, `spread` |
324
- | `Table` | A table | `align` |
325
- | `ThematicBreak` | A `---` line | none |
326
- | `HtmlBlock` | An HTML block | `value` |
327
-
328
- - `text` is the block's plain text, one line per list item, table row, or quoted line. `source` is the block's markdown.
329
- - `Default` draws the block as Claude Code would. `<Default source="…" />` draws other markdown in its place.
330
- - A markdown render draws in Claude's replies and in every `Markdown` element of the mod's panes.
331
- - While a reply streams in, Claude Code draws a block that is not finished yet. The mod draws it once it is.
332
- - A render that throws falls back to Claude Code's drawing of the block, and shows the error toast once.
355
+ Every block's props hold `text`, its plain text, one line per list item, table row, or quoted line, and `source`, its markdown. `Default` draws the block as Claude Code would, and `<Default source="…" />` draws other markdown in its place. A markdown style draws in Claude's replies and in every `Markdown` element of the mod's pages and panes. While a reply streams in, Claude Code draws a block that is not finished yet, and the mod draws it once it is.
333
356
 
334
357
  ```tsx
335
358
  import type { RenderElement } from 'claude-code'
336
- import { defineMod, type PlaceProps } from '../node_modules/@cmodjs/core/mod.js'
359
+ import { defineMod, type StyleProps } from '../node_modules/@cmodjs/core/mod.js'
337
360
  import { CodeBlock } from '../node_modules/@cmodjs/core/ui/markdown.js'
338
361
  import { Text } from '../node_modules/@cmodjs/core/ui/elements.js'
339
362
 
340
- function MermaidBlock({ lang, value, Default }: PlaceProps<typeof CodeBlock>): RenderElement {
363
+ function MermaidBlock({ lang, value, Default }: StyleProps<typeof CodeBlock>): RenderElement {
341
364
  return lang === 'mermaid' ? <Text>{`diagram: ${value}`}</Text> : <Default />
342
365
  }
343
366
 
344
367
  export const diagrams = defineMod({
345
368
  name: 'diagrams',
346
369
  setup(mod) {
347
- mod.ui.render(CodeBlock, MermaidBlock)
370
+ mod.styles.add({ kind: CodeBlock, render: (_mod, props) => <MermaidBlock {...props} /> })
348
371
  },
349
372
  })
350
373
  ```
351
374
 
352
- ```ts
353
- markdownBlocks<P extends MarkdownPlace>(text: string, place: P): Omit<PlaceProps<P>, 'Default'>[]
354
- ```
355
-
356
- `markdownBlocks`, exported from `ui/markdown.js`, returns the props of every block at one markdown place in a markdown text, such as every code block of a reply. A mod reads the blocks of a reply an `onReply` listener got with it, and a test calls a component with real props.
375
+ `markdownBlocks(text, kind)`, from `ui/markdown.js`, returns the props of every block of one kind in a markdown text, such as every code block of a reply:
357
376
 
358
377
  ```ts
359
378
  import { CodeBlock, markdownBlocks } from '../node_modules/@cmodjs/core/ui/markdown.js'
@@ -361,23 +380,15 @@ import { CodeBlock, markdownBlocks } from '../node_modules/@cmodjs/core/ui/markd
361
380
  const diagrams = markdownBlocks(reply, CodeBlock).filter((block) => block.lang === 'mermaid')
362
381
  ```
363
382
 
364
- ## Notices
365
-
366
- ```ts
367
- mod.ui.notice(callId: string, text: string | undefined): void
368
- ```
369
-
370
- `notice` shows `text` as one line under the permission dialog that is open for the tool call `callId`, the call's `tool_use_id`, such as `checked by safe-delete`. `undefined` removes the line, and Claude Code removes it on its own once the call is decided. A call with no open dialog takes no line.
371
-
372
383
  ## Toasts
373
384
 
374
- `mod.ui.toast` shows the person a short message in the band above the prompt, one row per toast, under the mod's name. Its shape follows [Sonner](https://sonner.emilkowal.ski/toast):
385
+ `mod.toast` shows the person a short message in the band above the prompt, one row per toast, under the mod's name. Its shape follows [Sonner](https://sonner.emilkowal.ski/toast):
375
386
 
376
387
  ```ts
377
- mod.ui.toast(message: string, options?: ToastOptions): ToastId
378
- mod.ui.toast.success(message, options?) / .info / .warning / .error / .loading
379
- mod.ui.toast.promise<T>(task: Promise<T> | (() => Promise<T>), messages: { loading: string; success: string | ((value: T) => string); error: string | ((error: unknown) => string) }): { unwrap(): Promise<T> }
380
- mod.ui.toast.dismiss(id?: ToastId): void
388
+ mod.toast(message: string, options?: ToastOptions): ToastId
389
+ mod.toast.success(message, options?) / .info / .warning / .error / .loading
390
+ mod.toast.promise<T>(task: Promise<T> | (() => Promise<T>), messages: { loading: string; success: string | ((value: T) => string); error: string | ((error: unknown) => string) }): { unwrap(): Promise<T> }
391
+ mod.toast.dismiss(id?: ToastId): void
381
392
 
382
393
  type ToastOptions = {
383
394
  id?: ToastId
@@ -388,39 +399,26 @@ type ToastOptions = {
388
399
  ```
389
400
 
390
401
  - Each type draws its icon: `✓` success, `ℹ` info, `⚠` warning, `✗` error, and a spinner for loading.
391
- - A toast stays 4 seconds once the band draws it, or `duration` ms. `Infinity` keeps it until the person closes it, and a loading toast stays until it changes. The time waits while Claude Code shows a survey over the band.
402
+ - A toast stays 4 seconds once the band draws it, or `duration` ms. `Infinity` keeps it until the person closes it, and a loading toast stays until it changes.
392
403
  - A toast with the `id` of a shown toast replaces it in place, so a loading toast turns into its success or error.
393
- - A text `description` shows its first line dim under the message. A press on the message unfolds the whole text and stops the time, and a second press folds it. An element `description`, such as a `Progress`, draws as it is.
394
- - `action` draws a button after the message. A press that throws shows an error toast with the cause.
404
+ - A text `description` shows its first line dim under the message. A press on the message unfolds the whole text and stops the time. An element `description`, such as a `Progress`, draws as it is.
405
+ - `action` draws a button after the message. Give every toast that asks the person to do something an `action` that does it.
395
406
  - `✕` closes the toast. The band shows the newest 3, and `+N more` shows every toast.
396
407
  - `promise` shows `loading` while `task` runs, then `success` or `error` in the same row. `unwrap()` resolves or rejects as `task` does.
397
408
 
398
- ```ts
399
- import { defineMod, fs, sessions } from '../node_modules/@cmodjs/core/mod.js'
400
-
401
- export const indexer = defineMod({
402
- name: 'indexer',
403
- setup(mod) {
404
- sessions.current.onSessionStart(async () => {
405
- const indexed = fs.list(sessions.current.project).then((entries) => entries.length)
406
- mod.ui.toast.promise(indexed, { loading: 'Indexing files', success: (count) => `Indexed ${count} files`, error: 'Indexing failed' })
407
- })
408
- },
409
- })
410
- ```
411
-
412
409
  An error cmod catches for a mod, such as a listener that throws, shows as an error toast that stays, with its buttons on a line under the message:
413
410
 
414
- - `Retry` runs the failed step again, where running it again can work: an install, the mod's setup, or a program that stopped. The toast stays where it is, and the step's progress shows in place of its description, or of its buttons when it has none, until the try ends.
415
- - `Report` replaces the toast's buttons with the ways to tell the mod's author, with the keys on the first, and `Back` returns to the buttons. The same failure happening again keeps them open. `Ask Claude to report it` sends this session a prompt to file the report with the person's own tools, and Claude Code asks before each step. `Report it on GitHub (opens your browser)` opens a filled-in issue on the mod's `supportUrl` or `repository` in its `plugin.json`, or `Email the author (opens your mail app)` writes to its `author.email`. `Copy the error` copies the mod's name and version, the `@cmodjs/core` version, the message, the whole cause, and the stack.
411
+ - `Settings` opens the mod's Permissions in Settings, when the failure came from a permission the person turned off.
412
+ - `Retry` runs the failed step again, where running it again can work: an install, the mod's setup, or a program that stopped.
413
+ - `Report` replaces the toast's buttons with the ways to tell the mod's author. `Ask Claude to report it` sends this session a prompt to file the report with the person's own tools. `Report it on GitHub (opens your browser)` opens a filled-in issue on the mod's `supportUrl` or `repository` in its `plugin.json`, or `Email the author (opens your mail app)` writes to its `author.email`. `Copy the error` copies the mod's name and version, the `@cmodjs/core` version, the message, the whole cause, and the stack.
416
414
  - `Hide` closes the toast and keeps the same failure hidden in later sessions. A new version of the mod, or a different failure, shows again.
417
415
 
418
- `mod.ui.copy(text)` puts `text` on the person's clipboard. It rejects with Claude Code's reason when the terminal cannot take it.
416
+ `clipboard.write(text)`, from `mod.js`, puts `text` on the person's clipboard. It rejects with Claude Code's reason when the terminal cannot take it.
419
417
 
420
418
  ## Questions
421
419
 
422
420
  ```ts
423
- mod.ui.ask(question: string, options?: readonly string[] | AskOptions): Promise<string>
421
+ mod.ask(question: string, options?: readonly string[] | AskOptions): Promise<string>
424
422
 
425
423
  type AskOptions = { options?: readonly string[]; header?: string; multiSelect?: true }
426
424
  ```
@@ -434,31 +432,25 @@ export const committer = defineMod({
434
432
  name: 'committer',
435
433
  setup(mod) {
436
434
  sessions.current.onTurnEnd(async () => {
437
- const answer = await mod.ui.ask('Commit the changes?', { options: ['Commit', 'Wait'], header: 'Commit' })
435
+ const answer = await mod.ask('Commit the changes?', { options: ['Commit', 'Wait'], header: 'Commit' })
438
436
  if (answer === 'Commit') await process.run(['git', 'commit', '-am', 'wip'], { cwd: sessions.current.project })
439
437
  })
440
438
  },
441
439
  })
442
440
  ```
443
441
 
444
- ## Scrolling
445
-
446
- ```ts
447
- mod.ui.scroll(args: { to: 'start' | 'end' | { key: string } | { requestId: string }; in?: string; block?: 'start' | 'center' | 'end' | 'nearest' }): Promise<UiScrollResult>
448
- ```
442
+ ## The conversation's rows
449
443
 
450
- `scroll` brings something the mod drew into view. `{ key }` is an element the mod drew with that `key`. `'start'` and `'end'` are the top and bottom of the pane whose id `in` names, and `'end'` keeps up with rows the mod adds until the person scrolls. `{ requestId }` is a row Claude Code drew, such as a message of the transcript, which scrolls only while the mod answers the person's own press or key. `block` places the target in the window, `'nearest'` by default. It resolves `{}` once the window moved, or `{ deny }` with the reason it did not.
444
+ Two members of `sessions.current` reach the rows Claude Code draws for the conversation ([session.md](session.md)):
451
445
 
452
- ```tsx
453
- <Button label="Latest" onPress={() => mod.ui.scroll({ to: 'end', in: 'log' })} />
446
+ ```ts
447
+ sessions.current.scrollTo(message: Message, options?: { block?: 'start' | 'center' | 'end' | 'nearest' }): Promise<void>
448
+ sessions.current.caption(toolCall: ToolCall message, text: string | undefined): void
454
449
  ```
455
450
 
456
- A [message](session.md#message)'s `id` names the row Claude Code draws it under, so a history pane brings the conversation to the message a person presses. When one row holds several messages, such as a reply with its tool calls, each `id` ends in `:<n>`, and the row is the `id` before it:
457
-
458
- ```tsx
459
- <Button label={message.text} onPress={() => mod.ui.scroll({ to: { requestId: message.id.split(':')[0] } })} />
460
- ```
451
+ - `scrollTo` brings a [message](session.md#message)'s row into view, such as the message a person pressed in a history page. It scrolls only while the mod answers the person's own press or key.
452
+ - `caption` shows `text` as one line under the permission dialog that is open for that tool call, such as `checked by safe-delete`. `undefined` removes the line, and Claude Code removes it on its own once the call is decided.
461
453
 
462
454
  ## Test the UI
463
455
 
464
- `tested.lines(paneId)` reads an open pane as rows of text. `tested.lines(place, props)` draws a place render. `tested.press(paneId, key)` presses a button. `tested.shown.toasts` lists the toasts shown now, each as `{ type, message, description? }`. [testing.md](testing.md) covers each.
456
+ `tested.shown.page` and `tested.shown.pane` read the page or pane the mod shows now, with `lines()`, `press(key)`, `input(key, text)`, and `select(key, value)`. `tested.lines(area)` draws the mod's widgets in an area, and `tested.lines(kind, props)` draws a style. `tested.hotkey(name)` presses a hotkey. `tested.shown.toasts` lists the toasts shown now, each as `{ type, message, description? }`. [testing.md](testing.md) covers each.