@cmodjs/core 0.2.3 → 1.0.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 +58 -10
- package/docs/dependencies.md +12 -12
- package/docs/hooks.md +41 -28
- package/docs/index.md +29 -25
- package/docs/install-steps.md +9 -7
- package/docs/jobs.md +59 -111
- package/docs/mod.md +200 -174
- package/docs/options.md +2 -2
- package/docs/permissions.md +27 -16
- package/docs/session.md +248 -0
- package/docs/state.md +11 -7
- package/docs/testing.md +66 -23
- package/docs/ui.md +108 -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 +8 -3
- 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 +154 -75
- package/mod.d.ts.map +1 -1
- package/mod.js +94 -3
- package/options.d.ts +2 -2
- package/options.js +19 -19
- package/package.json +3 -3
- package/records.d.ts +10 -8
- package/records.d.ts.map +1 -1
- package/records.js +28 -17
- package/register.d.ts.map +1 -1
- package/register.js +18 -6
- package/runtime/announce.d.ts +18 -0
- package/runtime/announce.d.ts.map +1 -0
- package/runtime/announce.js +66 -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 +3 -3
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +16 -0
- package/runtime/conversations.d.ts.map +1 -0
- package/runtime/conversations.js +291 -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 +9 -0
- package/runtime/engine.d.ts +17 -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 +63 -33
- package/runtime/hooks.d.ts +5 -4
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +10 -5
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +11 -9
- package/runtime/lifecycle.d.ts +3 -1
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +317 -186
- package/runtime/metadata.d.ts +3 -1
- package/runtime/metadata.d.ts.map +1 -1
- package/runtime/metadata.js +21 -11
- 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 +36 -0
- package/runtime/sessions.d.ts.map +1 -0
- package/runtime/sessions.js +235 -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 +31 -0
- package/runtime/toaster.d.ts.map +1 -0
- package/runtime/toaster.js +176 -0
- package/runtime/ui.d.ts +6 -17
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +36 -112
- package/session/context/context.d.ts +10 -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 +54 -0
- package/session/context/turn/message/message.d.ts.map +1 -0
- package/session/context/turn/message/message.js +137 -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 +12 -0
- package/session/context/turn/turn.d.ts.map +1 -0
- package/session/context/turn/turn.js +2 -0
- package/session/conversation.d.ts +64 -0
- package/session/conversation.d.ts.map +1 -0
- package/session/conversation.js +123 -0
- package/session/goal/goal.d.ts +10 -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 +99 -0
- package/session/session.d.ts.map +1 -0
- package/session/session.js +1 -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 +7 -3
- 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 +209 -97
- package/src/options.ts +22 -22
- package/src/records.ts +35 -23
- package/src/register.ts +18 -6
- package/src/runtime/announce.ts +74 -0
- package/src/runtime/bound.ts +12 -0
- package/src/runtime/claude.ts +5 -3
- package/src/runtime/conversations.ts +302 -0
- package/src/runtime/deadline.ts +2 -2
- package/src/runtime/dependencies.ts +12 -0
- package/src/runtime/engine.ts +22 -0
- package/src/runtime/grants.ts +66 -31
- package/src/runtime/hooks.ts +19 -10
- package/src/runtime/installer.ts +10 -9
- package/src/runtime/lifecycle.ts +313 -181
- package/src/runtime/metadata.ts +33 -11
- 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 +242 -0
- package/src/runtime/state.ts +50 -14
- package/src/runtime/toaster.ts +218 -0
- package/src/runtime/ui.ts +42 -139
- package/src/session/context/context.ts +12 -0
- package/src/session/context/turn/message/document.ts +8 -0
- package/src/session/context/turn/message/message.ts +165 -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 +14 -0
- package/src/session/conversation.ts +169 -0
- package/src/session/goal/goal.ts +9 -0
- package/src/session/goal/task.ts +19 -0
- package/src/session/session.ts +94 -0
- package/src/testing/fake-claude.ts +23 -18
- package/src/testing/fake-elements.ts +3 -2
- package/src/testing/fake-files.ts +1 -1
- package/src/testing/fake-sessions.ts +203 -0
- package/src/testing.ts +112 -33
- 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 +3 -3
- package/src/utils/changelog.ts +32 -0
- package/src/utils/matcher.ts +37 -0
- package/src/utils/metadata.ts +17 -15
- package/src/utils/parse-shell.ts +24 -16
- package/src/utils/paths.ts +15 -1
- package/src/utils/text.ts +4 -0
- package/testing/fake-claude.d.ts +4 -2
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +18 -12
- package/testing/fake-elements.js +4 -2
- package/testing/fake-files.js +1 -1
- package/testing/fake-sessions.d.ts +74 -0
- package/testing/fake-sessions.d.ts.map +1 -0
- package/testing/fake-sessions.js +170 -0
- package/testing.d.ts +33 -13
- package/testing.d.ts.map +1 -1
- package/testing.js +92 -21
- 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 +1 -1
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +2 -2
- package/utils/changelog.d.ts +9 -0
- package/utils/changelog.d.ts.map +1 -0
- package/utils/changelog.js +29 -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.d.ts +1 -0
- package/utils/parse-shell.d.ts.map +1 -1
- package/utils/parse-shell.js +22 -13
- package/utils/paths.d.ts +2 -0
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +13 -1
- 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,218 @@
|
|
|
1
|
+
import type { ElementTable, RenderElement, RenderPropsOf, Timer } from 'claude-code'
|
|
2
|
+
import type { Mod, Toast, ToastId, ToastOptions, ToastType } from '../mod.js'
|
|
3
|
+
import { Box, Button, drawWith, Text } from '../ui/elements.js'
|
|
4
|
+
import { coreVersion } 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 }
|
|
11
|
+
|
|
12
|
+
export type Toaster = {
|
|
13
|
+
readonly toast: Toast
|
|
14
|
+
copy(text: string): Promise<void>
|
|
15
|
+
readonly shown: readonly ShownToast[]
|
|
16
|
+
draw(drawn: RenderElement, table: ElementTable, band: RenderPropsOf['AbovePrompt']): RenderElement
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type Entry = {
|
|
20
|
+
readonly id: ToastId
|
|
21
|
+
readonly type: ToastType
|
|
22
|
+
readonly message: string
|
|
23
|
+
readonly options: ToastOptions
|
|
24
|
+
isOpen: boolean
|
|
25
|
+
timer: Timer | undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const defaultDuration = 4000
|
|
29
|
+
const visibleToasts = 3
|
|
30
|
+
const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] as const
|
|
31
|
+
const spinnerMs = 100
|
|
32
|
+
const icons: Record<Exclude<ToastType, 'loading'>, { readonly glyph: string; readonly color?: string }> = {
|
|
33
|
+
default: { glyph: ' ' },
|
|
34
|
+
success: { glyph: '✓', color: 'success' },
|
|
35
|
+
info: { glyph: 'ℹ', color: 'suggestion' },
|
|
36
|
+
warning: { glyph: '⚠', color: 'warning' },
|
|
37
|
+
error: { glyph: '✗', color: 'error' },
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createToaster(claude: Claude, name: string): Toaster {
|
|
41
|
+
const entries = new Map<ToastId, Entry>()
|
|
42
|
+
let lastId = 0
|
|
43
|
+
let frame = 0
|
|
44
|
+
let spinning: Timer | undefined
|
|
45
|
+
let isHeld = false
|
|
46
|
+
let showsAll = false
|
|
47
|
+
|
|
48
|
+
const changed = () => {
|
|
49
|
+
const isLoading = [...entries.values()].some((entry) => entry.type === 'loading')
|
|
50
|
+
if (isLoading && spinning === undefined) {
|
|
51
|
+
spinning = claude.clock.every(spinnerMs, () => {
|
|
52
|
+
frame += 1
|
|
53
|
+
claude.ui.invalidate('ui.render')
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
if (!isLoading && spinning !== undefined) {
|
|
57
|
+
spinning.cancel()
|
|
58
|
+
spinning = undefined
|
|
59
|
+
}
|
|
60
|
+
if (entries.size <= visibleToasts) showsAll = false
|
|
61
|
+
claude.ui.invalidate('ui.render')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const disarm = (entry: Entry) => {
|
|
65
|
+
entry.timer?.cancel()
|
|
66
|
+
entry.timer = undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const arm = (entry: Entry) => {
|
|
70
|
+
const duration = entry.options.duration ?? defaultDuration
|
|
71
|
+
if (entry.timer !== undefined || entry.type === 'loading' || entry.isOpen || !Number.isFinite(duration)) return
|
|
72
|
+
entry.timer = claude.clock.after(duration, () => {
|
|
73
|
+
entry.timer = undefined
|
|
74
|
+
if (isHeld) arm(entry)
|
|
75
|
+
else dismiss(entry.id)
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const dismiss = (id?: ToastId) => {
|
|
80
|
+
for (const entry of entries.values()) {
|
|
81
|
+
if (id !== undefined && entry.id !== id) continue
|
|
82
|
+
disarm(entry)
|
|
83
|
+
entries.delete(entry.id)
|
|
84
|
+
}
|
|
85
|
+
changed()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const show =
|
|
89
|
+
(type: ToastType) =>
|
|
90
|
+
(message: string, options: ToastOptions = {}): ToastId => {
|
|
91
|
+
const id = options.id ?? (lastId += 1)
|
|
92
|
+
const previous = entries.get(id)
|
|
93
|
+
if (previous !== undefined) disarm(previous)
|
|
94
|
+
const entry: Entry = { id, type, message, options, isOpen: previous?.isOpen ?? false, timer: undefined }
|
|
95
|
+
if (type === 'error' && previous?.message !== message) claude.ui.log(`${name}: ${message}${typeof options.description === 'string' ? `: ${options.description}` : ''}`, { to: 'debug' })
|
|
96
|
+
entries.set(id, entry)
|
|
97
|
+
changed()
|
|
98
|
+
return id
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const toast: Toast = Object.assign(show('default'), {
|
|
102
|
+
success: show('success'),
|
|
103
|
+
info: show('info'),
|
|
104
|
+
warning: show('warning'),
|
|
105
|
+
error: show('error'),
|
|
106
|
+
loading: show('loading'),
|
|
107
|
+
dismiss,
|
|
108
|
+
promise<T>(task: Promise<T> | (() => Promise<T>), messages: Parameters<Toast['promise']>[1]) {
|
|
109
|
+
const id = show('loading')(messages.loading)
|
|
110
|
+
const settled = Promise.resolve()
|
|
111
|
+
.then(() => (typeof task === 'function' ? task() : task))
|
|
112
|
+
.then(
|
|
113
|
+
(value) => {
|
|
114
|
+
show('success')(typeof messages.success === 'function' ? messages.success(value) : messages.success, { id })
|
|
115
|
+
return value
|
|
116
|
+
},
|
|
117
|
+
(error: unknown) => {
|
|
118
|
+
show('error')(typeof messages.error === 'function' ? messages.error(error) : messages.error, { id })
|
|
119
|
+
throw error
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
settled.catch(() => undefined)
|
|
123
|
+
return { unwrap: () => settled }
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const copy = async (text: string) => {
|
|
128
|
+
const copied = await claude.ui.copy({ text })
|
|
129
|
+
if (!copied.isCopied) throw new Error(copied.reason)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const press = (entry: Entry, run: () => unknown) => () =>
|
|
133
|
+
void Promise.resolve()
|
|
134
|
+
.then(run)
|
|
135
|
+
.catch((error: unknown) => showFailure({ name, claude, ui: { toast, copy } }, { id: `press:${entry.id}`, message: `${entry.message}: the press failed`, error }))
|
|
136
|
+
|
|
137
|
+
const toggle = (entry: Entry) => () => {
|
|
138
|
+
entry.isOpen = !entry.isOpen
|
|
139
|
+
disarm(entry)
|
|
140
|
+
changed()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const row = (entry: Entry): RenderElement => {
|
|
144
|
+
const { description, action } = entry.options
|
|
145
|
+
const isText = typeof description === 'string'
|
|
146
|
+
const icon = entry.type === 'loading' ? { glyph: spinner[frame % spinner.length] as string, color: 'claude' } : icons[entry.type]
|
|
147
|
+
const message = isText ? Button({ plain: true, key: `toast:${entry.id}:open`, label: entry.message, onPress: toggle(entry) }) : Text({ children: entry.message })
|
|
148
|
+
const details = !isText ? description : Text({ dimColor: true, wrap: entry.isOpen ? 'wrap' : 'truncate-end', children: entry.isOpen ? description : firstLine(description) })
|
|
149
|
+
const fixed = (child: RenderElement) => Box({ flexShrink: 0, children: child })
|
|
150
|
+
return Box({
|
|
151
|
+
gap: 2,
|
|
152
|
+
children: [
|
|
153
|
+
fixed(Text({ ...(icon.color === undefined ? {} : { color: icon.color }), children: icon.glyph })),
|
|
154
|
+
fixed(Text({ dimColor: true, children: name })),
|
|
155
|
+
Box({ flexDirection: 'column', flexGrow: 1, flexShrink: 1, children: details === undefined ? [message] : [message, details] }),
|
|
156
|
+
...(action === undefined ? [] : [fixed(Button({ plain: true, key: `toast:${entry.id}:action`, label: action.label, onPress: press(entry, action.onClick) }))]),
|
|
157
|
+
fixed(Button({ plain: true, dimColor: true, key: `toast:${entry.id}:close`, label: '✕', onPress: () => dismiss(entry.id) })),
|
|
158
|
+
],
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
toast,
|
|
164
|
+
copy,
|
|
165
|
+
get shown() {
|
|
166
|
+
return [...entries.values()].map(({ type, message, options }) => ({ type, message, ...(typeof options.description === 'string' ? { description: options.description } : {}) }))
|
|
167
|
+
},
|
|
168
|
+
draw(drawn, table, band) {
|
|
169
|
+
isHeld = band.hasSurvey === true
|
|
170
|
+
if (isHeld || entries.size === 0) return drawn
|
|
171
|
+
const all = [...entries.values()]
|
|
172
|
+
const hidden = showsAll ? 0 : Math.max(all.length - visibleToasts, 0)
|
|
173
|
+
for (const entry of all.slice(hidden)) arm(entry)
|
|
174
|
+
const more = Button({
|
|
175
|
+
plain: true,
|
|
176
|
+
dimColor: true,
|
|
177
|
+
key: 'toasts:more',
|
|
178
|
+
label: `+${hidden} more`,
|
|
179
|
+
onPress: () => {
|
|
180
|
+
showsAll = true
|
|
181
|
+
changed()
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
return drawWith(table, () => Box({ flexDirection: 'column', children: [drawn, ...(hidden > 0 ? [more] : []), ...all.slice(hidden).map(row)] }))
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export type FailureTarget = {
|
|
190
|
+
readonly name: string
|
|
191
|
+
readonly ui: Pick<Mod['ui'], 'toast' | 'copy'>
|
|
192
|
+
readonly claude: { readonly plugin: Claude['plugin']; readonly fs: Pick<Claude['fs'], 'read'> }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function showFailure({ name, ui, claude }: FailureTarget, { id, message, error, description = describe(error) }: Failure): ToastId {
|
|
196
|
+
const stack = error instanceof Error && error.stack !== undefined ? `\n\n${error.stack}` : ''
|
|
197
|
+
const show = (label: string): ToastId => ui.toast.error(message, { id, description, duration: Infinity, action: { label, onClick: copy } })
|
|
198
|
+
const copy = async (): Promise<ToastId> => {
|
|
199
|
+
const modVersion = await claude.fs
|
|
200
|
+
.read(`${claude.plugin.root}/.claude-plugin/plugin.json`)
|
|
201
|
+
.then((text) => String((JSON.parse(text) as { version?: unknown }).version))
|
|
202
|
+
.catch((failure: unknown) => `unknown (${messageOf(failure)})`)
|
|
203
|
+
return ui.copy(`${name} ${modVersion}, @cmodjs/core ${coreVersion}\n${message}\n${description}${stack}`).then(
|
|
204
|
+
() => show('Copied'),
|
|
205
|
+
(failure: unknown) => show(`Copy failed: ${messageOf(failure)}`),
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
return show('Copy')
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function describe(error: unknown): string {
|
|
212
|
+
const messages: string[] = []
|
|
213
|
+
for (let cause = error; cause !== undefined; cause = cause instanceof Error ? cause.cause : undefined) {
|
|
214
|
+
const text = messageOf(cause)
|
|
215
|
+
if (!messages.some((earlier) => earlier.includes(text) || text.includes(earlier))) messages.push(text)
|
|
216
|
+
}
|
|
217
|
+
return messages.join('\n')
|
|
218
|
+
}
|
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,18 +91,19 @@ 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>()
|
|
@@ -222,7 +117,7 @@ export function createUi<State extends object>({ name, claude, router, progress,
|
|
|
222
117
|
const logOnce = (what: string) => {
|
|
223
118
|
let hasLogged = false
|
|
224
119
|
return (failure: string) => {
|
|
225
|
-
if (!hasLogged)
|
|
120
|
+
if (!hasLogged) report({ id: `render:${what}`, message: `The ${what} render failed`, description: failure })
|
|
226
121
|
hasLogged = true
|
|
227
122
|
}
|
|
228
123
|
}
|
|
@@ -250,9 +145,9 @@ export function createUi<State extends object>({ name, claude, router, progress,
|
|
|
250
145
|
}
|
|
251
146
|
}
|
|
252
147
|
|
|
253
|
-
const routeMarkdown = (
|
|
148
|
+
const routeMarkdown = (createReader: () => MarkdownReader) => {
|
|
254
149
|
if (read !== undefined) return
|
|
255
|
-
const reader =
|
|
150
|
+
const reader = createReader()
|
|
256
151
|
read = reader
|
|
257
152
|
router.add('ui.render', async (e, next) => {
|
|
258
153
|
if (e.component !== 'AssistantMessage') return next(e)
|
|
@@ -330,7 +225,7 @@ export function createUi<State extends object>({ name, claude, router, progress,
|
|
|
330
225
|
try {
|
|
331
226
|
await run()
|
|
332
227
|
} catch (error) {
|
|
333
|
-
|
|
228
|
+
report({ id: `pane:${pane.id}:${handler}`, message: `The ${pane.title} pane's ${handler} threw`, error })
|
|
334
229
|
}
|
|
335
230
|
}
|
|
336
231
|
|
|
@@ -358,26 +253,34 @@ export function createUi<State extends object>({ name, claude, router, progress,
|
|
|
358
253
|
}
|
|
359
254
|
return handle
|
|
360
255
|
},
|
|
361
|
-
render<
|
|
362
|
-
const
|
|
363
|
-
const
|
|
364
|
-
|
|
256
|
+
render<P extends Place>(place: P, Component: (props: PlaceProps<P>) => RenderElement): Disposable {
|
|
257
|
+
const target: Place = place
|
|
258
|
+
const markdown = typeof target === 'string' ? undefined : target
|
|
259
|
+
const component = componentOf(place)
|
|
260
|
+
if (markdown === undefined && component === undefined) throw new Error(`${name}: ${String(place)} is not a place render draws, such as ToolCall or CodeBlock.`)
|
|
261
|
+
const feature = `a render of ${nameOf(place)}`
|
|
262
|
+
if (renders.has(feature)) throw new Error(`${name}: ${feature} is already added. Render each place once.`)
|
|
365
263
|
renders.add(feature)
|
|
366
264
|
announce(feature)
|
|
367
|
-
if (
|
|
368
|
-
const
|
|
369
|
-
markdownRenders.set(
|
|
370
|
-
routeMarkdown(
|
|
371
|
-
return
|
|
265
|
+
if (markdown !== undefined) {
|
|
266
|
+
const markdownType = markdown.markdown
|
|
267
|
+
markdownRenders.set(markdownType, { Component: Component as unknown as MarkdownRender['Component'], log: logOnce(nameOf(place)) })
|
|
268
|
+
routeMarkdown(markdown.createReader)
|
|
269
|
+
return {
|
|
270
|
+
dispose() {
|
|
271
|
+
markdownRenders.delete(markdownType)
|
|
272
|
+
renders.delete(feature)
|
|
273
|
+
claude.ui.invalidate('ui.render')
|
|
274
|
+
},
|
|
275
|
+
}
|
|
372
276
|
}
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
router.add('ui.render', async (e, next) => {
|
|
277
|
+
const log = logOnce(nameOf(place))
|
|
278
|
+
const drawing = router.add('ui.render', async (e, next) => {
|
|
376
279
|
if (e.component !== component) return next(e)
|
|
377
280
|
const table = claude.ui.resolve(e)
|
|
378
281
|
const drawn = await drawComponent(
|
|
379
282
|
table,
|
|
380
|
-
(Default) => Component({ ...e.props, Default } as unknown as
|
|
283
|
+
(Default) => Component({ ...e.props, Default } as unknown as PlaceProps<P>),
|
|
381
284
|
(given, isClaudesRow) => next({ ...e, props: pieceProps(e.props, given, isClaudesRow) } as typeof e),
|
|
382
285
|
)
|
|
383
286
|
if ('failure' in drawn) {
|
|
@@ -387,16 +290,16 @@ export function createUi<State extends object>({ name, claude, router, progress,
|
|
|
387
290
|
if (e.component !== 'AssistantMessage') return drawn.drawing
|
|
388
291
|
return drawWith(table, () => drawPieces([drawn], e.props.isFirstOfReply ? 'opensReply' : 'inReply'))
|
|
389
292
|
})
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
} finally {
|
|
397
|
-
line.end()
|
|
293
|
+
return {
|
|
294
|
+
dispose() {
|
|
295
|
+
drawing.dispose()
|
|
296
|
+
renders.delete(feature)
|
|
297
|
+
claude.ui.invalidate('ui.render')
|
|
298
|
+
},
|
|
398
299
|
}
|
|
399
300
|
},
|
|
301
|
+
toast: toaster.toast,
|
|
302
|
+
copy: toaster.copy,
|
|
400
303
|
ask: (question, options) => claude.ui.ask(question, options),
|
|
401
304
|
scroll: (args) => claude.ui.scroll(args),
|
|
402
305
|
},
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Turn } from './turn/turn.js'
|
|
2
|
+
|
|
3
|
+
export const ContextStart = 'contextStart'
|
|
4
|
+
|
|
5
|
+
export const ContextEnd = 'contextEnd'
|
|
6
|
+
|
|
7
|
+
export type Context = {
|
|
8
|
+
readonly number: number
|
|
9
|
+
readonly openedAt: string
|
|
10
|
+
readonly closedAt?: string
|
|
11
|
+
turns(): Promise<Turn[]>
|
|
12
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { isObject } from '../../../../records.js'
|
|
2
|
+
import { Task } from '../../../goal/task.js'
|
|
3
|
+
import { Document } from './document.js'
|
|
4
|
+
import { Note } from './note.js'
|
|
5
|
+
import { Notice } from './notice.js'
|
|
6
|
+
import { Prompt } from './prompt.js'
|
|
7
|
+
import { Reply } from './reply.js'
|
|
8
|
+
import { senderOf } from './sender.js'
|
|
9
|
+
import { Skill } from './skill.js'
|
|
10
|
+
import { ToolCall } from './tool-call.js'
|
|
11
|
+
import { ToolResult } from './tool-result.js'
|
|
12
|
+
|
|
13
|
+
export const Message = 'message'
|
|
14
|
+
|
|
15
|
+
export type MessageType = typeof Prompt | typeof Note | typeof Reply | typeof ToolCall | typeof ToolResult | typeof Skill | typeof Document | typeof Notice
|
|
16
|
+
|
|
17
|
+
export type MessageOf<Type extends MessageType> = {
|
|
18
|
+
readonly id: string
|
|
19
|
+
readonly type: Type
|
|
20
|
+
readonly at: string
|
|
21
|
+
readonly session: string
|
|
22
|
+
readonly context: number
|
|
23
|
+
readonly turn?: string
|
|
24
|
+
readonly task?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type Message = Prompt | Note | Reply | ToolCall | ToolResult | Skill | Document | Notice
|
|
28
|
+
|
|
29
|
+
export type Placed = 'session' | 'context' | 'turn' | 'task'
|
|
30
|
+
|
|
31
|
+
export type Unplaced<M extends Message = Message> = M extends Message ? Omit<M, Placed> : never
|
|
32
|
+
|
|
33
|
+
export type Row = {
|
|
34
|
+
readonly id: string
|
|
35
|
+
readonly at: string
|
|
36
|
+
readonly type: 'user' | 'assistant' | 'attachment' | 'system'
|
|
37
|
+
readonly name?: string
|
|
38
|
+
readonly isMeta: boolean
|
|
39
|
+
readonly isSummary: boolean
|
|
40
|
+
readonly content: readonly unknown[]
|
|
41
|
+
readonly origin?: unknown
|
|
42
|
+
readonly cwd?: string
|
|
43
|
+
readonly path?: string
|
|
44
|
+
readonly model?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const messageTypes: readonly MessageType[] = [Prompt, Note, Reply, ToolCall, ToolResult, Skill, Document, Notice]
|
|
48
|
+
|
|
49
|
+
export const skillPrefix = 'Base directory for this skill:'
|
|
50
|
+
|
|
51
|
+
const rowTypes: readonly string[] = ['user', 'assistant', 'attachment', 'system']
|
|
52
|
+
|
|
53
|
+
const memoryPath = /^Contents of (.+?) \(/
|
|
54
|
+
|
|
55
|
+
export function rowOfLine(row: Readonly<Record<string, unknown>>): Row | undefined {
|
|
56
|
+
const { uuid, timestamp, type } = row
|
|
57
|
+
if (typeof uuid !== 'string' || typeof timestamp !== 'string' || typeof type !== 'string' || !rowTypes.includes(type)) return undefined
|
|
58
|
+
const message = isObject(row['message']) ? row['message'] : {}
|
|
59
|
+
const attachment = isObject(row['attachment']) ? row['attachment'] : {}
|
|
60
|
+
const content = type === 'attachment' ? attachment['content'] : type === 'system' ? row['content'] : message['content']
|
|
61
|
+
const name = type === 'attachment' ? attachment['type'] : row['subtype']
|
|
62
|
+
return {
|
|
63
|
+
id: uuid,
|
|
64
|
+
at: timestamp,
|
|
65
|
+
type: type as Row['type'],
|
|
66
|
+
...(typeof name === 'string' ? { name } : {}),
|
|
67
|
+
isMeta: row['isMeta'] === true,
|
|
68
|
+
isSummary: row['isCompactSummary'] === true,
|
|
69
|
+
content: blocksOf(content),
|
|
70
|
+
...(row['origin'] === undefined ? {} : { origin: row['origin'] }),
|
|
71
|
+
...(typeof row['cwd'] === 'string' ? { cwd: row['cwd'] } : {}),
|
|
72
|
+
...(typeof attachment['path'] === 'string' ? { path: attachment['path'] } : {}),
|
|
73
|
+
...(typeof message['model'] === 'string' ? { model: message['model'] } : {}),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function rowOfAppend(append: { readonly uuid: string; readonly door: string; readonly origin: unknown; readonly message: { readonly type: Row['type']; readonly name?: string; readonly isMeta?: true; readonly content: readonly unknown[] } }, at: string, cwd: string): Row {
|
|
78
|
+
const { message, origin } = append
|
|
79
|
+
const text = textOf(message.content)
|
|
80
|
+
const path = message.type === 'attachment' ? memoryPath.exec(text)?.[1] : undefined
|
|
81
|
+
const model = isObject(origin) && origin['kind'] === 'model' && typeof origin['model'] === 'string' ? origin['model'] : undefined
|
|
82
|
+
return {
|
|
83
|
+
id: append.uuid,
|
|
84
|
+
at,
|
|
85
|
+
type: message.type,
|
|
86
|
+
...(message.name === undefined ? {} : { name: message.name }),
|
|
87
|
+
isMeta: message.isMeta === true,
|
|
88
|
+
isSummary: append.door === 'compaction',
|
|
89
|
+
content: message.content,
|
|
90
|
+
origin,
|
|
91
|
+
cwd,
|
|
92
|
+
...(path === undefined ? {} : { path }),
|
|
93
|
+
...(model === undefined ? {} : { model }),
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function messagesOf(row: Row, isInTask: boolean): Unplaced[] {
|
|
98
|
+
const found = unplacedOf(row, isInTask)
|
|
99
|
+
return found.length === 1 ? found : found.map((message, index) => ({ ...message, id: `${row.id}:${index}` }))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function unplacedOf(row: Row, isInTask: boolean): Unplaced[] {
|
|
103
|
+
const base = { id: row.id, at: row.at }
|
|
104
|
+
const text = textOf(row.content).trim()
|
|
105
|
+
if (row.type === 'system') return row.name === 'turn_duration' || row.name === 'compact_boundary' || text === '' ? [] : [{ ...base, type: Notice, text }]
|
|
106
|
+
if (row.type === 'attachment') return row.name === 'nested_memory' ? [{ ...base, type: Document, path: row.path ?? '', text }] : []
|
|
107
|
+
if (row.type === 'assistant') {
|
|
108
|
+
return row.content.filter(isObject).flatMap((block): Unplaced[] => {
|
|
109
|
+
if (block['type'] === 'text' && typeof block['text'] === 'string' && block['text'].trim() !== '') return [{ ...base, type: Reply, text: block['text'].trim() }]
|
|
110
|
+
if (block['type'] !== 'tool_use' || typeof block['id'] !== 'string' || typeof block['name'] !== 'string') return []
|
|
111
|
+
return [{ ...base, type: ToolCall, callId: block['id'], name: block['name'], input: isObject(block['input']) ? block['input'] : {}, cwd: row.cwd ?? '' }]
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
if (row.isSummary) return []
|
|
115
|
+
const results = row.content.filter((block) => isObject(block) && block['type'] === 'tool_result' && typeof block['tool_use_id'] === 'string') as Readonly<Record<string, unknown>>[]
|
|
116
|
+
if (results.length > 0) return results.map((block) => ({ ...base, type: ToolResult, callId: block['tool_use_id'] as string, text: textOf(block['content']), isError: block['is_error'] === true }))
|
|
117
|
+
if (text === '') return []
|
|
118
|
+
if (text.startsWith(skillPrefix)) return [{ ...base, type: Skill, name: basename(text.slice(skillPrefix.length).split('\n')[0]?.trim() ?? ''), text }]
|
|
119
|
+
if (text.startsWith('<task-notification')) return [{ ...base, type: Note, text: tagOf(text, 'summary') ?? text, from: { type: Task } }]
|
|
120
|
+
if (row.isMeta) return [{ ...base, type: Note, text, from: senderOf(row.origin, isInTask) }]
|
|
121
|
+
const prompt = promptText(text)
|
|
122
|
+
return prompt === undefined ? [] : [{ ...base, type: Prompt, text: prompt, from: senderOf(row.origin, isInTask) }]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function textOf(content: unknown): string {
|
|
126
|
+
if (typeof content === 'string') return content
|
|
127
|
+
if (!Array.isArray(content)) return ''
|
|
128
|
+
return content.flatMap((block) => (isObject(block) && block['type'] === 'text' && typeof block['text'] === 'string' ? [block['text']] : [])).join('\n')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function tagOf(text: string, tag: string): string | undefined {
|
|
132
|
+
return new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`).exec(text)?.[1]?.trim()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const toolValueChars = 1024
|
|
136
|
+
|
|
137
|
+
export function searchText(message: Unplaced): string {
|
|
138
|
+
if (message.type === ToolCall) return [message.name, ...stringsIn(message.input).map((value) => value.slice(0, toolValueChars))].join('\n')
|
|
139
|
+
if (message.type === ToolResult) return ''
|
|
140
|
+
if (message.type === Document) return `${message.path}\n${message.text}`
|
|
141
|
+
return message.text
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function blocksOf(content: unknown): readonly unknown[] {
|
|
145
|
+
if (Array.isArray(content)) return content
|
|
146
|
+
return typeof content === 'string' && content !== '' ? [{ type: 'text', text: content }] : []
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function promptText(text: string): string | undefined {
|
|
150
|
+
if (text.startsWith('<local-command-') || text.startsWith('<bash-stdout') || text.startsWith('<bash-stderr')) return undefined
|
|
151
|
+
const command = tagOf(text, 'command-name')
|
|
152
|
+
if (command === undefined) return text
|
|
153
|
+
const args = tagOf(text, 'command-args') ?? ''
|
|
154
|
+
return args === '' ? command : `${command} ${args}`
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function basename(path: string): string {
|
|
158
|
+
return path.slice(path.lastIndexOf('/') + 1)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function stringsIn(value: unknown): string[] {
|
|
162
|
+
if (typeof value === 'string') return [value]
|
|
163
|
+
if (Array.isArray(value)) return value.flatMap(stringsIn)
|
|
164
|
+
return isObject(value) ? Object.values(value).flatMap(stringsIn) : []
|
|
165
|
+
}
|