@cmodjs/core 4.0.2 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/commands.md +43 -8
- package/docs/index.md +3 -3
- package/docs/install-steps.md +6 -6
- package/docs/jobs.md +2 -1
- package/docs/mod.md +1 -1
- package/docs/options.md +1 -1
- package/docs/permissions.md +1 -1
- package/docs/session.md +44 -13
- package/docs/testing.md +14 -10
- package/docs/ui.md +43 -11
- package/installer.d.ts +3 -2
- package/installer.d.ts.map +1 -1
- package/jobs/program.d.ts +1 -0
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +39 -23
- package/mod.d.ts +3 -2
- package/mod.d.ts.map +1 -1
- package/mod.js +1 -0
- package/package.json +1 -1
- package/records.d.ts +15 -1
- package/records.d.ts.map +1 -1
- package/records.js +39 -1
- package/register.d.ts.map +1 -1
- package/register.js +5 -2
- package/runtime/before.d.ts +6 -2
- package/runtime/before.d.ts.map +1 -1
- package/runtime/before.js +56 -18
- package/runtime/buttons.d.ts +10 -0
- package/runtime/buttons.d.ts.map +1 -0
- package/runtime/buttons.js +12 -0
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +134 -25
- package/runtime/engine.d.ts +2 -0
- package/runtime/engine.d.ts.map +1 -1
- package/runtime/grants.d.ts +1 -0
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +5 -2
- package/runtime/hotkeys.d.ts.map +1 -1
- package/runtime/hotkeys.js +7 -4
- package/runtime/installer.d.ts +9 -6
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +104 -47
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +121 -40
- package/runtime/pages.d.ts +3 -1
- package/runtime/pages.d.ts.map +1 -1
- package/runtime/pages.js +34 -10
- package/runtime/parts.d.ts +1 -0
- package/runtime/parts.d.ts.map +1 -1
- package/runtime/parts.js +6 -3
- package/runtime/programs.d.ts +1 -0
- package/runtime/programs.d.ts.map +1 -1
- package/runtime/programs.js +4 -1
- package/runtime/sessions.d.ts +2 -2
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +88 -58
- package/runtime/toaster.d.ts.map +1 -1
- package/runtime/toaster.js +23 -9
- package/runtime/ui.d.ts +21 -4
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +158 -58
- package/runtime/watches.d.ts.map +1 -1
- package/runtime/watches.js +18 -4
- package/runtime/widgets.d.ts +7 -5
- package/runtime/widgets.d.ts.map +1 -1
- package/runtime/widgets.js +29 -20
- package/session/context/turn/message/attached-file.d.ts +13 -0
- package/session/context/turn/message/attached-file.d.ts.map +1 -0
- package/session/context/turn/message/attached-file.js +21 -0
- package/session/context/turn/message/message.d.ts +3 -2
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +20 -6
- package/session/conversation.d.ts +14 -3
- package/session/conversation.d.ts.map +1 -1
- package/session/conversation.js +23 -4
- package/session/goal/task.d.ts +5 -0
- package/session/goal/task.d.ts.map +1 -1
- package/session/session.d.ts +35 -7
- package/session/session.d.ts.map +1 -1
- package/session/session.js +2 -1
- package/src/installer.ts +3 -2
- package/src/jobs/program.ts +41 -21
- package/src/mod.ts +3 -2
- package/src/records.ts +49 -1
- package/src/register.ts +9 -6
- package/src/runtime/before.ts +61 -22
- package/src/runtime/buttons.ts +22 -0
- package/src/runtime/conversations.ts +138 -32
- package/src/runtime/engine.ts +2 -0
- package/src/runtime/grants.ts +6 -2
- package/src/runtime/hotkeys.ts +7 -4
- package/src/runtime/installer.ts +144 -82
- package/src/runtime/lifecycle.ts +128 -43
- package/src/runtime/pages.ts +45 -8
- package/src/runtime/parts.ts +8 -4
- package/src/runtime/programs.ts +5 -1
- package/src/runtime/sessions.ts +83 -57
- package/src/runtime/toaster.ts +27 -9
- package/src/runtime/ui.ts +163 -61
- package/src/runtime/watches.ts +20 -5
- package/src/runtime/widgets.ts +34 -22
- package/src/session/context/turn/message/attached-file.ts +36 -0
- package/src/session/context/turn/message/message.ts +20 -7
- package/src/session/conversation.ts +30 -6
- package/src/session/goal/task.ts +6 -0
- package/src/session/session.ts +17 -7
- package/src/testing/fake-claude.ts +21 -6
- package/src/testing/fake-elements.ts +51 -8
- package/src/testing/fake-sessions.ts +60 -23
- package/src/testing.ts +86 -28
- package/src/ui/areas.ts +1 -0
- package/src/ui/components.ts +14 -8
- package/src/ui/elements.ts +6 -2
- package/src/ui/markdown.ts +3 -3
- package/src/ui/panes.ts +2 -2
- package/src/ui/styles.ts +6 -2
- package/testing/fake-claude.d.ts +3 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +18 -5
- package/testing/fake-elements.d.ts.map +1 -1
- package/testing/fake-elements.js +46 -7
- package/testing/fake-sessions.d.ts +21 -10
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +58 -20
- package/testing.d.ts +8 -12
- package/testing.d.ts.map +1 -1
- package/testing.js +82 -21
- package/ui/areas.d.ts +4 -0
- package/ui/areas.d.ts.map +1 -1
- package/ui/components.d.ts +1 -0
- package/ui/components.d.ts.map +1 -1
- package/ui/components.js +11 -7
- package/ui/elements.d.ts +3 -0
- package/ui/elements.d.ts.map +1 -1
- package/ui/elements.js +6 -1
- package/ui/markdown.d.ts +2 -2
- package/ui/markdown.d.ts.map +1 -1
- package/ui/panes.d.ts +2 -1
- package/ui/panes.d.ts.map +1 -1
- package/ui/styles.d.ts +11 -2
- package/ui/styles.d.ts.map +1 -1
package/src/runtime/widgets.ts
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
import type { ElementTable, RenderElement } from 'claude-code'
|
|
2
|
-
import { isObject, widgetsPath } from '../records.js'
|
|
1
|
+
import type { ElementTable, MarkdownProps, RenderElement } from 'claude-code'
|
|
2
|
+
import { isObject, storeChangedPath, widgetsPath } from '../records.js'
|
|
3
3
|
import type { TurnData } from '../session/conversation.js'
|
|
4
4
|
import { AbovePrompt, areas, BelowPrompt, TurnEnd, Working, type Area } from '../ui/areas.js'
|
|
5
5
|
import { Box, drawWith } from '../ui/elements.js'
|
|
6
|
+
import type { StyledAt } from '../ui/styles.js'
|
|
6
7
|
import { ifMissing } from '../utils/paths.js'
|
|
7
8
|
import { messageOf } from '../utils/text.js'
|
|
8
9
|
import type { Claude } from './claude.js'
|
|
9
10
|
import { cmodPluginName } from './dependencies.js'
|
|
10
11
|
import type { DrawnParts } from './parts.js'
|
|
11
12
|
import type { Router } from './router.js'
|
|
12
|
-
import { partMethods, type PartsUpdate, type WidgetDeclaration } from './ui.js'
|
|
13
|
+
import { partMethods, styledAt, viewportColumns, type PartsUpdate, type WidgetDeclaration } from './ui.js'
|
|
13
14
|
|
|
14
15
|
export type WidgetRow = { readonly left: readonly string[]; readonly right: readonly string[] }
|
|
15
16
|
|
|
16
17
|
export type WidgetLayout = { readonly areas: Readonly<Record<Area, readonly WidgetRow[]>>; readonly hidden: readonly string[] }
|
|
17
18
|
|
|
18
|
-
export type PlacedWidget = { readonly key: string; readonly mod: string; readonly title: string; readonly areas: readonly Area[] }
|
|
19
|
+
export type PlacedWidget = { readonly key: string; readonly mod: string; readonly title: string; readonly areas: readonly Area[] } & Pick<WidgetDeclaration, 'place'>
|
|
19
20
|
|
|
20
21
|
export type Move = 'up' | 'down' | 'left' | 'right'
|
|
21
22
|
|
|
@@ -32,7 +33,8 @@ const promptIndent = 4
|
|
|
32
33
|
|
|
33
34
|
const transcriptIndent = 2
|
|
34
35
|
|
|
35
|
-
const
|
|
36
|
+
const widgetSizes = { fit: { flexShrink: 0 }, fill: { flexGrow: 1, flexShrink: 1, minWidth: 0 } } as const
|
|
37
|
+
|
|
36
38
|
|
|
37
39
|
const separator = ''
|
|
38
40
|
|
|
@@ -44,7 +46,7 @@ export type Widgets = {
|
|
|
44
46
|
reload(): Promise<void>
|
|
45
47
|
recheck(): Promise<void>
|
|
46
48
|
rows(area: Area, columns: number): RenderElement[]
|
|
47
|
-
draw(area: Area, table: ElementTable,
|
|
49
|
+
draw(area: Area, table: ElementTable, at: StyledAt, turn?: TurnData): Promise<RenderElement[]>
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export function layoutOf(value: unknown): WidgetLayout {
|
|
@@ -99,7 +101,9 @@ function declarationsOf(input: unknown): WidgetDeclaration[] {
|
|
|
99
101
|
const listed = isObject(input) && Array.isArray(input['widgets']) ? (input['widgets'] as unknown[]) : []
|
|
100
102
|
return listed.filter(isObject).flatMap((each) => {
|
|
101
103
|
const known = Array.isArray(each['areas']) ? (each['areas'] as unknown[]).filter((area): area is Area => (areas as readonly unknown[]).includes(area)) : []
|
|
102
|
-
|
|
104
|
+
const { side, size }: Record<string, unknown> = isObject(each['place']) ? each['place'] : {}
|
|
105
|
+
const place: NonNullable<WidgetDeclaration['place']> = { ...(side === 'left' || side === 'right' ? { side } : {}), ...(size === 'fit' || size === 'fill' ? { size } : {}) }
|
|
106
|
+
return typeof each['id'] === 'string' && typeof each['title'] === 'string' && known.length > 0 ? [{ id: each['id'], title: each['title'], areas: known, place }] : []
|
|
103
107
|
})
|
|
104
108
|
}
|
|
105
109
|
|
|
@@ -110,6 +114,7 @@ export function serveWidgets({
|
|
|
110
114
|
report,
|
|
111
115
|
turnClosedBy,
|
|
112
116
|
parts,
|
|
117
|
+
markdownIn,
|
|
113
118
|
}: {
|
|
114
119
|
readonly claude: Claude
|
|
115
120
|
readonly router: Router
|
|
@@ -117,6 +122,7 @@ export function serveWidgets({
|
|
|
117
122
|
readonly report: (id: string, message: string, error: unknown) => void
|
|
118
123
|
readonly turnClosedBy: (rowId: string) => TurnData | undefined
|
|
119
124
|
readonly parts: DrawnParts
|
|
125
|
+
readonly markdownIn: (table: ElementTable, at: StyledAt) => ((props: MarkdownProps) => RenderElement) | undefined
|
|
120
126
|
}): Widgets {
|
|
121
127
|
const path = widgetsPath(store)
|
|
122
128
|
const declared = new Map<string, readonly WidgetDeclaration[]>()
|
|
@@ -136,7 +142,7 @@ export function serveWidgets({
|
|
|
136
142
|
|
|
137
143
|
const redraw = () => claude.ui.invalidate('ui.render')
|
|
138
144
|
|
|
139
|
-
const placed = (): PlacedWidget[] => [...builtInWidgets, ...[...declared].flatMap(([mod, list]) => list.map(({ id, title, areas: listed }) => ({ key: `${mod}/${id}`, mod, title, areas: listed })))]
|
|
145
|
+
const placed = (): PlacedWidget[] => [...builtInWidgets, ...[...declared].flatMap(([mod, list]) => list.map(({ id, title, areas: listed, place }) => ({ key: `${mod}/${id}`, mod, title, areas: listed, ...(place === undefined ? {} : { place }) })))]
|
|
140
146
|
|
|
141
147
|
const change = (edit: (current: WidgetLayout) => WidgetLayout) => {
|
|
142
148
|
saving = saving
|
|
@@ -148,6 +154,7 @@ export function serveWidgets({
|
|
|
148
154
|
if (next !== current) {
|
|
149
155
|
const kept = isObject(file) ? file : {}
|
|
150
156
|
await claude.fs.write(path, `${JSON.stringify({ ...kept, ...next, areas: { ...(isObject(kept['areas']) ? kept['areas'] : {}), ...next.areas } }, null, 2)}\n`)
|
|
157
|
+
await claude.fs.write(storeChangedPath(store), '')
|
|
151
158
|
stored = next
|
|
152
159
|
}
|
|
153
160
|
place()
|
|
@@ -164,9 +171,11 @@ export function serveWidgets({
|
|
|
164
171
|
const withNewWidgets = (current: WidgetLayout) =>
|
|
165
172
|
placed()
|
|
166
173
|
.filter(({ key, mod }) => mod !== 'claude' && placedIn(current, key) === undefined && !placedKeys.has(key))
|
|
167
|
-
.reduce((next, { key, areas: [area = AbovePrompt] }) => {
|
|
174
|
+
.reduce((next, { key, areas: [area = AbovePrompt], place }) => {
|
|
168
175
|
const [first, ...rest] = next.areas[area]
|
|
169
|
-
|
|
176
|
+
const row = { ...first, left: first?.left ?? [], right: first?.right ?? [] }
|
|
177
|
+
const side = place?.side ?? 'left'
|
|
178
|
+
return { ...next, areas: { ...next.areas, [area]: [{ ...row, [side]: [...row[side], key] }, ...rest] } }
|
|
170
179
|
}, current)
|
|
171
180
|
|
|
172
181
|
const ask = async (mod: string) => {
|
|
@@ -215,7 +224,9 @@ export function serveWidgets({
|
|
|
215
224
|
|
|
216
225
|
const elementFor = (key: string, area: Area, columns: number, turn: TurnData | undefined): RenderElement | undefined => {
|
|
217
226
|
const widget = widgetOf(key)
|
|
218
|
-
|
|
227
|
+
if (widget === undefined) return undefined
|
|
228
|
+
const element = parts.element(widget.mod, cacheKey(key, area, columns, turn), `${key}:`, nameOf(widget), `widget:${key}`)
|
|
229
|
+
return element === undefined ? undefined : Box({ ...widgetSizes[widget.place?.size ?? 'fit'], children: element })
|
|
219
230
|
}
|
|
220
231
|
|
|
221
232
|
const isHidden = (key: string) => layout.hidden.includes(key)
|
|
@@ -227,19 +238,20 @@ export function serveWidgets({
|
|
|
227
238
|
const side = (keys: readonly string[]) => shown(keys).map((key) => elementFor(key, area, columns, turn)).filter((each) => each !== undefined)
|
|
228
239
|
const left = side(row.left)
|
|
229
240
|
const right = side(row.right)
|
|
230
|
-
|
|
241
|
+
const group = (keys: readonly string[], children: readonly RenderElement[]) => Box({ flexShrink: shown(keys).some((key) => widgetOf(key)?.place?.size === 'fill') ? 1 : 0, minWidth: 0, columnGap: 2, children })
|
|
242
|
+
return left.length + right.length === 0 ? [] : [Box({ justifyContent: 'space-between', width: columns, columnGap: 2, children: [group(row.left, left), group(row.right, right)] })]
|
|
231
243
|
})
|
|
232
244
|
|
|
233
245
|
const keysIn = (area: Area) => layout.areas[area].flatMap((row) => shown([...row.left, ...row.right]))
|
|
234
246
|
|
|
235
|
-
const draw = async (area: Area, table: ElementTable,
|
|
236
|
-
await Promise.all(keysIn(area).map((key) => fill(key, area, columns, turn)))
|
|
237
|
-
return drawWith(table, () => rowsNow(area, columns, turn))
|
|
247
|
+
const draw = async (area: Area, table: ElementTable, at: StyledAt, turn?: TurnData): Promise<RenderElement[]> => {
|
|
248
|
+
await Promise.all(keysIn(area).map((key) => fill(key, area, at.columns, turn)))
|
|
249
|
+
return drawWith(table, () => rowsNow(area, at.columns, turn), markdownIn(table, at))
|
|
238
250
|
}
|
|
239
251
|
|
|
240
252
|
const rows = (area: Area, columns: number): RenderElement[] => {
|
|
241
253
|
const started = keysIn(area).flatMap((key) => fill(key, area, columns, undefined) ?? [])
|
|
242
|
-
if (started.length > 0) void Promise.all(started).then(
|
|
254
|
+
if (started.length > 0) void Promise.all(started).then(redraw)
|
|
243
255
|
return rowsNow(area, columns, undefined)
|
|
244
256
|
}
|
|
245
257
|
|
|
@@ -249,17 +261,17 @@ export function serveWidgets({
|
|
|
249
261
|
|
|
250
262
|
router.add('ui.render', async (e, next) => {
|
|
251
263
|
const table = claude.ui.resolve(e)
|
|
252
|
-
const
|
|
253
|
-
if (e.component === 'AbovePrompt') return e.props.hasSurvey ? next(e) : below(table, await next(e), await draw(AbovePrompt, table, e.props.bodyColumns))
|
|
264
|
+
const inTranscript = styledAt(e, viewportColumns(e) - transcriptIndent)
|
|
265
|
+
if (e.component === 'AbovePrompt') return e.props.hasSurvey ? next(e) : below(table, await next(e), await draw(AbovePrompt, table, styledAt(e, e.props.bodyColumns)))
|
|
254
266
|
if (e.component === 'PromptHint') {
|
|
255
|
-
const added = await draw(BelowPrompt, table, (e
|
|
267
|
+
const added = await draw(BelowPrompt, table, styledAt(e, viewportColumns(e) - promptIndent))
|
|
256
268
|
return isHidden(claudeHint) ? column(table, added) : below(table, await next(e), added)
|
|
257
269
|
}
|
|
258
270
|
if (e.component === 'SessionMode') return isHidden(claudeModes) ? column(table, []) : next(e)
|
|
259
|
-
if (e.component === 'Spinner') return below(table, await next(e), await draw(Working, table,
|
|
271
|
+
if (e.component === 'Spinner') return below(table, await next(e), await draw(Working, table, inTranscript))
|
|
260
272
|
if (e.component === 'TurnDuration') {
|
|
261
273
|
const turn = turnClosedBy(e.requestId)
|
|
262
|
-
return below(table, await next(e), turn === undefined ? [] : await draw(TurnEnd, table,
|
|
274
|
+
return below(table, await next(e), turn === undefined ? [] : await draw(TurnEnd, table, inTranscript, turn))
|
|
263
275
|
}
|
|
264
276
|
return next(e)
|
|
265
277
|
})
|
|
@@ -272,7 +284,7 @@ export function serveWidgets({
|
|
|
272
284
|
const listed = declarationsOf(listing)
|
|
273
285
|
const isNewList = JSON.stringify(listed) !== JSON.stringify(declared.get(mod) ?? [])
|
|
274
286
|
declared.set(mod, listed)
|
|
275
|
-
if (isNewList) place()
|
|
287
|
+
if (isNewList) return place()
|
|
276
288
|
}
|
|
277
289
|
claude.ui.invalidate('ui.render')
|
|
278
290
|
},
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { MessageOf } from './message.js'
|
|
2
|
+
|
|
3
|
+
export const AttachedFile = 'attachedFile'
|
|
4
|
+
|
|
5
|
+
export type AttachmentType = 'contents' | 'reference' | 'change'
|
|
6
|
+
|
|
7
|
+
export type AttachedFile = MessageOf<typeof AttachedFile> & {
|
|
8
|
+
readonly path: string
|
|
9
|
+
readonly attachmentType: AttachmentType
|
|
10
|
+
readonly text: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const attachmentTypes: Readonly<Record<string, AttachmentType>> = {
|
|
14
|
+
file: 'contents',
|
|
15
|
+
compact_file_reference: 'reference',
|
|
16
|
+
edited_text_file: 'change',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function attachmentTypeOf(name: string | undefined): AttachmentType | undefined {
|
|
20
|
+
return name !== undefined && Object.hasOwn(attachmentTypes, name) ? attachmentTypes[name] : undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function attachmentNameOf(type: AttachmentType): string {
|
|
24
|
+
const name = Object.keys(attachmentTypes).find((each) => attachmentTypes[each] === type)
|
|
25
|
+
if (name === undefined) throw new Error(`Claude Code writes no attachment row for a ${type} file.`)
|
|
26
|
+
return name
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const readPath = /^Called the Read tool with the following input: \{.*?"file_path":\s*("(?:[^"\\]|\\["\\/bfnrt]|\\u[0-9a-fA-F]{4})*")/m
|
|
30
|
+
|
|
31
|
+
const noted = /^Note: (.+?) (?:was read before the last conversation was summarized|changed on disk since you last read it)/m
|
|
32
|
+
|
|
33
|
+
export function attachedPathOf(text: string): string {
|
|
34
|
+
const read = readPath.exec(text)?.[1]
|
|
35
|
+
return read === undefined ? (noted.exec(text)?.[1] ?? '') : (JSON.parse(read) as string)
|
|
36
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isObject } from '../../../../records.js'
|
|
2
2
|
import { Task } from '../../../goal/task.js'
|
|
3
|
+
import { AttachedFile, attachedPathOf, attachmentTypeOf } from './attached-file.js'
|
|
3
4
|
import { InstructionFile, isRulePath } from './instruction-file.js'
|
|
4
5
|
import { modNote, modNoteBlock, modNotesIn, Note } from './note.js'
|
|
5
6
|
import { Notice } from './notice.js'
|
|
@@ -12,7 +13,7 @@ import { ToolResult } from './tool-result.js'
|
|
|
12
13
|
|
|
13
14
|
export const Message = 'message'
|
|
14
15
|
|
|
15
|
-
export type MessageType = typeof Prompt | typeof Note | typeof Reply | typeof ToolCall | typeof ToolResult | typeof Skill | typeof InstructionFile | typeof Notice
|
|
16
|
+
export type MessageType = typeof Prompt | typeof Note | typeof Reply | typeof ToolCall | typeof ToolResult | typeof Skill | typeof InstructionFile | typeof AttachedFile | typeof Notice
|
|
16
17
|
|
|
17
18
|
export type MessageOf<Type extends MessageType> = {
|
|
18
19
|
readonly id: string
|
|
@@ -24,7 +25,7 @@ export type MessageOf<Type extends MessageType> = {
|
|
|
24
25
|
readonly task?: string
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
export type Message = Prompt | Note | Reply | ToolCall | ToolResult | Skill | InstructionFile | Notice
|
|
28
|
+
export type Message = Prompt | Note | Reply | ToolCall | ToolResult | Skill | InstructionFile | AttachedFile | Notice
|
|
28
29
|
|
|
29
30
|
export type Placed = 'session' | 'context' | 'turn' | 'task'
|
|
30
31
|
|
|
@@ -44,7 +45,7 @@ export type Row = {
|
|
|
44
45
|
readonly model?: string
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
export const messageTypes: readonly MessageType[] = [Prompt, Note, Reply, ToolCall, ToolResult, Skill, InstructionFile, Notice]
|
|
48
|
+
export const messageTypes: readonly MessageType[] = [Prompt, Note, Reply, ToolCall, ToolResult, Skill, InstructionFile, AttachedFile, Notice]
|
|
48
49
|
|
|
49
50
|
export const skillPrefix = 'Base directory for this skill:'
|
|
50
51
|
|
|
@@ -63,8 +64,9 @@ export function rowOfLine(row: Readonly<Record<string, unknown>>): Row | undefin
|
|
|
63
64
|
if (typeof uuid !== 'string' || typeof timestamp !== 'string' || typeof type !== 'string' || !rowTypes.includes(type)) return undefined
|
|
64
65
|
const message = isObject(row['message']) ? row['message'] : {}
|
|
65
66
|
const attachment = isObject(row['attachment']) ? row['attachment'] : {}
|
|
66
|
-
const content = type === 'attachment' ? attachment['content'] : type === 'system' ? row['content'] : message['content']
|
|
67
67
|
const name = type === 'attachment' ? attachment['type'] : row['subtype']
|
|
68
|
+
const content = type === 'attachment' ? attachedContentOf(attachment) : type === 'system' ? row['content'] : message['content']
|
|
69
|
+
const path = attachment['path'] ?? attachment['filename']
|
|
68
70
|
return {
|
|
69
71
|
id: uuid,
|
|
70
72
|
at: timestamp,
|
|
@@ -75,15 +77,21 @@ export function rowOfLine(row: Readonly<Record<string, unknown>>): Row | undefin
|
|
|
75
77
|
content: blocksOf(content),
|
|
76
78
|
...(row['origin'] === undefined ? {} : { origin: row['origin'] }),
|
|
77
79
|
...(typeof row['cwd'] === 'string' ? { cwd: row['cwd'] } : {}),
|
|
78
|
-
...(typeof
|
|
80
|
+
...(typeof path === 'string' ? { path } : {}),
|
|
79
81
|
...(typeof message['model'] === 'string' ? { model: message['model'] } : {}),
|
|
80
82
|
}
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
function attachedContentOf(attachment: Readonly<Record<string, unknown>>): unknown {
|
|
86
|
+
if (attachment['type'] === 'edited_text_file') return attachment['snippet']
|
|
87
|
+
const file = isObject(attachment['content']) ? attachment['content']['file'] : undefined
|
|
88
|
+
return attachment['type'] === 'file' && isObject(file) ? file['content'] : attachment['content']
|
|
89
|
+
}
|
|
90
|
+
|
|
83
91
|
export function rowOfAppend(append: { readonly uuid: string; readonly door: string; readonly origin: unknown; readonly message: { readonly type: Row['type']; readonly name?: string; readonly isMeta?: true; readonly content: readonly unknown[] } }, at: string, cwd: string): Row {
|
|
84
92
|
const { message, origin } = append
|
|
85
93
|
const text = textOf(message.content)
|
|
86
|
-
const path = message.type === 'attachment' ? instructionPathOf(text) || undefined : undefined
|
|
94
|
+
const path = message.type === 'attachment' ? (attachmentTypeOf(message.name) !== undefined ? attachedPathOf(text) : instructionPathOf(text)) || undefined : undefined
|
|
87
95
|
const model = isObject(origin) && origin['kind'] === 'model' && typeof origin['model'] === 'string' ? origin['model'] : undefined
|
|
88
96
|
return {
|
|
89
97
|
id: append.uuid,
|
|
@@ -113,7 +121,11 @@ function unplacedOf(row: Row, isInTask: boolean): Unplaced[] {
|
|
|
113
121
|
const notesFrom = (each: readonly { readonly mod: string; readonly text: string }[]): Unplaced[] => each.map((note) => ({ ...base, type: Note, text: note.text, from: { type: Mod, name: note.mod } }))
|
|
114
122
|
if (row.type === 'system') return row.name === 'turn_duration' || row.name === 'compact_boundary' || text === '' ? [] : [{ ...base, type: Notice, text }]
|
|
115
123
|
if (row.type === 'attachment' && row.name === 'hook_additional_context') return notesFrom(row.content.flatMap((entry) => modNotesIn(typeof entry === 'string' ? entry : textOf([entry]))))
|
|
116
|
-
if (row.type === 'attachment'
|
|
124
|
+
if (row.type === 'attachment' && row.name === 'nested_memory') return [{ ...base, type: InstructionFile, path: row.path ?? '', text, isRule: isRulePath(row.path ?? '') }]
|
|
125
|
+
if (row.type === 'attachment') {
|
|
126
|
+
const attachmentType = attachmentTypeOf(row.name)
|
|
127
|
+
return attachmentType === undefined ? [] : [{ ...base, type: AttachedFile, path: row.path ?? '', attachmentType, text }]
|
|
128
|
+
}
|
|
117
129
|
if (isModNotesRow(row)) return notesFrom(modNotesIn(text))
|
|
118
130
|
if (row.type === 'assistant') {
|
|
119
131
|
return row.content.filter(isObject).flatMap((block): Unplaced[] => {
|
|
@@ -212,6 +224,7 @@ export function searchText(message: Unplaced): string {
|
|
|
212
224
|
if (message.type === ToolCall) return [message.name, ...stringsIn(message.input).map((value) => value.slice(0, toolValueChars))].join('\n')
|
|
213
225
|
if (message.type === ToolResult) return ''
|
|
214
226
|
if (message.type === InstructionFile) return `${message.path}\n${message.text}`
|
|
227
|
+
if (message.type === AttachedFile) return message.path
|
|
215
228
|
return message.text
|
|
216
229
|
}
|
|
217
230
|
|
|
@@ -6,7 +6,7 @@ import { ToolCall } from './context/turn/message/tool-call.js'
|
|
|
6
6
|
import { ToolResult } from './context/turn/message/tool-result.js'
|
|
7
7
|
import { TurnEnd, TurnStart, type Turn } from './context/turn/turn.js'
|
|
8
8
|
import { GoalChange } from './goal/goal.js'
|
|
9
|
-
import { Task, TaskEnd, TaskStart } from './goal/task.js'
|
|
9
|
+
import { Task, TaskEnd, TaskStart, type TaskStatus } from './goal/task.js'
|
|
10
10
|
import type { UsageChange, Usage } from './session.js'
|
|
11
11
|
|
|
12
12
|
export type SessionData = {
|
|
@@ -28,7 +28,23 @@ export type TurnData = Pick<Turn, 'prompt' | 'context' | 'openedAt' | 'closedAt'
|
|
|
28
28
|
|
|
29
29
|
export type TurnEnding = Pick<Turn, 'answer' | 'reason' | 'failure'>
|
|
30
30
|
|
|
31
|
-
export type TaskData = Pick<Task, 'id' | 'title' | 'openedBy' | 'openedAt' | 'closedAt'> & { readonly agentType
|
|
31
|
+
export type TaskData = Pick<Task, 'id' | 'title' | 'openedBy' | 'openedAt' | 'closedAt' | 'status' | 'name' | 'spawnedBy' | 'isBackground'> & { readonly agentType?: string; readonly model?: string; readonly parent?: string; readonly forkedFrom?: string }
|
|
32
|
+
|
|
33
|
+
export const forkAgentType = 'fork'
|
|
34
|
+
|
|
35
|
+
export function ownerOfSpawn(agentType: string | undefined, session: string): Pick<TaskData, 'agentType' | 'forkedFrom'> {
|
|
36
|
+
return agentType === forkAgentType ? { forkedFrom: session } : { agentType: agentType ?? 'general-purpose' }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type Agent = { readonly id: string; readonly status: TaskStatus; readonly name?: string; readonly spawnedBy?: string }
|
|
40
|
+
|
|
41
|
+
export function tasksWithAgents(tasks: readonly TaskData[], agents: readonly Agent[]): TaskData[] {
|
|
42
|
+
const byId = new Map(agents.map((agent) => [agent.id, agent]))
|
|
43
|
+
return tasks.map((task) => {
|
|
44
|
+
const agent = byId.get(task.id)
|
|
45
|
+
return agent === undefined ? task : { ...task, status: agent.status, ...(agent.name === undefined ? {} : { name: agent.name }), ...(agent.spawnedBy === undefined ? {} : { spawnedBy: agent.spawnedBy }) }
|
|
46
|
+
})
|
|
47
|
+
}
|
|
32
48
|
|
|
33
49
|
export type RunningTask = TaskData & { readonly context: number; readonly contextOpenedAt?: string }
|
|
34
50
|
|
|
@@ -60,6 +76,7 @@ type OpenCall = Pick<ToolCall, 'name' | 'input' | 'cwd'>
|
|
|
60
76
|
export type Conversation = {
|
|
61
77
|
add(row: Row, task?: string): SessionChange[]
|
|
62
78
|
startTask(task: TaskData): SessionChange[]
|
|
79
|
+
endTask(task: TaskData, at: string): SessionChange[]
|
|
63
80
|
startContext(at: string): SessionChange[]
|
|
64
81
|
endTurn(at: string, ending: TurnEnding, task?: string): SessionChange[]
|
|
65
82
|
changeGoal(text: string): SessionChange[]
|
|
@@ -117,7 +134,7 @@ export function createConversation(start: ConversationState): Conversation {
|
|
|
117
134
|
return [{ type: TurnEnd, turn: ended }]
|
|
118
135
|
}
|
|
119
136
|
|
|
120
|
-
const
|
|
137
|
+
const endCall = (call: string, at: string, only?: string): SessionChange[] => {
|
|
121
138
|
const opened = [...tasks.values()].filter((each) => each.openedBy === call && (only === undefined || each.id === only))
|
|
122
139
|
if (opened.length === 0) {
|
|
123
140
|
if (agentCalls.has(call)) endedCalls.set(call, at)
|
|
@@ -168,10 +185,10 @@ export function createConversation(start: ConversationState): Conversation {
|
|
|
168
185
|
} as Message
|
|
169
186
|
changes.push({ type: Message, message: placed })
|
|
170
187
|
if (placed.type === ToolCall && agentTools.includes(placed.name)) agentCalls.add(placed.callId)
|
|
171
|
-
if (placed.type === ToolResult && agentCalls.has(placed.callId) && !String(placed.output).startsWith(asyncLaunch)) changes.push(...
|
|
188
|
+
if (placed.type === ToolResult && agentCalls.has(placed.callId) && !String(placed.output).startsWith(asyncLaunch)) changes.push(...endCall(placed.callId, row.at))
|
|
172
189
|
if (placed.type === Note && placed.from.type === Task) {
|
|
173
190
|
const call = tagOf(textOf(row.content), 'tool-use-id')
|
|
174
|
-
if (call !== undefined && agentCalls.has(call)) changes.push(...
|
|
191
|
+
if (call !== undefined && agentCalls.has(call)) changes.push(...endCall(call, row.at, tagOf(textOf(row.content), 'task-id')))
|
|
175
192
|
}
|
|
176
193
|
}
|
|
177
194
|
return changes
|
|
@@ -183,9 +200,16 @@ export function createConversation(start: ConversationState): Conversation {
|
|
|
183
200
|
agentCalls.add(data.openedBy)
|
|
184
201
|
const changes: SessionChange[] = [{ type: TaskStart, task: started }]
|
|
185
202
|
const endedAt = endedCalls.get(data.openedBy)
|
|
186
|
-
if (endedAt !== undefined) changes.push(...
|
|
203
|
+
if (endedAt !== undefined) changes.push(...endCall(data.openedBy, endedAt, data.id))
|
|
187
204
|
return changes
|
|
188
205
|
},
|
|
206
|
+
endTask(data, at) {
|
|
207
|
+
const open = tasks.get(data.id) ?? { ...data, context }
|
|
208
|
+
if (open.closedAt !== undefined) return []
|
|
209
|
+
const ended = { ...open, closedAt: at }
|
|
210
|
+
tasks.set(data.id, ended)
|
|
211
|
+
return [...endTaskTurn(data.id, at), { type: TaskEnd, task: ended }]
|
|
212
|
+
},
|
|
189
213
|
startContext,
|
|
190
214
|
endTurn: (at, ending, task) => (task === undefined ? endTurn(at, ending) : endTaskTurn(task, at, ending)),
|
|
191
215
|
changeGoal,
|
package/src/session/goal/task.ts
CHANGED
|
@@ -7,6 +7,8 @@ export const TaskStart = 'taskStart'
|
|
|
7
7
|
|
|
8
8
|
export const TaskEnd = 'taskEnd'
|
|
9
9
|
|
|
10
|
+
export type TaskStatus = 'pending' | 'running' | 'waiting' | 'idle' | 'completed' | 'failed' | 'killed'
|
|
11
|
+
|
|
10
12
|
export type Task = RecordPart & {
|
|
11
13
|
readonly id: string
|
|
12
14
|
readonly title: string
|
|
@@ -14,6 +16,10 @@ export type Task = RecordPart & {
|
|
|
14
16
|
readonly openedBy: string
|
|
15
17
|
readonly openedAt: string
|
|
16
18
|
readonly closedAt?: string
|
|
19
|
+
readonly status?: TaskStatus
|
|
20
|
+
readonly name?: string
|
|
21
|
+
readonly spawnedBy?: string
|
|
22
|
+
readonly isBackground?: true
|
|
17
23
|
tasks(): Promise<Task[]>
|
|
18
24
|
messages(): Promise<Message[]>
|
|
19
25
|
}
|
package/src/session/session.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Subscription } from '../utils/subscription.js'
|
|
|
2
2
|
import type { Matcher } from '../utils/matcher.js'
|
|
3
3
|
import type { Sender } from './context/turn/message/sender.js'
|
|
4
4
|
import type { Context, ContextEnd, ContextStart } from './context/context.js'
|
|
5
|
+
import type { AttachedFile } from './context/turn/message/attached-file.js'
|
|
5
6
|
import type { InstructionFile } from './context/turn/message/instruction-file.js'
|
|
6
7
|
import type { Message, MessageType } from './context/turn/message/message.js'
|
|
7
8
|
import type { Note } from './context/turn/message/note.js'
|
|
@@ -31,6 +32,7 @@ export type SessionPayload = {
|
|
|
31
32
|
readonly toolResult: ToolResult
|
|
32
33
|
readonly skill: Skill
|
|
33
34
|
readonly instructionFile: InstructionFile
|
|
35
|
+
readonly attachedFile: AttachedFile
|
|
34
36
|
readonly notice: Notice
|
|
35
37
|
readonly contextStart: Context
|
|
36
38
|
readonly contextEnd: Context
|
|
@@ -49,12 +51,13 @@ export type BeforeAnswer = {
|
|
|
49
51
|
readonly note: { readonly text: string }
|
|
50
52
|
readonly skill: { readonly text: string }
|
|
51
53
|
readonly instructionFile: { readonly text: string } | Refusal
|
|
54
|
+
readonly attachedFile: { readonly text: string } | Refusal
|
|
52
55
|
readonly reply: { readonly text: string }
|
|
53
|
-
readonly toolCall: { readonly input
|
|
56
|
+
readonly toolCall: { readonly input?: Readonly<Record<string, unknown>>; readonly note?: string } | { readonly result: string; readonly note?: string } | Refusal
|
|
54
57
|
readonly toolResult: { readonly output?: unknown; readonly isError?: true; readonly note?: string }
|
|
55
58
|
readonly notice: { readonly text: string }
|
|
56
|
-
readonly taskStart: { readonly owner
|
|
57
|
-
readonly turnEnd: Refusal
|
|
59
|
+
readonly taskStart: { readonly prompt?: string; readonly title?: string; readonly owner?: { readonly agentType?: string; readonly model?: string; readonly cwd?: string }; readonly isAsync?: boolean } | Refusal
|
|
60
|
+
readonly turnEnd: { readonly note: string } | Refusal
|
|
58
61
|
readonly contextEnd: Refusal
|
|
59
62
|
}
|
|
60
63
|
|
|
@@ -63,9 +66,12 @@ export type BeforeEvent = keyof BeforeAnswer
|
|
|
63
66
|
export type Usage = {
|
|
64
67
|
readonly model: string
|
|
65
68
|
readonly contextWindow: { readonly tokenCount?: number; readonly size?: number; readonly percent?: number }
|
|
69
|
+
readonly rateLimits?: readonly RateLimit[]
|
|
66
70
|
readonly cost?: { readonly usd: number }
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
export type RateLimit = { readonly type: string; readonly percent: number; readonly resetsAt?: string }
|
|
74
|
+
|
|
69
75
|
export type Surface = 'terminal' | 'desktop' | 'mobile' | 'vscode'
|
|
70
76
|
|
|
71
77
|
export const sessionEvents = {
|
|
@@ -77,6 +83,7 @@ export const sessionEvents = {
|
|
|
77
83
|
ToolResult: 'toolResult',
|
|
78
84
|
Skill: 'skill',
|
|
79
85
|
InstructionFile: 'instructionFile',
|
|
86
|
+
AttachedFile: 'attachedFile',
|
|
80
87
|
Notice: 'notice',
|
|
81
88
|
ContextStart: 'contextStart',
|
|
82
89
|
ContextEnd: 'contextEnd',
|
|
@@ -88,7 +95,7 @@ export const sessionEvents = {
|
|
|
88
95
|
UsageChange: 'usageChange',
|
|
89
96
|
} as const satisfies Readonly<Record<string, SessionEvent>>
|
|
90
97
|
|
|
91
|
-
export const beforeEvents = ['Prompt', 'Note', 'Skill', 'InstructionFile', 'Reply', 'ToolCall', 'ToolResult', 'Notice', 'TaskStart', 'TurnEnd', 'ContextEnd'] as const satisfies readonly (keyof typeof sessionEvents)[]
|
|
98
|
+
export const beforeEvents = ['Prompt', 'Note', 'Skill', 'InstructionFile', 'AttachedFile', 'Reply', 'ToolCall', 'ToolResult', 'Notice', 'TaskStart', 'TurnEnd', 'ContextEnd'] as const satisfies readonly (keyof typeof sessionEvents)[]
|
|
92
99
|
|
|
93
100
|
export type EventName = keyof typeof sessionEvents
|
|
94
101
|
|
|
@@ -138,6 +145,7 @@ export type TaskStartArgs = {
|
|
|
138
145
|
readonly title: string
|
|
139
146
|
readonly name?: string
|
|
140
147
|
readonly cwd?: string
|
|
148
|
+
readonly isBackground?: boolean
|
|
141
149
|
}
|
|
142
150
|
|
|
143
151
|
export type ToolCallArgs = {
|
|
@@ -164,12 +172,14 @@ export type Session = RecordPart & {
|
|
|
164
172
|
messages(options?: { readonly after?: string; readonly last?: number }): Promise<Message[]>
|
|
165
173
|
send(message: SessionSend): Promise<void>
|
|
166
174
|
startTask(task: TaskStartArgs): Promise<Task>
|
|
175
|
+
fork(task: Pick<TaskStartArgs, 'prompt' | 'title'>): Promise<Task>
|
|
176
|
+
clone(task: Pick<TaskStartArgs, 'prompt' | 'title'>): Promise<Task>
|
|
167
177
|
callTool(call: ToolCallArgs): Promise<ToolResult>
|
|
168
178
|
usage(): Promise<Usage>
|
|
169
179
|
surfaces(): Promise<Surface[]>
|
|
170
180
|
}
|
|
171
181
|
|
|
172
|
-
export type TaskOwner = Omit<Session, 'startTask' | 'callTool'>
|
|
182
|
+
export type TaskOwner = Omit<Session, 'startTask' | 'callTool' | 'fork' | 'clone'>
|
|
173
183
|
|
|
174
184
|
type ProcessEvent = { readonly on: string; readonly before?: string }
|
|
175
185
|
|
|
@@ -305,9 +315,9 @@ export type ProcessPayload = {
|
|
|
305
315
|
export type ProcessAsked = Omit<ProcessPayload, 'SessionStart'> & { readonly SessionStart: SessionStartAsked }
|
|
306
316
|
|
|
307
317
|
export type ProcessAnswer = {
|
|
308
|
-
readonly ProjectSetup:
|
|
318
|
+
readonly ProjectSetup: { readonly note: string }
|
|
309
319
|
readonly SessionStart: { readonly note?: string; readonly title?: string; readonly reloadSkills?: true; readonly firstPrompt?: string }
|
|
310
|
-
readonly ModelChange: { readonly ask: string } | Refusal
|
|
320
|
+
readonly ModelChange: { readonly ask: string } | { readonly note: string } | Refusal
|
|
311
321
|
readonly WorktreeCreate: { readonly result: string }
|
|
312
322
|
readonly Elicitation: Refusal
|
|
313
323
|
readonly ElicitationResponse: Refusal
|
|
@@ -20,7 +20,7 @@ export type Fakes = {
|
|
|
20
20
|
config: { list?: Claude['config']['list'] }
|
|
21
21
|
command: { list?: Claude['command']['list']; run?: Claude['command']['run'] }
|
|
22
22
|
ui: { ask?: Claude['ui']['ask']; open?: Claude['ui']['open']; scroll?: Claude['ui']['scroll']; copy?: Claude['ui']['copy']; focus?: Claude['ui']['focus'] }
|
|
23
|
-
session: { messages?: Claude['session']['messages']; append?: Claude['session']['append']; send?: Claude['session']['send'] }
|
|
23
|
+
session: { messages?: Claude['session']['messages']; append?: Claude['session']['append']; send?: Claude['session']['send']; surfaces?: Claude['session']['surfaces']; usage?: Claude['session']['usage'] }
|
|
24
24
|
prompt: { submit?: Claude['prompt']['submit'] }
|
|
25
25
|
model: { complete?: Claude['model']['complete'] }
|
|
26
26
|
agent: { list?: Claude['agent']['list']; spawn?: Claude['agent']['spawn'] }
|
|
@@ -49,6 +49,7 @@ export type FakeClaude = {
|
|
|
49
49
|
readonly shown: Shown
|
|
50
50
|
readonly store: Map<string, unknown>
|
|
51
51
|
settle(): Promise<void>
|
|
52
|
+
placePane(id: string): void
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
export function calledAnswer(caller: string, answer: EventResult<'cmod.call'>): unknown {
|
|
@@ -80,6 +81,16 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
80
81
|
return async (...args: Args): Promise<Result> => run(...args)
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
const cmodCall = rejected('cmod.call', () => fakes.cmod.call ?? cmodTakesParts)
|
|
85
|
+
|
|
86
|
+
const clonedAsClaudeCodeDoes = <Args,>(args: Args): Args => {
|
|
87
|
+
try {
|
|
88
|
+
return structuredClone(args)
|
|
89
|
+
} catch (error) {
|
|
90
|
+
throw new Error(`interface.call: arguments are not plain data: ${error instanceof Error ? error.message : String(error)}`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
83
94
|
const cmodTakesParts: Claude['cmod']['call'] = async (input) => {
|
|
84
95
|
if (input.to === cmodPluginName && input.method === partMethods.changed) return null
|
|
85
96
|
throw new Error(`cmod.call(${JSON.stringify(input)}) has no fake answer. Set fakes.cmod.call on the tested mod.`)
|
|
@@ -118,7 +129,7 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
118
129
|
lines.push(text)
|
|
119
130
|
},
|
|
120
131
|
notice: () => undefined,
|
|
121
|
-
invalidate: () =>
|
|
132
|
+
invalidate: (event) => void calls.push({ call: 'ui.invalidate', args: [event] }),
|
|
122
133
|
ask: rejected('ui.ask', () => fakes.ui.ask),
|
|
123
134
|
open: async (pane) => {
|
|
124
135
|
calls.push({ call: 'ui.open', args: [pane] })
|
|
@@ -188,8 +199,8 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
188
199
|
root: async () => plugin.root,
|
|
189
200
|
cwd: async () => plugin.root,
|
|
190
201
|
model: async () => 'test-model',
|
|
191
|
-
usage:
|
|
192
|
-
surfaces:
|
|
202
|
+
usage: () => fakes.session.usage?.() ?? Promise.resolve({ startedAt: Date.parse('2026-01-01T00:00:00.000Z'), context: { window: 200000 }, rateLimits: [], cost: { usd: 0 } }),
|
|
203
|
+
surfaces: () => fakes.session.surfaces?.() ?? Promise.resolve(['terminal']),
|
|
193
204
|
messages: rejected('session.messages', () => fakes.session.messages as ((args?: unknown) => Promise<unknown>) | undefined) as Claude['session']['messages'],
|
|
194
205
|
append: rejected('session.append', () => fakes.session.append ?? appendNote),
|
|
195
206
|
send: rejected('session.send', () => fakes.session.send ?? (async () => ({ isDelivered: true as const }))),
|
|
@@ -217,7 +228,7 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
217
228
|
dataHome: async () => undefined,
|
|
218
229
|
configHome: async () => undefined,
|
|
219
230
|
},
|
|
220
|
-
cmod: { call:
|
|
231
|
+
cmod: { call: async (args) => cmodCall(clonedAsClaudeCodeDoes(args)) },
|
|
221
232
|
}
|
|
222
233
|
|
|
223
234
|
const settle = async () => {
|
|
@@ -228,7 +239,11 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
228
239
|
} while (calls.length !== seen)
|
|
229
240
|
}
|
|
230
241
|
|
|
231
|
-
|
|
242
|
+
const placePane = (id: string) => {
|
|
243
|
+
if (unplacedPanes.delete(id)) shown.openPanes.add(id)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { claude, calls, fakes, shown, store, settle, placePane }
|
|
232
247
|
}
|
|
233
248
|
|
|
234
249
|
declare function setTimeout(fn: () => void, ms: number): unknown
|