@cmodjs/core 4.0.2 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/commands.md +43 -8
- package/docs/index.md +3 -3
- package/docs/install-steps.md +6 -6
- package/docs/jobs.md +2 -1
- package/docs/mod.md +1 -1
- package/docs/options.md +1 -1
- package/docs/permissions.md +1 -1
- package/docs/session.md +44 -13
- package/docs/testing.md +14 -10
- package/docs/ui.md +43 -11
- package/installer.d.ts +3 -2
- package/installer.d.ts.map +1 -1
- package/jobs/program.d.ts +1 -0
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +39 -23
- package/mod.d.ts +3 -2
- package/mod.d.ts.map +1 -1
- package/mod.js +1 -0
- package/package.json +1 -1
- package/records.d.ts +15 -1
- package/records.d.ts.map +1 -1
- package/records.js +39 -1
- package/register.d.ts.map +1 -1
- package/register.js +5 -2
- package/runtime/before.d.ts +6 -2
- package/runtime/before.d.ts.map +1 -1
- package/runtime/before.js +56 -18
- package/runtime/buttons.d.ts +10 -0
- package/runtime/buttons.d.ts.map +1 -0
- package/runtime/buttons.js +12 -0
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +134 -25
- package/runtime/engine.d.ts +2 -0
- package/runtime/engine.d.ts.map +1 -1
- package/runtime/grants.d.ts +1 -0
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +5 -2
- package/runtime/hotkeys.d.ts.map +1 -1
- package/runtime/hotkeys.js +7 -4
- package/runtime/installer.d.ts +9 -6
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +104 -47
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +121 -40
- package/runtime/pages.d.ts +3 -1
- package/runtime/pages.d.ts.map +1 -1
- package/runtime/pages.js +34 -10
- package/runtime/parts.d.ts +1 -0
- package/runtime/parts.d.ts.map +1 -1
- package/runtime/parts.js +6 -3
- package/runtime/programs.d.ts +1 -0
- package/runtime/programs.d.ts.map +1 -1
- package/runtime/programs.js +4 -1
- package/runtime/sessions.d.ts +2 -2
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +88 -58
- package/runtime/toaster.d.ts.map +1 -1
- package/runtime/toaster.js +23 -9
- package/runtime/ui.d.ts +21 -4
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +158 -58
- package/runtime/watches.d.ts.map +1 -1
- package/runtime/watches.js +18 -4
- package/runtime/widgets.d.ts +7 -5
- package/runtime/widgets.d.ts.map +1 -1
- package/runtime/widgets.js +29 -20
- package/session/context/turn/message/attached-file.d.ts +13 -0
- package/session/context/turn/message/attached-file.d.ts.map +1 -0
- package/session/context/turn/message/attached-file.js +21 -0
- package/session/context/turn/message/message.d.ts +3 -2
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +20 -6
- package/session/conversation.d.ts +14 -3
- package/session/conversation.d.ts.map +1 -1
- package/session/conversation.js +23 -4
- package/session/goal/task.d.ts +5 -0
- package/session/goal/task.d.ts.map +1 -1
- package/session/session.d.ts +35 -7
- package/session/session.d.ts.map +1 -1
- package/session/session.js +2 -1
- package/src/installer.ts +3 -2
- package/src/jobs/program.ts +41 -21
- package/src/mod.ts +3 -2
- package/src/records.ts +49 -1
- package/src/register.ts +9 -6
- package/src/runtime/before.ts +61 -22
- package/src/runtime/buttons.ts +22 -0
- package/src/runtime/conversations.ts +138 -32
- package/src/runtime/engine.ts +2 -0
- package/src/runtime/grants.ts +6 -2
- package/src/runtime/hotkeys.ts +7 -4
- package/src/runtime/installer.ts +144 -82
- package/src/runtime/lifecycle.ts +128 -43
- package/src/runtime/pages.ts +45 -8
- package/src/runtime/parts.ts +8 -4
- package/src/runtime/programs.ts +5 -1
- package/src/runtime/sessions.ts +83 -57
- package/src/runtime/toaster.ts +27 -9
- package/src/runtime/ui.ts +163 -61
- package/src/runtime/watches.ts +20 -5
- package/src/runtime/widgets.ts +34 -22
- package/src/session/context/turn/message/attached-file.ts +36 -0
- package/src/session/context/turn/message/message.ts +20 -7
- package/src/session/conversation.ts +30 -6
- package/src/session/goal/task.ts +6 -0
- package/src/session/session.ts +17 -7
- package/src/testing/fake-claude.ts +21 -6
- package/src/testing/fake-elements.ts +51 -8
- package/src/testing/fake-sessions.ts +60 -23
- package/src/testing.ts +86 -28
- package/src/ui/areas.ts +1 -0
- package/src/ui/components.ts +14 -8
- package/src/ui/elements.ts +6 -2
- package/src/ui/markdown.ts +3 -3
- package/src/ui/panes.ts +2 -2
- package/src/ui/styles.ts +6 -2
- package/testing/fake-claude.d.ts +3 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +18 -5
- package/testing/fake-elements.d.ts.map +1 -1
- package/testing/fake-elements.js +46 -7
- package/testing/fake-sessions.d.ts +21 -10
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +58 -20
- package/testing.d.ts +8 -12
- package/testing.d.ts.map +1 -1
- package/testing.js +82 -21
- package/ui/areas.d.ts +4 -0
- package/ui/areas.d.ts.map +1 -1
- package/ui/components.d.ts +1 -0
- package/ui/components.d.ts.map +1 -1
- package/ui/components.js +11 -7
- package/ui/elements.d.ts +3 -0
- package/ui/elements.d.ts.map +1 -1
- package/ui/elements.js +6 -1
- package/ui/markdown.d.ts +2 -2
- package/ui/markdown.d.ts.map +1 -1
- package/ui/panes.d.ts +2 -1
- package/ui/panes.d.ts.map +1 -1
- package/ui/styles.d.ts +11 -2
- package/ui/styles.d.ts.map +1 -1
package/src/runtime/sessions.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { Person, senderOf } from '../session/context/turn/message/sender.js'
|
|
|
7
7
|
import { ToolCall } from '../session/context/turn/message/tool-call.js'
|
|
8
8
|
import { ToolResult } from '../session/context/turn/message/tool-result.js'
|
|
9
9
|
import { TurnEnd, TurnStart, type Turn } from '../session/context/turn/turn.js'
|
|
10
|
-
import { searchLimit, type ContextData, type GoalData, type SessionChange, type SessionData, type TaskData, type TurnData } from '../session/conversation.js'
|
|
10
|
+
import { forkAgentType, searchLimit, type ContextData, type GoalData, type SessionChange, type SessionData, type TaskData, type TurnData } from '../session/conversation.js'
|
|
11
11
|
import { GoalChange, type Goal } from '../session/goal/goal.js'
|
|
12
12
|
import { TaskEnd, TaskStart, type Task } from '../session/goal/task.js'
|
|
13
13
|
import { beforeEvents, processEvents, Session, sessionEvents, type BeforeEvent, type Placement, type BeforeMethods, type Surface, type OnMethods, type ProcessBeforeMethods, type ProcessName, type ProcessOnMethods, type RecordPart, type SearchOptions, type SessionEvent, type SessionScope, type SessionSend, type Sessions, type TaskOwner, type TaskStartArgs, type ToolCallArgs, type Usage } from '../session/session.js'
|
|
@@ -41,9 +41,9 @@ export const conversationMethods = {
|
|
|
41
41
|
before: 'cmod:before',
|
|
42
42
|
} as const
|
|
43
43
|
|
|
44
|
-
export type Act = 'send' | 'startTask' | 'callTool' | 'usage' | 'surfaces' | 'abort'
|
|
44
|
+
export type Act = 'send' | 'startTask' | 'fork' | 'clone' | 'startBackgroundTask' | 'callTool' | 'usage' | 'surfaces' | 'abort'
|
|
45
45
|
|
|
46
|
-
const actPermissions: Readonly<Record<Exclude<Act, 'callTool'>, string>> = { send: 'prompt', startTask: 'tasks', usage: 'conversation', surfaces: 'conversation', abort: 'prompt' }
|
|
46
|
+
const actPermissions: Readonly<Record<Exclude<Act, 'callTool'>, string>> = { send: 'prompt', startTask: 'tasks', fork: 'tasks', clone: 'tasks', startBackgroundTask: 'tasks', usage: 'conversation', surfaces: 'conversation', abort: 'prompt' }
|
|
47
47
|
|
|
48
48
|
const startsTurns: readonly string[] = ['CronCreate']
|
|
49
49
|
|
|
@@ -51,6 +51,11 @@ const ownSchedule: readonly string[] = ['CronDelete', 'CronList']
|
|
|
51
51
|
|
|
52
52
|
export const beforePermission = (event: BeforeEvent): string => (event === 'toolCall' ? 'tools' : event === TaskStart ? 'tasks' : 'prompt')
|
|
53
53
|
|
|
54
|
+
const answerPermissions = (event: BeforeEvent, answer: Partial<Answers>): string[] => [
|
|
55
|
+
...(Object.keys(answer).some((key) => key !== 'note') ? [beforePermission(event)] : []),
|
|
56
|
+
...(answer.note === undefined ? [] : ['prompt']),
|
|
57
|
+
]
|
|
58
|
+
|
|
54
59
|
export const isAct = (action: string): action is Act => action === 'callTool' || action in actPermissions
|
|
55
60
|
|
|
56
61
|
export function toolPermission(tool: string): string | undefined {
|
|
@@ -82,7 +87,8 @@ type Owned = { readonly session: string; readonly task: string }
|
|
|
82
87
|
export async function usageOf(claude: Pick<Claude, 'session'>): Promise<Usage> {
|
|
83
88
|
const [model, usage] = await Promise.all([claude.session.model(), claude.session.usage()])
|
|
84
89
|
const { tokens, window, percent } = usage.context
|
|
85
|
-
|
|
90
|
+
const rateLimits = usage.rateLimits.map(({ kind, percentUsed, resetsAt }) => ({ type: kind, percent: percentUsed, ...(resetsAt === undefined ? {} : { resetsAt }) }))
|
|
91
|
+
return { model, contextWindow: { ...(tokens === undefined ? {} : { tokenCount: tokens }), size: window, ...(percent === undefined ? {} : { percent }) }, rateLimits, ...(usage.cost === undefined ? {} : { cost: { usd: usage.cost.usd } }) }
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
export function payloadOf(change: SessionChange): unknown {
|
|
@@ -90,8 +96,9 @@ export function payloadOf(change: SessionChange): unknown {
|
|
|
90
96
|
if ('turn' in change) return change.turn
|
|
91
97
|
if ('context' in change) return change.context
|
|
92
98
|
if ('task' in change) {
|
|
93
|
-
const { id, title, agentType, model, openedBy, openedAt, closedAt } = change.task
|
|
94
|
-
|
|
99
|
+
const { id, title, agentType, model, openedBy, openedAt, closedAt, isBackground, forkedFrom } = change.task
|
|
100
|
+
const owner = { id, title, ...(agentType === undefined ? {} : { agentType }), ...(model === undefined ? {} : { model }), ...(forkedFrom === undefined ? {} : { forkedFrom }), startedAt: openedAt }
|
|
101
|
+
return { id, title, openedBy, openedAt, ...(closedAt === undefined ? {} : { closedAt }), ...(isBackground === true ? { isBackground } : {}), owner }
|
|
95
102
|
}
|
|
96
103
|
if ('usage' in change) return change.usage
|
|
97
104
|
return change.goal
|
|
@@ -224,8 +231,10 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
224
231
|
if (!decides(entry, session, latest)) return undefined
|
|
225
232
|
const payload = await payloadFor(session, latest)
|
|
226
233
|
const answer = await invoke(entry.method, entry.options, async (time) => takenAnswer(entry.method, beforeAnswerKeys[event], await entry.listener(payload as never, time)), beforeAnswerKeys[event])
|
|
227
|
-
if (answer === undefined || answer === null || (typeof answer === 'object' && 'refuse' in answer)
|
|
228
|
-
|
|
234
|
+
if (answer === undefined || answer === null || (typeof answer === 'object' && 'refuse' in answer)) return answer
|
|
235
|
+
const missing = answerPermissions(event, answer as Partial<Answers>).find((permission) => !isGranted(permission))
|
|
236
|
+
if (missing === undefined) return answer
|
|
237
|
+
throw new Error(`Its ${entry.method} answer changes nothing until you grant ${name} "${permissionWords(missing)}". ${grantFix}`)
|
|
229
238
|
})
|
|
230
239
|
const answer = await foldBefore(event, change, askers, (error) => report(`session:${methodOf('before', event)}`, `The ${methodOf('before', event)} listener failed`, error))
|
|
231
240
|
return answer?.note === undefined ? answer : { ...answer, note: modNote(name, answer.note) }
|
|
@@ -293,17 +302,25 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
293
302
|
}
|
|
294
303
|
}
|
|
295
304
|
|
|
296
|
-
const taskOf = (session: string, data: TaskData): Task =>
|
|
297
|
-
id: data.id
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
305
|
+
const taskOf = (session: string, data: TaskData): Task => {
|
|
306
|
+
const record = data.isBackground === true ? { session: data.id } : { session, task: data.id }
|
|
307
|
+
return {
|
|
308
|
+
id: data.id,
|
|
309
|
+
title: data.title,
|
|
310
|
+
openedBy: data.openedBy,
|
|
311
|
+
openedAt: data.openedAt,
|
|
312
|
+
...(data.closedAt === undefined ? {} : { closedAt: data.closedAt }),
|
|
313
|
+
...(data.status === undefined ? {} : { status: data.status }),
|
|
314
|
+
...(data.name === undefined ? {} : { name: data.name }),
|
|
315
|
+
...(data.spawnedBy === undefined ? {} : { spawnedBy: data.spawnedBy }),
|
|
316
|
+
...(data.isBackground === true ? { isBackground: true } : {}),
|
|
317
|
+
owner: ownerOf(session, data),
|
|
318
|
+
...recordPart({ session, task: data.id }),
|
|
319
|
+
search: (text: string, matcher?: unknown) => searched({ text, ...record }, matcher),
|
|
320
|
+
tasks: async () => (await call<TaskData[]>(conversationMethods.tasks, record)).map((task) => taskOf(record.session, task)),
|
|
321
|
+
messages: () => messagesFrom(conversationMethods.messages, record),
|
|
322
|
+
}
|
|
323
|
+
}
|
|
307
324
|
|
|
308
325
|
const goalOf = (where: () => Scope): Goal =>
|
|
309
326
|
({
|
|
@@ -345,11 +362,9 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
345
362
|
if ('drop' in submitted && typeof submitted.drop === 'string') throw new Error(`${name}: Claude Code dropped the prompt: ${submitted.drop}`)
|
|
346
363
|
}
|
|
347
364
|
|
|
348
|
-
const startHere = async (task: TaskStartArgs): Promise<Task> => taskOf(current.id, await spawnTask(claude, task).catch((error: unknown) => Promise.reject(new Error(`${name}: ${messageOf(error)}`))))
|
|
349
|
-
|
|
350
365
|
const surfacesHere = async (): Promise<Surface[]> => [...(await claude.session.surfaces())]
|
|
351
366
|
|
|
352
|
-
const sessionOf = (read: () => SessionData, owned?: Owned) => {
|
|
367
|
+
const sessionOf = (read: () => SessionData, owned?: Owned, isOwner = owned !== undefined) => {
|
|
353
368
|
const id = () => read().id
|
|
354
369
|
const isHere = () => owned === undefined && id() === current.id
|
|
355
370
|
const scope = (): Scope => (owned === undefined ? { session: id(), isAgent: true } : { session: owned.session, task: owned.task, isAgent: true })
|
|
@@ -411,32 +426,34 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
411
426
|
await act(id(), 'send', message, owned)
|
|
412
427
|
},
|
|
413
428
|
}
|
|
414
|
-
if (
|
|
429
|
+
if (isOwner) return session as unknown as Sessions['current']
|
|
430
|
+
const started = async (starting: Promise<TaskData>): Promise<Task> => taskOf(id(), await starting.catch((error: unknown) => Promise.reject(new Error(`${name}: ${messageOf(error)}`))))
|
|
415
431
|
return Object.assign(session, {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
return taskOf(id(), started)
|
|
420
|
-
},
|
|
432
|
+
startTask: (task: TaskStartArgs): Promise<Task> => started(task.isBackground === true ? act<TaskData>(id(), 'startBackgroundTask', task) : isHere() ? spawnTask(claude, task) : act<TaskData>(id(), 'startTask', task)),
|
|
433
|
+
fork: (task: Pick<TaskStartArgs, 'prompt' | 'title'>): Promise<Task> => started(isHere() ? spawnTask(claude, task, current.id) : act<TaskData>(id(), 'fork', task)),
|
|
434
|
+
clone: (task: Pick<TaskStartArgs, 'prompt' | 'title'>): Promise<Task> => started(act<TaskData>(id(), 'clone', task)),
|
|
421
435
|
callTool: async (toolCall: ToolCallArgs): Promise<ToolResult> => (await withFiles(await act<ToolResult>(id(), 'callTool', toolCall))) as ToolResult,
|
|
422
436
|
}) as unknown as Sessions['current']
|
|
423
437
|
}
|
|
424
438
|
|
|
425
439
|
const ownerOf = (session: string, data: TaskData): TaskOwner => {
|
|
426
440
|
const parent = session === current.id ? current : known.get(session)
|
|
441
|
+
const isBackground = data.isBackground === true
|
|
427
442
|
return sessionOf(
|
|
428
443
|
() => ({
|
|
429
444
|
id: data.id,
|
|
430
445
|
title: data.title,
|
|
431
446
|
project: parent?.project ?? '',
|
|
432
|
-
agentType: data.agentType,
|
|
447
|
+
...(data.agentType === undefined ? {} : { agentType: data.agentType }),
|
|
433
448
|
model: data.model ?? '',
|
|
434
449
|
cwd: parent?.cwd ?? '',
|
|
435
|
-
isRunning: data.closedAt === undefined && (parent?.isRunning ?? true),
|
|
450
|
+
isRunning: data.closedAt === undefined && (isBackground || (parent?.isRunning ?? true)),
|
|
436
451
|
startedAt: data.openedAt,
|
|
437
452
|
lastAt: data.closedAt ?? data.openedAt,
|
|
453
|
+
...(data.forkedFrom === undefined ? {} : { forkedFrom: data.forkedFrom }),
|
|
438
454
|
}),
|
|
439
|
-
{ session, task: data.id },
|
|
455
|
+
isBackground ? undefined : { session, task: data.id },
|
|
456
|
+
true,
|
|
440
457
|
)
|
|
441
458
|
}
|
|
442
459
|
|
|
@@ -494,7 +511,11 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
494
511
|
}
|
|
495
512
|
const event: { readonly on: string; readonly before?: string } = processEvents[named as ProcessName]
|
|
496
513
|
const hook = (isBefore ? event.before : undefined) ?? event.on
|
|
497
|
-
if (method === 'beforeSessionStart') routeOn('session.compact',
|
|
514
|
+
if (method === 'beforeSessionStart') routeOn('session.compact', trackCompaction)
|
|
515
|
+
if (method === 'beforeModelChange') hookOn(event.on, () => {
|
|
516
|
+
const notes = modelNotes.splice(0)
|
|
517
|
+
return notes.length === 0 ? undefined : { additionalContext: notes }
|
|
518
|
+
})
|
|
498
519
|
const failed = (input: unknown, error: Error) => {
|
|
499
520
|
const refusal = refusalFor(method, processPayloadOf(named as ProcessFieldsName, input), error)
|
|
500
521
|
return refusal === undefined ? undefined : hookAnswerOf(name, named, refusal)
|
|
@@ -504,35 +525,38 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
504
525
|
async (input: unknown) => {
|
|
505
526
|
const payload = processPayloadOf(named as ProcessFieldsName, input)
|
|
506
527
|
if (named === 'SessionStart' && payload['source'] === 'compact') {
|
|
507
|
-
|
|
508
|
-
|
|
528
|
+
if (!isBefore) return undefined
|
|
529
|
+
const task = compacting?.agentId
|
|
530
|
+
const model = task === undefined ? await claude.session.model() : undefined
|
|
531
|
+
const answer = await raise(method, { ...payload, ...defined({ task, model, title: current.title || undefined }) })
|
|
532
|
+
if (answer === undefined) return undefined
|
|
533
|
+
return hookAnswerOf(name, named, task === undefined ? answer : defined({ note: answer['note'] }))
|
|
509
534
|
}
|
|
510
535
|
if (!isBefore) {
|
|
511
536
|
void raise(method, payload)
|
|
512
537
|
return undefined
|
|
513
538
|
}
|
|
539
|
+
if (named === 'ModelChange') modelNotes.splice(0)
|
|
514
540
|
const answer = await raise(method, payload)
|
|
515
|
-
|
|
541
|
+
if (answer === undefined) return undefined
|
|
542
|
+
if (named === 'ModelChange' && typeof answer['note'] === 'string' && answer['refuse'] === undefined) modelNotes.push(modNote(name, answer['note']))
|
|
543
|
+
return hookAnswerOf(name, named, answer)
|
|
516
544
|
},
|
|
517
545
|
isBefore ? failed : undefined,
|
|
518
546
|
)
|
|
519
547
|
}
|
|
520
548
|
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
if (!isGranted('prompt')) {
|
|
532
|
-
report('session:beforeSessionStart', 'The beforeSessionStart listener failed', new Error(`${name}: its note adds text Claude reads, which needs the prompt permission.`))
|
|
533
|
-
return compacted
|
|
549
|
+
const modelNotes: string[] = []
|
|
550
|
+
|
|
551
|
+
let compacting: Args<'session.compact'> | undefined
|
|
552
|
+
|
|
553
|
+
const trackCompaction = async (e: Args<'session.compact'>, next: (e: Args<'session.compact'>) => Promise<SessionCompactResult>): Promise<SessionCompactResult> => {
|
|
554
|
+
compacting = e
|
|
555
|
+
try {
|
|
556
|
+
return await next(e)
|
|
557
|
+
} finally {
|
|
558
|
+
if (compacting === e) compacting = undefined
|
|
534
559
|
}
|
|
535
|
-
return { ...compacted, messages: [...compacted.messages, { role: 'user', text: modNote(name, note), toolUses: [] }] }
|
|
536
560
|
}
|
|
537
561
|
|
|
538
562
|
const listen = (method: string, args: readonly unknown[], isEveryAgent = false): Subscription => {
|
|
@@ -654,9 +678,9 @@ const modelChangeOf = (input: HookFields) => ({
|
|
|
654
678
|
|
|
655
679
|
const answerKeys: Readonly<Record<string, readonly string[]>> = {
|
|
656
680
|
Run: ['args', 'note', 'result', 'exitCode', 'refuse'],
|
|
657
|
-
ProjectSetup: [],
|
|
681
|
+
ProjectSetup: ['note'],
|
|
658
682
|
SessionStart: ['note', 'title', 'reloadSkills', 'firstPrompt'],
|
|
659
|
-
ModelChange: ['ask', 'refuse'],
|
|
683
|
+
ModelChange: ['ask', 'refuse', 'note'],
|
|
660
684
|
WorktreeCreate: ['result'],
|
|
661
685
|
Elicitation: ['refuse'],
|
|
662
686
|
ElicitationResponse: ['refuse'],
|
|
@@ -706,19 +730,20 @@ const processFields: { readonly [Name in ProcessFieldsName]: (input: HookFields)
|
|
|
706
730
|
ReplyDisplay: (input) => ({ replyId: input['message_id'], index: input['index'], isLast: input['final'], text: input['delta'] }),
|
|
707
731
|
}
|
|
708
732
|
|
|
709
|
-
export async function spawnTask(claude: Pick<Claude, 'agent'>, task: TaskStartArgs): Promise<TaskData> {
|
|
733
|
+
export async function spawnTask(claude: Pick<Claude, 'agent'>, task: TaskStartArgs, forkedFrom?: string): Promise<TaskData> {
|
|
710
734
|
if (task.title.trim() === '') throw new Error('a task needs a title.')
|
|
735
|
+
const { name, agentType, model, cwd }: TaskStartArgs = forkedFrom === undefined ? task : { prompt: task.prompt, title: task.title, agentType: forkAgentType }
|
|
711
736
|
const started = await claude.agent.spawn({
|
|
712
737
|
prompt: task.prompt,
|
|
713
738
|
description: task.title,
|
|
714
|
-
...(
|
|
715
|
-
...(
|
|
716
|
-
...(
|
|
717
|
-
...(
|
|
739
|
+
...(name === undefined ? {} : { name }),
|
|
740
|
+
...(agentType === undefined ? {} : { subagentType: agentType }),
|
|
741
|
+
...(model === undefined ? {} : { model }),
|
|
742
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
718
743
|
})
|
|
719
744
|
if (started.deny !== undefined) throw new Error(`Claude Code refused the task: ${started.deny}`)
|
|
720
745
|
if (started.agentId === undefined) throw new Error('Claude Code answered without starting a task.')
|
|
721
|
-
return { id: started.agentId, title: task.title, agentType:
|
|
746
|
+
return { id: started.agentId, title: task.title, ...(forkedFrom === undefined ? { agentType: agentType ?? 'general-purpose' } : { forkedFrom }), model: started.model, openedBy: '', openedAt: new Date().toISOString() }
|
|
722
747
|
}
|
|
723
748
|
|
|
724
749
|
export function processPayloadOf(named: ProcessFieldsName, input: unknown): HookFields {
|
|
@@ -734,6 +759,7 @@ function hookAnswerOf(name: string, named: string, answer: HookFields): HookFiel
|
|
|
734
759
|
if (refuse !== undefined) return { permissionDecision: 'deny', permissionDecisionReason: refuse }
|
|
735
760
|
return typeof answer['ask'] === 'string' ? { permissionDecision: 'ask', permissionDecisionReason: answer['ask'] } : undefined
|
|
736
761
|
}
|
|
762
|
+
if (named === 'ProjectSetup') return defined({ additionalContext: note })
|
|
737
763
|
if (named === 'WorktreeCreate') return defined({ worktreePath: answer['result'] })
|
|
738
764
|
if (named === 'PermissionDenial') return answer['retry'] === true ? { retry: true } : undefined
|
|
739
765
|
if (named === 'ToolBatch') return defined({ additionalContext: note })
|
package/src/runtime/toaster.ts
CHANGED
|
@@ -30,9 +30,11 @@ type Entry = {
|
|
|
30
30
|
readonly options: ToastOptions
|
|
31
31
|
readonly actions: readonly Action[]
|
|
32
32
|
readonly hides?: string
|
|
33
|
+
at: Date
|
|
33
34
|
isOpen: boolean
|
|
34
35
|
reporting?: Reporting | undefined
|
|
35
36
|
trying?: string | RenderElement | undefined
|
|
37
|
+
spinsFrom?: number | undefined
|
|
36
38
|
timer: Timer | undefined
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -46,6 +48,8 @@ const defaultDuration = 4000
|
|
|
46
48
|
const visibleToasts = 3
|
|
47
49
|
const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] as const
|
|
48
50
|
const spinnerMs = 100
|
|
51
|
+
const spinFrames = 10_000 / spinnerMs
|
|
52
|
+
const still = '…'
|
|
49
53
|
const icons: Record<Exclude<ToastType, 'loading'>, { readonly glyph: string; readonly color?: string }> = {
|
|
50
54
|
default: { glyph: ' ' },
|
|
51
55
|
success: { glyph: '✓', color: 'success' },
|
|
@@ -71,15 +75,20 @@ export function createToaster(claude: Claude, name: string, version: () => strin
|
|
|
71
75
|
let showsAll = false
|
|
72
76
|
let bandId: string | undefined
|
|
73
77
|
|
|
78
|
+
const isLoading = (entry: Entry) => entry.type === 'loading' || entry.trying !== undefined
|
|
79
|
+
|
|
80
|
+
const isSpinning = (entry: Entry) => entry.spinsFrom !== undefined && frame - entry.spinsFrom < spinFrames
|
|
81
|
+
|
|
74
82
|
const changed = () => {
|
|
75
|
-
const
|
|
76
|
-
|
|
83
|
+
for (const entry of entries.values()) entry.spinsFrom = isLoading(entry) ? (entry.spinsFrom ?? frame) : undefined
|
|
84
|
+
const spins = [...entries.values()].some(isSpinning)
|
|
85
|
+
if (spins && spinning === undefined) {
|
|
77
86
|
spinning = claude.clock.every(spinnerMs, () => {
|
|
78
87
|
frame += 1
|
|
79
|
-
|
|
88
|
+
changed()
|
|
80
89
|
})
|
|
81
90
|
}
|
|
82
|
-
if (!
|
|
91
|
+
if (!spins && spinning !== undefined) {
|
|
83
92
|
spinning.cancel()
|
|
84
93
|
spinning = undefined
|
|
85
94
|
}
|
|
@@ -121,7 +130,7 @@ export function createToaster(claude: Claude, name: string, version: () => strin
|
|
|
121
130
|
}
|
|
122
131
|
if (previous !== undefined) disarm(previous)
|
|
123
132
|
const reporting = hides !== undefined && previous?.hides === hides ? previous.reporting : undefined
|
|
124
|
-
const entry: Entry = { id, type, message, options, actions, ...(hides === undefined ? {} : { hides }), isOpen: previous?.isOpen ?? false, reporting, timer: undefined }
|
|
133
|
+
const entry: Entry = { id, type, message, options, actions, ...(hides === undefined ? {} : { hides }), at: new Date(), isOpen: previous?.isOpen ?? false, reporting, timer: undefined }
|
|
125
134
|
if (type === 'error' && previous?.message !== message) claude.ui.log(`${name}: ${message}${typeof options.description === 'string' ? `: ${options.description}` : ''}`, { to: 'debug' })
|
|
126
135
|
entries.set(id, entry)
|
|
127
136
|
changed()
|
|
@@ -181,14 +190,19 @@ export function createToaster(claude: Claude, name: string, version: () => strin
|
|
|
181
190
|
if (hidden.get(id) === hides) return id
|
|
182
191
|
hidden.delete(id)
|
|
183
192
|
const shown = entries.get(id)
|
|
184
|
-
if (shown?.hides === hides && shown.trying === undefined && shown.options.description === description)
|
|
193
|
+
if (shown?.hides === hides && shown.trying === undefined && shown.options.description === description) {
|
|
194
|
+
const previous = clockTime(shown.at)
|
|
195
|
+
shown.at = new Date()
|
|
196
|
+
if (clockTime(shown.at) !== previous) changed()
|
|
197
|
+
return id
|
|
198
|
+
}
|
|
185
199
|
const stack = error instanceof Error && error.stack !== undefined ? `\n\n${error.stack}` : ''
|
|
186
200
|
const details = async () => {
|
|
187
201
|
const modVersion = await claude.fs
|
|
188
202
|
.read(`${claude.plugin.root}/.claude-plugin/plugin.json`)
|
|
189
203
|
.then((text) => String((JSON.parse(text) as { version?: unknown }).version))
|
|
190
204
|
.catch((failure: unknown) => `unknown (${messageOf(failure)})`)
|
|
191
|
-
return `${name} ${modVersion}, @cmodjs/core ${coreVersion}\n${message}${description === undefined ? '' : `\n${description}`}${stack}`
|
|
205
|
+
return `${name} ${modVersion}, @cmodjs/core ${coreVersion}, at ${(entries.get(id)?.at ?? new Date()).toISOString()}\n${message}${description === undefined ? '' : `\n${description}`}${stack}`
|
|
192
206
|
}
|
|
193
207
|
const startReporting = async () => {
|
|
194
208
|
const shown = entries.get(id)
|
|
@@ -233,13 +247,13 @@ export function createToaster(claude: Claude, name: string, version: () => strin
|
|
|
233
247
|
const row = (entry: Entry): RenderElement => {
|
|
234
248
|
const { description } = entry.options
|
|
235
249
|
const isText = typeof description === 'string'
|
|
236
|
-
const icon = entry.type === 'loading' || entry.trying !== undefined ? { glyph: spinner[frame % spinner.length] as string, color: 'claude' } : icons[entry.type]
|
|
250
|
+
const icon = entry.type === 'loading' || entry.trying !== undefined ? { glyph: isSpinning(entry) ? (spinner[frame % spinner.length] as string) : still, color: 'claude' } : icons[entry.type]
|
|
237
251
|
const message = isText ? Button({ plain: true, key: `toast:${entry.id}:open`, label: entry.message, onPress: toggle(entry) }) : Text({ children: entry.message })
|
|
238
252
|
const details = !isText ? description : Text({ dimColor: true, wrap: entry.isOpen ? 'wrap' : 'truncate-end', children: entry.isOpen ? description : firstLine(description) })
|
|
239
253
|
const fixed = (child: RenderElement) => Box({ flexShrink: 0, children: child })
|
|
240
254
|
const close = Button({ plain: true, dimColor: true, key: `toast:${entry.id}:close`, label: entry.hides === undefined ? '✕' : 'Hide', onPress: () => hide(entry) })
|
|
241
255
|
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) }))
|
|
242
|
-
const buttons = Box({ gap: 2, flexWrap: 'wrap', children: [...actions, close] })
|
|
256
|
+
const buttons = Box({ gap: 2, flexWrap: 'wrap', children: [...actions, close, Text({ dimColor: true, children: clockTime(entry.at) })].map(fixed) })
|
|
243
257
|
const trying = entry.trying === undefined ? undefined : Box({ gap: 1, children: [Text({ dimColor: true, children: 'Trying again:' }), typeof entry.trying === 'string' ? Text({ dimColor: true, wrap: 'truncate-end', children: entry.trying }) : entry.trying] })
|
|
244
258
|
const text = details === undefined ? [message] : [message, trying ?? details]
|
|
245
259
|
const under = trying !== undefined && details === undefined ? [trying] : entry.reporting === undefined ? [buttons] : reportChoices(entry, entry.reporting)
|
|
@@ -362,3 +376,7 @@ function reportPrompt(name: string, contact: Contact | undefined, details: strin
|
|
|
362
376
|
: `Email it to ${contact.address}.`
|
|
363
377
|
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}`
|
|
364
378
|
}
|
|
379
|
+
|
|
380
|
+
function clockTime(at: Date): string {
|
|
381
|
+
return `${String(at.getHours()).padStart(2, '0')}:${String(at.getMinutes()).padStart(2, '0')}`
|
|
382
|
+
}
|