@cmodjs/core 3.1.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.
- package/docs/commands.md +38 -21
- package/docs/dependencies.md +27 -20
- package/docs/index.md +21 -17
- package/docs/install-steps.md +37 -29
- package/docs/jobs.md +25 -52
- package/docs/mod.md +93 -65
- package/docs/options.md +2 -2
- package/docs/permissions.md +4 -4
- package/docs/session.md +14 -12
- package/docs/testing.md +55 -32
- package/docs/ui.md +255 -263
- package/hotkeys.d.ts +22 -0
- package/hotkeys.d.ts.map +1 -0
- package/hotkeys.js +26 -0
- package/installer.d.ts +1 -1
- package/installer.d.ts.map +1 -1
- package/installer.js +11 -6
- package/jobs/check.d.ts +2 -1
- package/jobs/check.d.ts.map +1 -1
- package/jobs/check.js +2 -2
- package/jobs/context.d.ts +2 -1
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +1 -1
- package/jobs/permissions.d.ts +2 -1
- package/jobs/permissions.d.ts.map +1 -1
- package/jobs/permissions.js +2 -2
- package/jobs/program.js +1 -1
- package/jobs/prompt.d.ts +2 -1
- package/jobs/prompt.d.ts.map +1 -1
- package/jobs/prompt.js +20 -13
- package/jobs/schedule.d.ts +2 -1
- package/jobs/schedule.d.ts.map +1 -1
- package/jobs/schedule.js +15 -1
- package/jobs/slash-command.d.ts +6 -2
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +6 -5
- package/jobs/tool.d.ts +6 -4
- package/jobs/tool.d.ts.map +1 -1
- package/jobs/tool.js +3 -3
- package/mod.d.ts +63 -42
- package/mod.d.ts.map +1 -1
- package/mod.js +11 -1
- package/package.json +1 -1
- package/records.d.ts +13 -8
- package/records.d.ts.map +1 -1
- package/records.js +41 -31
- package/register.d.ts.map +1 -1
- package/register.js +1 -2
- package/runtime/announce.d.ts +1 -1
- package/runtime/announce.d.ts.map +1 -1
- package/runtime/announce.js +6 -7
- package/runtime/before.d.ts +2 -2
- package/runtime/before.d.ts.map +1 -1
- package/runtime/before.js +1 -1
- package/runtime/claude.d.ts +0 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +4 -1
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +42 -14
- package/runtime/dependencies.d.ts.map +1 -1
- package/runtime/dependencies.js +2 -1
- package/runtime/engine.d.ts +5 -0
- package/runtime/engine.d.ts.map +1 -1
- package/runtime/grants.js +2 -2
- package/runtime/hooks.d.ts +1 -1
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hotkeys.d.ts +15 -0
- package/runtime/hotkeys.d.ts.map +1 -0
- package/runtime/hotkeys.js +58 -0
- package/runtime/installer.d.ts +5 -2
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +23 -12
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +190 -141
- package/runtime/options.js +2 -2
- package/runtime/pages.d.ts +28 -0
- package/runtime/pages.d.ts.map +1 -0
- package/runtime/pages.js +98 -0
- package/runtime/parts.d.ts +14 -0
- package/runtime/parts.d.ts.map +1 -0
- package/runtime/parts.js +63 -0
- package/runtime/permission-check.d.ts +2 -2
- package/runtime/permission-check.d.ts.map +1 -1
- package/runtime/programs.js +1 -1
- package/runtime/router.d.ts +2 -2
- package/runtime/router.d.ts.map +1 -1
- package/runtime/router.js +1 -1
- package/runtime/sessions.d.ts +10 -9
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +14 -5
- package/runtime/state.d.ts +2 -1
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +1 -0
- package/runtime/toaster.d.ts +1 -1
- package/runtime/toaster.d.ts.map +1 -1
- package/runtime/toaster.js +10 -4
- package/runtime/ui.d.ts +87 -5
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +372 -109
- package/runtime/watches.d.ts +9 -0
- package/runtime/watches.d.ts.map +1 -0
- package/runtime/watches.js +73 -0
- package/runtime/widgets.d.ts +48 -0
- package/runtime/widgets.d.ts.map +1 -0
- package/runtime/widgets.js +219 -0
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +7 -2
- package/session/session.d.ts +11 -5
- package/session/session.d.ts.map +1 -1
- package/src/hotkeys.ts +51 -0
- package/src/installer.ts +8 -4
- package/src/jobs/check.ts +4 -3
- package/src/jobs/context.ts +3 -2
- package/src/jobs/permissions.ts +4 -3
- package/src/jobs/program.ts +1 -1
- package/src/jobs/prompt.ts +23 -14
- package/src/jobs/schedule.ts +16 -2
- package/src/jobs/slash-command.ts +10 -7
- package/src/jobs/tool.ts +7 -5
- package/src/mod.ts +59 -47
- package/src/records.ts +46 -31
- package/src/register.ts +2 -3
- package/src/runtime/announce.ts +6 -7
- package/src/runtime/before.ts +4 -4
- package/src/runtime/claude.ts +0 -1
- package/src/runtime/conversations.ts +66 -20
- package/src/runtime/dependencies.ts +2 -1
- package/src/runtime/engine.ts +5 -0
- package/src/runtime/grants.ts +2 -2
- package/src/runtime/hooks.ts +1 -0
- package/src/runtime/hotkeys.ts +80 -0
- package/src/runtime/installer.ts +24 -12
- package/src/runtime/lifecycle.ts +189 -136
- package/src/runtime/options.ts +2 -2
- package/src/runtime/pages.ts +127 -0
- package/src/runtime/parts.ts +72 -0
- package/src/runtime/permission-check.ts +2 -2
- package/src/runtime/programs.ts +1 -1
- package/src/runtime/router.ts +3 -3
- package/src/runtime/sessions.ts +24 -15
- package/src/runtime/state.ts +3 -1
- package/src/runtime/toaster.ts +10 -4
- package/src/runtime/ui.ts +405 -115
- package/src/runtime/watches.ts +75 -0
- package/src/runtime/widgets.ts +287 -0
- package/src/session/context/turn/message/message.ts +7 -2
- package/src/session/session.ts +10 -6
- package/src/testing/fake-claude.ts +9 -6
- package/src/testing/fake-elements.ts +7 -4
- package/src/testing/fake-sessions.ts +6 -4
- package/src/testing.ts +203 -46
- package/src/ui/areas.ts +27 -0
- package/src/ui/elements.ts +35 -0
- package/src/ui/markdown.ts +18 -18
- package/src/ui/panes.ts +73 -0
- package/src/ui/styles.ts +53 -0
- package/src/utils/subscription.ts +1 -0
- package/testing/fake-claude.d.ts +0 -1
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +9 -5
- package/testing/fake-elements.js +7 -4
- package/testing/fake-sessions.d.ts +2 -1
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +1 -0
- package/testing.d.ts +21 -6
- package/testing.d.ts.map +1 -1
- package/testing.js +206 -45
- package/ui/areas.d.ts +29 -0
- package/ui/areas.d.ts.map +1 -0
- package/ui/areas.js +6 -0
- package/ui/elements.d.ts +3 -0
- package/ui/elements.d.ts.map +1 -1
- package/ui/elements.js +36 -0
- package/ui/markdown.d.ts +14 -14
- package/ui/markdown.d.ts.map +1 -1
- package/ui/markdown.js +11 -11
- package/ui/panes.d.ts +71 -0
- package/ui/panes.d.ts.map +1 -0
- package/ui/panes.js +16 -0
- package/ui/styles.d.ts +44 -0
- package/ui/styles.d.ts.map +1 -0
- package/ui/styles.js +24 -0
- package/utils/subscription.d.ts +4 -0
- package/utils/subscription.d.ts.map +1 -0
- package/jobs/status-line.d.ts +0 -6
- package/jobs/status-line.d.ts.map +0 -1
- package/jobs/status-line.js +0 -39
- package/src/jobs/status-line.ts +0 -44
- package/src/ui/define-pane.ts +0 -27
- package/src/ui/places.ts +0 -49
- package/src/utils/disposable.ts +0 -3
- package/ui/define-pane.d.ts +0 -15
- package/ui/define-pane.d.ts.map +0 -1
- package/ui/define-pane.js +0 -12
- package/ui/places.d.ts +0 -43
- package/ui/places.d.ts.map +0 -1
- package/ui/places.js +0 -31
- package/utils/disposable.d.ts +0 -4
- package/utils/disposable.d.ts.map +0 -1
- /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
|
|
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.
|
|
7
|
-
mod.
|
|
8
|
-
mod.
|
|
9
|
-
mod.
|
|
10
|
-
mod.
|
|
11
|
-
mod.
|
|
12
|
-
mod.
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
83
|
-
import {
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
140
|
+
A hotkey is the usual way in:
|
|
128
141
|
|
|
129
|
-
|
|
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
|
-
|
|
146
|
+
const initialState: NotesState = { global: { notes: [] } }
|
|
140
147
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 `
|
|
183
|
-
- `closeOnEscape` closes the pane when the person presses Escape while it holds the keyboard
|
|
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`,
|
|
186
|
-
- `onScroll` runs after this pane's window moved
|
|
187
|
-
- `
|
|
188
|
-
-
|
|
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
|
-
|
|
196
|
+
A pane with `closeOnEscape` and `holdToasts`, opened with `focus`, is a dialog:
|
|
191
197
|
|
|
192
198
|
```tsx
|
|
193
|
-
import {
|
|
194
|
-
import {
|
|
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
|
|
199
|
-
id: '
|
|
200
|
-
title: '
|
|
201
|
-
|
|
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
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
223
|
+
Open it with `mod.panes.add(confirmPane).open({ focus: true })`. It draws as:
|
|
212
224
|
|
|
213
|
-
|
|
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
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
- `
|
|
280
|
-
-
|
|
281
|
-
-
|
|
282
|
-
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
-
|
|
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 {
|
|
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
|
|
293
|
-
name: '
|
|
294
|
-
state: { session: {
|
|
322
|
+
export const turns = defineMod({
|
|
323
|
+
name: 'turns',
|
|
324
|
+
state: { session: { turns: 0 } },
|
|
295
325
|
setup(mod) {
|
|
296
|
-
mod.
|
|
297
|
-
|
|
298
|
-
|
|
326
|
+
mod.styles.add({
|
|
327
|
+
kind: TurnEnd,
|
|
328
|
+
render: (current, { Default }) => (
|
|
299
329
|
<Box flexDirection="column">
|
|
300
330
|
<Default />
|
|
301
|
-
<Text dimColor>{`
|
|
331
|
+
<Text dimColor>{`Turn ${current.state.session.turns}`}</Text>
|
|
302
332
|
</Box>
|
|
303
|
-
)
|
|
304
|
-
|
|
305
|
-
|
|
333
|
+
),
|
|
334
|
+
})
|
|
335
|
+
sessions.current.onTurnEnd(() => {
|
|
336
|
+
mod.state.session.turns += 1
|
|
306
337
|
})
|
|
307
338
|
},
|
|
308
339
|
})
|
|
309
340
|
```
|
|
310
341
|
|
|
311
|
-
|
|
342
|
+
### Markdown blocks
|
|
312
343
|
|
|
313
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 }:
|
|
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.
|
|
370
|
+
mod.styles.add({ kind: CodeBlock, render: (_mod, props) => <MermaidBlock {...props} /> })
|
|
348
371
|
},
|
|
349
372
|
})
|
|
350
373
|
```
|
|
351
374
|
|
|
352
|
-
|
|
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.
|
|
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.
|
|
378
|
-
mod.
|
|
379
|
-
mod.
|
|
380
|
-
mod.
|
|
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.
|
|
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
|
|
394
|
-
- `action` draws a button after the message.
|
|
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
|
-
- `
|
|
415
|
-
- `
|
|
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
|
-
`
|
|
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.
|
|
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.
|
|
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
|
-
##
|
|
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
|
-
|
|
444
|
+
Two members of `sessions.current` reach the rows Claude Code draws for the conversation ([session.md](session.md)):
|
|
451
445
|
|
|
452
|
-
```
|
|
453
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|