@cmodjs/core 0.2.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/commands.md +58 -10
- package/docs/dependencies.md +12 -12
- package/docs/hooks.md +41 -28
- package/docs/index.md +29 -25
- package/docs/install-steps.md +9 -7
- package/docs/jobs.md +59 -111
- package/docs/mod.md +200 -174
- package/docs/options.md +2 -2
- package/docs/permissions.md +27 -16
- package/docs/session.md +248 -0
- package/docs/state.md +11 -7
- package/docs/testing.md +66 -23
- package/docs/ui.md +108 -76
- package/jobs/check.d.ts.map +1 -1
- package/jobs/check.js +5 -2
- package/jobs/context.d.ts +7 -8
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +23 -73
- package/jobs/permissions/decide-permission.d.ts +5 -3
- package/jobs/permissions/decide-permission.d.ts.map +1 -1
- package/jobs/permissions/decide-permission.js +6 -4
- package/jobs/permissions/find-project-scope.d.ts +1 -1
- package/jobs/permissions/find-project-scope.d.ts.map +1 -1
- package/jobs/permissions/find-project-scope.js +8 -11
- package/jobs/permissions/match-target.d.ts +2 -2
- package/jobs/permissions/match-target.d.ts.map +1 -1
- package/jobs/permissions.d.ts +1 -1
- package/jobs/permissions.d.ts.map +1 -1
- package/jobs/permissions.js +6 -4
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +8 -3
- package/jobs/prompt.d.ts +4 -2
- package/jobs/prompt.d.ts.map +1 -1
- package/jobs/prompt.js +16 -15
- package/jobs/schedule.d.ts +2 -2
- package/jobs/schedule.d.ts.map +1 -1
- package/jobs/schedule.js +9 -9
- package/jobs/slash-command.d.ts +4 -4
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +14 -9
- package/jobs/status-line.d.ts +1 -11
- package/jobs/status-line.d.ts.map +1 -1
- package/jobs/status-line.js +8 -12
- package/jobs/tool.d.ts +2 -2
- package/jobs/tool.d.ts.map +1 -1
- package/jobs/tool.js +15 -9
- package/mod.d.ts +154 -75
- package/mod.d.ts.map +1 -1
- package/mod.js +94 -3
- package/options.d.ts +2 -2
- package/options.js +19 -19
- package/package.json +3 -3
- package/records.d.ts +10 -8
- package/records.d.ts.map +1 -1
- package/records.js +28 -17
- package/register.d.ts.map +1 -1
- package/register.js +18 -6
- package/runtime/announce.d.ts +18 -0
- package/runtime/announce.d.ts.map +1 -0
- package/runtime/announce.js +66 -0
- package/runtime/bound.d.ts +4 -0
- package/runtime/bound.d.ts.map +1 -0
- package/runtime/bound.js +9 -0
- package/runtime/claude.d.ts +3 -3
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +16 -0
- package/runtime/conversations.d.ts.map +1 -0
- package/runtime/conversations.js +291 -0
- package/runtime/deadline.d.ts +0 -1
- package/runtime/deadline.d.ts.map +1 -1
- package/runtime/deadline.js +1 -1
- package/runtime/dependencies.d.ts +2 -0
- package/runtime/dependencies.d.ts.map +1 -1
- package/runtime/dependencies.js +9 -0
- package/runtime/engine.d.ts +17 -0
- package/runtime/engine.d.ts.map +1 -0
- package/runtime/engine.js +10 -0
- package/runtime/grants.d.ts +4 -0
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +63 -33
- package/runtime/hooks.d.ts +5 -4
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +10 -5
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +11 -9
- package/runtime/lifecycle.d.ts +3 -1
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +317 -186
- package/runtime/metadata.d.ts +3 -1
- package/runtime/metadata.d.ts.map +1 -1
- package/runtime/metadata.js +21 -11
- package/runtime/options.d.ts +3 -1
- package/runtime/options.d.ts.map +1 -1
- package/runtime/options.js +4 -4
- package/runtime/programs.d.ts +8 -0
- package/runtime/programs.d.ts.map +1 -1
- package/runtime/programs.js +39 -1
- package/runtime/router.d.ts +2 -1
- package/runtime/router.d.ts.map +1 -1
- package/runtime/router.js +11 -3
- package/runtime/sessions.d.ts +36 -0
- package/runtime/sessions.d.ts.map +1 -0
- package/runtime/sessions.js +235 -0
- package/runtime/state.d.ts +6 -2
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +46 -13
- package/runtime/toaster.d.ts +31 -0
- package/runtime/toaster.d.ts.map +1 -0
- package/runtime/toaster.js +176 -0
- package/runtime/ui.d.ts +6 -17
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +36 -112
- package/session/context/context.d.ts +10 -0
- package/session/context/context.d.ts.map +1 -0
- package/session/context/context.js +2 -0
- package/session/context/turn/message/document.d.ts +7 -0
- package/session/context/turn/message/document.d.ts.map +1 -0
- package/session/context/turn/message/document.js +1 -0
- package/session/context/turn/message/message.d.ts +54 -0
- package/session/context/turn/message/message.d.ts.map +1 -0
- package/session/context/turn/message/message.js +137 -0
- package/session/context/turn/message/note.d.ts +8 -0
- package/session/context/turn/message/note.d.ts.map +1 -0
- package/session/context/turn/message/note.js +1 -0
- package/session/context/turn/message/notice.d.ts +6 -0
- package/session/context/turn/message/notice.d.ts.map +1 -0
- package/session/context/turn/message/notice.js +1 -0
- package/session/context/turn/message/prompt.d.ts +8 -0
- package/session/context/turn/message/prompt.d.ts.map +1 -0
- package/session/context/turn/message/prompt.js +1 -0
- package/session/context/turn/message/reply.d.ts +6 -0
- package/session/context/turn/message/reply.d.ts.map +1 -0
- package/session/context/turn/message/reply.js +1 -0
- package/session/context/turn/message/sender.d.ts +14 -0
- package/session/context/turn/message/sender.d.ts.map +1 -0
- package/session/context/turn/message/sender.js +30 -0
- package/session/context/turn/message/skill.d.ts +7 -0
- package/session/context/turn/message/skill.d.ts.map +1 -0
- package/session/context/turn/message/skill.js +1 -0
- package/session/context/turn/message/tool-call.d.ts +9 -0
- package/session/context/turn/message/tool-call.d.ts.map +1 -0
- package/session/context/turn/message/tool-call.js +1 -0
- package/session/context/turn/message/tool-result.d.ts +8 -0
- package/session/context/turn/message/tool-result.d.ts.map +1 -0
- package/session/context/turn/message/tool-result.js +1 -0
- package/session/context/turn/turn.d.ts +12 -0
- package/session/context/turn/turn.d.ts.map +1 -0
- package/session/context/turn/turn.js +2 -0
- package/session/conversation.d.ts +64 -0
- package/session/conversation.d.ts.map +1 -0
- package/session/conversation.js +123 -0
- package/session/goal/goal.d.ts +10 -0
- package/session/goal/goal.d.ts.map +1 -0
- package/session/goal/goal.js +1 -0
- package/session/goal/task.d.ts +16 -0
- package/session/goal/task.d.ts.map +1 -0
- package/session/goal/task.js +3 -0
- package/session/session.d.ts +99 -0
- package/session/session.d.ts.map +1 -0
- package/session/session.js +1 -0
- package/src/jobs/check.ts +7 -4
- package/src/jobs/context.ts +24 -82
- package/src/jobs/permissions/decide-permission.ts +10 -8
- package/src/jobs/permissions/find-project-scope.ts +8 -14
- package/src/jobs/permissions/match-target.ts +3 -3
- package/src/jobs/permissions.ts +8 -6
- package/src/jobs/program.ts +7 -3
- package/src/jobs/prompt.ts +21 -24
- package/src/jobs/schedule.ts +14 -14
- package/src/jobs/slash-command.ts +20 -14
- package/src/jobs/status-line.ts +9 -21
- package/src/jobs/tool.ts +17 -11
- package/src/mod.ts +209 -97
- package/src/options.ts +22 -22
- package/src/records.ts +35 -23
- package/src/register.ts +18 -6
- package/src/runtime/announce.ts +74 -0
- package/src/runtime/bound.ts +12 -0
- package/src/runtime/claude.ts +5 -3
- package/src/runtime/conversations.ts +302 -0
- package/src/runtime/deadline.ts +2 -2
- package/src/runtime/dependencies.ts +12 -0
- package/src/runtime/engine.ts +22 -0
- package/src/runtime/grants.ts +66 -31
- package/src/runtime/hooks.ts +19 -10
- package/src/runtime/installer.ts +10 -9
- package/src/runtime/lifecycle.ts +313 -181
- package/src/runtime/metadata.ts +33 -11
- package/src/runtime/options.ts +19 -4
- package/src/runtime/programs.ts +40 -1
- package/src/runtime/router.ts +11 -4
- package/src/runtime/sessions.ts +242 -0
- package/src/runtime/state.ts +50 -14
- package/src/runtime/toaster.ts +218 -0
- package/src/runtime/ui.ts +42 -139
- package/src/session/context/context.ts +12 -0
- package/src/session/context/turn/message/document.ts +8 -0
- package/src/session/context/turn/message/message.ts +165 -0
- package/src/session/context/turn/message/note.ts +9 -0
- package/src/session/context/turn/message/notice.ts +7 -0
- package/src/session/context/turn/message/prompt.ts +9 -0
- package/src/session/context/turn/message/reply.ts +7 -0
- package/src/session/context/turn/message/sender.ts +38 -0
- package/src/session/context/turn/message/skill.ts +8 -0
- package/src/session/context/turn/message/tool-call.ts +10 -0
- package/src/session/context/turn/message/tool-result.ts +9 -0
- package/src/session/context/turn/turn.ts +14 -0
- package/src/session/conversation.ts +169 -0
- package/src/session/goal/goal.ts +9 -0
- package/src/session/goal/task.ts +19 -0
- package/src/session/session.ts +94 -0
- package/src/testing/fake-claude.ts +23 -18
- package/src/testing/fake-elements.ts +3 -2
- package/src/testing/fake-files.ts +1 -1
- package/src/testing/fake-sessions.ts +203 -0
- package/src/testing.ts +112 -33
- package/src/ui/components.ts +3 -3
- package/src/ui/elements.ts +22 -8
- package/src/ui/markdown.ts +22 -26
- package/src/ui/places.ts +49 -0
- package/src/utils/call-effects.ts +3 -3
- package/src/utils/changelog.ts +32 -0
- package/src/utils/matcher.ts +37 -0
- package/src/utils/metadata.ts +17 -15
- package/src/utils/parse-shell.ts +24 -16
- package/src/utils/paths.ts +15 -1
- package/src/utils/text.ts +4 -0
- package/testing/fake-claude.d.ts +4 -2
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +18 -12
- package/testing/fake-elements.js +4 -2
- package/testing/fake-files.js +1 -1
- package/testing/fake-sessions.d.ts +74 -0
- package/testing/fake-sessions.d.ts.map +1 -0
- package/testing/fake-sessions.js +170 -0
- package/testing.d.ts +33 -13
- package/testing.d.ts.map +1 -1
- package/testing.js +92 -21
- package/ui/components.d.ts +2 -3
- package/ui/components.d.ts.map +1 -1
- package/ui/components.js +3 -3
- package/ui/elements.d.ts.map +1 -1
- package/ui/elements.js +19 -9
- package/ui/markdown.d.ts +35 -33
- package/ui/markdown.d.ts.map +1 -1
- package/ui/markdown.js +14 -16
- package/ui/places.d.ts +43 -0
- package/ui/places.d.ts.map +1 -0
- package/ui/places.js +31 -0
- package/utils/call-effects.d.ts +1 -1
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +2 -2
- package/utils/changelog.d.ts +9 -0
- package/utils/changelog.d.ts.map +1 -0
- package/utils/changelog.js +29 -0
- package/utils/matcher.d.ts +13 -0
- package/utils/matcher.d.ts.map +1 -0
- package/utils/matcher.js +32 -0
- package/utils/metadata.d.ts +3 -3
- package/utils/metadata.js +18 -14
- package/utils/parse-shell.d.ts +1 -0
- package/utils/parse-shell.d.ts.map +1 -1
- package/utils/parse-shell.js +22 -13
- package/utils/paths.d.ts +2 -0
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +13 -1
- package/utils/text.d.ts +1 -0
- package/utils/text.d.ts.map +1 -1
- package/utils/text.js +3 -0
- package/src/ui/slots.ts +0 -36
- package/ui/slots.d.ts +0 -208
- package/ui/slots.d.ts.map +0 -1
- package/ui/slots.js +0 -19
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isObject } from '../../../../records.js'
|
|
2
|
+
import { Task } from '../../../goal/task.js'
|
|
3
|
+
|
|
4
|
+
export const Person = 'person'
|
|
5
|
+
|
|
6
|
+
export const Mod = 'mod'
|
|
7
|
+
|
|
8
|
+
export const Schedule = 'schedule'
|
|
9
|
+
|
|
10
|
+
export const Channel = 'channel'
|
|
11
|
+
|
|
12
|
+
export const Sdk = 'sdk'
|
|
13
|
+
|
|
14
|
+
export type SenderType = typeof Person | typeof Mod | 'session' | typeof Task | typeof Schedule | typeof Channel | typeof Sdk
|
|
15
|
+
|
|
16
|
+
export type Sender = {
|
|
17
|
+
readonly type: SenderType
|
|
18
|
+
readonly name?: string
|
|
19
|
+
readonly as?: typeof Person
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const personOrigins: readonly string[] = ['human', 'composer', 'bridge', 'slack-ping', 'auto-continuation']
|
|
23
|
+
|
|
24
|
+
const sessionOrigins: readonly string[] = ['peer', 'peer-send-message', 'coordinator', 'projects-relay', 'observer', 'observer-activity']
|
|
25
|
+
|
|
26
|
+
export function senderOf(origin: unknown, isInTask: boolean): Sender {
|
|
27
|
+
const fields: Readonly<Record<string, unknown>> = isObject(origin) ? origin : {}
|
|
28
|
+
const kind = typeof fields['kind'] === 'string' ? fields['kind'] : undefined
|
|
29
|
+
if (kind === undefined) return { type: isInTask ? Task : Person }
|
|
30
|
+
if (personOrigins.includes(kind)) return { type: Person }
|
|
31
|
+
if (sessionOrigins.includes(kind)) return { type: 'session' }
|
|
32
|
+
if (kind === 'plugin' && typeof fields['name'] === 'string') return { type: Mod, name: fields['name'], ...(fields['asUser'] === true ? { as: Person } : {}) }
|
|
33
|
+
if (kind === 'scheduled-trigger') return { type: Schedule }
|
|
34
|
+
if (kind === 'channel' && typeof fields['server'] === 'string') return { type: Channel, name: fields['server'] }
|
|
35
|
+
if (kind === 'sdk') return { type: Sdk }
|
|
36
|
+
if (kind === 'task-notification') return { type: Task }
|
|
37
|
+
return { type: isInTask ? Task : Person }
|
|
38
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MessageOf } from './message.js'
|
|
2
|
+
|
|
3
|
+
export const ToolCall = 'toolCall'
|
|
4
|
+
|
|
5
|
+
export type ToolCall = MessageOf<typeof ToolCall> & {
|
|
6
|
+
readonly callId: string
|
|
7
|
+
readonly name: string
|
|
8
|
+
readonly input: Readonly<Record<string, unknown>>
|
|
9
|
+
readonly cwd: string
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Message } from './message/message.js'
|
|
2
|
+
import type { Prompt } from './message/prompt.js'
|
|
3
|
+
|
|
4
|
+
export const TurnStart = 'turnStart'
|
|
5
|
+
|
|
6
|
+
export const TurnEnd = 'turnEnd'
|
|
7
|
+
|
|
8
|
+
export type Turn = {
|
|
9
|
+
readonly prompt: Prompt
|
|
10
|
+
readonly context: number
|
|
11
|
+
readonly openedAt: string
|
|
12
|
+
readonly closedAt?: string
|
|
13
|
+
messages(): Promise<Message[]>
|
|
14
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { ContextEnd, ContextStart, type Context } from './context/context.js'
|
|
2
|
+
import { Message, messagesOf, tagOf, textOf, type Row } from './context/turn/message/message.js'
|
|
3
|
+
import { Note } from './context/turn/message/note.js'
|
|
4
|
+
import { Prompt } from './context/turn/message/prompt.js'
|
|
5
|
+
import { ToolCall } from './context/turn/message/tool-call.js'
|
|
6
|
+
import { ToolResult } from './context/turn/message/tool-result.js'
|
|
7
|
+
import { TurnEnd, TurnStart, type Turn } from './context/turn/turn.js'
|
|
8
|
+
import { GoalChange, type Goal } from './goal/goal.js'
|
|
9
|
+
import { Task, TaskEnd, TaskStart } from './goal/task.js'
|
|
10
|
+
|
|
11
|
+
export type SessionData = {
|
|
12
|
+
readonly id: string
|
|
13
|
+
readonly title: string
|
|
14
|
+
readonly project: string
|
|
15
|
+
readonly cwd: string
|
|
16
|
+
readonly model: string
|
|
17
|
+
readonly startedAt: string
|
|
18
|
+
readonly lastAt: string
|
|
19
|
+
readonly forkedFrom?: string
|
|
20
|
+
readonly isRunning: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ContextData = Omit<Context, 'turns'>
|
|
24
|
+
|
|
25
|
+
export type TurnData = Omit<Turn, 'messages'>
|
|
26
|
+
|
|
27
|
+
export type TaskData = Omit<Task, 'messages' | 'tasks'> & { readonly parent?: string }
|
|
28
|
+
|
|
29
|
+
export type GoalData = Omit<Goal, 'update' | 'tasks'>
|
|
30
|
+
|
|
31
|
+
export type SessionChange =
|
|
32
|
+
| { readonly type: typeof Message; readonly message: Message }
|
|
33
|
+
| { readonly type: typeof TurnStart | typeof TurnEnd; readonly turn: TurnData }
|
|
34
|
+
| { readonly type: typeof ContextStart | typeof ContextEnd; readonly context: ContextData }
|
|
35
|
+
| { readonly type: typeof TaskStart | typeof TaskEnd; readonly task: TaskData & { readonly context: number } }
|
|
36
|
+
| { readonly type: typeof GoalChange; readonly goal: GoalData }
|
|
37
|
+
|
|
38
|
+
export type ConversationState = {
|
|
39
|
+
readonly session: string
|
|
40
|
+
readonly context: number
|
|
41
|
+
readonly contextOpenedAt: string
|
|
42
|
+
readonly turn?: TurnData
|
|
43
|
+
readonly goal?: string
|
|
44
|
+
readonly agentCalls: readonly string[]
|
|
45
|
+
readonly endedCalls: Readonly<Record<string, string>>
|
|
46
|
+
readonly tasks: Readonly<Record<string, TaskData & { readonly context: number }>>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type Conversation = {
|
|
50
|
+
add(row: Row, task?: string): SessionChange[]
|
|
51
|
+
startTask(task: TaskData): SessionChange[]
|
|
52
|
+
endContext(at: string): SessionChange[]
|
|
53
|
+
startContext(at: string): SessionChange[]
|
|
54
|
+
changeGoal(text: string): SessionChange[]
|
|
55
|
+
readonly state: ConversationState
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const searchLimit = 50
|
|
59
|
+
|
|
60
|
+
const agentTools: readonly string[] = ['Agent', 'Task']
|
|
61
|
+
|
|
62
|
+
const asyncLaunch = 'Async agent launched'
|
|
63
|
+
|
|
64
|
+
export function emptyConversation(session: string): ConversationState {
|
|
65
|
+
return { session, context: 0, contextOpenedAt: '', agentCalls: [], endedCalls: {}, tasks: {} }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createConversation(start: ConversationState): Conversation {
|
|
69
|
+
let { context, contextOpenedAt, turn, goal } = start
|
|
70
|
+
const agentCalls = new Set(start.agentCalls)
|
|
71
|
+
const endedCalls = new Map(Object.entries(start.endedCalls))
|
|
72
|
+
const tasks = new Map(Object.entries(start.tasks))
|
|
73
|
+
|
|
74
|
+
const startContext = (at: string): SessionChange[] => {
|
|
75
|
+
context += 1
|
|
76
|
+
contextOpenedAt = at
|
|
77
|
+
return [{ type: ContextStart, context: { number: context, openedAt: at } }]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const endContext = (at: string): SessionChange[] => (context === 0 ? [] : [{ type: ContextEnd, context: { number: context, openedAt: contextOpenedAt, closedAt: at } }])
|
|
81
|
+
|
|
82
|
+
const endTurn = (at: string): SessionChange[] => {
|
|
83
|
+
if (turn === undefined) return []
|
|
84
|
+
const ended = { ...turn, closedAt: at }
|
|
85
|
+
turn = undefined
|
|
86
|
+
return [{ type: TurnEnd, turn: ended }]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const endTask = (call: string, at: string): SessionChange[] => {
|
|
90
|
+
const task = [...tasks.values()].find((each) => each.openedBy === call)
|
|
91
|
+
if (task === undefined) {
|
|
92
|
+
if (agentCalls.has(call)) endedCalls.set(call, at)
|
|
93
|
+
return []
|
|
94
|
+
}
|
|
95
|
+
if (task.closedAt !== undefined) return []
|
|
96
|
+
const ended = { ...task, closedAt: at }
|
|
97
|
+
tasks.set(task.id, ended)
|
|
98
|
+
agentCalls.delete(call)
|
|
99
|
+
return [{ type: TaskEnd, task: ended }]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const changeGoal = (text: string): SessionChange[] => {
|
|
103
|
+
goal = text
|
|
104
|
+
return [{ type: GoalChange, goal: { text } }]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
add(row, task) {
|
|
109
|
+
const changes: SessionChange[] = []
|
|
110
|
+
if (task === undefined && context === 0) changes.push(...startContext(row.at))
|
|
111
|
+
if (task === undefined && row.type === 'system' && row.name === 'turn_duration') return [...changes, ...endTurn(row.at)]
|
|
112
|
+
if (task === undefined && row.type === 'system' && row.name === 'compact_boundary') return [...changes, ...endContext(row.at), ...startContext(row.at)]
|
|
113
|
+
const owner = task === undefined ? undefined : tasks.get(task)
|
|
114
|
+
for (const unplaced of messagesOf(row, task !== undefined)) {
|
|
115
|
+
if (task === undefined && unplaced.type === Prompt) {
|
|
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
|
|
119
|
+
changes.push({ type: TurnStart, turn: opened })
|
|
120
|
+
if (goal === undefined) changes.push(...changeGoal(unplaced.text))
|
|
121
|
+
}
|
|
122
|
+
const placed = {
|
|
123
|
+
...unplaced,
|
|
124
|
+
session: start.session,
|
|
125
|
+
context: owner?.context ?? context,
|
|
126
|
+
...(task === undefined && turn !== undefined ? { turn: turn.prompt.id } : {}),
|
|
127
|
+
...(task === undefined ? {} : { task }),
|
|
128
|
+
} as Message
|
|
129
|
+
changes.push({ type: Message, message: placed })
|
|
130
|
+
if (task !== undefined) continue
|
|
131
|
+
if (placed.type === ToolCall && agentTools.includes(placed.name)) agentCalls.add(placed.callId)
|
|
132
|
+
if (placed.type === ToolResult && agentCalls.has(placed.callId) && !placed.text.startsWith(asyncLaunch)) changes.push(...endTask(placed.callId, row.at))
|
|
133
|
+
if (placed.type === Note && placed.from.type === Task) {
|
|
134
|
+
const call = tagOf(textOf(row.content), 'tool-use-id')
|
|
135
|
+
if (call !== undefined && agentCalls.has(call)) changes.push(...endTask(call, row.at))
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return changes
|
|
139
|
+
},
|
|
140
|
+
startTask(data) {
|
|
141
|
+
if (tasks.has(data.id)) return []
|
|
142
|
+
const started = { ...data, context }
|
|
143
|
+
tasks.set(data.id, started)
|
|
144
|
+
agentCalls.add(data.openedBy)
|
|
145
|
+
const changes: SessionChange[] = [{ type: TaskStart, task: started }]
|
|
146
|
+
const endedAt = endedCalls.get(data.openedBy)
|
|
147
|
+
if (endedAt !== undefined) {
|
|
148
|
+
endedCalls.delete(data.openedBy)
|
|
149
|
+
changes.push(...endTask(data.openedBy, endedAt))
|
|
150
|
+
}
|
|
151
|
+
return changes
|
|
152
|
+
},
|
|
153
|
+
endContext,
|
|
154
|
+
startContext,
|
|
155
|
+
changeGoal,
|
|
156
|
+
get state() {
|
|
157
|
+
return {
|
|
158
|
+
session: start.session,
|
|
159
|
+
context,
|
|
160
|
+
contextOpenedAt,
|
|
161
|
+
...(turn === undefined ? {} : { turn }),
|
|
162
|
+
...(goal === undefined ? {} : { goal }),
|
|
163
|
+
agentCalls: [...agentCalls],
|
|
164
|
+
endedCalls: Object.fromEntries(endedCalls),
|
|
165
|
+
tasks: Object.fromEntries(tasks),
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Message } from '../context/turn/message/message.js'
|
|
2
|
+
|
|
3
|
+
export const Task = 'task'
|
|
4
|
+
|
|
5
|
+
export const TaskStart = 'taskStart'
|
|
6
|
+
|
|
7
|
+
export const TaskEnd = 'taskEnd'
|
|
8
|
+
|
|
9
|
+
export type Task = {
|
|
10
|
+
readonly id: string
|
|
11
|
+
readonly title: string
|
|
12
|
+
readonly agentType: string
|
|
13
|
+
readonly model?: string
|
|
14
|
+
readonly openedBy: string
|
|
15
|
+
readonly openedAt: string
|
|
16
|
+
readonly closedAt?: string
|
|
17
|
+
messages(): Promise<Message[]>
|
|
18
|
+
tasks(): Promise<Task[]>
|
|
19
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Disposable } from '../mod.js'
|
|
2
|
+
import type { Matcher } from '../utils/matcher.js'
|
|
3
|
+
import type { Context, ContextEnd, ContextStart } from './context/context.js'
|
|
4
|
+
import type { Document } from './context/turn/message/document.js'
|
|
5
|
+
import type { Message, MessageType } from './context/turn/message/message.js'
|
|
6
|
+
import type { Note } from './context/turn/message/note.js'
|
|
7
|
+
import type { Notice } from './context/turn/message/notice.js'
|
|
8
|
+
import type { Prompt } from './context/turn/message/prompt.js'
|
|
9
|
+
import type { Reply } from './context/turn/message/reply.js'
|
|
10
|
+
import type { Person } from './context/turn/message/sender.js'
|
|
11
|
+
import type { Skill } from './context/turn/message/skill.js'
|
|
12
|
+
import type { ToolCall } from './context/turn/message/tool-call.js'
|
|
13
|
+
import type { ToolResult } from './context/turn/message/tool-result.js'
|
|
14
|
+
import type { Turn, TurnEnd, TurnStart } from './context/turn/turn.js'
|
|
15
|
+
import type { Goal, GoalChange } from './goal/goal.js'
|
|
16
|
+
import type { Task, TaskEnd, TaskStart } from './goal/task.js'
|
|
17
|
+
|
|
18
|
+
export const Session = 'session'
|
|
19
|
+
|
|
20
|
+
export type SessionEvent = typeof Message | MessageType | typeof ContextStart | typeof ContextEnd | typeof TurnStart | typeof TurnEnd | typeof TaskStart | typeof TaskEnd | typeof GoalChange
|
|
21
|
+
|
|
22
|
+
export type SessionPayload = {
|
|
23
|
+
readonly message: Message
|
|
24
|
+
readonly prompt: Prompt
|
|
25
|
+
readonly note: Note
|
|
26
|
+
readonly reply: Reply
|
|
27
|
+
readonly toolCall: ToolCall
|
|
28
|
+
readonly toolResult: ToolResult
|
|
29
|
+
readonly skill: Skill
|
|
30
|
+
readonly document: Document
|
|
31
|
+
readonly notice: Notice
|
|
32
|
+
readonly contextStart: Context
|
|
33
|
+
readonly contextEnd: Context
|
|
34
|
+
readonly turnStart: Turn
|
|
35
|
+
readonly turnEnd: Turn
|
|
36
|
+
readonly taskStart: Task
|
|
37
|
+
readonly taskEnd: Task
|
|
38
|
+
readonly goalChange: Goal
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type Usage = {
|
|
42
|
+
readonly model: string
|
|
43
|
+
readonly context: { readonly tokens?: number; readonly window: number; readonly percent?: number }
|
|
44
|
+
readonly cost?: { readonly usd: number }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type Listener<Event extends SessionEvent> = (payload: SessionPayload[Event]) => unknown
|
|
48
|
+
|
|
49
|
+
export type SessionSend = {
|
|
50
|
+
readonly type: typeof Prompt | typeof Note
|
|
51
|
+
readonly text: string
|
|
52
|
+
readonly as?: typeof Person | typeof Session
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type TaskStartArgs = {
|
|
56
|
+
readonly prompt: string
|
|
57
|
+
readonly agentType?: string
|
|
58
|
+
readonly model?: string
|
|
59
|
+
readonly name?: string
|
|
60
|
+
readonly cwd?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type Session = {
|
|
64
|
+
readonly id: string
|
|
65
|
+
readonly title: string
|
|
66
|
+
readonly project: string
|
|
67
|
+
readonly cwd: string
|
|
68
|
+
readonly model: string
|
|
69
|
+
readonly startedAt: string
|
|
70
|
+
readonly lastAt: string
|
|
71
|
+
readonly forkedFrom?: string
|
|
72
|
+
readonly isRunning: boolean
|
|
73
|
+
usage(): Promise<Usage>
|
|
74
|
+
on<Event extends SessionEvent>(event: Event, listener: Listener<Event>): Disposable
|
|
75
|
+
on<Event extends SessionEvent>(event: Event, matcher: Matcher<SessionPayload[Event]>, listener: Listener<Event>): Disposable
|
|
76
|
+
send(message: SessionSend): Promise<void>
|
|
77
|
+
startTask(task: TaskStartArgs): Promise<Task>
|
|
78
|
+
messages(options?: { readonly after?: string; readonly last?: number }): Promise<Message[]>
|
|
79
|
+
turns(options?: { readonly last?: number }): Promise<Turn[]>
|
|
80
|
+
contexts(): Promise<Context[]>
|
|
81
|
+
goal(): Promise<Goal>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type SessionScope = {
|
|
85
|
+
readonly running?: boolean
|
|
86
|
+
readonly since?: string
|
|
87
|
+
readonly everyProject?: boolean
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type Sessions = {
|
|
91
|
+
readonly current: Session
|
|
92
|
+
list(options?: SessionScope): Promise<Session[]>
|
|
93
|
+
search(text: string, options?: SessionScope & { readonly limit?: number; readonly before?: Pick<Message, 'session' | 'id'> }): Promise<Message[]>
|
|
94
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type { Timer } from 'claude-code'
|
|
1
|
+
import type { EventResult, Timer } from 'claude-code'
|
|
2
|
+
import { isObject } from '../records.js'
|
|
2
3
|
import type { Claude } from '../runtime/claude.js'
|
|
3
4
|
import { elements } from './fake-elements.js'
|
|
5
|
+
import { fakeFiles } from './fake-files.js'
|
|
4
6
|
|
|
5
7
|
export type TestCall = {
|
|
6
8
|
readonly call: string
|
|
@@ -12,9 +14,9 @@ export type Fakes = {
|
|
|
12
14
|
fs: { read?: Claude['fs']['read']; write?: Claude['fs']['write']; list?: Claude['fs']['list']; exists?: Claude['fs']['exists']; stat?: Claude['fs']['stat'] }
|
|
13
15
|
http: { fetch?: Claude['http']['fetch'] }
|
|
14
16
|
settings: { read?: Claude['settings']['read'] }
|
|
15
|
-
config: { list?: Claude['config']['list']
|
|
16
|
-
ui: { ask?: Claude['ui']['ask']; open?: Claude['ui']['open']; scroll?: Claude['ui']['scroll'] }
|
|
17
|
-
session: { messages?: Claude['session']['messages']; append?: Claude['session']['append'] }
|
|
17
|
+
config: { list?: Claude['config']['list'] }
|
|
18
|
+
ui: { ask?: Claude['ui']['ask']; open?: Claude['ui']['open']; scroll?: Claude['ui']['scroll']; copy?: Claude['ui']['copy'] }
|
|
19
|
+
session: { messages?: Claude['session']['messages']; append?: Claude['session']['append']; send?: Claude['session']['send'] }
|
|
18
20
|
prompt: { submit?: Claude['prompt']['submit'] }
|
|
19
21
|
model: { complete?: Claude['model']['complete'] }
|
|
20
22
|
agent: { list?: Claude['agent']['list']; spawn?: Claude['agent']['spawn'] }
|
|
@@ -26,7 +28,6 @@ export type Fakes = {
|
|
|
26
28
|
export type ShownSchedule = { readonly id: string; readonly cron: string; readonly prompt: string }
|
|
27
29
|
|
|
28
30
|
export type Shown = {
|
|
29
|
-
readonly toasts: string[]
|
|
30
31
|
readonly notes: string[]
|
|
31
32
|
readonly prompts: string[]
|
|
32
33
|
readonly logs: string[]
|
|
@@ -47,10 +48,16 @@ export type FakeClaude = {
|
|
|
47
48
|
settle(): Promise<void>
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
export function calledAnswer(caller: string, answer: EventResult<'cmod.call'>): unknown {
|
|
52
|
+
if (isObject(answer) && typeof answer['deny'] === 'string') throw new Error(`${caller}: $.cmod.call: ${answer['deny']}`)
|
|
53
|
+
return isObject(answer) ? answer['value'] : undefined
|
|
54
|
+
}
|
|
55
|
+
|
|
50
56
|
export function fakeClaude(plugin: { readonly name: string; readonly root: string }): FakeClaude {
|
|
51
57
|
const calls: TestCall[] = []
|
|
52
58
|
const fakes: Fakes = { process: {}, fs: {}, http: {}, settings: {}, config: {}, ui: {}, session: {}, prompt: {}, model: {}, agent: {}, clock: {}, cmod: {}, tool: {} }
|
|
53
|
-
const
|
|
59
|
+
const emptyDisk = fakeFiles({})
|
|
60
|
+
const shown: Shown = { notes: [], prompts: [], logs: [], debug: [], statuses: [], openPanes: new Set(), commands: [], tools: [], schedules: [] }
|
|
54
61
|
const unplacedPanes = new Set<string>()
|
|
55
62
|
let focusedPane: string | undefined
|
|
56
63
|
const store = new Map<string, unknown>()
|
|
@@ -72,7 +79,7 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
72
79
|
|
|
73
80
|
const appendNote: Claude['session']['append'] = async ({ message }) => {
|
|
74
81
|
shown.notes.push(message.content.map((block) => (typeof block['text'] === 'string' ? block['text'] : '')).join(''))
|
|
75
|
-
return { message, uuid: `note-${shown.notes.length}` }
|
|
82
|
+
return { message, uuid: `note-${shown.notes.length}` }
|
|
76
83
|
}
|
|
77
84
|
const submitPrompt: Claude['prompt']['submit'] = async ({ text }) => {
|
|
78
85
|
shown.prompts.push(text)
|
|
@@ -98,9 +105,6 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
98
105
|
const claude: Claude = {
|
|
99
106
|
plugin,
|
|
100
107
|
ui: {
|
|
101
|
-
toast: (text) => {
|
|
102
|
-
shown.toasts.push(text)
|
|
103
|
-
},
|
|
104
108
|
status: (text) => {
|
|
105
109
|
shown.statuses.push(text)
|
|
106
110
|
},
|
|
@@ -134,6 +138,7 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
134
138
|
...[...unplacedPanes].map((id) => ({ id, title: id, isShown: false, isFocused: false, isPlaced: false })),
|
|
135
139
|
],
|
|
136
140
|
scroll: rejected('ui.scroll', () => fakes.ui.scroll ?? (async () => ({}))),
|
|
141
|
+
copy: rejected('ui.copy', () => fakes.ui.copy ?? (async () => ({ isCopied: true as const }))),
|
|
137
142
|
resolve: () => elements,
|
|
138
143
|
},
|
|
139
144
|
process: {
|
|
@@ -141,17 +146,16 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
141
146
|
spawn: faked('process.spawn', () => fakes.process.spawn),
|
|
142
147
|
},
|
|
143
148
|
fs: {
|
|
144
|
-
read: rejected('fs.read', () => fakes.fs.read),
|
|
145
|
-
write: rejected('fs.write', () => fakes.fs.write),
|
|
146
|
-
list: rejected('fs.list', () => fakes.fs.list),
|
|
147
|
-
exists: rejected('fs.exists', () => fakes.fs.exists ??
|
|
148
|
-
stat: rejected('fs.stat', () => fakes.fs.stat),
|
|
149
|
+
read: rejected('fs.read', () => fakes.fs.read ?? emptyDisk.read),
|
|
150
|
+
write: rejected('fs.write', () => fakes.fs.write ?? emptyDisk.write),
|
|
151
|
+
list: rejected('fs.list', () => fakes.fs.list ?? emptyDisk.list),
|
|
152
|
+
exists: rejected('fs.exists', () => fakes.fs.exists ?? emptyDisk.exists),
|
|
153
|
+
stat: rejected('fs.stat', () => fakes.fs.stat ?? emptyDisk.stat),
|
|
149
154
|
},
|
|
150
155
|
http: { fetch: rejected('http.fetch', () => fakes.http.fetch) },
|
|
151
|
-
settings: { read: rejected('settings.read', () => fakes.settings.read) },
|
|
156
|
+
settings: { read: rejected('settings.read', () => fakes.settings.read ?? (async () => ({}))) },
|
|
152
157
|
config: {
|
|
153
158
|
list: rejected('config.list', () => fakes.config.list ?? (async () => [])),
|
|
154
|
-
set: rejected('config.set', () => fakes.config.set ?? (async ({ value }) => ({ value }))) as Claude['config']['set'],
|
|
155
159
|
},
|
|
156
160
|
agent: {
|
|
157
161
|
list: rejected('agent.list', () => fakes.agent.list),
|
|
@@ -177,10 +181,11 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
177
181
|
root: async () => plugin.root,
|
|
178
182
|
cwd: async () => plugin.root,
|
|
179
183
|
model: async () => 'test-model',
|
|
180
|
-
usage: async () => ({ context: { window: 200000 }, rateLimits:
|
|
184
|
+
usage: async () => ({ startedAt: Date.parse('2026-01-01T00:00:00.000Z'), context: { window: 200000 }, rateLimits: [], cost: { usd: 0 } }),
|
|
181
185
|
surfaces: async () => ['terminal'],
|
|
182
186
|
messages: rejected('session.messages', () => fakes.session.messages as ((args?: unknown) => Promise<unknown>) | undefined) as Claude['session']['messages'],
|
|
183
187
|
append: rejected('session.append', () => fakes.session.append ?? appendNote),
|
|
188
|
+
send: rejected('session.send', () => fakes.session.send ?? (async () => ({ isDelivered: true as const }))),
|
|
184
189
|
},
|
|
185
190
|
prompt: { submit: rejected('prompt.submit', () => fakes.prompt.submit ?? submitPrompt) },
|
|
186
191
|
model: { complete: rejected('model.complete', () => fakes.model.complete) },
|
|
@@ -116,8 +116,9 @@ function sideBySide(children: readonly RenderNode[], width: number, gap: number)
|
|
|
116
116
|
|
|
117
117
|
function minWidthOf(node: RenderNode): number {
|
|
118
118
|
if (typeof node === 'string') return 0
|
|
119
|
-
const
|
|
120
|
-
|
|
119
|
+
const { props } = insideOf(node)
|
|
120
|
+
if (props['flexShrink'] === 0) return Number.POSITIVE_INFINITY
|
|
121
|
+
return typeof props['minWidth'] === 'number' ? props['minWidth'] : 0
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
function blankRows(count: number): string[] {
|
|
@@ -36,7 +36,7 @@ export function fakeFiles(files: Readonly<Record<string, string>>, links: Readon
|
|
|
36
36
|
modified.set(realPath, writes)
|
|
37
37
|
},
|
|
38
38
|
list: async (path) => {
|
|
39
|
-
if (path === undefined) throw new Error('fs.list() lists the working folder, which the fake files do not know. Name the folder, such as fs.list(
|
|
39
|
+
if (path === undefined) throw new Error('fs.list() lists the working folder, which the fake files do not know. Name the folder, such as fs.list(sessions.current.cwd).')
|
|
40
40
|
const folder = realPathOf(path)
|
|
41
41
|
if (kindOf(folder) !== 'dir') throw new Error(`ENOENT: no such directory, list '${path}'`)
|
|
42
42
|
const names = new Set(below(folder).map((file) => file.split('/')[0] as string))
|