@cmodjs/core 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/commands.md +39 -22
- 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 +94 -66
- package/docs/options.md +2 -2
- package/docs/permissions.md +7 -5
- 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 +14 -8
- package/records.d.ts.map +1 -1
- package/records.js +44 -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/permission-check.js +12 -4
- 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 +50 -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 +13 -5
- 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/session.md
CHANGED
|
@@ -34,7 +34,7 @@ type Sessions = {
|
|
|
34
34
|
readonly current: CurrentSession
|
|
35
35
|
list(options?: { running?: boolean; since?: string; everyProject?: boolean }): Promise<Session[]>
|
|
36
36
|
search(text: string, matcher?: Matcher<Message>, options?: { running?: boolean; since?: string; everyProject?: boolean; before?: Message }): AsyncIterable<Message>
|
|
37
|
-
onMessage(matcher?, listener, options?):
|
|
37
|
+
onMessage(matcher?, listener, options?): Subscription // and every on method below
|
|
38
38
|
}
|
|
39
39
|
```
|
|
40
40
|
|
|
@@ -76,8 +76,8 @@ type Session = {
|
|
|
76
76
|
turns(options?: { last?: number }): Promise<Turn[]>
|
|
77
77
|
messages(options?: { after?: string; last?: number }): Promise<Message[]>
|
|
78
78
|
search(text: string, matcher?: Matcher<Message>): AsyncIterable<Message>
|
|
79
|
-
onMessage(matcher?, listener, options?):
|
|
80
|
-
beforePrompt(matcher?, listener, options?):
|
|
79
|
+
onMessage(matcher?, listener, options?): Subscription // and every on method below
|
|
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
82
|
startTask(task: { prompt: string; title: string; agentType?: string; model?: string; name?: string; cwd?: string }): Promise<Task>
|
|
83
83
|
callTool(call: { name: string; input: Record<string, unknown> }): Promise<ToolResult>
|
|
@@ -109,11 +109,13 @@ const explorers = (await sessions.current.goal.tasks()).filter((task) => task.ow
|
|
|
109
109
|
|
|
110
110
|
A session that has ended keeps its record, so every read works on it.
|
|
111
111
|
|
|
112
|
+
`sessions.current` adds two members for the rows Claude Code draws, `scrollTo(message, options?)` and `caption(toolCall, text)` ([ui.md](ui.md#the-conversations-rows)).
|
|
113
|
+
|
|
112
114
|
### Listen to a record
|
|
113
115
|
|
|
114
116
|
```ts
|
|
115
|
-
part.on<Event>(listener: (payload, time: { deadline: number; signal: AbortSignal }) => unknown, options?: { timeoutMs?: number }):
|
|
116
|
-
part.on<Event>(matcher: Matcher<payload>, listener, options?):
|
|
117
|
+
part.on<Event>(listener: (payload, time: { deadline: number; signal: AbortSignal }) => unknown, options?: { timeoutMs?: number }): Subscription
|
|
118
|
+
part.on<Event>(matcher: Matcher<payload>, listener, options?): Subscription
|
|
117
119
|
```
|
|
118
120
|
|
|
119
121
|
Each event has its own method, such as `onReply` or `onTaskStart`, which runs `listener` for each new message, turn, context, task, goal change, or usage change, as it happens. `sessions`, a session, its goal, a task, a context, and a turn each have every `on` method, and each hears what happens inside it: an event reaches the part it happens in and every part above it.
|
|
@@ -153,7 +155,7 @@ sessions.current.onToolCall({ name: /^(Edit|Write)$/ }, (call) => void (mod.stat
|
|
|
153
155
|
sessions.current.onTaskStart({ owner: { agentType: 'Explore' } }, (task) => mod.log(`Exploring: ${task.title}`))
|
|
154
156
|
```
|
|
155
157
|
|
|
156
|
-
A listener that throws shows the error toast `The <method> listener failed`, such as `The onReply listener failed`. `
|
|
158
|
+
A listener that throws shows the error toast `The <method> listener failed`, such as `The onReply listener failed`. `remove()` on the `Subscription` stops the listener.
|
|
157
159
|
|
|
158
160
|
### Give a listener time
|
|
159
161
|
|
|
@@ -170,8 +172,8 @@ An `on` listener that passes its time stops quietly. A `before` listener that pa
|
|
|
170
172
|
### Change what happens
|
|
171
173
|
|
|
172
174
|
```ts
|
|
173
|
-
part.before<Event>(listener: (payload, time) => Answer | void, options?: { timeoutMs?: number; onFailure?: 'pass' | 'refuse' }):
|
|
174
|
-
part.before<Event>(matcher: Matcher<payload>, listener, options?):
|
|
175
|
+
part.before<Event>(listener: (payload, time) => Answer | void, options?: { timeoutMs?: number; onFailure?: 'pass' | 'refuse' }): Subscription
|
|
176
|
+
part.before<Event>(matcher: Matcher<payload>, listener, options?): Subscription
|
|
175
177
|
```
|
|
176
178
|
|
|
177
179
|
Each `before` method, such as `beforeToolCall`, runs `listener` just before something happens, and its answer changes or refuses it. Returning nothing lets it happen as it was. Each part except `sessions` has every `before` method, and hears what is about to happen inside it, the way `on` methods do.
|
|
@@ -277,7 +279,7 @@ mod.use(slashCommand({
|
|
|
277
279
|
const task = await sessions.current.startTask({ prompt: 'Summarize README.md in one line.', title: 'Summarize the README', agentType: 'Explore' })
|
|
278
280
|
task.onTaskEnd(async (ended) => {
|
|
279
281
|
const [answer] = await ended.owner.messages({ last: 1 })
|
|
280
|
-
if (answer?.type === Reply) mod.
|
|
282
|
+
if (answer?.type === Reply) mod.toast(answer.text)
|
|
281
283
|
})
|
|
282
284
|
return 'Summarizing the README.'
|
|
283
285
|
},
|
|
@@ -300,14 +302,14 @@ type MessageOf<Type> = {
|
|
|
300
302
|
}
|
|
301
303
|
```
|
|
302
304
|
|
|
303
|
-
- `id` names the transcript row the message is in. When one row holds several messages, such as a reply with its tool calls, each `id` ends in `:<n>`. `
|
|
305
|
+
- `id` names the transcript row the message is in. When one row holds several messages, such as a reply with its tool calls, each `id` ends in `:<n>`. `sessions.current.scrollTo(message)` scrolls to the message's row ([ui.md](ui.md#the-conversations-rows)).
|
|
304
306
|
- `at` is its ISO time. `session` is the session's id, and `context` the number of the context it is in, from 1.
|
|
305
307
|
- `turn` is the `id` of the prompt that opened its turn. A task's message names its owner's turn. A message before the first prompt has none.
|
|
306
308
|
- `task` is the id of the task whose message it is. A message of the session itself has none.
|
|
307
309
|
|
|
308
310
|
| Type | Constant | Its own fields |
|
|
309
311
|
| --- | --- | --- |
|
|
310
|
-
| `'prompt'` | `Prompt` | `text`, `from`: the prompt that opens a turn. A command Claude Code sends as a prompt reads as typed, such as `/review 12`. |
|
|
312
|
+
| `'prompt'` | `Prompt` | `text`, `from`: the prompt that opens a turn. A command Claude Code sends as a prompt reads as typed, such as `/review 12`. A command a mod runs, such as cmod's `/reload-plugins`, is no prompt, and `commands.onRun` hears it. |
|
|
311
313
|
| `'note'` | `Note` | `text`, `from`: text Claude reads that is not a prompt, such as a mod's note or a task's notification. |
|
|
312
314
|
| `'reply'` | `Reply` | `text`: what Claude said. |
|
|
313
315
|
| `'toolCall'` | `ToolCall` | `callId`, `name`, `input`, `cwd`, `files`: a tool Claude called, the folder it called it in, and the files it reads and changes. |
|
|
@@ -396,7 +398,7 @@ type Task = {
|
|
|
396
398
|
|
|
397
399
|
```ts
|
|
398
400
|
sessions.current.onTurnEnd((turn) => {
|
|
399
|
-
if (turn.failure !== undefined) mod.
|
|
401
|
+
if (turn.failure !== undefined) mod.toast.error(`Claude stopped: ${turn.failure.text}`)
|
|
400
402
|
})
|
|
401
403
|
```
|
|
402
404
|
- `abort()` stops a turn while it runs, a task's turn included, and needs the `prompt` permission. It throws on a turn that has ended. Claude Code shows no marker, so a mod that aborts tells the person with a toast.
|
package/docs/testing.md
CHANGED
|
@@ -19,7 +19,7 @@ test('/todo adds the item to the project list', async () => {
|
|
|
19
19
|
})
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Assert what a person or Claude sees: `tested.lines(...)`, `tested.shown`, `tested.state`, and what `tested.raise...`, `tested.type(...)`, and `tested.callTool(...)` return.
|
|
22
|
+
Assert what a person or Claude sees: `tested.shown.page.lines()`, `tested.lines(...)`, `tested.shown`, `tested.state`, and what `tested.raise...`, `tested.type(...)`, and `tested.callTool(...)` return.
|
|
23
23
|
|
|
24
24
|
## TestOptions
|
|
25
25
|
|
|
@@ -46,7 +46,7 @@ type TestOptions<State, Options> = {
|
|
|
46
46
|
| `scope` | `'project'` makes the mod a project plugin, in `<projectRoot>/.claude/skills/<name>` | `'user'` |
|
|
47
47
|
| `projectRoot` | `sessions.current.project` | `/test/plugins/<name>`, or `/test/project` for a project plugin |
|
|
48
48
|
| `cwd` | `sessions.current.cwd` | `projectRoot` |
|
|
49
|
-
| `dependencies` | The methods other mods answer with | none: every call fails with `<name> is not installed.
|
|
49
|
+
| `dependencies` | The methods other mods answer with | none: every call fails with `<name> is not installed. Install it in Plugins.` |
|
|
50
50
|
| `files` | The fake file system, absolute path to text | no files |
|
|
51
51
|
| `links` | Symbolic links in the fake file system, absolute link path to its target | no links |
|
|
52
52
|
| `sessions` | The earlier conversations `sessions.list` and `sessions.search` read, each `{ id, messages?, goal?, … }` ([Build a conversation](#build-a-conversation)) | none: only the current session, `test-session` |
|
|
@@ -68,14 +68,14 @@ type TestedMod<State> = {
|
|
|
68
68
|
compact(): Promise<void>
|
|
69
69
|
startSession(start: { source: 'startup' | 'resume' | 'clear' | 'fork'; id?: string }): Promise<void>
|
|
70
70
|
startTask(task: { title: string; agentType?: string; model?: string; openedBy?: string; parent?: string }): Promise<string>
|
|
71
|
-
lines(
|
|
72
|
-
lines(
|
|
71
|
+
lines(area: Area, props?: { columns?: number }): Promise<string[]>
|
|
72
|
+
lines(kind: Row, props, requestId?): Promise<string[]>
|
|
73
73
|
type(line: string): Promise<CommandReply>
|
|
74
|
+
hotkey(name: string): Promise<void>
|
|
74
75
|
callTool(name: string, input: Record<string, unknown>): Promise<EventResult<'tool.call'>>
|
|
75
|
-
press(
|
|
76
|
-
input(paneId: string, key: string, text: string, type?: 'submit' | 'change'): Promise<void>
|
|
77
|
-
select(paneId: string, key: string, value: string): Promise<void>
|
|
76
|
+
press(area: Area, key: string): Promise<void>
|
|
78
77
|
moveTo(projectRoot: string, cwd?: string): Promise<void>
|
|
78
|
+
readonly api: { [method: string]: (input?: unknown) => Promise<unknown> }
|
|
79
79
|
readonly state: Readonly<State>
|
|
80
80
|
readonly calls: readonly TestCall[]
|
|
81
81
|
readonly fakes: Fakes
|
|
@@ -85,7 +85,7 @@ type TestedMod<State> = {
|
|
|
85
85
|
|
|
86
86
|
### start
|
|
87
87
|
|
|
88
|
-
Starts the mod: loads its state, runs `setup`, and adds its listeners, panes, and jobs. `raise`, `fire`, `lines`, `type`, `
|
|
88
|
+
Starts the mod: loads its state, runs `setup`, and adds its listeners, pages, panes, widgets, and jobs. `raise`, `fire`, `lines`, `type`, `hotkey`, `callTool`, `press`, `moveTo`, and `api` start the mod first, so a test calls `start` only before it reads `state` or `shown` with nothing else to run. It rejects with the reason when the mod does not start.
|
|
89
89
|
|
|
90
90
|
### raise
|
|
91
91
|
|
|
@@ -121,8 +121,8 @@ fire(event: 'tool.check' | 'prompt.context' | 'cmod.call' | 'ui.render' | 'ui.sc
|
|
|
121
121
|
|
|
122
122
|
- `fire('tool.check', { tool: 'Edit', input: { … }, tool_use_id: 'toolu_1' }, { decision: 'allow' })` asks the mod's `permissions` job and `beforePermissionCheck` listeners about any call. `below` is Claude Code's own verdict.
|
|
123
123
|
- `fire('prompt.context', { blocks: [] }, { blocks: [] })` starts the conversation's context, which gives each `prompt` job its first block.
|
|
124
|
-
- `fire('cmod.call', { to, method, input })` calls a method
|
|
125
|
-
- `ui.render`, `ui.scroll`, and `ui.close` are what `lines` and a pane's own events raise. `lines`
|
|
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
|
+
- `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
126
|
|
|
127
127
|
```ts
|
|
128
128
|
expect(await tested.fire('cmod.call', { to: 'safe-delete', method: 'restore', input: { path: 'a.ts' } })).toEqual({ value: { restored: true } })
|
|
@@ -210,30 +210,41 @@ A `before` listener on the current session answers what `raise` asks, such as `t
|
|
|
210
210
|
|
|
211
211
|
### lines
|
|
212
212
|
|
|
213
|
-
- `lines(
|
|
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
214
|
- 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
|
-
- `lines(
|
|
215
|
+
- `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
|
+
- `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.
|
|
216
217
|
|
|
217
218
|
```ts
|
|
218
219
|
import { expect, test } from 'bun:test'
|
|
219
|
-
import
|
|
220
|
-
import { AbovePrompt, Prompt } from '../node_modules/@cmodjs/core/mod.js'
|
|
220
|
+
import { BelowPrompt, Prompt } from '../node_modules/@cmodjs/core/mod.js'
|
|
221
221
|
import { testMod } from '../node_modules/@cmodjs/core/testing.js'
|
|
222
|
-
import {
|
|
222
|
+
import { prompts } from '../src/mod.js'
|
|
223
223
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
test('the band shows the prompt count', async () => {
|
|
227
|
-
const tested = testMod(hints)
|
|
224
|
+
test('the prompt count shows below the prompt', async () => {
|
|
225
|
+
const tested = testMod(prompts)
|
|
228
226
|
await tested.add({ type: Prompt, text: 'hello' })
|
|
229
227
|
|
|
230
|
-
expect(await tested.lines(
|
|
228
|
+
expect(await tested.lines(BelowPrompt)).toEqual(['Prompts: 1'])
|
|
231
229
|
})
|
|
232
230
|
```
|
|
233
231
|
|
|
234
232
|
### type
|
|
235
233
|
|
|
236
|
-
`type('/<command> <arguments>')` runs a slash command as the person types it and returns its reply, `{ text?, note? }`. The mod's `commands.beforeRun` listeners hear it first, so `type('/compact')` tests a listener that refuses a built-in. It rejects a line that does not start with `/`,
|
|
234
|
+
`type('/<command> <arguments>')` runs a slash command as the person types it and returns its reply, `{ text?, note? }`. The mod's `commands.beforeRun` listeners hear it first, so `type('/compact')` tests a listener that refuses a built-in. It rejects a line that does not start with `/`, a command nothing in the mod answers, and a command of the mod that calls `next(e)` on to Claude Code's own run of it, which shows the person `<mod> registered /<command> but no command.run hook answered it`. `hotkey` rejects the same way.
|
|
235
|
+
|
|
236
|
+
### hotkey
|
|
237
|
+
|
|
238
|
+
`hotkey(name)` presses the hotkey `defineMod` declared under `name`, as the person pressing its combo does, and awaits its listeners. A page it opens shows as `shown.page`. It rejects a name the mod does not declare.
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
test('the hotkey shows the prompt count in the Sidebar', async () => {
|
|
242
|
+
const tested = testMod(myMod)
|
|
243
|
+
await tested.add({ type: Prompt, text: 'hello' })
|
|
244
|
+
await tested.hotkey('toggle')
|
|
245
|
+
expect(await tested.shown.page.lines()).toEqual(['Prompts this session: 1'])
|
|
246
|
+
})
|
|
247
|
+
```
|
|
237
248
|
|
|
238
249
|
### callTool
|
|
239
250
|
|
|
@@ -241,17 +252,18 @@ test('the band shows the prompt count', async () => {
|
|
|
241
252
|
|
|
242
253
|
### press, input, and select
|
|
243
254
|
|
|
244
|
-
`
|
|
255
|
+
`shown.page` and `shown.pane` act on an element of what shows as the person does, and await its handler. Each rejects when it does not draw the element: `The pane "<pane>" of <mod> draws no <Element> with the key "<key>".`, or the same for a page.
|
|
245
256
|
|
|
246
|
-
- `press(
|
|
247
|
-
- `press(
|
|
248
|
-
- `input(
|
|
249
|
-
- `select(
|
|
257
|
+
- `press(key)` presses the `Button` whose `key` or `label` is `key`, and runs its `onPress`.
|
|
258
|
+
- `press(key, link)` presses the link `link` in the `Markdown` whose `key` is `key`, and runs its `onLinkPress`.
|
|
259
|
+
- `input(key, text)` types `text` into the `Input` whose `key` is `key` and presses Enter, which runs its `onSubmit`. With a third argument `'change'` it runs `onInput` instead, as one edit does.
|
|
260
|
+
- `select(key, value)` picks the option `value` of the `Select` whose `key` is `key`, and runs its `onSelect`. It rejects an option the `Select` does not have.
|
|
261
|
+
- `tested.press(area, key)` presses a `Button` one of the mod's widgets draws in that area, the way the person presses it in a row.
|
|
250
262
|
|
|
251
263
|
```ts
|
|
252
|
-
await tested.input('
|
|
253
|
-
await tested.select('
|
|
254
|
-
expect(await tested.lines(
|
|
264
|
+
await tested.shown.page.input('search', 'deploy')
|
|
265
|
+
await tested.shown.page.select('agent', 'explorer')
|
|
266
|
+
expect(await tested.shown.page.lines()).toContain('deploy by explorer')
|
|
255
267
|
```
|
|
256
268
|
|
|
257
269
|
### moveTo
|
|
@@ -276,7 +288,7 @@ type Fakes = {
|
|
|
276
288
|
settings: { read? }
|
|
277
289
|
config: { list? }
|
|
278
290
|
command: { list?, run? }
|
|
279
|
-
ui: { ask?, open?, scroll? }
|
|
291
|
+
ui: { ask?, open?, scroll?, copy?, focus? }
|
|
280
292
|
session: { messages?, append? }
|
|
281
293
|
prompt: { submit? }
|
|
282
294
|
model: { complete? }
|
|
@@ -294,6 +306,7 @@ Set a fake to answer a call:
|
|
|
294
306
|
- `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.
|
|
295
307
|
- `ui.open` places every pane by default. A fake that answers `{ isPlaced: false }` holds a pane back.
|
|
296
308
|
- `ui.scroll` answers `{}` by default, as when the window moved. A fake that answers `{ deny }` refuses the scroll.
|
|
309
|
+
- `clipboard.write(text)` reaches `ui.copy({ text })`, so a test keeps each copied text with a fake that answers `{ isCopied: true }`.
|
|
297
310
|
- `clock.now()` answers the real time unless a fake gives its own, so a test of a deadline can move time. `clock.after(ms, fn)` and `clock.every(ms, fn)` each return `{ cancel() }`, and so must a fake of either. `clock.after` runs a real timer by default. `clock.every` never fires unless a fake keeps `fn` for the test to call, so a test of `mod.every` keeps it and calls it once per tick.
|
|
298
311
|
- `session.append` and `prompt.submit` take every note and prompt by default, into `shown.notes` and `shown.prompts`. A fake that answers `{ deny }` or `{ drop }` refuses one.
|
|
299
312
|
- `model.complete` has no default: set it to the answer the model gives, such as `async () => ({ isAnswered: true, text: 'no', usage })`.
|
|
@@ -333,18 +346,28 @@ type Shown = {
|
|
|
333
346
|
readonly prompts: string[]
|
|
334
347
|
readonly logs: string[]
|
|
335
348
|
readonly debug: string[]
|
|
336
|
-
readonly statuses: (string | undefined)[]
|
|
337
349
|
readonly openPanes: Set<string>
|
|
338
350
|
readonly commands: string[]
|
|
339
351
|
readonly tools: string[]
|
|
340
352
|
readonly schedules: { readonly id: string; readonly cron: string; readonly prompt: string }[]
|
|
341
353
|
readonly sent: { readonly session: string; readonly type: string; readonly text: string; readonly as?: string }[]
|
|
354
|
+
readonly page: ShownPart
|
|
355
|
+
readonly pane: ShownPart
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
type ShownPart = {
|
|
359
|
+
readonly id: string
|
|
360
|
+
readonly params: unknown
|
|
361
|
+
lines(options?: { columns?: number }): Promise<string[]>
|
|
362
|
+
press(key: string, link?: string): Promise<void>
|
|
363
|
+
input(key: string, text: string, type?: 'submit' | 'change'): Promise<void>
|
|
364
|
+
select(key: string, value: string): Promise<void>
|
|
342
365
|
}
|
|
343
366
|
|
|
344
367
|
type ShownToast = { readonly type: ToastType; readonly message: string; readonly description?: string }
|
|
345
368
|
```
|
|
346
369
|
|
|
347
|
-
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,
|
|
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.
|
|
348
371
|
|
|
349
372
|
```ts
|
|
350
373
|
test('the Mode prompt follows the mode', async () => {
|