@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/runtime/lifecycle.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { Args, ClassicHookInputs, EventResult, Frozen, HookBudget,
|
|
1
|
+
import type { Args, Caught, ClassicHookInputs, EventResult, Frozen, HookBudget, PluginOptions, RenderElement, Timer } from 'claude-code'
|
|
2
2
|
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,14 +15,14 @@ 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'
|
|
23
23
|
import { createOptions, MissingOptions } from './options.js'
|
|
24
24
|
import { createRouter, type Router, type RouterNext } from './router.js'
|
|
25
|
-
import { conversationMethods, createSessionsClient, type SessionsClient } from './sessions.js'
|
|
25
|
+
import { conversationMethods, createSessionsClient, type SessionColor, type SessionsClient } from './sessions.js'
|
|
26
26
|
import type { SessionChange, TurnData } from '../session/conversation.js'
|
|
27
27
|
import type { BeforeEvent } from '../session/session.js'
|
|
28
28
|
import { matches } from '../utils/matcher.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'
|
|
@@ -58,7 +58,7 @@ export type Lifecycle<State extends object> = {
|
|
|
58
58
|
readonly toasts: readonly ShownToast[]
|
|
59
59
|
start(claude: Claude, read: (claude: Claude) => Promise<Plugin>): Promise<void>
|
|
60
60
|
route<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>): Promise<EventResult<N>>
|
|
61
|
-
caught<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>, failure:
|
|
61
|
+
caught<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>, failure: Caught): Promise<EventResult<N>>
|
|
62
62
|
raise(method: string, payload: Readonly<Record<string, unknown>>): Promise<unknown>
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -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,7 +381,9 @@ 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')
|
|
386
|
+
if (definition.name === cmodPluginName) runtime.claude.ui.invalidate('command.describe')
|
|
367
387
|
void runSteps(active.mod).catch((error: unknown) => toastFailure({ id: 'cmod:steps', message: 'Its installer stopped', error }))
|
|
368
388
|
const missed = missedSessionStart
|
|
369
389
|
missedSessionStart = undefined
|
|
@@ -418,9 +438,14 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
418
438
|
toastFailure({ id: 'cmod:updates', message: 'Could not read whether its marketplace updates automatically', error })
|
|
419
439
|
return undefined
|
|
420
440
|
})
|
|
421
|
-
const
|
|
441
|
+
const installedElsewhere = new Promise<'installed'>((resolve) => (settleAsked = () => resolve('installed')))
|
|
442
|
+
watchRecord()
|
|
443
|
+
const answer = await Promise.race([installer.consent({ ...event, program: plugin?.steps.program ?? '', updates }), installedElsewhere]).finally(() => {
|
|
444
|
+
settleAsked = undefined
|
|
445
|
+
})
|
|
422
446
|
await installer.close()
|
|
423
|
-
if (
|
|
447
|
+
if (answer === 'installed') return startInstalled()
|
|
448
|
+
if (answer) {
|
|
424
449
|
consented = event.sha256
|
|
425
450
|
return install(consented)
|
|
426
451
|
}
|
|
@@ -544,20 +569,45 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
544
569
|
}
|
|
545
570
|
let recordMs = 0
|
|
546
571
|
let readStarted: ((claude: Claude) => Promise<Plugin>) | undefined
|
|
547
|
-
const
|
|
548
|
-
if (plugin === undefined || readStarted === undefined
|
|
572
|
+
const isNewlyInstalled = async (): Promise<boolean> => {
|
|
573
|
+
if (plugin === undefined || readStarted === undefined) return false
|
|
549
574
|
const modifiedMs = await stampOf(recordPath(plugin.store, plugin.name))
|
|
550
575
|
if (modifiedMs === recordMs) return false
|
|
551
576
|
recordMs = modifiedMs
|
|
552
577
|
const current = await readStarted(claude())
|
|
553
578
|
if (!current.isInstalled) return false
|
|
554
579
|
plugin = current
|
|
580
|
+
return true
|
|
581
|
+
}
|
|
582
|
+
const startInstalled = async () => {
|
|
555
583
|
failure = undefined
|
|
556
584
|
activation = undefined
|
|
557
585
|
await refreshGrants()
|
|
558
586
|
await finish()
|
|
587
|
+
}
|
|
588
|
+
let settleAsked: (() => void) | undefined
|
|
589
|
+
const recover = async (): Promise<boolean> => {
|
|
590
|
+
if ((settleAsked === undefined && phase !== 'failed' && phase !== 'declined') || !(await isNewlyInstalled())) return false
|
|
591
|
+
if (settleAsked === undefined) await startInstalled()
|
|
592
|
+
else settleAsked()
|
|
559
593
|
return true
|
|
560
594
|
}
|
|
595
|
+
let recordCheck: Timer | undefined
|
|
596
|
+
let checking: Promise<void> | undefined
|
|
597
|
+
const watchRecord = () => {
|
|
598
|
+
recordCheck ??= claude().clock.every(recordCheckMs, () => {
|
|
599
|
+
checking ??= recover()
|
|
600
|
+
.then(() => undefined)
|
|
601
|
+
.catch((error: unknown) => claude().ui.log(`${definition.name} could not read whether another cmod installed it: ${messageOf(error)}`, { to: 'debug' }))
|
|
602
|
+
.finally(() => {
|
|
603
|
+
checking = undefined
|
|
604
|
+
})
|
|
605
|
+
})
|
|
606
|
+
}
|
|
607
|
+
const stopWatchingRecord = () => {
|
|
608
|
+
recordCheck?.cancel()
|
|
609
|
+
recordCheck = undefined
|
|
610
|
+
}
|
|
561
611
|
|
|
562
612
|
const refreshGrants = async () => {
|
|
563
613
|
if (plugin === undefined || plugin.name === cmodPluginName) return
|
|
@@ -639,6 +689,7 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
639
689
|
runtime = { claude: programs.claude, toaster: createToaster(claudeCalls, definition.name, () => plugin?.version, () => openSettings('permissions')) }
|
|
640
690
|
readStarted = read
|
|
641
691
|
try {
|
|
692
|
+
drawCount = (await claudeCalls.clock.now()) * drawsPerMs
|
|
642
693
|
await programs.refresh()
|
|
643
694
|
await begin().catch(report)
|
|
644
695
|
} finally {
|
|
@@ -647,8 +698,13 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
647
698
|
},
|
|
648
699
|
async route<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>): Promise<EventResult<N>> {
|
|
649
700
|
if (imports !== undefined) bind(imports)
|
|
650
|
-
|
|
651
|
-
if (event === '
|
|
701
|
+
const asked = e as Frozen<Args<'cmod.call'>> & { readonly caller?: string }
|
|
702
|
+
if (event === 'cmod.call' && asked.to === definition.name && asked.caller === cmodPluginName) {
|
|
703
|
+
if (asked.method === partMethods.shown) installer.hear(asked.input as ShownPages)
|
|
704
|
+
const answered = installer.answer(asked.method, asked.input)
|
|
705
|
+
if (answered !== undefined) return (await answered.catch((error: unknown) => ({ deny: `${definition.name}: ${messageOf(error)}` }))) as EventResult<N>
|
|
706
|
+
if (asked.method === partMethods.shown && phase !== 'active') return { value: null } as EventResult<N>
|
|
707
|
+
}
|
|
652
708
|
if (event === 'classic.SessionStart') {
|
|
653
709
|
missedSessionStart = undefined
|
|
654
710
|
hasHeardSessionStart = true
|
|
@@ -659,9 +715,10 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
659
715
|
}
|
|
660
716
|
if (shouldRecord && event === 'classic.UserPromptSubmit') record()
|
|
661
717
|
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
|
|
718
|
+
const tool = (e as { readonly tool?: unknown }).tool
|
|
719
|
+
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
720
|
if (event === 'cmod.call' && !isCallToThisMod) return next(e)
|
|
664
|
-
await (event === 'classic.SessionStart' || isCallToThisMod ? holdUntilStarted() : whenActive())
|
|
721
|
+
await (event === 'classic.SessionStart' || event === 'session.append' || isCallToThisMod ? holdUntilStarted() : whenActive())
|
|
665
722
|
if (event === 'classic.SessionStart' && phase !== 'active') {
|
|
666
723
|
missedSessionStart = e as Frozen<Args<'classic.SessionStart'>>
|
|
667
724
|
return next(e)
|
|
@@ -673,7 +730,11 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
673
730
|
const call = e as Frozen<Args<'cmod.call'>>
|
|
674
731
|
if (event === 'cmod.call' && call.to === definition.name && mod !== undefined) {
|
|
675
732
|
if (call.method === pendingStepsMethod) return { value: await pendingSteps(mod) } as EventResult<N>
|
|
676
|
-
if (call.method === finishStepsMethod)
|
|
733
|
+
if (call.method === finishStepsMethod) {
|
|
734
|
+
const active = mod
|
|
735
|
+
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()]))
|
|
736
|
+
return { value: opened } as EventResult<N>
|
|
737
|
+
}
|
|
677
738
|
}
|
|
678
739
|
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
740
|
if (event === 'classic.UserPromptSubmit' && phase === 'active' && !hasHeardSessionStart) {
|
|
@@ -686,14 +747,15 @@ export function createLifecycle<State extends object, Declared extends Options =
|
|
|
686
747
|
if (event !== 'ui.render' || render.component !== 'AbovePrompt' || runtime === undefined) return answer
|
|
687
748
|
return runtime.toaster.draw((await answer) as EventResult<'ui.render'>, runtime.claude.ui.resolve(render), render.props, render.requestId) as EventResult<N>
|
|
688
749
|
},
|
|
689
|
-
async caught<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>, { kind, message }:
|
|
750
|
+
async caught<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>, { error: { kind, message }, called }: Caught): Promise<EventResult<N>> {
|
|
690
751
|
const call = e as Frozen<Args<'cmod.call'>>
|
|
691
752
|
const isAsk = event === 'cmod.call' && call.to === definition.name && beforeAskOf(call) !== undefined
|
|
692
753
|
const reasons: Readonly<Record<string, string>> = { timeout: `it ran past the ${hookMs / 1000} s Claude Code gives a hook`, throw: message ?? 'it threw' }
|
|
693
754
|
const reason = reasons[kind]
|
|
694
755
|
if (reason === undefined) return isAsk ? ({ value: null } as EventResult<N>) : next(e)
|
|
695
756
|
const error = new Error(reason)
|
|
696
|
-
|
|
757
|
+
const failedBeneath = called && kind === 'throw' ? await next(e).then(() => undefined, messageOf) : undefined
|
|
758
|
+
if (failedBeneath === undefined || failedBeneath !== message) toastFailure({ id: `stopped:${event}`, message: `Claude Code stopped its ${event.replace(/^classic\./, '')} hook`, error })
|
|
697
759
|
return router.dispatchFailure(event, e, error, next)
|
|
698
760
|
},
|
|
699
761
|
}
|
|
@@ -737,7 +799,13 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
737
799
|
let parentAsked: Promise<string | undefined> | undefined
|
|
738
800
|
let hasEarlierPrompts = true
|
|
739
801
|
let agentType: string | undefined
|
|
740
|
-
|
|
802
|
+
let parts: DrawnParts | undefined
|
|
803
|
+
const borrowed: BorrowedMarkdown = (styler, cached, input) => {
|
|
804
|
+
const name = `The markdown style of ${styler}`
|
|
805
|
+
void parts?.fill(styler, cached, input, name)?.then(() => claude.ui.invalidate('ui.render'))
|
|
806
|
+
return parts?.element(styler, cached, `${styler}/markdown:`, name, `markdown:${styler}`)
|
|
807
|
+
}
|
|
808
|
+
const area = createUi<State>({ name: definition.name, claude, router, report, announce, mod: () => mod, borrowed, nextDraw: runtime.nextDraw })
|
|
741
809
|
const askParent = async (): Promise<string | undefined> => {
|
|
742
810
|
if (definition.name === cmodPluginName) return undefined
|
|
743
811
|
try {
|
|
@@ -1003,9 +1071,23 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
1003
1071
|
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
1072
|
let pages: Pages | undefined
|
|
1005
1073
|
const reportPart = (id: string, message: string, error: unknown) => report({ id, message, error })
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1008
|
-
|
|
1074
|
+
const pressElsewhere = (to: string, input: PartPress) =>
|
|
1075
|
+
void claude.cmod.call({ to, method: partMethods.press, input }).catch((error: unknown) => {
|
|
1076
|
+
parts?.forget(to)
|
|
1077
|
+
claude.ui.invalidate('ui.render')
|
|
1078
|
+
reportPart(`press:${to}`, `${to} did not take the press`, error)
|
|
1079
|
+
})
|
|
1080
|
+
parts = drawnParts({ claude, report: reportPart, press: (to, input) => (pages === undefined ? pressElsewhere(to, input) : pages.press(to, input)) })
|
|
1081
|
+
const widgets = conversations === undefined ? undefined : serveWidgets({ claude, router, store: runtime.store, report: reportPart, turnClosedBy: (rowId) => conversations.turnClosedBy(rowId), parts, markdownIn: area.markdownIn })
|
|
1082
|
+
pages = widgets === undefined ? undefined : servePages({ claude, router, report: reportPart, widgets, parts, tellStylers: area.restyled })
|
|
1083
|
+
const tellColor = (to: string, told: SessionColor) => {
|
|
1084
|
+
if (told.session === '' || to === cmodPluginName) return
|
|
1085
|
+
claude.clock.after(0, () => void claude.cmod.call({ to, method: conversationMethods.sessionColor, input: told }).catch((error: unknown) => claude.ui.log(`cmod: ${to} did not hear the session's color: ${messageOf(error)}`, { to: 'debug' })))
|
|
1086
|
+
}
|
|
1087
|
+
conversations?.onColor((told) => {
|
|
1088
|
+
client.recolor(told)
|
|
1089
|
+
for (const { mod: to } of pages?.declared() ?? []) tellColor(to, told)
|
|
1090
|
+
})
|
|
1009
1091
|
router.add('cmod.call', async (e, next) => {
|
|
1010
1092
|
if (e.to !== definition.name) return next(e)
|
|
1011
1093
|
const call = e as typeof e & { readonly caller?: string }
|
|
@@ -1015,15 +1097,21 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
1015
1097
|
client.changed(changedSession, change, isInProject !== false)
|
|
1016
1098
|
return { value: null }
|
|
1017
1099
|
}
|
|
1100
|
+
if (e.method === conversationMethods.sessionColor && call.caller === cmodPluginName) {
|
|
1101
|
+
client.recolor(e.input as SessionColor)
|
|
1102
|
+
return { value: null }
|
|
1103
|
+
}
|
|
1018
1104
|
const ask = beforeAskOf(call)
|
|
1019
1105
|
if (ask !== undefined) return { value: (await client.answerBefore(ask.session, ask.event, ask.change, ask.isInProject)) ?? null }
|
|
1020
1106
|
if (e.method === partMethods.changed && call.caller !== undefined && pages !== undefined) {
|
|
1021
|
-
|
|
1107
|
+
const update = e.input as PartsUpdate
|
|
1108
|
+
if (update.parts !== undefined && conversations !== undefined) tellColor(call.caller, conversations.color())
|
|
1109
|
+
return { value: { isHeld: pages.update(call.caller, update), stylers: pages.stylers() } }
|
|
1022
1110
|
}
|
|
1023
|
-
if (call.caller
|
|
1111
|
+
if (call.caller !== undefined) {
|
|
1024
1112
|
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
|
|
1113
|
+
const answered = area.answer(e.method, turn === undefined ? e.input : { ...(e.input as PartDraw), turn: client.turn(turn) }, call.caller)
|
|
1114
|
+
if (answered !== undefined) return answered.catch((error: unknown) => ({ deny: `${definition.name}: ${messageOf(error)}` }))
|
|
1027
1115
|
}
|
|
1028
1116
|
const served = await conversations?.answer(call)
|
|
1029
1117
|
return served ?? answerCall(definition.name, api, e)
|
|
@@ -1033,7 +1121,20 @@ async function createMod<State extends object, Declared extends Options>(definit
|
|
|
1033
1121
|
return ask === undefined ? { deny: `${definition.name}: ${error.message}` } : { value: client.failedBefore(ask.session, ask.event, ask.change, ask.isInProject, error) ?? null }
|
|
1034
1122
|
})
|
|
1035
1123
|
const agents = toolCalls(claude, router)
|
|
1036
|
-
keepEngine(mod, {
|
|
1124
|
+
keepEngine(mod, {
|
|
1125
|
+
claude: checked,
|
|
1126
|
+
router,
|
|
1127
|
+
toolCalls: agents,
|
|
1128
|
+
report,
|
|
1129
|
+
hasPermissionCheck: () => runtime.permissionRoutes() !== undefined,
|
|
1130
|
+
answerInput: area.answerInput,
|
|
1131
|
+
spawnCmod: async (args, program) => {
|
|
1132
|
+
await checkRun(grants, 'process.spawn', program.argv)
|
|
1133
|
+
return spawnCmod(claude, args, session, program.env)
|
|
1134
|
+
},
|
|
1135
|
+
...(widgets === undefined ? {} : { widgets }),
|
|
1136
|
+
...(pages === undefined ? {} : { pages }),
|
|
1137
|
+
})
|
|
1037
1138
|
const names = new Set<string>()
|
|
1038
1139
|
const reserveName = (type: string, name: string, taken: string): Reservation => {
|
|
1039
1140
|
const key = `${type}:${name}`
|
package/src/runtime/pages.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
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
|
-
import { partMethods, type PageDeclaration, type PageRequest, type PartPress, type PartsUpdate, type ShownPages } from './ui.js'
|
|
9
|
+
import { partMethods, stalePress, type PageDeclaration, type PageRequest, type PartPress, type PartsUpdate, type ShownPages } from './ui.js'
|
|
8
10
|
import type { Widgets } from './widgets.js'
|
|
9
11
|
|
|
10
12
|
const askedWaitMs = 1000
|
|
@@ -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 })
|
|
@@ -96,7 +129,8 @@ export function servePages({ claude, router, report, widgets, parts }: { readonl
|
|
|
96
129
|
(error: unknown) => {
|
|
97
130
|
parts.forget(mod)
|
|
98
131
|
claude.ui.invalidate('ui.render')
|
|
99
|
-
|
|
132
|
+
if (messageOf(error).includes(stalePress)) claude.ui.log(`cmod: ${mod} draws its page again, because the button pressed was from an older drawing:${messageOf(error)}`, { to: 'debug' })
|
|
133
|
+
else report(`press:${mod}`, `${mod} did not take the press`, error)
|
|
100
134
|
},
|
|
101
135
|
)
|
|
102
136
|
.finally(() => pressing.delete(pending))
|
|
@@ -104,21 +138,31 @@ export function servePages({ claude, router, report, widgets, parts }: { readonl
|
|
|
104
138
|
if (inputDepth === 0) void pending.then(takeWaiting)
|
|
105
139
|
}
|
|
106
140
|
|
|
107
|
-
const
|
|
141
|
+
const lastDrawn = new Map<string, { readonly params: string; readonly cached: string }>()
|
|
142
|
+
|
|
143
|
+
const plain = <Data,>(data: Data): Data => JSON.parse(JSON.stringify(data)) as Data
|
|
108
144
|
|
|
109
145
|
return {
|
|
110
146
|
declared: () => [...declared].map(([mod, pages]) => ({ mod, pages })),
|
|
147
|
+
stylers: () => [...stylers],
|
|
111
148
|
element(mod, page, params, columns, isFocused) {
|
|
112
149
|
if (!declared.has(mod)) void ask(mod)
|
|
113
|
-
const
|
|
150
|
+
const given = JSON.stringify(params ?? null)
|
|
151
|
+
const cached = `${page}\u0000${given}\u0000${columns}\u0000${isFocused}`
|
|
114
152
|
const name = `The ${page} page of ${mod}`
|
|
115
|
-
|
|
116
|
-
|
|
153
|
+
const keyPrefix = `${mod}/${page}:`
|
|
154
|
+
void parts.fill(mod, cached, { page, params: plain(params ?? null), columns, isFocused }, name)?.then(() => claude.ui.invalidate('ui.render'))
|
|
155
|
+
const failure = parts.failure(mod, cached)
|
|
156
|
+
const drawnAt = (key: string) => parts.element(mod, key, keyPrefix, name, `page:${mod}/${page}`)
|
|
157
|
+
const last = lastDrawn.get(keyPrefix)
|
|
158
|
+
const drawn = drawnAt(cached)
|
|
159
|
+
if (drawn !== undefined) lastDrawn.set(keyPrefix, { params: given, cached })
|
|
160
|
+
return drawn ?? (last?.params === given ? drawnAt(last.cached) : undefined) ?? (failure === undefined ? undefined : Text({ color: 'error', children: `${name} could not draw: ${failure}` }))
|
|
117
161
|
},
|
|
118
162
|
press,
|
|
119
163
|
update,
|
|
120
164
|
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' }))
|
|
165
|
+
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
166
|
},
|
|
123
167
|
apply(listener) {
|
|
124
168
|
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
|
}
|