@cmodjs/core 0.3.0 → 1.5.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 +61 -10
- package/docs/dependencies.md +16 -14
- package/docs/hooks.md +64 -29
- package/docs/index.md +29 -25
- package/docs/install-steps.md +13 -9
- package/docs/jobs.md +59 -111
- package/docs/mod.md +227 -175
- package/docs/options.md +2 -2
- package/docs/permissions.md +27 -16
- package/docs/session.md +311 -0
- package/docs/state.md +11 -7
- package/docs/testing.md +68 -23
- package/docs/ui.md +116 -76
- package/jobs/check.d.ts.map +1 -1
- package/jobs/check.js +5 -2
- package/jobs/context.d.ts +7 -8
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +23 -73
- package/jobs/permissions/decide-permission.d.ts +5 -3
- package/jobs/permissions/decide-permission.d.ts.map +1 -1
- package/jobs/permissions/decide-permission.js +6 -4
- package/jobs/permissions/find-project-scope.d.ts +1 -1
- package/jobs/permissions/find-project-scope.d.ts.map +1 -1
- package/jobs/permissions/find-project-scope.js +8 -11
- package/jobs/permissions/match-target.d.ts +2 -2
- package/jobs/permissions/match-target.d.ts.map +1 -1
- package/jobs/permissions.d.ts +1 -1
- package/jobs/permissions.d.ts.map +1 -1
- package/jobs/permissions.js +6 -4
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +14 -4
- package/jobs/prompt.d.ts +4 -2
- package/jobs/prompt.d.ts.map +1 -1
- package/jobs/prompt.js +16 -15
- package/jobs/schedule.d.ts +2 -2
- package/jobs/schedule.d.ts.map +1 -1
- package/jobs/schedule.js +9 -9
- package/jobs/slash-command.d.ts +4 -4
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +14 -9
- package/jobs/status-line.d.ts +1 -11
- package/jobs/status-line.d.ts.map +1 -1
- package/jobs/status-line.js +8 -12
- package/jobs/tool.d.ts +2 -2
- package/jobs/tool.d.ts.map +1 -1
- package/jobs/tool.js +15 -9
- package/mod.d.ts +178 -75
- package/mod.d.ts.map +1 -1
- package/mod.js +127 -3
- package/options.d.ts +2 -2
- package/options.js +19 -19
- package/package.json +3 -3
- package/records.d.ts +13 -8
- package/records.d.ts.map +1 -1
- package/records.js +33 -17
- package/register.d.ts.map +1 -1
- package/register.js +34 -5
- package/runtime/announce.d.ts +18 -0
- package/runtime/announce.d.ts.map +1 -0
- package/runtime/announce.js +68 -0
- package/runtime/before.d.ts +32 -0
- package/runtime/before.d.ts.map +1 -0
- package/runtime/before.js +214 -0
- package/runtime/bound.d.ts +4 -0
- package/runtime/bound.d.ts.map +1 -0
- package/runtime/bound.js +9 -0
- package/runtime/claude.d.ts +4 -2
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +17 -0
- package/runtime/conversations.d.ts.map +1 -0
- package/runtime/conversations.js +563 -0
- package/runtime/deadline.d.ts +0 -1
- package/runtime/deadline.d.ts.map +1 -1
- package/runtime/deadline.js +1 -1
- package/runtime/dependencies.d.ts +2 -0
- package/runtime/dependencies.d.ts.map +1 -1
- package/runtime/dependencies.js +10 -1
- package/runtime/engine.d.ts +13 -0
- package/runtime/engine.d.ts.map +1 -0
- package/runtime/engine.js +10 -0
- package/runtime/grants.d.ts +4 -0
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +68 -31
- package/runtime/hooks.d.ts +5 -4
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +29 -9
- package/runtime/installer.d.ts +1 -1
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +17 -14
- package/runtime/lifecycle.d.ts +3 -1
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +414 -212
- package/runtime/metadata.d.ts +3 -1
- package/runtime/metadata.d.ts.map +1 -1
- package/runtime/metadata.js +25 -14
- package/runtime/options.d.ts +3 -1
- package/runtime/options.d.ts.map +1 -1
- package/runtime/options.js +4 -4
- package/runtime/programs.d.ts +8 -0
- package/runtime/programs.d.ts.map +1 -1
- package/runtime/programs.js +39 -1
- package/runtime/router.d.ts +2 -1
- package/runtime/router.d.ts.map +1 -1
- package/runtime/router.js +11 -3
- package/runtime/sessions.d.ts +58 -0
- package/runtime/sessions.d.ts.map +1 -0
- package/runtime/sessions.js +433 -0
- package/runtime/state.d.ts +6 -2
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +46 -13
- package/runtime/toaster.d.ts +24 -0
- package/runtime/toaster.d.ts.map +1 -0
- package/runtime/toaster.js +286 -0
- package/runtime/ui.d.ts +6 -17
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +62 -118
- package/session/context/context.d.ts +12 -0
- package/session/context/context.d.ts.map +1 -0
- package/session/context/context.js +2 -0
- package/session/context/turn/message/document.d.ts +7 -0
- package/session/context/turn/message/document.d.ts.map +1 -0
- package/session/context/turn/message/document.js +1 -0
- package/session/context/turn/message/message.d.ts +55 -0
- package/session/context/turn/message/message.d.ts.map +1 -0
- package/session/context/turn/message/message.js +178 -0
- package/session/context/turn/message/note.d.ts +8 -0
- package/session/context/turn/message/note.d.ts.map +1 -0
- package/session/context/turn/message/note.js +1 -0
- package/session/context/turn/message/notice.d.ts +6 -0
- package/session/context/turn/message/notice.d.ts.map +1 -0
- package/session/context/turn/message/notice.js +1 -0
- package/session/context/turn/message/prompt.d.ts +8 -0
- package/session/context/turn/message/prompt.d.ts.map +1 -0
- package/session/context/turn/message/prompt.js +1 -0
- package/session/context/turn/message/reply.d.ts +6 -0
- package/session/context/turn/message/reply.d.ts.map +1 -0
- package/session/context/turn/message/reply.js +1 -0
- package/session/context/turn/message/sender.d.ts +14 -0
- package/session/context/turn/message/sender.d.ts.map +1 -0
- package/session/context/turn/message/sender.js +30 -0
- package/session/context/turn/message/skill.d.ts +7 -0
- package/session/context/turn/message/skill.d.ts.map +1 -0
- package/session/context/turn/message/skill.js +1 -0
- package/session/context/turn/message/tool-call.d.ts +9 -0
- package/session/context/turn/message/tool-call.d.ts.map +1 -0
- package/session/context/turn/message/tool-call.js +1 -0
- package/session/context/turn/message/tool-result.d.ts +8 -0
- package/session/context/turn/message/tool-result.d.ts.map +1 -0
- package/session/context/turn/message/tool-result.js +1 -0
- package/session/context/turn/turn.d.ts +13 -0
- package/session/context/turn/turn.d.ts.map +1 -0
- package/session/context/turn/turn.js +2 -0
- package/session/conversation.d.ts +73 -0
- package/session/conversation.d.ts.map +1 -0
- package/session/conversation.js +146 -0
- package/session/goal/goal.d.ts +11 -0
- package/session/goal/goal.d.ts.map +1 -0
- package/session/goal/goal.js +1 -0
- package/session/goal/task.d.ts +16 -0
- package/session/goal/task.d.ts.map +1 -0
- package/session/goal/task.js +3 -0
- package/session/session.d.ts +158 -0
- package/session/session.d.ts.map +1 -0
- package/session/session.js +2 -0
- package/src/jobs/check.ts +7 -4
- package/src/jobs/context.ts +24 -82
- package/src/jobs/permissions/decide-permission.ts +10 -8
- package/src/jobs/permissions/find-project-scope.ts +8 -14
- package/src/jobs/permissions/match-target.ts +3 -3
- package/src/jobs/permissions.ts +8 -6
- package/src/jobs/program.ts +13 -4
- package/src/jobs/prompt.ts +21 -24
- package/src/jobs/schedule.ts +14 -14
- package/src/jobs/slash-command.ts +20 -14
- package/src/jobs/status-line.ts +9 -21
- package/src/jobs/tool.ts +17 -11
- package/src/mod.ts +252 -97
- package/src/options.ts +22 -22
- package/src/records.ts +40 -23
- package/src/register.ts +34 -5
- package/src/runtime/announce.ts +76 -0
- package/src/runtime/before.ts +200 -0
- package/src/runtime/bound.ts +12 -0
- package/src/runtime/claude.ts +7 -2
- package/src/runtime/conversations.ts +579 -0
- package/src/runtime/deadline.ts +2 -2
- package/src/runtime/dependencies.ts +13 -1
- package/src/runtime/engine.ts +23 -0
- package/src/runtime/grants.ts +69 -29
- package/src/runtime/hooks.ts +39 -14
- package/src/runtime/installer.ts +16 -14
- package/src/runtime/lifecycle.ts +413 -201
- package/src/runtime/metadata.ts +37 -14
- package/src/runtime/options.ts +19 -4
- package/src/runtime/programs.ts +40 -1
- package/src/runtime/router.ts +11 -4
- package/src/runtime/sessions.ts +447 -0
- package/src/runtime/state.ts +50 -14
- package/src/runtime/toaster.ts +334 -0
- package/src/runtime/ui.ts +65 -143
- package/src/session/context/context.ts +14 -0
- package/src/session/context/turn/message/document.ts +8 -0
- package/src/session/context/turn/message/message.ts +203 -0
- package/src/session/context/turn/message/note.ts +9 -0
- package/src/session/context/turn/message/notice.ts +7 -0
- package/src/session/context/turn/message/prompt.ts +9 -0
- package/src/session/context/turn/message/reply.ts +7 -0
- package/src/session/context/turn/message/sender.ts +38 -0
- package/src/session/context/turn/message/skill.ts +8 -0
- package/src/session/context/turn/message/tool-call.ts +10 -0
- package/src/session/context/turn/message/tool-result.ts +9 -0
- package/src/session/context/turn/turn.ts +15 -0
- package/src/session/conversation.ts +194 -0
- package/src/session/goal/goal.ts +10 -0
- package/src/session/goal/task.ts +19 -0
- package/src/session/session.ts +141 -0
- package/src/testing/fake-claude.ts +25 -17
- package/src/testing/fake-elements.ts +3 -2
- package/src/testing/fake-files.ts +1 -1
- package/src/testing/fake-sessions.ts +277 -0
- package/src/testing.ts +138 -35
- package/src/ui/components.ts +3 -3
- package/src/ui/elements.ts +22 -8
- package/src/ui/markdown.ts +22 -26
- package/src/ui/places.ts +49 -0
- package/src/utils/call-effects.ts +5 -5
- package/src/utils/changelog.ts +32 -0
- package/src/utils/disposable.ts +3 -0
- package/src/utils/matcher.ts +37 -0
- package/src/utils/metadata.ts +17 -15
- package/src/utils/parse-shell.ts +9 -8
- package/src/utils/paths.ts +21 -4
- package/src/utils/text.ts +4 -0
- package/testing/fake-claude.d.ts +5 -1
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +21 -12
- package/testing/fake-elements.js +4 -2
- package/testing/fake-files.js +1 -1
- package/testing/fake-sessions.d.ts +81 -0
- package/testing/fake-sessions.d.ts.map +1 -0
- package/testing/fake-sessions.js +249 -0
- package/testing.d.ts +34 -13
- package/testing.d.ts.map +1 -1
- package/testing.js +115 -23
- package/ui/components.d.ts +2 -3
- package/ui/components.d.ts.map +1 -1
- package/ui/components.js +3 -3
- package/ui/elements.d.ts.map +1 -1
- package/ui/elements.js +19 -9
- package/ui/markdown.d.ts +35 -33
- package/ui/markdown.d.ts.map +1 -1
- package/ui/markdown.js +14 -16
- package/ui/places.d.ts +43 -0
- package/ui/places.d.ts.map +1 -0
- package/ui/places.js +31 -0
- package/utils/call-effects.d.ts +2 -1
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +3 -3
- package/utils/changelog.d.ts +9 -0
- package/utils/changelog.d.ts.map +1 -0
- package/utils/changelog.js +29 -0
- package/utils/disposable.d.ts +4 -0
- package/utils/disposable.d.ts.map +1 -0
- package/utils/disposable.js +1 -0
- package/utils/matcher.d.ts +13 -0
- package/utils/matcher.d.ts.map +1 -0
- package/utils/matcher.js +32 -0
- package/utils/metadata.d.ts +3 -3
- package/utils/metadata.js +18 -14
- package/utils/parse-shell.js +9 -7
- package/utils/paths.d.ts +4 -1
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +19 -4
- package/utils/text.d.ts +1 -0
- package/utils/text.d.ts.map +1 -1
- package/utils/text.js +3 -0
- package/src/ui/slots.ts +0 -36
- package/ui/slots.d.ts +0 -208
- package/ui/slots.d.ts.map +0 -1
- package/ui/slots.js +0 -19
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import type { ElementTable, RenderElement, RenderPropsOf, Timer } from 'claude-code'
|
|
2
|
+
import type { Toast, ToastId, ToastOptions, ToastType } from '../mod.js'
|
|
3
|
+
import { Box, Button, drawWith, Link, Text } from '../ui/elements.js'
|
|
4
|
+
import { coreVersion, isObject } from '../records.js'
|
|
5
|
+
import { firstLine, messageOf } from '../utils/text.js'
|
|
6
|
+
import type { Claude } from './claude.js'
|
|
7
|
+
|
|
8
|
+
export type ShownToast = { readonly type: ToastType; readonly message: string; readonly description?: string }
|
|
9
|
+
|
|
10
|
+
export type Failure = { readonly id: string; readonly message: string; readonly error?: unknown; readonly description?: string; readonly retry?: () => unknown }
|
|
11
|
+
|
|
12
|
+
export type Toaster = {
|
|
13
|
+
readonly toast: Toast
|
|
14
|
+
copy(text: string): Promise<void>
|
|
15
|
+
fail(failure: Failure): ToastId
|
|
16
|
+
readonly shown: readonly ShownToast[]
|
|
17
|
+
draw(drawn: RenderElement, table: ElementTable, band: RenderPropsOf['AbovePrompt'], bandId: string): RenderElement
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type Action = { readonly label: string; readonly onClick: () => unknown }
|
|
21
|
+
|
|
22
|
+
type Contact = { readonly kind: 'github'; readonly repository: string } | { readonly kind: 'email'; readonly address: string }
|
|
23
|
+
|
|
24
|
+
type Reporting = { readonly contact: Contact | undefined; readonly details: string; readonly status?: string }
|
|
25
|
+
|
|
26
|
+
type Entry = {
|
|
27
|
+
readonly id: ToastId
|
|
28
|
+
readonly type: ToastType
|
|
29
|
+
readonly message: string
|
|
30
|
+
readonly options: ToastOptions
|
|
31
|
+
readonly actions: readonly Action[]
|
|
32
|
+
readonly hides?: string
|
|
33
|
+
isOpen: boolean
|
|
34
|
+
reporting?: Reporting | undefined
|
|
35
|
+
timer: Timer | undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const githubRepository = /^https:\/\/github\.com\/([^/]+\/[^/#?]+?)(?:\.git)?(?:\/issues)?\/?$/
|
|
39
|
+
|
|
40
|
+
const linkLimit = 2048
|
|
41
|
+
|
|
42
|
+
const hiddenKey = 'cmod:hidden-failures'
|
|
43
|
+
|
|
44
|
+
const defaultDuration = 4000
|
|
45
|
+
const visibleToasts = 3
|
|
46
|
+
const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] as const
|
|
47
|
+
const spinnerMs = 100
|
|
48
|
+
const icons: Record<Exclude<ToastType, 'loading'>, { readonly glyph: string; readonly color?: string }> = {
|
|
49
|
+
default: { glyph: ' ' },
|
|
50
|
+
success: { glyph: '✓', color: 'success' },
|
|
51
|
+
info: { glyph: 'ℹ', color: 'suggestion' },
|
|
52
|
+
warning: { glyph: '⚠', color: 'warning' },
|
|
53
|
+
error: { glyph: '✗', color: 'error' },
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createToaster(claude: Claude, name: string, version: () => string | undefined): Toaster {
|
|
57
|
+
const entries = new Map<ToastId, Entry>()
|
|
58
|
+
const hidden = new Map<string, string>()
|
|
59
|
+
void claude.store.get(hiddenKey).then(
|
|
60
|
+
(saved) => {
|
|
61
|
+
if (typeof saved !== 'object' || saved === null) return
|
|
62
|
+
for (const [id, hides] of Object.entries(saved)) if (typeof hides === 'string' && !hidden.has(id)) hidden.set(id, hides)
|
|
63
|
+
},
|
|
64
|
+
(error: unknown) => claude.ui.log(`${name}: could not read the failures you hid: ${messageOf(error)}`, { to: 'debug' }),
|
|
65
|
+
)
|
|
66
|
+
let lastId = 0
|
|
67
|
+
let frame = 0
|
|
68
|
+
let spinning: Timer | undefined
|
|
69
|
+
let isHeld = false
|
|
70
|
+
let showsAll = false
|
|
71
|
+
let bandId: string | undefined
|
|
72
|
+
|
|
73
|
+
const changed = () => {
|
|
74
|
+
const isLoading = [...entries.values()].some((entry) => entry.type === 'loading')
|
|
75
|
+
if (isLoading && spinning === undefined) {
|
|
76
|
+
spinning = claude.clock.every(spinnerMs, () => {
|
|
77
|
+
frame += 1
|
|
78
|
+
claude.ui.invalidate('ui.render')
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
if (!isLoading && spinning !== undefined) {
|
|
82
|
+
spinning.cancel()
|
|
83
|
+
spinning = undefined
|
|
84
|
+
}
|
|
85
|
+
if (entries.size <= visibleToasts) showsAll = false
|
|
86
|
+
claude.ui.invalidate('ui.render')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const disarm = (entry: Entry) => {
|
|
90
|
+
entry.timer?.cancel()
|
|
91
|
+
entry.timer = undefined
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const arm = (entry: Entry) => {
|
|
95
|
+
const duration = entry.options.duration ?? defaultDuration
|
|
96
|
+
if (entry.timer !== undefined || entry.type === 'loading' || entry.isOpen || !Number.isFinite(duration)) return
|
|
97
|
+
entry.timer = claude.clock.after(duration, () => {
|
|
98
|
+
entry.timer = undefined
|
|
99
|
+
if (isHeld) arm(entry)
|
|
100
|
+
else dismiss(entry.id)
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const dismiss = (id?: ToastId) => {
|
|
105
|
+
for (const entry of entries.values()) {
|
|
106
|
+
if (id !== undefined && entry.id !== id) continue
|
|
107
|
+
disarm(entry)
|
|
108
|
+
entries.delete(entry.id)
|
|
109
|
+
}
|
|
110
|
+
changed()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const put = (type: ToastType, message: string, options: ToastOptions, actions: readonly Action[], hides?: string): ToastId => {
|
|
114
|
+
const id = options.id ?? (lastId += 1)
|
|
115
|
+
const previous = entries.get(id)
|
|
116
|
+
if (previous !== undefined) disarm(previous)
|
|
117
|
+
const reporting = hides !== undefined && previous?.hides === hides ? previous.reporting : undefined
|
|
118
|
+
const entry: Entry = { id, type, message, options, actions, ...(hides === undefined ? {} : { hides }), isOpen: previous?.isOpen ?? false, reporting, timer: undefined }
|
|
119
|
+
if (type === 'error' && previous?.message !== message) claude.ui.log(`${name}: ${message}${typeof options.description === 'string' ? `: ${options.description}` : ''}`, { to: 'debug' })
|
|
120
|
+
entries.set(id, entry)
|
|
121
|
+
changed()
|
|
122
|
+
return id
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const show =
|
|
126
|
+
(type: ToastType) =>
|
|
127
|
+
(message: string, options: ToastOptions = {}): ToastId =>
|
|
128
|
+
put(type, message, options, options.action === undefined ? [] : [options.action])
|
|
129
|
+
|
|
130
|
+
const hide = (entry: Entry) => {
|
|
131
|
+
dismiss(entry.id)
|
|
132
|
+
if (entry.hides === undefined) return
|
|
133
|
+
hidden.set(String(entry.id), entry.hides)
|
|
134
|
+
void claude.store.set(hiddenKey, Object.fromEntries(hidden)).catch((error: unknown) => claude.ui.log(`${name}: could not remember the failure you hid: ${messageOf(error)}`, { to: 'debug' }))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const toast: Toast = Object.assign(show('default'), {
|
|
138
|
+
success: show('success'),
|
|
139
|
+
info: show('info'),
|
|
140
|
+
warning: show('warning'),
|
|
141
|
+
error: show('error'),
|
|
142
|
+
loading: show('loading'),
|
|
143
|
+
dismiss,
|
|
144
|
+
promise<T>(task: Promise<T> | (() => Promise<T>), messages: Parameters<Toast['promise']>[1]) {
|
|
145
|
+
const id = show('loading')(messages.loading)
|
|
146
|
+
const settled = Promise.resolve()
|
|
147
|
+
.then(() => (typeof task === 'function' ? task() : task))
|
|
148
|
+
.then(
|
|
149
|
+
(value) => {
|
|
150
|
+
show('success')(typeof messages.success === 'function' ? messages.success(value) : messages.success, { id })
|
|
151
|
+
return value
|
|
152
|
+
},
|
|
153
|
+
(error: unknown) => {
|
|
154
|
+
show('error')(typeof messages.error === 'function' ? messages.error(error) : messages.error, { id })
|
|
155
|
+
throw error
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
settled.catch(() => undefined)
|
|
159
|
+
return { unwrap: () => settled }
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const copy = async (text: string) => {
|
|
164
|
+
const copied = await claude.ui.copy({ text })
|
|
165
|
+
if (!copied.isCopied) throw new Error(copied.reason)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const press = (entry: Entry, run: () => unknown) => () =>
|
|
169
|
+
void Promise.resolve()
|
|
170
|
+
.then(run)
|
|
171
|
+
.catch((error: unknown) => fail({ id: `press:${entry.id}`, message: `${entry.message}: the press failed`, error }))
|
|
172
|
+
|
|
173
|
+
const fail = ({ id, message, error, description = error === undefined ? undefined : describe(error), retry }: Failure): ToastId => {
|
|
174
|
+
const hides = `${version() ?? ''}\n${message}\n${error === undefined ? '' : describe(error)}`
|
|
175
|
+
if (hidden.get(id) === hides) return id
|
|
176
|
+
hidden.delete(id)
|
|
177
|
+
const stack = error instanceof Error && error.stack !== undefined ? `\n\n${error.stack}` : ''
|
|
178
|
+
const details = async () => {
|
|
179
|
+
const modVersion = await claude.fs
|
|
180
|
+
.read(`${claude.plugin.root}/.claude-plugin/plugin.json`)
|
|
181
|
+
.then((text) => String((JSON.parse(text) as { version?: unknown }).version))
|
|
182
|
+
.catch((failure: unknown) => `unknown (${messageOf(failure)})`)
|
|
183
|
+
return `${name} ${modVersion}, @cmodjs/core ${coreVersion}\n${message}${description === undefined ? '' : `\n${description}`}${stack}`
|
|
184
|
+
}
|
|
185
|
+
const startReporting = async () => {
|
|
186
|
+
const shown = entries.get(id)
|
|
187
|
+
if (shown === undefined) return
|
|
188
|
+
shown.reporting = { contact: await contactOf(claude), details: await details() }
|
|
189
|
+
disarm(shown)
|
|
190
|
+
changed()
|
|
191
|
+
if (bandId === undefined) return
|
|
192
|
+
const focused = await claude.ui.focus({ requestId: bandId, key: `toast:${id}:ask-claude` })
|
|
193
|
+
if ('deny' in focused) claude.ui.log(`${name}: the Report choices did not take the keys: ${String(focused.deny)}`, { to: 'debug' })
|
|
194
|
+
}
|
|
195
|
+
const actions = [...(retry === undefined ? [] : [{ label: 'Retry', onClick: retry }]), { label: 'Report', onClick: startReporting }]
|
|
196
|
+
return put('error', message, { id, ...(description === undefined ? {} : { description }), duration: Infinity }, actions, hides)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const toggle = (entry: Entry) => () => {
|
|
200
|
+
entry.isOpen = !entry.isOpen
|
|
201
|
+
disarm(entry)
|
|
202
|
+
changed()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const row = (entry: Entry): RenderElement => {
|
|
206
|
+
const { description } = entry.options
|
|
207
|
+
const isText = typeof description === 'string'
|
|
208
|
+
const icon = entry.type === 'loading' ? { glyph: spinner[frame % spinner.length] as string, color: 'claude' } : icons[entry.type]
|
|
209
|
+
const message = isText ? Button({ plain: true, key: `toast:${entry.id}:open`, label: entry.message, onPress: toggle(entry) }) : Text({ children: entry.message })
|
|
210
|
+
const details = !isText ? description : Text({ dimColor: true, wrap: entry.isOpen ? 'wrap' : 'truncate-end', children: entry.isOpen ? description : firstLine(description) })
|
|
211
|
+
const fixed = (child: RenderElement) => Box({ flexShrink: 0, children: child })
|
|
212
|
+
const close = Button({ plain: true, dimColor: true, key: `toast:${entry.id}:close`, label: entry.hides === undefined ? '✕' : 'Hide', onPress: () => hide(entry) })
|
|
213
|
+
const actions = entry.actions.map((action, index) => Button({ plain: true, key: `toast:${entry.id}:${index === 0 ? 'action' : `action-${index}`}`, label: action.label, onPress: press(entry, action.onClick) }))
|
|
214
|
+
const text = details === undefined ? [message] : [message, details]
|
|
215
|
+
const under = entry.reporting === undefined ? [Box({ gap: 2, flexWrap: 'wrap', children: [...actions, close] })] : reportChoices(entry, entry.reporting)
|
|
216
|
+
return Box({
|
|
217
|
+
gap: 2,
|
|
218
|
+
children: [
|
|
219
|
+
fixed(Text({ ...(icon.color === undefined ? {} : { color: icon.color }), children: icon.glyph })),
|
|
220
|
+
fixed(Text({ dimColor: true, children: name })),
|
|
221
|
+
Box({ flexDirection: 'column', flexGrow: 1, flexShrink: 1, children: entry.hides === undefined ? text : [...text, ...under] }),
|
|
222
|
+
...(entry.hides === undefined ? [...actions.map(fixed), fixed(close)] : []),
|
|
223
|
+
],
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const reportChoices = (entry: Entry, { contact, details, status }: Reporting): RenderElement[] => {
|
|
228
|
+
const say = (said: string) => {
|
|
229
|
+
entry.reporting = { contact, details, status: said }
|
|
230
|
+
changed()
|
|
231
|
+
}
|
|
232
|
+
const back = () => {
|
|
233
|
+
entry.reporting = undefined
|
|
234
|
+
changed()
|
|
235
|
+
}
|
|
236
|
+
const askClaude = async () => {
|
|
237
|
+
await claude.prompt.submit({ text: reportPrompt(name, contact, details), asUser: true })
|
|
238
|
+
back()
|
|
239
|
+
}
|
|
240
|
+
const copyError = async () => {
|
|
241
|
+
const copied = await claude.ui.copy({ text: details })
|
|
242
|
+
say(copied.isCopied ? 'Copied the error.' : `Could not copy it: ${copied.reason}`)
|
|
243
|
+
}
|
|
244
|
+
const report = contact === undefined ? [] : [Link({ href: reportLink(contact, name, entry.message, details), label: contact.kind === 'github' ? 'Report it on GitHub (opens your browser)' : 'Email the author (opens your mail app)' })]
|
|
245
|
+
return [
|
|
246
|
+
Text({ dimColor: true, children: 'Claude writes up the problem and files it with your tools. You approve each step before anything is posted.' }),
|
|
247
|
+
Box({
|
|
248
|
+
gap: 2,
|
|
249
|
+
flexWrap: 'wrap',
|
|
250
|
+
children: [
|
|
251
|
+
Button({ plain: true, key: `toast:${entry.id}:ask-claude`, label: 'Ask Claude to report it', onPress: press(entry, askClaude) }),
|
|
252
|
+
...report,
|
|
253
|
+
Button({ plain: true, key: `toast:${entry.id}:copy`, label: 'Copy the error', onPress: press(entry, copyError) }),
|
|
254
|
+
Button({ plain: true, dimColor: true, key: `toast:${entry.id}:back`, label: 'Back', onPress: back }),
|
|
255
|
+
],
|
|
256
|
+
}),
|
|
257
|
+
...(status === undefined ? [] : [Text({ dimColor: true, children: status })]),
|
|
258
|
+
]
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
toast,
|
|
263
|
+
copy,
|
|
264
|
+
fail,
|
|
265
|
+
get shown() {
|
|
266
|
+
return [...entries.values()].map(({ type, message, options }) => ({ type, message, ...(typeof options.description === 'string' ? { description: options.description } : {}) }))
|
|
267
|
+
},
|
|
268
|
+
draw(drawn, table, band, drawnIn) {
|
|
269
|
+
bandId = drawnIn
|
|
270
|
+
isHeld = band.hasSurvey === true
|
|
271
|
+
if (isHeld || entries.size === 0) return drawn
|
|
272
|
+
const all = [...entries.values()]
|
|
273
|
+
const hidden = showsAll ? 0 : Math.max(all.length - visibleToasts, 0)
|
|
274
|
+
for (const entry of all.slice(hidden)) arm(entry)
|
|
275
|
+
const more = Button({
|
|
276
|
+
plain: true,
|
|
277
|
+
dimColor: true,
|
|
278
|
+
key: 'toasts:more',
|
|
279
|
+
label: `+${hidden} more`,
|
|
280
|
+
onPress: () => {
|
|
281
|
+
showsAll = true
|
|
282
|
+
changed()
|
|
283
|
+
},
|
|
284
|
+
})
|
|
285
|
+
return drawWith(table, () => Box({ flexDirection: 'column', children: [drawn, ...(hidden > 0 ? [more] : []), ...all.slice(hidden).map(row)] }))
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function describe(error: unknown): string {
|
|
291
|
+
const messages: string[] = []
|
|
292
|
+
for (let cause = error; cause !== undefined; cause = cause instanceof Error ? cause.cause : undefined) {
|
|
293
|
+
const text = messageOf(cause)
|
|
294
|
+
if (!messages.some((earlier) => earlier.includes(text) || text.includes(earlier))) messages.push(text)
|
|
295
|
+
}
|
|
296
|
+
return messages.join('\n')
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function contactOf(claude: Claude): Promise<Contact | undefined> {
|
|
300
|
+
const manifest: Record<string, unknown> = await claude.fs
|
|
301
|
+
.read(`${claude.plugin.root}/.claude-plugin/plugin.json`)
|
|
302
|
+
.then((text) => JSON.parse(text) as Record<string, unknown>)
|
|
303
|
+
.catch((error: unknown) => {
|
|
304
|
+
claude.ui.log(`${claude.plugin.name}: could not read whom to report to: ${messageOf(error)}`, { to: 'debug' })
|
|
305
|
+
return {}
|
|
306
|
+
})
|
|
307
|
+
const repositoryUrl = isObject(manifest['repository']) ? manifest['repository']['url'] : manifest['repository']
|
|
308
|
+
const repository = [manifest['supportUrl'], repositoryUrl].map((url) => (typeof url === 'string' ? githubRepository.exec(url)?.[1] : undefined)).find((found) => found !== undefined)
|
|
309
|
+
if (repository !== undefined) return { kind: 'github', repository }
|
|
310
|
+
const author = manifest['author']
|
|
311
|
+
const address = isObject(author) ? author['email'] : undefined
|
|
312
|
+
return typeof address === 'string' && address !== '' ? { kind: 'email', address } : undefined
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function reportLink(contact: Contact, name: string, message: string, details: string): string {
|
|
316
|
+
const title = `${name}: ${message}`.slice(0, 120)
|
|
317
|
+
const linkOf = (body: string) =>
|
|
318
|
+
contact.kind === 'github'
|
|
319
|
+
? `https://github.com/${contact.repository}/issues/new?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`
|
|
320
|
+
: `mailto:${contact.address}?subject=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`
|
|
321
|
+
let body = details
|
|
322
|
+
while (linkOf(body).length > linkLimit && body.length > 0) body = `${body.slice(0, Math.floor(body.length * 0.8))}…`
|
|
323
|
+
return linkOf(body)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function reportPrompt(name: string, contact: Contact | undefined, details: string): string {
|
|
327
|
+
const where =
|
|
328
|
+
contact === undefined
|
|
329
|
+
? `${name} names no place to report it, so find its author from its plugin.json or its marketplace.`
|
|
330
|
+
: contact.kind === 'github'
|
|
331
|
+
? `Report it as an issue on https://github.com/${contact.repository}.`
|
|
332
|
+
: `Email it to ${contact.address}.`
|
|
333
|
+
return `The ${name} mod failed. Report it to its author. ${where} First check whether it is reported already, and add to that report instead of filing a new one. Show me the report before you post it.\n\n${details}`
|
|
334
|
+
}
|
package/src/runtime/ui.ts
CHANGED
|
@@ -1,125 +1,19 @@
|
|
|
1
|
-
import type { ElementTable, MarkdownProps, PaneOpenArgs, RenderElement
|
|
2
|
-
import type { Mod, PaneHandle
|
|
3
|
-
import { ProgressBar } from '../ui/components.js'
|
|
1
|
+
import type { ElementTable, MarkdownProps, PaneOpenArgs, RenderElement } from 'claude-code'
|
|
2
|
+
import type { Disposable, Mod, PaneHandle } from '../mod.js'
|
|
4
3
|
import type { Pane } from '../ui/define-pane.js'
|
|
5
4
|
import { Box, drawWith, Text } from '../ui/elements.js'
|
|
6
|
-
import type {
|
|
7
|
-
import
|
|
5
|
+
import type { MarkdownReader } from '../ui/markdown.js'
|
|
6
|
+
import { componentOf, nameOf, type Place, type PlaceProps } from '../ui/places.js'
|
|
8
7
|
import { messageOf } from '../utils/text.js'
|
|
9
8
|
import type { Claude } from './claude.js'
|
|
10
9
|
import type { Router } from './router.js'
|
|
11
|
-
|
|
12
|
-
export type ProgressLine = {
|
|
13
|
-
report(step: ProgressStep): void
|
|
14
|
-
wait(text: string): void
|
|
15
|
-
fail(reason: string, fix: string): void
|
|
16
|
-
end(): void
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export type Progress = {
|
|
20
|
-
readonly isShown: boolean
|
|
21
|
-
start(title: string): ProgressLine
|
|
22
|
-
draw(drawn: RenderElement, table: ElementTable, columns: number | undefined): RenderElement
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
type Line = {
|
|
26
|
-
readonly title: string
|
|
27
|
-
step: ProgressStep | undefined
|
|
28
|
-
waiting: string | undefined
|
|
29
|
-
failure: { readonly reason: string; readonly fix: string } | undefined
|
|
30
|
-
}
|
|
10
|
+
import type { Failure, Toaster } from './toaster.js'
|
|
31
11
|
|
|
32
12
|
type PaneSize = Pick<PaneOpenArgs, 'columns' | 'rows'>
|
|
33
13
|
|
|
34
|
-
const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] as const
|
|
35
|
-
const spinnerMs = 100
|
|
36
14
|
const bullet = '⏺'
|
|
37
|
-
const widestBar = 30
|
|
38
|
-
const narrowestBar = 10
|
|
39
15
|
const sizeKeys = ['columns', 'rows'] as const
|
|
40
16
|
|
|
41
|
-
export function createProgress(claude: Claude): Progress {
|
|
42
|
-
const lines: Line[] = []
|
|
43
|
-
let frame = 0
|
|
44
|
-
let timer: Timer | undefined
|
|
45
|
-
|
|
46
|
-
const changed = () => {
|
|
47
|
-
const isSpinning = lines.some((line) => line.failure === undefined && line.waiting === undefined)
|
|
48
|
-
if (isSpinning && timer === undefined) {
|
|
49
|
-
timer = claude.clock.every(spinnerMs, () => {
|
|
50
|
-
frame += 1
|
|
51
|
-
claude.ui.invalidate('ui.render')
|
|
52
|
-
})
|
|
53
|
-
}
|
|
54
|
-
if (!isSpinning && timer !== undefined) {
|
|
55
|
-
timer.cancel()
|
|
56
|
-
timer = undefined
|
|
57
|
-
}
|
|
58
|
-
claude.ui.invalidate('ui.render')
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
get isShown() {
|
|
63
|
-
return lines.length > 0
|
|
64
|
-
},
|
|
65
|
-
start(title) {
|
|
66
|
-
const line: Line = { title, step: undefined, waiting: undefined, failure: undefined }
|
|
67
|
-
lines.push(line)
|
|
68
|
-
changed()
|
|
69
|
-
return {
|
|
70
|
-
report(step) {
|
|
71
|
-
line.step = step
|
|
72
|
-
line.waiting = undefined
|
|
73
|
-
changed()
|
|
74
|
-
},
|
|
75
|
-
wait(text) {
|
|
76
|
-
line.waiting = text
|
|
77
|
-
changed()
|
|
78
|
-
},
|
|
79
|
-
fail(reason, fix) {
|
|
80
|
-
line.failure = { reason, fix }
|
|
81
|
-
changed()
|
|
82
|
-
},
|
|
83
|
-
end() {
|
|
84
|
-
const index = lines.indexOf(line)
|
|
85
|
-
if (index >= 0) lines.splice(index, 1)
|
|
86
|
-
changed()
|
|
87
|
-
},
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
|
-
draw(drawn, table, columns) {
|
|
91
|
-
return drawWith(table, () => Box({ flexDirection: 'column', children: [drawn, ...lines.map((line) => drawLine(line, frame, columns))] }))
|
|
92
|
-
},
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function drawLine(line: Line, frame: number, columns: number | undefined): RenderElement {
|
|
97
|
-
const title = Text({ bold: true, children: line.title })
|
|
98
|
-
if (line.failure !== undefined) {
|
|
99
|
-
return Box({
|
|
100
|
-
flexDirection: 'column',
|
|
101
|
-
children: [
|
|
102
|
-
Box({ children: [Box({ minWidth: 2, children: Text({ color: 'error', children: '✗' }) }), Text({ children: [title, ' ', Text({ color: 'error', children: line.failure.reason })] })] }),
|
|
103
|
-
Box({ paddingLeft: 2, children: Text({ dimColor: true, children: line.failure.fix }) }),
|
|
104
|
-
],
|
|
105
|
-
})
|
|
106
|
-
}
|
|
107
|
-
if (line.waiting !== undefined) {
|
|
108
|
-
return Text({ wrap: 'truncate-end', children: [Text({ color: 'warning', children: '◌' }), ' ', title, ' ', Text({ dimColor: true, children: line.waiting })] })
|
|
109
|
-
}
|
|
110
|
-
const glyph = Text({ color: 'claude', children: spinner[frame % spinner.length] })
|
|
111
|
-
if (line.step === undefined) return Text({ wrap: 'truncate-end', children: [glyph, ' ', title, Text({ dimColor: true, children: '…' })] })
|
|
112
|
-
const { done, total, label = '' } = line.step
|
|
113
|
-
const width = barWidth(columns, `${line.title}${done}/${total}${label}`.length)
|
|
114
|
-
return Text({ wrap: 'truncate-end', children: [glyph, ' ', title, ' ', ProgressBar({ done, total, width }), ...(label === '' ? [] : [' ', label])] })
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function barWidth(columns: number | undefined, textLength: number): number {
|
|
118
|
-
if (columns === undefined) return widestBar
|
|
119
|
-
const spacing = 2 + 2 + 2 + 2
|
|
120
|
-
return Math.max(narrowestBar, Math.min(widestBar, columns - textLength - spacing))
|
|
121
|
-
}
|
|
122
|
-
|
|
123
17
|
function paneSize<State extends object>(pane: Pane<State>, state: State): PaneSize {
|
|
124
18
|
const size: { columns?: number; rows?: number } = {}
|
|
125
19
|
for (const key of sizeKeys) {
|
|
@@ -197,21 +91,31 @@ export type UiOptions<State extends object> = {
|
|
|
197
91
|
readonly name: string
|
|
198
92
|
readonly claude: Claude
|
|
199
93
|
readonly router: Router
|
|
200
|
-
readonly
|
|
94
|
+
readonly toaster: Toaster
|
|
95
|
+
readonly report: (failure: Failure) => void
|
|
201
96
|
readonly announce: (feature: string) => void
|
|
202
97
|
readonly mod: () => Mod<State>
|
|
203
98
|
}
|
|
204
99
|
|
|
205
100
|
export type UiArea<State extends object> = {
|
|
206
|
-
readonly ui: Mod<State>['ui']
|
|
101
|
+
readonly ui: Omit<Mod<State>['ui'], 'notice'>
|
|
207
102
|
changed(): void
|
|
208
103
|
restorePanes(): Promise<void>
|
|
209
104
|
}
|
|
210
105
|
|
|
211
|
-
export function createUi<State extends object>({ name, claude, router,
|
|
106
|
+
export function createUi<State extends object>({ name, claude, router, toaster, report, announce, mod }: UiOptions<State>): UiArea<State> {
|
|
212
107
|
const panes = new Map<string, Pane<State>>()
|
|
213
108
|
const paneLogs = new Map<string, (failure: string) => void>()
|
|
214
109
|
const openPanes = new Set<string>()
|
|
110
|
+
const paneStates = new Map<string, { readonly isShown: boolean; readonly isFocused: boolean }>()
|
|
111
|
+
const syncPanes = async () => {
|
|
112
|
+
for (const open of await claude.ui.panes()) {
|
|
113
|
+
if (!panes.has(open.id)) continue
|
|
114
|
+
if (open.isPlaced) openPanes.add(open.id)
|
|
115
|
+
paneStates.set(open.id, { isShown: open.isShown, isFocused: open.isFocused })
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const readPanes = () => void syncPanes().catch((error: unknown) => claude.ui.log(`${name} keeps its panes' shown and focused state from before: ${messageOf(error)}`, { to: 'debug' }))
|
|
215
119
|
const requestedSizes = new Map<string, PaneSize>()
|
|
216
120
|
const renders = new Set<string>()
|
|
217
121
|
const markdownRenders = new Map<string, MarkdownRender>()
|
|
@@ -222,7 +126,7 @@ export function createUi<State extends object>({ name, claude, router, progress,
|
|
|
222
126
|
const logOnce = (what: string) => {
|
|
223
127
|
let hasLogged = false
|
|
224
128
|
return (failure: string) => {
|
|
225
|
-
if (!hasLogged)
|
|
129
|
+
if (!hasLogged) report({ id: `render:${what}`, message: `The ${what} render failed`, description: failure })
|
|
226
130
|
hasLogged = true
|
|
227
131
|
}
|
|
228
132
|
}
|
|
@@ -250,9 +154,9 @@ export function createUi<State extends object>({ name, claude, router, progress,
|
|
|
250
154
|
}
|
|
251
155
|
}
|
|
252
156
|
|
|
253
|
-
const routeMarkdown = (
|
|
157
|
+
const routeMarkdown = (createReader: () => MarkdownReader) => {
|
|
254
158
|
if (read !== undefined) return
|
|
255
|
-
const reader =
|
|
159
|
+
const reader = createReader()
|
|
256
160
|
read = reader
|
|
257
161
|
router.add('ui.render', async (e, next) => {
|
|
258
162
|
if (e.component !== 'AssistantMessage') return next(e)
|
|
@@ -302,6 +206,8 @@ export function createUi<State extends object>({ name, claude, router, progress,
|
|
|
302
206
|
const pane = panes.get(e.requestId)
|
|
303
207
|
if (pane === undefined || e.component !== 'Pane') return next(e)
|
|
304
208
|
openPanes.add(pane.id)
|
|
209
|
+
paneStates.set(pane.id, { isShown: true, isFocused: e.props.isFocused })
|
|
210
|
+
readPanes()
|
|
305
211
|
const table = claude.ui.resolve(e)
|
|
306
212
|
try {
|
|
307
213
|
return drawWith(table, () => pane.render(mod(), e.props), markdownIn(table))
|
|
@@ -319,6 +225,8 @@ export function createUi<State extends object>({ name, claude, router, progress,
|
|
|
319
225
|
})
|
|
320
226
|
router.add('ui.close', async (e, next) => {
|
|
321
227
|
openPanes.delete(e.id)
|
|
228
|
+
paneStates.delete(e.id)
|
|
229
|
+
readPanes()
|
|
322
230
|
const closed = await next(e)
|
|
323
231
|
const pane = panes.get(e.id)
|
|
324
232
|
if (pane?.onClose !== undefined) await handle(pane, 'onClose', () => pane.onClose?.(mod(), e))
|
|
@@ -330,7 +238,7 @@ export function createUi<State extends object>({ name, claude, router, progress,
|
|
|
330
238
|
try {
|
|
331
239
|
await run()
|
|
332
240
|
} catch (error) {
|
|
333
|
-
|
|
241
|
+
report({ id: `pane:${pane.id}:${handler}`, message: `The ${pane.title} pane's ${handler} threw`, error })
|
|
334
242
|
}
|
|
335
243
|
}
|
|
336
244
|
|
|
@@ -346,38 +254,55 @@ export function createUi<State extends object>({ name, claude, router, progress,
|
|
|
346
254
|
get isOpen() {
|
|
347
255
|
return openPanes.has(pane.id)
|
|
348
256
|
},
|
|
257
|
+
get isShown() {
|
|
258
|
+
return paneStates.get(pane.id)?.isShown ?? false
|
|
259
|
+
},
|
|
260
|
+
get isFocused() {
|
|
261
|
+
return paneStates.get(pane.id)?.isFocused ?? false
|
|
262
|
+
},
|
|
349
263
|
async open(options) {
|
|
350
264
|
const { isPlaced } = await openAt(pane, paneSize(pane, mod().state), options?.focus)
|
|
351
265
|
if (isPlaced) openPanes.add(pane.id)
|
|
266
|
+
await syncPanes()
|
|
352
267
|
},
|
|
353
268
|
async close() {
|
|
354
269
|
await claude.ui.close({ id: pane.id })
|
|
355
270
|
openPanes.delete(pane.id)
|
|
271
|
+
paneStates.delete(pane.id)
|
|
272
|
+
await syncPanes()
|
|
356
273
|
},
|
|
357
274
|
toggle: (options) => (openPanes.has(pane.id) ? handle.close() : handle.open(options)),
|
|
358
275
|
}
|
|
359
276
|
return handle
|
|
360
277
|
},
|
|
361
|
-
render<
|
|
362
|
-
const
|
|
363
|
-
const
|
|
364
|
-
|
|
278
|
+
render<P extends Place>(place: P, Component: (props: PlaceProps<P>) => RenderElement): Disposable {
|
|
279
|
+
const target: Place = place
|
|
280
|
+
const markdown = typeof target === 'string' ? undefined : target
|
|
281
|
+
const component = componentOf(place)
|
|
282
|
+
if (markdown === undefined && component === undefined) throw new Error(`${name}: ${String(place)} is not a place render draws, such as ToolCall or CodeBlock.`)
|
|
283
|
+
const feature = `a render of ${nameOf(place)}`
|
|
284
|
+
if (renders.has(feature)) throw new Error(`${name}: ${feature} is already added. Render each place once.`)
|
|
365
285
|
renders.add(feature)
|
|
366
286
|
announce(feature)
|
|
367
|
-
if (
|
|
368
|
-
const
|
|
369
|
-
markdownRenders.set(
|
|
370
|
-
routeMarkdown(
|
|
371
|
-
return
|
|
287
|
+
if (markdown !== undefined) {
|
|
288
|
+
const markdownType = markdown.markdown
|
|
289
|
+
markdownRenders.set(markdownType, { Component: Component as unknown as MarkdownRender['Component'], log: logOnce(nameOf(place)) })
|
|
290
|
+
routeMarkdown(markdown.createReader)
|
|
291
|
+
return {
|
|
292
|
+
dispose() {
|
|
293
|
+
markdownRenders.delete(markdownType)
|
|
294
|
+
renders.delete(feature)
|
|
295
|
+
claude.ui.invalidate('ui.render')
|
|
296
|
+
},
|
|
297
|
+
}
|
|
372
298
|
}
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
router.add('ui.render', async (e, next) => {
|
|
299
|
+
const log = logOnce(nameOf(place))
|
|
300
|
+
const drawing = router.add('ui.render', async (e, next) => {
|
|
376
301
|
if (e.component !== component) return next(e)
|
|
377
302
|
const table = claude.ui.resolve(e)
|
|
378
303
|
const drawn = await drawComponent(
|
|
379
304
|
table,
|
|
380
|
-
(Default) => Component({ ...e.props, Default } as unknown as
|
|
305
|
+
(Default) => Component({ ...e.props, Default } as unknown as PlaceProps<P>),
|
|
381
306
|
(given, isClaudesRow) => next({ ...e, props: pieceProps(e.props, given, isClaudesRow) } as typeof e),
|
|
382
307
|
)
|
|
383
308
|
if ('failure' in drawn) {
|
|
@@ -387,16 +312,16 @@ export function createUi<State extends object>({ name, claude, router, progress,
|
|
|
387
312
|
if (e.component !== 'AssistantMessage') return drawn.drawing
|
|
388
313
|
return drawWith(table, () => drawPieces([drawn], e.props.isFirstOfReply ? 'opensReply' : 'inReply'))
|
|
389
314
|
})
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
} finally {
|
|
397
|
-
line.end()
|
|
315
|
+
return {
|
|
316
|
+
dispose() {
|
|
317
|
+
drawing.dispose()
|
|
318
|
+
renders.delete(feature)
|
|
319
|
+
claude.ui.invalidate('ui.render')
|
|
320
|
+
},
|
|
398
321
|
}
|
|
399
322
|
},
|
|
323
|
+
toast: toaster.toast,
|
|
324
|
+
copy: toaster.copy,
|
|
400
325
|
ask: (question, options) => claude.ui.ask(question, options),
|
|
401
326
|
scroll: (args) => claude.ui.scroll(args),
|
|
402
327
|
},
|
|
@@ -413,10 +338,7 @@ export function createUi<State extends object>({ name, claude, router, progress,
|
|
|
413
338
|
})
|
|
414
339
|
},
|
|
415
340
|
async restorePanes() {
|
|
416
|
-
if (panes.size
|
|
417
|
-
for (const open of await claude.ui.panes()) {
|
|
418
|
-
if (open.isPlaced && panes.has(open.id)) openPanes.add(open.id)
|
|
419
|
-
}
|
|
341
|
+
if (panes.size > 0) await syncPanes()
|
|
420
342
|
},
|
|
421
343
|
}
|
|
422
344
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RecordPart } from '../session.js'
|
|
2
|
+
import type { Turn } from './turn/turn.js'
|
|
3
|
+
|
|
4
|
+
export const ContextStart = 'contextStart'
|
|
5
|
+
|
|
6
|
+
export const ContextEnd = 'contextEnd'
|
|
7
|
+
|
|
8
|
+
export type Context = RecordPart & {
|
|
9
|
+
readonly number: number
|
|
10
|
+
readonly openedAt: string
|
|
11
|
+
readonly closedAt?: string
|
|
12
|
+
readonly task?: string
|
|
13
|
+
turns(): Promise<Turn[]>
|
|
14
|
+
}
|