@cmodjs/core 4.0.3 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/commands.md +44 -9
- package/docs/index.md +3 -3
- package/docs/install-steps.md +8 -8
- package/docs/jobs.md +2 -1
- package/docs/mod.md +1 -1
- package/docs/options.md +1 -1
- package/docs/permissions.md +1 -1
- package/docs/session.md +46 -13
- package/docs/testing.md +15 -11
- package/docs/ui.md +43 -11
- package/installer.d.ts +3 -2
- package/installer.d.ts.map +1 -1
- package/jobs/program.d.ts +1 -0
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +39 -23
- package/mod.d.ts +3 -2
- package/mod.d.ts.map +1 -1
- package/mod.js +1 -0
- package/package.json +1 -1
- package/records.d.ts +15 -1
- package/records.d.ts.map +1 -1
- package/records.js +39 -1
- package/register.d.ts.map +1 -1
- package/register.js +8 -5
- package/runtime/before.d.ts +6 -2
- package/runtime/before.d.ts.map +1 -1
- package/runtime/before.js +56 -18
- package/runtime/buttons.d.ts +10 -0
- package/runtime/buttons.d.ts.map +1 -0
- package/runtime/buttons.js +12 -0
- package/runtime/conversations.d.ts +3 -0
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +164 -27
- package/runtime/engine.d.ts +2 -0
- package/runtime/engine.d.ts.map +1 -1
- package/runtime/grants.d.ts +1 -0
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +5 -2
- package/runtime/hotkeys.d.ts.map +1 -1
- package/runtime/hotkeys.js +7 -4
- package/runtime/installer.d.ts +10 -6
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +106 -48
- package/runtime/lifecycle.d.ts +2 -2
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +144 -42
- package/runtime/pages.d.ts +3 -1
- package/runtime/pages.d.ts.map +1 -1
- package/runtime/pages.js +48 -14
- package/runtime/parts.d.ts +1 -0
- package/runtime/parts.d.ts.map +1 -1
- package/runtime/parts.js +6 -3
- package/runtime/programs.d.ts +1 -0
- package/runtime/programs.d.ts.map +1 -1
- package/runtime/programs.js +4 -1
- package/runtime/sessions.d.ts +10 -3
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +104 -59
- package/runtime/toaster.d.ts.map +1 -1
- package/runtime/toaster.js +23 -9
- package/runtime/ui.d.ts +22 -4
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +161 -59
- package/runtime/watches.d.ts.map +1 -1
- package/runtime/watches.js +18 -4
- package/runtime/widgets.d.ts +7 -5
- package/runtime/widgets.d.ts.map +1 -1
- package/runtime/widgets.js +31 -21
- package/session/context/turn/message/attached-file.d.ts +13 -0
- package/session/context/turn/message/attached-file.d.ts.map +1 -0
- package/session/context/turn/message/attached-file.js +21 -0
- package/session/context/turn/message/message.d.ts +3 -2
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +20 -6
- package/session/conversation.d.ts +15 -3
- package/session/conversation.d.ts.map +1 -1
- package/session/conversation.js +23 -4
- package/session/goal/task.d.ts +5 -0
- package/session/goal/task.d.ts.map +1 -1
- package/session/session.d.ts +36 -7
- package/session/session.d.ts.map +1 -1
- package/session/session.js +2 -1
- package/src/installer.ts +3 -2
- package/src/jobs/program.ts +41 -21
- package/src/mod.ts +3 -2
- package/src/records.ts +49 -1
- package/src/register.ts +11 -8
- package/src/runtime/before.ts +61 -22
- package/src/runtime/buttons.ts +22 -0
- package/src/runtime/conversations.ts +170 -34
- package/src/runtime/engine.ts +2 -0
- package/src/runtime/grants.ts +6 -2
- package/src/runtime/hotkeys.ts +7 -4
- package/src/runtime/installer.ts +147 -83
- package/src/runtime/lifecycle.ts +149 -48
- package/src/runtime/pages.ts +56 -12
- package/src/runtime/parts.ts +8 -4
- package/src/runtime/programs.ts +5 -1
- package/src/runtime/sessions.ts +106 -59
- package/src/runtime/toaster.ts +27 -9
- package/src/runtime/ui.ts +166 -62
- package/src/runtime/watches.ts +20 -5
- package/src/runtime/widgets.ts +35 -23
- package/src/session/context/turn/message/attached-file.ts +36 -0
- package/src/session/context/turn/message/message.ts +20 -7
- package/src/session/conversation.ts +31 -6
- package/src/session/goal/task.ts +6 -0
- package/src/session/session.ts +18 -7
- package/src/testing/fake-claude.ts +21 -6
- package/src/testing/fake-elements.ts +57 -8
- package/src/testing/fake-sessions.ts +72 -24
- package/src/testing.ts +114 -33
- package/src/ui/areas.ts +1 -0
- package/src/ui/components.ts +14 -8
- package/src/ui/elements.ts +6 -2
- package/src/ui/markdown.ts +3 -3
- package/src/ui/panes.ts +2 -2
- package/src/ui/styles.ts +6 -2
- package/testing/fake-claude.d.ts +3 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +18 -5
- package/testing/fake-elements.d.ts.map +1 -1
- package/testing/fake-elements.js +52 -7
- package/testing/fake-sessions.d.ts +24 -10
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +66 -20
- package/testing.d.ts +8 -12
- package/testing.d.ts.map +1 -1
- package/testing.js +113 -27
- package/ui/areas.d.ts +4 -0
- package/ui/areas.d.ts.map +1 -1
- package/ui/components.d.ts +1 -0
- package/ui/components.d.ts.map +1 -1
- package/ui/components.js +11 -7
- package/ui/elements.d.ts +3 -0
- package/ui/elements.d.ts.map +1 -1
- package/ui/elements.js +6 -1
- package/ui/markdown.d.ts +2 -2
- package/ui/markdown.d.ts.map +1 -1
- package/ui/panes.d.ts +2 -1
- package/ui/panes.d.ts.map +1 -1
- package/ui/styles.d.ts +11 -2
- package/ui/styles.d.ts.map +1 -1
package/src/runtime/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) => {
|
|
@@ -187,7 +196,7 @@ export function serveWidgets({
|
|
|
187
196
|
|
|
188
197
|
const reload = async () => {
|
|
189
198
|
stored = await read()
|
|
190
|
-
place()
|
|
199
|
+
if (JSON.stringify(withNewWidgets(stored)) !== JSON.stringify(layout)) place()
|
|
191
200
|
}
|
|
192
201
|
|
|
193
202
|
saving = (async () => {
|
|
@@ -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,12 +6,13 @@ 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 = {
|
|
13
13
|
readonly id: string
|
|
14
14
|
readonly title: string
|
|
15
|
+
readonly color?: string
|
|
15
16
|
readonly project: string
|
|
16
17
|
readonly cwd: string
|
|
17
18
|
readonly model: string
|
|
@@ -28,7 +29,23 @@ export type TurnData = Pick<Turn, 'prompt' | 'context' | 'openedAt' | 'closedAt'
|
|
|
28
29
|
|
|
29
30
|
export type TurnEnding = Pick<Turn, 'answer' | 'reason' | 'failure'>
|
|
30
31
|
|
|
31
|
-
export type TaskData = Pick<Task, 'id' | 'title' | 'openedBy' | 'openedAt' | 'closedAt'> & { readonly agentType
|
|
32
|
+
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 }
|
|
33
|
+
|
|
34
|
+
export const forkAgentType = 'fork'
|
|
35
|
+
|
|
36
|
+
export function ownerOfSpawn(agentType: string | undefined, session: string): Pick<TaskData, 'agentType' | 'forkedFrom'> {
|
|
37
|
+
return agentType === forkAgentType ? { forkedFrom: session } : { agentType: agentType ?? 'general-purpose' }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type Agent = { readonly id: string; readonly status: TaskStatus; readonly name?: string; readonly spawnedBy?: string }
|
|
41
|
+
|
|
42
|
+
export function tasksWithAgents(tasks: readonly TaskData[], agents: readonly Agent[]): TaskData[] {
|
|
43
|
+
const byId = new Map(agents.map((agent) => [agent.id, agent]))
|
|
44
|
+
return tasks.map((task) => {
|
|
45
|
+
const agent = byId.get(task.id)
|
|
46
|
+
return agent === undefined ? task : { ...task, status: agent.status, ...(agent.name === undefined ? {} : { name: agent.name }), ...(agent.spawnedBy === undefined ? {} : { spawnedBy: agent.spawnedBy }) }
|
|
47
|
+
})
|
|
48
|
+
}
|
|
32
49
|
|
|
33
50
|
export type RunningTask = TaskData & { readonly context: number; readonly contextOpenedAt?: string }
|
|
34
51
|
|
|
@@ -60,6 +77,7 @@ type OpenCall = Pick<ToolCall, 'name' | 'input' | 'cwd'>
|
|
|
60
77
|
export type Conversation = {
|
|
61
78
|
add(row: Row, task?: string): SessionChange[]
|
|
62
79
|
startTask(task: TaskData): SessionChange[]
|
|
80
|
+
endTask(task: TaskData, at: string): SessionChange[]
|
|
63
81
|
startContext(at: string): SessionChange[]
|
|
64
82
|
endTurn(at: string, ending: TurnEnding, task?: string): SessionChange[]
|
|
65
83
|
changeGoal(text: string): SessionChange[]
|
|
@@ -117,7 +135,7 @@ export function createConversation(start: ConversationState): Conversation {
|
|
|
117
135
|
return [{ type: TurnEnd, turn: ended }]
|
|
118
136
|
}
|
|
119
137
|
|
|
120
|
-
const
|
|
138
|
+
const endCall = (call: string, at: string, only?: string): SessionChange[] => {
|
|
121
139
|
const opened = [...tasks.values()].filter((each) => each.openedBy === call && (only === undefined || each.id === only))
|
|
122
140
|
if (opened.length === 0) {
|
|
123
141
|
if (agentCalls.has(call)) endedCalls.set(call, at)
|
|
@@ -168,10 +186,10 @@ export function createConversation(start: ConversationState): Conversation {
|
|
|
168
186
|
} as Message
|
|
169
187
|
changes.push({ type: Message, message: placed })
|
|
170
188
|
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(...
|
|
189
|
+
if (placed.type === ToolResult && agentCalls.has(placed.callId) && !String(placed.output).startsWith(asyncLaunch)) changes.push(...endCall(placed.callId, row.at))
|
|
172
190
|
if (placed.type === Note && placed.from.type === Task) {
|
|
173
191
|
const call = tagOf(textOf(row.content), 'tool-use-id')
|
|
174
|
-
if (call !== undefined && agentCalls.has(call)) changes.push(...
|
|
192
|
+
if (call !== undefined && agentCalls.has(call)) changes.push(...endCall(call, row.at, tagOf(textOf(row.content), 'task-id')))
|
|
175
193
|
}
|
|
176
194
|
}
|
|
177
195
|
return changes
|
|
@@ -183,9 +201,16 @@ export function createConversation(start: ConversationState): Conversation {
|
|
|
183
201
|
agentCalls.add(data.openedBy)
|
|
184
202
|
const changes: SessionChange[] = [{ type: TaskStart, task: started }]
|
|
185
203
|
const endedAt = endedCalls.get(data.openedBy)
|
|
186
|
-
if (endedAt !== undefined) changes.push(...
|
|
204
|
+
if (endedAt !== undefined) changes.push(...endCall(data.openedBy, endedAt, data.id))
|
|
187
205
|
return changes
|
|
188
206
|
},
|
|
207
|
+
endTask(data, at) {
|
|
208
|
+
const open = tasks.get(data.id) ?? { ...data, context }
|
|
209
|
+
if (open.closedAt !== undefined) return []
|
|
210
|
+
const ended = { ...open, closedAt: at }
|
|
211
|
+
tasks.set(data.id, ended)
|
|
212
|
+
return [...endTaskTurn(data.id, at), { type: TaskEnd, task: ended }]
|
|
213
|
+
},
|
|
189
214
|
startContext,
|
|
190
215
|
endTurn: (at, ending, task) => (task === undefined ? endTurn(at, ending) : endTaskTurn(task, at, ending)),
|
|
191
216
|
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 = {
|
|
@@ -148,6 +156,7 @@ export type ToolCallArgs = {
|
|
|
148
156
|
export type Session = RecordPart & {
|
|
149
157
|
readonly id: string
|
|
150
158
|
readonly title: string
|
|
159
|
+
readonly color?: string
|
|
151
160
|
readonly project: string
|
|
152
161
|
readonly agentType?: string
|
|
153
162
|
readonly model: string
|
|
@@ -164,12 +173,14 @@ export type Session = RecordPart & {
|
|
|
164
173
|
messages(options?: { readonly after?: string; readonly last?: number }): Promise<Message[]>
|
|
165
174
|
send(message: SessionSend): Promise<void>
|
|
166
175
|
startTask(task: TaskStartArgs): Promise<Task>
|
|
176
|
+
fork(task: Pick<TaskStartArgs, 'prompt' | 'title'>): Promise<Task>
|
|
177
|
+
clone(task: Pick<TaskStartArgs, 'prompt' | 'title'>): Promise<Task>
|
|
167
178
|
callTool(call: ToolCallArgs): Promise<ToolResult>
|
|
168
179
|
usage(): Promise<Usage>
|
|
169
180
|
surfaces(): Promise<Surface[]>
|
|
170
181
|
}
|
|
171
182
|
|
|
172
|
-
export type TaskOwner = Omit<Session, 'startTask' | 'callTool'>
|
|
183
|
+
export type TaskOwner = Omit<Session, 'startTask' | 'callTool' | 'fork' | 'clone'>
|
|
173
184
|
|
|
174
185
|
type ProcessEvent = { readonly on: string; readonly before?: string }
|
|
175
186
|
|
|
@@ -305,9 +316,9 @@ export type ProcessPayload = {
|
|
|
305
316
|
export type ProcessAsked = Omit<ProcessPayload, 'SessionStart'> & { readonly SessionStart: SessionStartAsked }
|
|
306
317
|
|
|
307
318
|
export type ProcessAnswer = {
|
|
308
|
-
readonly ProjectSetup:
|
|
319
|
+
readonly ProjectSetup: { readonly note: string }
|
|
309
320
|
readonly SessionStart: { readonly note?: string; readonly title?: string; readonly reloadSkills?: true; readonly firstPrompt?: string }
|
|
310
|
-
readonly ModelChange: { readonly ask: string } | Refusal
|
|
321
|
+
readonly ModelChange: { readonly ask: string } | { readonly note: string } | Refusal
|
|
311
322
|
readonly WorktreeCreate: { readonly result: string }
|
|
312
323
|
readonly Elicitation: Refusal
|
|
313
324
|
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
|