@cmodjs/core 4.0.3 → 4.1.1
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 +44 -9
- package/docs/index.md +3 -3
- package/docs/install-steps.md +8 -8
- 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 +46 -13
- package/docs/testing.md +15 -11
- 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 +8 -5
- 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 +3 -0
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +164 -27
- 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 +10 -6
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +106 -48
- package/runtime/lifecycle.d.ts +2 -2
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +144 -42
- package/runtime/pages.d.ts +3 -1
- package/runtime/pages.d.ts.map +1 -1
- package/runtime/pages.js +48 -14
- 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 +10 -3
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +104 -59
- package/runtime/toaster.d.ts.map +1 -1
- package/runtime/toaster.js +23 -9
- package/runtime/ui.d.ts +22 -4
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +161 -59
- 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 +31 -21
- 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 +15 -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 +36 -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 +11 -8
- package/src/runtime/before.ts +61 -22
- package/src/runtime/buttons.ts +22 -0
- package/src/runtime/conversations.ts +170 -34
- 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 +147 -83
- package/src/runtime/lifecycle.ts +149 -48
- package/src/runtime/pages.ts +56 -12
- package/src/runtime/parts.ts +8 -4
- package/src/runtime/programs.ts +5 -1
- package/src/runtime/sessions.ts +106 -59
- package/src/runtime/toaster.ts +27 -9
- package/src/runtime/ui.ts +166 -62
- package/src/runtime/watches.ts +20 -5
- package/src/runtime/widgets.ts +35 -23
- 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 +31 -6
- package/src/session/goal/task.ts +6 -0
- package/src/session/session.ts +18 -7
- package/src/testing/fake-claude.ts +21 -6
- package/src/testing/fake-elements.ts +57 -8
- package/src/testing/fake-sessions.ts +72 -24
- package/src/testing.ts +114 -33
- 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 +52 -7
- package/testing/fake-sessions.d.ts +24 -10
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +66 -20
- package/testing.d.ts +8 -12
- package/testing.d.ts.map +1 -1
- package/testing.js +113 -27
- 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'
|
|
@@ -39,11 +39,21 @@ export const conversationMethods = {
|
|
|
39
39
|
act: 'cmod:act',
|
|
40
40
|
changed: 'cmod:changed',
|
|
41
41
|
before: 'cmod:before',
|
|
42
|
+
sessionColor: 'cmod:sessionColor',
|
|
42
43
|
} as const
|
|
43
44
|
|
|
44
|
-
export type
|
|
45
|
+
export type SessionColor = { readonly session: string; readonly color?: string }
|
|
45
46
|
|
|
46
|
-
const
|
|
47
|
+
const resetColors: readonly string[] = ['default', 'reset', 'none', 'gray', 'grey']
|
|
48
|
+
|
|
49
|
+
export function pickedColor(args: string): string | undefined {
|
|
50
|
+
const picked = args.trim().toLowerCase()
|
|
51
|
+
return resetColors.includes(picked) ? undefined : picked
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type Act = 'send' | 'startTask' | 'fork' | 'clone' | 'startBackgroundTask' | 'callTool' | 'usage' | 'surfaces' | 'abort'
|
|
55
|
+
|
|
56
|
+
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
57
|
|
|
48
58
|
const startsTurns: readonly string[] = ['CronCreate']
|
|
49
59
|
|
|
@@ -51,6 +61,11 @@ const ownSchedule: readonly string[] = ['CronDelete', 'CronList']
|
|
|
51
61
|
|
|
52
62
|
export const beforePermission = (event: BeforeEvent): string => (event === 'toolCall' ? 'tools' : event === TaskStart ? 'tasks' : 'prompt')
|
|
53
63
|
|
|
64
|
+
const answerPermissions = (event: BeforeEvent, answer: Partial<Answers>): string[] => [
|
|
65
|
+
...(Object.keys(answer).some((key) => key !== 'note') ? [beforePermission(event)] : []),
|
|
66
|
+
...(answer.note === undefined ? [] : ['prompt']),
|
|
67
|
+
]
|
|
68
|
+
|
|
54
69
|
export const isAct = (action: string): action is Act => action === 'callTool' || action in actPermissions
|
|
55
70
|
|
|
56
71
|
export function toolPermission(tool: string): string | undefined {
|
|
@@ -72,7 +87,8 @@ export type SessionsClient = {
|
|
|
72
87
|
listen(method: string, args: readonly unknown[]): Subscription
|
|
73
88
|
turn(data: TurnData): Turn
|
|
74
89
|
/** An empty title keeps the title the session already has. */
|
|
75
|
-
follow(current: Omit<SessionData, 'isRunning' | 'lastAt'>): void
|
|
90
|
+
follow(current: Omit<SessionData, 'isRunning' | 'lastAt' | 'color'>): void
|
|
91
|
+
recolor(told: SessionColor): void
|
|
76
92
|
}
|
|
77
93
|
|
|
78
94
|
type Listening = { scope: Scope; readonly event: SessionEvent; readonly method: string; readonly matcher: unknown; readonly listener: (payload: never, time: ListenTime) => unknown; readonly options: ListenOptions }
|
|
@@ -82,7 +98,8 @@ type Owned = { readonly session: string; readonly task: string }
|
|
|
82
98
|
export async function usageOf(claude: Pick<Claude, 'session'>): Promise<Usage> {
|
|
83
99
|
const [model, usage] = await Promise.all([claude.session.model(), claude.session.usage()])
|
|
84
100
|
const { tokens, window, percent } = usage.context
|
|
85
|
-
|
|
101
|
+
const rateLimits = usage.rateLimits.map(({ kind, percentUsed, resetsAt }) => ({ type: kind, percent: percentUsed, ...(resetsAt === undefined ? {} : { resetsAt }) }))
|
|
102
|
+
return { model, contextWindow: { ...(tokens === undefined ? {} : { tokenCount: tokens }), size: window, ...(percent === undefined ? {} : { percent }) }, rateLimits, ...(usage.cost === undefined ? {} : { cost: { usd: usage.cost.usd } }) }
|
|
86
103
|
}
|
|
87
104
|
|
|
88
105
|
export function payloadOf(change: SessionChange): unknown {
|
|
@@ -90,8 +107,9 @@ export function payloadOf(change: SessionChange): unknown {
|
|
|
90
107
|
if ('turn' in change) return change.turn
|
|
91
108
|
if ('context' in change) return change.context
|
|
92
109
|
if ('task' in change) {
|
|
93
|
-
const { id, title, agentType, model, openedBy, openedAt, closedAt } = change.task
|
|
94
|
-
|
|
110
|
+
const { id, title, agentType, model, openedBy, openedAt, closedAt, isBackground, forkedFrom } = change.task
|
|
111
|
+
const owner = { id, title, ...(agentType === undefined ? {} : { agentType }), ...(model === undefined ? {} : { model }), ...(forkedFrom === undefined ? {} : { forkedFrom }), startedAt: openedAt }
|
|
112
|
+
return { id, title, openedBy, openedAt, ...(closedAt === undefined ? {} : { closedAt }), ...(isBackground === true ? { isBackground } : {}), owner }
|
|
95
113
|
}
|
|
96
114
|
if ('usage' in change) return change.usage
|
|
97
115
|
return change.goal
|
|
@@ -149,6 +167,9 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
149
167
|
const parents = new Map<string, Map<string, string | undefined>>()
|
|
150
168
|
const known = new Map<string, SessionData>()
|
|
151
169
|
let current: SessionData = { id: '', title: '', project: '', cwd: '', model: '', startedAt: '', lastAt: '', isRunning: true }
|
|
170
|
+
let told: SessionColor = { session: '' }
|
|
171
|
+
|
|
172
|
+
const colored = ({ color: _color, ...data }: SessionData): SessionData => (told.session === data.id && told.color !== undefined ? { ...data, color: told.color } : data)
|
|
152
173
|
|
|
153
174
|
const keyOf = (scope: Scope) => scope.session ?? everySession
|
|
154
175
|
|
|
@@ -224,8 +245,10 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
224
245
|
if (!decides(entry, session, latest)) return undefined
|
|
225
246
|
const payload = await payloadFor(session, latest)
|
|
226
247
|
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
|
-
|
|
248
|
+
if (answer === undefined || answer === null || (typeof answer === 'object' && 'refuse' in answer)) return answer
|
|
249
|
+
const missing = answerPermissions(event, answer as Partial<Answers>).find((permission) => !isGranted(permission))
|
|
250
|
+
if (missing === undefined) return answer
|
|
251
|
+
throw new Error(`Its ${entry.method} answer changes nothing until you grant ${name} "${permissionWords(missing)}". ${grantFix}`)
|
|
229
252
|
})
|
|
230
253
|
const answer = await foldBefore(event, change, askers, (error) => report(`session:${methodOf('before', event)}`, `The ${methodOf('before', event)} listener failed`, error))
|
|
231
254
|
return answer?.note === undefined ? answer : { ...answer, note: modNote(name, answer.note) }
|
|
@@ -293,17 +316,25 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
293
316
|
}
|
|
294
317
|
}
|
|
295
318
|
|
|
296
|
-
const taskOf = (session: string, data: TaskData): Task =>
|
|
297
|
-
id: data.id
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
319
|
+
const taskOf = (session: string, data: TaskData): Task => {
|
|
320
|
+
const record = data.isBackground === true ? { session: data.id } : { session, task: data.id }
|
|
321
|
+
return {
|
|
322
|
+
id: data.id,
|
|
323
|
+
title: data.title,
|
|
324
|
+
openedBy: data.openedBy,
|
|
325
|
+
openedAt: data.openedAt,
|
|
326
|
+
...(data.closedAt === undefined ? {} : { closedAt: data.closedAt }),
|
|
327
|
+
...(data.status === undefined ? {} : { status: data.status }),
|
|
328
|
+
...(data.name === undefined ? {} : { name: data.name }),
|
|
329
|
+
...(data.spawnedBy === undefined ? {} : { spawnedBy: data.spawnedBy }),
|
|
330
|
+
...(data.isBackground === true ? { isBackground: true } : {}),
|
|
331
|
+
owner: ownerOf(session, data),
|
|
332
|
+
...recordPart({ session, task: data.id }),
|
|
333
|
+
search: (text: string, matcher?: unknown) => searched({ text, ...record }, matcher),
|
|
334
|
+
tasks: async () => (await call<TaskData[]>(conversationMethods.tasks, record)).map((task) => taskOf(record.session, task)),
|
|
335
|
+
messages: () => messagesFrom(conversationMethods.messages, record),
|
|
336
|
+
}
|
|
337
|
+
}
|
|
307
338
|
|
|
308
339
|
const goalOf = (where: () => Scope): Goal =>
|
|
309
340
|
({
|
|
@@ -345,11 +376,9 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
345
376
|
if ('drop' in submitted && typeof submitted.drop === 'string') throw new Error(`${name}: Claude Code dropped the prompt: ${submitted.drop}`)
|
|
346
377
|
}
|
|
347
378
|
|
|
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
379
|
const surfacesHere = async (): Promise<Surface[]> => [...(await claude.session.surfaces())]
|
|
351
380
|
|
|
352
|
-
const sessionOf = (read: () => SessionData, owned?: Owned) => {
|
|
381
|
+
const sessionOf = (read: () => SessionData, owned?: Owned, isOwner = owned !== undefined) => {
|
|
353
382
|
const id = () => read().id
|
|
354
383
|
const isHere = () => owned === undefined && id() === current.id
|
|
355
384
|
const scope = (): Scope => (owned === undefined ? { session: id(), isAgent: true } : { session: owned.session, task: owned.task, isAgent: true })
|
|
@@ -363,6 +392,9 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
363
392
|
get title() {
|
|
364
393
|
return read().title
|
|
365
394
|
},
|
|
395
|
+
get color() {
|
|
396
|
+
return read().color
|
|
397
|
+
},
|
|
366
398
|
get project() {
|
|
367
399
|
return read().project
|
|
368
400
|
},
|
|
@@ -411,32 +443,34 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
411
443
|
await act(id(), 'send', message, owned)
|
|
412
444
|
},
|
|
413
445
|
}
|
|
414
|
-
if (
|
|
446
|
+
if (isOwner) return session as unknown as Sessions['current']
|
|
447
|
+
const started = async (starting: Promise<TaskData>): Promise<Task> => taskOf(id(), await starting.catch((error: unknown) => Promise.reject(new Error(`${name}: ${messageOf(error)}`))))
|
|
415
448
|
return Object.assign(session, {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
return taskOf(id(), started)
|
|
420
|
-
},
|
|
449
|
+
startTask: (task: TaskStartArgs): Promise<Task> => started(task.isBackground === true ? act<TaskData>(id(), 'startBackgroundTask', task) : isHere() ? spawnTask(claude, task) : act<TaskData>(id(), 'startTask', task)),
|
|
450
|
+
fork: (task: Pick<TaskStartArgs, 'prompt' | 'title'>): Promise<Task> => started(isHere() ? spawnTask(claude, task, current.id) : act<TaskData>(id(), 'fork', task)),
|
|
451
|
+
clone: (task: Pick<TaskStartArgs, 'prompt' | 'title'>): Promise<Task> => started(act<TaskData>(id(), 'clone', task)),
|
|
421
452
|
callTool: async (toolCall: ToolCallArgs): Promise<ToolResult> => (await withFiles(await act<ToolResult>(id(), 'callTool', toolCall))) as ToolResult,
|
|
422
453
|
}) as unknown as Sessions['current']
|
|
423
454
|
}
|
|
424
455
|
|
|
425
456
|
const ownerOf = (session: string, data: TaskData): TaskOwner => {
|
|
426
457
|
const parent = session === current.id ? current : known.get(session)
|
|
458
|
+
const isBackground = data.isBackground === true
|
|
427
459
|
return sessionOf(
|
|
428
460
|
() => ({
|
|
429
461
|
id: data.id,
|
|
430
462
|
title: data.title,
|
|
431
463
|
project: parent?.project ?? '',
|
|
432
|
-
agentType: data.agentType,
|
|
464
|
+
...(data.agentType === undefined ? {} : { agentType: data.agentType }),
|
|
433
465
|
model: data.model ?? '',
|
|
434
466
|
cwd: parent?.cwd ?? '',
|
|
435
|
-
isRunning: data.closedAt === undefined && (parent?.isRunning ?? true),
|
|
467
|
+
isRunning: data.closedAt === undefined && (isBackground || (parent?.isRunning ?? true)),
|
|
436
468
|
startedAt: data.openedAt,
|
|
437
469
|
lastAt: data.closedAt ?? data.openedAt,
|
|
470
|
+
...(data.forkedFrom === undefined ? {} : { forkedFrom: data.forkedFrom }),
|
|
438
471
|
}),
|
|
439
|
-
{ session, task: data.id },
|
|
472
|
+
isBackground ? undefined : { session, task: data.id },
|
|
473
|
+
true,
|
|
440
474
|
)
|
|
441
475
|
}
|
|
442
476
|
|
|
@@ -494,7 +528,11 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
494
528
|
}
|
|
495
529
|
const event: { readonly on: string; readonly before?: string } = processEvents[named as ProcessName]
|
|
496
530
|
const hook = (isBefore ? event.before : undefined) ?? event.on
|
|
497
|
-
if (method === 'beforeSessionStart') routeOn('session.compact',
|
|
531
|
+
if (method === 'beforeSessionStart') routeOn('session.compact', trackCompaction)
|
|
532
|
+
if (method === 'beforeModelChange') hookOn(event.on, () => {
|
|
533
|
+
const notes = modelNotes.splice(0)
|
|
534
|
+
return notes.length === 0 ? undefined : { additionalContext: notes }
|
|
535
|
+
})
|
|
498
536
|
const failed = (input: unknown, error: Error) => {
|
|
499
537
|
const refusal = refusalFor(method, processPayloadOf(named as ProcessFieldsName, input), error)
|
|
500
538
|
return refusal === undefined ? undefined : hookAnswerOf(name, named, refusal)
|
|
@@ -504,35 +542,38 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
504
542
|
async (input: unknown) => {
|
|
505
543
|
const payload = processPayloadOf(named as ProcessFieldsName, input)
|
|
506
544
|
if (named === 'SessionStart' && payload['source'] === 'compact') {
|
|
507
|
-
|
|
508
|
-
|
|
545
|
+
if (!isBefore) return undefined
|
|
546
|
+
const task = compacting?.agentId
|
|
547
|
+
const model = task === undefined ? await claude.session.model() : undefined
|
|
548
|
+
const answer = await raise(method, { ...payload, ...defined({ task, model, title: current.title || undefined }) })
|
|
549
|
+
if (answer === undefined) return undefined
|
|
550
|
+
return hookAnswerOf(name, named, task === undefined ? answer : defined({ note: answer['note'] }))
|
|
509
551
|
}
|
|
510
552
|
if (!isBefore) {
|
|
511
553
|
void raise(method, payload)
|
|
512
554
|
return undefined
|
|
513
555
|
}
|
|
556
|
+
if (named === 'ModelChange') modelNotes.splice(0)
|
|
514
557
|
const answer = await raise(method, payload)
|
|
515
|
-
|
|
558
|
+
if (answer === undefined) return undefined
|
|
559
|
+
if (named === 'ModelChange' && typeof answer['note'] === 'string' && answer['refuse'] === undefined) modelNotes.push(modNote(name, answer['note']))
|
|
560
|
+
return hookAnswerOf(name, named, answer)
|
|
516
561
|
},
|
|
517
562
|
isBefore ? failed : undefined,
|
|
518
563
|
)
|
|
519
564
|
}
|
|
520
565
|
|
|
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
|
|
566
|
+
const modelNotes: string[] = []
|
|
567
|
+
|
|
568
|
+
let compacting: Args<'session.compact'> | undefined
|
|
569
|
+
|
|
570
|
+
const trackCompaction = async (e: Args<'session.compact'>, next: (e: Args<'session.compact'>) => Promise<SessionCompactResult>): Promise<SessionCompactResult> => {
|
|
571
|
+
compacting = e
|
|
572
|
+
try {
|
|
573
|
+
return await next(e)
|
|
574
|
+
} finally {
|
|
575
|
+
if (compacting === e) compacting = undefined
|
|
534
576
|
}
|
|
535
|
-
return { ...compacted, messages: [...compacted.messages, { role: 'user', text: modNote(name, note), toolUses: [] }] }
|
|
536
577
|
}
|
|
537
578
|
|
|
538
579
|
const listen = (method: string, args: readonly unknown[], isEveryAgent = false): Subscription => {
|
|
@@ -608,7 +649,7 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
608
649
|
follow(next) {
|
|
609
650
|
const moved = next.id !== current.id && current.id !== ''
|
|
610
651
|
const previous = current.id
|
|
611
|
-
current = { ...next, title: next.title || (next.id === current.id ? current.title : ''), isRunning: true, lastAt: new Date().toISOString() }
|
|
652
|
+
current = colored({ ...next, title: next.title || (next.id === current.id ? current.title : ''), isRunning: true, lastAt: new Date().toISOString() })
|
|
612
653
|
if (!moved) return
|
|
613
654
|
for (const into of [listening, deciding]) {
|
|
614
655
|
const kept = into.get(previous)
|
|
@@ -621,6 +662,10 @@ export function createSessionsClient({ name, claude, report, isGranted, hookOn,
|
|
|
621
662
|
subscribe(previous)
|
|
622
663
|
subscribe(current.id)
|
|
623
664
|
},
|
|
665
|
+
recolor(next) {
|
|
666
|
+
told = next
|
|
667
|
+
current = colored(current)
|
|
668
|
+
},
|
|
624
669
|
}
|
|
625
670
|
}
|
|
626
671
|
|
|
@@ -654,9 +699,9 @@ const modelChangeOf = (input: HookFields) => ({
|
|
|
654
699
|
|
|
655
700
|
const answerKeys: Readonly<Record<string, readonly string[]>> = {
|
|
656
701
|
Run: ['args', 'note', 'result', 'exitCode', 'refuse'],
|
|
657
|
-
ProjectSetup: [],
|
|
702
|
+
ProjectSetup: ['note'],
|
|
658
703
|
SessionStart: ['note', 'title', 'reloadSkills', 'firstPrompt'],
|
|
659
|
-
ModelChange: ['ask', 'refuse'],
|
|
704
|
+
ModelChange: ['ask', 'refuse', 'note'],
|
|
660
705
|
WorktreeCreate: ['result'],
|
|
661
706
|
Elicitation: ['refuse'],
|
|
662
707
|
ElicitationResponse: ['refuse'],
|
|
@@ -706,19 +751,20 @@ const processFields: { readonly [Name in ProcessFieldsName]: (input: HookFields)
|
|
|
706
751
|
ReplyDisplay: (input) => ({ replyId: input['message_id'], index: input['index'], isLast: input['final'], text: input['delta'] }),
|
|
707
752
|
}
|
|
708
753
|
|
|
709
|
-
export async function spawnTask(claude: Pick<Claude, 'agent'>, task: TaskStartArgs): Promise<TaskData> {
|
|
754
|
+
export async function spawnTask(claude: Pick<Claude, 'agent'>, task: TaskStartArgs, forkedFrom?: string): Promise<TaskData> {
|
|
710
755
|
if (task.title.trim() === '') throw new Error('a task needs a title.')
|
|
756
|
+
const { name, agentType, model, cwd }: TaskStartArgs = forkedFrom === undefined ? task : { prompt: task.prompt, title: task.title, agentType: forkAgentType }
|
|
711
757
|
const started = await claude.agent.spawn({
|
|
712
758
|
prompt: task.prompt,
|
|
713
759
|
description: task.title,
|
|
714
|
-
...(
|
|
715
|
-
...(
|
|
716
|
-
...(
|
|
717
|
-
...(
|
|
760
|
+
...(name === undefined ? {} : { name }),
|
|
761
|
+
...(agentType === undefined ? {} : { subagentType: agentType }),
|
|
762
|
+
...(model === undefined ? {} : { model }),
|
|
763
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
718
764
|
})
|
|
719
765
|
if (started.deny !== undefined) throw new Error(`Claude Code refused the task: ${started.deny}`)
|
|
720
766
|
if (started.agentId === undefined) throw new Error('Claude Code answered without starting a task.')
|
|
721
|
-
return { id: started.agentId, title: task.title, agentType:
|
|
767
|
+
return { id: started.agentId, title: task.title, ...(forkedFrom === undefined ? { agentType: agentType ?? 'general-purpose' } : { forkedFrom }), model: started.model, openedBy: '', openedAt: new Date().toISOString() }
|
|
722
768
|
}
|
|
723
769
|
|
|
724
770
|
export function processPayloadOf(named: ProcessFieldsName, input: unknown): HookFields {
|
|
@@ -734,6 +780,7 @@ function hookAnswerOf(name: string, named: string, answer: HookFields): HookFiel
|
|
|
734
780
|
if (refuse !== undefined) return { permissionDecision: 'deny', permissionDecisionReason: refuse }
|
|
735
781
|
return typeof answer['ask'] === 'string' ? { permissionDecision: 'ask', permissionDecisionReason: answer['ask'] } : undefined
|
|
736
782
|
}
|
|
783
|
+
if (named === 'ProjectSetup') return defined({ additionalContext: note })
|
|
737
784
|
if (named === 'WorktreeCreate') return defined({ worktreePath: answer['result'] })
|
|
738
785
|
if (named === 'PermissionDenial') return answer['retry'] === true ? { retry: true } : undefined
|
|
739
786
|
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
|
+
}
|