@cmodjs/core 1.6.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/commands.md +7 -7
- package/docs/dependencies.md +1 -1
- package/docs/index.md +12 -11
- package/docs/jobs.md +12 -16
- package/docs/mod.md +111 -36
- package/docs/permissions.md +14 -18
- package/docs/session.md +171 -82
- package/docs/state.md +5 -5
- package/docs/testing.md +57 -41
- package/docs/ui.md +9 -9
- package/jobs/permissions/decide-permission.d.ts.map +1 -1
- package/jobs/permissions/decide-permission.js +1 -1
- package/jobs/prompt.d.ts +1 -3
- package/jobs/prompt.d.ts.map +1 -1
- package/jobs/prompt.js +2 -21
- package/jobs/slash-command.d.ts +1 -1
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +1 -1
- package/jobs/status-line.d.ts +0 -1
- package/jobs/status-line.d.ts.map +1 -1
- package/jobs/status-line.js +3 -4
- package/mod.d.ts +53 -105
- package/mod.d.ts.map +1 -1
- package/mod.js +18 -80
- package/package.json +1 -1
- package/records.d.ts +1 -1
- package/records.js +1 -1
- package/register.d.ts.map +1 -1
- package/register.js +8 -5
- package/runtime/before.d.ts +11 -4
- package/runtime/before.d.ts.map +1 -1
- package/runtime/before.js +131 -67
- package/runtime/claude.d.ts +6 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +82 -38
- package/runtime/deadline.d.ts +17 -0
- package/runtime/deadline.d.ts.map +1 -1
- package/runtime/deadline.js +34 -0
- package/runtime/dependencies.d.ts +2 -0
- package/runtime/dependencies.d.ts.map +1 -1
- package/runtime/dependencies.js +12 -0
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +13 -8
- package/runtime/hooks.d.ts +15 -11
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +8 -165
- package/runtime/lifecycle.d.ts +4 -2
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +96 -62
- package/runtime/permission-check.d.ts +15 -0
- package/runtime/permission-check.d.ts.map +1 -0
- package/runtime/permission-check.js +37 -0
- package/runtime/sessions.d.ts +35 -5
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +304 -82
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +14 -11
- package/runtime/tool-calls.d.ts +0 -1
- package/runtime/tool-calls.d.ts.map +1 -1
- package/runtime/tool-calls.js +0 -19
- package/session/context/turn/message/instruction-file.d.ts +9 -0
- package/session/context/turn/message/instruction-file.d.ts.map +1 -0
- package/session/context/turn/message/instruction-file.js +4 -0
- package/session/context/turn/message/message.d.ts +6 -4
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +12 -8
- package/session/context/turn/message/sender.d.ts +2 -1
- package/session/context/turn/message/sender.d.ts.map +1 -1
- package/session/context/turn/message/sender.js +4 -1
- package/session/context/turn/message/tool-call.d.ts +5 -0
- package/session/context/turn/message/tool-call.d.ts.map +1 -1
- package/session/context/turn/message/tool-result.d.ts +6 -1
- package/session/context/turn/message/tool-result.d.ts.map +1 -1
- package/session/context/turn/turn.d.ts +7 -0
- package/session/context/turn/turn.d.ts.map +1 -1
- package/session/conversation.d.ts +10 -3
- package/session/conversation.d.ts.map +1 -1
- package/session/conversation.js +14 -5
- package/session/goal/goal.d.ts +1 -1
- package/session/goal/goal.d.ts.map +1 -1
- package/session/goal/task.d.ts +2 -2
- package/session/goal/task.d.ts.map +1 -1
- package/session/session.d.ts +361 -26
- package/session/session.d.ts.map +1 -1
- package/session/session.js +40 -0
- package/src/jobs/permissions/decide-permission.ts +2 -1
- package/src/jobs/prompt.ts +3 -20
- package/src/jobs/slash-command.ts +2 -2
- package/src/jobs/status-line.ts +4 -7
- package/src/mod.ts +52 -147
- package/src/records.ts +1 -1
- package/src/register.ts +8 -5
- package/src/runtime/before.ts +140 -66
- package/src/runtime/claude.ts +5 -1
- package/src/runtime/conversations.ts +75 -37
- package/src/runtime/deadline.ts +38 -0
- package/src/runtime/dependencies.ts +12 -0
- package/src/runtime/grants.ts +11 -7
- package/src/runtime/hooks.ts +24 -163
- package/src/runtime/lifecycle.ts +110 -77
- package/src/runtime/permission-check.ts +47 -0
- package/src/runtime/sessions.ts +334 -91
- package/src/runtime/state.ts +15 -8
- package/src/runtime/tool-calls.ts +0 -19
- package/src/session/context/turn/message/instruction-file.ts +13 -0
- package/src/session/context/turn/message/message.ts +17 -11
- package/src/session/context/turn/message/sender.ts +5 -2
- package/src/session/context/turn/message/tool-call.ts +3 -0
- package/src/session/context/turn/message/tool-result.ts +6 -1
- package/src/session/context/turn/turn.ts +4 -0
- package/src/session/conversation.ts +21 -8
- package/src/session/goal/goal.ts +1 -1
- package/src/session/goal/task.ts +2 -2
- package/src/session/session.ts +227 -24
- package/src/testing/claude-code.ts +23 -0
- package/src/testing/fake-claude.ts +5 -1
- package/src/testing/fake-sessions.ts +38 -24
- package/src/testing.ts +88 -19
- package/testing/claude-code.d.ts +12 -0
- package/testing/claude-code.d.ts.map +1 -0
- package/testing/claude-code.js +10 -0
- package/testing/fake-claude.d.ts +4 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +4 -1
- package/testing/fake-sessions.d.ts +8 -6
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +35 -20
- package/testing.d.ts +38 -9
- package/testing.d.ts.map +1 -1
- package/testing.js +58 -8
- package/docs/hooks.md +0 -259
- package/session/context/turn/message/document.d.ts +0 -7
- package/session/context/turn/message/document.d.ts.map +0 -1
- package/session/context/turn/message/document.js +0 -1
- package/src/session/context/turn/message/document.ts +0 -8
package/docs/testing.md
CHANGED
|
@@ -14,12 +14,12 @@ import { todo } from '../src/mod.js'
|
|
|
14
14
|
test('/todo adds the item to the project list', async () => {
|
|
15
15
|
const tested = testMod(todo)
|
|
16
16
|
|
|
17
|
-
expect(await tested.type('/todo buy milk')).toEqual({ text: 'Added: buy milk',
|
|
17
|
+
expect(await tested.type('/todo buy milk')).toEqual({ text: 'Added: buy milk', note: 'The to-do list is now: buy milk' })
|
|
18
18
|
expect(tested.state.project.items).toEqual(['buy milk'])
|
|
19
19
|
})
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Assert what a person or Claude sees: `tested.lines(...)`, `tested.shown`, `tested.state`, and what `tested.
|
|
22
|
+
Assert what a person or Claude sees: `tested.lines(...)`, `tested.shown`, `tested.state`, and what `tested.raise...`, `tested.type(...)`, and `tested.callTool(...)` return.
|
|
23
23
|
|
|
24
24
|
## TestOptions
|
|
25
25
|
|
|
@@ -60,11 +60,13 @@ The fake session's id is `test-session`, its model is `test-model`, and its home
|
|
|
60
60
|
```ts
|
|
61
61
|
type TestedMod<State> = {
|
|
62
62
|
start(): Promise<void>
|
|
63
|
+
readonly raise: Raise
|
|
63
64
|
fire(event, input, below?): Promise<EventResult>
|
|
64
65
|
settle(): Promise<void>
|
|
65
66
|
add(message: MessageDraft, options?: { task?: string }): Promise<Message[]>
|
|
66
|
-
endTurn(): Promise<void>
|
|
67
|
+
endTurn(ending?: { answer?: string; failure?: { type: string; text: string } }): Promise<void>
|
|
67
68
|
compact(): Promise<void>
|
|
69
|
+
startSession(start: { source: 'startup' | 'resume' | 'clear' | 'fork'; id?: string }): Promise<void>
|
|
68
70
|
startTask(task: { title: string; agentType?: string; model?: string; openedBy?: string; parent?: string }): Promise<string>
|
|
69
71
|
lines(paneId: string): Promise<string[]>
|
|
70
72
|
lines(place, props, requestId?): Promise<string[]>
|
|
@@ -83,24 +85,41 @@ type TestedMod<State> = {
|
|
|
83
85
|
|
|
84
86
|
### start
|
|
85
87
|
|
|
86
|
-
Starts the mod: loads its state, runs `setup`, and adds its
|
|
88
|
+
Starts the mod: loads its state, runs `setup`, and adds its listeners, panes, and jobs. `raise`, `fire`, `lines`, `type`, `callTool`, `press`, `input`, `select`, and `moveTo` 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
|
+
|
|
90
|
+
### raise
|
|
91
|
+
|
|
92
|
+
`raise.<method>(payload)` runs the mod's listeners of that `on` or `before` method of `sessions.current`, and of `commands`, and returns the answer they combine to. For a `before` method of a Claude Code event, such as `beforeSessionStart`, `beforePermissionRequest`, or `beforeRun`, that is the answer before cmod drops the parts the mod has no permission for ([permissions.md](permissions.md#when-a-before-answer-has-no-grant)).
|
|
93
|
+
|
|
94
|
+
- Each process method takes its payload without `session` and `cwd`, which `raise` fills: `raise.beforeModelChange({ model: 'haiku', previous: 'opus', source: 'command', … })`, `raise.onCwdChange({ previous: '/work/old' })`, `raise.beforeRun({ name: 'compact', args: '', from: { type: Person } })`.
|
|
95
|
+
- Each message `before` method takes a `MessageDraft` without its `type`, placed in the open turn: `raise.beforeToolCall({ name: 'Bash', input: { command: 'rm -rf build' } })` and `raise.beforePrompt({ text: 'Deploy to prod' })`. `{ task }` places it in a task.
|
|
96
|
+
- `raise.beforeTaskStart({ title, agentType?, model? })` asks about a task about to start.
|
|
97
|
+
- `raise.beforeTurnEnd({ answer?, failure? })` asks about the open turn ending. It rejects when no turn is open: add a `Prompt` first.
|
|
98
|
+
- `raise.beforeContextEnd()` asks about a compaction of the open context.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
test('a force push is refused', async () => {
|
|
102
|
+
const tested = testMod(guard)
|
|
103
|
+
await tested.add({ type: Prompt, text: 'Push it' })
|
|
104
|
+
|
|
105
|
+
expect(await tested.raise.beforeToolCall({ name: 'Bash', input: { command: 'git push --force' } })).toEqual({ refuse: 'Force pushes rewrite shared history.' })
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The `on` methods of the conversation, such as `onPrompt` and `onTurnEnd`, run when the test builds the conversation ([Build a conversation](#build-a-conversation)).
|
|
87
110
|
|
|
88
111
|
### fire
|
|
89
112
|
|
|
90
113
|
```ts
|
|
91
|
-
fire
|
|
92
|
-
fire<N extends RoutedEvent>(event: N, input: Args<N>, below?): Promise<EventResult<N>>
|
|
114
|
+
fire(event: 'tool.check' | 'prompt.context' | 'cmod.call' | 'ui.render' | 'ui.scroll' | 'ui.close', input: Args<event>, below?): Promise<EventResult<event>>
|
|
93
115
|
```
|
|
94
116
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
-
|
|
98
|
-
- `fire(
|
|
99
|
-
- On every other event, `below` is what the hooks beneath the mod answered. Pass it to test how the mod's answer combines with another. When nothing in the mod answers, `fire` returns `below` as it is.
|
|
100
|
-
- Left out, `below` is `{}`, except on `prompt.submit`, where it is what Claude Code answers: the prompt that entered, `{ text, context, origin }`, with the `context` the mod's hooks added.
|
|
101
|
-
- It returns the combined answer in Claude Code's own field names: `additionalContext` is a list, and `decision: 'block'` is `block`.
|
|
102
|
-
- `fire('tool.check', { tool: 'Edit', input: { … }, tool_use_id: 'toolu_1' }, { decision: 'allow' })` asks the mod's permission rules about any call. A name with a dot fires that Claude Code hook-module event as it is.
|
|
117
|
+
`fire` raises the Claude Code events no method covers:
|
|
118
|
+
|
|
119
|
+
- `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.
|
|
120
|
+
- `fire('prompt.context', { blocks: [] }, { blocks: [] })` starts the conversation's context, which gives each `prompt` job its first block.
|
|
103
121
|
- `fire('cmod.call', { to, method, input })` calls a method of the mod's `api`, as another mod's `mod.dependencies` does, and returns `{ value }` with what the method returned, or `{ deny }` with the reason it failed.
|
|
122
|
+
- `ui.render`, `ui.scroll`, and `ui.close` are what `lines` and a pane's own events raise. `lines` is the way to draw.
|
|
104
123
|
|
|
105
124
|
```ts
|
|
106
125
|
expect(await tested.fire('cmod.call', { to: 'safe-delete', method: 'restore', input: { path: 'a.ts' } })).toEqual({ value: { restored: true } })
|
|
@@ -120,21 +139,17 @@ test('a force push is denied with the reason', async () => {
|
|
|
120
139
|
})
|
|
121
140
|
```
|
|
122
141
|
|
|
123
|
-
### Test a
|
|
124
|
-
|
|
125
|
-
A `prompt` with `when` runs on the hook-module event `prompt.submit`, not on `UserPromptSubmit`, so `fire(UserPromptSubmit, …)` never reaches it. `fire('prompt.submit', …)` returns the prompt that entered, with the text the prompt added in its `context`:
|
|
142
|
+
### Test a note on matching prompts
|
|
126
143
|
|
|
127
|
-
|
|
128
|
-
- `context` is a list of strings, each `# <name>\n<text>`. The answer has no `context` when no prompt added anything.
|
|
144
|
+
A mod that adds a note to the prompts about a release answers `beforePrompt`, and `raise.beforePrompt` asks it:
|
|
129
145
|
|
|
130
146
|
```ts
|
|
131
|
-
import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
|
|
132
|
-
import { prompt } from '../node_modules/@cmodjs/core/jobs/prompt.js'
|
|
147
|
+
import { defineMod, sessions } from '../node_modules/@cmodjs/core/mod.js'
|
|
133
148
|
|
|
134
149
|
export const release = defineMod({
|
|
135
150
|
name: 'release',
|
|
136
|
-
setup(
|
|
137
|
-
|
|
151
|
+
setup() {
|
|
152
|
+
sessions.current.beforePrompt({ text: /\brelease\b/i }, () => ({ note: 'Run bun run release, then push the tag.' }))
|
|
138
153
|
},
|
|
139
154
|
})
|
|
140
155
|
```
|
|
@@ -146,30 +161,27 @@ import { release } from '../src/mod.js'
|
|
|
146
161
|
|
|
147
162
|
test('a prompt about a release gets the release steps, and another prompt gets nothing', async () => {
|
|
148
163
|
const tested = testMod(release)
|
|
149
|
-
const typed = { wait: false, origin: { kind: 'composer' } } as const
|
|
150
164
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
expect(aboutRelease).toEqual({ text: 'Cut a release', context: ['# release-steps\nRun bun run release, then push the tag.'], origin: typed.origin })
|
|
155
|
-
expect(aboutBug).toEqual({ text: 'Fix the bug', origin: typed.origin })
|
|
165
|
+
expect(await tested.raise.beforePrompt({ text: 'Cut a release' })).toEqual({ note: 'Run bun run release, then push the tag.' })
|
|
166
|
+
expect(await tested.raise.beforePrompt({ text: 'Fix the bug' })).toBeUndefined()
|
|
156
167
|
})
|
|
157
168
|
```
|
|
158
169
|
|
|
159
170
|
### settle
|
|
160
171
|
|
|
161
|
-
Waits until the work a
|
|
172
|
+
Waits until the work a listener started without awaiting it has run, such as a `void` call in an `onTurnEnd` listener. It returns while a fake the test holds open has not answered.
|
|
162
173
|
|
|
163
174
|
### Build a conversation
|
|
164
175
|
|
|
165
176
|
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:
|
|
166
177
|
|
|
167
178
|
- `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.
|
|
168
|
-
- `endTurn()` ends the open turn, as Claude stopping does.
|
|
179
|
+
- `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' }`.
|
|
169
180
|
- `compact()` records a compaction boundary, which ends the context and starts the next, as a compaction does.
|
|
181
|
+
- `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`.
|
|
170
182
|
- `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.
|
|
171
183
|
|
|
172
|
-
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,
|
|
184
|
+
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 }`.
|
|
173
185
|
|
|
174
186
|
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.
|
|
175
187
|
|
|
@@ -191,7 +203,7 @@ test('/recall finds the earlier session about the deploy', async () => {
|
|
|
191
203
|
|
|
192
204
|
`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`. Another session's `startTask` starts a task in that fake session.
|
|
193
205
|
|
|
194
|
-
A `before` listener on the current session answers
|
|
206
|
+
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' } })`.
|
|
195
207
|
|
|
196
208
|
### lines
|
|
197
209
|
|
|
@@ -202,7 +214,7 @@ A `before` listener on the current session answers the event `fire` raises, such
|
|
|
202
214
|
```ts
|
|
203
215
|
import { expect, test } from 'bun:test'
|
|
204
216
|
import type { RenderPropsOf } from 'claude-code'
|
|
205
|
-
import { AbovePrompt,
|
|
217
|
+
import { AbovePrompt, Prompt } from '../node_modules/@cmodjs/core/mod.js'
|
|
206
218
|
import { testMod } from '../node_modules/@cmodjs/core/testing.js'
|
|
207
219
|
import { hints } from '../src/mod.js'
|
|
208
220
|
|
|
@@ -210,7 +222,7 @@ const band: RenderPropsOf['AbovePrompt'] = { hasSurvey: false, isWorking: false,
|
|
|
210
222
|
|
|
211
223
|
test('the band shows the prompt count', async () => {
|
|
212
224
|
const tested = testMod(hints)
|
|
213
|
-
await tested.
|
|
225
|
+
await tested.add({ type: Prompt, text: 'hello' })
|
|
214
226
|
|
|
215
227
|
expect(await tested.lines(AbovePrompt, band)).toContain('Prompts: 1')
|
|
216
228
|
})
|
|
@@ -218,7 +230,7 @@ test('the band shows the prompt count', async () => {
|
|
|
218
230
|
|
|
219
231
|
### type
|
|
220
232
|
|
|
221
|
-
`type('/<command> <arguments>')` runs a slash command as the person types it and returns its reply, `{ text?,
|
|
233
|
+
`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 `/`, and a command nothing in the mod answers.
|
|
222
234
|
|
|
223
235
|
### callTool
|
|
224
236
|
|
|
@@ -241,7 +253,7 @@ expect(await tested.lines('history')).toContain('deploy by explorer')
|
|
|
241
253
|
|
|
242
254
|
### moveTo
|
|
243
255
|
|
|
244
|
-
`moveTo(projectRoot, cwd?)` moves the session. A new `projectRoot` runs a `/cd` there, so the mod loads that project's state. A `cwd` then runs a Bash `cd` there. The mod's `
|
|
256
|
+
`moveTo(projectRoot, cwd?)` moves the session. A new `projectRoot` runs a `/cd` there, so the mod loads that project's state. A `cwd` then runs a Bash `cd` there. The mod's `onCwdChange` listeners run for each move, and `sessions.current` follows it.
|
|
245
257
|
|
|
246
258
|
### state
|
|
247
259
|
|
|
@@ -259,7 +271,8 @@ type Fakes = {
|
|
|
259
271
|
fs: { read?, write?, list?, exists?, stat? }
|
|
260
272
|
http: { fetch? }
|
|
261
273
|
settings: { read? }
|
|
262
|
-
config: { list
|
|
274
|
+
config: { list? }
|
|
275
|
+
command: { list?, run? }
|
|
263
276
|
ui: { ask?, open?, scroll? }
|
|
264
277
|
session: { messages?, append? }
|
|
265
278
|
prompt: { submit? }
|
|
@@ -283,6 +296,7 @@ Set a fake to answer a call:
|
|
|
283
296
|
- `model.complete` has no default: set it to the answer the model gives, such as `async () => ({ isAnswered: true, text: 'no', usage })`.
|
|
284
297
|
- `settings.read` answers `{}` by default, as Claude Code does with no `settings.json` files.
|
|
285
298
|
- `config.list` answers no `/config` rows by default. A `config.list` fake that answers a row with `isLocked: true` locks that option as managed settings do.
|
|
299
|
+
- `command.list` answers the slash commands the mod added by default, and `command.run` answers `{}`, a command that printed nothing.
|
|
286
300
|
- `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.
|
|
287
301
|
- `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.
|
|
288
302
|
- `testMod` runs no installer pane: 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`.
|
|
@@ -290,7 +304,7 @@ Set a fake to answer a call:
|
|
|
290
304
|
|
|
291
305
|
```ts
|
|
292
306
|
import { expect, test } from 'bun:test'
|
|
293
|
-
import {
|
|
307
|
+
import { Prompt } from '../node_modules/@cmodjs/core/mod.js'
|
|
294
308
|
import { testMod } from '../node_modules/@cmodjs/core/testing.js'
|
|
295
309
|
import { committer } from '../src/mod.js'
|
|
296
310
|
|
|
@@ -299,7 +313,9 @@ test('a Commit answer commits', async () => {
|
|
|
299
313
|
tested.fakes.ui.ask = async () => 'Commit'
|
|
300
314
|
tested.fakes.process.run = async () => ({ exitCode: 0, stdout: '', stderr: '', isStdoutTruncated: false, isStderrTruncated: false })
|
|
301
315
|
|
|
302
|
-
await tested.
|
|
316
|
+
await tested.add({ type: Prompt, text: 'Fix the bug' })
|
|
317
|
+
await tested.endTurn()
|
|
318
|
+
await tested.settle()
|
|
303
319
|
|
|
304
320
|
expect(tested.calls.filter(({ call }) => call === 'process.run').map(({ args }) => args[0])).toEqual([['git', 'commit', '-am', 'wip']])
|
|
305
321
|
})
|
|
@@ -339,7 +355,7 @@ test('the Mode prompt follows the mode', async () => {
|
|
|
339
355
|
})
|
|
340
356
|
```
|
|
341
357
|
|
|
342
|
-
`testing.js` exports the types `TestOptions`, `
|
|
358
|
+
`testing.js` exports the types `TestOptions`, `TestedMod`, `Raise`, `Fakes`, `Shown`, and `TestCall`, so a test helper can take them.
|
|
343
359
|
|
|
344
360
|
## Run the tests
|
|
345
361
|
|
package/docs/ui.md
CHANGED
|
@@ -228,7 +228,7 @@ type PaneHandle = {
|
|
|
228
228
|
- `open` asks Claude Code to show the pane. `isOpen` turns true once Claude Code places it. Claude Code may hold a pane back, and then `isOpen` stays false.
|
|
229
229
|
- `{ focus: true }` asks Claude Code to give the pane the keyboard as it opens. Claude Code gives it only while the prompt is empty and holds the keyboard, so typed text, a dialog, or another pane the person is using keeps it. cmod sends `focus` only on that one open, never when it resizes the pane.
|
|
230
230
|
- `mod.ui.pane` only adds the pane. cmod opens it only when the mod calls `open` or `toggle`, and Claude Code shows a pane only once the plugin has opened it. So give the person a way in, such as a slash command whose `reply` calls `pane.toggle()`.
|
|
231
|
-
- A pane opened from what the person did, such as a slash command they typed or a `Button` they pressed, shows at any terminal width. A pane opened from anything else, such as `setup`, a timer, or `
|
|
231
|
+
- A pane opened from what the person did, such as a slash command they typed or a `Button` they pressed, shows at any terminal width. A pane opened from anything else, such as `setup`, a timer, or `onSessionStart`, shows only on a terminal 144 columns wide, or 110 for a pane the person opened before. Below that it waits undrawn until the person opens it or the terminal widens.
|
|
232
232
|
- `close` closes it. The person closes a pane too, and `isOpen` follows.
|
|
233
233
|
- `toggle` closes an open pane and opens a closed one.
|
|
234
234
|
- When the mod starts, `isOpen` is true for each of its panes Claude Code already shows.
|
|
@@ -286,7 +286,7 @@ export const notes = defineMod({
|
|
|
286
286
|
- No place covers the permission dialog. `mod.ui.notice` adds a line under it instead ([Notices](#notices)).
|
|
287
287
|
|
|
288
288
|
```tsx
|
|
289
|
-
import { AbovePrompt, defineMod, PromptHint,
|
|
289
|
+
import { AbovePrompt, defineMod, PromptHint, sessions } from '../node_modules/@cmodjs/core/mod.js'
|
|
290
290
|
import { Box, Text } from '../node_modules/@cmodjs/core/ui/elements.js'
|
|
291
291
|
|
|
292
292
|
export const hints = defineMod({
|
|
@@ -301,7 +301,7 @@ export const hints = defineMod({
|
|
|
301
301
|
<Text dimColor>{`Prompts: ${mod.state.session.prompts}`}</Text>
|
|
302
302
|
</Box>
|
|
303
303
|
))
|
|
304
|
-
|
|
304
|
+
sessions.current.onPrompt(() => {
|
|
305
305
|
mod.state.session.prompts += 1
|
|
306
306
|
})
|
|
307
307
|
},
|
|
@@ -353,7 +353,7 @@ export const diagrams = defineMod({
|
|
|
353
353
|
markdownBlocks<P extends MarkdownPlace>(text: string, place: P): Omit<PlaceProps<P>, 'Default'>[]
|
|
354
354
|
```
|
|
355
355
|
|
|
356
|
-
`markdownBlocks`, exported from `ui/markdown.js`, returns the props of every block at one markdown place in a markdown text, such as every code block of a reply. A mod reads the blocks of a reply
|
|
356
|
+
`markdownBlocks`, exported from `ui/markdown.js`, returns the props of every block at one markdown place in a markdown text, such as every code block of a reply. A mod reads the blocks of a reply an `onReply` listener got with it, and a test calls a component with real props.
|
|
357
357
|
|
|
358
358
|
```ts
|
|
359
359
|
import { CodeBlock, markdownBlocks } from '../node_modules/@cmodjs/core/ui/markdown.js'
|
|
@@ -396,12 +396,12 @@ type ToastOptions = {
|
|
|
396
396
|
- `promise` shows `loading` while `task` runs, then `success` or `error` in the same row. `unwrap()` resolves or rejects as `task` does.
|
|
397
397
|
|
|
398
398
|
```ts
|
|
399
|
-
import { defineMod, fs,
|
|
399
|
+
import { defineMod, fs, sessions } from '../node_modules/@cmodjs/core/mod.js'
|
|
400
400
|
|
|
401
401
|
export const indexer = defineMod({
|
|
402
402
|
name: 'indexer',
|
|
403
403
|
setup(mod) {
|
|
404
|
-
|
|
404
|
+
sessions.current.onSessionStart(async () => {
|
|
405
405
|
const indexed = fs.list(sessions.current.project).then((entries) => entries.length)
|
|
406
406
|
mod.ui.toast.promise(indexed, { loading: 'Indexing files', success: (count) => `Indexed ${count} files`, error: 'Indexing failed' })
|
|
407
407
|
})
|
|
@@ -409,7 +409,7 @@ export const indexer = defineMod({
|
|
|
409
409
|
})
|
|
410
410
|
```
|
|
411
411
|
|
|
412
|
-
An error cmod catches for a mod, such as a
|
|
412
|
+
An error cmod catches for a mod, such as a listener that throws, shows as an error toast that stays, with its buttons on a line under the message:
|
|
413
413
|
|
|
414
414
|
- `Retry` runs the failed step again, where running it again can work: an install, the mod's setup, or a program that stopped. The toast stays where it is, and the step's progress shows in place of its description, or of its buttons when it has none, until the try ends.
|
|
415
415
|
- `Report` replaces the toast's buttons with the ways to tell the mod's author, with the keys on the first, and `Back` returns to the buttons. The same failure happening again keeps them open. `Ask Claude to report it` sends this session a prompt to file the report with the person's own tools, and Claude Code asks before each step. `Report it on GitHub (opens your browser)` opens a filled-in issue on the mod's `supportUrl` or `repository` in its `plugin.json`, or `Email the author (opens your mail app)` writes to its `author.email`. `Copy the error` copies the mod's name and version, the `@cmodjs/core` version, the message, the whole cause, and the stack.
|
|
@@ -428,12 +428,12 @@ type AskOptions = { options?: readonly string[]; header?: string; multiSelect?:
|
|
|
428
428
|
`ask` shows a question with 2 to 4 options and resolves the answer. Fewer than two options are padded with Yes and No, and the person may type an answer of their own. `header` is a short chip beside the question, 12 characters at most. `multiSelect` lets the person pick several, and the answer comes back joined by commas.
|
|
429
429
|
|
|
430
430
|
```ts
|
|
431
|
-
import { defineMod, process, sessions
|
|
431
|
+
import { defineMod, process, sessions } from '../node_modules/@cmodjs/core/mod.js'
|
|
432
432
|
|
|
433
433
|
export const committer = defineMod({
|
|
434
434
|
name: 'committer',
|
|
435
435
|
setup(mod) {
|
|
436
|
-
|
|
436
|
+
sessions.current.onTurnEnd(async () => {
|
|
437
437
|
const answer = await mod.ui.ask('Commit the changes?', { options: ['Commit', 'Wait'], header: 'Commit' })
|
|
438
438
|
if (answer === 'Commit') await process.run(['git', 'commit', '-am', 'wip'], { cwd: sessions.current.project })
|
|
439
439
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decide-permission.d.ts","sourceRoot":"","sources":["../../src/jobs/permissions/decide-permission.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"decide-permission.d.ts","sourceRoot":"","sources":["../../src/jobs/permissions/decide-permission.ts"],"names":[],"mappings":"AACA,OAAO,EAA6C,KAAK,IAAI,EAAE,MAAM,6BAA6B,CAAA;AAClG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AACxD,OAAO,EAAuE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAMxJ,MAAM,MAAM,IAAI,CAAC,GAAG,IAAI;KACrB,GAAG,IAAI,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG;QAClC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,CAAC,GAAG,CAAC,GAAG;YAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;SAAE,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;QACrG,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB;CACF,CAAC,SAAS,CAAC,CAAA;AAEZ,MAAM,MAAM,eAAe,CAAC,GAAG,IAAI;IAAE,IAAI,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IAAC,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAA;CAAE,CAAA;AAC5E,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAA;AAC/C,MAAM,MAAM,OAAO,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAI7D,wBAAgB,eAAe,CAAC,MAAM,SAAS,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE7F;AAED,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,KAAK,EAAE,eAAe,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAerN"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { strictness } from '../../runtime/hooks.js';
|
|
2
1
|
import { messageOf } from '../../utils/text.js';
|
|
3
2
|
import { callBaseOf, callEffects } from '../../utils/call-effects.js';
|
|
4
3
|
import { folderInScope, matchTarget, targetOf } from './match-target.js';
|
|
5
4
|
import { resolve } from '../../vendor.js';
|
|
6
5
|
import { inFolder } from '../context.js';
|
|
6
|
+
const strictness = ['allow', 'ask', 'deny'];
|
|
7
7
|
const wordBoundary = '\\s;&|()<>\'"`=';
|
|
8
8
|
export function stricterVerdict(first, second) {
|
|
9
9
|
return strictness.indexOf(second.decision) > strictness.indexOf(first.decision) ? second : first;
|
package/jobs/prompt.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type { Job, Mod } from '../mod.js';
|
|
|
2
2
|
import { type Call } from '../utils/call-effects.js';
|
|
3
3
|
import { type Target } from './permissions/match-target.js';
|
|
4
4
|
type PromptInput = {
|
|
5
|
-
userPrompt?: string;
|
|
6
5
|
call?: Call & {
|
|
7
6
|
readonly folder: string;
|
|
8
7
|
};
|
|
@@ -10,9 +9,8 @@ type PromptInput = {
|
|
|
10
9
|
type PromptOptions<State extends object> = {
|
|
11
10
|
name: string;
|
|
12
11
|
prompt: string | ((input: PromptInput, mod: Mod<State>) => string | undefined | Promise<string | undefined>);
|
|
13
|
-
when?: RegExp | ((userPrompt: string) => boolean);
|
|
14
12
|
after?: Target | Target[];
|
|
15
13
|
};
|
|
16
|
-
export declare function prompt<State extends object = Record<never, never>>({ name, prompt: text,
|
|
14
|
+
export declare function prompt<State extends object = Record<never, never>>({ name, prompt: text, after }: PromptOptions<State>): Job<void, State>;
|
|
17
15
|
export {};
|
|
18
16
|
//# sourceMappingURL=prompt.d.ts.map
|
package/jobs/prompt.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/jobs/prompt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAc,GAAG,EAAE,MAAM,WAAW,CAAA;AAKrD,OAAO,EAAe,KAAK,IAAI,EAAE,MAAM,0BAA0B,CAAA;AACjE,OAAO,EAAyB,KAAK,MAAM,EAAE,MAAM,+BAA+B,CAAA;AAGlF,KAAK,WAAW,GAAG;IAAE,
|
|
1
|
+
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/jobs/prompt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAc,GAAG,EAAE,MAAM,WAAW,CAAA;AAKrD,OAAO,EAAe,KAAK,IAAI,EAAE,MAAM,0BAA0B,CAAA;AACjE,OAAO,EAAyB,KAAK,MAAM,EAAE,MAAM,+BAA+B,CAAA;AAGlF,KAAK,WAAW,GAAG;IAAE,IAAI,CAAC,EAAE,IAAI,GAAG;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAA;AAEhE,KAAK,aAAa,CAAC,KAAK,SAAS,MAAM,IAAI;IACzC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;IAC5G,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CAC1B,CAAA;AAID,wBAAgB,MAAM,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,aAAa,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAgCzI"}
|
package/jobs/prompt.js
CHANGED
|
@@ -5,11 +5,9 @@ import { callEffects } from '../utils/call-effects.js';
|
|
|
5
5
|
import { matchTarget, targetOf } from './permissions/match-target.js';
|
|
6
6
|
import { afterCall, inFolder, targetWords, withinDeadline } from './context.js';
|
|
7
7
|
const deadline = { ms: 5000, job: 'prompt' };
|
|
8
|
-
export function prompt({ name, prompt: text,
|
|
8
|
+
export function prompt({ name, prompt: text, after }) {
|
|
9
9
|
if (name.trim() === '')
|
|
10
10
|
throw new Error('prompt: give the block a name. Claude reads the text under it.');
|
|
11
|
-
if (when !== undefined && after !== undefined)
|
|
12
|
-
throw new Error(`prompt "${name}": use when or after, not both. Add a second prompt for the other trigger.`);
|
|
13
11
|
const targets = after === undefined ? [] : Array.isArray(after) ? after : [after];
|
|
14
12
|
for (const target of targets)
|
|
15
13
|
targetOf(target);
|
|
@@ -30,11 +28,7 @@ export function prompt({ name, prompt: text, when, after }) {
|
|
|
30
28
|
const value = await textFor(input);
|
|
31
29
|
return value === undefined ? undefined : `# ${name}\n${value}`;
|
|
32
30
|
};
|
|
33
|
-
if (
|
|
34
|
-
addAfterUserPrompts(job, when, (userPrompt) => headedText({ userPrompt }), log);
|
|
35
|
-
job.announce(`the ${name} prompt after matching user prompts`);
|
|
36
|
-
}
|
|
37
|
-
else if (targets.length > 0) {
|
|
31
|
+
if (targets.length > 0) {
|
|
38
32
|
addAfterCalls(job, targets, headedText, log);
|
|
39
33
|
job.announce(`the ${name} prompt after ${listed(targets.map(targetWords))}`);
|
|
40
34
|
}
|
|
@@ -86,19 +80,6 @@ function addKeptCurrent(job, name, textFor, log) {
|
|
|
86
80
|
updates = updates.then(update).catch((error) => log(messageOf(error)));
|
|
87
81
|
});
|
|
88
82
|
}
|
|
89
|
-
function addAfterUserPrompts(job, when, textFor, log) {
|
|
90
|
-
engineOf(job.mod).router.add('prompt.submit', async (e, next) => {
|
|
91
|
-
let value;
|
|
92
|
-
try {
|
|
93
|
-
if (typeof when === 'function' ? when(e.text) : e.text.search(when) >= 0)
|
|
94
|
-
value = await textFor(e.text);
|
|
95
|
-
}
|
|
96
|
-
catch (error) {
|
|
97
|
-
log(`its when check failed: ${messageOf(error)}`);
|
|
98
|
-
}
|
|
99
|
-
return next(value === undefined ? e : { ...e, context: [...(e.context ?? []), value] });
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
83
|
function addAfterCalls(job, targets, textFor, log) {
|
|
103
84
|
afterCall(job, async (use, workspace) => {
|
|
104
85
|
const effects = callEffects(use, workspace);
|
package/jobs/slash-command.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Disposable, Job, Mod } from '../mod.js';
|
|
2
2
|
export type CommandReply = string | {
|
|
3
3
|
text?: string;
|
|
4
|
-
|
|
4
|
+
note?: string;
|
|
5
5
|
} | undefined;
|
|
6
6
|
export declare function slashCommand<State extends object = Record<never, never>>(options: {
|
|
7
7
|
readonly name: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slash-command.d.ts","sourceRoot":"","sources":["../src/jobs/slash-command.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAOrD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,
|
|
1
|
+
{"version":3,"file":"slash-command.d.ts","sourceRoot":"","sources":["../src/jobs/slash-command.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAOrD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAA;AAMhF,wBAAgB,YAAY,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,OAAO,EAAE;IACjF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,IAAI,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,YAAY,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAA;CACxI,GAAG,GAAG,CAAC,UAAU,EAAE,KAAK,CAAC,CAkCzB"}
|
package/jobs/slash-command.js
CHANGED
|
@@ -42,7 +42,7 @@ function answerOf(reply) {
|
|
|
42
42
|
return {};
|
|
43
43
|
if (typeof reply === 'string')
|
|
44
44
|
return { text: reply };
|
|
45
|
-
return { ...(reply.text === undefined ? {} : { text: reply.text }), ...(reply.
|
|
45
|
+
return { ...(reply.text === undefined ? {} : { text: reply.text }), ...(reply.note === undefined ? {} : { context: [reply.note] }) };
|
|
46
46
|
}
|
|
47
47
|
function splitWords(args) {
|
|
48
48
|
return parse(args, (key) => `$${key}`).flatMap((entry) => {
|
package/jobs/status-line.d.ts
CHANGED
|
@@ -2,6 +2,5 @@ import type { Job, Mod } from '../mod.js';
|
|
|
2
2
|
import type { Usage } from '../session/session.js';
|
|
3
3
|
export declare function statusLine<State extends object = Record<never, never>>(options: {
|
|
4
4
|
readonly text: (usage: Usage, mod: Mod<State>) => string | undefined | Promise<string | undefined>;
|
|
5
|
-
readonly interval?: number;
|
|
6
5
|
}): Job<void, State>;
|
|
7
6
|
//# sourceMappingURL=status-line.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status-line.d.ts","sourceRoot":"","sources":["../src/jobs/status-line.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"status-line.d.ts","sourceRoot":"","sources":["../src/jobs/status-line.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAKzC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAA;AAMlD,wBAAgB,UAAU,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,OAAO,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CAAE,GAAG,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAgCzM"}
|
package/jobs/status-line.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { engineOf } from '../runtime/engine.js';
|
|
2
2
|
import { usageOf } from '../runtime/sessions.js';
|
|
3
|
+
import { onStateChange } from '../runtime/state.js';
|
|
3
4
|
import { messageOf } from '../utils/text.js';
|
|
4
5
|
import { withinDeadline } from './context.js';
|
|
5
6
|
const deadline = { ms: 2000, job: 'statusLine' };
|
|
6
7
|
export function statusLine(options) {
|
|
7
|
-
const { text
|
|
8
|
+
const { text } = options;
|
|
8
9
|
return (job) => {
|
|
9
10
|
const { mod, announce, reserveName } = job;
|
|
10
11
|
const { claude, router } = engineOf(mod);
|
|
11
|
-
if (!(interval >= 1))
|
|
12
|
-
throw new Error(`${mod.name}: the status line interval is ${interval}. Give it in milliseconds, 1 or more.`);
|
|
13
12
|
reserveName('statusLine', '', `${mod.name}: a status line is already added. A mod has one status line: join the texts in one statusLine.`);
|
|
14
13
|
let shown;
|
|
15
14
|
let queue = Promise.resolve();
|
|
@@ -34,7 +33,7 @@ export function statusLine(options) {
|
|
|
34
33
|
refresh();
|
|
35
34
|
return next(e);
|
|
36
35
|
});
|
|
37
|
-
|
|
36
|
+
onStateChange(mod.state, refresh);
|
|
38
37
|
refresh();
|
|
39
38
|
};
|
|
40
39
|
}
|