@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.
Files changed (273) hide show
  1. package/docs/commands.md +58 -10
  2. package/docs/dependencies.md +12 -12
  3. package/docs/hooks.md +41 -28
  4. package/docs/index.md +29 -25
  5. package/docs/install-steps.md +9 -7
  6. package/docs/jobs.md +59 -111
  7. package/docs/mod.md +200 -174
  8. package/docs/options.md +2 -2
  9. package/docs/permissions.md +27 -16
  10. package/docs/session.md +248 -0
  11. package/docs/state.md +11 -7
  12. package/docs/testing.md +66 -23
  13. package/docs/ui.md +108 -76
  14. package/jobs/check.d.ts.map +1 -1
  15. package/jobs/check.js +5 -2
  16. package/jobs/context.d.ts +7 -8
  17. package/jobs/context.d.ts.map +1 -1
  18. package/jobs/context.js +23 -73
  19. package/jobs/permissions/decide-permission.d.ts +5 -3
  20. package/jobs/permissions/decide-permission.d.ts.map +1 -1
  21. package/jobs/permissions/decide-permission.js +6 -4
  22. package/jobs/permissions/find-project-scope.d.ts +1 -1
  23. package/jobs/permissions/find-project-scope.d.ts.map +1 -1
  24. package/jobs/permissions/find-project-scope.js +8 -11
  25. package/jobs/permissions/match-target.d.ts +2 -2
  26. package/jobs/permissions/match-target.d.ts.map +1 -1
  27. package/jobs/permissions.d.ts +1 -1
  28. package/jobs/permissions.d.ts.map +1 -1
  29. package/jobs/permissions.js +6 -4
  30. package/jobs/program.d.ts.map +1 -1
  31. package/jobs/program.js +8 -3
  32. package/jobs/prompt.d.ts +4 -2
  33. package/jobs/prompt.d.ts.map +1 -1
  34. package/jobs/prompt.js +16 -15
  35. package/jobs/schedule.d.ts +2 -2
  36. package/jobs/schedule.d.ts.map +1 -1
  37. package/jobs/schedule.js +9 -9
  38. package/jobs/slash-command.d.ts +4 -4
  39. package/jobs/slash-command.d.ts.map +1 -1
  40. package/jobs/slash-command.js +14 -9
  41. package/jobs/status-line.d.ts +1 -11
  42. package/jobs/status-line.d.ts.map +1 -1
  43. package/jobs/status-line.js +8 -12
  44. package/jobs/tool.d.ts +2 -2
  45. package/jobs/tool.d.ts.map +1 -1
  46. package/jobs/tool.js +15 -9
  47. package/mod.d.ts +154 -75
  48. package/mod.d.ts.map +1 -1
  49. package/mod.js +94 -3
  50. package/options.d.ts +2 -2
  51. package/options.js +19 -19
  52. package/package.json +3 -3
  53. package/records.d.ts +10 -8
  54. package/records.d.ts.map +1 -1
  55. package/records.js +28 -17
  56. package/register.d.ts.map +1 -1
  57. package/register.js +18 -6
  58. package/runtime/announce.d.ts +18 -0
  59. package/runtime/announce.d.ts.map +1 -0
  60. package/runtime/announce.js +66 -0
  61. package/runtime/bound.d.ts +4 -0
  62. package/runtime/bound.d.ts.map +1 -0
  63. package/runtime/bound.js +9 -0
  64. package/runtime/claude.d.ts +3 -3
  65. package/runtime/claude.d.ts.map +1 -1
  66. package/runtime/conversations.d.ts +16 -0
  67. package/runtime/conversations.d.ts.map +1 -0
  68. package/runtime/conversations.js +291 -0
  69. package/runtime/deadline.d.ts +0 -1
  70. package/runtime/deadline.d.ts.map +1 -1
  71. package/runtime/deadline.js +1 -1
  72. package/runtime/dependencies.d.ts +2 -0
  73. package/runtime/dependencies.d.ts.map +1 -1
  74. package/runtime/dependencies.js +9 -0
  75. package/runtime/engine.d.ts +17 -0
  76. package/runtime/engine.d.ts.map +1 -0
  77. package/runtime/engine.js +10 -0
  78. package/runtime/grants.d.ts +4 -0
  79. package/runtime/grants.d.ts.map +1 -1
  80. package/runtime/grants.js +63 -33
  81. package/runtime/hooks.d.ts +5 -4
  82. package/runtime/hooks.d.ts.map +1 -1
  83. package/runtime/hooks.js +10 -5
  84. package/runtime/installer.d.ts.map +1 -1
  85. package/runtime/installer.js +11 -9
  86. package/runtime/lifecycle.d.ts +3 -1
  87. package/runtime/lifecycle.d.ts.map +1 -1
  88. package/runtime/lifecycle.js +317 -186
  89. package/runtime/metadata.d.ts +3 -1
  90. package/runtime/metadata.d.ts.map +1 -1
  91. package/runtime/metadata.js +21 -11
  92. package/runtime/options.d.ts +3 -1
  93. package/runtime/options.d.ts.map +1 -1
  94. package/runtime/options.js +4 -4
  95. package/runtime/programs.d.ts +8 -0
  96. package/runtime/programs.d.ts.map +1 -1
  97. package/runtime/programs.js +39 -1
  98. package/runtime/router.d.ts +2 -1
  99. package/runtime/router.d.ts.map +1 -1
  100. package/runtime/router.js +11 -3
  101. package/runtime/sessions.d.ts +36 -0
  102. package/runtime/sessions.d.ts.map +1 -0
  103. package/runtime/sessions.js +235 -0
  104. package/runtime/state.d.ts +6 -2
  105. package/runtime/state.d.ts.map +1 -1
  106. package/runtime/state.js +46 -13
  107. package/runtime/toaster.d.ts +31 -0
  108. package/runtime/toaster.d.ts.map +1 -0
  109. package/runtime/toaster.js +176 -0
  110. package/runtime/ui.d.ts +6 -17
  111. package/runtime/ui.d.ts.map +1 -1
  112. package/runtime/ui.js +36 -112
  113. package/session/context/context.d.ts +10 -0
  114. package/session/context/context.d.ts.map +1 -0
  115. package/session/context/context.js +2 -0
  116. package/session/context/turn/message/document.d.ts +7 -0
  117. package/session/context/turn/message/document.d.ts.map +1 -0
  118. package/session/context/turn/message/document.js +1 -0
  119. package/session/context/turn/message/message.d.ts +54 -0
  120. package/session/context/turn/message/message.d.ts.map +1 -0
  121. package/session/context/turn/message/message.js +137 -0
  122. package/session/context/turn/message/note.d.ts +8 -0
  123. package/session/context/turn/message/note.d.ts.map +1 -0
  124. package/session/context/turn/message/note.js +1 -0
  125. package/session/context/turn/message/notice.d.ts +6 -0
  126. package/session/context/turn/message/notice.d.ts.map +1 -0
  127. package/session/context/turn/message/notice.js +1 -0
  128. package/session/context/turn/message/prompt.d.ts +8 -0
  129. package/session/context/turn/message/prompt.d.ts.map +1 -0
  130. package/session/context/turn/message/prompt.js +1 -0
  131. package/session/context/turn/message/reply.d.ts +6 -0
  132. package/session/context/turn/message/reply.d.ts.map +1 -0
  133. package/session/context/turn/message/reply.js +1 -0
  134. package/session/context/turn/message/sender.d.ts +14 -0
  135. package/session/context/turn/message/sender.d.ts.map +1 -0
  136. package/session/context/turn/message/sender.js +30 -0
  137. package/session/context/turn/message/skill.d.ts +7 -0
  138. package/session/context/turn/message/skill.d.ts.map +1 -0
  139. package/session/context/turn/message/skill.js +1 -0
  140. package/session/context/turn/message/tool-call.d.ts +9 -0
  141. package/session/context/turn/message/tool-call.d.ts.map +1 -0
  142. package/session/context/turn/message/tool-call.js +1 -0
  143. package/session/context/turn/message/tool-result.d.ts +8 -0
  144. package/session/context/turn/message/tool-result.d.ts.map +1 -0
  145. package/session/context/turn/message/tool-result.js +1 -0
  146. package/session/context/turn/turn.d.ts +12 -0
  147. package/session/context/turn/turn.d.ts.map +1 -0
  148. package/session/context/turn/turn.js +2 -0
  149. package/session/conversation.d.ts +64 -0
  150. package/session/conversation.d.ts.map +1 -0
  151. package/session/conversation.js +123 -0
  152. package/session/goal/goal.d.ts +10 -0
  153. package/session/goal/goal.d.ts.map +1 -0
  154. package/session/goal/goal.js +1 -0
  155. package/session/goal/task.d.ts +16 -0
  156. package/session/goal/task.d.ts.map +1 -0
  157. package/session/goal/task.js +3 -0
  158. package/session/session.d.ts +99 -0
  159. package/session/session.d.ts.map +1 -0
  160. package/session/session.js +1 -0
  161. package/src/jobs/check.ts +7 -4
  162. package/src/jobs/context.ts +24 -82
  163. package/src/jobs/permissions/decide-permission.ts +10 -8
  164. package/src/jobs/permissions/find-project-scope.ts +8 -14
  165. package/src/jobs/permissions/match-target.ts +3 -3
  166. package/src/jobs/permissions.ts +8 -6
  167. package/src/jobs/program.ts +7 -3
  168. package/src/jobs/prompt.ts +21 -24
  169. package/src/jobs/schedule.ts +14 -14
  170. package/src/jobs/slash-command.ts +20 -14
  171. package/src/jobs/status-line.ts +9 -21
  172. package/src/jobs/tool.ts +17 -11
  173. package/src/mod.ts +209 -97
  174. package/src/options.ts +22 -22
  175. package/src/records.ts +35 -23
  176. package/src/register.ts +18 -6
  177. package/src/runtime/announce.ts +74 -0
  178. package/src/runtime/bound.ts +12 -0
  179. package/src/runtime/claude.ts +5 -3
  180. package/src/runtime/conversations.ts +302 -0
  181. package/src/runtime/deadline.ts +2 -2
  182. package/src/runtime/dependencies.ts +12 -0
  183. package/src/runtime/engine.ts +22 -0
  184. package/src/runtime/grants.ts +66 -31
  185. package/src/runtime/hooks.ts +19 -10
  186. package/src/runtime/installer.ts +10 -9
  187. package/src/runtime/lifecycle.ts +313 -181
  188. package/src/runtime/metadata.ts +33 -11
  189. package/src/runtime/options.ts +19 -4
  190. package/src/runtime/programs.ts +40 -1
  191. package/src/runtime/router.ts +11 -4
  192. package/src/runtime/sessions.ts +242 -0
  193. package/src/runtime/state.ts +50 -14
  194. package/src/runtime/toaster.ts +218 -0
  195. package/src/runtime/ui.ts +42 -139
  196. package/src/session/context/context.ts +12 -0
  197. package/src/session/context/turn/message/document.ts +8 -0
  198. package/src/session/context/turn/message/message.ts +165 -0
  199. package/src/session/context/turn/message/note.ts +9 -0
  200. package/src/session/context/turn/message/notice.ts +7 -0
  201. package/src/session/context/turn/message/prompt.ts +9 -0
  202. package/src/session/context/turn/message/reply.ts +7 -0
  203. package/src/session/context/turn/message/sender.ts +38 -0
  204. package/src/session/context/turn/message/skill.ts +8 -0
  205. package/src/session/context/turn/message/tool-call.ts +10 -0
  206. package/src/session/context/turn/message/tool-result.ts +9 -0
  207. package/src/session/context/turn/turn.ts +14 -0
  208. package/src/session/conversation.ts +169 -0
  209. package/src/session/goal/goal.ts +9 -0
  210. package/src/session/goal/task.ts +19 -0
  211. package/src/session/session.ts +94 -0
  212. package/src/testing/fake-claude.ts +23 -18
  213. package/src/testing/fake-elements.ts +3 -2
  214. package/src/testing/fake-files.ts +1 -1
  215. package/src/testing/fake-sessions.ts +203 -0
  216. package/src/testing.ts +112 -33
  217. package/src/ui/components.ts +3 -3
  218. package/src/ui/elements.ts +22 -8
  219. package/src/ui/markdown.ts +22 -26
  220. package/src/ui/places.ts +49 -0
  221. package/src/utils/call-effects.ts +3 -3
  222. package/src/utils/changelog.ts +32 -0
  223. package/src/utils/matcher.ts +37 -0
  224. package/src/utils/metadata.ts +17 -15
  225. package/src/utils/parse-shell.ts +24 -16
  226. package/src/utils/paths.ts +15 -1
  227. package/src/utils/text.ts +4 -0
  228. package/testing/fake-claude.d.ts +4 -2
  229. package/testing/fake-claude.d.ts.map +1 -1
  230. package/testing/fake-claude.js +18 -12
  231. package/testing/fake-elements.js +4 -2
  232. package/testing/fake-files.js +1 -1
  233. package/testing/fake-sessions.d.ts +74 -0
  234. package/testing/fake-sessions.d.ts.map +1 -0
  235. package/testing/fake-sessions.js +170 -0
  236. package/testing.d.ts +33 -13
  237. package/testing.d.ts.map +1 -1
  238. package/testing.js +92 -21
  239. package/ui/components.d.ts +2 -3
  240. package/ui/components.d.ts.map +1 -1
  241. package/ui/components.js +3 -3
  242. package/ui/elements.d.ts.map +1 -1
  243. package/ui/elements.js +19 -9
  244. package/ui/markdown.d.ts +35 -33
  245. package/ui/markdown.d.ts.map +1 -1
  246. package/ui/markdown.js +14 -16
  247. package/ui/places.d.ts +43 -0
  248. package/ui/places.d.ts.map +1 -0
  249. package/ui/places.js +31 -0
  250. package/utils/call-effects.d.ts +1 -1
  251. package/utils/call-effects.d.ts.map +1 -1
  252. package/utils/call-effects.js +2 -2
  253. package/utils/changelog.d.ts +9 -0
  254. package/utils/changelog.d.ts.map +1 -0
  255. package/utils/changelog.js +29 -0
  256. package/utils/matcher.d.ts +13 -0
  257. package/utils/matcher.d.ts.map +1 -0
  258. package/utils/matcher.js +32 -0
  259. package/utils/metadata.d.ts +3 -3
  260. package/utils/metadata.js +18 -14
  261. package/utils/parse-shell.d.ts +1 -0
  262. package/utils/parse-shell.d.ts.map +1 -1
  263. package/utils/parse-shell.js +22 -13
  264. package/utils/paths.d.ts +2 -0
  265. package/utils/paths.d.ts.map +1 -1
  266. package/utils/paths.js +13 -1
  267. package/utils/text.d.ts +1 -0
  268. package/utils/text.d.ts.map +1 -1
  269. package/utils/text.js +3 -0
  270. package/src/ui/slots.ts +0 -36
  271. package/ui/slots.d.ts +0 -208
  272. package/ui/slots.d.ts.map +0 -1
  273. package/ui/slots.js +0 -19
