@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.
Files changed (136) hide show
  1. package/docs/commands.md +7 -7
  2. package/docs/dependencies.md +1 -1
  3. package/docs/index.md +12 -11
  4. package/docs/jobs.md +12 -16
  5. package/docs/mod.md +111 -36
  6. package/docs/permissions.md +14 -18
  7. package/docs/session.md +171 -82
  8. package/docs/state.md +5 -5
  9. package/docs/testing.md +57 -41
  10. package/docs/ui.md +9 -9
  11. package/jobs/permissions/decide-permission.d.ts.map +1 -1
  12. package/jobs/permissions/decide-permission.js +1 -1
  13. package/jobs/prompt.d.ts +1 -3
  14. package/jobs/prompt.d.ts.map +1 -1
  15. package/jobs/prompt.js +2 -21
  16. package/jobs/slash-command.d.ts +1 -1
  17. package/jobs/slash-command.d.ts.map +1 -1
  18. package/jobs/slash-command.js +1 -1
  19. package/jobs/status-line.d.ts +0 -1
  20. package/jobs/status-line.d.ts.map +1 -1
  21. package/jobs/status-line.js +3 -4
  22. package/mod.d.ts +53 -105
  23. package/mod.d.ts.map +1 -1
  24. package/mod.js +18 -80
  25. package/package.json +1 -1
  26. package/records.d.ts +1 -1
  27. package/records.js +1 -1
  28. package/register.d.ts.map +1 -1
  29. package/register.js +8 -5
  30. package/runtime/before.d.ts +11 -4
  31. package/runtime/before.d.ts.map +1 -1
  32. package/runtime/before.js +131 -67
  33. package/runtime/claude.d.ts +6 -1
  34. package/runtime/claude.d.ts.map +1 -1
  35. package/runtime/conversations.d.ts.map +1 -1
  36. package/runtime/conversations.js +82 -38
  37. package/runtime/deadline.d.ts +17 -0
  38. package/runtime/deadline.d.ts.map +1 -1
  39. package/runtime/deadline.js +34 -0
  40. package/runtime/dependencies.d.ts +2 -0
  41. package/runtime/dependencies.d.ts.map +1 -1
  42. package/runtime/dependencies.js +12 -0
  43. package/runtime/grants.d.ts.map +1 -1
  44. package/runtime/grants.js +13 -8
  45. package/runtime/hooks.d.ts +15 -11
  46. package/runtime/hooks.d.ts.map +1 -1
  47. package/runtime/hooks.js +8 -165
  48. package/runtime/lifecycle.d.ts +4 -2
  49. package/runtime/lifecycle.d.ts.map +1 -1
  50. package/runtime/lifecycle.js +96 -62
  51. package/runtime/permission-check.d.ts +15 -0
  52. package/runtime/permission-check.d.ts.map +1 -0
  53. package/runtime/permission-check.js +37 -0
  54. package/runtime/sessions.d.ts +35 -5
  55. package/runtime/sessions.d.ts.map +1 -1
  56. package/runtime/sessions.js +304 -82
  57. package/runtime/state.d.ts.map +1 -1
  58. package/runtime/state.js +14 -11
  59. package/runtime/tool-calls.d.ts +0 -1
  60. package/runtime/tool-calls.d.ts.map +1 -1
  61. package/runtime/tool-calls.js +0 -19
  62. package/session/context/turn/message/instruction-file.d.ts +9 -0
  63. package/session/context/turn/message/instruction-file.d.ts.map +1 -0
  64. package/session/context/turn/message/instruction-file.js +4 -0
  65. package/session/context/turn/message/message.d.ts +6 -4
  66. package/session/context/turn/message/message.d.ts.map +1 -1
  67. package/session/context/turn/message/message.js +12 -8
  68. package/session/context/turn/message/sender.d.ts +2 -1
  69. package/session/context/turn/message/sender.d.ts.map +1 -1
  70. package/session/context/turn/message/sender.js +4 -1
  71. package/session/context/turn/message/tool-call.d.ts +5 -0
  72. package/session/context/turn/message/tool-call.d.ts.map +1 -1
  73. package/session/context/turn/message/tool-result.d.ts +6 -1
  74. package/session/context/turn/message/tool-result.d.ts.map +1 -1
  75. package/session/context/turn/turn.d.ts +7 -0
  76. package/session/context/turn/turn.d.ts.map +1 -1
  77. package/session/conversation.d.ts +10 -3
  78. package/session/conversation.d.ts.map +1 -1
  79. package/session/conversation.js +14 -5
  80. package/session/goal/goal.d.ts +1 -1
  81. package/session/goal/goal.d.ts.map +1 -1
  82. package/session/goal/task.d.ts +2 -2
  83. package/session/goal/task.d.ts.map +1 -1
  84. package/session/session.d.ts +361 -26
  85. package/session/session.d.ts.map +1 -1
  86. package/session/session.js +40 -0
  87. package/src/jobs/permissions/decide-permission.ts +2 -1
  88. package/src/jobs/prompt.ts +3 -20
  89. package/src/jobs/slash-command.ts +2 -2
  90. package/src/jobs/status-line.ts +4 -7
  91. package/src/mod.ts +52 -147
  92. package/src/records.ts +1 -1
  93. package/src/register.ts +8 -5
  94. package/src/runtime/before.ts +140 -66
  95. package/src/runtime/claude.ts +5 -1
  96. package/src/runtime/conversations.ts +75 -37
  97. package/src/runtime/deadline.ts +38 -0
  98. package/src/runtime/dependencies.ts +12 -0
  99. package/src/runtime/grants.ts +11 -7
  100. package/src/runtime/hooks.ts +24 -163
  101. package/src/runtime/lifecycle.ts +110 -77
  102. package/src/runtime/permission-check.ts +47 -0
  103. package/src/runtime/sessions.ts +334 -91
  104. package/src/runtime/state.ts +15 -8
  105. package/src/runtime/tool-calls.ts +0 -19
  106. package/src/session/context/turn/message/instruction-file.ts +13 -0
  107. package/src/session/context/turn/message/message.ts +17 -11
  108. package/src/session/context/turn/message/sender.ts +5 -2
  109. package/src/session/context/turn/message/tool-call.ts +3 -0
  110. package/src/session/context/turn/message/tool-result.ts +6 -1
  111. package/src/session/context/turn/turn.ts +4 -0
  112. package/src/session/conversation.ts +21 -8
  113. package/src/session/goal/goal.ts +1 -1
  114. package/src/session/goal/task.ts +2 -2
  115. package/src/session/session.ts +227 -24
  116. package/src/testing/claude-code.ts +23 -0
  117. package/src/testing/fake-claude.ts +5 -1
  118. package/src/testing/fake-sessions.ts +38 -24
  119. package/src/testing.ts +88 -19
  120. package/testing/claude-code.d.ts +12 -0
  121. package/testing/claude-code.d.ts.map +1 -0
  122. package/testing/claude-code.js +10 -0
  123. package/testing/fake-claude.d.ts +4 -0
  124. package/testing/fake-claude.d.ts.map +1 -1
  125. package/testing/fake-claude.js +4 -1
  126. package/testing/fake-sessions.d.ts +8 -6
  127. package/testing/fake-sessions.d.ts.map +1 -1
  128. package/testing/fake-sessions.js +35 -20
  129. package/testing.d.ts +38 -9
  130. package/testing.d.ts.map +1 -1
  131. package/testing.js +58 -8
  132. package/docs/hooks.md +0 -259
  133. package/session/context/turn/message/document.d.ts +0 -7
  134. package/session/context/turn/message/document.d.ts.map +0 -1
  135. package/session/context/turn/message/document.js +0 -1
  136. 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', context: 'The to-do list is now: 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.fire(...)`, `tested.type(...)`, and `tested.callTool(...)` return.
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 hooks, panes, and jobs. `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.
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<H extends Hook>(hook: H, input: TestInput<H>, below?): Promise<EventResult<`classic.${H}`>>
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
- - `fire(UserPromptSubmit, { prompt: 'hello' })` runs the mod's listeners on a hook. `TestInput<H>` is the hook's input without the fields `fire` fills: `session_id`, `transcript_path`, `cwd`, `hook_event_name`, and a new `tool_use_id` for each tool event.
96
- - A `SessionStart` that names no `transcript_path` also writes its transcript as an empty file in the test's files, so the session's `session` values last as they do in Claude Code ([state.md](state.md)).
97
- - For a tool event, `files` is worked out from the call and the test's `files`, as in Claude Code.
98
- - `fire(PreToolUse, …)` runs the call's `tool.call`, and its `tool.check` inside it, as Claude Code does. `testMod` runs a mod as if `register.ts` calls `registerPermissionCheck`, so it returns `allow: true` or `ask` with the reason when the mod's hooks or `permissions` job decided the permission. A `PreToolUse` with `agent_id` and `agent_type`, such as `fire(PreToolUse, { tool_name: 'Read', tool_input: { file_path: 'a.ts' }, agent_id: 'agent-7', agent_type: 'explorer' })`, runs as that subagent's call. It returns `updatedInput` when the hooks rewrote the input, `additionalContext` as a list, and `deny` when a hook or Claude Code refused the call. Its `below` is `{ deny }` to test a call Claude Code refuses, and Claude Code runs the call otherwise.
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 prompt with when
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
- - The `prompt.submit` input is `{ text, wait, origin }`. A prompt the person typed has `wait: false` and `origin: { kind: 'composer' }`.
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(mod) {
137
- mod.use(prompt({ name: 'release-steps', prompt: 'Run bun run release, then push the tag.', when: /\brelease\b/i }))
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
- const aboutRelease = await tested.fire('prompt.submit', { text: 'Cut a release', ...typed })
152
- const aboutBug = await tested.fire('prompt.submit', { text: 'Fix the bug', ...typed })
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 hook started without awaiting it has run, such as a `void` call in a `Stop` hook. It returns while a fake the test holds open has not answered.
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, text, isError? }`, `{ type: Skill, name, text }`, `{ type: Document, path, text }`, or `{ type: Notice, text }`.
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 the event `fire` raises, such as `tested.fire('prompt.submit', { text: 'Deploy to prod', origin: { kind: 'composer' }, wait: false })` or `tested.fire('tool.call', { tool: 'Bash', tool_use_id: 'toolu_1', command: 'rm -rf build' })`.
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, UserPromptSubmit } from '../node_modules/@cmodjs/core/mod.js'
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.fire(UserPromptSubmit, { prompt: 'hello' })
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?, context? }`. It rejects a line that does not start with `/`, and a command the mod does not add.
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 `CwdChanged` listeners run for each move, and `sessions.current` follows it.
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?, set? }
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 { Stop } from '../node_modules/@cmodjs/core/mod.js'
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.fire(Stop, { stop_hook_active: false })
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`, `TestInput`, `TestedMod`, `Fakes`, `Shown`, and `TestCall`, so a test helper can take them.
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 `SessionStart`, 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.
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, UserPromptSubmit } from '../node_modules/@cmodjs/core/mod.js'
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
- mod.on(UserPromptSubmit, () => {
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 it got in a hook with it, and a test calls a component with real props.
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, SessionStart, sessions } from '../node_modules/@cmodjs/core/mod.js'
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
- mod.on(SessionStart, async () => {
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 hook that throws, shows as an error toast that stays, with its buttons on a line under the message:
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, Stop } from '../node_modules/@cmodjs/core/mod.js'
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
- mod.on(Stop, async () => {
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":"AAEA,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;AAIxJ,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
+ {"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, when, after }: PromptOptions<State>): Job<void, State>;
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
@@ -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,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,IAAI,GAAG;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAA;AAErF,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,IAAI,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,CAAA;IACjD,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,IAAI,EAAE,KAAK,EAAE,EAAE,aAAa,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAoC/I"}
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, when, after }) {
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 (when !== undefined) {
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);
@@ -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
- context?: string;
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,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAA;AAMnF,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"}
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"}
@@ -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.context === undefined ? {} : { context: [reply.context] }) };
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) => {
@@ -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;AAIzC,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;IAC/E,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;IAClG,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAC3B,GAAG,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAiCnB"}
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"}
@@ -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, interval = 10000 } = options;
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
- claude.clock.every(interval, refresh);
36
+ onStateChange(mod.state, refresh);
38
37
  refresh();
39
38
  };
40
39
  }