@cmodjs/core 3.1.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/commands.md +38 -21
- package/docs/dependencies.md +27 -20
- package/docs/index.md +21 -17
- package/docs/install-steps.md +37 -29
- package/docs/jobs.md +25 -52
- package/docs/mod.md +93 -65
- package/docs/options.md +2 -2
- package/docs/permissions.md +4 -4
- package/docs/session.md +14 -12
- package/docs/testing.md +55 -32
- package/docs/ui.md +255 -263
- package/hotkeys.d.ts +22 -0
- package/hotkeys.d.ts.map +1 -0
- package/hotkeys.js +26 -0
- package/installer.d.ts +1 -1
- package/installer.d.ts.map +1 -1
- package/installer.js +11 -6
- package/jobs/check.d.ts +2 -1
- package/jobs/check.d.ts.map +1 -1
- package/jobs/check.js +2 -2
- package/jobs/context.d.ts +2 -1
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +1 -1
- package/jobs/permissions.d.ts +2 -1
- package/jobs/permissions.d.ts.map +1 -1
- package/jobs/permissions.js +2 -2
- package/jobs/program.js +1 -1
- package/jobs/prompt.d.ts +2 -1
- package/jobs/prompt.d.ts.map +1 -1
- package/jobs/prompt.js +20 -13
- package/jobs/schedule.d.ts +2 -1
- package/jobs/schedule.d.ts.map +1 -1
- package/jobs/schedule.js +15 -1
- package/jobs/slash-command.d.ts +6 -2
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +6 -5
- package/jobs/tool.d.ts +6 -4
- package/jobs/tool.d.ts.map +1 -1
- package/jobs/tool.js +3 -3
- package/mod.d.ts +63 -42
- package/mod.d.ts.map +1 -1
- package/mod.js +11 -1
- package/package.json +1 -1
- package/records.d.ts +13 -8
- package/records.d.ts.map +1 -1
- package/records.js +41 -31
- package/register.d.ts.map +1 -1
- package/register.js +1 -2
- package/runtime/announce.d.ts +1 -1
- package/runtime/announce.d.ts.map +1 -1
- package/runtime/announce.js +6 -7
- package/runtime/before.d.ts +2 -2
- package/runtime/before.d.ts.map +1 -1
- package/runtime/before.js +1 -1
- package/runtime/claude.d.ts +0 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +4 -1
- package/runtime/conversations.d.ts.map +1 -1
- package/runtime/conversations.js +42 -14
- package/runtime/dependencies.d.ts.map +1 -1
- package/runtime/dependencies.js +2 -1
- package/runtime/engine.d.ts +5 -0
- package/runtime/engine.d.ts.map +1 -1
- package/runtime/grants.js +2 -2
- package/runtime/hooks.d.ts +1 -1
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hotkeys.d.ts +15 -0
- package/runtime/hotkeys.d.ts.map +1 -0
- package/runtime/hotkeys.js +58 -0
- package/runtime/installer.d.ts +5 -2
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +23 -12
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +190 -141
- package/runtime/options.js +2 -2
- package/runtime/pages.d.ts +28 -0
- package/runtime/pages.d.ts.map +1 -0
- package/runtime/pages.js +98 -0
- package/runtime/parts.d.ts +14 -0
- package/runtime/parts.d.ts.map +1 -0
- package/runtime/parts.js +63 -0
- package/runtime/permission-check.d.ts +2 -2
- package/runtime/permission-check.d.ts.map +1 -1
- package/runtime/programs.js +1 -1
- package/runtime/router.d.ts +2 -2
- package/runtime/router.d.ts.map +1 -1
- package/runtime/router.js +1 -1
- package/runtime/sessions.d.ts +10 -9
- package/runtime/sessions.d.ts.map +1 -1
- package/runtime/sessions.js +14 -5
- package/runtime/state.d.ts +2 -1
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +1 -0
- package/runtime/toaster.d.ts +1 -1
- package/runtime/toaster.d.ts.map +1 -1
- package/runtime/toaster.js +10 -4
- package/runtime/ui.d.ts +87 -5
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +372 -109
- package/runtime/watches.d.ts +9 -0
- package/runtime/watches.d.ts.map +1 -0
- package/runtime/watches.js +73 -0
- package/runtime/widgets.d.ts +48 -0
- package/runtime/widgets.d.ts.map +1 -0
- package/runtime/widgets.js +219 -0
- package/session/context/turn/message/message.d.ts.map +1 -1
- package/session/context/turn/message/message.js +7 -2
- package/session/session.d.ts +11 -5
- package/session/session.d.ts.map +1 -1
- package/src/hotkeys.ts +51 -0
- package/src/installer.ts +8 -4
- package/src/jobs/check.ts +4 -3
- package/src/jobs/context.ts +3 -2
- package/src/jobs/permissions.ts +4 -3
- package/src/jobs/program.ts +1 -1
- package/src/jobs/prompt.ts +23 -14
- package/src/jobs/schedule.ts +16 -2
- package/src/jobs/slash-command.ts +10 -7
- package/src/jobs/tool.ts +7 -5
- package/src/mod.ts +59 -47
- package/src/records.ts +46 -31
- package/src/register.ts +2 -3
- package/src/runtime/announce.ts +6 -7
- package/src/runtime/before.ts +4 -4
- package/src/runtime/claude.ts +0 -1
- package/src/runtime/conversations.ts +66 -20
- package/src/runtime/dependencies.ts +2 -1
- package/src/runtime/engine.ts +5 -0
- package/src/runtime/grants.ts +2 -2
- package/src/runtime/hooks.ts +1 -0
- package/src/runtime/hotkeys.ts +80 -0
- package/src/runtime/installer.ts +24 -12
- package/src/runtime/lifecycle.ts +189 -136
- package/src/runtime/options.ts +2 -2
- package/src/runtime/pages.ts +127 -0
- package/src/runtime/parts.ts +72 -0
- package/src/runtime/permission-check.ts +2 -2
- package/src/runtime/programs.ts +1 -1
- package/src/runtime/router.ts +3 -3
- package/src/runtime/sessions.ts +24 -15
- package/src/runtime/state.ts +3 -1
- package/src/runtime/toaster.ts +10 -4
- package/src/runtime/ui.ts +405 -115
- package/src/runtime/watches.ts +75 -0
- package/src/runtime/widgets.ts +287 -0
- package/src/session/context/turn/message/message.ts +7 -2
- package/src/session/session.ts +10 -6
- package/src/testing/fake-claude.ts +9 -6
- package/src/testing/fake-elements.ts +7 -4
- package/src/testing/fake-sessions.ts +6 -4
- package/src/testing.ts +203 -46
- package/src/ui/areas.ts +27 -0
- package/src/ui/elements.ts +35 -0
- package/src/ui/markdown.ts +18 -18
- package/src/ui/panes.ts +73 -0
- package/src/ui/styles.ts +53 -0
- package/src/utils/subscription.ts +1 -0
- package/testing/fake-claude.d.ts +0 -1
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +9 -5
- package/testing/fake-elements.js +7 -4
- package/testing/fake-sessions.d.ts +2 -1
- package/testing/fake-sessions.d.ts.map +1 -1
- package/testing/fake-sessions.js +1 -0
- package/testing.d.ts +21 -6
- package/testing.d.ts.map +1 -1
- package/testing.js +206 -45
- package/ui/areas.d.ts +29 -0
- package/ui/areas.d.ts.map +1 -0
- package/ui/areas.js +6 -0
- package/ui/elements.d.ts +3 -0
- package/ui/elements.d.ts.map +1 -1
- package/ui/elements.js +36 -0
- package/ui/markdown.d.ts +14 -14
- package/ui/markdown.d.ts.map +1 -1
- package/ui/markdown.js +11 -11
- package/ui/panes.d.ts +71 -0
- package/ui/panes.d.ts.map +1 -0
- package/ui/panes.js +16 -0
- package/ui/styles.d.ts +44 -0
- package/ui/styles.d.ts.map +1 -0
- package/ui/styles.js +24 -0
- package/utils/subscription.d.ts +4 -0
- package/utils/subscription.d.ts.map +1 -0
- package/jobs/status-line.d.ts +0 -6
- package/jobs/status-line.d.ts.map +0 -1
- package/jobs/status-line.js +0 -39
- package/src/jobs/status-line.ts +0 -44
- package/src/ui/define-pane.ts +0 -27
- package/src/ui/places.ts +0 -49
- package/src/utils/disposable.ts +0 -3
- package/ui/define-pane.d.ts +0 -15
- package/ui/define-pane.d.ts.map +0 -1
- package/ui/define-pane.js +0 -12
- package/ui/places.d.ts +0 -43
- package/ui/places.d.ts.map +0 -1
- package/ui/places.js +0 -31
- package/utils/disposable.d.ts +0 -4
- package/utils/disposable.d.ts.map +0 -1
- /package/utils/{disposable.js → subscription.js} +0 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { FileChange } from '../mod.js'
|
|
2
|
+
import { ifMissing } from '../utils/paths.js'
|
|
3
|
+
import type { Subscription } from '../utils/subscription.js'
|
|
4
|
+
import type { Claude } from './claude.js'
|
|
5
|
+
import type { Failure } from './toaster.js'
|
|
6
|
+
|
|
7
|
+
export type Watches = { add(paths: readonly string[], listener: (change: FileChange) => unknown): Subscription }
|
|
8
|
+
|
|
9
|
+
type Watch = { readonly paths: readonly string[]; readonly listener: (change: FileChange) => unknown; seen: ReadonlyMap<string, number> | undefined }
|
|
10
|
+
|
|
11
|
+
const watchMs = 500
|
|
12
|
+
|
|
13
|
+
const watchedFiles = 2000
|
|
14
|
+
|
|
15
|
+
export function createWatches(claude: Claude, report: (failure: Failure) => void): Watches {
|
|
16
|
+
const watches = new Set<Watch>()
|
|
17
|
+
let polling: { cancel(): void } | undefined
|
|
18
|
+
let isPolling = false
|
|
19
|
+
const stampsOf = async (paths: readonly string[]): Promise<Map<string, number>> => {
|
|
20
|
+
const stamps = new Map<string, number>()
|
|
21
|
+
const visit = async (path: string): Promise<void> => {
|
|
22
|
+
const stat = await claude.fs.stat(path).catch(ifMissing(undefined))
|
|
23
|
+
if (stat === undefined) return
|
|
24
|
+
if (stat.kind !== 'dir') {
|
|
25
|
+
if (stamps.size === watchedFiles) throw new Error(`fs.onChange watches at most ${watchedFiles} files, and ${paths.join(', ')} holds more, so it stopped. Watch a smaller folder.`)
|
|
26
|
+
return void stamps.set(path, stat.mtimeMs)
|
|
27
|
+
}
|
|
28
|
+
for (const entry of await claude.fs.list(path).catch(ifMissing([]))) await visit(`${path}/${entry.name}`)
|
|
29
|
+
}
|
|
30
|
+
for (const path of paths) await visit(path)
|
|
31
|
+
return stamps
|
|
32
|
+
}
|
|
33
|
+
const stop = (watch: Watch) => {
|
|
34
|
+
watches.delete(watch)
|
|
35
|
+
if (watches.size > 0) return
|
|
36
|
+
polling?.cancel()
|
|
37
|
+
polling = undefined
|
|
38
|
+
}
|
|
39
|
+
const stampsOrStop = (watch: Watch) =>
|
|
40
|
+
stampsOf(watch.paths).catch((error: unknown) => {
|
|
41
|
+
stop(watch)
|
|
42
|
+
report({ id: 'fs:onChange', message: 'Could not read the files fs.onChange watches', error })
|
|
43
|
+
return undefined
|
|
44
|
+
})
|
|
45
|
+
const poll = async () => {
|
|
46
|
+
if (isPolling) return
|
|
47
|
+
isPolling = true
|
|
48
|
+
try {
|
|
49
|
+
for (const watch of watches) {
|
|
50
|
+
const now = await stampsOrStop(watch)
|
|
51
|
+
if (now === undefined) continue
|
|
52
|
+
const before = watch.seen
|
|
53
|
+
watch.seen = now
|
|
54
|
+
if (before === undefined) continue
|
|
55
|
+
const changes: FileChange[] = [
|
|
56
|
+
...[...now].filter(([path, at]) => before.has(path) && before.get(path) !== at).map(([path]): FileChange => ({ path, type: 'change' })),
|
|
57
|
+
...[...now.keys()].filter((path) => !before.has(path)).map((path): FileChange => ({ path, type: 'add' })),
|
|
58
|
+
...[...before.keys()].filter((path) => !now.has(path)).map((path): FileChange => ({ path, type: 'unlink' })),
|
|
59
|
+
]
|
|
60
|
+
for (const change of changes) await Promise.resolve().then(() => watch.listener(change)).catch((error: unknown) => report({ id: 'fs:onChange', message: 'The fs.onChange listener failed', error }))
|
|
61
|
+
}
|
|
62
|
+
} finally {
|
|
63
|
+
isPolling = false
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
add(paths, listener) {
|
|
68
|
+
const watch: Watch = { paths, listener, seen: undefined }
|
|
69
|
+
watches.add(watch)
|
|
70
|
+
void stampsOrStop(watch).then((stamps) => void (watch.seen ??= stamps))
|
|
71
|
+
polling ??= claude.clock.every(watchMs, () => void poll())
|
|
72
|
+
return { remove: () => stop(watch) }
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import type { ElementTable, RenderElement } from 'claude-code'
|
|
2
|
+
import { isObject, widgetsPath } from '../records.js'
|
|
3
|
+
import type { TurnData } from '../session/conversation.js'
|
|
4
|
+
import { AbovePrompt, areas, BelowPrompt, TurnEnd, Working, type Area } from '../ui/areas.js'
|
|
5
|
+
import { Box, drawWith } from '../ui/elements.js'
|
|
6
|
+
import { ifMissing } from '../utils/paths.js'
|
|
7
|
+
import { messageOf } from '../utils/text.js'
|
|
8
|
+
import type { Claude } from './claude.js'
|
|
9
|
+
import { cmodPluginName } from './dependencies.js'
|
|
10
|
+
import type { DrawnParts } from './parts.js'
|
|
11
|
+
import type { Router } from './router.js'
|
|
12
|
+
import { partMethods, type PartsUpdate, type WidgetDeclaration } from './ui.js'
|
|
13
|
+
|
|
14
|
+
export type WidgetRow = { readonly left: readonly string[]; readonly right: readonly string[] }
|
|
15
|
+
|
|
16
|
+
export type WidgetLayout = { readonly areas: Readonly<Record<Area, readonly WidgetRow[]>>; readonly hidden: readonly string[] }
|
|
17
|
+
|
|
18
|
+
export type PlacedWidget = { readonly key: string; readonly mod: string; readonly title: string; readonly areas: readonly Area[] }
|
|
19
|
+
|
|
20
|
+
export type Move = 'up' | 'down' | 'left' | 'right'
|
|
21
|
+
|
|
22
|
+
export const claudeHint = 'claude/hint'
|
|
23
|
+
|
|
24
|
+
export const claudeModes = 'claude/modes'
|
|
25
|
+
|
|
26
|
+
export const builtInWidgets: readonly PlacedWidget[] = [
|
|
27
|
+
{ key: claudeHint, mod: 'claude', title: "Claude Code's hint", areas: [BelowPrompt] },
|
|
28
|
+
{ key: claudeModes, mod: 'claude', title: "Claude Code's mode labels", areas: [BelowPrompt] },
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
const promptIndent = 4
|
|
32
|
+
|
|
33
|
+
const transcriptIndent = 2
|
|
34
|
+
|
|
35
|
+
const terminalColumns = 80
|
|
36
|
+
|
|
37
|
+
const separator = ''
|
|
38
|
+
|
|
39
|
+
export type Widgets = {
|
|
40
|
+
update(mod: string, update: Pick<PartsUpdate, 'parts' | 'redraw'>): void
|
|
41
|
+
widgets(): readonly PlacedWidget[]
|
|
42
|
+
layout(): WidgetLayout
|
|
43
|
+
change(edit: (layout: WidgetLayout) => WidgetLayout): Promise<void>
|
|
44
|
+
reload(): Promise<void>
|
|
45
|
+
recheck(): Promise<void>
|
|
46
|
+
rows(area: Area, columns: number): RenderElement[]
|
|
47
|
+
draw(area: Area, table: ElementTable, columns: number, turn?: TurnData): Promise<RenderElement[]>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function layoutOf(value: unknown): WidgetLayout {
|
|
51
|
+
const keys = (list: unknown) => (Array.isArray(list) ? list.filter((key): key is string => typeof key === 'string') : [])
|
|
52
|
+
const rowsOf = (list: unknown): WidgetRow[] => (Array.isArray(list) ? list.filter(isObject).map((row) => ({ ...row, left: keys(row['left']), right: keys(row['right']) })) : [])
|
|
53
|
+
const stored = isObject(value) && isObject(value['areas']) ? value['areas'] : {}
|
|
54
|
+
return { areas: Object.fromEntries(areas.map((area) => [area, rowsOf(stored[area])])) as unknown as WidgetLayout['areas'], hidden: isObject(value) ? keys(value['hidden']) : [] }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const stringsIn = (value: unknown): string[] => (typeof value === 'string' ? [value] : Array.isArray(value) ? value.flatMap(stringsIn) : isObject(value) ? Object.values(value).flatMap(stringsIn) : [])
|
|
58
|
+
|
|
59
|
+
const holds = (row: WidgetRow, key: string) => row.left.includes(key) || row.right.includes(key)
|
|
60
|
+
|
|
61
|
+
export function placedIn(layout: WidgetLayout, key: string): Area | undefined {
|
|
62
|
+
return areas.find((area) => layout.areas[area].some((row) => holds(row, key)))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function toggled(layout: WidgetLayout, key: string): WidgetLayout {
|
|
66
|
+
return { ...layout, hidden: layout.hidden.includes(key) ? layout.hidden.filter((each) => each !== key) : [...layout.hidden, key] }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function moved(layout: WidgetLayout, widget: PlacedWidget, move: Move): WidgetLayout {
|
|
70
|
+
const area = placedIn(layout, widget.key)
|
|
71
|
+
if (area === undefined) return layout
|
|
72
|
+
const rows = layout.areas[area]
|
|
73
|
+
const at = rows.findIndex((row) => holds(row, widget.key))
|
|
74
|
+
const row = rows[at] as WidgetRow
|
|
75
|
+
const side = row.left.includes(widget.key) ? 'left' : 'right'
|
|
76
|
+
const withAreas = (changed: Partial<Record<Area, readonly WidgetRow[]>>): WidgetLayout => ({ ...layout, areas: { ...layout.areas, ...Object.fromEntries(Object.entries(changed).map(([name, list]) => [name, list.filter((each) => stringsIn(each).length > 0)])) } })
|
|
77
|
+
if (move === 'left' || move === 'right') {
|
|
78
|
+
const sequence = [...row.left, separator, ...row.right]
|
|
79
|
+
const from = sequence.indexOf(widget.key)
|
|
80
|
+
const to = move === 'left' ? from - 1 : from + 1
|
|
81
|
+
if (to < 0 || to >= sequence.length) return layout
|
|
82
|
+
;[sequence[from], sequence[to]] = [sequence[to] as string, widget.key]
|
|
83
|
+
const split = sequence.indexOf(separator)
|
|
84
|
+
return withAreas({ [area]: rows.map((each, index) => (index === at ? { ...each, left: sequence.slice(0, split), right: sequence.slice(split + 1) } : each)) })
|
|
85
|
+
}
|
|
86
|
+
const without = rows.map((each, index) => (index === at ? { ...each, left: each.left.filter((key) => key !== widget.key), right: each.right.filter((key) => key !== widget.key) } : each))
|
|
87
|
+
const added = (target: WidgetRow | undefined): WidgetRow => ({ ...target, left: target?.left ?? [], right: target?.right ?? [], [side]: [...(target?.[side] ?? []), widget.key] })
|
|
88
|
+
const target = at + (move === 'up' ? -1 : 1)
|
|
89
|
+
if (target >= 0 && target < rows.length) return withAreas({ [area]: without.map((each, index) => (index === target ? added(each) : each)) })
|
|
90
|
+
if (move === 'down' && row.left.length + row.right.length > 1) return withAreas({ [area]: [...without, added(undefined)] })
|
|
91
|
+
const next = (move === 'up' ? areas.slice(0, areas.indexOf(area)).reverse() : areas.slice(areas.indexOf(area) + 1)).find((each) => widget.areas.includes(each))
|
|
92
|
+
if (next === undefined) return layout
|
|
93
|
+
const nextRows = layout.areas[next]
|
|
94
|
+
const landing = move === 'up' ? [...nextRows.slice(0, -1), added(nextRows.at(-1))] : [added(nextRows[0]), ...nextRows.slice(1)]
|
|
95
|
+
return withAreas({ [area]: without, [next]: landing })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function declarationsOf(input: unknown): WidgetDeclaration[] {
|
|
99
|
+
const listed = isObject(input) && Array.isArray(input['widgets']) ? (input['widgets'] as unknown[]) : []
|
|
100
|
+
return listed.filter(isObject).flatMap((each) => {
|
|
101
|
+
const known = Array.isArray(each['areas']) ? (each['areas'] as unknown[]).filter((area): area is Area => (areas as readonly unknown[]).includes(area)) : []
|
|
102
|
+
return typeof each['id'] === 'string' && typeof each['title'] === 'string' && known.length > 0 ? [{ id: each['id'], title: each['title'], areas: known }] : []
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function serveWidgets({
|
|
107
|
+
claude,
|
|
108
|
+
router,
|
|
109
|
+
store,
|
|
110
|
+
report,
|
|
111
|
+
turnClosedBy,
|
|
112
|
+
parts,
|
|
113
|
+
}: {
|
|
114
|
+
readonly claude: Claude
|
|
115
|
+
readonly router: Router
|
|
116
|
+
readonly store: string
|
|
117
|
+
readonly report: (id: string, message: string, error: unknown) => void
|
|
118
|
+
readonly turnClosedBy: (rowId: string) => TurnData | undefined
|
|
119
|
+
readonly parts: DrawnParts
|
|
120
|
+
}): Widgets {
|
|
121
|
+
const path = widgetsPath(store)
|
|
122
|
+
const declared = new Map<string, readonly WidgetDeclaration[]>()
|
|
123
|
+
let stored = layoutOf({})
|
|
124
|
+
let layout = stored
|
|
125
|
+
let saving: Promise<void> = Promise.resolve()
|
|
126
|
+
|
|
127
|
+
let placedKeys: ReadonlySet<string> = new Set()
|
|
128
|
+
|
|
129
|
+
const readFile = async () => {
|
|
130
|
+
const file = JSON.parse(await claude.fs.read(path).catch(ifMissing('{}'))) as unknown
|
|
131
|
+
placedKeys = new Set(stringsIn(isObject(file) ? file['areas'] : undefined))
|
|
132
|
+
return file
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const read = async () => layoutOf(await readFile())
|
|
136
|
+
|
|
137
|
+
const redraw = () => claude.ui.invalidate('ui.render')
|
|
138
|
+
|
|
139
|
+
const placed = (): PlacedWidget[] => [...builtInWidgets, ...[...declared].flatMap(([mod, list]) => list.map(({ id, title, areas: listed }) => ({ key: `${mod}/${id}`, mod, title, areas: listed })))]
|
|
140
|
+
|
|
141
|
+
const change = (edit: (current: WidgetLayout) => WidgetLayout) => {
|
|
142
|
+
saving = saving
|
|
143
|
+
.then(async () => {
|
|
144
|
+
const file = await readFile()
|
|
145
|
+
stored = layoutOf(file)
|
|
146
|
+
const current = withNewWidgets(stored)
|
|
147
|
+
const next = edit(current)
|
|
148
|
+
if (next !== current) {
|
|
149
|
+
const kept = isObject(file) ? file : {}
|
|
150
|
+
await claude.fs.write(path, `${JSON.stringify({ ...kept, ...next, areas: { ...(isObject(kept['areas']) ? kept['areas'] : {}), ...next.areas } }, null, 2)}\n`)
|
|
151
|
+
stored = next
|
|
152
|
+
}
|
|
153
|
+
place()
|
|
154
|
+
})
|
|
155
|
+
.catch((error: unknown) => report('widgets:layout', 'Could not save the widget layout', error))
|
|
156
|
+
return saving
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const place = () => {
|
|
160
|
+
layout = withNewWidgets(stored)
|
|
161
|
+
redraw()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const withNewWidgets = (current: WidgetLayout) =>
|
|
165
|
+
placed()
|
|
166
|
+
.filter(({ key, mod }) => mod !== 'claude' && placedIn(current, key) === undefined && !placedKeys.has(key))
|
|
167
|
+
.reduce((next, { key, areas: [area = AbovePrompt] }) => {
|
|
168
|
+
const [first, ...rest] = next.areas[area]
|
|
169
|
+
return { ...next, areas: { ...next.areas, [area]: [{ ...first, left: [...(first?.left ?? []), key], right: first?.right ?? [] }, ...rest] } }
|
|
170
|
+
}, current)
|
|
171
|
+
|
|
172
|
+
const ask = async (mod: string) => {
|
|
173
|
+
try {
|
|
174
|
+
declared.set(mod, declarationsOf(await claude.cmod.call({ to: mod, method: partMethods.list, input: null })))
|
|
175
|
+
} catch (error) {
|
|
176
|
+
declared.delete(mod)
|
|
177
|
+
claude.ui.log(`cmod: ${mod} did not list its widgets: ${messageOf(error)}`, { to: 'debug' })
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const recheck = async () => {
|
|
182
|
+
const mods = [...declared.keys()]
|
|
183
|
+
await Promise.all(mods.map(ask))
|
|
184
|
+
for (const mod of mods) parts.forget(mod)
|
|
185
|
+
redraw()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const reload = async () => {
|
|
189
|
+
stored = await read()
|
|
190
|
+
place()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
saving = (async () => {
|
|
194
|
+
const recorded = (await claude.fs.list(`${store}/records`).catch(ifMissing([]))).map((entry) => entry.name).filter((name) => name.endsWith('.json')).map((name) => name.slice(0, -'.json'.length))
|
|
195
|
+
stored = await read().catch((error: unknown) => {
|
|
196
|
+
report('widgets:layout', 'Could not read the widget layout', error)
|
|
197
|
+
return stored
|
|
198
|
+
})
|
|
199
|
+
const named = areas.flatMap((area) => stored.areas[area].flatMap((row) => [...row.left, ...row.right])).map((key) => key.slice(0, key.indexOf('/')))
|
|
200
|
+
await Promise.all([...new Set([...recorded, ...named])].filter((mod) => mod !== cmodPluginName && mod !== 'claude' && !declared.has(mod)).map(ask))
|
|
201
|
+
place()
|
|
202
|
+
})().catch((error: unknown) => report('widgets:layout', 'Could not list the widgets of the mods', error))
|
|
203
|
+
|
|
204
|
+
const widgetOf = (key: string) => placed().find((widget) => widget.key === key)
|
|
205
|
+
|
|
206
|
+
const cacheKey = (key: string, area: Area, columns: number, turn: TurnData | undefined) => `${key}\u0000${area}\u0000${columns}\u0000${turn?.prompt.id ?? ''}`
|
|
207
|
+
|
|
208
|
+
const nameOf = (widget: PlacedWidget) => `The ${widget.title} widget of ${widget.mod}`
|
|
209
|
+
|
|
210
|
+
const fill = (key: string, area: Area, columns: number, turn: TurnData | undefined): Promise<void> | undefined => {
|
|
211
|
+
const widget = widgetOf(key)
|
|
212
|
+
if (widget === undefined || !widget.areas.includes(area)) return undefined
|
|
213
|
+
return parts.fill(widget.mod, cacheKey(key, area, columns, turn), { widget: key.slice(widget.mod.length + 1), area, columns, ...(turn === undefined ? {} : { turn }) }, nameOf(widget))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const elementFor = (key: string, area: Area, columns: number, turn: TurnData | undefined): RenderElement | undefined => {
|
|
217
|
+
const widget = widgetOf(key)
|
|
218
|
+
return widget === undefined ? undefined : parts.element(widget.mod, cacheKey(key, area, columns, turn), `${key}:`, nameOf(widget), `widget:${key}`)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const isHidden = (key: string) => layout.hidden.includes(key)
|
|
222
|
+
|
|
223
|
+
const shown = (keys: readonly string[]) => keys.filter((key) => !isHidden(key))
|
|
224
|
+
|
|
225
|
+
const rowsNow = (area: Area, columns: number, turn: TurnData | undefined): RenderElement[] =>
|
|
226
|
+
layout.areas[area].flatMap((row) => {
|
|
227
|
+
const side = (keys: readonly string[]) => shown(keys).map((key) => elementFor(key, area, columns, turn)).filter((each) => each !== undefined)
|
|
228
|
+
const left = side(row.left)
|
|
229
|
+
const right = side(row.right)
|
|
230
|
+
return left.length + right.length === 0 ? [] : [Box({ justifyContent: 'space-between', width: columns, children: [Box({ gap: 2, children: left }), Box({ gap: 2, children: right })] })]
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
const keysIn = (area: Area) => layout.areas[area].flatMap((row) => shown([...row.left, ...row.right]))
|
|
234
|
+
|
|
235
|
+
const draw = async (area: Area, table: ElementTable, columns: number, turn?: TurnData): Promise<RenderElement[]> => {
|
|
236
|
+
await Promise.all(keysIn(area).map((key) => fill(key, area, columns, turn)))
|
|
237
|
+
return drawWith(table, () => rowsNow(area, columns, turn))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const rows = (area: Area, columns: number): RenderElement[] => {
|
|
241
|
+
const started = keysIn(area).flatMap((key) => fill(key, area, columns, undefined) ?? [])
|
|
242
|
+
if (started.length > 0) void Promise.all(started).then(() => claude.ui.invalidate('ui.render'))
|
|
243
|
+
return rowsNow(area, columns, undefined)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const column = (table: ElementTable, children: readonly RenderElement[]) => drawWith(table, () => Box({ flexDirection: 'column', children }))
|
|
247
|
+
|
|
248
|
+
const below = (table: ElementTable, above: RenderElement, added: readonly RenderElement[]) => (added.length === 0 ? above : column(table, [above, ...added]))
|
|
249
|
+
|
|
250
|
+
router.add('ui.render', async (e, next) => {
|
|
251
|
+
const table = claude.ui.resolve(e)
|
|
252
|
+
const columns = (e.viewport?.columns ?? terminalColumns) - transcriptIndent
|
|
253
|
+
if (e.component === 'AbovePrompt') return e.props.hasSurvey ? next(e) : below(table, await next(e), await draw(AbovePrompt, table, e.props.bodyColumns))
|
|
254
|
+
if (e.component === 'PromptHint') {
|
|
255
|
+
const added = await draw(BelowPrompt, table, (e.viewport?.columns ?? terminalColumns) - promptIndent)
|
|
256
|
+
return isHidden(claudeHint) ? column(table, added) : below(table, await next(e), added)
|
|
257
|
+
}
|
|
258
|
+
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, columns))
|
|
260
|
+
if (e.component === 'TurnDuration') {
|
|
261
|
+
const turn = turnClosedBy(e.requestId)
|
|
262
|
+
return below(table, await next(e), turn === undefined ? [] : await draw(TurnEnd, table, columns, turn))
|
|
263
|
+
}
|
|
264
|
+
return next(e)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
update(mod, { parts: listing, redraw }) {
|
|
269
|
+
if (listing === undefined && redraw !== true) return
|
|
270
|
+
parts.forget(mod)
|
|
271
|
+
if (listing !== undefined) {
|
|
272
|
+
const listed = declarationsOf(listing)
|
|
273
|
+
const isNewList = JSON.stringify(listed) !== JSON.stringify(declared.get(mod) ?? [])
|
|
274
|
+
declared.set(mod, listed)
|
|
275
|
+
if (isNewList) place()
|
|
276
|
+
}
|
|
277
|
+
claude.ui.invalidate('ui.render')
|
|
278
|
+
},
|
|
279
|
+
widgets: placed,
|
|
280
|
+
layout: () => layout,
|
|
281
|
+
change,
|
|
282
|
+
reload,
|
|
283
|
+
recheck,
|
|
284
|
+
rows,
|
|
285
|
+
draw,
|
|
286
|
+
}
|
|
287
|
+
}
|
|
@@ -50,6 +50,8 @@ export const skillPrefix = 'Base directory for this skill:'
|
|
|
50
50
|
|
|
51
51
|
const rowTypes: readonly string[] = ['user', 'assistant', 'attachment', 'system']
|
|
52
52
|
|
|
53
|
+
const interruption = /^\[Request interrupted by user( for tool use)?\]$/
|
|
54
|
+
|
|
53
55
|
const memoryPath = /^Contents of (.+?)(?: \([^)\n]*\))?:\r?$/m
|
|
54
56
|
|
|
55
57
|
export function instructionPathOf(text: string): string {
|
|
@@ -126,9 +128,12 @@ function unplacedOf(row: Row, isInTask: boolean): Unplaced[] {
|
|
|
126
128
|
if (text === '') return []
|
|
127
129
|
if (text.startsWith(skillPrefix)) return [{ ...base, type: Skill, name: basename(text.slice(skillPrefix.length).split('\n')[0]?.trim() ?? ''), text }]
|
|
128
130
|
if (text.startsWith('<task-notification')) return [{ ...base, type: Note, text: tagOf(text, 'summary') ?? text, from: { type: Task } }]
|
|
129
|
-
if (
|
|
131
|
+
if (interruption.test(text)) return []
|
|
132
|
+
const from = senderOf(row.origin, isInTask)
|
|
133
|
+
if (row.isMeta) return [{ ...base, type: Note, text, from }]
|
|
130
134
|
const prompt = promptText(text)
|
|
131
|
-
|
|
135
|
+
const isPluginCommand = from.type === Mod && tagOf(text, 'command-name') !== undefined
|
|
136
|
+
return prompt === undefined || isPluginCommand ? [] : [{ ...base, type: Prompt, text: prompt, from }]
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
function isModNotesRow(row: Row): boolean {
|
package/src/session/session.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
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'
|
|
@@ -107,13 +107,13 @@ export type ListenerTime = { readonly deadline: number; readonly signal: AbortSi
|
|
|
107
107
|
export type Listener<Payload, Answer = void> = (payload: Payload, time: ListenerTime) => Answer | void | Promise<Answer | void>
|
|
108
108
|
|
|
109
109
|
export type OnMethod<Payload> = {
|
|
110
|
-
(listener: Listener<Payload, unknown>, options?: ReadOptions):
|
|
111
|
-
(matcher: Matcher<Payload>, listener: Listener<Payload, unknown>, options?: ReadOptions):
|
|
110
|
+
(listener: Listener<Payload, unknown>, options?: ReadOptions): Subscription
|
|
111
|
+
(matcher: Matcher<Payload>, listener: Listener<Payload, unknown>, options?: ReadOptions): Subscription
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
export type BeforeMethod<Payload, Answer> = {
|
|
115
|
-
(listener: Listener<Payload, Answer>, options?: AnswerOptions):
|
|
116
|
-
(matcher: Matcher<Payload>, listener: Listener<Payload, Answer>, options?: AnswerOptions):
|
|
115
|
+
(listener: Listener<Payload, Answer>, options?: AnswerOptions): Subscription
|
|
116
|
+
(matcher: Matcher<Payload>, listener: Listener<Payload, Answer>, options?: AnswerOptions): Subscription
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
export type OnMethods = { readonly [Name in EventName as `on${Name}`]: OnMethod<PayloadOf<Name>> }
|
|
@@ -326,7 +326,11 @@ export type ProcessOnMethods = { readonly [Name in ProcessName as `on${Name}`]:
|
|
|
326
326
|
|
|
327
327
|
export type ProcessBeforeMethods = { readonly [Name in keyof ProcessAnswer as `before${Name}`]: BeforeMethod<ProcessAsked[Name], ProcessAnswer[Name]> }
|
|
328
328
|
|
|
329
|
-
export type CurrentSession = Omit<Session, 'goal'> & ProcessOnMethods & ProcessBeforeMethods & {
|
|
329
|
+
export type CurrentSession = Omit<Session, 'goal'> & ProcessOnMethods & ProcessBeforeMethods & {
|
|
330
|
+
readonly goal: Goal & Pick<ProcessBeforeMethods, 'beforeSessionStart'>
|
|
331
|
+
scrollTo(message: Message, options?: { readonly block?: 'start' | 'center' | 'end' | 'nearest' }): Promise<void>
|
|
332
|
+
caption(toolCall: Message & { readonly type: typeof ToolCall }, text: string | undefined): void
|
|
333
|
+
}
|
|
330
334
|
|
|
331
335
|
export type SessionScope = {
|
|
332
336
|
readonly running?: boolean
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { EventResult, Timer } from 'claude-code'
|
|
2
2
|
import { isObject } from '../records.js'
|
|
3
3
|
import type { Claude } from '../runtime/claude.js'
|
|
4
|
+
import { cmodPluginName } from '../runtime/dependencies.js'
|
|
5
|
+
import { partMethods } from '../runtime/ui.js'
|
|
4
6
|
import { untagged } from '../session/context/turn/message/note.js'
|
|
5
7
|
import { elements } from './fake-elements.js'
|
|
6
8
|
import { fakeFiles } from './fake-files.js'
|
|
@@ -34,7 +36,6 @@ export type Shown = {
|
|
|
34
36
|
readonly prompts: string[]
|
|
35
37
|
readonly logs: string[]
|
|
36
38
|
readonly debug: string[]
|
|
37
|
-
readonly statuses: (string | undefined)[]
|
|
38
39
|
readonly openPanes: Set<string>
|
|
39
40
|
readonly commands: string[]
|
|
40
41
|
readonly tools: string[]
|
|
@@ -59,7 +60,7 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
59
60
|
const calls: TestCall[] = []
|
|
60
61
|
const fakes: Fakes = { process: {}, fs: {}, http: {}, settings: {}, config: {}, command: {}, ui: {}, session: {}, prompt: {}, model: {}, agent: {}, clock: {}, cmod: {}, tool: {} }
|
|
61
62
|
const emptyDisk = fakeFiles({})
|
|
62
|
-
const shown: Shown = { notes: [], prompts: [], logs: [], debug: [],
|
|
63
|
+
const shown: Shown = { notes: [], prompts: [], logs: [], debug: [], openPanes: new Set(), commands: [], tools: [], schedules: [] }
|
|
63
64
|
const unplacedPanes = new Set<string>()
|
|
64
65
|
let focusedPane: string | undefined
|
|
65
66
|
const store = new Map<string, unknown>()
|
|
@@ -79,6 +80,11 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
79
80
|
return async (...args: Args): Promise<Result> => run(...args)
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
const cmodTakesParts: Claude['cmod']['call'] = async (input) => {
|
|
84
|
+
if (input.to === cmodPluginName && input.method === partMethods.changed) return null
|
|
85
|
+
throw new Error(`cmod.call(${JSON.stringify(input)}) has no fake answer. Set fakes.cmod.call on the tested mod.`)
|
|
86
|
+
}
|
|
87
|
+
|
|
82
88
|
const appendNote: Claude['session']['append'] = async ({ message }) => {
|
|
83
89
|
shown.notes.push(untagged(message.content.map((block) => (typeof block['text'] === 'string' ? block['text'] : '')).join('')))
|
|
84
90
|
return { message, uuid: `note-${shown.notes.length}` }
|
|
@@ -107,9 +113,6 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
107
113
|
const claude: Claude = {
|
|
108
114
|
plugin,
|
|
109
115
|
ui: {
|
|
110
|
-
status: (text) => {
|
|
111
|
-
shown.statuses.push(text)
|
|
112
|
-
},
|
|
113
116
|
log: (text, options) => {
|
|
114
117
|
const lines = options?.to === 'debug' ? shown.debug : shown.logs
|
|
115
118
|
lines.push(text)
|
|
@@ -214,7 +217,7 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
|
|
|
214
217
|
dataHome: async () => undefined,
|
|
215
218
|
configHome: async () => undefined,
|
|
216
219
|
},
|
|
217
|
-
cmod: { call: rejected('cmod.call', () => fakes.cmod.call) },
|
|
220
|
+
cmod: { call: rejected('cmod.call', () => fakes.cmod.call ?? cmodTakesParts) },
|
|
218
221
|
}
|
|
219
222
|
|
|
220
223
|
const settle = async () => {
|
|
@@ -91,8 +91,8 @@ function boxRows({ props, children = [] }: Drawn, width: number): string[] {
|
|
|
91
91
|
const left = space('Left', 'X')
|
|
92
92
|
const direction = String(props['flexDirection'] ?? 'row')
|
|
93
93
|
const ordered = direction.endsWith('-reverse') ? [...children].reverse() : children
|
|
94
|
-
const inner = width - left - space('Right', 'X')
|
|
95
|
-
const rows = direction.startsWith('column') ? stacked(ordered, inner, size('rowGap') ?? size('gap') ?? 0) : sideBySide(ordered, inner, size('columnGap') ?? size('gap') ?? 0)
|
|
94
|
+
const inner = Math.min(width, size('width') ?? width) - left - space('Right', 'X')
|
|
95
|
+
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
96
|
return [...blankRows(space('Top', 'Y')), ...rows.map((row) => paddedTo(`${' '.repeat(left)}${row}`, size('minWidth') ?? 0)), ...blankRows(space('Bottom', 'Y'))]
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -101,7 +101,7 @@ function stacked(children: readonly RenderNode[], width: number, gap: number): s
|
|
|
101
101
|
return blocks.flatMap((block, index) => [...blankRows(index === 0 ? 0 : gap), ...block])
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
function sideBySide(children: readonly RenderNode[], width: number, gap: number): string[] {
|
|
104
|
+
function sideBySide(children: readonly RenderNode[], width: number, gap: number, isSpread = false): string[] {
|
|
105
105
|
const laid = children
|
|
106
106
|
.map((child) => ({ child, natural: rowsOf(child, Number.POSITIVE_INFINITY) }))
|
|
107
107
|
.filter(({ natural }) => natural.length > 0)
|
|
@@ -111,7 +111,10 @@ function sideBySide(children: readonly RenderNode[], width: number, gap: number)
|
|
|
111
111
|
const blocks = laid.map(({ child, natural, canShrink }) => (overflow <= 0 || !canShrink || total === 0 ? natural : rowsOf(child, widthOf(natural) - Math.ceil((overflow * widthOf(natural)) / total))))
|
|
112
112
|
const height = Math.max(0, ...blocks.map((block) => block.length))
|
|
113
113
|
const columns = blocks.map(widthOf)
|
|
114
|
-
|
|
114
|
+
const spaces = blocks.length - 1
|
|
115
|
+
const free = isSpread && Number.isFinite(width) ? Math.max(width - columns.reduce((sum, each) => sum + each, 0) - gap * spaces, 0) : 0
|
|
116
|
+
const gapAfter = (index: number) => gap + Math.floor(free / spaces) + (index < free % spaces ? 1 : 0)
|
|
117
|
+
return Array.from({ length: height }, (_, row) => blocks.map((block, index) => `${paddedTo(block[row] ?? '', index === spaces ? 0 : (columns[index] ?? 0))}${index === spaces ? '' : ' '.repeat(gapAfter(index))}`).join(''))
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
function minWidthOf(node: RenderNode): number {
|
|
@@ -14,7 +14,7 @@ import { ToolResult } from '../session/context/turn/message/tool-result.js'
|
|
|
14
14
|
import { createConversation, emptyConversation, searchLimit, type ContextData, type Conversation, type SessionChange, type SessionData, type TaskData, type TurnData, type TurnEnding } from '../session/conversation.js'
|
|
15
15
|
import type { Place } from '../runtime/before.js'
|
|
16
16
|
import type { Claude } from '../runtime/claude.js'
|
|
17
|
-
import { conversationMethods, everySession, isHeardBy, resultOf, type
|
|
17
|
+
import { conversationMethods, everySession, isHeardBy, resultOf, type Listened } from '../runtime/sessions.js'
|
|
18
18
|
import { receivedMatcher } from '../utils/matcher.js'
|
|
19
19
|
import { firstLine, messageOf } from '../utils/text.js'
|
|
20
20
|
|
|
@@ -43,7 +43,7 @@ type Kept = {
|
|
|
43
43
|
readonly tasks: Map<string, TaskData>
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
type Heard = { readonly event:
|
|
46
|
+
type Heard = { readonly event: Listened['event']; readonly matcher: unknown }
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
export type FakeSessions = {
|
|
@@ -56,6 +56,7 @@ export type FakeSessions = {
|
|
|
56
56
|
compact(): void
|
|
57
57
|
startTask(task: { readonly title: string; readonly agentType?: string; readonly model?: string; readonly openedBy?: string; readonly parent?: string }): string
|
|
58
58
|
start(id: string): void
|
|
59
|
+
lastTurn(): TurnData | undefined
|
|
59
60
|
readonly sent: { readonly session: string; readonly task?: string; readonly type: string; readonly text: string; readonly as?: string }[]
|
|
60
61
|
}
|
|
61
62
|
|
|
@@ -191,7 +192,7 @@ export function fakeSessions({ project, cwd, given, push, callTool }: { readonly
|
|
|
191
192
|
},
|
|
192
193
|
[conversationMethods.watchBefore](request, caller) {
|
|
193
194
|
const targets = watches.get(caller) ?? new Map<string, readonly string[]>()
|
|
194
|
-
targets.set(String(request['session']), (Array.isArray(request['subscriptions']) ? (request['subscriptions'] as readonly
|
|
195
|
+
targets.set(String(request['session']), (Array.isArray(request['subscriptions']) ? (request['subscriptions'] as readonly Listened[]) : []).map(({ event }) => event))
|
|
195
196
|
watches.set(caller, targets)
|
|
196
197
|
return null
|
|
197
198
|
},
|
|
@@ -223,7 +224,7 @@ export function fakeSessions({ project, cwd, given, push, callTool }: { readonly
|
|
|
223
224
|
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 }
|
|
224
225
|
},
|
|
225
226
|
[conversationMethods.subscribe](request, caller) {
|
|
226
|
-
const list = Array.isArray(request['subscriptions']) ? (request['subscriptions'] as readonly
|
|
227
|
+
const list = Array.isArray(request['subscriptions']) ? (request['subscriptions'] as readonly Listened[]) : []
|
|
227
228
|
const sessions = heard.get(caller) ?? new Map<string, readonly Heard[]>()
|
|
228
229
|
sessions.set(String(request['session']), list.map(({ event, matcher }) => ({ event, matcher: matcher === undefined ? undefined : receivedMatcher(matcher) })))
|
|
229
230
|
heard.set(caller, sessions)
|
|
@@ -272,6 +273,7 @@ export function fakeSessions({ project, cwd, given, push, callTool }: { readonly
|
|
|
272
273
|
session.data = { ...session.data, isRunning: true }
|
|
273
274
|
if (session.conversation.state.context === 0) record(session, session.conversation.startContext(now()))
|
|
274
275
|
},
|
|
276
|
+
lastTurn: () => [...currentSession().turns.values()].filter((turn) => turn.prompt.task === undefined).at(-1),
|
|
275
277
|
sent,
|
|
276
278
|
}
|
|
277
279
|
}
|