@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
|
@@ -4,6 +4,8 @@ type Drawn = { readonly type: string; readonly props: Record<string, unknown>; r
|
|
|
4
4
|
|
|
5
5
|
const boxInsides = new WeakMap<object, Drawn>()
|
|
6
6
|
|
|
7
|
+
const measuredProps = ['width', 'height', 'minWidth', 'minHeight'] as const
|
|
8
|
+
|
|
7
9
|
function element(type: string) {
|
|
8
10
|
return (props: Record<string, unknown>): RenderElement => {
|
|
9
11
|
const { children, ...rest } = props
|
|
@@ -11,13 +13,17 @@ function element(type: string) {
|
|
|
11
13
|
const keyed = type === 'Button' ? { key: rest['label'] ?? flat.map(textOf).join(''), ...rest } : rest
|
|
12
14
|
const drawn: Drawn = { type, props: keyed, ...(flat.length === 0 ? {} : { children: flat }) }
|
|
13
15
|
if (type !== 'Box') return drawn as unknown as RenderElement
|
|
16
|
+
for (const size of measuredProps) {
|
|
17
|
+
const value = rest[size]
|
|
18
|
+
if (typeof value === 'number' && !(Number.isFinite(value) && value >= 0 && value <= 10000)) throw new Error(`Box prop "${size}" must be a finite number between 0 and 10000`)
|
|
19
|
+
}
|
|
14
20
|
const box = Object.freeze({})
|
|
15
21
|
boxInsides.set(box, drawn)
|
|
16
22
|
return box as unknown as RenderElement
|
|
17
23
|
}
|
|
18
24
|
}
|
|
19
25
|
|
|
20
|
-
export const elements = {
|
|
26
|
+
export const elements: ElementTable = {
|
|
21
27
|
Box: element('Box'),
|
|
22
28
|
Text: element('Text'),
|
|
23
29
|
Button: element('Button'),
|
|
@@ -27,7 +33,10 @@ export const elements = {
|
|
|
27
33
|
Input: element('Input'),
|
|
28
34
|
Select: element('Select'),
|
|
29
35
|
Image: element('Image'),
|
|
30
|
-
|
|
36
|
+
Svg: element('Svg'),
|
|
37
|
+
Raster: element('Raster'),
|
|
38
|
+
Client: element('Client'),
|
|
39
|
+
}
|
|
31
40
|
|
|
32
41
|
export function findElement(node: RenderNode, type: string, key: string): Drawn['props'] | undefined {
|
|
33
42
|
if (typeof node === 'string') return undefined
|
|
@@ -41,11 +50,12 @@ export function findElement(node: RenderNode, type: string, key: string): Drawn[
|
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
export function rowsOf(node: RenderNode, width: number): string[] {
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
const drawn = typeof node === 'string' ? undefined : insideOf(node)
|
|
54
|
+
if (drawn?.type === 'Box') return boxRows(drawn, width)
|
|
55
|
+
if (drawn?.type === 'Svg') return []
|
|
46
56
|
return textOf(node)
|
|
47
57
|
.split('\n')
|
|
48
|
-
.flatMap((line) => fitted(line, width, wrap))
|
|
58
|
+
.flatMap((line) => fitted(line, width, drawn?.props['wrap']))
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
export function textOf(node: RenderNode): string {
|
|
@@ -89,11 +99,50 @@ function boxRows({ props, children = [] }: Drawn, width: number): string[] {
|
|
|
89
99
|
const size = (key: string) => (typeof props[key] === 'number' ? props[key] : undefined)
|
|
90
100
|
const space = (side: string, axis: string) => (size(`padding${side}`) ?? size(`padding${axis}`) ?? size('padding') ?? 0) + (size(`margin${side}`) ?? size(`margin${axis}`) ?? size('margin') ?? 0)
|
|
91
101
|
const left = space('Left', 'X')
|
|
102
|
+
const right = space('Right', 'X')
|
|
103
|
+
const border = typeof props['borderStyle'] === 'string' ? 1 : 0
|
|
92
104
|
const direction = String(props['flexDirection'] ?? 'row')
|
|
93
|
-
const
|
|
94
|
-
const
|
|
105
|
+
const flowing = children.filter((child) => !isPlaced(child))
|
|
106
|
+
const ordered = direction.endsWith('-reverse') ? [...flowing].reverse() : flowing
|
|
107
|
+
const outer = Math.min(width, size('width') ?? width)
|
|
108
|
+
const inner = outer - left - right - border * 2
|
|
95
109
|
const rows = direction.startsWith('column') ? stacked(ordered, inner, size('rowGap') ?? size('gap') ?? 0) : sideBySide(ordered, inner, size('columnGap') ?? size('gap') ?? 0, props['justifyContent'] === 'space-between')
|
|
96
|
-
|
|
110
|
+
const content = border === 1 && Number.isFinite(inner) ? inner : right === 0 && border === 0 ? 0 : Math.max(widthOf(rows), (size('minWidth') ?? 0) - left - right - border * 2)
|
|
111
|
+
const padded = [...blankRows(space('Top', 'Y')), ...rows.map((row) => `${' '.repeat(left)}${paddedTo(row, content)}${' '.repeat(right)}`), ...blankRows(space('Bottom', 'Y'))]
|
|
112
|
+
const framed = border === 0 ? padded : framedRows(padded, left + content + right, props['borderStyle'])
|
|
113
|
+
return placedOver(
|
|
114
|
+
framed.map((row) => paddedTo(row, size('minWidth') ?? 0)),
|
|
115
|
+
children.filter(isPlaced),
|
|
116
|
+
outer,
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isPlaced(node: RenderNode): node is RenderElement {
|
|
121
|
+
if (typeof node === 'string') return false
|
|
122
|
+
const { props } = insideOf(node)
|
|
123
|
+
return props['position'] === 'absolute' && typeof props['top'] === 'number'
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function framedRows(rows: readonly string[], columns: number, style: unknown): string[] {
|
|
127
|
+
const [topLeft, topRight, bottomLeft, bottomRight] = style === 'round' ? ['╭', '╮', '╰', '╯'] : ['┌', '┐', '└', '┘']
|
|
128
|
+
return [`${topLeft}${'─'.repeat(columns)}${topRight}`, ...rows.map((row) => `│${paddedTo(row, columns)}│`), `${bottomLeft}${'─'.repeat(columns)}${bottomRight}`]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function placedOver(rows: readonly string[], placed: readonly RenderElement[], width: number): string[] {
|
|
132
|
+
const drawn = [...rows]
|
|
133
|
+
for (const node of placed) {
|
|
134
|
+
const { props } = insideOf(node)
|
|
135
|
+
const top = Number(props['top'])
|
|
136
|
+
const left = typeof props['left'] === 'number' ? props['left'] : 0
|
|
137
|
+
rowsOf(node, width - left).forEach((line, index) => {
|
|
138
|
+
const row = top + index
|
|
139
|
+
if (row < 0 || row >= drawn.length) return
|
|
140
|
+
const cells = [...paddedTo(drawn[row] ?? '', left)]
|
|
141
|
+
cells.splice(left, [...line].length, ...line)
|
|
142
|
+
drawn[row] = cells.join('')
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
return drawn
|
|
97
146
|
}
|
|
98
147
|
|
|
99
148
|
function stacked(children: readonly RenderNode[], width: number, gap: number): string[] {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isObject } from '../records.js'
|
|
2
|
+
import { AttachedFile, attachmentNameOf, type AttachmentType } from '../session/context/turn/message/attached-file.js'
|
|
2
3
|
import { InstructionFile } from '../session/context/turn/message/instruction-file.js'
|
|
3
4
|
import { Message, messagesOf, noFiles, searchText, skillPrefix, type Row } from '../session/context/turn/message/message.js'
|
|
4
5
|
import { Note } from '../session/context/turn/message/note.js'
|
|
@@ -7,14 +8,14 @@ import { Prompt } from '../session/context/turn/message/prompt.js'
|
|
|
7
8
|
import { Reply } from '../session/context/turn/message/reply.js'
|
|
8
9
|
import { Mod, Person, Schedule, type Sender } from '../session/context/turn/message/sender.js'
|
|
9
10
|
import { GoalChange } from '../session/goal/goal.js'
|
|
10
|
-
import { Task } from '../session/goal/task.js'
|
|
11
|
+
import { Task, TaskStart } from '../session/goal/task.js'
|
|
11
12
|
import { Skill } from '../session/context/turn/message/skill.js'
|
|
12
13
|
import { ToolCall } from '../session/context/turn/message/tool-call.js'
|
|
13
14
|
import { ToolResult } from '../session/context/turn/message/tool-result.js'
|
|
14
|
-
import { createConversation, emptyConversation, searchLimit, type ContextData, type Conversation, type SessionChange, type SessionData, type TaskData, type TurnData, type TurnEnding } from '../session/conversation.js'
|
|
15
|
-
import type { Place } from '../runtime/before.js'
|
|
15
|
+
import { createConversation, emptyConversation, searchLimit, tasksWithAgents, type ContextData, type Conversation, type SessionChange, type SessionData, type TaskData, type TurnData, type TurnEnding } from '../session/conversation.js'
|
|
16
|
+
import type { Decide, Place } from '../runtime/before.js'
|
|
16
17
|
import type { Claude } from '../runtime/claude.js'
|
|
17
|
-
import { conversationMethods, everySession, isHeardBy, resultOf, type Listened } from '../runtime/sessions.js'
|
|
18
|
+
import { conversationMethods, everySession, isHeardBy, resultOf, type Listened, type SessionColor } from '../runtime/sessions.js'
|
|
18
19
|
import { receivedMatcher } from '../utils/matcher.js'
|
|
19
20
|
import { firstLine, messageOf } from '../utils/text.js'
|
|
20
21
|
|
|
@@ -26,6 +27,7 @@ export type MessageDraft =
|
|
|
26
27
|
| { readonly type: typeof ToolResult; readonly callId: string; readonly output: string; readonly isError?: boolean }
|
|
27
28
|
| { readonly type: typeof Skill; readonly name: string; readonly text: string }
|
|
28
29
|
| { readonly type: typeof InstructionFile; readonly path: string; readonly text: string }
|
|
30
|
+
| { readonly type: typeof AttachedFile; readonly path: string; readonly attachmentType: AttachmentType; readonly text: string }
|
|
29
31
|
| { readonly type: typeof Notice; readonly text: string }
|
|
30
32
|
|
|
31
33
|
export type FakeSession = Partial<Omit<SessionData, 'id'>> & {
|
|
@@ -45,6 +47,8 @@ type Kept = {
|
|
|
45
47
|
|
|
46
48
|
type Heard = { readonly event: Listened['event']; readonly matcher: unknown }
|
|
47
49
|
|
|
50
|
+
export type StartedTask = { readonly title: string; readonly agentType?: string; readonly model?: string; readonly openedBy?: string; readonly parent?: string; readonly isBackground?: true; readonly forkedFrom?: string }
|
|
51
|
+
|
|
48
52
|
|
|
49
53
|
export type FakeSessions = {
|
|
50
54
|
answer(caller: string, method: string, input: unknown): unknown
|
|
@@ -52,15 +56,17 @@ export type FakeSessions = {
|
|
|
52
56
|
draft(draft: MessageDraft, task?: string): Message
|
|
53
57
|
place(): Place
|
|
54
58
|
wants(mod: string, event: string): boolean
|
|
55
|
-
endTurn(ending?: TurnEnding): void
|
|
59
|
+
endTurn(ending?: TurnEnding, task?: string): void
|
|
56
60
|
compact(): void
|
|
57
|
-
startTask(task:
|
|
61
|
+
startTask(task: StartedTask): string
|
|
58
62
|
start(id: string): void
|
|
63
|
+
color(): SessionColor
|
|
64
|
+
recolor(color: string | undefined): SessionColor
|
|
59
65
|
lastTurn(): TurnData | undefined
|
|
60
66
|
readonly sent: { readonly session: string; readonly task?: string; readonly type: string; readonly text: string; readonly as?: string }[]
|
|
61
67
|
}
|
|
62
68
|
|
|
63
|
-
export function fakeSessions({ project, cwd, given, push, callTool }: { readonly project: () => string; readonly cwd: () => string; readonly given: readonly FakeSession[]; readonly push: (mod: string, session: string, change: SessionChange) => void; readonly callTool: Claude['tool']['call'] }): FakeSessions {
|
|
69
|
+
export function fakeSessions({ project, cwd, given, push, callTool, listAgents, decide }: { readonly project: () => string; readonly cwd: () => string; readonly given: readonly FakeSession[]; readonly push: (mod: string, session: string, change: SessionChange) => void; readonly callTool: Claude['tool']['call']; readonly listAgents: Claude['agent']['list']; readonly decide: Decide }): FakeSessions {
|
|
64
70
|
const kept = new Map<string, Kept>()
|
|
65
71
|
const heard = new Map<string, Map<string, readonly Heard[]>>()
|
|
66
72
|
const watches = new Map<string, Map<string, readonly string[]>>()
|
|
@@ -96,6 +102,14 @@ export function fakeSessions({ project, cwd, given, push, callTool }: { readonly
|
|
|
96
102
|
return session.messages.slice(before)
|
|
97
103
|
}
|
|
98
104
|
|
|
105
|
+
const openTask = (session: Kept, { title, agentType, model, openedBy = '', parent, isBackground, forkedFrom }: StartedTask): TaskData => {
|
|
106
|
+
const owner = forkedFrom !== undefined ? { forkedFrom } : isBackground === true ? (agentType === undefined ? {} : { agentType }) : { agentType: agentType ?? 'general-purpose' }
|
|
107
|
+
const data: TaskData = { id: `task-${(ids += 1)}`, title, ...owner, openedBy, openedAt: now(), ...(model === undefined ? {} : { model }), ...(parent === undefined ? {} : { parent }), ...(isBackground === true ? { isBackground } : {}) }
|
|
108
|
+
if (isBackground === true) keep({ id: data.id, title, project: session.data.project, cwd: session.data.cwd, model: model ?? session.data.model, startedAt: data.openedAt, lastAt: data.openedAt, isRunning: true, ...owner })
|
|
109
|
+
record(session, session.conversation.startTask(data))
|
|
110
|
+
return data
|
|
111
|
+
}
|
|
112
|
+
|
|
99
113
|
for (const each of given) {
|
|
100
114
|
const session = keep({ title: '', project: project(), cwd: project(), model: 'test-model', startedAt: now(), lastAt: '', isRunning: false, ...each })
|
|
101
115
|
for (const draft of each.messages ?? []) add(session, draft)
|
|
@@ -110,11 +124,15 @@ export function fakeSessions({ project, cwd, given, push, callTool }: { readonly
|
|
|
110
124
|
|
|
111
125
|
const currentSession = () => sessionOf(current)
|
|
112
126
|
|
|
127
|
+
const colorOf = ({ data: { id, color } }: Kept): SessionColor => ({ session: id, ...(color === undefined ? {} : { color }) })
|
|
128
|
+
|
|
113
129
|
const place = (): Place => {
|
|
114
130
|
const { session, context, contextOpenedAt, turn, tasks, taskTurns, openCalls } = currentSession().conversation.state
|
|
115
131
|
return { session, context, contextOpenedAt, ...(turn === undefined ? {} : { turn }), tasks, taskTurns, ...(openCalls === undefined ? {} : { openCalls }) }
|
|
116
132
|
}
|
|
117
133
|
|
|
134
|
+
const wantsBefore = (mod: string, event: string) => [current, everySession].some((target) => (watches.get(mod)?.get(target) ?? []).includes(event))
|
|
135
|
+
|
|
118
136
|
const taskOf = (session: Kept, id: string) => {
|
|
119
137
|
const task = session.tasks.get(id)
|
|
120
138
|
if (task === undefined) throw new Error(`Session ${session.data.id} has no task ${id}. Start it with tested.startTask.`)
|
|
@@ -172,7 +190,11 @@ export function fakeSessions({ project, cwd, given, push, callTool }: { readonly
|
|
|
172
190
|
const task = taskOf(session, request['task'])
|
|
173
191
|
return [{ number: session.conversation.state.tasks[task.id]?.context ?? 1, openedAt: task.openedAt, ...(task.closedAt === undefined ? {} : { closedAt: task.closedAt }) }]
|
|
174
192
|
},
|
|
175
|
-
[conversationMethods.tasks]
|
|
193
|
+
async [conversationMethods.tasks](request) {
|
|
194
|
+
const session = String(request['session'])
|
|
195
|
+
const tasks = [...sessionOf(session).tasks.values()].filter((task) => request['all'] === true || task.parent === request['task'])
|
|
196
|
+
return session === current ? tasksWithAgents(tasks, await listAgents()) : tasks
|
|
197
|
+
},
|
|
176
198
|
[conversationMethods.goal](request) {
|
|
177
199
|
const session = sessionOf(String(request['session']))
|
|
178
200
|
const task = request['task']
|
|
@@ -196,7 +218,7 @@ export function fakeSessions({ project, cwd, given, push, callTool }: { readonly
|
|
|
196
218
|
watches.set(caller, targets)
|
|
197
219
|
return null
|
|
198
220
|
},
|
|
199
|
-
async [conversationMethods.act](request) {
|
|
221
|
+
async [conversationMethods.act](request, caller) {
|
|
200
222
|
const session = sessionOf(String(request['session']))
|
|
201
223
|
const action = String(request['action'])
|
|
202
224
|
const input = isObject(request['input']) ? request['input'] : {}
|
|
@@ -214,11 +236,24 @@ export function fakeSessions({ project, cwd, given, push, callTool }: { readonly
|
|
|
214
236
|
sent.push({ session: session.data.id, ...(task === undefined ? {} : { task }), type: String(input['type']), text: String(input['text']), ...(typeof input['as'] === 'string' ? { as: input['as'] } : {}) })
|
|
215
237
|
return null
|
|
216
238
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
239
|
+
const text = (key: string) => (typeof input[key] === 'string' ? input[key] : undefined)
|
|
240
|
+
const title = text('title') ?? ''
|
|
241
|
+
if (action === 'startTask' || action === 'fork' || action === 'startBackgroundTask' || action === 'clone') {
|
|
242
|
+
if (title.trim() === '') throw new Error('a task needs a title.')
|
|
243
|
+
const isClone = action === 'clone'
|
|
244
|
+
const isBackground = isClone || action === 'startBackgroundTask'
|
|
245
|
+
const forkedFrom = isClone || action === 'fork' ? session.data.id : undefined
|
|
246
|
+
const agentType = forkedFrom === undefined ? text('agentType') : undefined
|
|
247
|
+
const model = forkedFrom === undefined ? text('model') : undefined
|
|
248
|
+
const fresh = { ...(agentType === undefined ? {} : { agentType }), ...(model === undefined ? {} : { model }), ...(forkedFrom === undefined ? {} : { forkedFrom }) }
|
|
249
|
+
if (!isBackground) return openTask(session, { title, ...fresh })
|
|
250
|
+
if (input['name'] !== undefined) throw new Error('a background task takes no name.')
|
|
251
|
+
const asked: TaskData = { id: '', title, ...fresh, openedBy: '', openedAt: now(), isBackground: true }
|
|
252
|
+
const answer = wantsBefore(caller, TaskStart) ? await decide(TaskStart, { type: TaskStart, task: { ...asked, context: session.conversation.state.context } }) : undefined
|
|
253
|
+
if (answer?.refuse !== undefined) throw new Error(`a beforeTaskStart listener refused the task: ${answer.refuse}`)
|
|
254
|
+
const owner = forkedFrom === undefined ? (answer?.owner ?? {}) : {}
|
|
255
|
+
const chosen = { ...(owner.agentType === undefined ? {} : { agentType: owner.agentType }), ...(owner.model === undefined ? {} : { model: owner.model }) }
|
|
256
|
+
return openTask(session, { ...fresh, ...chosen, title: answer?.title ?? title, isBackground: true })
|
|
222
257
|
}
|
|
223
258
|
if (session.data.id === current) return resultOf(place(), { name: String(input['name']), input: input['input'] as Readonly<Record<string, unknown>>, cwd: project() }, await callTool({ ...input['input'] as object, tool: String(input['name']) } as never))
|
|
224
259
|
return { id: '', type: ToolResult, at: now(), session: session.data.id, context: session.conversation.state.context, callId: '', name: '', input: {}, cwd: project(), files: noFiles, output: '', isError: false }
|
|
@@ -242,7 +277,10 @@ export function fakeSessions({ project, cwd, given, push, callTool }: { readonly
|
|
|
242
277
|
return { deny: messageOf(error) }
|
|
243
278
|
}
|
|
244
279
|
},
|
|
245
|
-
add
|
|
280
|
+
add(draft, task) {
|
|
281
|
+
const session = currentSession()
|
|
282
|
+
return task !== undefined && session.tasks.get(task)?.isBackground === true ? add(sessionOf(task), draft) : add(session, draft, task)
|
|
283
|
+
},
|
|
246
284
|
draft(draft, task) {
|
|
247
285
|
const where = place()
|
|
248
286
|
const [unplaced] = messagesOf(rowOf(draft, `message-${(ids += 1)}`, now(), cwd()), task !== undefined)
|
|
@@ -252,27 +290,36 @@ export function fakeSessions({ project, cwd, given, push, callTool }: { readonly
|
|
|
252
290
|
return { ...unplaced, ...call, session: where.session, context: task === undefined ? where.context : (where.tasks[task]?.context ?? where.context), ...(turn === undefined ? {} : { turn: turn.prompt.id }), ...(task === undefined ? {} : { task }) }
|
|
253
291
|
},
|
|
254
292
|
place,
|
|
255
|
-
wants:
|
|
256
|
-
endTurn(ending = {}) {
|
|
293
|
+
wants: wantsBefore,
|
|
294
|
+
endTurn(ending = {}, task) {
|
|
257
295
|
const session = currentSession()
|
|
258
|
-
|
|
296
|
+
const background = task === undefined ? undefined : session.tasks.get(task)
|
|
297
|
+
if (background?.isBackground !== true) {
|
|
298
|
+
record(session, session.conversation.endTurn(now(), { reason: 'answer', ...ending }, task))
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
const owner = sessionOf(background.id)
|
|
302
|
+
record(owner, owner.conversation.endTurn(now(), { reason: 'answer', ...ending }))
|
|
303
|
+
record(session, session.conversation.endTask(background, now()))
|
|
259
304
|
},
|
|
260
305
|
compact() {
|
|
261
306
|
const session = currentSession()
|
|
262
307
|
record(session, session.conversation.add({ id: `compact-${(ids += 1)}`, at: now(), type: 'system', name: 'compact_boundary', isMeta: false, isSummary: false, content: [] }))
|
|
263
308
|
},
|
|
264
|
-
startTask(
|
|
265
|
-
const session = currentSession()
|
|
266
|
-
const id = `task-${(ids += 1)}`
|
|
267
|
-
record(session, session.conversation.startTask({ id, title, agentType, openedBy, openedAt: now(), ...(model === undefined ? {} : { model }), ...(parent === undefined ? {} : { parent }) }))
|
|
268
|
-
return id
|
|
269
|
-
},
|
|
309
|
+
startTask: (task) => openTask(currentSession(), task).id,
|
|
270
310
|
start(id) {
|
|
271
311
|
current = id
|
|
272
312
|
const session = kept.get(id) ?? keep({ id, title: '', project: project(), cwd: project(), model: 'test-model', startedAt: now(), lastAt: now(), isRunning: true })
|
|
273
313
|
session.data = { ...session.data, isRunning: true }
|
|
274
314
|
if (session.conversation.state.context === 0) record(session, session.conversation.startContext(now()))
|
|
275
315
|
},
|
|
316
|
+
color: () => colorOf(currentSession()),
|
|
317
|
+
recolor(color) {
|
|
318
|
+
const session = currentSession()
|
|
319
|
+
const { color: _color, ...data } = session.data
|
|
320
|
+
session.data = color === undefined ? data : { ...data, color }
|
|
321
|
+
return colorOf(session)
|
|
322
|
+
},
|
|
276
323
|
lastTurn: () => [...currentSession().turns.values()].filter((turn) => turn.prompt.task === undefined).at(-1),
|
|
277
324
|
sent,
|
|
278
325
|
}
|
|
@@ -289,5 +336,6 @@ function rowOf(draft: MessageDraft, id: string, at: string, cwd: string): Row {
|
|
|
289
336
|
if (draft.type === ToolResult) return { ...base, type: 'user', content: [{ type: 'tool_result', tool_use_id: draft.callId, content: draft.output, ...(draft.isError === true ? { is_error: true } : {}) }] }
|
|
290
337
|
if (draft.type === Skill) return { ...base, type: 'user', isMeta: true, content: text(`${skillPrefix} /skills/${draft.name}\n\n${draft.text}`) }
|
|
291
338
|
if (draft.type === InstructionFile) return { ...base, type: 'attachment', name: 'nested_memory', content: text(draft.text), path: draft.path }
|
|
339
|
+
if (draft.type === AttachedFile) return { ...base, type: 'attachment', name: attachmentNameOf(draft.attachmentType), content: text(draft.text), path: draft.path }
|
|
292
340
|
return { ...base, type: 'system', name: 'informational', content: text(draft.text) }
|
|
293
341
|
}
|