@cmodjs/core 4.0.3 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/commands.md +43 -8
- package/docs/index.md +3 -3
- package/docs/install-steps.md +6 -6
- package/docs/jobs.md +2 -1
- package/docs/mod.md +1 -1
- package/docs/options.md +1 -1
- package/docs/permissions.md +1 -1
- package/docs/session.md +44 -13
- package/docs/testing.md +14 -10
- package/docs/ui.md +42 -10
- package/installer.d.ts +3 -2
- package/installer.d.ts.map +1 -1
- package/jobs/program.d.ts +1 -0
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +39 -23
- package/mod.d.ts +3 -2
- package/mod.d.ts.map +1 -1
- package/mod.js +1 -0
- package/package.json +1 -1
- package/records.d.ts +15 -1
- package/records.d.ts.map +1 -1
- package/records.js +39 -1
- package/register.d.ts.map +1 -1
- package/register.js +5 -2
- package/runtime/before.d.ts +6 -2
- package/runtime/before.d.ts.map +1 -1
- package/runtime/before.js +56 -18
- package/runtime/buttons.d.ts +10 -0
- package/runtime/buttons.d.ts.map +1 -0
- package/runtime/buttons.js +12 -0
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +134 -25
- package/runtime/engine.d.ts +2 -0
- package/runtime/engine.d.ts.map +1 -1
- package/runtime/grants.d.ts +1 -0
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +5 -2
- package/runtime/hotkeys.d.ts.map +1 -1
- package/runtime/hotkeys.js +7 -4
- package/runtime/installer.d.ts +9 -6
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +104 -47
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +121 -40
- package/runtime/pages.d.ts +3 -1
- package/runtime/pages.d.ts.map +1 -1
- package/runtime/pages.js +34 -10
- package/runtime/parts.d.ts +1 -0
- package/runtime/parts.d.ts.map +1 -1
- package/runtime/parts.js +6 -3
- package/runtime/programs.d.ts +1 -0
- package/runtime/programs.d.ts.map +1 -1
- package/runtime/programs.js +4 -1
- package/runtime/sessions.d.ts +2 -2
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +88 -58
- package/runtime/toaster.d.ts.map +1 -1
- package/runtime/toaster.js +23 -9
- package/runtime/ui.d.ts +21 -4
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +158 -58
- package/runtime/watches.d.ts.map +1 -1
- package/runtime/watches.js +18 -4
- package/runtime/widgets.d.ts +7 -5
- package/runtime/widgets.d.ts.map +1 -1
- package/runtime/widgets.js +29 -20
- package/session/context/turn/message/attached-file.d.ts +13 -0
- package/session/context/turn/message/attached-file.d.ts.map +1 -0
- package/session/context/turn/message/attached-file.js +21 -0
- package/session/context/turn/message/message.d.ts +3 -2
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +20 -6
- package/session/conversation.d.ts +14 -3
- package/session/conversation.d.ts.map +1 -1
- package/session/conversation.js +23 -4
- package/session/goal/task.d.ts +5 -0
- package/session/goal/task.d.ts.map +1 -1
- package/session/session.d.ts +35 -7
- package/session/session.d.ts.map +1 -1
- package/session/session.js +2 -1
- package/src/installer.ts +3 -2
- package/src/jobs/program.ts +41 -21
- package/src/mod.ts +3 -2
- package/src/records.ts +49 -1
- package/src/register.ts +9 -6
- package/src/runtime/before.ts +61 -22
- package/src/runtime/buttons.ts +22 -0
- package/src/runtime/conversations.ts +138 -32
- package/src/runtime/engine.ts +2 -0
- package/src/runtime/grants.ts +6 -2
- package/src/runtime/hotkeys.ts +7 -4
- package/src/runtime/installer.ts +144 -82
- package/src/runtime/lifecycle.ts +128 -43
- package/src/runtime/pages.ts +45 -8
- package/src/runtime/parts.ts +8 -4
- package/src/runtime/programs.ts +5 -1
- package/src/runtime/sessions.ts +83 -57
- package/src/runtime/toaster.ts +27 -9
- package/src/runtime/ui.ts +163 -61
- package/src/runtime/watches.ts +20 -5
- package/src/runtime/widgets.ts +34 -22
- package/src/session/context/turn/message/attached-file.ts +36 -0
- package/src/session/context/turn/message/message.ts +20 -7
- package/src/session/conversation.ts +30 -6
- package/src/session/goal/task.ts +6 -0
- package/src/session/session.ts +17 -7
- package/src/testing/fake-claude.ts +21 -6
- package/src/testing/fake-elements.ts +51 -8
- package/src/testing/fake-sessions.ts +60 -23
- package/src/testing.ts +86 -28
- package/src/ui/areas.ts +1 -0
- package/src/ui/components.ts +14 -8
- package/src/ui/elements.ts +6 -2
- package/src/ui/markdown.ts +3 -3
- package/src/ui/panes.ts +2 -2
- package/src/ui/styles.ts +6 -2
- package/testing/fake-claude.d.ts +3 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +18 -5
- package/testing/fake-elements.d.ts.map +1 -1
- package/testing/fake-elements.js +46 -7
- package/testing/fake-sessions.d.ts +21 -10
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +58 -20
- package/testing.d.ts +8 -12
- package/testing.d.ts.map +1 -1
- package/testing.js +82 -21
- package/ui/areas.d.ts +4 -0
- package/ui/areas.d.ts.map +1 -1
- package/ui/components.d.ts +1 -0
- package/ui/components.d.ts.map +1 -1
- package/ui/components.js +11 -7
- package/ui/elements.d.ts +3 -0
- package/ui/elements.d.ts.map +1 -1
- package/ui/elements.js +6 -1
- package/ui/markdown.d.ts +2 -2
- package/ui/markdown.d.ts.map +1 -1
- package/ui/panes.d.ts +2 -1
- package/ui/panes.d.ts.map +1 -1
- package/ui/styles.d.ts +11 -2
- package/ui/styles.d.ts.map +1 -1
package/src/runtime/lifecycle.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Step } from '../installer.js'
|
|
|
3
3
|
|
|
4
4
|
import type { Allowed, Imports, Mod, ModDefinition, Reservation, SettingsFile, SettingsFileChange, SettingsFileChangeListener, Subscription, Timer as ModTimer } from '../mod.js'
|
|
5
5
|
import type { Options, OptionValues } from '../options.js'
|
|
6
|
-
import { consentPath, dataFolder, finishStepsMethod, grantFix, coreVersion, isAtLeast, marketplaceOf, parseConsent, pendingStepsMethod, parseEvent, permissionWords, readRecord, readSteps, recordPath, scriptsSha256, storeFolder, updatesToTurnOn, type ReadFile, type RunnerEvent, type Steps } from '../records.js'
|
|
6
|
+
import { consentPath, dataFolder, finishStepsMethod, grantFix, coreVersion, isAtLeast, isProgramNewer, marketplaceOf, parseConsent, pendingStepsMethod, parseEvent, permissionWords, programPath, readRecord, readSteps, recordPath, scriptsSha256, storeFolder, updatesToTurnOn, type ReadFile, type RunnerEvent, type Steps } from '../records.js'
|
|
7
7
|
import { homePrefix, ifMissing, relativePath } from '../utils/paths.js'
|
|
8
8
|
import { firstLine, formatExit, messageOf } from '../utils/text.js'
|
|
9
9
|
import type { Claude } from './claude.js'
|
|
@@ -15,8 +15,8 @@ import { answerCall, answeringOwnCalls, cmodPluginName, dependencyCalls, withBar
|
|
|
15
15
|
import { keepEngine } from './engine.js'
|
|
16
16
|
import { classicFailure, classicHook, userSkillHook, type Hook, type HookFields, type HookListener, type RoutedEvent } from './hooks.js'
|
|
17
17
|
import type { PermissionRoutes } from './permission-check.js'
|
|
18
|
-
import { checkedAnswer, checkingGrants, checkSettings, checksAnswers, checkWrite, isCovered, itemOf, passedDown, programAddresses, type AnswerCheck, type Grants } from './grants.js'
|
|
19
|
-
import { cmodError, locatingPrograms, readLines, type Programs } from './programs.js'
|
|
18
|
+
import { checkedAnswer, checkingGrants, checkRun, checkSettings, checksAnswers, checkWrite, isCovered, itemOf, passedDown, programAddresses, type AnswerCheck, type Grants } from './grants.js'
|
|
19
|
+
import { cmodError, locatingPrograms, readLines, spawnCmod, type Programs } from './programs.js'
|
|
20
20
|
import { createModFiles, resolvedPath, type Places } from './metadata.js'
|
|
21
21
|
import { fitsOption } from '../options.js'
|
|
22
22
|
import { createInstaller } from './installer.js'
|
|
@@ -31,9 +31,9 @@ import { createToaster, type Failure, type ShownToast, type Toaster } from './to
|
|
|
31
31
|
import { toolCalls } from './tool-calls.js'
|
|
32
32
|
import { createHotkeys } from './hotkeys.js'
|
|
33
33
|
import { servePages, type Pages } from './pages.js'
|
|
34
|
-
import { drawnParts } from './parts.js'
|
|
34
|
+
import { drawnParts, type DrawnParts } from './parts.js'
|
|
35
35
|
import { createWatches } from './watches.js'
|
|
36
|
-
import { createUi, partMethods, type PartDraw, type PartsUpdate, type SettingsTab, type UiArea } from './ui.js'
|
|
36
|
+
import { createUi, partMethods, type BorrowedMarkdown, type PartDraw, type PartPress, type PartsUpdate, type SettingsTab, type ShownPages, type UiArea } from './ui.js'
|
|
37
37
|
import { serveWidgets } from './widgets.js'
|
|
38
38
|
import { Progress } from '../ui/components.js'
|
|
39
39
|
import { Box, Text } from '../ui/elements.js'
|
|
@@ -73,6 +73,7 @@ type ModRuntime = {
|
|
|
73
73
|
readonly refreshGrants: () => Promise<void>
|
|
74
74
|
readonly permissionRoutes: () => PermissionRoutes | undefined
|
|
75
75
|
readonly options: PluginOptions
|
|
76
|
+
readonly nextDraw: () => number
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
type ActiveMod<State extends object, Declared extends Options> = {
|
|
@@ -97,6 +98,10 @@ const installRetrySeconds = [2, 4, 8, 16, 32, 64, 128] as const
|
|
|
97
98
|
|
|
98
99
|
const cmodCheckMs = 1000
|
|
99
100
|
|
|
101
|
+
const recordCheckMs = 1000
|
|
102
|
+
|
|
103
|
+
const drawsPerMs = 1000
|
|
104
|
+
|
|
100
105
|
const cmodWaitMs = 60_000
|
|
101
106
|
|
|
102
107
|
const cmodSlowCheckMs = 10_000
|
|
@@ -135,7 +140,8 @@ export async function readPlugin(claude: Claude): Promise<Plugin> {
|
|
|
135
140
|
})
|
|
136
141
|
if (version === undefined || record?.scriptsSha256 !== scripts) return { ...plugin, isInstalled: false, shouldRecord: false }
|
|
137
142
|
const isInstalled = record.version === version || record.root === root
|
|
138
|
-
|
|
143
|
+
const programModifiedMs = record.program === null ? undefined : (await claude.fs.stat(programPath(store, record.program, record.version)).catch(ifMissing(undefined)))?.mtimeMs
|
|
144
|
+
return { ...plugin, isInstalled, shouldRecord: isInstalled && (record.version !== version || isProgramNewer(record, programModifiedMs)) }
|
|
139
145
|
}
|
|
140
146
|
|
|
141
147
|
async function filesBelow(claude: Claude, folder: string): Promise<string[]> {
|
|
@@ -193,13 +199,20 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
193
199
|
let raise: SessionsClient['raise'] | undefined
|
|
194
200
|
let failure: unknown
|
|
195
201
|
let pluginOptions = options
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
202
|
+
let drawCount = 0
|
|
203
|
+
const nextDraw = () => (drawCount += 1)
|
|
204
|
+
const installer = createInstaller(
|
|
205
|
+
definition.name,
|
|
206
|
+
() => claude(),
|
|
207
|
+
{
|
|
208
|
+
show: (open) => {
|
|
209
|
+
if (phase !== 'active') runtime?.toaster.toast.dismiss(installId)
|
|
210
|
+
runtime?.toaster.toast.info('Installing', { id: askingId, description: 'Waiting for your answer', duration: Infinity, action: { label: 'Answer', onClick: open } })
|
|
211
|
+
},
|
|
212
|
+
hide: () => runtime?.toaster.toast.dismiss(askingId),
|
|
200
213
|
},
|
|
201
|
-
|
|
202
|
-
|
|
214
|
+
nextDraw,
|
|
215
|
+
)
|
|
203
216
|
let shouldRecord = false
|
|
204
217
|
let recording: Promise<void> | undefined
|
|
205
218
|
let missedSessionStart: Frozen<Args<'classic.SessionStart'>> | undefined
|
|
@@ -221,6 +234,7 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
221
234
|
failure ??= new Error(`${definition.name}: ${reason}${/[.!?]$/.test(reason) ? '' : '.'}${fix === undefined ? '' : ` ${fix}`}`, { cause: error })
|
|
222
235
|
const again = retry === undefined ? {} : { retry: () => ((failure = undefined), stopRetrying(), retry().catch(report)) }
|
|
223
236
|
toastFailure({ id: installId, message: reason, ...(fix === undefined ? {} : { description: fix }), ...(error === undefined ? {} : { error }), ...again })
|
|
237
|
+
watchRecord()
|
|
224
238
|
}
|
|
225
239
|
|
|
226
240
|
let consented: string | undefined
|
|
@@ -332,27 +346,31 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
332
346
|
|
|
333
347
|
const runSteps = async (active: Mod<State>) => {
|
|
334
348
|
const steps = asksPerson ? (definition.installer ?? []) : []
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
349
|
+
try {
|
|
350
|
+
for (const [index, step] of steps.entries()) {
|
|
351
|
+
if (await isStepDone(active, step)) continue
|
|
352
|
+
const isAnswered = (await claude().session.surfaces()).length > 0 && (await installer.step(active, step, index + 1, steps.length))
|
|
353
|
+
if (!isAnswered) {
|
|
354
|
+
runtime?.toaster.toast.warning(steps.length - index === 1 ? 'Needs one more step' : `Needs ${steps.length - index} more steps`, {
|
|
355
|
+
id: 'cmod:steps',
|
|
356
|
+
description: `Starting with ${step.title}.`,
|
|
357
|
+
duration: Infinity,
|
|
358
|
+
action: { label: 'Finish', onClick: () => runSteps(active) },
|
|
359
|
+
})
|
|
360
|
+
break
|
|
361
|
+
}
|
|
346
362
|
}
|
|
363
|
+
} finally {
|
|
364
|
+
await installer.close()
|
|
347
365
|
}
|
|
348
|
-
await installer.close()
|
|
349
366
|
}
|
|
350
367
|
|
|
351
368
|
const activate = async (): Promise<void> => {
|
|
352
369
|
if (runtime === undefined || plugin === undefined) return
|
|
353
370
|
const activeRouter = createRouter()
|
|
354
|
-
const active = await createMod(definition, { ...runtime, router: activeRouter, dataFolder: dataFolder(plugin.store, plugin.name), store: plugin.store, steps: plugin.steps, granted, refreshGrants, permissionRoutes, options: pluginOptions }).catch(async (error: unknown) => {
|
|
371
|
+
const active = await createMod(definition, { ...runtime, router: activeRouter, dataFolder: dataFolder(plugin.store, plugin.name), store: plugin.store, steps: plugin.steps, granted, refreshGrants, permissionRoutes, options: pluginOptions, nextDraw }).catch(async (error: unknown) => {
|
|
355
372
|
if (error instanceof MissingOptions && (await askOptions(error))) return undefined
|
|
373
|
+
await installer.close()
|
|
356
374
|
fail(messageOf(error), error instanceof MissingOptions ? `Retry asks for ${error.keys.length === 1 ? 'it' : 'them'}.` : undefined, begin, error)
|
|
357
375
|
throw error
|
|
358
376
|
})
|
|
@@ -363,6 +381,7 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
363
381
|
raise = active.raise
|
|
364
382
|
router = activeRouter
|
|
365
383
|
phase = 'active'
|
|
384
|
+
stopWatchingRecord()
|
|
366
385
|
runtime.claude.ui.invalidate('ui.render')
|
|
367
386
|
void runSteps(active.mod).catch((error: unknown) => toastFailure({ id: 'cmod:steps', message: 'Its installer stopped', error }))
|
|
368
387
|
const missed = missedSessionStart
|
|
@@ -418,9 +437,14 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
418
437
|
toastFailure({ id: 'cmod:updates', message: 'Could not read whether its marketplace updates automatically', error })
|
|
419
438
|
return undefined
|
|
420
439
|
})
|
|
421
|
-
const
|
|
440
|
+
const installedElsewhere = new Promise<'installed'>((resolve) => (settleAsked = () => resolve('installed')))
|
|
441
|
+
watchRecord()
|
|
442
|
+
const answer = await Promise.race([installer.consent({ ...event, updates }), installedElsewhere]).finally(() => {
|
|
443
|
+
settleAsked = undefined
|
|
444
|
+
})
|
|
422
445
|
await installer.close()
|
|
423
|
-
if (
|
|
446
|
+
if (answer === 'installed') return startInstalled()
|
|
447
|
+
if (answer) {
|
|
424
448
|
consented = event.sha256
|
|
425
449
|
return install(consented)
|
|
426
450
|
}
|
|
@@ -544,20 +568,45 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
544
568
|
}
|
|
545
569
|
let recordMs = 0
|
|
546
570
|
let readStarted: ((claude: Claude) => Promise<Plugin>) | undefined
|
|
547
|
-
const
|
|
548
|
-
if (plugin === undefined || readStarted === undefined
|
|
571
|
+
const isNewlyInstalled = async (): Promise<boolean> => {
|
|
572
|
+
if (plugin === undefined || readStarted === undefined) return false
|
|
549
573
|
const modifiedMs = await stampOf(recordPath(plugin.store, plugin.name))
|
|
550
574
|
if (modifiedMs === recordMs) return false
|
|
551
575
|
recordMs = modifiedMs
|
|
552
576
|
const current = await readStarted(claude())
|
|
553
577
|
if (!current.isInstalled) return false
|
|
554
578
|
plugin = current
|
|
579
|
+
return true
|
|
580
|
+
}
|
|
581
|
+
const startInstalled = async () => {
|
|
555
582
|
failure = undefined
|
|
556
583
|
activation = undefined
|
|
557
584
|
await refreshGrants()
|
|
558
585
|
await finish()
|
|
586
|
+
}
|
|
587
|
+
let settleAsked: (() => void) | undefined
|
|
588
|
+
const recover = async (): Promise<boolean> => {
|
|
589
|
+
if ((settleAsked === undefined && phase !== 'failed' && phase !== 'declined') || !(await isNewlyInstalled())) return false
|
|
590
|
+
if (settleAsked === undefined) await startInstalled()
|
|
591
|
+
else settleAsked()
|
|
559
592
|
return true
|
|
560
593
|
}
|
|
594
|
+
let recordCheck: Timer | undefined
|
|
595
|
+
let checking: Promise<void> | undefined
|
|
596
|
+
const watchRecord = () => {
|
|
597
|
+
recordCheck ??= claude().clock.every(recordCheckMs, () => {
|
|
598
|
+
checking ??= recover()
|
|
599
|
+
.then(() => undefined)
|
|
600
|
+
.catch((error: unknown) => claude().ui.log(`${definition.name} could not read whether another cmod installed it: ${messageOf(error)}`, { to: 'debug' }))
|
|
601
|
+
.finally(() => {
|
|
602
|
+
checking = undefined
|
|
603
|
+
})
|
|
604
|
+
})
|
|
605
|
+
}
|
|
606
|
+
const stopWatchingRecord = () => {
|
|
607
|
+
recordCheck?.cancel()
|
|
608
|
+
recordCheck = undefined
|
|
609
|
+
}
|
|
561
610
|
|
|
562
611
|
const refreshGrants = async () => {
|
|
563
612
|
if (plugin === undefined || plugin.name === cmodPluginName) return
|
|
@@ -639,6 +688,7 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
639
688
|
runtime = { claude: programs.claude, toaster: createToaster(claudeCalls, definition.name, () => plugin?.version, () => openSettings('permissions')) }
|
|
640
689
|
readStarted = read
|
|
641
690
|
try {
|
|
691
|
+
drawCount = (await claudeCalls.clock.now()) * drawsPerMs
|
|
642
692
|
await programs.refresh()
|
|
643
693
|
await begin().catch(report)
|
|
644
694
|
} finally {
|
|
@@ -647,8 +697,13 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
647
697
|
},
|
|
648
698
|
async route<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>): Promise<EventResult<N>> {
|
|
649
699
|
if (imports !== undefined) bind(imports)
|
|
650
|
-
|
|
651
|
-
if (event === '
|
|
700
|
+
const asked = e as Frozen<Args<'cmod.call'>> & { readonly caller?: string }
|
|
701
|
+
if (event === 'cmod.call' && asked.to === definition.name && asked.caller === cmodPluginName) {
|
|
702
|
+
if (asked.method === partMethods.shown) installer.hear(asked.input as ShownPages)
|
|
703
|
+
const answered = installer.answer(asked.method, asked.input)
|
|
704
|
+
if (answered !== undefined) return (await answered.catch((error: unknown) => ({ deny: `${definition.name}: ${messageOf(error)}` }))) as EventResult<N>
|
|
705
|
+
if (asked.method === partMethods.shown && phase !== 'active') return { value: null } as EventResult<N>
|
|
706
|
+
}
|
|
652
707
|
if (event === 'classic.SessionStart') {
|
|
653
708
|
missedSessionStart = undefined
|
|
654
709
|
hasHeardSessionStart = true
|
|
@@ -659,9 +714,10 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
659
714
|
}
|
|
660
715
|
if (shouldRecord && event === 'classic.UserPromptSubmit') record()
|
|
661
716
|
if (event === 'classic.UserPromptSubmit' || event === 'classic.SessionStart') await recover().catch((error: unknown) => claude().ui.log(`${definition.name} stays stopped: ${messageOf(error)}`, { to: 'debug' }))
|
|
662
|
-
const
|
|
717
|
+
const tool = (e as { readonly tool?: unknown }).tool
|
|
718
|
+
const isCallToThisMod = (event === 'cmod.call' && (e as Frozen<Args<'cmod.call'>>).to === definition.name) || (event === 'tool.call' && typeof tool === 'string' && tool.startsWith(`mcp__${definition.name}__`))
|
|
663
719
|
if (event === 'cmod.call' && !isCallToThisMod) return next(e)
|
|
664
|
-
await (event === 'classic.SessionStart' || isCallToThisMod ? holdUntilStarted() : whenActive())
|
|
720
|
+
await (event === 'classic.SessionStart' || event === 'session.append' || isCallToThisMod ? holdUntilStarted() : whenActive())
|
|
665
721
|
if (event === 'classic.SessionStart' && phase !== 'active') {
|
|
666
722
|
missedSessionStart = e as Frozen<Args<'classic.SessionStart'>>
|
|
667
723
|
return next(e)
|
|
@@ -673,7 +729,11 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
673
729
|
const call = e as Frozen<Args<'cmod.call'>>
|
|
674
730
|
if (event === 'cmod.call' && call.to === definition.name && mod !== undefined) {
|
|
675
731
|
if (call.method === pendingStepsMethod) return { value: await pendingSteps(mod) } as EventResult<N>
|
|
676
|
-
if (call.method === finishStepsMethod)
|
|
732
|
+
if (call.method === finishStepsMethod) {
|
|
733
|
+
const active = mod
|
|
734
|
+
const opened = await installer.collect(() => Promise.race([runSteps(active).catch((error: unknown) => void toastFailure({ id: 'cmod:steps', message: 'Could not finish the setup', error })), installer.shown()]))
|
|
735
|
+
return { value: opened } as EventResult<N>
|
|
736
|
+
}
|
|
677
737
|
}
|
|
678
738
|
if (event === 'classic.UserPromptSubmit' || event === 'classic.SessionStart') await refreshGrants().catch((error: unknown) => claude().ui.log(`${definition.name} keeps its grants from before: ${messageOf(error)}`, { to: 'debug' }))
|
|
679
739
|
if (event === 'classic.UserPromptSubmit' && phase === 'active' && !hasHeardSessionStart) {
|
|
@@ -737,7 +797,13 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
737
797
|
let parentAsked: Promise<string | undefined> | undefined
|
|
738
798
|
let hasEarlierPrompts = true
|
|
739
799
|
let agentType: string | undefined
|
|
740
|
-
|
|
800
|
+
let parts: DrawnParts | undefined
|
|
801
|
+
const borrowed: BorrowedMarkdown = (styler, cached, input) => {
|
|
802
|
+
const name = `The markdown style of ${styler}`
|
|
803
|
+
void parts?.fill(styler, cached, input, name)?.then(() => claude.ui.invalidate('ui.render'))
|
|
804
|
+
return parts?.element(styler, cached, `${styler}/markdown:`, name, `markdown:${styler}`)
|
|
805
|
+
}
|
|
806
|
+
const area = createUi<State>({ name: definition.name, claude, router, report, announce, mod: () => mod, borrowed, nextDraw: runtime.nextDraw })
|
|
741
807
|
const askParent = async (): Promise<string | undefined> => {
|
|
742
808
|
if (definition.name === cmodPluginName) return undefined
|
|
743
809
|
try {
|
|
@@ -1003,9 +1069,15 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
1003
1069
|
const conversations = definition.name === cmodPluginName ? serveConversations({ claude, router, store: runtime.store, project: () => modState.root, report: (id, message, error) => report({ id, message, error }), clear: (id) => toaster.toast.dismiss(id) }) : undefined
|
|
1004
1070
|
let pages: Pages | undefined
|
|
1005
1071
|
const reportPart = (id: string, message: string, error: unknown) => report({ id, message, error })
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1008
|
-
|
|
1072
|
+
const pressElsewhere = (to: string, input: PartPress) =>
|
|
1073
|
+
void claude.cmod.call({ to, method: partMethods.press, input }).catch((error: unknown) => {
|
|
1074
|
+
parts?.forget(to)
|
|
1075
|
+
claude.ui.invalidate('ui.render')
|
|
1076
|
+
reportPart(`press:${to}`, `${to} did not take the press`, error)
|
|
1077
|
+
})
|
|
1078
|
+
parts = drawnParts({ claude, report: reportPart, press: (to, input) => (pages === undefined ? pressElsewhere(to, input) : pages.press(to, input)) })
|
|
1079
|
+
const widgets = conversations === undefined ? undefined : serveWidgets({ claude, router, store: runtime.store, report: reportPart, turnClosedBy: (rowId) => conversations.turnClosedBy(rowId), parts, markdownIn: area.markdownIn })
|
|
1080
|
+
pages = widgets === undefined ? undefined : servePages({ claude, router, report: reportPart, widgets, parts, tellStylers: area.restyled })
|
|
1009
1081
|
router.add('cmod.call', async (e, next) => {
|
|
1010
1082
|
if (e.to !== definition.name) return next(e)
|
|
1011
1083
|
const call = e as typeof e & { readonly caller?: string }
|
|
@@ -1018,12 +1090,12 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
1018
1090
|
const ask = beforeAskOf(call)
|
|
1019
1091
|
if (ask !== undefined) return { value: (await client.answerBefore(ask.session, ask.event, ask.change, ask.isInProject)) ?? null }
|
|
1020
1092
|
if (e.method === partMethods.changed && call.caller !== undefined && pages !== undefined) {
|
|
1021
|
-
return { value: { isHeld: pages.update(call.caller, e.input as PartsUpdate) } }
|
|
1093
|
+
return { value: { isHeld: pages.update(call.caller, e.input as PartsUpdate), stylers: pages.stylers() } }
|
|
1022
1094
|
}
|
|
1023
|
-
if (call.caller
|
|
1095
|
+
if (call.caller !== undefined) {
|
|
1024
1096
|
const turn = e.method === partMethods.draw ? (e.input as { readonly turn?: TurnData }).turn : undefined
|
|
1025
|
-
const answered = area.answer(e.method, turn === undefined ? e.input : { ...(e.input as PartDraw), turn: client.turn(turn) })
|
|
1026
|
-
if (answered !== undefined) return answered
|
|
1097
|
+
const answered = area.answer(e.method, turn === undefined ? e.input : { ...(e.input as PartDraw), turn: client.turn(turn) }, call.caller)
|
|
1098
|
+
if (answered !== undefined) return answered.catch((error: unknown) => ({ deny: `${definition.name}: ${messageOf(error)}` }))
|
|
1027
1099
|
}
|
|
1028
1100
|
const served = await conversations?.answer(call)
|
|
1029
1101
|
return served ?? answerCall(definition.name, api, e)
|
|
@@ -1033,7 +1105,20 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
1033
1105
|
return ask === undefined ? { deny: `${definition.name}: ${error.message}` } : { value: client.failedBefore(ask.session, ask.event, ask.change, ask.isInProject, error) ?? null }
|
|
1034
1106
|
})
|
|
1035
1107
|
const agents = toolCalls(claude, router)
|
|
1036
|
-
keepEngine(mod, {
|
|
1108
|
+
keepEngine(mod, {
|
|
1109
|
+
claude: checked,
|
|
1110
|
+
router,
|
|
1111
|
+
toolCalls: agents,
|
|
1112
|
+
report,
|
|
1113
|
+
hasPermissionCheck: () => runtime.permissionRoutes() !== undefined,
|
|
1114
|
+
answerInput: area.answerInput,
|
|
1115
|
+
spawnCmod: async (args, program) => {
|
|
1116
|
+
await checkRun(grants, 'process.spawn', program.argv)
|
|
1117
|
+
return spawnCmod(claude, args, session, program.env)
|
|
1118
|
+
},
|
|
1119
|
+
...(widgets === undefined ? {} : { widgets }),
|
|
1120
|
+
...(pages === undefined ? {} : { pages }),
|
|
1121
|
+
})
|
|
1037
1122
|
const names = new Set<string>()
|
|
1038
1123
|
const reserveName = (type: string, name: string, taken: string): Reservation => {
|
|
1039
1124
|
const key = `${type}:${name}`
|
package/src/runtime/pages.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { RenderElement } from 'claude-code'
|
|
2
2
|
import { isObject } from '../records.js'
|
|
3
|
+
import { Text } from '../ui/elements.js'
|
|
3
4
|
import { messageOf } from '../utils/text.js'
|
|
4
5
|
import type { Claude } from './claude.js'
|
|
6
|
+
import { cmodPluginName } from './dependencies.js'
|
|
5
7
|
import type { DrawnParts } from './parts.js'
|
|
6
8
|
import type { Router } from './router.js'
|
|
7
9
|
import { partMethods, type PageDeclaration, type PageRequest, type PartPress, type PartsUpdate, type ShownPages } from './ui.js'
|
|
@@ -15,6 +17,7 @@ export type PageApply = (mod: string, requests: readonly PageRequest[]) => Promi
|
|
|
15
17
|
|
|
16
18
|
export type Pages = {
|
|
17
19
|
declared(): readonly ModPages[]
|
|
20
|
+
stylers(): readonly string[]
|
|
18
21
|
element(mod: string, page: string, params: unknown, columns: number, isFocused: boolean): RenderElement | undefined
|
|
19
22
|
press(mod: string, press: PartPress): void
|
|
20
23
|
update(mod: string, update: PartsUpdate): boolean
|
|
@@ -27,8 +30,23 @@ export function pagesOf(input: unknown): PageDeclaration[] {
|
|
|
27
30
|
return listed.filter(isObject).flatMap((each) => (typeof each['id'] === 'string' && typeof each['title'] === 'string' && (each['place'] === 'sidebar' || each['place'] === 'settings') ? [{ id: each['id'], title: each['title'], place: each['place'] }] : []))
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
export function servePages({
|
|
33
|
+
export function servePages({
|
|
34
|
+
claude,
|
|
35
|
+
router,
|
|
36
|
+
report,
|
|
37
|
+
widgets,
|
|
38
|
+
parts,
|
|
39
|
+
tellStylers,
|
|
40
|
+
}: {
|
|
41
|
+
readonly claude: Claude
|
|
42
|
+
readonly router: Router
|
|
43
|
+
readonly report: (id: string, message: string, error: unknown) => void
|
|
44
|
+
readonly widgets: Pick<Widgets, 'update'>
|
|
45
|
+
readonly parts: DrawnParts
|
|
46
|
+
readonly tellStylers: (stylers: readonly string[]) => void
|
|
47
|
+
}): Pages {
|
|
31
48
|
const declared = new Map<string, readonly PageDeclaration[]>()
|
|
49
|
+
const stylers = new Set<string>()
|
|
32
50
|
const asking = new Map<string, Promise<void>>()
|
|
33
51
|
const pressing = new Set<Promise<void>>()
|
|
34
52
|
let waiting: { readonly mod: string; readonly requests: readonly PageRequest[] }[] = []
|
|
@@ -44,16 +62,18 @@ export function servePages({ claude, router, report, widgets, parts }: { readonl
|
|
|
44
62
|
for (const { mod, requests } of taken) await run(mod, requests)
|
|
45
63
|
}
|
|
46
64
|
|
|
47
|
-
const settle = async () => {
|
|
48
|
-
|
|
65
|
+
const settle = async (earlier: ReadonlySet<Promise<void>>) => {
|
|
66
|
+
const own = () => [...pressing].filter((pending) => !earlier.has(pending))
|
|
67
|
+
for (let pending = own(); pending.length > 0; pending = own()) await Promise.allSettled(pending)
|
|
49
68
|
await takeWaiting()
|
|
50
69
|
}
|
|
51
70
|
|
|
52
71
|
const wrapped = async <Answer>(next: () => Promise<Answer>): Promise<Answer> => {
|
|
53
72
|
inputDepth += 1
|
|
73
|
+
const earlier = new Set(pressing)
|
|
54
74
|
try {
|
|
55
75
|
const answer = await next()
|
|
56
|
-
await settle()
|
|
76
|
+
await settle(earlier)
|
|
57
77
|
return answer
|
|
58
78
|
} finally {
|
|
59
79
|
inputDepth -= 1
|
|
@@ -77,9 +97,22 @@ export function servePages({ claude, router, report, widgets, parts }: { readonl
|
|
|
77
97
|
return pending
|
|
78
98
|
}
|
|
79
99
|
|
|
100
|
+
const restyle = (mod: string, isStyler: boolean) => {
|
|
101
|
+
if (!isStyler && !stylers.delete(mod)) return
|
|
102
|
+
if (isStyler) stylers.add(mod)
|
|
103
|
+
parts.forget(mod)
|
|
104
|
+
const listed = [...stylers]
|
|
105
|
+
tellStylers(listed)
|
|
106
|
+
for (const told of declared.keys()) {
|
|
107
|
+
if (told !== cmodPluginName) void claude.cmod.call({ to: told, method: partMethods.stylers, input: listed }).catch((error: unknown) => claude.ui.log(`cmod: ${told} did not hear which mods style markdown: ${messageOf(error)}`, { to: 'debug' }))
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
80
111
|
const update = (mod: string, { parts: listing, redraw, requests, isAsked }: PartsUpdate) => {
|
|
81
112
|
if (listing !== undefined) declared.set(mod, pagesOf(listing))
|
|
82
|
-
if (listing !== undefined
|
|
113
|
+
if (listing !== undefined) restyle(mod, isObject(listing) && listing['stylesMarkdown'] === true)
|
|
114
|
+
const isRedrawn = redraw === true || requests?.some((request) => 'open' in request) === true
|
|
115
|
+
if (listing !== undefined || isRedrawn) widgets.update(mod, { ...(listing === undefined ? {} : { parts: listing }), ...(isRedrawn ? { redraw: true } : {}) })
|
|
83
116
|
if (requests === undefined || requests.length === 0) return false
|
|
84
117
|
if (isAsked !== true) return (void run(mod, requests), false)
|
|
85
118
|
waiting.push({ mod, requests })
|
|
@@ -106,19 +139,23 @@ export function servePages({ claude, router, report, widgets, parts }: { readonl
|
|
|
106
139
|
|
|
107
140
|
const cacheKey = (page: string, params: unknown, columns: number, isFocused: boolean) => `${page}\u0000${JSON.stringify(params ?? null)}\u0000${columns}\u0000${isFocused}`
|
|
108
141
|
|
|
142
|
+
const plain = <Data,>(data: Data): Data => JSON.parse(JSON.stringify(data)) as Data
|
|
143
|
+
|
|
109
144
|
return {
|
|
110
145
|
declared: () => [...declared].map(([mod, pages]) => ({ mod, pages })),
|
|
146
|
+
stylers: () => [...stylers],
|
|
111
147
|
element(mod, page, params, columns, isFocused) {
|
|
112
148
|
if (!declared.has(mod)) void ask(mod)
|
|
113
149
|
const cached = cacheKey(page, params, columns, isFocused)
|
|
114
150
|
const name = `The ${page} page of ${mod}`
|
|
115
|
-
void parts.fill(mod, cached, { page, params: params ?? null, columns, isFocused }, name)?.then(() => claude.ui.invalidate('ui.render'))
|
|
116
|
-
|
|
151
|
+
void parts.fill(mod, cached, { page, params: plain(params ?? null), columns, isFocused }, name)?.then(() => claude.ui.invalidate('ui.render'))
|
|
152
|
+
const failure = parts.failure(mod, cached)
|
|
153
|
+
return parts.element(mod, cached, `${mod}/${page}:`, name, `page:${mod}/${page}`) ?? (failure === undefined ? undefined : Text({ color: 'error', children: `${name} could not draw: ${failure}` }))
|
|
117
154
|
},
|
|
118
155
|
press,
|
|
119
156
|
update,
|
|
120
157
|
tell(mod, shown) {
|
|
121
|
-
void claude.cmod.call({ to: mod, method: partMethods.shown, input: shown }).catch((error: unknown) => claude.ui.log(`cmod: ${mod} did not hear which of its pages show: ${messageOf(error)}`, { to: 'debug' }))
|
|
158
|
+
void claude.cmod.call({ to: mod, method: partMethods.shown, input: plain(shown) }).catch((error: unknown) => claude.ui.log(`cmod: ${mod} did not hear which of its pages show: ${messageOf(error)}`, { to: 'debug' }))
|
|
122
159
|
},
|
|
123
160
|
apply(listener) {
|
|
124
161
|
applyTo = listener
|
package/src/runtime/parts.ts
CHANGED
|
@@ -7,10 +7,11 @@ import { partMethods, type DrawnPart, type PartPress } from './ui.js'
|
|
|
7
7
|
export type DrawnParts = {
|
|
8
8
|
fill(mod: string, cached: string, input: Readonly<Record<string, unknown>>, name: string): Promise<void> | undefined
|
|
9
9
|
element(mod: string, cached: string, keyPrefix: string, name: string, reportId: string): RenderElement | undefined
|
|
10
|
+
failure(mod: string, cached: string): string | undefined
|
|
10
11
|
forget(mod: string): void
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
type Drawn = { readonly data: DrawnPart | null; readonly generation: number }
|
|
14
|
+
type Drawn = { readonly data: DrawnPart | null; readonly failure?: string; readonly generation: number }
|
|
14
15
|
|
|
15
16
|
const keptDrawings = 200
|
|
16
17
|
|
|
@@ -26,9 +27,9 @@ export function drawnParts({ claude, report, press }: { readonly claude: Claude;
|
|
|
26
27
|
const key = keyOf(mod, cached)
|
|
27
28
|
const generation = generationOf(mod)
|
|
28
29
|
if (drawn.get(key)?.generation === generation) return undefined
|
|
29
|
-
const keep = (data: DrawnPart | null) => {
|
|
30
|
+
const keep = (data: DrawnPart | null, failure?: string) => {
|
|
30
31
|
drawn.delete(key)
|
|
31
|
-
drawn.set(key, { data, generation })
|
|
32
|
+
drawn.set(key, { data, ...(failure === undefined ? {} : { failure }), generation })
|
|
32
33
|
const [oldest] = drawn.keys()
|
|
33
34
|
if (drawn.size > keptDrawings && oldest !== undefined) drawn.delete(oldest)
|
|
34
35
|
if (generationOf(mod) !== generation) claude.ui.invalidate('ui.render')
|
|
@@ -41,7 +42,7 @@ export function drawnParts({ claude, report, press }: { readonly claude: Claude;
|
|
|
41
42
|
(data) => keep((data ?? null) as DrawnPart | null),
|
|
42
43
|
(error: unknown) => {
|
|
43
44
|
claude.ui.log(`cmod: ${name} drew nothing: ${messageOf(error)}`, { to: 'debug' })
|
|
44
|
-
keep(null)
|
|
45
|
+
keep(drawn.get(key)?.data ?? null, messageOf(error))
|
|
45
46
|
},
|
|
46
47
|
)
|
|
47
48
|
.finally(() => drawing.delete(key))
|
|
@@ -63,6 +64,9 @@ export function drawnParts({ claude, report, press }: { readonly claude: Claude;
|
|
|
63
64
|
return undefined
|
|
64
65
|
}
|
|
65
66
|
},
|
|
67
|
+
failure(mod, cached) {
|
|
68
|
+
return drawn.get(keyOf(mod, cached))?.failure
|
|
69
|
+
},
|
|
66
70
|
forget(mod) {
|
|
67
71
|
generations.set(mod, generationOf(mod) + 1)
|
|
68
72
|
},
|
package/src/runtime/programs.ts
CHANGED
|
@@ -35,9 +35,13 @@ export async function readLines(stream: HookStream<ProcessSpawnChunk, ProcessSpa
|
|
|
35
35
|
return { code: piece.value.code, lastError }
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export function spawnCmod(claude: Claude, args: readonly string[], session: string, env: Readonly<Record<string, string>> = {}): HookStream<ProcessSpawnChunk, ProcessSpawnResult> {
|
|
39
|
+
return claude.process.spawn({ argv: ['cmod', ...args], env: { ...env, CLAUDE_CODE_SESSION_ID: session } })
|
|
40
|
+
}
|
|
41
|
+
|
|
38
42
|
export async function runCmod(claude: Claude, args: readonly string[], session: string): Promise<string> {
|
|
39
43
|
const lines: string[] = []
|
|
40
|
-
const { code, lastError } = await readLines(claude
|
|
44
|
+
const { code, lastError } = await readLines(spawnCmod(claude, args, session), (line) => void lines.push(line))
|
|
41
45
|
if (code !== 0) throw new Error(cmodError(lastError, args[0]) || `cmod ${args.slice(0, 2).join(' ')} ${formatExit(code, '')}.`)
|
|
42
46
|
return lines.join('\n')
|
|
43
47
|
}
|