@@ -0,0 +1,7 @@
1
+ import type { MessageOf } from './message.js'
2
+
3
+ export const Notice = 'notice'
4
+
5
+ export type Notice = MessageOf<typeof Notice> & {
6
+ readonly text: string
7
+ }
@@ -0,0 +1,9 @@
1
+ import type { MessageOf } from './message.js'
2
+ import type { Sender } from './sender.js'
3
+
4
+ export const Prompt = 'prompt'
5
+
6
+ export type Prompt = MessageOf<typeof Prompt> & {
7
+ readonly text: string
8
+ readonly from: Sender
9
+ }
@@ -0,0 +1,7 @@
1
+ import type { MessageOf } from './message.js'
2
+
3
+ export const Reply = 'reply'
4
+
5
+ export type Reply = MessageOf<typeof Reply> & {
6
+ readonly text: string
7
+ }
@@ -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,8 @@
1
+ import type { MessageOf } from './message.js'
2
+
3
+ export const Skill = 'skill'
4
+
5
+ export type Skill = MessageOf<typeof Skill> & {
6
+ readonly name: string
7
+ readonly text: string
8
+ }
@@ -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,9 @@
1
+ import type { MessageOf } from './message.js'
2
+
3
+ export const ToolResult = 'toolResult'
4
+
5
+ export type ToolResult = MessageOf<typeof ToolResult> & {
6
+ readonly callId: string
7
+ readonly text: string
8
+ readonly isError: boolean
9
+ }
@@ -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,9 @@
1
+ import type { Task } from './task.js'
2
+
3
+ export const GoalChange = 'goalChange'
4
+
5
+ export type Goal = {
6
+ readonly text: string
7
+ update(change: { readonly text: string }): Promise<void>
8
+ tasks(): Promise<Task[]>
9
+ }
@@ -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']; set?: Claude['config']['set'] }
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 shown: Shown = { toasts: [], notes: [], prompts: [], logs: [], debug: [], statuses: [], openPanes: new Set(), commands: [], tools: [], schedules: [] }
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}` } as unknown as Awaited<ReturnType<Claude['session']['append']>>
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 ?? (async () => false)),
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: {}, cost: { usd: 0 } }) as unknown as Awaited<ReturnType<Claude['session']['usage']>>,
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 minWidth = insideOf(node).props['minWidth']
120
- return typeof minWidth === 'number' ? minWidth : 0
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(mod.cwd).')
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))