@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/ui.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { ElementTable, MarkdownProps, PaneOpenArgs, RenderElement } from 'claude-code'
|
|
1
|
+
import type { Args, ElementTable, Frozen, MarkdownProps, PaneOpenArgs, RenderElement } from 'claude-code'
|
|
2
2
|
import type { Mod, Turn } from '../mod.js'
|
|
3
3
|
import { isObject } from '../records.js'
|
|
4
4
|
import { TurnEnd, type Area, type Widget, type WidgetContext, type WidgetInit } from '../ui/areas.js'
|
|
5
|
-
import { Box, drawWith, elementData, Text, type Handler } from '../ui/elements.js'
|
|
6
|
-
import type { Block, MarkdownReader } from '../ui/markdown.js'
|
|
5
|
+
import { Box, drawWith, elementData, Markdown, Text, type Handler } from '../ui/elements.js'
|
|
6
|
+
import type { Block, MarkdownPiece, MarkdownReader } from '../ui/markdown.js'
|
|
7
7
|
import { checkPane, checkPart, sizeKeys, type OpenOptions, type Page, type PageInit, type Pane, type PaneInit, type ScrollOptions, type ScrollTarget } from '../ui/panes.js'
|
|
8
|
-
import { componentOf, nameOf, type Kind, type Style, type StyleInit } from '../ui/styles.js'
|
|
8
|
+
import { componentOf, nameOf, type Kind, type Style, type StyledAt, type StyleInit } from '../ui/styles.js'
|
|
9
9
|
import type { Subscription } from '../utils/subscription.js'
|
|
10
10
|
import { messageOf } from '../utils/text.js'
|
|
11
11
|
import type { Claude } from './claude.js'
|
|
@@ -19,19 +19,35 @@ export const partMethods = {
|
|
|
19
19
|
press: 'cmod:press',
|
|
20
20
|
shown: 'cmod:shown',
|
|
21
21
|
changed: 'cmod:partsChanged',
|
|
22
|
+
stylers: 'cmod:markdownStylers',
|
|
22
23
|
} as const
|
|
23
24
|
|
|
25
|
+
export const stalePress = 'that button drew before the last redraw. Press the one it shows now.'
|
|
26
|
+
|
|
24
27
|
export type PagePlace = 'sidebar' | 'settings'
|
|
25
28
|
|
|
26
|
-
export type WidgetDeclaration = { readonly id: string; readonly title: string; readonly areas: readonly Area[] }
|
|
29
|
+
export type WidgetDeclaration = { readonly id: string; readonly title: string; readonly areas: readonly Area[]; readonly place?: NonNullable<WidgetInit['place']> }
|
|
27
30
|
|
|
28
31
|
export type PageDeclaration = { readonly id: string; readonly title: string; readonly place: PagePlace }
|
|
29
32
|
|
|
30
|
-
export type PartList = { readonly widgets: readonly WidgetDeclaration[]; readonly pages: readonly PageDeclaration[] }
|
|
33
|
+
export type PartList = { readonly widgets: readonly WidgetDeclaration[]; readonly pages: readonly PageDeclaration[]; readonly stylesMarkdown?: true }
|
|
31
34
|
|
|
32
35
|
export type PartDraw =
|
|
33
36
|
| { readonly widget: string; readonly area: Area; readonly columns: number; readonly turn?: Turn }
|
|
34
37
|
| { readonly page: string; readonly params: unknown; readonly columns: number; readonly isFocused: boolean }
|
|
38
|
+
| ({ readonly markdown: string } & StyledAt)
|
|
39
|
+
|
|
40
|
+
export type BorrowedMarkdown = (mod: string, cached: string, input: { readonly markdown: string } & StyledAt) => RenderElement | undefined
|
|
41
|
+
|
|
42
|
+
const unmeasuredColumns = 80
|
|
43
|
+
|
|
44
|
+
export function viewportColumns(e: Frozen<Args<'ui.render'>>): number {
|
|
45
|
+
return e.viewport?.columns ?? unmeasuredColumns
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function styledAt(e: Frozen<Args<'ui.render'>>, columns = viewportColumns(e)): StyledAt {
|
|
49
|
+
return { surface: e.surface, columns: Math.max(columns, 0) }
|
|
50
|
+
}
|
|
35
51
|
|
|
36
52
|
export type PartPress = { readonly draw: number; readonly handler: number; readonly args: readonly unknown[] }
|
|
37
53
|
|
|
@@ -81,6 +97,8 @@ function threw(error: unknown): string {
|
|
|
81
97
|
|
|
82
98
|
type DrawnPiece = { readonly drawing: RenderElement; readonly isClaudesRow: boolean }
|
|
83
99
|
|
|
100
|
+
type SentDrawing = { readonly draws: readonly number[]; readonly tree: string; readonly render: () => RenderElement | undefined }
|
|
101
|
+
|
|
84
102
|
async function drawComponent<Given extends object>(
|
|
85
103
|
table: ElementTable,
|
|
86
104
|
draw: (Default: (props: Given & { readonly children?: unknown }) => RenderElement) => RenderElement,
|
|
@@ -122,7 +140,7 @@ function drawPieces(pieces: readonly DrawnPiece[], place: 'pane' | 'opensReply'
|
|
|
122
140
|
return column([opening, Box({ paddingLeft: 2, children: column(rest) })])
|
|
123
141
|
}
|
|
124
142
|
|
|
125
|
-
function merged(updates: readonly PartsUpdate[]): PartsUpdate {
|
|
143
|
+
export function merged(updates: readonly PartsUpdate[]): PartsUpdate {
|
|
126
144
|
const parts = updates.findLast((update) => update.parts !== undefined)?.parts
|
|
127
145
|
const requests = updates.flatMap((update) => update.requests ?? [])
|
|
128
146
|
const isRedrawn = updates.some((update) => update.redraw === true)
|
|
@@ -145,6 +163,8 @@ export type UiOptions<State extends object> = {
|
|
|
145
163
|
readonly report: (failure: Failure) => void
|
|
146
164
|
readonly announce: (feature: string) => void
|
|
147
165
|
readonly mod: () => Mod<State>
|
|
166
|
+
readonly borrowed: BorrowedMarkdown
|
|
167
|
+
readonly nextDraw: () => number
|
|
148
168
|
}
|
|
149
169
|
|
|
150
170
|
export type UiArea<State extends object> = {
|
|
@@ -155,7 +175,9 @@ export type UiArea<State extends object> = {
|
|
|
155
175
|
readonly styles: Mod<State>['styles']
|
|
156
176
|
asked<Value>(run: () => Value | Promise<Value>): Promise<Value>
|
|
157
177
|
answerInput<Answer>(run: () => Promise<Answer>, passOn: () => Promise<unknown>): Promise<Answer>
|
|
158
|
-
answer(method: string, input: unknown): Promise<{ readonly value: unknown }> | undefined
|
|
178
|
+
answer(method: string, input: unknown, caller: string): Promise<{ readonly value: unknown }> | undefined
|
|
179
|
+
markdownIn(table: ElementTable, at: StyledAt): ((props: MarkdownProps) => RenderElement) | undefined
|
|
180
|
+
restyled(stylers: readonly string[]): void
|
|
159
181
|
started(): void
|
|
160
182
|
changed(): void
|
|
161
183
|
restorePanes(): Promise<void>
|
|
@@ -163,23 +185,28 @@ export type UiArea<State extends object> = {
|
|
|
163
185
|
|
|
164
186
|
const keptDrawInputs = 200
|
|
165
187
|
|
|
166
|
-
export function createUi<State extends object>({ name, claude, router, report, announce, mod }: UiOptions<State>): UiArea<State> {
|
|
188
|
+
export function createUi<State extends object>({ name, claude, router, report, announce, mod, borrowed, nextDraw }: UiOptions<State>): UiArea<State> {
|
|
167
189
|
const panes = new Map<string, AddedPane<State>>()
|
|
168
190
|
const paneLogs = new Map<string, (failure: string) => void>()
|
|
169
191
|
const openPanes = new Set<string>()
|
|
170
192
|
const paneStates = new Map<string, { readonly isShown: boolean; readonly isFocused: boolean }>()
|
|
171
193
|
const syncPanes = async () => {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
194
|
+
const listed = new Map((await claude.ui.panes()).map((open) => [open.id, open]))
|
|
195
|
+
for (const id of panes.keys()) {
|
|
196
|
+
const open = listed.get(id)
|
|
197
|
+
if (open?.isPlaced === true) openPanes.add(id)
|
|
198
|
+
else openPanes.delete(id)
|
|
199
|
+
if (open === undefined) paneStates.delete(id)
|
|
200
|
+
else paneStates.set(id, { isShown: open.isShown, isFocused: open.isFocused })
|
|
176
201
|
}
|
|
177
202
|
}
|
|
178
|
-
const readPanes = () =>
|
|
203
|
+
const readPanes = () => syncPanes().catch((error: unknown) => claude.ui.log(`${name} keeps its panes' open, shown, and focused state from before: ${messageOf(error)}`, { to: 'debug' }))
|
|
179
204
|
const requestedSizes = new Map<string, PaneSize>()
|
|
180
205
|
const styles = new Set<string>()
|
|
181
206
|
const markdownRenders = new Map<string, MarkdownRender>()
|
|
182
207
|
let read: MarkdownReader | undefined
|
|
208
|
+
let stylers: readonly string[] = []
|
|
209
|
+
let stylersSeen = 0
|
|
183
210
|
let isRouted = false
|
|
184
211
|
let isResizeQueued = false
|
|
185
212
|
const widgets = new Map<string, WidgetInit<State>>()
|
|
@@ -187,21 +214,56 @@ export function createUi<State extends object>({ name, claude, router, report, a
|
|
|
187
214
|
const pages = new Map<string, AddedPage<State>>()
|
|
188
215
|
let shownPages: ShownPages = { open: [], shown: null, isFocused: false }
|
|
189
216
|
const draws = new Map<number, { readonly id: string; readonly handlers: readonly Handler[] }>()
|
|
190
|
-
const latestDraws = new Map<string, Map<string,
|
|
217
|
+
const latestDraws = new Map<string, Map<string, SentDrawing>>()
|
|
191
218
|
const failedDraws = new Set<string>()
|
|
192
|
-
let drawCount = 0
|
|
193
219
|
let beforeStart: PartsUpdate[] | undefined = []
|
|
194
220
|
let askedDepth = 0
|
|
221
|
+
const askedCalls = new Set<Promise<void>>()
|
|
195
222
|
let unheldRequests = 0
|
|
196
223
|
let collected: PartsUpdate[] | undefined
|
|
197
224
|
let collecting = 0
|
|
198
225
|
let queued: PartsUpdate[] | undefined
|
|
199
226
|
|
|
200
227
|
const partList = (): PartList => ({
|
|
201
|
-
widgets: [...widgets.values()].map(({ id, title, areas: listed }) => ({ id, title, areas: listed })),
|
|
228
|
+
widgets: [...widgets.values()].map(({ id, title, areas: listed, place }) => ({ id, title, areas: listed, ...(place === undefined ? {} : { place }) })),
|
|
202
229
|
pages: [...pages.values()].map(({ init: { id, title }, place }) => ({ id, title, place })),
|
|
230
|
+
...(markdownRenders.size > 0 ? { stylesMarkdown: true } : {}),
|
|
203
231
|
})
|
|
204
232
|
|
|
233
|
+
const hear = (listed: unknown, isRestyled: boolean) => {
|
|
234
|
+
if (!Array.isArray(listed)) return
|
|
235
|
+
const heard = listed.filter((styler): styler is string => typeof styler === 'string' && styler !== name)
|
|
236
|
+
if (!isRestyled && heard.join('\0') === stylers.join('\0')) return
|
|
237
|
+
stylers = heard
|
|
238
|
+
stylersSeen += 1
|
|
239
|
+
if (name === cmodPluginName || panes.size > 0) claude.ui.invalidate('ui.render')
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const treeOf = (render: () => RenderElement | undefined, handlers: Handler[]) => {
|
|
243
|
+
const drawn = render()
|
|
244
|
+
return drawn === undefined ? null : elementData(drawn, handlers)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const isAnyDrawingChanged = () => {
|
|
248
|
+
for (const [key, inputs] of latestDraws) {
|
|
249
|
+
if (key === 'markdown') continue
|
|
250
|
+
for (const sent of inputs.values()) {
|
|
251
|
+
const handlers: Handler[] = []
|
|
252
|
+
try {
|
|
253
|
+
if (JSON.stringify(treeOf(sent.render, handlers)) !== sent.tree) return true
|
|
254
|
+
} catch (error) {
|
|
255
|
+
claude.ui.log(`${name}: the ${key} drawing threw, so the cmod plugin asks for it again: ${messageOf(error)}`, { to: 'debug' })
|
|
256
|
+
return true
|
|
257
|
+
}
|
|
258
|
+
const latest = sent.draws.at(-1)
|
|
259
|
+
if (latest !== undefined && draws.has(latest)) draws.set(latest, { id: key, handlers })
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return false
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const withNeededRedraw = ({ redraw, ...update }: PartsUpdate): PartsUpdate => (redraw === true && isAnyDrawingChanged() ? { ...update, redraw } : update)
|
|
266
|
+
|
|
205
267
|
const send = (update: PartsUpdate) => {
|
|
206
268
|
if (collected !== undefined) return void collected.push(update)
|
|
207
269
|
if (beforeStart !== undefined) return void beforeStart.push(update)
|
|
@@ -209,23 +271,31 @@ export function createUi<State extends object>({ name, claude, router, report, a
|
|
|
209
271
|
if (queued !== undefined && !isAsked) return void queued.push(update)
|
|
210
272
|
const batch: PartsUpdate[] = [update]
|
|
211
273
|
if (!isAsked) queued = batch
|
|
212
|
-
const tell = () => {
|
|
274
|
+
const tell = async () => {
|
|
213
275
|
if (queued === batch) queued = undefined
|
|
214
|
-
const
|
|
276
|
+
const needed = withNeededRedraw(merged(batch))
|
|
277
|
+
if (Object.keys(needed).length === 0) return
|
|
278
|
+
const input = { ...needed, ...(isAsked ? { isAsked: true as const } : {}) }
|
|
215
279
|
return claude.cmod.call({ to: cmodPluginName, method: partMethods.changed, input }).then(
|
|
216
280
|
(answer) => {
|
|
217
281
|
if (isAsked && input.requests !== undefined && !(isObject(answer) && answer['isHeld'] === true)) unheldRequests += 1
|
|
282
|
+
if (isObject(answer) && name !== cmodPluginName) hear(answer['stylers'], false)
|
|
218
283
|
},
|
|
219
284
|
(error: unknown) => claude.ui.log(`${name}: the cmod plugin did not hear about its pages and widgets: ${messageOf(error)}`, { to: 'debug' }),
|
|
220
285
|
)
|
|
221
286
|
}
|
|
222
|
-
|
|
287
|
+
if (!isAsked) return Promise.resolve().then(tell)
|
|
288
|
+
const told = tell().finally(() => askedCalls.delete(told))
|
|
289
|
+
askedCalls.add(told)
|
|
290
|
+
return told
|
|
223
291
|
}
|
|
224
292
|
|
|
225
293
|
const asked = async <Value>(run: () => Value | Promise<Value>): Promise<Value> => {
|
|
226
294
|
askedDepth += 1
|
|
227
295
|
try {
|
|
228
|
-
|
|
296
|
+
const value = await run()
|
|
297
|
+
while (askedCalls.size > 0) await Promise.allSettled(askedCalls)
|
|
298
|
+
return value
|
|
229
299
|
} finally {
|
|
230
300
|
askedDepth -= 1
|
|
231
301
|
}
|
|
@@ -241,7 +311,7 @@ export function createUi<State extends object>({ name, claude, router, report, a
|
|
|
241
311
|
}
|
|
242
312
|
const mine = collected
|
|
243
313
|
collected = collecting === 0 ? undefined : []
|
|
244
|
-
return merged(mine)
|
|
314
|
+
return withNeededRedraw(merged(mine))
|
|
245
315
|
}
|
|
246
316
|
|
|
247
317
|
const logOnce = (what: string) => {
|
|
@@ -254,27 +324,48 @@ export function createUi<State extends object>({ name, claude, router, report, a
|
|
|
254
324
|
|
|
255
325
|
const renderOf = (type: string) => markdownRenders.get(type)
|
|
256
326
|
|
|
257
|
-
const
|
|
327
|
+
const drawnBlock = (piece: MarkdownPiece<MarkdownRender>, at: StyledAt, drawClaudes: (text: string) => RenderElement, table?: ElementTable): RenderElement => {
|
|
328
|
+
if ('text' in piece) return drawClaudes(piece.text)
|
|
329
|
+
const { render, props: block } = piece
|
|
330
|
+
const draw = () => render.Component({ ...block, ...at, Default: ({ source = block.source }: { readonly source?: string }) => drawClaudes(source) })
|
|
331
|
+
try {
|
|
332
|
+
return table === undefined ? draw() : drawWith(table, draw)
|
|
333
|
+
} catch (error) {
|
|
334
|
+
render.log(threw(error))
|
|
335
|
+
return drawClaudes(block.source)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const othersFrom =
|
|
340
|
+
(table: ElementTable, at: StyledAt, from: number) =>
|
|
341
|
+
(props: MarkdownProps): RenderElement => {
|
|
342
|
+
for (const [index, styler] of stylers.entries()) {
|
|
343
|
+
if (index < from) continue
|
|
344
|
+
const cached = `${stylersSeen}\u0000${at.surface}\u0000${at.columns}\u0000${props.text}`
|
|
345
|
+
const drawn = drawWith(table, () => borrowed(styler, cached, { markdown: props.text, ...at }), othersFrom(table, at, index + 1))
|
|
346
|
+
if (drawn !== undefined) return drawn
|
|
347
|
+
}
|
|
348
|
+
return table.Markdown(props)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const markdownIn = (table: ElementTable, at: StyledAt) => {
|
|
258
352
|
const reader = read
|
|
259
|
-
|
|
353
|
+
const others = othersFrom(table, at, 0)
|
|
354
|
+
if (reader === undefined) return stylers.length === 0 ? undefined : others
|
|
260
355
|
return (props: MarkdownProps) => {
|
|
261
356
|
const pieces = reader(props.text, renderOf)
|
|
262
|
-
if (pieces.every((piece) => 'text' in piece)) return
|
|
263
|
-
const
|
|
264
|
-
const drawings = pieces.map((piece) => {
|
|
265
|
-
if ('text' in piece) return drawClaudes(piece.text)
|
|
266
|
-
const { render, props: block } = piece
|
|
267
|
-
try {
|
|
268
|
-
return drawWith(table, () => render.Component({ ...block, Default: ({ source = block.source }: { readonly source?: string }) => drawClaudes(source) }))
|
|
269
|
-
} catch (error) {
|
|
270
|
-
render.log(threw(error))
|
|
271
|
-
return drawClaudes(block.source)
|
|
272
|
-
}
|
|
273
|
-
})
|
|
357
|
+
if (pieces.every((piece) => 'text' in piece)) return others(props)
|
|
358
|
+
const drawings = pieces.map((piece) => drawnBlock(piece, at, (text) => others({ ...props, text }), table))
|
|
274
359
|
return drawPieces(drawings.map((drawing) => ({ drawing, isClaudesRow: false })), 'pane')
|
|
275
360
|
}
|
|
276
361
|
}
|
|
277
362
|
|
|
363
|
+
const markdownDrawing = (text: string, at: StyledAt): RenderElement => {
|
|
364
|
+
const pieces = read === undefined ? [] : read(text, renderOf)
|
|
365
|
+
if (pieces.every((piece) => 'text' in piece)) return Markdown({ text })
|
|
366
|
+
return drawPieces(pieces.map((piece) => ({ drawing: drawnBlock(piece, at, (source) => Markdown({ text: source })), isClaudesRow: false })), 'pane')
|
|
367
|
+
}
|
|
368
|
+
|
|
278
369
|
const routeMarkdown = (createReader: () => MarkdownReader) => {
|
|
279
370
|
if (read !== undefined) return
|
|
280
371
|
const reader = createReader()
|
|
@@ -291,7 +382,7 @@ export function createUi<State extends object>({ name, claude, router, report, a
|
|
|
291
382
|
const { render, props: block } = piece
|
|
292
383
|
const component = await drawComponent<{ readonly source?: string }>(
|
|
293
384
|
table,
|
|
294
|
-
(Default) => render.Component({ ...block, Default }),
|
|
385
|
+
(Default) => render.Component({ ...block, ...styledAt(e), id: e.requestId, Default }),
|
|
295
386
|
({ source = block.source }, isClaudesRow) => drawClaudes(source, index === 0 && isClaudesRow),
|
|
296
387
|
)
|
|
297
388
|
if (!('failure' in component)) return component
|
|
@@ -337,10 +428,10 @@ export function createUi<State extends object>({ name, claude, router, report, a
|
|
|
337
428
|
const { init } = added
|
|
338
429
|
openPanes.add(init.id)
|
|
339
430
|
paneStates.set(init.id, { isShown: true, isFocused: e.props.isFocused })
|
|
340
|
-
readPanes()
|
|
431
|
+
void readPanes()
|
|
341
432
|
const table = claude.ui.resolve(e)
|
|
342
433
|
try {
|
|
343
|
-
return drawWith(table, () => init.render(mod(), { ...e.props, params: added.params }), markdownIn(table))
|
|
434
|
+
return drawWith(table, () => init.render(mod(), { ...e.props, surface: e.surface, params: added.params }), markdownIn(table, styledAt(e, e.props.bodyColumns)))
|
|
344
435
|
} catch (error) {
|
|
345
436
|
paneLogs.get(init.id)?.(`threw: ${messageOf(error)}`)
|
|
346
437
|
return drawWith(table, () => Text({ color: 'error', children: `The ${init.title} pane could not draw: ${messageOf(error)}` }))
|
|
@@ -356,8 +447,8 @@ export function createUi<State extends object>({ name, claude, router, report, a
|
|
|
356
447
|
router.add('ui.close', async (e, next) => {
|
|
357
448
|
openPanes.delete(e.id)
|
|
358
449
|
paneStates.delete(e.id)
|
|
359
|
-
readPanes()
|
|
360
450
|
const closed = await next(e)
|
|
451
|
+
await readPanes()
|
|
361
452
|
const init = panes.get(e.id)?.init
|
|
362
453
|
if (init?.onClose !== undefined) await handle(init, 'onClose', () => init.onClose?.(mod(), e))
|
|
363
454
|
return closed
|
|
@@ -495,13 +586,14 @@ export function createUi<State extends object>({ name, claude, router, report, a
|
|
|
495
586
|
const Component = (props: object) => render(mod(), props as never)
|
|
496
587
|
const stop = (subscription?: Subscription) => () => {
|
|
497
588
|
subscription?.remove()
|
|
498
|
-
if (markdown !== undefined
|
|
589
|
+
if (markdown !== undefined && markdownRenders.delete(markdown.markdown)) void send({ parts: partList() })
|
|
499
590
|
styles.delete(feature)
|
|
500
591
|
claude.ui.invalidate('ui.render')
|
|
501
592
|
}
|
|
502
593
|
if (markdown !== undefined) {
|
|
503
594
|
markdownRenders.set(markdown.markdown, { Component, log: logOnce(`${nameOf(kind)} style`) })
|
|
504
595
|
routeMarkdown(markdown.createReader)
|
|
596
|
+
void send({ parts: partList() })
|
|
505
597
|
return { kind, remove: stop() }
|
|
506
598
|
}
|
|
507
599
|
const log = logOnce(`${nameOf(kind)} style`)
|
|
@@ -510,7 +602,7 @@ export function createUi<State extends object>({ name, claude, router, report, a
|
|
|
510
602
|
const table = claude.ui.resolve(e)
|
|
511
603
|
const drawn = await drawComponent(
|
|
512
604
|
table,
|
|
513
|
-
(Default) => Component({ ...e.props, Default }),
|
|
605
|
+
(Default) => Component({ ...e.props, ...styledAt(e), id: e.requestId, Default }),
|
|
514
606
|
(given, isClaudesRow) => next({ ...e, props: pieceProps(e.props, given, isClaudesRow) } as typeof e),
|
|
515
607
|
)
|
|
516
608
|
if ('failure' in drawn) {
|
|
@@ -524,43 +616,43 @@ export function createUi<State extends object>({ name, claude, router, report, a
|
|
|
524
616
|
}
|
|
525
617
|
|
|
526
618
|
const forgetDraws = (key: string) => {
|
|
527
|
-
for (const kept of latestDraws.get(key)?.values() ?? []) for (const draw of kept) draws.delete(draw)
|
|
619
|
+
for (const kept of latestDraws.get(key)?.values() ?? []) for (const draw of kept.draws) draws.delete(draw)
|
|
528
620
|
latestDraws.delete(key)
|
|
529
621
|
}
|
|
530
622
|
|
|
531
623
|
const drawPart = (key: string, title: string, input: PartDraw, render: () => RenderElement | undefined): DrawnPart | null => {
|
|
532
624
|
try {
|
|
533
625
|
const pressed: Handler[] = []
|
|
534
|
-
const
|
|
535
|
-
const tree = drawn === undefined ? null : elementData(drawn, pressed)
|
|
626
|
+
const tree = treeOf(render, pressed)
|
|
536
627
|
failedDraws.delete(key)
|
|
537
|
-
|
|
538
|
-
const inputs = latestDraws.get(key) ?? new Map<string,
|
|
628
|
+
const number = nextDraw()
|
|
629
|
+
const inputs = latestDraws.get(key) ?? new Map<string, SentDrawing>()
|
|
539
630
|
latestDraws.set(key, inputs)
|
|
540
631
|
const drawnFor = JSON.stringify('turn' in input ? { ...input, turn: input.turn.prompt.id } : input)
|
|
541
|
-
const earlier = inputs.get(drawnFor) ?? []
|
|
542
|
-
const kept = [...earlier,
|
|
632
|
+
const earlier = inputs.get(drawnFor)?.draws ?? []
|
|
633
|
+
const kept = [...earlier, number].slice(-2)
|
|
543
634
|
for (const draw of earlier) if (!kept.includes(draw)) draws.delete(draw)
|
|
544
635
|
inputs.delete(drawnFor)
|
|
545
|
-
inputs.set(drawnFor, kept)
|
|
636
|
+
inputs.set(drawnFor, { draws: kept, tree: JSON.stringify(tree), render })
|
|
546
637
|
const [oldest] = inputs
|
|
547
638
|
if (inputs.size > keptDrawInputs && oldest !== undefined) {
|
|
548
|
-
for (const draw of oldest[1]) draws.delete(draw)
|
|
639
|
+
for (const draw of oldest[1].draws) draws.delete(draw)
|
|
549
640
|
inputs.delete(oldest[0])
|
|
550
641
|
}
|
|
551
|
-
draws.set(
|
|
552
|
-
return { draw:
|
|
642
|
+
draws.set(number, { id: key, handlers: pressed })
|
|
643
|
+
return { draw: number, tree }
|
|
553
644
|
} catch (error) {
|
|
554
645
|
if (!failedDraws.has(key)) report({ id: key, message: `The ${title} could not draw`, error })
|
|
555
646
|
failedDraws.add(key)
|
|
556
|
-
|
|
647
|
+
throw error
|
|
557
648
|
}
|
|
558
649
|
}
|
|
559
650
|
|
|
560
651
|
const draw = (input: PartDraw): DrawnPart | null => {
|
|
652
|
+
if ('markdown' in input) return drawPart('markdown', 'markdown style', input, () => markdownDrawing(input.markdown, { surface: input.surface, columns: input.columns }))
|
|
561
653
|
if ('page' in input) {
|
|
562
654
|
const added = pages.get(input.page)
|
|
563
|
-
if (added === undefined)
|
|
655
|
+
if (added === undefined) throw new Error(`${name} has no ${input.page} page.`)
|
|
564
656
|
return drawPart(`page:${input.page}`, `${added.init.title} page`, input, () => added.init.render(mod(), { params: input.params, isFocused: input.isFocused, columns: input.columns }))
|
|
565
657
|
}
|
|
566
658
|
const widget = widgets.get(input.widget)
|
|
@@ -570,17 +662,21 @@ export function createUi<State extends object>({ name, claude, router, report, a
|
|
|
570
662
|
return drawPart(`widget:${widget.id}`, `${widget.title} widget`, input, () => widget.render(mod(), context))
|
|
571
663
|
}
|
|
572
664
|
|
|
573
|
-
const press = async ({ draw: drawn, handler, args }: PartPress): Promise<PartsUpdate> => {
|
|
665
|
+
const press = async ({ draw: drawn, handler, args }: PartPress, caller: string): Promise<PartsUpdate | null> => {
|
|
574
666
|
const known = draws.get(drawn)
|
|
575
667
|
const run = known?.handlers[handler]
|
|
576
|
-
if (known === undefined || run === undefined) throw new Error(`${name}:
|
|
577
|
-
|
|
668
|
+
if (known === undefined || run === undefined) throw new Error(`${name}: ${stalePress}`)
|
|
669
|
+
if (caller !== cmodPluginName && known.id !== 'markdown') throw new Error(`${name}: ${caller} pressed a button only the cmod plugin draws.`)
|
|
670
|
+
const pressed = async () => {
|
|
578
671
|
try {
|
|
579
672
|
await run(...args)
|
|
580
673
|
} catch (error) {
|
|
581
674
|
report({ id: known.id, message: 'Its button failed', error })
|
|
582
675
|
}
|
|
583
|
-
}
|
|
676
|
+
}
|
|
677
|
+
if (caller === cmodPluginName) return collect(pressed)
|
|
678
|
+
await pressed()
|
|
679
|
+
return null
|
|
584
680
|
}
|
|
585
681
|
|
|
586
682
|
return {
|
|
@@ -601,16 +697,24 @@ export function createUi<State extends object>({ name, claude, router, report, a
|
|
|
601
697
|
if (unheldRequests > before) await passOn()
|
|
602
698
|
return answer
|
|
603
699
|
},
|
|
604
|
-
answer(method, input) {
|
|
700
|
+
answer(method, input, caller) {
|
|
701
|
+
const isMarkdown = method === partMethods.draw && isObject(input) && typeof input['markdown'] === 'string'
|
|
702
|
+
if (caller !== cmodPluginName && !isMarkdown && method !== partMethods.press) return undefined
|
|
605
703
|
if (method === partMethods.list) return Promise.resolve({ value: partList() })
|
|
606
|
-
if (method === partMethods.draw) return Promise.resolve({ value: draw(input as PartDraw) })
|
|
607
|
-
if (method === partMethods.press) return press(input as PartPress).then((value) => ({ value }))
|
|
704
|
+
if (method === partMethods.draw) return Promise.resolve().then(() => ({ value: draw(input as PartDraw) }))
|
|
705
|
+
if (method === partMethods.press) return press(input as PartPress, caller).then((value) => ({ value }))
|
|
608
706
|
if (method === partMethods.shown) {
|
|
609
707
|
shownPages = input as ShownPages
|
|
610
708
|
return Promise.resolve({ value: null })
|
|
611
709
|
}
|
|
710
|
+
if (method === partMethods.stylers) {
|
|
711
|
+
hear(input, true)
|
|
712
|
+
return Promise.resolve({ value: null })
|
|
713
|
+
}
|
|
612
714
|
return undefined
|
|
613
715
|
},
|
|
716
|
+
markdownIn,
|
|
717
|
+
restyled: (stylers) => hear(stylers, true),
|
|
614
718
|
started() {
|
|
615
719
|
const held = merged(beforeStart ?? [])
|
|
616
720
|
beforeStart = undefined
|
package/src/runtime/watches.ts
CHANGED
|
@@ -6,18 +6,24 @@ import type { Failure } from './toaster.js'
|
|
|
6
6
|
|
|
7
7
|
export type Watches = { add(paths: readonly string[], listener: (change: FileChange) => unknown): Subscription }
|
|
8
8
|
|
|
9
|
-
type Watch = { readonly paths: readonly string[]; readonly listener: (change: FileChange) => unknown; seen: ReadonlyMap<string, number> | undefined }
|
|
9
|
+
type Watch = { readonly paths: readonly string[]; readonly listener: (change: FileChange) => unknown; seen: ReadonlyMap<string, number> | undefined; missing: ReadonlySet<string> }
|
|
10
10
|
|
|
11
11
|
const watchMs = 500
|
|
12
12
|
|
|
13
|
+
const missingEvery = 4
|
|
14
|
+
|
|
13
15
|
const watchedFiles = 2000
|
|
14
16
|
|
|
15
17
|
export function createWatches(claude: Claude, report: (failure: Failure) => void): Watches {
|
|
16
18
|
const watches = new Set<Watch>()
|
|
17
19
|
let polling: { cancel(): void } | undefined
|
|
18
20
|
let isPolling = false
|
|
19
|
-
|
|
21
|
+
let ticks = 0
|
|
22
|
+
const stampsOf = async (watch: Watch): Promise<Map<string, number>> => {
|
|
23
|
+
const { paths } = watch
|
|
20
24
|
const stamps = new Map<string, number>()
|
|
25
|
+
const missing = new Set<string>()
|
|
26
|
+
const skipped = ticks % missingEvery === 0 ? new Set<string>() : watch.missing
|
|
21
27
|
const visit = async (path: string): Promise<void> => {
|
|
22
28
|
const stat = await claude.fs.stat(path).catch(ifMissing(undefined))
|
|
23
29
|
if (stat === undefined) return
|
|
@@ -27,7 +33,15 @@ export function createWatches(claude: Claude, report: (failure: Failure) => void
|
|
|
27
33
|
}
|
|
28
34
|
for (const entry of await claude.fs.list(path).catch(ifMissing([]))) await visit(`${path}/${entry.name}`)
|
|
29
35
|
}
|
|
30
|
-
for (const path of paths)
|
|
36
|
+
for (const path of paths) {
|
|
37
|
+
if (skipped.has(path)) {
|
|
38
|
+
missing.add(path)
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
await visit(path)
|
|
42
|
+
if (![...stamps.keys()].some((seen) => seen === path || seen.startsWith(`${path}/`))) missing.add(path)
|
|
43
|
+
}
|
|
44
|
+
watch.missing = missing
|
|
31
45
|
return stamps
|
|
32
46
|
}
|
|
33
47
|
const stop = (watch: Watch) => {
|
|
@@ -37,7 +51,7 @@ export function createWatches(claude: Claude, report: (failure: Failure) => void
|
|
|
37
51
|
polling = undefined
|
|
38
52
|
}
|
|
39
53
|
const stampsOrStop = (watch: Watch) =>
|
|
40
|
-
stampsOf(watch
|
|
54
|
+
stampsOf(watch).catch((error: unknown) => {
|
|
41
55
|
stop(watch)
|
|
42
56
|
report({ id: 'fs:onChange', message: 'Could not read the files fs.onChange watches', error })
|
|
43
57
|
return undefined
|
|
@@ -45,6 +59,7 @@ export function createWatches(claude: Claude, report: (failure: Failure) => void
|
|
|
45
59
|
const poll = async () => {
|
|
46
60
|
if (isPolling) return
|
|
47
61
|
isPolling = true
|
|
62
|
+
ticks += 1
|
|
48
63
|
try {
|
|
49
64
|
for (const watch of watches) {
|
|
50
65
|
const now = await stampsOrStop(watch)
|
|
@@ -65,7 +80,7 @@ export function createWatches(claude: Claude, report: (failure: Failure) => void
|
|
|
65
80
|
}
|
|
66
81
|
return {
|
|
67
82
|
add(paths, listener) {
|
|
68
|
-
const watch: Watch = { paths, listener, seen: undefined }
|
|
83
|
+
const watch: Watch = { paths, listener, seen: undefined, missing: new Set() }
|
|
69
84
|
watches.add(watch)
|
|
70
85
|
void stampsOrStop(watch).then((stamps) => void (watch.seen ??= stamps))
|
|
71
86
|
polling ??= claude.clock.every(watchMs, () => void poll())
|