@cmodjs/core 4.0.2 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/commands.md +43 -8
- package/docs/index.md +3 -3
- package/docs/install-steps.md +6 -6
- package/docs/jobs.md +2 -1
- package/docs/mod.md +1 -1
- package/docs/options.md +1 -1
- package/docs/permissions.md +1 -1
- package/docs/session.md +44 -13
- package/docs/testing.md +14 -10
- package/docs/ui.md +43 -11
- package/installer.d.ts +3 -2
- package/installer.d.ts.map +1 -1
- package/jobs/program.d.ts +1 -0
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +39 -23
- package/mod.d.ts +3 -2
- package/mod.d.ts.map +1 -1
- package/mod.js +1 -0
- package/package.json +1 -1
- package/records.d.ts +15 -1
- package/records.d.ts.map +1 -1
- package/records.js +39 -1
- package/register.d.ts.map +1 -1
- package/register.js +5 -2
- package/runtime/before.d.ts +6 -2
- package/runtime/before.d.ts.map +1 -1
- package/runtime/before.js +56 -18
- package/runtime/buttons.d.ts +10 -0
- package/runtime/buttons.d.ts.map +1 -0
- package/runtime/buttons.js +12 -0
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +134 -25
- package/runtime/engine.d.ts +2 -0
- package/runtime/engine.d.ts.map +1 -1
- package/runtime/grants.d.ts +1 -0
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +5 -2
- package/runtime/hotkeys.d.ts.map +1 -1
- package/runtime/hotkeys.js +7 -4
- package/runtime/installer.d.ts +9 -6
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +104 -47
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +121 -40
- package/runtime/pages.d.ts +3 -1
- package/runtime/pages.d.ts.map +1 -1
- package/runtime/pages.js +34 -10
- package/runtime/parts.d.ts +1 -0
- package/runtime/parts.d.ts.map +1 -1
- package/runtime/parts.js +6 -3
- package/runtime/programs.d.ts +1 -0
- package/runtime/programs.d.ts.map +1 -1
- package/runtime/programs.js +4 -1
- package/runtime/sessions.d.ts +2 -2
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +88 -58
- package/runtime/toaster.d.ts.map +1 -1
- package/runtime/toaster.js +23 -9
- package/runtime/ui.d.ts +21 -4
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +158 -58
- package/runtime/watches.d.ts.map +1 -1
- package/runtime/watches.js +18 -4
- package/runtime/widgets.d.ts +7 -5
- package/runtime/widgets.d.ts.map +1 -1
- package/runtime/widgets.js +29 -20
- package/session/context/turn/message/attached-file.d.ts +13 -0
- package/session/context/turn/message/attached-file.d.ts.map +1 -0
- package/session/context/turn/message/attached-file.js +21 -0
- package/session/context/turn/message/message.d.ts +3 -2
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +20 -6
- package/session/conversation.d.ts +14 -3
- package/session/conversation.d.ts.map +1 -1
- package/session/conversation.js +23 -4
- package/session/goal/task.d.ts +5 -0
- package/session/goal/task.d.ts.map +1 -1
- package/session/session.d.ts +35 -7
- package/session/session.d.ts.map +1 -1
- package/session/session.js +2 -1
- package/src/installer.ts +3 -2
- package/src/jobs/program.ts +41 -21
- package/src/mod.ts +3 -2
- package/src/records.ts +49 -1
- package/src/register.ts +9 -6
- package/src/runtime/before.ts +61 -22
- package/src/runtime/buttons.ts +22 -0
- package/src/runtime/conversations.ts +138 -32
- package/src/runtime/engine.ts +2 -0
- package/src/runtime/grants.ts +6 -2
- package/src/runtime/hotkeys.ts +7 -4
- package/src/runtime/installer.ts +144 -82
- package/src/runtime/lifecycle.ts +128 -43
- package/src/runtime/pages.ts +45 -8
- package/src/runtime/parts.ts +8 -4
- package/src/runtime/programs.ts +5 -1
- package/src/runtime/sessions.ts +83 -57
- package/src/runtime/toaster.ts +27 -9
- package/src/runtime/ui.ts +163 -61
- package/src/runtime/watches.ts +20 -5
- package/src/runtime/widgets.ts +34 -22
- package/src/session/context/turn/message/attached-file.ts +36 -0
- package/src/session/context/turn/message/message.ts +20 -7
- package/src/session/conversation.ts +30 -6
- package/src/session/goal/task.ts +6 -0
- package/src/session/session.ts +17 -7
- package/src/testing/fake-claude.ts +21 -6
- package/src/testing/fake-elements.ts +51 -8
- package/src/testing/fake-sessions.ts +60 -23
- package/src/testing.ts +86 -28
- package/src/ui/areas.ts +1 -0
- package/src/ui/components.ts +14 -8
- package/src/ui/elements.ts +6 -2
- package/src/ui/markdown.ts +3 -3
- package/src/ui/panes.ts +2 -2
- package/src/ui/styles.ts +6 -2
- package/testing/fake-claude.d.ts +3 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +18 -5
- package/testing/fake-elements.d.ts.map +1 -1
- package/testing/fake-elements.js +46 -7
- package/testing/fake-sessions.d.ts +21 -10
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +58 -20
- package/testing.d.ts +8 -12
- package/testing.d.ts.map +1 -1
- package/testing.js +82 -21
- package/ui/areas.d.ts +4 -0
- package/ui/areas.d.ts.map +1 -1
- package/ui/components.d.ts +1 -0
- package/ui/components.d.ts.map +1 -1
- package/ui/components.js +11 -7
- package/ui/elements.d.ts +3 -0
- package/ui/elements.d.ts.map +1 -1
- package/ui/elements.js +6 -1
- package/ui/markdown.d.ts +2 -2
- package/ui/markdown.d.ts.map +1 -1
- package/ui/panes.d.ts +2 -1
- package/ui/panes.d.ts.map +1 -1
- package/ui/styles.d.ts +11 -2
- package/ui/styles.d.ts.map +1 -1
package/docs/commands.md
CHANGED
|
@@ -25,10 +25,19 @@ Creates a mod in `./<name>`: a `defineMod` with one listener, a Sidebar page, a
|
|
|
25
25
|
cmod link [path] [--yes] [--option key=value]...
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
Loads the checkout at `path` (default: the current folder) in every
|
|
28
|
+
Loads the checkout at `path` (default: the current folder) in every Claude Code session, the running ones too, in place of the installed mod. Installs the cmod plugin when Claude Code lacks it, unless the checkout is cmod itself. Then installs the checkout's packages, builds the program `cli/` declares and links it into cmod's programs folder and `~/.local/bin`, and runs the checkout's install step.
|
|
29
|
+
|
|
30
|
+
It loads the checkout through a marketplace cmod keeps in the checkout's parent folder:
|
|
31
|
+
|
|
32
|
+
1. It lists the checkout in `<parent>/.claude-plugin/marketplace.json`, named `<parent folder>-linked`, beside the cmod plugin from the `release` branch of `heyJordanParker/cmod`, so the mod's `cmod` dependency resolves in that marketplace. Every checkout linked from one folder shares its marketplace. The checkout's own `.claude-plugin/marketplace.json`, the one `cmod publish` writes, stays as it was.
|
|
33
|
+
2. It adds that file to the parent's `.git/info/exclude` when the parent is inside a git repository.
|
|
34
|
+
3. It adds the marketplace to Claude Code and installs `<mod>@<parent folder>-linked` from it, which loads the checkout's own files in every session at the next reload.
|
|
35
|
+
4. It turns off the installed copy of the mod from any other marketplace, and records it in `links.json` in the cmod store, so `cmod unlink` turns that copy back on.
|
|
36
|
+
|
|
37
|
+
It refuses a parent folder whose `.claude-plugin/marketplace.json` cmod did not write. `cmod watch` reloads every session once a change builds.
|
|
29
38
|
|
|
30
39
|
- `--yes` approves the mod's install and uninstall commands without asking.
|
|
31
|
-
- `--option key=value` sets one of the mod's options through `claude plugin configure`, once per option, under the
|
|
40
|
+
- `--option key=value` sets one of the mod's options through `claude plugin configure`, once per option, under the linked id, such as `my-mod@Developer-linked`. It refuses a key the mod does not declare, and names the keys it does ([options.md](options.md)).
|
|
32
41
|
|
|
33
42
|
### cmod unlink
|
|
34
43
|
|
|
@@ -36,7 +45,7 @@ Loads the checkout at `path` (default: the current folder) in every new Claude C
|
|
|
36
45
|
cmod unlink [path]
|
|
37
46
|
```
|
|
38
47
|
|
|
39
|
-
Stops loading the checkout at `path` (default: the current folder) in
|
|
48
|
+
Stops loading the checkout at `path` (default: the current folder) in every Claude Code session. Uninstalls it through Claude Code and takes it out of the marketplace in its parent folder. Once that marketplace lists no checkout, it uninstalls the cmod plugin the marketplace installed, removes the marketplace from Claude Code, and deletes its file and its line in `.git/info/exclude`. Then it turns back on the copy of the mod `cmod link` turned off, runs the checkout's uninstall step, and removes the program `cmod link` built, with its links.
|
|
40
49
|
|
|
41
50
|
### cmod check
|
|
42
51
|
|
|
@@ -72,7 +81,7 @@ Runs every check this machine can run on the mod at `path` (default: the current
|
|
|
72
81
|
cmod try <owner/repo | path>... [--yes] [--option key=value]... [--home <folder>] [-- claude arguments]
|
|
73
82
|
```
|
|
74
83
|
|
|
75
|
-
Starts one throwaway Claude Code session with mods set up, for trying a mod by hand or testing it with an agent. Installs each mod's packages, installs the cmod plugin when Claude Code lacks it, and runs each mod's install step after asking consent. Then
|
|
84
|
+
Starts one throwaway Claude Code session with mods set up, for trying a mod by hand or testing it with an agent. Installs each mod's packages, installs the cmod plugin when Claude Code lacks it, and runs each mod's install step after asking consent. Then starts one Claude Code session that loads every mod with `--plugin-dir`.
|
|
76
85
|
|
|
77
86
|
- `--option key=value` sets one option of the mod. With several mods, write `--option <mod>.key=value`.
|
|
78
87
|
- The session runs with a new home folder, so it sees only the mods named here and changes none of your Claude Code settings, mods, key bindings, or history. The folder is deleted when the session ends.
|
|
@@ -80,7 +89,7 @@ Starts one throwaway Claude Code session with mods set up, for trying a mod by h
|
|
|
80
89
|
- On macOS, Claude Code reads your login from the keychain, which `cmod try` links into the new home as `~/Library/Keychains`. Deleting the home removes only the link.
|
|
81
90
|
- Elsewhere, `CLAUDE_SECURESTORAGE_CONFIG_DIR` points Claude Code at the folder that holds your login, so it reads and refreshes that login where it is.
|
|
82
91
|
- `ANTHROPIC_CONFIG_DIR` points at your `~/.config/anthropic` login profiles, and `CLAUDE_CODE_OAUTH_TOKEN` and `ANTHROPIC_API_KEY` pass through.
|
|
83
|
-
- The session keeps how Claude Code reaches the model and what it must never do, so a try is as safe as your own session. From your `settings.json` it keeps `env` (a proxy or `ANTHROPIC_BASE_URL
|
|
92
|
+
- The session keeps how Claude Code reaches the model and what it must never do, so a try is as safe as your own session. From your `settings.json` it keeps `env` (a proxy or `ANTHROPIC_BASE_URL`), the credential helpers (`apiKeyHelper`, `awsAuthRefresh`, `awsCredentialExport`, `gcpAuthRefresh`, `otelHeadersHelper`, `proxyAuthHelper`), the login pins (`forceLoginMethod`, `forceLoginOrgUUID`, `forceLoginGatewayUrl`, `gatewayInternalNetworks`), `sandbox`, and `permissions.deny`. From `~/.claude.json` it keeps your account and the API keys you approved. A `~/` in a helper still means your own home. Hooks, plugins, allow rules, the model, and every other preference stay behind. Managed settings apply as always.
|
|
84
93
|
- `--home <folder>` runs in that folder and keeps it, so the next `cmod try --home <folder>` starts from it. `--home ~` runs in your own home, with your settings and mods.
|
|
85
94
|
- A checkout's program is built from its `cli/` the way `cmod link` builds it, with your own home and environment, so the build finds your toolchains and caches. A GitHub mod's program downloads from the GitHub release of the mod's version, so it fails until `cmod publish` has released that version.
|
|
86
95
|
- When the session ends, even when its terminal closes, it runs each mod's uninstall step and deletes its record, data folder, approval, key bindings, and program, so the mod stays uninstalled. A checkout already set up from the same folder keeps its setup.
|
|
@@ -125,7 +134,7 @@ It needs, and refuses to start without:
|
|
|
125
134
|
|
|
126
135
|
## Install mods
|
|
127
136
|
|
|
128
|
-
`cmod install`, `cmod update`, `cmod remove`, `cmod enable`, `cmod disable`, `cmod link`, `cmod unlink`, and `cmod try` write the time to `plugins-changed` in the cmod store when they finish, also when they fail partway, such as an update that updated some mods. The cmod plugin of every running session watches
|
|
137
|
+
`cmod install`, `cmod update`, `cmod remove`, `cmod enable`, `cmod disable`, `cmod link`, `cmod unlink`, and `cmod try` write the time to `plugins-changed` in the cmod store, then write `store-changed` beside it, when they finish, also when they fail partway, such as an update that updated some mods. The cmod plugin of every running session watches `store-changed` and Claude Code's `installed_plugins.json`, reads the `enabledPlugins` of the user's `settings.json` each time Claude Code reports that file changed and at each prompt, and runs `/reload-plugins` when `plugins-changed`, `installed_plugins.json`, or `enabledPlugins` changed. So each session runs the plugins the person has now, whether a `cmod` command, `/plugin`, `claude plugin`, or Claude Code's automatic update changed them. A reload keeps the prompt cache: the next request reads the whole conversation from the cache and pays only for the plugins' new content. A reload clears each mod's `memory` state. A machine where cmod set up no mod has no store, and these commands write nothing there. `cmod permission` needs no reload, because a mod sees its grants change before its next call.
|
|
129
138
|
|
|
130
139
|
### cmod install
|
|
131
140
|
|
|
@@ -218,6 +227,16 @@ Reads every conversation Claude Code saved, as the sessions, contexts, turns, ta
|
|
|
218
227
|
|
|
219
228
|
## Run in an install script
|
|
220
229
|
|
|
230
|
+
### cmod watch
|
|
231
|
+
|
|
232
|
+
```text
|
|
233
|
+
cmod watch
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Reloads every running session once a checkout `cmod link` linked builds after a change. It watches each linked folder, outside `node_modules` and `.git`. Once a folder has had no change for 2 seconds, it bundles the mod's hooks module as `cmod publish` does. A bundle that builds marks `plugins-changed`, so every session runs `/reload-plugins` once. A bundle that still fails 10 seconds after the last change shows `<mod> does not build` with the error as one toast in every session, and the toast goes once the mod builds. So an agent's half-written file reloads nothing and shows nothing. The cmod plugin starts one only in a session that an app shows, once a mod is linked, at the session's start or later, so a machine with no linked mod runs none. The first holds `watch.lock` in the cmod store, watches, and touches its file there every 5 seconds. Another one exits at once while it runs, and the cmod plugin of each such session starts one again once that file is 15 seconds old, so a machine runs one watcher however many sessions it has open.
|
|
237
|
+
|
|
238
|
+
A cmod before 4.1.0 linked a checkout by writing it into `CLAUDE_CODE_PLUGIN_DIRS` in the `env` block of Claude Code's `settings.json`. `cmod watch` at its start, and `cmod setup`, link each folder still in that list as `cmod link` does now and take it out of the list. A folder that fails to link stays in the list, and its error prints on stderr.
|
|
239
|
+
|
|
221
240
|
### cmod download
|
|
222
241
|
|
|
223
242
|
```text
|
|
@@ -236,7 +255,7 @@ cmod runs these itself. A person runs them to see a step's whole log, to finish
|
|
|
236
255
|
cmod setup <plugin-root> [--events] [--consent <sha256>] [--yes]
|
|
237
256
|
```
|
|
238
257
|
|
|
239
|
-
Refuses a machine the `os` and `cpu` keys of the mod's `package.json` leave out. Downloads the program its `cmod.program` names, or builds it from the checkout's `cli/` when `cmod link` loaded the mod, so a linked checkout whose version has no release yet still sets up, and links it into cmod's programs folder, which Claude Code runs it from by its name, and into `~/.local/bin` unless a file there is not cmod's. Runs the mod's install step, saves its uninstall step, grants the permissions its `cmod.permissions` lists, and records the mod as set up. A mod installed from a marketplace whose automatic updates the person has not turned on or off gets them turned on, through `"autoUpdate": true` on the marketplace's `extraKnownMarketplaces` entry in Claude Code's `settings.json`, and the consent question says so. The consent covers the scripts, the program, the hotkeys, and the permissions together, and an update asks again only for what changed. An unchanged mod runs nothing. `--events` prints one event per line for a program to read, and the mod's install toast in Claude Code reads them.
|
|
258
|
+
Refuses a machine the `os` and `cpu` keys of the mod's `package.json` leave out. Downloads the program its `cmod.program` names, or builds it from the checkout's `cli/` when `cmod link` loaded the mod, so a linked checkout whose version has no release yet still sets up, and links it into cmod's programs folder, which Claude Code runs it from by its name, and into `~/.local/bin` unless a file there is not cmod's. Runs the mod's install step, saves its uninstall step, grants the permissions its `cmod.permissions` lists, and records the mod as set up. A mod installed from a marketplace whose automatic updates the person has not turned on or off gets them turned on, through `"autoUpdate": true` on the marketplace's `extraKnownMarketplaces` entry in Claude Code's `settings.json`, and the consent question says so. The consent covers the scripts, the program, the hotkeys, and the permissions together, and an update asks again only for what changed. An unchanged mod runs nothing. `--events` prints one event per line for a program to read, and the mod's install toast in Claude Code reads them. Before all of that, it links each folder a cmod before 4.1.0 left in `CLAUDE_CODE_PLUGIN_DIRS`, as [`cmod watch`](#cmod-watch) does.
|
|
240
259
|
|
|
241
260
|
### cmod teardown
|
|
242
261
|
|
|
@@ -244,7 +263,7 @@ Refuses a machine the `os` and `cpu` keys of the mod's `package.json` leave out.
|
|
|
244
263
|
cmod teardown <plugin-name> [--events]
|
|
245
264
|
```
|
|
246
265
|
|
|
247
|
-
Runs a removed mod's saved uninstall step, then deletes its install record, the saved step, every version of its program,
|
|
266
|
+
Runs a removed mod's saved uninstall step, then deletes its install record, the saved step, every version of its program, its data folder, and `instances/<plugin-name>/`, where its `singleInstance` programs keep their addresses, takes out each Claude Code setting it changed that still has the value it set, and forgets the scripts and the permissions the person approved for it. It keeps the mod's config folder, `~/.claude/cmods/<plugin-name>`. A failed uninstall step keeps the record, so `cmod teardown` can run again.
|
|
248
267
|
|
|
249
268
|
### cmod permission
|
|
250
269
|
|
|
@@ -278,6 +297,22 @@ cmod rules [--project <folder>] [--json]
|
|
|
278
297
|
|
|
279
298
|
Links the `rules/` folder of each mod that is on and has the `prompt` permission into Claude Code's rules folder, and removes the link of each that is off, lacks the permission, or is removed ([mod.md](mod.md#rules)). A mod the person installed links at `<config>/rules/<mod>`, and one turned on only for `--project`, the current folder when it is left out, links at `<project>/.claude/rules/<mod>`. It prints the rules it linked. With `--json` it prints `{ linked, placed, removed, watched }`, where `placed` and `removed` name the mods whose link this run made or took away, and `watched` lists the files and folders whose change can change the next run. `cmod install`, `update`, `remove`, `enable`, `disable`, `permission`, `setup`, `teardown`, `link`, and `unlink` run it after they succeed. The cmod plugin runs it when a session starts and before a prompt, each time only when a `watched` path changed since its last run.
|
|
280
299
|
|
|
300
|
+
### cmod task
|
|
301
|
+
|
|
302
|
+
```text
|
|
303
|
+
cmod task start <prompt> --title <title> [--clone] [--agent <type>] [--model <model>] [--cwd <folder>] [--json]
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Starts a task in a new background Claude Code session, for a mod's `startTask` with `isBackground` and its `clone` ([session.md](session.md#act-on-a-session)). The task belongs to the session Claude Code names in `CLAUDE_CODE_SESSION_ID`, which the cmod plugin sets when it runs the command. It runs `claude --bg` on the prompt in `--cwd`, the current folder when it is left out, and with `--clone` runs `claude --resume <session> --fork-session --bg`, so the new session starts on a copy of the conversation. `--agent` and `--model` pick the agent definition and the model of a fresh session. It finds the new session's id from the 8 characters `claude --bg` prints, through `claude agents --json --all`, and passes the starting session and the task in the new session's `--settings` env, so its cmod plugin tells the starting session at its first stop that the task answered. It saves the task in cmod's data folder, so `cmod history tasks` of the starting session lists it, with the status `claude agents` gives it. It prints the task, as one line of JSON with `--json`.
|
|
307
|
+
|
|
308
|
+
### cmod program
|
|
309
|
+
|
|
310
|
+
```text
|
|
311
|
+
cmod program <mod> -- <command…>
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Runs one copy of a mod's program for the whole computer, for a `program` job with `singleInstance` ([jobs.md](jobs.md#program)), which every session of the mod runs it for. The first one takes `instances/<mod>/<program>.json.lock` in the cmod store, runs the command without `CLAUDE_CODE_SESSION_ID`, because the copy serves every session, writes its first line on standard output, its address, with the copy's process to `instances/<mod>/<program>.json`, and prints it. Every other one prints the address in that file, then each new one as it changes, and tries the lock again whenever that folder changes and every 5 seconds, so it takes over once the holder exits. A lock whose process is gone counts as free. The holder stops its copy and gives up the lock on SIGTERM, SIGINT, or SIGHUP, and when the session that started it exits, which it checks every 5 seconds. When the copy exits on its own, the holder exits with the copy's exit code, and the copy's standard error passes through. A new holder that finds a copy an earlier holder left running sends it SIGTERM, then SIGKILL after 5 seconds, and starts its own copy only once that one is gone, so two copies never run at once.
|
|
315
|
+
|
|
281
316
|
### cmod option
|
|
282
317
|
|
|
283
318
|
```text
|
package/docs/index.md
CHANGED
|
@@ -88,9 +88,9 @@ A mod imports from the files below, and every other file in this package is inte
|
|
|
88
88
|
| `mod.js` | `defineMod`, `ModDefinition`, `Mod`, `Subscription`, `Timer`, `Reservation`, `Permissions`, `Allowed`, `JobContext`, `Job`, `SlashCommand`, `Tool`, `Step`, `longestMs`, `messageOf` | [mod.md](mod.md), [jobs.md](jobs.md), [install-steps.md](install-steps.md#steps-in-claude-code) |
|
|
89
89
|
| `mod.js` | `Hotkey`, `HotkeyInit`, `Combo`, `Key` | [mod.md](mod.md#hotkeys) |
|
|
90
90
|
| `mod.js` | `settings`, `SettingsFile`, `SettingsFileChange`, `SettingsFileChangeListener`, `config`, `ConfigRow`, `commands`, `CommandInfo`, `CommandRun`, `CommandRunAnswer`, `CommandRunResult`, `model`, `fs`, `Entry`, `Stat`, `FileMatch`, `FileChange`, `metadata`, `Metadata`, `process`, `http`, `clipboard`, `Imports` | [mod.md](mod.md) |
|
|
91
|
-
| `mod.js` | `sessions`, `Sessions`, `Session`, `CurrentSession`, `TaskOwner`, `RecordPart`, `OnMethods`, `BeforeMethods`, `Listener`, `ListenerTime`, `ReadOptions`, `AnswerOptions`, `Refusal`, `SearchOptions`, `SessionScope`, `SessionSend`, `TaskStartArgs`, `ToolCallArgs`, `Usage`, `Surface`, `Matcher` | [session.md](session.md) |
|
|
91
|
+
| `mod.js` | `sessions`, `Sessions`, `Session`, `CurrentSession`, `TaskOwner`, `RecordPart`, `OnMethods`, `BeforeMethods`, `Listener`, `ListenerTime`, `ReadOptions`, `AnswerOptions`, `Refusal`, `SearchOptions`, `SessionScope`, `SessionSend`, `TaskStartArgs`, `ToolCallArgs`, `Usage`, `RateLimit`, `TaskStatus`, `Surface`, `Matcher` | [session.md](session.md) |
|
|
92
92
|
| `mod.js` | `Placement`, `ProjectSetup`, `SessionStartAsked`, `SessionStarted`, `SessionEnded`, `ModelChange`, `CwdChange`, `DirectoryAdd`, `Worktree`, `Notification`, `Elicitation`, `ElicitationResponse`, `McpServer`, `PermissionUpdate`, `PermissionCheck`, `PermissionRequest`, `PermissionDenial`, `BatchedCall`, `ToolBatch`, `Todo`, `ReplyDisplay` | [session.md](session.md#the-session-s-own-events) |
|
|
93
|
-
| `mod.js` | `Message`, `MessageType`, `Prompt`, `Note`, `Reply`, `ToolCall`, `ToolResult`, `Skill`, `InstructionFile`, `Notice`, `Sender`, `SenderType`, `Person`, `Mod`, `Schedule`, `Channel`, `Sdk`, `Engine` | [session.md](session.md#message) |
|
|
93
|
+
| `mod.js` | `Message`, `MessageType`, `Prompt`, `Note`, `Reply`, `ToolCall`, `ToolResult`, `Skill`, `InstructionFile`, `AttachedFile`, `AttachmentType`, `Notice`, `Sender`, `SenderType`, `Person`, `Mod`, `Schedule`, `Channel`, `Sdk`, `Engine` | [session.md](session.md#message) |
|
|
94
94
|
| `mod.js` | `Context`, `Turn`, `Goal`, `Task` | [session.md](session.md#turn-context-goal-and-task) |
|
|
95
95
|
| `mod.js` | `Toast`, `ToastId`, `ToastType`, `ToastOptions` | [ui.md](ui.md#toasts) |
|
|
96
96
|
| `mod.js` | `PageInit`, `Page`, `PageProps`, `PaneInit`, `Pane`, `PaneProps`, `OpenOptions`, `ScrollTarget`, `ScrollOptions` | [ui.md](ui.md#the-sidebar-and-its-pages) |
|
|
@@ -102,7 +102,7 @@ A mod imports from the files below, and every other file in this package is inte
|
|
|
102
102
|
| `shell.js` | `parseShell`, `ParsedShell`, `ShellCommand` | [mod.md](mod.md#shell-js-and-path-js-read-a-command-line) |
|
|
103
103
|
| `path.js` | `resolve`, `join`, `dirname`, `basename`, `extname`, `relative`, `isAbsolute`, `normalize` | [mod.md](mod.md#shell-js-and-path-js-read-a-command-line) |
|
|
104
104
|
| `testing.js` | `testMod`, `TestOptions`, `TestedMod`, `Raise`, `Fakes`, `Shown`, `ShownToast`, `TestCall`, `FakeSession`, `MessageDraft`, `RoutedEvent` | [testing.md](testing.md) |
|
|
105
|
-
| `ui/elements.js` | `Box`, `Text`, `Button`, `Link`, `Code`, `Markdown`, `Input`, `Select`, `Image`, `drawWith` | [ui.md](ui.md) |
|
|
105
|
+
| `ui/elements.js` | `Box`, `Text`, `Button`, `Link`, `Code`, `Markdown`, `Input`, `Select`, `Image`, `Raster`, `Svg`, `Client`, `drawWith` | [ui.md](ui.md) |
|
|
106
106
|
| `ui/components.js` | `Tabs`, `Tab`, `Split`, `Panel`, `Toggle`, `Tooltip`, `Help`, `Dialog`, `Pagination`, `Progress` | [ui.md](ui.md) |
|
|
107
107
|
| `ui/markdown.js` | `Heading`, `Paragraph`, `CodeBlock`, `BlockQuote`, `List`, `Table`, `ThematicBreak`, `HtmlBlock`, `Block`, `markdownBlocks`, `BlockProps`, `MarkdownReader`, `MarkdownPiece` | [ui.md](ui.md#markdown-blocks) |
|
|
108
108
|
| `jobs/slash-command.js` | `slashCommand`, `CommandReply` | [jobs.md](jobs.md) |
|
package/docs/install-steps.md
CHANGED
|
@@ -36,7 +36,7 @@ cmod refuses a `cmod` key that breaks these rules, with the fix in the message:
|
|
|
36
36
|
|
|
37
37
|
Before a step runs for the first time, cmod shows the person the commands, the hotkeys, and the permissions, and asks to run them. cmod hashes the commands, the hotkeys, the permissions, and every file in each script folder, and remembers the hash the person approved with each permission. A change to any of those asks again, and an added permission is the only one it asks about ([permissions.md](permissions.md)).
|
|
38
38
|
|
|
39
|
-
- In Claude Code, the mod opens
|
|
39
|
+
- In Claude Code, the mod opens its install page, the `Install <mod>` tab of the Sidebar. Claude Code gives the Sidebar the keys when the prompt is empty and nothing else holds them. Otherwise a click on the page does. A Sidebar opened this way shows only on a terminal 144 columns wide, so below that the toast `Installing`, with `Waiting for your answer` and an `Answer` button, opens it. The page asks `Install <mod>?` and lists `<mod> wants to:`, each permission as one sentence, then `and change your computer:` with `Run <install> now, and <uninstall> when you remove it` and `Bind shift+tab to Cycle the mode`, naming only what the mod has. `Accept` (a) runs the setup, and a loading toast above the prompt shows the install with a progress bar. `Not now` (n) and the `×` on the tab decline. Escape hides the Sidebar, and the question waits in its tab and its toast.
|
|
40
40
|
- In a session with no screen, such as `claude -p`, the mod waits and logs `<mod> waits for consent to run <install>. Run cmod install <mod> in a terminal.` That person is already in a terminal, and no screen exists to draw a button on.
|
|
41
41
|
- In a terminal, `cmod install`, `cmod link`, `cmod try`, and `cmod update` print the commands, the program, the hotkeys, and the permissions, and ask `Set up <mod>? [y/N]`. `--yes` approves without asking.
|
|
42
42
|
- `Not now` leaves the mod off with the toast `Not installed` and an `Install` button that asks again.
|
|
@@ -53,7 +53,7 @@ In Claude Code, the mod sets itself up through the `cmod` program the cmod plugi
|
|
|
53
53
|
|
|
54
54
|
## Steps in Claude Code
|
|
55
55
|
|
|
56
|
-
Some setup only the person can do, such as signing in to a service. A mod lists those steps in `installer`, and
|
|
56
|
+
Some setup only the person can do, such as signing in to a service. A mod lists those steps in `installer`, and its install page shows each one after the permissions and the options, once the mod has started:
|
|
57
57
|
|
|
58
58
|
```tsx
|
|
59
59
|
import { defineMod, type Step } from '../node_modules/@cmodjs/core/mod.js'
|
|
@@ -74,16 +74,16 @@ export const ciWatch = defineMod({ name: 'ci-watch', installer: [signIn], setup(
|
|
|
74
74
|
type Step<State> = {
|
|
75
75
|
readonly id: string
|
|
76
76
|
readonly title: string
|
|
77
|
-
render(mod: Mod<State>, props:
|
|
77
|
+
render(mod: Mod<State>, props: PageProps): RenderElement
|
|
78
78
|
isDone(mod: Mod<State>): boolean | Promise<boolean>
|
|
79
79
|
}
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
- `id` is 1 to 64 letters, digits, `_`, or `-`, unique among the steps. `title` is the line above the step, with `(1 of 2)`. `defineMod` throws for an id that is not one, a blank title, or two steps with one id.
|
|
83
|
-
- `render` draws the step's body, the way a
|
|
84
|
-
- `isDone` says whether the step is finished. A step that is done is skipped, so the
|
|
83
|
+
- `render` draws the step's body, the way a page's `render` does, with the elements a page draws ([ui.md](ui.md#the-sidebar-and-its-pages)). Give the step a `Button` that does the work or opens the place to do it, never a command to type.
|
|
84
|
+
- `isDone` says whether the step is finished. A step that is done is skipped, so the page opens only for what is left. `Next` checks `isDone`, and shows `Finish <title> first.` while it is false. The last step's button says `Finish`.
|
|
85
85
|
- An `isDone` that throws or rejects counts as not done, and `Next` shows `The <title> step's isDone failed: <error>`.
|
|
86
|
-
- `Not now` closes the
|
|
86
|
+
- `Not now` closes the page, and the mod keeps running. A toast stays above the prompt, `Needs one more step` with `Starting with <title>.` under it, and its `Finish` button opens the page on the first step left. Settings shows `Needs setup: <title>` with a `Finish setup` button too.
|
|
87
87
|
|
|
88
88
|
## Hotkeys
|
|
89
89
|
|
package/docs/jobs.md
CHANGED
|
@@ -324,7 +324,7 @@ export const loop = defineMod({
|
|
|
324
324
|
It needs `"run"` with the program's name. Without the grant the program stops at once and names the line to add ([permissions.md](permissions.md)).
|
|
325
325
|
|
|
326
326
|
```ts
|
|
327
|
-
program(options: { readonly command: readonly string[]; readonly environment?: Record<string, string
|
|
327
|
+
program(options: { readonly command: readonly string[]; readonly environment?: Record<string, string>; readonly singleInstance?: boolean }): Job<Program>
|
|
328
328
|
|
|
329
329
|
type Program = {
|
|
330
330
|
readonly state: 'stopped' | 'starting' | 'running' | 'backoff' | 'fatal'
|
|
@@ -338,6 +338,7 @@ type Program = {
|
|
|
338
338
|
- The program's first line on standard output must be its address: `127.0.0.1:<port>`, or `unix:<absolute socket path>`. `ready()` then resolves `{ url: 'http://127.0.0.1:<port>' }`, or `{ url: 'http://localhost', socketPath }`. Pass both to `http.fetch`, which reaches that address with no `network` permission while the program runs.
|
|
339
339
|
- In an interactive session the program starts with the mod. Otherwise it starts at the first `ready()`.
|
|
340
340
|
- When the program exits, cmod starts it again after 1, 2, 4, 8, and 16 seconds. After 5 failed tries, or a first line that is not an address, the state is `fatal`, `ready()` rejects, and cmod logs `<mod>: the <program> program stopped: <reason>`.
|
|
341
|
+
- `singleInstance: true` runs one copy on the computer, however many sessions load the mod, such as a server that is the only writer of its data. Each session runs it through [`cmod program`](commands.md#cmod-program): the first session starts the copy, and every session's `ready()` resolves that copy's address and reaches it with no `network` permission. When the session running the copy ends, another open session starts a new copy, and from then on `ready()` resolves the new address in every session, so ask `ready()` before each use rather than keeping its answer. A copy a killed session left running is stopped before the new one starts. Two `singleInstance` programs with the same program name in one mod throw at `mod.use`.
|
|
341
342
|
- A program that ships with the mod gets onto `PATH` through the `program` key of the mod's `package.json` ([install-steps.md](install-steps.md)). A program an install step fetched with `cmod download` is not on `PATH`, so `command` names it by its full path, `${mod.dataFolder}/bin/<program>`.
|
|
342
343
|
|
|
343
344
|
```ts
|
package/docs/mod.md
CHANGED
|
@@ -45,7 +45,7 @@ export const greeter = defineMod({
|
|
|
45
45
|
|
|
46
46
|
## When setup runs
|
|
47
47
|
|
|
48
|
-
Claude Code loads the mod at the start of each session. cmod then checks that the mod is set up. A mod the person has not accepted yet opens
|
|
48
|
+
Claude Code loads the mod at the start of each session. cmod then checks that the mod is set up. A mod the person has not accepted yet opens its install page, the `Install <name>` tab of the Sidebar, which lists its permissions and the changes it makes to the computer, and waits for Accept ([install-steps.md](install-steps.md)). An option with no default and no value is asked next on the same page. Then cmod loads the saved state, runs `setup`, marks as open each of the mod's panes that Claude Code already shows, and opens the install page again for each `installer` step the mod declares that is not done.
|
|
49
49
|
|
|
50
50
|
The first time a mod starts on a machine, cmod shows the toast `Installed` under its name. Opened, it lists the mod's `plugin.json` `description`, what setup added, such as `Adds the my-mod pane, the Branch widget and /deploy.`, what its install step set up, and each permission the person allowed, with a Settings button that opens Settings at the mod. The first time a new version starts, the toast is `Updated to <version>`: the first three changes of that version's `CHANGELOG.md` section, the version the machine ran before, and a Changelog button ([Release notes](#release-notes)). A mod with no changelog lists what it adds now and no longer adds instead. A new session, `/reload-plugins`, and a version the machine already ran show no toast.
|
|
51
51
|
|
package/docs/options.md
CHANGED
|
@@ -49,7 +49,7 @@ Each value is, in order of precedence:
|
|
|
49
49
|
3. The value the person set in `/config`.
|
|
50
50
|
4. The `default` in the declaration.
|
|
51
51
|
|
|
52
|
-
An option with no default and no value is asked before `setup` runs, on
|
|
52
|
+
An option with no default and no value is asked before `setup` runs, on the mod's install page in the Sidebar: one field per option, each saved to `/config` through `cmod option` as the person submits it. A secret is set in `/config` itself, where Claude Code keeps it in the keychain, and the page says so. When the person chooses Not now, or no one can answer, as in `claude -p`, the mod does not start: its error toast says `it needs <title>` and `Retry asks for it.`, and `Retry` opens the question again. A secret has an `Open /config` button, because Claude Code keeps secrets in /config and the keychain.
|
|
53
53
|
|
|
54
54
|
`cmod install`, `cmod link`, and `cmod try` set options from the terminal with `--option key=value`, once per option ([commands.md](commands.md)).
|
|
55
55
|
|
package/docs/permissions.md
CHANGED
|
@@ -35,7 +35,7 @@ Anthropic's plugin directory does not list a mod that can approve Claude's tool
|
|
|
35
35
|
|
|
36
36
|
## What the person sees
|
|
37
37
|
|
|
38
|
-
The install opens the
|
|
38
|
+
The install opens the mod's install page in the Sidebar of Claude Code and lists every permission in `permissions`, with the install step, the program, and the hotkeys. An update asks again only when it adds a permission, and lists only the new ones. Removing the mod revokes everything it was granted.
|
|
39
39
|
|
|
40
40
|
Settings lists the same permissions as toggles, under the mod, Permissions. A toggle runs `cmod permission <mod> <name> [value] on|off`, and the mod sees the change before its next call, with no reload.
|
|
41
41
|
|
package/docs/session.md
CHANGED
|
@@ -79,7 +79,9 @@ type Session = {
|
|
|
79
79
|
onMessage(matcher?, listener, options?): Subscription // and every on method below
|
|
80
80
|
beforePrompt(matcher?, listener, options?): Subscription // and every before method below
|
|
81
81
|
send(message: { type: Prompt | Note; text: string; as?: Person | Session }): Promise<void>
|
|
82
|
-
startTask(task: { prompt: string; title: string; agentType?: string; model?: string; name?: string; cwd?: string }): Promise<Task>
|
|
82
|
+
startTask(task: { prompt: string; title: string; agentType?: string; model?: string; name?: string; cwd?: string; isBackground?: boolean }): Promise<Task>
|
|
83
|
+
fork(task: { prompt: string; title: string }): Promise<Task>
|
|
84
|
+
clone(task: { prompt: string; title: string }): Promise<Task>
|
|
83
85
|
callTool(call: { name: string; input: Record<string, unknown> }): Promise<ToolResult>
|
|
84
86
|
usage(): Promise<Usage>
|
|
85
87
|
surfaces(): Promise<Surface[]>
|
|
@@ -90,7 +92,7 @@ type Session = {
|
|
|
90
92
|
- `project` is the project the session works in, and `cwd` is its working folder. For the current session both follow `/cd`, and `cwd` follows a `cd` in a Bash or PowerShell call.
|
|
91
93
|
- `agentType` is the agent definition the session runs, such as `Explore` for a task's owner, or the agent a session started with `claude --agent` runs. A plain session has none.
|
|
92
94
|
- `model` is the model the session runs on. `startedAt` and `lastAt` are ISO times.
|
|
93
|
-
- `forkedFrom` names the session a `/branch` or a fork copied it from.
|
|
95
|
+
- `forkedFrom` names the session a `/branch` or a fork copied it from, and for a task's owner the session whose `fork` or `clone` started it.
|
|
94
96
|
- `isRunning` is true while the session is open in a Claude Code window, and for a task's owner while its task runs.
|
|
95
97
|
|
|
96
98
|
### Read a record
|
|
@@ -137,7 +139,7 @@ A turn or a context that has ended takes no listener: its methods throw `<mod>:
|
|
|
137
139
|
| Method | The listener gets | It happens in |
|
|
138
140
|
| --- | --- | --- |
|
|
139
141
|
| `onMessage` | Every new message. | Its turn |
|
|
140
|
-
| `onPrompt`, `onNote`, `onReply`, `onToolCall`, `onToolResult`, `onSkill`, `onInstructionFile`, `onNotice` | Each new message of that type. | Its turn |
|
|
142
|
+
| `onPrompt`, `onNote`, `onReply`, `onToolCall`, `onToolResult`, `onSkill`, `onInstructionFile`, `onAttachedFile`, `onNotice` | Each new message of that type. | Its turn |
|
|
141
143
|
| `onTurnStart`, `onTurnEnd` | The `Turn` that started or ended. | Its context |
|
|
142
144
|
| `onContextStart`, `onContextEnd` | The `Context` that started or ended. | The session |
|
|
143
145
|
| `onTaskStart`, `onTaskEnd` | The `Task` that started or ended. | Its parent task, or the goal |
|
|
@@ -184,11 +186,12 @@ Each `before` method, such as `beforeToolCall`, runs `listener` just before some
|
|
|
184
186
|
| `beforeNote` | `{ text }` | A note any mod or Claude Code adds |
|
|
185
187
|
| `beforeSkill` | `{ text }` | A Skill as Claude Code loads it |
|
|
186
188
|
| `beforeInstructionFile` | `{ text }` or `{ refuse }` | A `CLAUDE.md` file as Claude Code reads it. `refuse` leaves it out |
|
|
189
|
+
| `beforeAttachedFile` | `{ text }` or `{ refuse }` | A file Claude Code attaches by itself, such as one the person mentions with `@` or one it puts back after a compaction. `text` replaces what Claude reads, and `refuse` leaves it out |
|
|
187
190
|
| `beforeReply`, `beforeNotice` | `{ text }` | The text the transcript keeps |
|
|
188
191
|
| `beforeToolResult` | `{ output?, isError?, note? }` | The result, after the tool ran. Here `output` is what the tool answered Claude Code, such as `{ stdout, stderr }` for Bash, and an `output` answer replaces it in the same shape, `note` adds text beside it, and `isError: true` makes Claude read it as a failure, with the note as the reason. A failed call takes only a note |
|
|
189
|
-
| `beforeToolCall` | `{ input }`, `{ result }`, or `{ refuse }` | A tool call. `result` answers it without running the tool |
|
|
190
|
-
| `beforeTaskStart` | `{ owner
|
|
191
|
-
| `beforeTurnEnd` | `{ refuse }` | The agent about to stop. `refuse`
|
|
192
|
+
| `beforeToolCall` | `{ input?, note? }`, `{ result, note? }`, or `{ refuse }` | A tool call. `result` answers it without running the tool, and `note` reaches Claude with the call's result |
|
|
193
|
+
| `beforeTaskStart` | `{ prompt?, title?, owner?: { agentType?, model?, cwd? }, isAsync? }` or `{ refuse }` | A task about to start. `prompt` and `title` replace what Claude wrote, `owner` picks the agent that does it, its model, and its folder, and `isAsync` runs a subagent while Claude goes on. A background task takes `prompt`, `title`, `owner`, and `refuse`, a fork and a clone take `prompt`, `title`, and `refuse`, and a workflow's agent takes only `refuse` |
|
|
194
|
+
| `beforeTurnEnd` | `{ note }` or `{ refuse }` | The agent about to stop. `refuse` tells it it may not stop yet, and it reads the reason as an error. `note` gives it new information, which it reads at once and keeps working. On `task.owner`, it is the task about to hand back its answer |
|
|
192
195
|
| `beforeContextEnd` | `{ refuse }` | A compaction about to start. `refuse` keeps the context |
|
|
193
196
|
|
|
194
197
|
```ts
|
|
@@ -197,7 +200,7 @@ sessions.current.beforeTaskStart({ owner: { agentType: 'Explore' } }, () => ({ o
|
|
|
197
200
|
sessions.current.beforeTurnEnd(async () => ((await testsPass()) ? undefined : { refuse: 'The tests fail. Fix them before you stop.' }))
|
|
198
201
|
```
|
|
199
202
|
|
|
200
|
-
An answer that changes text needs the `prompt` permission, a `beforeToolCall` answer needs `tools`, and a `beforeTaskStart` answer needs `tasks`. Without it the answer changes nothing, and the toast `The <method> listener failed` names the permission. When several listeners answer, each sees what the one before it changed, and the first `refuse` or `result` decides.
|
|
203
|
+
An answer that changes text needs the `prompt` permission, a `beforeToolCall` answer needs `tools`, and a `beforeTaskStart` answer needs `tasks`. A `note` always needs `prompt`, so a `beforeToolCall` answer with both `input` and `note` needs both. Without it the answer changes nothing, and the toast `The <method> listener failed` names the permission. When several listeners answer, each sees what the one before it changed, and the first `refuse` or `result` decides.
|
|
201
204
|
|
|
202
205
|
Claude reads each mod's `note` inside the mod's own tag, such as `<tracer_mod>…</tracer_mod>` for the mod `tracer`. Every note Claude received becomes a `Note` whose `from` is `{ type: 'mod', name }`, so `onNote({ from: { type: 'mod', name: 'tracer' } })` hears each note of tracer's that Claude got, and never one Claude Code dropped.
|
|
203
206
|
|
|
@@ -207,10 +210,10 @@ Claude reads each mod's `note` inside the mod's own tag, such as `<tracer_mod>
|
|
|
207
210
|
|
|
208
211
|
| Method | Claude Code's moment | The payload | The listener can answer |
|
|
209
212
|
| --- | --- | --- | --- |
|
|
210
|
-
| `beforeProjectSetup`, `onProjectSetup` | `claude --init-only` sets up the project | `ProjectSetup`: `trigger` |
|
|
213
|
+
| `beforeProjectSetup`, `onProjectSetup` | `claude --init-only` sets up the project | `ProjectSetup`: `trigger` | `{ note }`, which Claude reads in the session it sets up. Claude Code waits for the answer |
|
|
211
214
|
| `beforeSessionStart`, `onSessionStart` | a startup, resume, `/clear`, or fork, and for `beforeSessionStart` a compaction | `SessionStartAsked`, `SessionStarted`: `source`, `model?`, `title?`, `idleSeconds?`, `contextTokenCount?`, `isCacheWarm?`, `cacheWriteUsd?` | `{ note?, title?, reloadSkills?, firstPrompt? }` |
|
|
212
215
|
| `onSessionEnd` | the session ends | `SessionEnded`: `reason` | |
|
|
213
|
-
| `beforeModelChange`, `onModelChange` | `/model` switches the model | `ModelChange`: `model`, `previous`, `requested?`, `source`, `contextTokenCount`, `isCacheWarm`, `cacheTtl`, `cacheWriteUsd`, `pricing?` | `{ ask }` or `{
|
|
216
|
+
| `beforeModelChange`, `onModelChange` | `/model` switches the model | `ModelChange`: `model`, `previous`, `requested?`, `source`, `contextTokenCount`, `isCacheWarm`, `cacheTtl`, `cacheWriteUsd`, `pricing?` | `{ ask }`, `{ refuse }`, or `{ note }`, which Claude reads right after the switch |
|
|
214
217
|
| `onCwdChange` | the working folder moves | `CwdChange`: `previous`, and `cwd` is the new folder | |
|
|
215
218
|
| `onDirectoryAdd` | `/add-dir` adds a folder | `DirectoryAdd`: `directory`, `source` | |
|
|
216
219
|
| `beforeWorktreeCreate`, `onWorktreeCreate`, `onWorktreeRemove` | a worktree is made or removed | `Worktree`: `name?`, `path?` | `{ result }`, the worktree's path |
|
|
@@ -226,7 +229,7 @@ Claude reads each mod's `note` inside the mod's own tag, such as `<tracer_mod>
|
|
|
226
229
|
|
|
227
230
|
The four `PermissionCheck` and `PermissionRequest` methods work only in a mod whose `hooks/register.ts` calls `registerPermissionCheck` ([mod.md](mod.md#register-the-mod-with-claude-code)). Without it they throw where they are called.
|
|
228
231
|
|
|
229
|
-
At a compaction, Claude reads the `note` at the start of the window the compaction opens, and `title`, `reloadSkills`, and `firstPrompt` apply to the whole session. The payload carries `model` for the main agent and `contextTokenCount` as the compaction left it. `sessions.current.beforeSessionStart` hears the main agent's compactions. `sessions.current.goal.beforeSessionStart` also hears every subagent's, with the subagent in `task`, and its note reaches that subagent's window
|
|
232
|
+
At a compaction, Claude reads the `note` at the start of the window the compaction opens, and the person never sees it, and `title`, `reloadSkills`, and `firstPrompt` apply to the whole session. The payload carries `model` for the main agent and `contextTokenCount` as the compaction left it. `sessions.current.beforeSessionStart` hears the main agent's compactions. `sessions.current.goal.beforeSessionStart` also hears every subagent's, with the subagent in `task`, and its note reaches that subagent's window. A subagent's compaction takes only `note`:
|
|
230
233
|
|
|
231
234
|
```ts
|
|
232
235
|
sessions.current.goal.beforeSessionStart({ source: 'compact' }, ({ task }) => ({ note: primerFor(task) }))
|
|
@@ -252,11 +255,30 @@ Every action works on the current session and on every other running session. cm
|
|
|
252
255
|
- `as: Session` sends it to another session as a message from this one, which Claude Code shows as one session writing to another.
|
|
253
256
|
- To a task's owner, `send` delivers the text to that running subagent.
|
|
254
257
|
- `startTask` starts a task in the session, the way the Agent tool starts one, and resolves the `Task` once it started. It does not wait for the task to finish. `title` is the task's title, `agentType` names the owner's agent type, such as `Explore`, `name` is the address a teammate's `SendMessage` reaches it by, and `model` and `cwd` change what it runs on and where. It needs the `tasks` permission, and rejects with `<mod>: Claude Code refused the task: <reason>` when a hook refuses it.
|
|
258
|
+
- With `isBackground: true`, the owner is a new background Claude Code session, as `claude --bg` starts, which starts fresh on the prompt and keeps running after this session ends. It takes no `name`, and rejects with `<mod>: a background task takes no name.` for one.
|
|
259
|
+
- `fork` starts a subagent that keeps this session's whole conversation and does what the prompt says.
|
|
260
|
+
- `clone` starts a new background session with a copy of this session's conversation, which carries on with the prompt.
|
|
261
|
+
|
|
262
|
+
Every task needs the `tasks` permission and shows in this session's goal. `fork` and `clone` take only `prompt` and `title`, because the owner is a copy of this session. A `beforeTaskStart` listener that refuses a background task or a clone rejects the call with `<mod>: a beforeTaskStart listener refused the task: <reason>`.
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
await sessions.current.startTask({ title: 'Map the auth code', prompt, agentType: 'Explore' })
|
|
266
|
+
await sessions.current.fork({ title: 'Write the tests', prompt: 'Write tests for what we just built.' })
|
|
267
|
+
await sessions.current.clone({ title: 'Try SQL instead', prompt: 'Rebuild this on SQL.' })
|
|
268
|
+
await sessions.current.startTask({ title: 'Upgrade dependencies', prompt, isBackground: true })
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
| Call | `task.isBackground` | `task.owner` |
|
|
272
|
+
| --- | --- | --- |
|
|
273
|
+
| `startTask` | absent | a subagent of this session, with its `agentType` |
|
|
274
|
+
| `fork` | absent | a subagent of this session, `forkedFrom` this session |
|
|
275
|
+
| `clone` | `true` | a new session whose `id` is the task's, `forkedFrom` this session |
|
|
276
|
+
| `startTask` with `isBackground` | `true` | a new session whose `id` is the task's |
|
|
255
277
|
- `callTool` runs a tool in the session, such as `{ name: 'Bash', input: { command: 'bun test' } }`, and resolves its `ToolResult`. It needs the `tools` permission.
|
|
256
|
-
- `usage` answers the session's model, its context window's fill, and its cost so far, as the status line shows them: `{ model, contextWindow: { tokenCount?, size?, percent? }, cost?: { usd } }`. For an ended session and a task's owner it answers the model and the tokens of its last reply, from its transcript.
|
|
278
|
+
- `usage` answers the session's model, its context window's fill, its rate limits, and its cost so far, as the status line shows them: `{ model, contextWindow: { tokenCount?, size?, percent? }, rateLimits?: { type, percent, resetsAt? }[], cost?: { usd } }`. Each rate limit is one window of the account's plan: its `type`, such as `five_hour`, `seven_day`, or a Claude gateway's `spend_limit`, how much of it is used, 0 to 100, and when it resets, as an ISO 8601 time. For an ended session and a task's owner it answers the model and the tokens of its last reply, from its transcript, with no rate limits.
|
|
257
279
|
- `surfaces` answers the Claude apps showing the session now: `'terminal'`, `'desktop'`, `'mobile'`, and `'vscode'`. An ended session shows in none.
|
|
258
280
|
|
|
259
|
-
A task's owner is a `TaskOwner`, a session without `startTask` and `callTool`, because Claude Code gives a subagent no way to start a task or run a tool from outside. It takes `send`, `usage`, and `surfaces`. An action on a session that has ended rejects with `session <id> has ended`, and `send` with `<mod>: session <id> has ended, so it takes no messages.`
|
|
281
|
+
A task's owner is a `TaskOwner`, a session without `startTask`, `fork`, `clone`, and `callTool`, because Claude Code gives a subagent no way to start a task or run a tool from outside. It takes `send`, `usage`, and `surfaces`. An action on a session that has ended rejects with `session <id> has ended`, and `send` with `<mod>: session <id> has ended, so it takes no messages.`
|
|
260
282
|
|
|
261
283
|
A prompt that comes on a clock, such as every 10 minutes, belongs in a `schedule` job instead, which Claude Code shows as one `Running scheduled task` row ([jobs.md](jobs.md#schedule)).
|
|
262
284
|
|
|
@@ -316,6 +338,7 @@ type MessageOf<Type> = {
|
|
|
316
338
|
| `'toolResult'` | `ToolResult` | `callId`, `name`, `input`, `cwd`, `files`, `output`, `isError`: the call's tool, input, folder, and files, and what the tool answered. `output` is the text Claude read, or the tool's own result for `callTool`. |
|
|
317
339
|
| `'skill'` | `Skill` | `name`, `text`: a Skill Claude Code loaded. |
|
|
318
340
|
| `'instructionFile'` | `InstructionFile` | `path`, `text`, `isRule`: a `CLAUDE.md` file or a rule file Claude Code read. `isRule` is true for a file in a `.claude/rules/` folder. |
|
|
341
|
+
| `'attachedFile'` | `AttachedFile` | `path`, `attachmentType`, `text`: a file Claude Code attached by itself, without a tool call. `attachmentType` is `'contents'` for the whole file, such as one the person mentioned with `@` or one Claude Code put back after a compaction, `'reference'` for a file Claude read before a compaction whose contents Claude Code left out, and `'change'` for what changed in a file Claude read since it read it. `text` is what Claude reads. A file Claude reads with a tool is a `ToolResult` instead. |
|
|
319
342
|
| `'notice'` | `Notice` | `text`: a line Claude Code added, such as a model switch. |
|
|
320
343
|
|
|
321
344
|
### files: what a tool call reads and changes
|
|
@@ -380,6 +403,8 @@ type Context = { readonly number: number; readonly openedAt: string; readonly cl
|
|
|
380
403
|
|
|
381
404
|
type Goal = { text(): Promise<string>; tasks(): Promise<Task[]>; search; on…; before…; update(change: { text: string }): Promise<void> }
|
|
382
405
|
|
|
406
|
+
type TaskStatus = 'pending' | 'running' | 'waiting' | 'idle' | 'completed' | 'failed' | 'killed'
|
|
407
|
+
|
|
383
408
|
type Task = {
|
|
384
409
|
readonly id: string
|
|
385
410
|
readonly title: string
|
|
@@ -387,6 +412,10 @@ type Task = {
|
|
|
387
412
|
readonly openedBy: string
|
|
388
413
|
readonly openedAt: string
|
|
389
414
|
readonly closedAt?: string
|
|
415
|
+
readonly status?: TaskStatus
|
|
416
|
+
readonly name?: string
|
|
417
|
+
readonly spawnedBy?: string
|
|
418
|
+
readonly isBackground?: true
|
|
390
419
|
tasks(): Promise<Task[]>
|
|
391
420
|
messages(): Promise<Message[]>
|
|
392
421
|
search; on…; before…
|
|
@@ -405,7 +434,9 @@ sessions.current.onTurnEnd((turn) => {
|
|
|
405
434
|
- A context's `number` counts from 1. Its `closedAt` is set at the compaction that ended it.
|
|
406
435
|
- A goal's text starts as the session's first prompt, and `text()` reads it. `update` replaces it, and `onGoalChange` hears the goal, whose `text()` reads the new text.
|
|
407
436
|
- A task's `owner` is a `TaskOwner`: the session doing the work, which can't start tasks or call tools. Its goal is the task's prompt, its `agentType` and `model` are what the task runs, and its turns and messages are the task's own. Claude Code's checklist tools, `TaskCreate`, `TaskUpdate`, and `TaskList`, are not tasks: they are its todo list, which `onTodoCreate` and `onTodoComplete` hear.
|
|
408
|
-
- A task's `openedBy` is the `callId` of the Agent call that started it, and empty for a task a mod started. Its `closedAt` is set once it answered.
|
|
437
|
+
- A task's `openedBy` is the `callId` of the Agent call that started it, and empty for a task a mod started. Its `closedAt` is set once it answered: a subagent's when it stops, and a background session's at that session's first stop, which its cmod tells this session.
|
|
438
|
+
- A task with `isBackground` is done by a separate session, whose `id` is the task's. Its owner reads that session's own turns and messages, and `task.messages()`, `task.tasks()`, and `task.search` read them too. `task.on…` and `task.before…` hear the task's start and end, and `task.owner.on…` hears what happens in that session. Claude Code's own fork agent reads as a fork: an Agent call that forks shows an owner `forkedFrom` this session, with no `agentType`.
|
|
439
|
+
- A task of `sessions.current` that Claude Code still runs holds `status`, where its agent stands now, `name`, the name a teammate is messaged by, and `spawnedBy`, the plugin that started it, absent when Claude or the person did. A task read from another session or after its agent ended has none of these. A background task holds the `status` `claude agents` lists for its session.
|
|
409
440
|
|
|
410
441
|
## Test it
|
|
411
442
|
|
package/docs/testing.md
CHANGED
|
@@ -64,10 +64,10 @@ type TestedMod<State> = {
|
|
|
64
64
|
fire(event, input, below?): Promise<EventResult>
|
|
65
65
|
settle(): Promise<void>
|
|
66
66
|
add(message: MessageDraft, options?: { task?: string }): Promise<Message[]>
|
|
67
|
-
endTurn(ending?: { answer?: string; failure?: { type: string; text: string } }): Promise<void>
|
|
67
|
+
endTurn(ending?: { answer?: string; failure?: { type: string; text: string } }, options?: { task?: string }): Promise<void>
|
|
68
68
|
compact(): Promise<void>
|
|
69
69
|
startSession(start: { source: 'startup' | 'resume' | 'clear' | 'fork'; id?: string }): Promise<void>
|
|
70
|
-
startTask(task: { title: string; agentType?: string; model?: string; openedBy?: string; parent?: string }): Promise<string>
|
|
70
|
+
startTask(task: { title: string; agentType?: string; model?: string; openedBy?: string; parent?: string; isBackground?: true; forkedFrom?: string }): Promise<string>
|
|
71
71
|
lines(area: Area, props?: { columns?: number }): Promise<string[]>
|
|
72
72
|
lines(kind: Row, props, requestId?): Promise<string[]>
|
|
73
73
|
type(line: string): Promise<CommandReply>
|
|
@@ -123,6 +123,7 @@ fire(event: 'tool.check' | 'prompt.context' | 'cmod.call' | 'ui.render' | 'ui.sc
|
|
|
123
123
|
- `fire('prompt.context', { blocks: [] }, { blocks: [] })` starts the conversation's context, which gives each `prompt` job its first block.
|
|
124
124
|
- `fire('cmod.call', { to, method, input })` calls a method `setup` returned, as another mod's `mod.dependencies` does, and returns `{ value }` with what the method returned, or `{ deny }` with the reason it failed. `tested.api.<method>(input)` does the same and resolves the value, or rejects with the reason.
|
|
125
125
|
- `ui.render`, `ui.scroll`, and `ui.close` are what `lines` and a pane's own events raise. `shown.pane.lines()` and `lines` are the way to draw.
|
|
126
|
+
- `fire('ui.close', { id, origin: { kind: 'person' } })` closes the pane as Escape or its ✕ does, and draws it once more before it closes, as Claude Code does. A pane's `ui.render` shows a pane Claude Code held back, as Claude Code does when the terminal widens.
|
|
126
127
|
|
|
127
128
|
```ts
|
|
128
129
|
expect(await tested.fire('cmod.call', { to: 'safe-delete', method: 'restore', input: { path: 'a.ts' } })).toEqual({ value: { restored: true } })
|
|
@@ -179,12 +180,12 @@ Waits until the work a listener started without awaiting it has run, such as a `
|
|
|
179
180
|
The current session, `test-session`, starts empty. A test builds the conversation the mod reads through `sessions` ([session.md](session.md)), the way Claude Code would write it:
|
|
180
181
|
|
|
181
182
|
- `add(message, { task? })` adds a message and returns it, placed in its context and turn. A `Prompt` opens a new turn. With `task`, the message is that task's.
|
|
182
|
-
- `endTurn({ answer?, failure? })` ends the open turn, as Claude stopping does. `answer` is Claude's last words, and `failure` ends it on an error, such as `{ type: 'rate_limit', text: 'Rate limited' }`.
|
|
183
|
+
- `endTurn({ answer?, failure? })` ends the open turn, as Claude stopping does. `answer` is Claude's last words, and `failure` ends it on an error, such as `{ type: 'rate_limit', text: 'Rate limited' }`. `{ task }` ends the open turn of that task's owner instead, and the first one a background task's session ends also ends the task.
|
|
183
184
|
- `compact()` records a compaction boundary, which ends the context and starts the next, as a compaction does.
|
|
184
185
|
- `startSession({ source, id? })` starts a session as `/clear`, `/resume`, or a fork does, under `id` when it names one. The mod's `session` values are that session's, so a `resume` to `test-session` brings the first session's values back, and `onSessionStart` hears the `source`.
|
|
185
|
-
- `startTask({ title, agentType?, model?, openedBy?, parent? })` starts a task and returns its id, under the task `parent` names when it has one. Its owner runs `agentType` on `model`, and a `Prompt` added with its `task` opens the owner's own turn.
|
|
186
|
+
- `startTask({ title, agentType?, model?, openedBy?, parent?, isBackground?, forkedFrom? })` starts a task and returns its id, under the task `parent` names when it has one. Its owner runs `agentType` on `model`, and a `Prompt` added with its `task` opens the owner's own turn. `forkedFrom: 'test-session'` starts a fork, `isBackground: true` a background task whose owner is a session of its own, and both together a clone. A message added with a background task's `task` goes into its owner's session.
|
|
186
187
|
|
|
187
|
-
Each one runs the mod's session listeners, as a new message does in Claude Code. A `MessageDraft` is a message without its place: `{ type: Prompt, text, from? }`, `{ type: Note, text, from? }`, `{ type: Reply, text }`, `{ type: ToolCall, name, input?, callId? }`, `{ type: ToolResult, callId, output, isError? }`, `{ type: Skill, name, text }`, `{ type: InstructionFile, path, text }`, or `{ type: Notice, text }`.
|
|
188
|
+
Each one runs the mod's session listeners, as a new message does in Claude Code. A `MessageDraft` is a message without its place: `{ type: Prompt, text, from? }`, `{ type: Note, text, from? }`, `{ type: Reply, text }`, `{ type: ToolCall, name, input?, callId? }`, `{ type: ToolResult, callId, output, isError? }`, `{ type: Skill, name, text }`, `{ type: InstructionFile, path, text }`, `{ type: AttachedFile, path, attachmentType, text }`, or `{ type: Notice, text }`.
|
|
188
189
|
|
|
189
190
|
The `sessions` option gives the earlier conversations, each a `FakeSession`: an `id`, its `messages` as drafts, its `goal`, and any `Session` field, such as `title`, `project`, or `isRunning`. A session that names no `project` is in the test's project.
|
|
190
191
|
|
|
@@ -204,13 +205,14 @@ test('/recall finds the earlier session about the deploy', async () => {
|
|
|
204
205
|
})
|
|
205
206
|
```
|
|
206
207
|
|
|
207
|
-
`shown.sent` lists what the mod sent to other sessions and to tasks' owners, each with its `session`, and its `task` for an owner. `fakes.agent.spawn` answers the current session's `startTask`, such as `async () => ({ agentId: 'agent-7', model: 'test-model' })`, and `fakes.tool.call` its `callTool`.
|
|
208
|
+
`shown.sent` lists what the mod sent to other sessions and to tasks' owners, each with its `session`, and its `task` for an owner. `fakes.agent.spawn` answers the current session's `startTask` and `fork`, such as `async () => ({ agentId: 'agent-7', model: 'test-model' })`, and `fakes.tool.call` its `callTool`. A background `startTask` and a `clone` start their task in the fake session, after asking the mod's `beforeTaskStart` listeners, and so do another session's `startTask` and `fork`.
|
|
208
209
|
|
|
209
210
|
A `before` listener on the current session answers what `raise` asks, such as `tested.raise.beforePrompt({ text: 'Deploy to prod' })` or `tested.raise.beforeToolCall({ name: 'Bash', input: { command: 'rm -rf build' } })`.
|
|
210
211
|
|
|
211
212
|
### lines
|
|
212
213
|
|
|
213
|
-
- `shown.page.lines()` draws the page the mod shows in the Sidebar now, and `shown.pane.lines()` the pane it shows now, and each returns its rows of text. Each rejects when the mod shows none, such as `No pane of notes shows. Open one with pane.open(), from a slash command, a hotkey, or setup.` A page gets `params`, `isFocused`, and `columns`, 80 unless `lines({ columns })` gives another. A pane gets `params`, `title`, `isFocused`, true once an `open` or `toggle` with `{ focus: true }` opened it, `bodyColumns`, `placement: 'dock'`, `scroll: { offset: 0, bodyRows: 24 }`, and `view: {}`. `shown.page.id` and `shown.pane.id` name what shows, and `params` holds its params.
|
|
214
|
+
- `shown.page.lines()` draws the page the mod shows in the Sidebar now, and `shown.pane.lines()` the pane it shows now, and each returns its rows of text. Each rejects when the mod shows none, such as `No pane of notes shows. Open one with pane.open(), from a slash command, a hotkey, or setup.` A page gets `params`, `isFocused`, and `columns`, 80 unless `lines({ columns })` gives another. A pane gets `params`, `title`, `isFocused`, true once an `open` or `toggle` with `{ focus: true }` opened it, `bodyColumns`, which is the `columns` that `lines({ columns })` gives, else the pane's own `columns`, else 80, `placement: 'dock'`, `scroll: { offset: 0, bodyRows: 24 }`, and `view: {}`. `shown.page.id` and `shown.pane.id` name what shows, and `params` holds its params.
|
|
215
|
+
- A `Markdown` element in the mod's pages, panes, and widgets draws through the mod's own markdown styles, as it does in a session. Other mods' styles are not drawn.
|
|
214
216
|
- A `Button` draws as Claude Code draws it: `[ Save ]`, or `1: Save` when it is `plain` with a `hotkey`, or `Save` when it is `plain` alone. Borders and colors are not drawn.
|
|
215
217
|
- `lines(area)` draws the mod's widgets in that area, in the rows the person would see, and returns them. A new widget is in the first row of its first area. A layout given in `files` as `/test/home/.local/share/cmod/widgets.json` places the widgets another way. `{ columns }` sets the row's width, 80 by default. `lines(TurnEnd)` draws under the last turn that ended, and rejects when none has: end one with `tested.endTurn()` first.
|
|
216
218
|
- `lines(kind, props, requestId?)` draws the mod's style of that kind with `props` and returns its rows. `Default` draws a plain listing of the props it gets, so a test sees what the style changed. Draw `Reply` with a reply's `text` to test a markdown style. Draws that pass one `requestId` grow one streamed reply.
|
|
@@ -289,7 +291,7 @@ type Fakes = {
|
|
|
289
291
|
config: { list? }
|
|
290
292
|
command: { list?, run? }
|
|
291
293
|
ui: { ask?, open?, scroll?, copy?, focus? }
|
|
292
|
-
session: { messages?, append? }
|
|
294
|
+
session: { messages?, append?, send?, surfaces?, usage? }
|
|
293
295
|
prompt: { submit? }
|
|
294
296
|
model: { complete? }
|
|
295
297
|
agent: { list?, spawn? }
|
|
@@ -301,8 +303,10 @@ type Fakes = {
|
|
|
301
303
|
|
|
302
304
|
Set a fake to answer a call:
|
|
303
305
|
|
|
306
|
+
- `session.surfaces` answers `['terminal']` unless set. Set it to `async () => []` to test a session no Claude app shows, such as `claude -p`.
|
|
304
307
|
- A call with no fake throws `<call>(<args>) has no fake answer. Set fakes.<call> on the tested mod.` So set `fakes.process.run` before a test runs a program, and `fakes.ui.ask` before the mod asks a question.
|
|
305
308
|
- A `program` job is the exception: it catches the `process.spawn` error and retries after 1, 2, 4, 8 and 16 seconds on real timers, so its `ready()` waits past `bun test`'s 5-second timeout. Set `fakes.process.spawn` before a test starts a `program` job.
|
|
309
|
+
- A `program` job with `singleInstance` spawns `/test/home/.local/share/cmod/programs/cmod program <mod> -- <command…>`, so `fakes.process.spawn` gets that `argv`. Each line the fake writes to standard output is the address of the copy that runs now, so a test fakes another session's copy taking over by writing a second line, and `ready()` resolves the new address.
|
|
306
310
|
- `files` already fills the `fs` fakes: a write is read back, and `list` lists the given files. The fake file system resolves a relative path against `/`, not against the session's working folder, so a mod under test reads `${sessions.current.project}/<file>` rather than a relative path. `fs.list()` with no path rejects.
|
|
307
311
|
- `ui.open` places every pane by default. A fake that answers `{ isPlaced: false }` holds a pane back.
|
|
308
312
|
- `ui.scroll` answers `{}` by default, as when the window moved. A fake that answers `{ deny }` refuses the scroll.
|
|
@@ -315,7 +319,7 @@ Set a fake to answer a call:
|
|
|
315
319
|
- `command.list` answers the slash commands the mod added by default, and `command.run` answers `{}`, a command that printed nothing.
|
|
316
320
|
- `tool.call` answers `CronCreate`, `CronDelete`, and `CronList` by default, keeping the crons a `schedule` makes in `shown.schedules`. A call to any other tool needs a fake.
|
|
317
321
|
- `files` takes symbolic links too: `testMod(mod, { files, links: { '/work/skills': '/shared/skills' } })` makes `/work/skills` a link to `/shared/skills`, so a test of `fs.find` or `metadata.update` follows it as a real file system does.
|
|
318
|
-
- `testMod`
|
|
322
|
+
- `testMod` opens no install page: an option with no default and no value fails `start()` with its fix, and the `installer` steps are not shown. Test a step's `render` and `isDone` by calling them with the started `mod`.
|
|
319
323
|
- The `committer` below runs `git`, so its `package.json` declares `"permissions": { "run": ["git"] }`. A gated call the mod does not declare fails before it reaches a fake ([permissions.md](permissions.md)).
|
|
320
324
|
|
|
321
325
|
```ts
|
|
@@ -367,7 +371,7 @@ type ShownPart = {
|
|
|
367
371
|
type ShownToast = { readonly type: ToastType; readonly message: string; readonly description?: string }
|
|
368
372
|
```
|
|
369
373
|
|
|
370
|
-
What the person and Claude would see: the toasts shown now, with a text description, the notes the mod added for Claude with `sessions.current.send` or a `prompt` that follows the state, the prompts it sent to the current session, the messages it sent to other sessions in `sent`, the log lines, the debug log lines, the open panes, the slash commands and tools the mod added, and the crons its `schedule` keeps now.
|
|
374
|
+
What the person and Claude would see: the toasts shown now, with a text description, the notes the mod added for Claude with `sessions.current.send` or a `prompt` that follows the state, and the `beforePrompt` notes a subagent reads as it starts from a spawn the test fires, the prompts it sent to the current session, the messages it sent to other sessions in `sent`, the log lines, the debug log lines, the open panes, the slash commands and tools the mod added, and the crons its `schedule` keeps now.
|
|
371
375
|
|
|
372
376
|
```ts
|
|
373
377
|
test('the Mode prompt follows the mode', async () => {
|