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