@cmodjs/core 1.0.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 +22 -11
- package/docs/dependencies.md +6 -4
- package/docs/hooks.md +26 -3
- package/docs/index.md +1 -1
- package/docs/install-steps.md +5 -3
- package/docs/mod.md +29 -3
- package/docs/permissions.md +4 -4
- package/docs/session.md +126 -63
- package/docs/testing.md +7 -5
- package/docs/ui.md +9 -1
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +7 -2
- package/mod.d.ts +30 -6
- package/mod.d.ts.map +1 -1
- package/mod.js +34 -1
- package/package.json +1 -1
- package/records.d.ts +4 -1
- package/records.d.ts.map +1 -1
- package/records.js +6 -1
- package/register.d.ts.map +1 -1
- package/register.js +17 -0
- package/runtime/announce.d.ts.map +1 -1
- package/runtime/announce.js +2 -0
- package/runtime/before.d.ts +32 -0
- package/runtime/before.d.ts.map +1 -0
- package/runtime/before.js +214 -0
- package/runtime/claude.d.ts +2 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +1 -0
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +333 -61
- package/runtime/dependencies.js +1 -1
- package/runtime/engine.d.ts +2 -6
- package/runtime/engine.d.ts.map +1 -1
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +8 -8
- package/runtime/hooks.d.ts +1 -1
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +19 -4
- package/runtime/installer.d.ts +1 -1
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +6 -5
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +129 -58
- package/runtime/metadata.d.ts.map +1 -1
- package/runtime/metadata.js +5 -4
- package/runtime/sessions.d.ts +26 -4
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +315 -117
- package/runtime/toaster.d.ts +5 -12
- package/runtime/toaster.d.ts.map +1 -1
- package/runtime/toaster.js +133 -23
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +26 -6
- package/session/context/context.d.ts +3 -1
- package/session/context/context.d.ts.map +1 -1
- package/session/context/turn/message/message.d.ts +1 -0
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +42 -1
- package/session/context/turn/turn.d.ts +2 -1
- package/session/context/turn/turn.d.ts.map +1 -1
- package/session/conversation.d.ts +20 -11
- package/session/conversation.d.ts.map +1 -1
- package/session/conversation.js +50 -27
- package/session/goal/goal.d.ts +3 -2
- package/session/goal/goal.d.ts.map +1 -1
- package/session/goal/task.d.ts +4 -4
- package/session/goal/task.d.ts.map +1 -1
- package/session/session.d.ts +80 -21
- package/session/session.d.ts.map +1 -1
- package/session/session.js +1 -0
- package/src/jobs/program.ts +7 -2
- package/src/mod.ts +48 -5
- package/src/records.ts +6 -1
- package/src/register.ts +17 -0
- package/src/runtime/announce.ts +2 -0
- package/src/runtime/before.ts +200 -0
- package/src/runtime/claude.ts +3 -0
- package/src/runtime/conversations.ts +337 -60
- package/src/runtime/dependencies.ts +1 -1
- package/src/runtime/engine.ts +2 -1
- package/src/runtime/grants.ts +8 -10
- package/src/runtime/hooks.ts +20 -4
- package/src/runtime/installer.ts +6 -5
- package/src/runtime/lifecycle.ts +135 -55
- package/src/runtime/metadata.ts +5 -4
- package/src/runtime/sessions.ts +316 -111
- package/src/runtime/toaster.ts +160 -44
- package/src/runtime/ui.ts +23 -4
- package/src/session/context/context.ts +3 -1
- package/src/session/context/turn/message/message.ts +39 -1
- package/src/session/context/turn/turn.ts +2 -1
- package/src/session/conversation.ts +56 -31
- package/src/session/goal/goal.ts +3 -2
- package/src/session/goal/task.ts +4 -4
- package/src/session/session.ts +62 -15
- package/src/testing/fake-claude.ts +4 -2
- package/src/testing/fake-sessions.ts +94 -20
- package/src/testing.ts +27 -3
- package/src/utils/call-effects.ts +2 -2
- package/src/utils/disposable.ts +3 -0
- package/src/utils/matcher.ts +1 -1
- package/src/utils/paths.ts +6 -3
- package/testing/fake-claude.d.ts +1 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +3 -1
- package/testing/fake-sessions.d.ts +8 -1
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +97 -18
- package/testing.d.ts +1 -0
- package/testing.d.ts.map +1 -1
- package/testing.js +23 -2
- package/utils/call-effects.d.ts +1 -0
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +1 -1
- package/utils/disposable.d.ts +4 -0
- package/utils/disposable.d.ts.map +1 -0
- package/utils/disposable.js +1 -0
- package/utils/matcher.js +1 -1
- package/utils/paths.d.ts +2 -1
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +6 -3
package/src/runtime/toaster.ts
CHANGED
|
@@ -1,30 +1,46 @@
|
|
|
1
1
|
import type { ElementTable, RenderElement, RenderPropsOf, Timer } from 'claude-code'
|
|
2
|
-
import type {
|
|
3
|
-
import { Box, Button, drawWith, Text } from '../ui/elements.js'
|
|
4
|
-
import { coreVersion } from '../records.js'
|
|
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
5
|
import { firstLine, messageOf } from '../utils/text.js'
|
|
6
6
|
import type { Claude } from './claude.js'
|
|
7
7
|
|
|
8
8
|
export type ShownToast = { readonly type: ToastType; readonly message: string; readonly description?: string }
|
|
9
9
|
|
|
10
|
-
export type Failure = { readonly id: string; readonly message: string; readonly error?: unknown; readonly description?: string }
|
|
10
|
+
export type Failure = { readonly id: string; readonly message: string; readonly error?: unknown; readonly description?: string; readonly retry?: () => unknown }
|
|
11
11
|
|
|
12
12
|
export type Toaster = {
|
|
13
13
|
readonly toast: Toast
|
|
14
14
|
copy(text: string): Promise<void>
|
|
15
|
+
fail(failure: Failure): ToastId
|
|
15
16
|
readonly shown: readonly ShownToast[]
|
|
16
|
-
draw(drawn: RenderElement, table: ElementTable, band: RenderPropsOf['AbovePrompt']): RenderElement
|
|
17
|
+
draw(drawn: RenderElement, table: ElementTable, band: RenderPropsOf['AbovePrompt'], bandId: string): RenderElement
|
|
17
18
|
}
|
|
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
|
+
|
|
19
26
|
type Entry = {
|
|
20
27
|
readonly id: ToastId
|
|
21
28
|
readonly type: ToastType
|
|
22
29
|
readonly message: string
|
|
23
30
|
readonly options: ToastOptions
|
|
31
|
+
readonly actions: readonly Action[]
|
|
32
|
+
readonly hides?: string
|
|
24
33
|
isOpen: boolean
|
|
34
|
+
reporting?: Reporting | undefined
|
|
25
35
|
timer: Timer | undefined
|
|
26
36
|
}
|
|
27
37
|
|
|
38
|
+
const githubRepository = /^https:\/\/github\.com\/([^/]+\/[^/#?]+?)(?:\.git)?(?:\/issues)?\/?$/
|
|
39
|
+
|
|
40
|
+
const linkLimit = 2048
|
|
41
|
+
|
|
42
|
+
const hiddenKey = 'cmod:hidden-failures'
|
|
43
|
+
|
|
28
44
|
const defaultDuration = 4000
|
|
29
45
|
const visibleToasts = 3
|
|
30
46
|
const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] as const
|
|
@@ -37,13 +53,22 @@ const icons: Record<Exclude<ToastType, 'loading'>, { readonly glyph: string; rea
|
|
|
37
53
|
error: { glyph: '✗', color: 'error' },
|
|
38
54
|
}
|
|
39
55
|
|
|
40
|
-
export function createToaster(claude: Claude, name: string): Toaster {
|
|
56
|
+
export function createToaster(claude: Claude, name: string, version: () => string | undefined): Toaster {
|
|
41
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
|
+
)
|
|
42
66
|
let lastId = 0
|
|
43
67
|
let frame = 0
|
|
44
68
|
let spinning: Timer | undefined
|
|
45
69
|
let isHeld = false
|
|
46
70
|
let showsAll = false
|
|
71
|
+
let bandId: string | undefined
|
|
47
72
|
|
|
48
73
|
const changed = () => {
|
|
49
74
|
const isLoading = [...entries.values()].some((entry) => entry.type === 'loading')
|
|
@@ -85,18 +110,29 @@ export function createToaster(claude: Claude, name: string): Toaster {
|
|
|
85
110
|
changed()
|
|
86
111
|
}
|
|
87
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
|
+
|
|
88
125
|
const show =
|
|
89
126
|
(type: ToastType) =>
|
|
90
|
-
(message: string, options: ToastOptions = {}): ToastId =>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
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
|
+
}
|
|
100
136
|
|
|
101
137
|
const toast: Toast = Object.assign(show('default'), {
|
|
102
138
|
success: show('success'),
|
|
@@ -132,7 +168,33 @@ export function createToaster(claude: Claude, name: string): Toaster {
|
|
|
132
168
|
const press = (entry: Entry, run: () => unknown) => () =>
|
|
133
169
|
void Promise.resolve()
|
|
134
170
|
.then(run)
|
|
135
|
-
.catch((error: unknown) =>
|
|
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
|
+
}
|
|
136
198
|
|
|
137
199
|
const toggle = (entry: Entry) => () => {
|
|
138
200
|
entry.isOpen = !entry.isOpen
|
|
@@ -141,31 +203,70 @@ export function createToaster(claude: Claude, name: string): Toaster {
|
|
|
141
203
|
}
|
|
142
204
|
|
|
143
205
|
const row = (entry: Entry): RenderElement => {
|
|
144
|
-
const { description
|
|
206
|
+
const { description } = entry.options
|
|
145
207
|
const isText = typeof description === 'string'
|
|
146
208
|
const icon = entry.type === 'loading' ? { glyph: spinner[frame % spinner.length] as string, color: 'claude' } : icons[entry.type]
|
|
147
209
|
const message = isText ? Button({ plain: true, key: `toast:${entry.id}:open`, label: entry.message, onPress: toggle(entry) }) : Text({ children: entry.message })
|
|
148
210
|
const details = !isText ? description : Text({ dimColor: true, wrap: entry.isOpen ? 'wrap' : 'truncate-end', children: entry.isOpen ? description : firstLine(description) })
|
|
149
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)
|
|
150
216
|
return Box({
|
|
151
217
|
gap: 2,
|
|
152
218
|
children: [
|
|
153
219
|
fixed(Text({ ...(icon.color === undefined ? {} : { color: icon.color }), children: icon.glyph })),
|
|
154
220
|
fixed(Text({ dimColor: true, children: name })),
|
|
155
|
-
Box({ flexDirection: 'column', flexGrow: 1, flexShrink: 1, children:
|
|
156
|
-
...(
|
|
157
|
-
fixed(Button({ plain: true, dimColor: true, key: `toast:${entry.id}:close`, label: '✕', onPress: () => dismiss(entry.id) })),
|
|
221
|
+
Box({ flexDirection: 'column', flexGrow: 1, flexShrink: 1, children: entry.hides === undefined ? text : [...text, ...under] }),
|
|
222
|
+
...(entry.hides === undefined ? [...actions.map(fixed), fixed(close)] : []),
|
|
158
223
|
],
|
|
159
224
|
})
|
|
160
225
|
}
|
|
161
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
|
+
|
|
162
261
|
return {
|
|
163
262
|
toast,
|
|
164
263
|
copy,
|
|
264
|
+
fail,
|
|
165
265
|
get shown() {
|
|
166
266
|
return [...entries.values()].map(({ type, message, options }) => ({ type, message, ...(typeof options.description === 'string' ? { description: options.description } : {}) }))
|
|
167
267
|
},
|
|
168
|
-
draw(drawn, table, band) {
|
|
268
|
+
draw(drawn, table, band, drawnIn) {
|
|
269
|
+
bandId = drawnIn
|
|
169
270
|
isHeld = band.hasSurvey === true
|
|
170
271
|
if (isHeld || entries.size === 0) return drawn
|
|
171
272
|
const all = [...entries.values()]
|
|
@@ -186,28 +287,6 @@ export function createToaster(claude: Claude, name: string): Toaster {
|
|
|
186
287
|
}
|
|
187
288
|
}
|
|
188
289
|
|
|
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
290
|
function describe(error: unknown): string {
|
|
212
291
|
const messages: string[] = []
|
|
213
292
|
for (let cause = error; cause !== undefined; cause = cause instanceof Error ? cause.cause : undefined) {
|
|
@@ -216,3 +295,40 @@ function describe(error: unknown): string {
|
|
|
216
295
|
}
|
|
217
296
|
return messages.join('\n')
|
|
218
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
|
@@ -107,6 +107,15 @@ export function createUi<State extends object>({ name, claude, router, toaster,
|
|
|
107
107
|
const panes = new Map<string, Pane<State>>()
|
|
108
108
|
const paneLogs = new Map<string, (failure: string) => void>()
|
|
109
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' }))
|
|
110
119
|
const requestedSizes = new Map<string, PaneSize>()
|
|
111
120
|
const renders = new Set<string>()
|
|
112
121
|
const markdownRenders = new Map<string, MarkdownRender>()
|
|
@@ -197,6 +206,8 @@ export function createUi<State extends object>({ name, claude, router, toaster,
|
|
|
197
206
|
const pane = panes.get(e.requestId)
|
|
198
207
|
if (pane === undefined || e.component !== 'Pane') return next(e)
|
|
199
208
|
openPanes.add(pane.id)
|
|
209
|
+
paneStates.set(pane.id, { isShown: true, isFocused: e.props.isFocused })
|
|
210
|
+
readPanes()
|
|
200
211
|
const table = claude.ui.resolve(e)
|
|
201
212
|
try {
|
|
202
213
|
return drawWith(table, () => pane.render(mod(), e.props), markdownIn(table))
|
|
@@ -214,6 +225,8 @@ export function createUi<State extends object>({ name, claude, router, toaster,
|
|
|
214
225
|
})
|
|
215
226
|
router.add('ui.close', async (e, next) => {
|
|
216
227
|
openPanes.delete(e.id)
|
|
228
|
+
paneStates.delete(e.id)
|
|
229
|
+
readPanes()
|
|
217
230
|
const closed = await next(e)
|
|
218
231
|
const pane = panes.get(e.id)
|
|
219
232
|
if (pane?.onClose !== undefined) await handle(pane, 'onClose', () => pane.onClose?.(mod(), e))
|
|
@@ -241,13 +254,22 @@ export function createUi<State extends object>({ name, claude, router, toaster,
|
|
|
241
254
|
get isOpen() {
|
|
242
255
|
return openPanes.has(pane.id)
|
|
243
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
|
+
},
|
|
244
263
|
async open(options) {
|
|
245
264
|
const { isPlaced } = await openAt(pane, paneSize(pane, mod().state), options?.focus)
|
|
246
265
|
if (isPlaced) openPanes.add(pane.id)
|
|
266
|
+
await syncPanes()
|
|
247
267
|
},
|
|
248
268
|
async close() {
|
|
249
269
|
await claude.ui.close({ id: pane.id })
|
|
250
270
|
openPanes.delete(pane.id)
|
|
271
|
+
paneStates.delete(pane.id)
|
|
272
|
+
await syncPanes()
|
|
251
273
|
},
|
|
252
274
|
toggle: (options) => (openPanes.has(pane.id) ? handle.close() : handle.open(options)),
|
|
253
275
|
}
|
|
@@ -316,10 +338,7 @@ export function createUi<State extends object>({ name, claude, router, toaster,
|
|
|
316
338
|
})
|
|
317
339
|
},
|
|
318
340
|
async restorePanes() {
|
|
319
|
-
if (panes.size
|
|
320
|
-
for (const open of await claude.ui.panes()) {
|
|
321
|
-
if (open.isPlaced && panes.has(open.id)) openPanes.add(open.id)
|
|
322
|
-
}
|
|
341
|
+
if (panes.size > 0) await syncPanes()
|
|
323
342
|
},
|
|
324
343
|
}
|
|
325
344
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import type { RecordPart } from '../session.js'
|
|
1
2
|
import type { Turn } from './turn/turn.js'
|
|
2
3
|
|
|
3
4
|
export const ContextStart = 'contextStart'
|
|
4
5
|
|
|
5
6
|
export const ContextEnd = 'contextEnd'
|
|
6
7
|
|
|
7
|
-
export type Context = {
|
|
8
|
+
export type Context = RecordPart & {
|
|
8
9
|
readonly number: number
|
|
9
10
|
readonly openedAt: string
|
|
10
11
|
readonly closedAt?: string
|
|
12
|
+
readonly task?: string
|
|
11
13
|
turns(): Promise<Turn[]>
|
|
12
14
|
}
|
|
@@ -112,7 +112,7 @@ function unplacedOf(row: Row, isInTask: boolean): Unplaced[] {
|
|
|
112
112
|
})
|
|
113
113
|
}
|
|
114
114
|
if (row.isSummary) return []
|
|
115
|
-
const results = row.content.filter(
|
|
115
|
+
const results = row.content.filter(isToolResultBlock)
|
|
116
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
117
|
if (text === '') return []
|
|
118
118
|
if (text.startsWith(skillPrefix)) return [{ ...base, type: Skill, name: basename(text.slice(skillPrefix.length).split('\n')[0]?.trim() ?? ''), text }]
|
|
@@ -122,6 +122,44 @@ function unplacedOf(row: Row, isInTask: boolean): Unplaced[] {
|
|
|
122
122
|
return prompt === undefined ? [] : [{ ...base, type: Prompt, text: prompt, from: senderOf(row.origin, isInTask) }]
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
export function withTexts(row: Row, texts: ReadonlyMap<number, string>): readonly unknown[] {
|
|
126
|
+
if (row.type === 'assistant') {
|
|
127
|
+
let index = -1
|
|
128
|
+
return row.content.map((block) => {
|
|
129
|
+
if (!isObject(block)) return block
|
|
130
|
+
const isText = block['type'] === 'text' && typeof block['text'] === 'string' && block['text'].trim() !== ''
|
|
131
|
+
const isCall = block['type'] === 'tool_use' && typeof block['id'] === 'string' && typeof block['name'] === 'string'
|
|
132
|
+
if (!isText && !isCall) return block
|
|
133
|
+
index += 1
|
|
134
|
+
const text = texts.get(index)
|
|
135
|
+
return isText && text !== undefined ? { ...block, text } : block
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
if (row.type === 'user' && row.content.some(isToolResultBlock)) {
|
|
139
|
+
let index = -1
|
|
140
|
+
return row.content.map((block) => {
|
|
141
|
+
if (!isToolResultBlock(block)) return block
|
|
142
|
+
index += 1
|
|
143
|
+
const text = texts.get(index)
|
|
144
|
+
if (text === undefined) return block
|
|
145
|
+
return { ...block, content: Array.isArray(block['content']) ? keepingMedia(block['content'], text) : text }
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
const text = texts.get(0)
|
|
149
|
+
if (text === undefined) return row.content
|
|
150
|
+
const whole = textOf(row.content)
|
|
151
|
+
if (whole.trim().startsWith('<task-notification') && tagOf(whole, 'summary') !== undefined) return keepingMedia(row.content, whole.replace(/<summary>[\s\S]*?<\/summary>/, () => `<summary>${text}</summary>`))
|
|
152
|
+
return keepingMedia(row.content, text)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function keepingMedia(content: readonly unknown[], text: string): unknown[] {
|
|
156
|
+
return [{ type: 'text', text }, ...content.filter((block) => !isObject(block) || block['type'] !== 'text')]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isToolResultBlock(block: unknown): block is Readonly<Record<string, unknown>> {
|
|
160
|
+
return isObject(block) && block['type'] === 'tool_result' && typeof block['tool_use_id'] === 'string'
|
|
161
|
+
}
|
|
162
|
+
|
|
125
163
|
export function textOf(content: unknown): string {
|
|
126
164
|
if (typeof content === 'string') return content
|
|
127
165
|
if (!Array.isArray(content)) return ''
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RecordPart } from '../../session.js'
|
|
1
2
|
import type { Message } from './message/message.js'
|
|
2
3
|
import type { Prompt } from './message/prompt.js'
|
|
3
4
|
|
|
@@ -5,7 +6,7 @@ export const TurnStart = 'turnStart'
|
|
|
5
6
|
|
|
6
7
|
export const TurnEnd = 'turnEnd'
|
|
7
8
|
|
|
8
|
-
export type Turn = {
|
|
9
|
+
export type Turn = RecordPart & {
|
|
9
10
|
readonly prompt: Prompt
|
|
10
11
|
readonly context: number
|
|
11
12
|
readonly openedAt: string
|
|
@@ -7,6 +7,7 @@ import { ToolResult } from './context/turn/message/tool-result.js'
|
|
|
7
7
|
import { TurnEnd, TurnStart, type Turn } from './context/turn/turn.js'
|
|
8
8
|
import { GoalChange, type Goal } from './goal/goal.js'
|
|
9
9
|
import { Task, TaskEnd, TaskStart } from './goal/task.js'
|
|
10
|
+
import type { UsageChange, Usage } from './session.js'
|
|
10
11
|
|
|
11
12
|
export type SessionData = {
|
|
12
13
|
readonly id: string
|
|
@@ -18,22 +19,26 @@ export type SessionData = {
|
|
|
18
19
|
readonly lastAt: string
|
|
19
20
|
readonly forkedFrom?: string
|
|
20
21
|
readonly isRunning: boolean
|
|
22
|
+
readonly agentType?: string
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
export type ContextData =
|
|
25
|
+
export type ContextData = Pick<Context, 'number' | 'openedAt' | 'closedAt' | 'task'>
|
|
24
26
|
|
|
25
|
-
export type TurnData =
|
|
27
|
+
export type TurnData = Pick<Turn, 'prompt' | 'context' | 'openedAt' | 'closedAt'>
|
|
26
28
|
|
|
27
|
-
export type TaskData =
|
|
29
|
+
export type TaskData = Pick<Task, 'id' | 'title' | 'openedBy' | 'openedAt' | 'closedAt'> & { readonly agentType: string; readonly model?: string; readonly parent?: string }
|
|
28
30
|
|
|
29
|
-
export type
|
|
31
|
+
export type RunningTask = TaskData & { readonly context: number; readonly contextOpenedAt?: string }
|
|
32
|
+
|
|
33
|
+
export type GoalData = Pick<Goal, 'text'> & { readonly task?: string }
|
|
30
34
|
|
|
31
35
|
export type SessionChange =
|
|
32
36
|
| { readonly type: typeof Message; readonly message: Message }
|
|
33
37
|
| { readonly type: typeof TurnStart | typeof TurnEnd; readonly turn: TurnData }
|
|
34
38
|
| { readonly type: typeof ContextStart | typeof ContextEnd; readonly context: ContextData }
|
|
35
|
-
| { readonly type: typeof TaskStart | typeof TaskEnd; readonly task:
|
|
39
|
+
| { readonly type: typeof TaskStart | typeof TaskEnd; readonly task: RunningTask }
|
|
36
40
|
| { readonly type: typeof GoalChange; readonly goal: GoalData }
|
|
41
|
+
| { readonly type: typeof UsageChange; readonly usage: Usage }
|
|
37
42
|
|
|
38
43
|
export type ConversationState = {
|
|
39
44
|
readonly session: string
|
|
@@ -43,13 +48,13 @@ export type ConversationState = {
|
|
|
43
48
|
readonly goal?: string
|
|
44
49
|
readonly agentCalls: readonly string[]
|
|
45
50
|
readonly endedCalls: Readonly<Record<string, string>>
|
|
46
|
-
readonly tasks: Readonly<Record<string,
|
|
51
|
+
readonly tasks: Readonly<Record<string, RunningTask>>
|
|
52
|
+
readonly taskTurns: Readonly<Record<string, TurnData>>
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
export type Conversation = {
|
|
50
56
|
add(row: Row, task?: string): SessionChange[]
|
|
51
57
|
startTask(task: TaskData): SessionChange[]
|
|
52
|
-
endContext(at: string): SessionChange[]
|
|
53
58
|
startContext(at: string): SessionChange[]
|
|
54
59
|
changeGoal(text: string): SessionChange[]
|
|
55
60
|
readonly state: ConversationState
|
|
@@ -62,7 +67,7 @@ const agentTools: readonly string[] = ['Agent', 'Task']
|
|
|
62
67
|
const asyncLaunch = 'Async agent launched'
|
|
63
68
|
|
|
64
69
|
export function emptyConversation(session: string): ConversationState {
|
|
65
|
-
return { session, context: 0, contextOpenedAt: '', agentCalls: [], endedCalls: {}, tasks: {} }
|
|
70
|
+
return { session, context: 0, contextOpenedAt: '', agentCalls: [], endedCalls: {}, tasks: {}, taskTurns: {} }
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
export function createConversation(start: ConversationState): Conversation {
|
|
@@ -70,6 +75,14 @@ export function createConversation(start: ConversationState): Conversation {
|
|
|
70
75
|
const agentCalls = new Set(start.agentCalls)
|
|
71
76
|
const endedCalls = new Map(Object.entries(start.endedCalls))
|
|
72
77
|
const tasks = new Map(Object.entries(start.tasks))
|
|
78
|
+
const taskTurns = new Map(Object.entries(start.taskTurns))
|
|
79
|
+
|
|
80
|
+
const endTaskTurn = (task: string, at: string): SessionChange[] => {
|
|
81
|
+
const open = taskTurns.get(task)
|
|
82
|
+
if (open === undefined) return []
|
|
83
|
+
taskTurns.delete(task)
|
|
84
|
+
return open.closedAt === undefined ? [{ type: TurnEnd, turn: { ...open, closedAt: at } }] : []
|
|
85
|
+
}
|
|
73
86
|
|
|
74
87
|
const startContext = (at: string): SessionChange[] => {
|
|
75
88
|
context += 1
|
|
@@ -79,6 +92,17 @@ export function createConversation(start: ConversationState): Conversation {
|
|
|
79
92
|
|
|
80
93
|
const endContext = (at: string): SessionChange[] => (context === 0 ? [] : [{ type: ContextEnd, context: { number: context, openedAt: contextOpenedAt, closedAt: at } }])
|
|
81
94
|
|
|
95
|
+
const compactTask = (task: string, at: string): SessionChange[] => {
|
|
96
|
+
const owner = tasks.get(task)
|
|
97
|
+
if (owner === undefined) return []
|
|
98
|
+
const next = { ...owner, context: owner.context + 1, contextOpenedAt: at }
|
|
99
|
+
tasks.set(task, next)
|
|
100
|
+
return [
|
|
101
|
+
{ type: ContextEnd, context: { number: owner.context, openedAt: owner.contextOpenedAt ?? owner.openedAt, closedAt: at, task } },
|
|
102
|
+
{ type: ContextStart, context: { number: next.context, openedAt: at, task } },
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
|
|
82
106
|
const endTurn = (at: string): SessionChange[] => {
|
|
83
107
|
if (turn === undefined) return []
|
|
84
108
|
const ended = { ...turn, closedAt: at }
|
|
@@ -86,17 +110,19 @@ export function createConversation(start: ConversationState): Conversation {
|
|
|
86
110
|
return [{ type: TurnEnd, turn: ended }]
|
|
87
111
|
}
|
|
88
112
|
|
|
89
|
-
const endTask = (call: string, at: string): SessionChange[] => {
|
|
90
|
-
const
|
|
91
|
-
if (
|
|
113
|
+
const endTask = (call: string, at: string, only?: string): SessionChange[] => {
|
|
114
|
+
const opened = [...tasks.values()].filter((each) => each.openedBy === call && (only === undefined || each.id === only))
|
|
115
|
+
if (opened.length === 0) {
|
|
92
116
|
if (agentCalls.has(call)) endedCalls.set(call, at)
|
|
93
117
|
return []
|
|
94
118
|
}
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
119
|
+
if (only === undefined) agentCalls.delete(call)
|
|
120
|
+
return opened.flatMap((task) => {
|
|
121
|
+
if (task.closedAt !== undefined) return []
|
|
122
|
+
const ended = { ...task, closedAt: at }
|
|
123
|
+
tasks.set(task.id, ended)
|
|
124
|
+
return [...endTaskTurn(task.id, at), { type: TaskEnd, task: ended }]
|
|
125
|
+
})
|
|
100
126
|
}
|
|
101
127
|
|
|
102
128
|
const changeGoal = (text: string): SessionChange[] => {
|
|
@@ -109,30 +135,32 @@ export function createConversation(start: ConversationState): Conversation {
|
|
|
109
135
|
const changes: SessionChange[] = []
|
|
110
136
|
if (task === undefined && context === 0) changes.push(...startContext(row.at))
|
|
111
137
|
if (task === undefined && row.type === 'system' && row.name === 'turn_duration') return [...changes, ...endTurn(row.at)]
|
|
112
|
-
if (
|
|
138
|
+
if (row.type === 'system' && row.name === 'compact_boundary') return [...changes, ...(task === undefined ? [...endContext(row.at), ...startContext(row.at)] : compactTask(task, row.at))]
|
|
113
139
|
const owner = task === undefined ? undefined : tasks.get(task)
|
|
140
|
+
const placedIn = owner?.context ?? context
|
|
114
141
|
for (const unplaced of messagesOf(row, task !== undefined)) {
|
|
115
|
-
if (
|
|
116
|
-
changes.push(...endTurn(row.at))
|
|
117
|
-
const opened: TurnData = { prompt: { ...unplaced, session: start.session, context, turn: unplaced.id }, context, openedAt: row.at }
|
|
118
|
-
turn = opened
|
|
142
|
+
if (unplaced.type === Prompt) {
|
|
143
|
+
changes.push(...(task === undefined ? endTurn(row.at) : endTaskTurn(task, row.at)))
|
|
144
|
+
const opened: TurnData = { prompt: { ...unplaced, session: start.session, context: placedIn, turn: unplaced.id, ...(task === undefined ? {} : { task }) }, context: placedIn, openedAt: row.at, ...(owner?.closedAt === undefined ? {} : { closedAt: owner.closedAt }) }
|
|
145
|
+
if (task === undefined) turn = opened
|
|
146
|
+
else taskTurns.set(task, opened)
|
|
119
147
|
changes.push({ type: TurnStart, turn: opened })
|
|
120
|
-
if (goal === undefined) changes.push(...changeGoal(unplaced.text))
|
|
148
|
+
if (task === undefined && goal === undefined) changes.push(...changeGoal(unplaced.text))
|
|
121
149
|
}
|
|
150
|
+
const open = task === undefined ? turn : taskTurns.get(task)
|
|
122
151
|
const placed = {
|
|
123
152
|
...unplaced,
|
|
124
153
|
session: start.session,
|
|
125
|
-
context:
|
|
126
|
-
...(
|
|
154
|
+
context: placedIn,
|
|
155
|
+
...(open === undefined ? {} : { turn: open.prompt.id }),
|
|
127
156
|
...(task === undefined ? {} : { task }),
|
|
128
157
|
} as Message
|
|
129
158
|
changes.push({ type: Message, message: placed })
|
|
130
|
-
if (task !== undefined) continue
|
|
131
159
|
if (placed.type === ToolCall && agentTools.includes(placed.name)) agentCalls.add(placed.callId)
|
|
132
160
|
if (placed.type === ToolResult && agentCalls.has(placed.callId) && !placed.text.startsWith(asyncLaunch)) changes.push(...endTask(placed.callId, row.at))
|
|
133
161
|
if (placed.type === Note && placed.from.type === Task) {
|
|
134
162
|
const call = tagOf(textOf(row.content), 'tool-use-id')
|
|
135
|
-
if (call !== undefined && agentCalls.has(call)) changes.push(...endTask(call, row.at))
|
|
163
|
+
if (call !== undefined && agentCalls.has(call)) changes.push(...endTask(call, row.at, tagOf(textOf(row.content), 'task-id')))
|
|
136
164
|
}
|
|
137
165
|
}
|
|
138
166
|
return changes
|
|
@@ -144,13 +172,9 @@ export function createConversation(start: ConversationState): Conversation {
|
|
|
144
172
|
agentCalls.add(data.openedBy)
|
|
145
173
|
const changes: SessionChange[] = [{ type: TaskStart, task: started }]
|
|
146
174
|
const endedAt = endedCalls.get(data.openedBy)
|
|
147
|
-
if (endedAt !== undefined)
|
|
148
|
-
endedCalls.delete(data.openedBy)
|
|
149
|
-
changes.push(...endTask(data.openedBy, endedAt))
|
|
150
|
-
}
|
|
175
|
+
if (endedAt !== undefined) changes.push(...endTask(data.openedBy, endedAt, data.id))
|
|
151
176
|
return changes
|
|
152
177
|
},
|
|
153
|
-
endContext,
|
|
154
178
|
startContext,
|
|
155
179
|
changeGoal,
|
|
156
180
|
get state() {
|
|
@@ -163,6 +187,7 @@ export function createConversation(start: ConversationState): Conversation {
|
|
|
163
187
|
agentCalls: [...agentCalls],
|
|
164
188
|
endedCalls: Object.fromEntries(endedCalls),
|
|
165
189
|
tasks: Object.fromEntries(tasks),
|
|
190
|
+
taskTurns: Object.fromEntries(taskTurns),
|
|
166
191
|
}
|
|
167
192
|
},
|
|
168
193
|
}
|
package/src/session/goal/goal.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import type { RecordPart } from '../session.js'
|
|
1
2
|
import type { Task } from './task.js'
|
|
2
3
|
|
|
3
4
|
export const GoalChange = 'goalChange'
|
|
4
5
|
|
|
5
|
-
export type Goal = {
|
|
6
|
+
export type Goal = RecordPart & {
|
|
6
7
|
readonly text: string
|
|
7
|
-
update(change: { readonly text: string }): Promise<void>
|
|
8
8
|
tasks(): Promise<Task[]>
|
|
9
|
+
update(change: { readonly text: string }): Promise<void>
|
|
9
10
|
}
|