@cmodjs/core 4.0.3 → 4.1.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 +44 -9
- package/docs/index.md +3 -3
- package/docs/install-steps.md +8 -8
- package/docs/jobs.md +2 -1
- package/docs/mod.md +1 -1
- package/docs/options.md +1 -1
- package/docs/permissions.md +1 -1
- package/docs/session.md +46 -13
- package/docs/testing.md +15 -11
- package/docs/ui.md +43 -11
- package/installer.d.ts +3 -2
- package/installer.d.ts.map +1 -1
- package/jobs/program.d.ts +1 -0
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +39 -23
- package/mod.d.ts +3 -2
- package/mod.d.ts.map +1 -1
- package/mod.js +1 -0
- package/package.json +1 -1
- package/records.d.ts +15 -1
- package/records.d.ts.map +1 -1
- package/records.js +39 -1
- package/register.d.ts.map +1 -1
- package/register.js +8 -5
- package/runtime/before.d.ts +6 -2
- package/runtime/before.d.ts.map +1 -1
- package/runtime/before.js +56 -18
- package/runtime/buttons.d.ts +10 -0
- package/runtime/buttons.d.ts.map +1 -0
- package/runtime/buttons.js +12 -0
- package/runtime/conversations.d.ts +3 -0
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +164 -27
- package/runtime/engine.d.ts +2 -0
- package/runtime/engine.d.ts.map +1 -1
- package/runtime/grants.d.ts +1 -0
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +5 -2
- package/runtime/hotkeys.d.ts.map +1 -1
- package/runtime/hotkeys.js +7 -4
- package/runtime/installer.d.ts +10 -6
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +106 -48
- package/runtime/lifecycle.d.ts +2 -2
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +144 -42
- package/runtime/pages.d.ts +3 -1
- package/runtime/pages.d.ts.map +1 -1
- package/runtime/pages.js +48 -14
- package/runtime/parts.d.ts +1 -0
- package/runtime/parts.d.ts.map +1 -1
- package/runtime/parts.js +6 -3
- package/runtime/programs.d.ts +1 -0
- package/runtime/programs.d.ts.map +1 -1
- package/runtime/programs.js +4 -1
- package/runtime/sessions.d.ts +10 -3
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +104 -59
- package/runtime/toaster.d.ts.map +1 -1
- package/runtime/toaster.js +23 -9
- package/runtime/ui.d.ts +22 -4
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +161 -59
- package/runtime/watches.d.ts.map +1 -1
- package/runtime/watches.js +18 -4
- package/runtime/widgets.d.ts +7 -5
- package/runtime/widgets.d.ts.map +1 -1
- package/runtime/widgets.js +31 -21
- package/session/context/turn/message/attached-file.d.ts +13 -0
- package/session/context/turn/message/attached-file.d.ts.map +1 -0
- package/session/context/turn/message/attached-file.js +21 -0
- package/session/context/turn/message/message.d.ts +3 -2
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +20 -6
- package/session/conversation.d.ts +15 -3
- package/session/conversation.d.ts.map +1 -1
- package/session/conversation.js +23 -4
- package/session/goal/task.d.ts +5 -0
- package/session/goal/task.d.ts.map +1 -1
- package/session/session.d.ts +36 -7
- package/session/session.d.ts.map +1 -1
- package/session/session.js +2 -1
- package/src/installer.ts +3 -2
- package/src/jobs/program.ts +41 -21
- package/src/mod.ts +3 -2
- package/src/records.ts +49 -1
- package/src/register.ts +11 -8
- package/src/runtime/before.ts +61 -22
- package/src/runtime/buttons.ts +22 -0
- package/src/runtime/conversations.ts +170 -34
- package/src/runtime/engine.ts +2 -0
- package/src/runtime/grants.ts +6 -2
- package/src/runtime/hotkeys.ts +7 -4
- package/src/runtime/installer.ts +147 -83
- package/src/runtime/lifecycle.ts +149 -48
- package/src/runtime/pages.ts +56 -12
- package/src/runtime/parts.ts +8 -4
- package/src/runtime/programs.ts +5 -1
- package/src/runtime/sessions.ts +106 -59
- package/src/runtime/toaster.ts +27 -9
- package/src/runtime/ui.ts +166 -62
- package/src/runtime/watches.ts +20 -5
- package/src/runtime/widgets.ts +35 -23
- package/src/session/context/turn/message/attached-file.ts +36 -0
- package/src/session/context/turn/message/message.ts +20 -7
- package/src/session/conversation.ts +31 -6
- package/src/session/goal/task.ts +6 -0
- package/src/session/session.ts +18 -7
- package/src/testing/fake-claude.ts +21 -6
- package/src/testing/fake-elements.ts +57 -8
- package/src/testing/fake-sessions.ts +72 -24
- package/src/testing.ts +114 -33
- package/src/ui/areas.ts +1 -0
- package/src/ui/components.ts +14 -8
- package/src/ui/elements.ts +6 -2
- package/src/ui/markdown.ts +3 -3
- package/src/ui/panes.ts +2 -2
- package/src/ui/styles.ts +6 -2
- package/testing/fake-claude.d.ts +3 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +18 -5
- package/testing/fake-elements.d.ts.map +1 -1
- package/testing/fake-elements.js +52 -7
- package/testing/fake-sessions.d.ts +24 -10
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +66 -20
- package/testing.d.ts +8 -12
- package/testing.d.ts.map +1 -1
- package/testing.js +113 -27
- package/ui/areas.d.ts +4 -0
- package/ui/areas.d.ts.map +1 -1
- package/ui/components.d.ts +1 -0
- package/ui/components.d.ts.map +1 -1
- package/ui/components.js +11 -7
- package/ui/elements.d.ts +3 -0
- package/ui/elements.d.ts.map +1 -1
- package/ui/elements.js +6 -1
- package/ui/markdown.d.ts +2 -2
- package/ui/markdown.d.ts.map +1 -1
- package/ui/panes.d.ts +2 -1
- package/ui/panes.d.ts.map +1 -1
- package/ui/styles.d.ts +11 -2
- package/ui/styles.d.ts.map +1 -1
package/src/testing.ts
CHANGED
|
@@ -4,8 +4,10 @@ import type {
|
|
|
4
4
|
CmodDependencies,
|
|
5
5
|
CommandPresentation,
|
|
6
6
|
CommandRunResult,
|
|
7
|
+
ElementTable,
|
|
7
8
|
EventResult,
|
|
8
9
|
Frozen,
|
|
10
|
+
MarkdownProps,
|
|
9
11
|
PaneOpenArgs,
|
|
10
12
|
PluginOptions,
|
|
11
13
|
PressedLink,
|
|
@@ -23,7 +25,7 @@ import type { Options, OptionValues } from './options.js'
|
|
|
23
25
|
import { hotkeyCommand, readSteps, storeFolder, type Steps } from './records.js'
|
|
24
26
|
import type { Claude } from './runtime/claude.js'
|
|
25
27
|
import { answerCall, cmodPluginName, notInstalled } from './runtime/dependencies.js'
|
|
26
|
-
import { closingContext, foldBefore, installBefore, withAnswer, type Answers } from './runtime/before.js'
|
|
28
|
+
import { closingContext, foldBefore, installBefore, withAnswer, type Answers, type Decide } from './runtime/before.js'
|
|
27
29
|
import { permissionRoutes } from './runtime/permission-check.js'
|
|
28
30
|
import type { RoutedEvent } from './runtime/hooks.js'
|
|
29
31
|
import { keepClaudeCode, type ClaudeCode } from './testing/claude-code.js'
|
|
@@ -31,7 +33,7 @@ import { createRouter } from './runtime/router.js'
|
|
|
31
33
|
import { toolInputOf } from './runtime/tool-calls.js'
|
|
32
34
|
import { createLifecycle } from './runtime/lifecycle.js'
|
|
33
35
|
import { MissingOptions } from './runtime/options.js'
|
|
34
|
-
import { conversationMethods } from './runtime/sessions.js'
|
|
36
|
+
import { conversationMethods, pickedColor, type SessionColor } from './runtime/sessions.js'
|
|
35
37
|
import type { ShownToast } from './runtime/toaster.js'
|
|
36
38
|
import { Message } from './session/context/turn/message/message.js'
|
|
37
39
|
import { Note, untagged } from './session/context/turn/message/note.js'
|
|
@@ -45,19 +47,19 @@ import { TaskStart } from './session/goal/task.js'
|
|
|
45
47
|
import { calledAnswer, fakeClaude, type Fakes, type Shown as ShownByClaude, type TestCall } from './testing/fake-claude.js'
|
|
46
48
|
import { elements, findElement, rowsOf } from './testing/fake-elements.js'
|
|
47
49
|
import { fakeFiles } from './testing/fake-files.js'
|
|
48
|
-
import { fakeSessions, type FakeSession, type FakeSessions, type MessageDraft } from './testing/fake-sessions.js'
|
|
50
|
+
import { fakeSessions, type FakeSession, type FakeSessions, type MessageDraft, type StartedTask } from './testing/fake-sessions.js'
|
|
49
51
|
import { listed, messageOf } from './utils/text.js'
|
|
50
|
-
import { componentOf, type Row, type
|
|
52
|
+
import { componentOf, type KindProps, type Row, type StyledAt } from './ui/styles.js'
|
|
51
53
|
import { areas, TurnEnd as TurnEndArea, type Area } from './ui/areas.js'
|
|
52
54
|
import { drawWith, elementOf } from './ui/elements.js'
|
|
53
55
|
import { drawnParts } from './runtime/parts.js'
|
|
54
56
|
import { serveWidgets, type Widgets } from './runtime/widgets.js'
|
|
55
57
|
import { engineOf } from './runtime/engine.js'
|
|
56
|
-
import { pagesOf } from './runtime/pages.js'
|
|
58
|
+
import { pagesOf, servePages } from './runtime/pages.js'
|
|
57
59
|
import { partMethods, type DrawnPart, type PageRequest, type PartPress, type PartsUpdate } from './runtime/ui.js'
|
|
58
60
|
|
|
59
61
|
export type { Fakes, TestCall } from './testing/fake-claude.js'
|
|
60
|
-
export type { FakeSession, MessageDraft } from './testing/fake-sessions.js'
|
|
62
|
+
export type { FakeSession, MessageDraft, StartedTask } from './testing/fake-sessions.js'
|
|
61
63
|
export type { RoutedEvent } from './runtime/hooks.js'
|
|
62
64
|
export type { ShownToast } from './runtime/toaster.js'
|
|
63
65
|
|
|
@@ -116,12 +118,12 @@ export type TestedMod<State extends object> = {
|
|
|
116
118
|
fire<N extends FiredEvent>(event: N, input: Args<N>, below?: EventResult<N>): Promise<EventResult<N>>
|
|
117
119
|
settle(): Promise<void>
|
|
118
120
|
add(message: MessageDraft, options?: { readonly task?: string }): Promise<Message[]>
|
|
119
|
-
endTurn(ending?: Pick<Turn, 'answer' | 'failure'
|
|
121
|
+
endTurn(ending?: Pick<Turn, 'answer' | 'failure'>, options?: { readonly task?: string }): Promise<void>
|
|
120
122
|
compact(): Promise<void>
|
|
121
123
|
startSession(start: { readonly source: 'startup' | 'resume' | 'clear' | 'fork'; readonly id?: string }): Promise<void>
|
|
122
|
-
startTask(task:
|
|
124
|
+
startTask(task: StartedTask): Promise<string>
|
|
123
125
|
lines(area: Area, props?: { readonly columns?: number }): Promise<string[]>
|
|
124
|
-
lines<K extends Row>(kind: K, props:
|
|
126
|
+
lines<K extends Row>(kind: K, props: KindProps<K>, requestId?: string): Promise<string[]>
|
|
125
127
|
type(line: string): Promise<CommandReply>
|
|
126
128
|
hotkey(name: string): Promise<void>
|
|
127
129
|
callTool(name: string, input: Record<string, unknown>): Promise<EventResult<'tool.call'>>
|
|
@@ -159,9 +161,11 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
159
161
|
fake.claude.session.cwd = async () => cwd
|
|
160
162
|
const conversations = fakeSessions({
|
|
161
163
|
callTool: (args: Parameters<Claude["tool"]["call"]>[0]) => fake.claude.tool.call(args),
|
|
164
|
+
listAgents: () => fake.fakes.agent.list?.() ?? Promise.resolve([]),
|
|
162
165
|
project: () => projectRoot,
|
|
163
166
|
cwd: () => cwd,
|
|
164
167
|
given: options.sessions ?? [],
|
|
168
|
+
decide: (event, change) => decide(event, change),
|
|
165
169
|
push: (mod, session, change) => {
|
|
166
170
|
if (mod !== name) return
|
|
167
171
|
const pushed = { to: mod, method: conversationMethods.changed, input: { session, change }, caller: cmodPluginName } as unknown as Frozen<Args<'cmod.call'>>
|
|
@@ -170,6 +174,13 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
170
174
|
})
|
|
171
175
|
conversations.start(sessionId)
|
|
172
176
|
const callMod = async (method: string, input: unknown, caller = cmodPluginName) => calledAnswer(caller, await lifecycle.route('cmod.call', { to: name, method, input, caller } as unknown as Frozen<Args<'cmod.call'>>, async () => ({ deny: notInstalled(name) })))
|
|
177
|
+
const tellColor = async (told: SessionColor) => {
|
|
178
|
+
if (name !== cmodPluginName) await callMod(conversationMethods.sessionColor, told)
|
|
179
|
+
}
|
|
180
|
+
const tellHeldColor = async () => {
|
|
181
|
+
const held = conversations.color()
|
|
182
|
+
if (held.color !== undefined) await tellColor(held)
|
|
183
|
+
}
|
|
173
184
|
let declaredPages: ReturnType<typeof pagesOf> = []
|
|
174
185
|
let openPages: { readonly id: string; readonly params: unknown }[] = []
|
|
175
186
|
let shownPage: { readonly id: string; readonly params: unknown; readonly isFocused: boolean } | undefined
|
|
@@ -191,33 +202,61 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
191
202
|
await tellShown()
|
|
192
203
|
}
|
|
193
204
|
let widgets: Promise<Widgets> | undefined
|
|
205
|
+
let stylesMarkdown = false
|
|
206
|
+
const ownMarkdown = new Map<string, DrawnPart | null>()
|
|
207
|
+
const missedMarkdown = new Map<string, { readonly markdown: string } & StyledAt>()
|
|
194
208
|
const updated = async ({ parts, redraw, requests }: PartsUpdate) => {
|
|
195
|
-
if (parts !== undefined)
|
|
209
|
+
if (parts !== undefined) {
|
|
210
|
+
declaredPages = pagesOf(parts)
|
|
211
|
+
stylesMarkdown = parts.stylesMarkdown === true
|
|
212
|
+
ownMarkdown.clear()
|
|
213
|
+
}
|
|
196
214
|
if (parts !== undefined || redraw !== undefined) (await widgetsOf()).update(name, { ...(parts === undefined ? {} : { parts }), ...(redraw === undefined ? {} : { redraw }) })
|
|
197
215
|
if (requests !== undefined && requests.length > 0) await applyRequests(requests)
|
|
198
216
|
}
|
|
199
217
|
const pressPart = (input: PartPress) => callMod(partMethods.press, input).then((update) => updated((update ?? {}) as PartsUpdate))
|
|
218
|
+
const markdownIn = (table: ElementTable, at: StyledAt) => (props: MarkdownProps): RenderElement => {
|
|
219
|
+
const drawnAt = `${at.surface}\u0000${at.columns}\u0000${props.text}`
|
|
220
|
+
const drawn = stylesMarkdown ? ownMarkdown.get(drawnAt) : null
|
|
221
|
+
if (drawn === undefined) missedMarkdown.set(drawnAt, { markdown: props.text, ...at })
|
|
222
|
+
if (drawn === undefined || drawn === null || drawn.tree === null) return table.Markdown(props)
|
|
223
|
+
const { draw, tree } = drawn
|
|
224
|
+
return drawWith(table, () => elementOf(tree, '', (handler, args) => void pressPart({ draw, handler, args }))) as RenderElement
|
|
225
|
+
}
|
|
226
|
+
const withOwnMarkdown = async <Drawn>(draw: () => Promise<Drawn>): Promise<Drawn> => {
|
|
227
|
+
const first = await draw()
|
|
228
|
+
if (missedMarkdown.size === 0) return first
|
|
229
|
+
for (const [drawnAt, input] of missedMarkdown) ownMarkdown.set(drawnAt, (await callMod(partMethods.draw, input)) as DrawnPart | null)
|
|
230
|
+
missedMarkdown.clear()
|
|
231
|
+
return draw()
|
|
232
|
+
}
|
|
233
|
+
const cmodClaude: Claude = { ...fake.claude, cmod: { call: async (input) => (input.to === name ? callMod(input.method, input.input) : fake.claude.cmod.call(input)) } }
|
|
234
|
+
const reportPart = (_id: string, message: string, error: unknown) => fake.claude.ui.log(`cmod: ${message}: ${messageOf(error)}`, { to: 'debug' })
|
|
235
|
+
const parts = drawnParts({ claude: cmodClaude, report: reportPart, press: (_mod: string, input: PartPress) => void pressPart(input).catch((error: unknown) => fake.claude.ui.log(`cmod: ${name} did not take the press: ${messageOf(error)}`, { to: 'debug' })) })
|
|
200
236
|
const servedWidgets = async (): Promise<Widgets> => {
|
|
201
237
|
const own = name === cmodPluginName && lifecycle.mod !== undefined ? engineOf(lifecycle.mod).widgets : undefined
|
|
202
238
|
if (own !== undefined) return own
|
|
203
|
-
const claude: Claude = { ...fake.claude, cmod: { call: async (input) => (input.to === name ? callMod(input.method, input.input) : fake.claude.cmod.call(input)) } }
|
|
204
|
-
const report = (_id: string, message: string, error: unknown) => fake.claude.ui.log(`cmod: ${message}: ${messageOf(error)}`, { to: 'debug' })
|
|
205
|
-
const press = (_mod: string, input: PartPress) => void pressPart(input).catch((error: unknown) => fake.claude.ui.log(`cmod: ${name} did not take the press: ${messageOf(error)}`, { to: 'debug' }))
|
|
206
239
|
return serveWidgets({
|
|
207
|
-
claude,
|
|
240
|
+
claude: cmodClaude,
|
|
208
241
|
router: cmodPlugin,
|
|
209
242
|
store: storeFolder({ HOME: await fake.claude.env.home(), XDG_DATA_HOME: await fake.claude.env.dataHome() }),
|
|
210
|
-
report,
|
|
243
|
+
report: reportPart,
|
|
211
244
|
turnClosedBy: () => conversations.lastTurn(),
|
|
212
|
-
parts
|
|
245
|
+
parts,
|
|
246
|
+
markdownIn,
|
|
213
247
|
})
|
|
214
248
|
}
|
|
215
249
|
const widgetsOf = () => (widgets ??= servedWidgets())
|
|
250
|
+
const cmodBeneath = createRouter()
|
|
251
|
+
const pages = servePages({ claude: cmodClaude, router: cmodBeneath, report: reportPart, widgets: { update: (mod, update) => void widgetsOf().then((served) => served.update(mod, update)) }, parts, tellStylers: () => undefined })
|
|
252
|
+
pages.apply((_mod, requests) => applyRequests(requests))
|
|
216
253
|
const dependencies = fakeDependencies(name, options.dependencies ?? {}, conversations)
|
|
217
254
|
fake.fakes.cmod.call = async (call) => {
|
|
218
255
|
if (call.to !== cmodPluginName || call.method !== partMethods.changed) return dependencies(call)
|
|
219
|
-
|
|
220
|
-
|
|
256
|
+
const { requests, isAsked, ...listing } = call.input as PartsUpdate
|
|
257
|
+
const isHeld = isAsked === true && requests !== undefined && pages.update(name, { requests, isAsked })
|
|
258
|
+
await updated(isAsked === true ? listing : (call.input as PartsUpdate))
|
|
259
|
+
return { isHeld }
|
|
221
260
|
}
|
|
222
261
|
const appendRow = fake.claude.session.append
|
|
223
262
|
fake.claude.session.append = async (args) => {
|
|
@@ -244,6 +283,7 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
244
283
|
const state = Object.fromEntries(declared.map(([lifetime, values]) => [lifetime, { ...values, ...starting?.[lifetime] }])) as State
|
|
245
284
|
const lifecycle = createLifecycle<State, Declared>({ ...definition, state }, () => permissionRoutes,(options.options ?? {}) as PluginOptions, false)
|
|
246
285
|
let started: Promise<void> | undefined
|
|
286
|
+
let isColorTold = false
|
|
247
287
|
|
|
248
288
|
const start = async () => {
|
|
249
289
|
started ??= lifecycle.start(fake.claude, async (claude) => {
|
|
@@ -267,6 +307,9 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
267
307
|
throw new Error(`${name} needs ${listed(failure.titles)}, which a person sets in /config. Give ${failure.keys.length === 1 ? 'it' : 'them'} to the tested mod: testMod(mod, { options: { ${given} } }).`)
|
|
268
308
|
}
|
|
269
309
|
if (lifecycle.phase !== 'active') throw failure ?? new Error(`${name} did not start: the lifecycle is ${lifecycle.phase}.`)
|
|
310
|
+
if (isColorTold) return
|
|
311
|
+
isColorTold = true
|
|
312
|
+
await tellHeldColor()
|
|
270
313
|
}
|
|
271
314
|
|
|
272
315
|
const cmodPlugin = createRouter()
|
|
@@ -274,39 +317,69 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
274
317
|
const asked = { to: name, method: conversationMethods.before, input: { session: sessionId, event, change }, caller: cmodPluginName } as unknown as Frozen<Args<'cmod.call'>>
|
|
275
318
|
return calledAnswer(cmodPluginName, await lifecycle.route('cmod.call', asked, async () => ({ deny: notInstalled(name) })))
|
|
276
319
|
}
|
|
320
|
+
const decide: Decide = (event, change) => foldBefore(event, change, [(latest) => askMod(event, latest)], (error) => fake.claude.ui.log(`cmod: passed the ${event} on without ${name}'s answer: ${messageOf(error)}`, { to: 'debug' }))
|
|
277
321
|
installBefore({
|
|
278
322
|
claude: fake.claude,
|
|
279
323
|
router: cmodPlugin,
|
|
280
324
|
hooks: {
|
|
281
325
|
place: async () => conversations.place(),
|
|
282
326
|
wants: async (event) => conversations.wants(name, event),
|
|
283
|
-
fold: () =>
|
|
327
|
+
fold: () => decide,
|
|
284
328
|
},
|
|
285
329
|
})
|
|
286
330
|
|
|
287
331
|
const route = async <N extends RoutedEvent>(event: N, input: unknown, below: unknown): Promise<EventResult<N>> => {
|
|
288
332
|
await start()
|
|
289
333
|
if (event === 'ui.render') {
|
|
334
|
+
const { component, requestId } = input as Args<'ui.render'>
|
|
335
|
+
if (component === 'Pane') fake.placePane(requestId)
|
|
290
336
|
await widgetsOf()
|
|
291
337
|
await fake.settle()
|
|
292
338
|
}
|
|
293
|
-
return cmodPlugin.dispatch(event, input as Frozen<Args<N>>, (passed) => lifecycle.route(event, passed, async (e) => (
|
|
339
|
+
return cmodPlugin.dispatch(event, input as Frozen<Args<N>>, (passed) => lifecycle.route(event, passed, async (e) => (await claudeCodeRuns(event, e, below)) as EventResult<N>))
|
|
294
340
|
}
|
|
295
341
|
|
|
296
|
-
const
|
|
342
|
+
const claudeCodeRuns = async (event: RoutedEvent, e: unknown, below: unknown): Promise<unknown> => {
|
|
343
|
+
if (event === 'session.compact' && (e as Args<'session.compact'>).trigger !== 'precompute') {
|
|
344
|
+
const started = await fire('SessionStart', { source: 'compact' })
|
|
345
|
+
fake.shown.notes.push(...(started.additionalContext ?? []).map(untagged))
|
|
346
|
+
}
|
|
347
|
+
if (below !== undefined) return below
|
|
348
|
+
const answer = claudeCodeAnswer(event, e)
|
|
349
|
+
if (event === 'ui.close') {
|
|
350
|
+
const { id, origin } = e as Args<'ui.close'>
|
|
351
|
+
if (origin.kind === 'person' && fake.shown.openPanes.has(id)) await drawPane(id)
|
|
352
|
+
fake.shown.openPanes.delete(id)
|
|
353
|
+
}
|
|
354
|
+
if (event === 'agent.spawn') {
|
|
355
|
+
const started = await route('classic.SubagentStart', { hook_event_name: 'SubagentStart', agent_id: (answer as { agentId: string }).agentId, agent_type: (e as Args<'agent.spawn'>).subagentType }, undefined)
|
|
356
|
+
fake.shown.notes.push(...(started.additionalContext ?? []).map(untagged))
|
|
357
|
+
}
|
|
358
|
+
return answer
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const runCommand = async (command: string, args: string, builtIn: CommandRunResult = {}): Promise<CommandRunResult> => {
|
|
297
362
|
await start()
|
|
298
363
|
const reached: Set<string> = new Set()
|
|
299
|
-
const ownRun = async (): Promise<CommandRunResult> => (reached.add(command),
|
|
300
|
-
const answer = await cmodPlugin.dispatch('command.run', { command, args, origin: { kind: 'composer' }, presentation }, (passed) => lifecycle.route('command.run', passed, ownRun))
|
|
364
|
+
const ownRun = async (): Promise<CommandRunResult> => (reached.add(command), builtIn)
|
|
365
|
+
const answer = await cmodPlugin.dispatch('command.run', { command, args, origin: { kind: 'composer' }, presentation }, (passed) => lifecycle.route('command.run', passed, (e) => cmodBeneath.dispatch('command.run', e, ownRun)))
|
|
301
366
|
if (reached.has(command) && answer.text === undefined) throw new Error(!fake.shown.commands.includes(command) ? `${name} has no slash command /${command}. Add it in setup with mod.use(slashCommand({ name: '${command}', … })).` : `${name}: /${command} reached Claude Code's own run of the command, so Claude Code shows "${name} registered /${command} but no command.run hook answered it". Answer it without calling next(e).`)
|
|
302
367
|
return answer
|
|
303
368
|
}
|
|
304
369
|
|
|
305
|
-
const
|
|
370
|
+
const runColor = async (args: string): Promise<CommandRunResult> => {
|
|
371
|
+
if (args.trim() === '') throw new Error(`Claude Code's /color with no color picks one at random, so a test names the color, such as tested.type('/color cyan').`)
|
|
372
|
+
const color = pickedColor(args)
|
|
373
|
+
const answer = await runCommand('color', args, { text: color === undefined ? 'Session color reset to default' : `Session color set to: ${color}` })
|
|
374
|
+
await tellColor(conversations.recolor(color))
|
|
375
|
+
return answer
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const drawPane = async (paneId: string, asked?: number) => {
|
|
306
379
|
await start()
|
|
307
380
|
if (!fake.shown.openPanes.has(paneId)) throw new Error(`The pane "${paneId}" of ${name} closed, and a user sees a pane only while it is open.`)
|
|
308
381
|
const opened = fake.calls.findLast((call) => call.call === 'ui.open' && (call.args[0] as PaneOpenArgs).id === paneId)?.args[0] as PaneOpenArgs | undefined
|
|
309
|
-
const columns = opened?.columns ?? defaultColumns
|
|
382
|
+
const columns = asked ?? opened?.columns ?? defaultColumns
|
|
310
383
|
const isFocused = (await fake.claude.ui.panes()).some((open) => open.id === paneId && open.isFocused)
|
|
311
384
|
const props: RenderPropsOf['Pane'] = { title: opened?.title ?? paneId, isFocused, bodyColumns: columns, placement: 'dock', scroll: { offset: 0, bodyRows: defaultRows }, view: {} }
|
|
312
385
|
const drawing = await route('ui.render', { surface: 'terminal', component: 'Pane', requestId: paneId, props }, elements.Box({}))
|
|
@@ -320,7 +393,7 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
320
393
|
const drawn = (await callMod(partMethods.draw, { page: pageId, params: page.params, columns, isFocused: page.isFocused })) as DrawnPart | null
|
|
321
394
|
await fake.settle()
|
|
322
395
|
const tree = drawn?.tree ?? null
|
|
323
|
-
const drawing = tree === null || drawn === null ? elements.Box({}) : (drawWith(elements, () => elementOf(tree, '', (handler, args) => void pressPart({ draw: drawn.draw, handler, args }))) as RenderElement)
|
|
396
|
+
const drawing = tree === null || drawn === null ? elements.Box({}) : await withOwnMarkdown(async () => drawWith(elements, () => elementOf(tree, '', (handler, args) => void pressPart({ draw: drawn.draw, handler, args })), markdownIn(elements, { surface: 'terminal', columns })) as RenderElement)
|
|
324
397
|
return { drawing, columns }
|
|
325
398
|
}
|
|
326
399
|
|
|
@@ -339,7 +412,7 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
339
412
|
const draw = async (columns?: number) => {
|
|
340
413
|
await start()
|
|
341
414
|
const { id } = shownNow(isPane)
|
|
342
|
-
return { id, ...(isPane ? await drawPane(id) : await drawPage(id, columns)) }
|
|
415
|
+
return { id, ...(isPane ? await drawPane(id, columns) : await drawPage(id, columns)) }
|
|
343
416
|
}
|
|
344
417
|
const drawnElement = async (type: 'Button' | 'Input' | 'Select' | 'Markdown', key: string) => {
|
|
345
418
|
const { id, drawing } = await draw()
|
|
@@ -368,7 +441,7 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
368
441
|
else await (props['onLinkPress'] as (link: PressedLink, e: Frozen<UiPressArgument>) => unknown)(e.link, e)
|
|
369
442
|
return { element: e.element }
|
|
370
443
|
}
|
|
371
|
-
if (isPane) await lifecycle.route('ui.press', pressed, run)
|
|
444
|
+
if (isPane) await lifecycle.route('ui.press', pressed, (e) => cmodBeneath.dispatch('ui.press', e, run))
|
|
372
445
|
else await run(pressed)
|
|
373
446
|
await fake.settle()
|
|
374
447
|
},
|
|
@@ -397,7 +470,7 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
397
470
|
await fake.settle()
|
|
398
471
|
const turn = area === TurnEndArea ? conversations.lastTurn() : undefined
|
|
399
472
|
if (area === TurnEndArea && turn?.closedAt === undefined) throw new Error(`${name} has no ended turn, so TurnEnd draws nothing. End one with tested.add({ type: Prompt, text: '…' }), then tested.endTurn().`)
|
|
400
|
-
return { drawing: elements.Box({ flexDirection: 'column', children: [...(await served.draw(area, elements, columns, turn))] }), columns }
|
|
473
|
+
return { drawing: elements.Box({ flexDirection: 'column', children: [...(await withOwnMarkdown(() => served.draw(area, elements, { surface: 'terminal', columns }, turn)))] }), columns }
|
|
401
474
|
}
|
|
402
475
|
|
|
403
476
|
const drawSlot = async (component: RenderComponent, props: object, requestId = `${component}_${(slotDraws += 1)}`) => {
|
|
@@ -420,7 +493,11 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
420
493
|
const toolUse = toolUseEvents.includes(event) ? { tool_use_id: `toolu_${(toolUses += 1)}` } : {}
|
|
421
494
|
const transcript = `/test/transcripts/${sessionId}.jsonl`
|
|
422
495
|
const filled = { session_id: sessionId, transcript_path: transcript, cwd, hook_event_name: event, ...toolUse, ...input }
|
|
423
|
-
if (event === 'SessionStart'
|
|
496
|
+
if (event === 'SessionStart') {
|
|
497
|
+
const source = (input as { readonly source?: string }).source
|
|
498
|
+
const started = 'transcript_path' in input ? route('classic.SessionStart', filled, below) : files.write(transcript, '').then(() => route('classic.SessionStart', filled, below))
|
|
499
|
+
return source === 'startup' || source === 'clear' ? started : started.then(async (answer) => (await tellHeldColor(), answer))
|
|
500
|
+
}
|
|
424
501
|
if (event !== 'PreToolUse') return route(`classic.${event}` as RoutedEvent, filled, below)
|
|
425
502
|
return firePreToolUse(filled as ClassicHookInputs['PreToolUse'], below as { deny?: string } | undefined)
|
|
426
503
|
}) as ClaudeCode['fire']
|
|
@@ -500,9 +577,9 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
500
577
|
await start()
|
|
501
578
|
return settled(conversations.add(message, added?.task))
|
|
502
579
|
},
|
|
503
|
-
endTurn: async (ending) => {
|
|
580
|
+
endTurn: async (ending, options) => {
|
|
504
581
|
await start()
|
|
505
|
-
await settled(conversations.endTurn(ending === undefined ? undefined : { ...ending, ...(ending.failure === undefined ? {} : { reason: 'error' as const }) }))
|
|
582
|
+
await settled(conversations.endTurn(ending === undefined ? undefined : { ...ending, ...(ending.failure === undefined ? {} : { reason: 'error' as const }) }, options?.task))
|
|
506
583
|
},
|
|
507
584
|
compact: async () => {
|
|
508
585
|
await start()
|
|
@@ -531,7 +608,7 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
531
608
|
const typed = /^\/(\S+)\s*(.*)$/s.exec(line)
|
|
532
609
|
if (typed === null) throw new Error(`type takes a slash command the way the user types it, such as tested.type('/help'), and "${line}" does not start with /.`)
|
|
533
610
|
const [, command = '', args = ''] = typed
|
|
534
|
-
return replyOf(await runCommand(command, args))
|
|
611
|
+
return replyOf(await (command === 'color' ? runColor(args) : runCommand(command, args)))
|
|
535
612
|
},
|
|
536
613
|
async callTool(toolName, input) {
|
|
537
614
|
const tool = `mcp__${name}__${toolName}`
|
|
@@ -617,6 +694,10 @@ function claudeCodeAnswer(event: RoutedEvent, e: unknown): unknown {
|
|
|
617
694
|
const { model, parentModel } = e as Args<'agent.spawn'>
|
|
618
695
|
return { agentId: 'agent-1', model: model ?? parentModel }
|
|
619
696
|
}
|
|
697
|
+
if (event === 'session.append') {
|
|
698
|
+
const { message, uuid } = e as Args<'session.append'>
|
|
699
|
+
return { message, uuid }
|
|
700
|
+
}
|
|
620
701
|
if (event !== 'prompt.submit') return {}
|
|
621
702
|
const { text, context, origin } = e as Args<'prompt.submit'>
|
|
622
703
|
return { text, ...(context === undefined ? {} : { context }), origin }
|
package/src/ui/areas.ts
CHANGED
|
@@ -17,6 +17,7 @@ export type WidgetInit<State extends object = Record<never, never>> = {
|
|
|
17
17
|
readonly id: string
|
|
18
18
|
readonly title: string
|
|
19
19
|
readonly areas: readonly [Area, ...Area[]]
|
|
20
|
+
readonly place?: { readonly side?: 'left' | 'right'; readonly size?: 'fit' | 'fill' }
|
|
20
21
|
render(mod: Mod<State>, context: WidgetContext): RenderElement | undefined
|
|
21
22
|
}
|
|
22
23
|
|
package/src/ui/components.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import type { RenderChildren, RenderElement } from 'claude-code'
|
|
2
2
|
import { Box, Button, Text } from './elements.js'
|
|
3
3
|
|
|
4
|
-
export type Tab = { readonly key: string; readonly label: string }
|
|
4
|
+
export type Tab = { readonly key: string; readonly label: string; readonly hotkey?: string }
|
|
5
5
|
|
|
6
6
|
export function Tabs({ tabs, selected, onSelect, children }: { readonly tabs: readonly Tab[]; readonly selected: string; readonly onSelect: (key: string) => void; readonly children?: RenderChildren }): RenderElement {
|
|
7
|
-
const row = tabs.map((
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
})
|
|
7
|
+
const row = tabs.map(({ key, label, hotkey }) =>
|
|
8
|
+
key === selected
|
|
9
|
+
? Text({ children: [...(hotkey === undefined ? [] : [Text({ color: 'claude', children: hotkey }), ': ']), Text({ bold: true, underline: true, children: label })] })
|
|
10
|
+
: Button({ plain: true, dimColor: true, key: `tab:${key}`, ...(hotkey === undefined ? {} : { hotkey }), label, onPress: () => onSelect(key) }),
|
|
11
|
+
)
|
|
13
12
|
return Box({ flexDirection: 'column', gap: 1, children: [Box({ gap: 3, children: row }), children] })
|
|
14
13
|
}
|
|
15
14
|
|
|
@@ -19,7 +18,14 @@ export function Split({ children }: { readonly children: readonly RenderElement[
|
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
export function Panel({ title, children }: { readonly title: string; readonly children?: RenderChildren }): RenderElement {
|
|
22
|
-
return Box({
|
|
21
|
+
return Box({
|
|
22
|
+
flexDirection: 'column',
|
|
23
|
+
flexGrow: 1,
|
|
24
|
+
children: [
|
|
25
|
+
Box({ flexDirection: 'column', flexGrow: 1, minWidth: [...title].length + 6, borderStyle: 'round', borderColor: 'subtle', paddingX: 1, children }),
|
|
26
|
+
Box({ position: 'absolute', top: 0, left: 2, children: Text({ bold: true, wrap: 'truncate-end', children: ` ${title} ` }) }),
|
|
27
|
+
],
|
|
28
|
+
})
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
export function Toggle({ label, checked, onChange }: { readonly label: string; readonly checked: boolean; readonly onChange: (checked: boolean) => void }): RenderElement {
|
package/src/ui/elements.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ElementName, ElementTable, Elements, MarkdownProps, RenderElement } from 'claude-code'
|
|
2
2
|
|
|
3
|
-
type Terminal = Elements['terminal']
|
|
3
|
+
type Terminal = Elements['terminal'] & Elements['desktop']
|
|
4
4
|
|
|
5
5
|
type CompletedTable = Record<ElementName, (props: object) => RenderElement>
|
|
6
6
|
|
|
@@ -53,6 +53,7 @@ export function elementData(value: unknown, handlers: Handler[]): unknown {
|
|
|
53
53
|
if (typeof value !== 'object' || value === null) return value
|
|
54
54
|
if (unbuilt in value) {
|
|
55
55
|
const { name, props } = (value as Unbuilt)[unbuilt]
|
|
56
|
+
if (name === 'Client') throw new Error('Client draws only in panes and styles.')
|
|
56
57
|
return { [elementKey]: name, props: elementData(props, handlers) }
|
|
57
58
|
}
|
|
58
59
|
if (Object.getPrototypeOf(value) !== Object.prototype) throw new Error('a widget draws only the elements of ui/elements.js and ui/components.js, text, numbers, and plain objects.')
|
|
@@ -83,5 +84,8 @@ export const Markdown = element('Markdown')
|
|
|
83
84
|
export const Input = element('Input')
|
|
84
85
|
export const Select = element('Select')
|
|
85
86
|
export const Image = element('Image')
|
|
87
|
+
export const Svg = element('Svg')
|
|
88
|
+
export const Raster = element('Raster')
|
|
89
|
+
export const Client = element('Client')
|
|
86
90
|
|
|
87
|
-
const constructors = { Box, Text, Button, Link, Code, Markdown, Input, Select, Image }
|
|
91
|
+
const constructors = { Box, Text, Button, Link, Code, Markdown, Input, Select, Image, Svg, Raster }
|
package/src/ui/markdown.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Nodes, RootContent } from 'mdast'
|
|
2
2
|
import { fromMarkdown, gfm, gfmFromMarkdown, toString } from '../vendor-markdown.js'
|
|
3
|
-
import type {
|
|
3
|
+
import type { KindProps } from './styles.js'
|
|
4
4
|
|
|
5
5
|
type BlockText = { readonly text: string; readonly source: string }
|
|
6
6
|
|
|
@@ -163,7 +163,7 @@ function plainText(node: Nodes): string {
|
|
|
163
163
|
return node.children.map(plainText).join(node.type === 'tableRow' ? '\t' : '\n')
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
export function markdownBlocks<K extends Block>(text: string, kind: K):
|
|
166
|
+
export function markdownBlocks<K extends Block>(text: string, kind: K): KindProps<K>[] {
|
|
167
167
|
const blocks = parsed(text, 0).filter((block) => block.type === kind.markdown)
|
|
168
|
-
return blocks.map((block) => block.props) as unknown as
|
|
168
|
+
return blocks.map((block) => block.props) as unknown as KindProps<K>[]
|
|
169
169
|
}
|
package/src/ui/panes.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Frozen, PaneCloseInput, RenderElement, RenderPropsOf, UiScrollInput } from 'claude-code'
|
|
2
|
-
import type { Mod } from '../mod.js'
|
|
2
|
+
import type { Mod, Surface } from '../mod.js'
|
|
3
3
|
|
|
4
4
|
export type ScrollTarget = 'start' | 'end' | { readonly key: string }
|
|
5
5
|
|
|
@@ -7,7 +7,7 @@ export type ScrollOptions = { readonly block?: 'start' | 'center' | 'end' | 'nea
|
|
|
7
7
|
|
|
8
8
|
export type OpenOptions<Params> = { readonly focus?: true } & (undefined extends Params ? { readonly params?: Params } : { readonly params: Params })
|
|
9
9
|
|
|
10
|
-
export type PaneProps<Params> = Frozen<RenderPropsOf['Pane']> & { readonly params: Params }
|
|
10
|
+
export type PaneProps<Params> = Frozen<RenderPropsOf['Pane']> & { readonly surface: Surface; readonly params: Params }
|
|
11
11
|
|
|
12
12
|
export type PaneInit<State extends object = Record<never, never>, Params = undefined> = {
|
|
13
13
|
readonly id: string
|
package/src/ui/styles.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Frozen, RenderComponent, RenderElement, RenderPropsOf } from 'claude-code'
|
|
2
|
-
import type { Mod } from '../mod.js'
|
|
2
|
+
import type { Mod, Surface } from '../mod.js'
|
|
3
3
|
import { TurnEnd, Working } from './areas.js'
|
|
4
4
|
import type { Block, BlockProps } from './markdown.js'
|
|
5
5
|
|
|
@@ -32,7 +32,11 @@ type PropsOf<K extends Kind> = K extends Row ? RowProps[K] : K extends Block<inf
|
|
|
32
32
|
|
|
33
33
|
type DefaultOf<K extends Kind> = K extends Row ? Partial<Omit<RowProps[K], 'onScreen'>> : { readonly source?: string }
|
|
34
34
|
|
|
35
|
-
export type StyleProps<K extends Kind> = Frozen<PropsOf<K>> & { readonly Default: (props: Frozen<DefaultOf<K>>) => RenderElement }
|
|
35
|
+
export type StyleProps<K extends Kind> = Frozen<PropsOf<K>> & StyledAt & (K extends Row ? { readonly id: string } : { readonly id?: string }) & { readonly Default: (props: Frozen<DefaultOf<K>>) => RenderElement }
|
|
36
|
+
|
|
37
|
+
export type KindProps<K extends Kind> = Omit<StyleProps<K>, 'Default' | 'surface' | 'columns' | 'id'>
|
|
38
|
+
|
|
39
|
+
export type StyledAt = { readonly surface: Surface; readonly columns: number }
|
|
36
40
|
|
|
37
41
|
export type StyleInit<K extends Kind, State extends object = Record<never, never>> = {
|
|
38
42
|
readonly kind: K
|
package/testing/fake-claude.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ export type Fakes = {
|
|
|
40
40
|
messages?: Claude['session']['messages'];
|
|
41
41
|
append?: Claude['session']['append'];
|
|
42
42
|
send?: Claude['session']['send'];
|
|
43
|
+
surfaces?: Claude['session']['surfaces'];
|
|
44
|
+
usage?: Claude['session']['usage'];
|
|
43
45
|
};
|
|
44
46
|
prompt: {
|
|
45
47
|
submit?: Claude['prompt']['submit'];
|
|
@@ -85,6 +87,7 @@ export type FakeClaude = {
|
|
|
85
87
|
readonly shown: Shown;
|
|
86
88
|
readonly store: Map<string, unknown>;
|
|
87
89
|
settle(): Promise<void>;
|
|
90
|
+
placePane(id: string): void;
|
|
88
91
|
};
|
|
89
92
|
export declare function calledAnswer(caller: string, answer: EventResult<'cmod.call'>): unknown;
|
|
90
93
|
export declare function fakeClaude(plugin: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fake-claude.d.ts","sourceRoot":"","sources":["../src/testing/fake-claude.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAS,MAAM,aAAa,CAAA;AAErD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAOlD,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,CAAA;CAClC,CAAA;AAED,MAAM,MAAM,KAAK,GAAG;IAClB,OAAO,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;IAC/E,EAAE,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IAC7J,IAAI,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;IACzC,QAAQ,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IAC/C,MAAM,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IAC3C,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAA;KAAE,CAAA;IAC7E,EAAE,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;IAC3J,OAAO,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,UAAU,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"fake-claude.d.ts","sourceRoot":"","sources":["../src/testing/fake-claude.ts"],"names":[],"mappings":"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"}
|
package/testing/fake-claude.js
CHANGED
|
@@ -31,6 +31,15 @@ export function fakeClaude(plugin) {
|
|
|
31
31
|
const run = faked(call, answer);
|
|
32
32
|
return async (...args) => run(...args);
|
|
33
33
|
};
|
|
34
|
+
const cmodCall = rejected('cmod.call', () => fakes.cmod.call ?? cmodTakesParts);
|
|
35
|
+
const clonedAsClaudeCodeDoes = (args) => {
|
|
36
|
+
try {
|
|
37
|
+
return structuredClone(args);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new Error(`interface.call: arguments are not plain data: ${error instanceof Error ? error.message : String(error)}`);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
34
43
|
const cmodTakesParts = async (input) => {
|
|
35
44
|
if (input.to === cmodPluginName && input.method === partMethods.changed)
|
|
36
45
|
return null;
|
|
@@ -70,7 +79,7 @@ export function fakeClaude(plugin) {
|
|
|
70
79
|
lines.push(text);
|
|
71
80
|
},
|
|
72
81
|
notice: () => undefined,
|
|
73
|
-
invalidate: () =>
|
|
82
|
+
invalidate: (event) => void calls.push({ call: 'ui.invalidate', args: [event] }),
|
|
74
83
|
ask: rejected('ui.ask', () => fakes.ui.ask),
|
|
75
84
|
open: async (pane) => {
|
|
76
85
|
calls.push({ call: 'ui.open', args: [pane] });
|
|
@@ -143,8 +152,8 @@ export function fakeClaude(plugin) {
|
|
|
143
152
|
root: async () => plugin.root,
|
|
144
153
|
cwd: async () => plugin.root,
|
|
145
154
|
model: async () => 'test-model',
|
|
146
|
-
usage:
|
|
147
|
-
surfaces:
|
|
155
|
+
usage: () => fakes.session.usage?.() ?? Promise.resolve({ startedAt: Date.parse('2026-01-01T00:00:00.000Z'), context: { window: 200000 }, rateLimits: [], cost: { usd: 0 } }),
|
|
156
|
+
surfaces: () => fakes.session.surfaces?.() ?? Promise.resolve(['terminal']),
|
|
148
157
|
messages: rejected('session.messages', () => fakes.session.messages),
|
|
149
158
|
append: rejected('session.append', () => fakes.session.append ?? appendNote),
|
|
150
159
|
send: rejected('session.send', () => fakes.session.send ?? (async () => ({ isDelivered: true }))),
|
|
@@ -172,7 +181,7 @@ export function fakeClaude(plugin) {
|
|
|
172
181
|
dataHome: async () => undefined,
|
|
173
182
|
configHome: async () => undefined,
|
|
174
183
|
},
|
|
175
|
-
cmod: { call:
|
|
184
|
+
cmod: { call: async (args) => cmodCall(clonedAsClaudeCodeDoes(args)) },
|
|
176
185
|
};
|
|
177
186
|
const settle = async () => {
|
|
178
187
|
let seen;
|
|
@@ -181,7 +190,11 @@ export function fakeClaude(plugin) {
|
|
|
181
190
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
182
191
|
} while (calls.length !== seen);
|
|
183
192
|
};
|
|
184
|
-
|
|
193
|
+
const placePane = (id) => {
|
|
194
|
+
if (unplacedPanes.delete(id))
|
|
195
|
+
shown.openPanes.add(id);
|
|
196
|
+
};
|
|
197
|
+
return { claude, calls, fakes, shown, store, settle, placePane };
|
|
185
198
|
}
|
|
186
199
|
function realTimer(ms, fn) {
|
|
187
200
|
const timeout = setTimeout(fn, ms);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fake-elements.d.ts","sourceRoot":"","sources":["../src/testing/fake-elements.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAiB,UAAU,EAAE,MAAM,aAAa,CAAA;AAE1E,KAAK,KAAK,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,UAAU,EAAE,CAAA;CAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"fake-elements.d.ts","sourceRoot":"","sources":["../src/testing/fake-elements.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAiB,UAAU,EAAE,MAAM,aAAa,CAAA;AAE1E,KAAK,KAAK,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,UAAU,EAAE,CAAA;CAAE,CAAA;AAuB1H,eAAO,MAAM,QAAQ,EAAE,YAatB,CAAA;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,SAAS,CASnG;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAOhE;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAiB/C"}
|