@cmodjs/core 0.3.0 → 1.5.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 +61 -10
- package/docs/dependencies.md +16 -14
- package/docs/hooks.md +64 -29
- package/docs/index.md +29 -25
- package/docs/install-steps.md +13 -9
- package/docs/jobs.md +59 -111
- package/docs/mod.md +227 -175
- package/docs/options.md +2 -2
- package/docs/permissions.md +27 -16
- package/docs/session.md +311 -0
- package/docs/state.md +11 -7
- package/docs/testing.md +68 -23
- package/docs/ui.md +116 -76
- package/jobs/check.d.ts.map +1 -1
- package/jobs/check.js +5 -2
- package/jobs/context.d.ts +7 -8
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +23 -73
- package/jobs/permissions/decide-permission.d.ts +5 -3
- package/jobs/permissions/decide-permission.d.ts.map +1 -1
- package/jobs/permissions/decide-permission.js +6 -4
- package/jobs/permissions/find-project-scope.d.ts +1 -1
- package/jobs/permissions/find-project-scope.d.ts.map +1 -1
- package/jobs/permissions/find-project-scope.js +8 -11
- package/jobs/permissions/match-target.d.ts +2 -2
- package/jobs/permissions/match-target.d.ts.map +1 -1
- package/jobs/permissions.d.ts +1 -1
- package/jobs/permissions.d.ts.map +1 -1
- package/jobs/permissions.js +6 -4
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +14 -4
- package/jobs/prompt.d.ts +4 -2
- package/jobs/prompt.d.ts.map +1 -1
- package/jobs/prompt.js +16 -15
- package/jobs/schedule.d.ts +2 -2
- package/jobs/schedule.d.ts.map +1 -1
- package/jobs/schedule.js +9 -9
- package/jobs/slash-command.d.ts +4 -4
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +14 -9
- package/jobs/status-line.d.ts +1 -11
- package/jobs/status-line.d.ts.map +1 -1
- package/jobs/status-line.js +8 -12
- package/jobs/tool.d.ts +2 -2
- package/jobs/tool.d.ts.map +1 -1
- package/jobs/tool.js +15 -9
- package/mod.d.ts +178 -75
- package/mod.d.ts.map +1 -1
- package/mod.js +127 -3
- package/options.d.ts +2 -2
- package/options.js +19 -19
- package/package.json +3 -3
- package/records.d.ts +13 -8
- package/records.d.ts.map +1 -1
- package/records.js +33 -17
- package/register.d.ts.map +1 -1
- package/register.js +34 -5
- package/runtime/announce.d.ts +18 -0
- package/runtime/announce.d.ts.map +1 -0
- package/runtime/announce.js +68 -0
- package/runtime/before.d.ts +32 -0
- package/runtime/before.d.ts.map +1 -0
- package/runtime/before.js +214 -0
- package/runtime/bound.d.ts +4 -0
- package/runtime/bound.d.ts.map +1 -0
- package/runtime/bound.js +9 -0
- package/runtime/claude.d.ts +4 -2
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +17 -0
- package/runtime/conversations.d.ts.map +1 -0
- package/runtime/conversations.js +563 -0
- package/runtime/deadline.d.ts +0 -1
- package/runtime/deadline.d.ts.map +1 -1
- package/runtime/deadline.js +1 -1
- package/runtime/dependencies.d.ts +2 -0
- package/runtime/dependencies.d.ts.map +1 -1
- package/runtime/dependencies.js +10 -1
- package/runtime/engine.d.ts +13 -0
- package/runtime/engine.d.ts.map +1 -0
- package/runtime/engine.js +10 -0
- package/runtime/grants.d.ts +4 -0
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +68 -31
- package/runtime/hooks.d.ts +5 -4
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +29 -9
- package/runtime/installer.d.ts +1 -1
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +17 -14
- package/runtime/lifecycle.d.ts +3 -1
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +414 -212
- package/runtime/metadata.d.ts +3 -1
- package/runtime/metadata.d.ts.map +1 -1
- package/runtime/metadata.js +25 -14
- package/runtime/options.d.ts +3 -1
- package/runtime/options.d.ts.map +1 -1
- package/runtime/options.js +4 -4
- package/runtime/programs.d.ts +8 -0
- package/runtime/programs.d.ts.map +1 -1
- package/runtime/programs.js +39 -1
- package/runtime/router.d.ts +2 -1
- package/runtime/router.d.ts.map +1 -1
- package/runtime/router.js +11 -3
- package/runtime/sessions.d.ts +58 -0
- package/runtime/sessions.d.ts.map +1 -0
- package/runtime/sessions.js +433 -0
- package/runtime/state.d.ts +6 -2
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +46 -13
- package/runtime/toaster.d.ts +24 -0
- package/runtime/toaster.d.ts.map +1 -0
- package/runtime/toaster.js +286 -0
- package/runtime/ui.d.ts +6 -17
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +62 -118
- package/session/context/context.d.ts +12 -0
- package/session/context/context.d.ts.map +1 -0
- package/session/context/context.js +2 -0
- package/session/context/turn/message/document.d.ts +7 -0
- package/session/context/turn/message/document.d.ts.map +1 -0
- package/session/context/turn/message/document.js +1 -0
- package/session/context/turn/message/message.d.ts +55 -0
- package/session/context/turn/message/message.d.ts.map +1 -0
- package/session/context/turn/message/message.js +178 -0
- package/session/context/turn/message/note.d.ts +8 -0
- package/session/context/turn/message/note.d.ts.map +1 -0
- package/session/context/turn/message/note.js +1 -0
- package/session/context/turn/message/notice.d.ts +6 -0
- package/session/context/turn/message/notice.d.ts.map +1 -0
- package/session/context/turn/message/notice.js +1 -0
- package/session/context/turn/message/prompt.d.ts +8 -0
- package/session/context/turn/message/prompt.d.ts.map +1 -0
- package/session/context/turn/message/prompt.js +1 -0
- package/session/context/turn/message/reply.d.ts +6 -0
- package/session/context/turn/message/reply.d.ts.map +1 -0
- package/session/context/turn/message/reply.js +1 -0
- package/session/context/turn/message/sender.d.ts +14 -0
- package/session/context/turn/message/sender.d.ts.map +1 -0
- package/session/context/turn/message/sender.js +30 -0
- package/session/context/turn/message/skill.d.ts +7 -0
- package/session/context/turn/message/skill.d.ts.map +1 -0
- package/session/context/turn/message/skill.js +1 -0
- package/session/context/turn/message/tool-call.d.ts +9 -0
- package/session/context/turn/message/tool-call.d.ts.map +1 -0
- package/session/context/turn/message/tool-call.js +1 -0
- package/session/context/turn/message/tool-result.d.ts +8 -0
- package/session/context/turn/message/tool-result.d.ts.map +1 -0
- package/session/context/turn/message/tool-result.js +1 -0
- package/session/context/turn/turn.d.ts +13 -0
- package/session/context/turn/turn.d.ts.map +1 -0
- package/session/context/turn/turn.js +2 -0
- package/session/conversation.d.ts +73 -0
- package/session/conversation.d.ts.map +1 -0
- package/session/conversation.js +146 -0
- package/session/goal/goal.d.ts +11 -0
- package/session/goal/goal.d.ts.map +1 -0
- package/session/goal/goal.js +1 -0
- package/session/goal/task.d.ts +16 -0
- package/session/goal/task.d.ts.map +1 -0
- package/session/goal/task.js +3 -0
- package/session/session.d.ts +158 -0
- package/session/session.d.ts.map +1 -0
- package/session/session.js +2 -0
- package/src/jobs/check.ts +7 -4
- package/src/jobs/context.ts +24 -82
- package/src/jobs/permissions/decide-permission.ts +10 -8
- package/src/jobs/permissions/find-project-scope.ts +8 -14
- package/src/jobs/permissions/match-target.ts +3 -3
- package/src/jobs/permissions.ts +8 -6
- package/src/jobs/program.ts +13 -4
- package/src/jobs/prompt.ts +21 -24
- package/src/jobs/schedule.ts +14 -14
- package/src/jobs/slash-command.ts +20 -14
- package/src/jobs/status-line.ts +9 -21
- package/src/jobs/tool.ts +17 -11
- package/src/mod.ts +252 -97
- package/src/options.ts +22 -22
- package/src/records.ts +40 -23
- package/src/register.ts +34 -5
- package/src/runtime/announce.ts +76 -0
- package/src/runtime/before.ts +200 -0
- package/src/runtime/bound.ts +12 -0
- package/src/runtime/claude.ts +7 -2
- package/src/runtime/conversations.ts +579 -0
- package/src/runtime/deadline.ts +2 -2
- package/src/runtime/dependencies.ts +13 -1
- package/src/runtime/engine.ts +23 -0
- package/src/runtime/grants.ts +69 -29
- package/src/runtime/hooks.ts +39 -14
- package/src/runtime/installer.ts +16 -14
- package/src/runtime/lifecycle.ts +413 -201
- package/src/runtime/metadata.ts +37 -14
- package/src/runtime/options.ts +19 -4
- package/src/runtime/programs.ts +40 -1
- package/src/runtime/router.ts +11 -4
- package/src/runtime/sessions.ts +447 -0
- package/src/runtime/state.ts +50 -14
- package/src/runtime/toaster.ts +334 -0
- package/src/runtime/ui.ts +65 -143
- package/src/session/context/context.ts +14 -0
- package/src/session/context/turn/message/document.ts +8 -0
- package/src/session/context/turn/message/message.ts +203 -0
- package/src/session/context/turn/message/note.ts +9 -0
- package/src/session/context/turn/message/notice.ts +7 -0
- package/src/session/context/turn/message/prompt.ts +9 -0
- package/src/session/context/turn/message/reply.ts +7 -0
- package/src/session/context/turn/message/sender.ts +38 -0
- package/src/session/context/turn/message/skill.ts +8 -0
- package/src/session/context/turn/message/tool-call.ts +10 -0
- package/src/session/context/turn/message/tool-result.ts +9 -0
- package/src/session/context/turn/turn.ts +15 -0
- package/src/session/conversation.ts +194 -0
- package/src/session/goal/goal.ts +10 -0
- package/src/session/goal/task.ts +19 -0
- package/src/session/session.ts +141 -0
- package/src/testing/fake-claude.ts +25 -17
- package/src/testing/fake-elements.ts +3 -2
- package/src/testing/fake-files.ts +1 -1
- package/src/testing/fake-sessions.ts +277 -0
- package/src/testing.ts +138 -35
- package/src/ui/components.ts +3 -3
- package/src/ui/elements.ts +22 -8
- package/src/ui/markdown.ts +22 -26
- package/src/ui/places.ts +49 -0
- package/src/utils/call-effects.ts +5 -5
- package/src/utils/changelog.ts +32 -0
- package/src/utils/disposable.ts +3 -0
- package/src/utils/matcher.ts +37 -0
- package/src/utils/metadata.ts +17 -15
- package/src/utils/parse-shell.ts +9 -8
- package/src/utils/paths.ts +21 -4
- package/src/utils/text.ts +4 -0
- package/testing/fake-claude.d.ts +5 -1
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +21 -12
- package/testing/fake-elements.js +4 -2
- package/testing/fake-files.js +1 -1
- package/testing/fake-sessions.d.ts +81 -0
- package/testing/fake-sessions.d.ts.map +1 -0
- package/testing/fake-sessions.js +249 -0
- package/testing.d.ts +34 -13
- package/testing.d.ts.map +1 -1
- package/testing.js +115 -23
- package/ui/components.d.ts +2 -3
- package/ui/components.d.ts.map +1 -1
- package/ui/components.js +3 -3
- package/ui/elements.d.ts.map +1 -1
- package/ui/elements.js +19 -9
- package/ui/markdown.d.ts +35 -33
- package/ui/markdown.d.ts.map +1 -1
- package/ui/markdown.js +14 -16
- package/ui/places.d.ts +43 -0
- package/ui/places.d.ts.map +1 -0
- package/ui/places.js +31 -0
- package/utils/call-effects.d.ts +2 -1
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +3 -3
- package/utils/changelog.d.ts +9 -0
- package/utils/changelog.d.ts.map +1 -0
- package/utils/changelog.js +29 -0
- package/utils/disposable.d.ts +4 -0
- package/utils/disposable.d.ts.map +1 -0
- package/utils/disposable.js +1 -0
- package/utils/matcher.d.ts +13 -0
- package/utils/matcher.d.ts.map +1 -0
- package/utils/matcher.js +32 -0
- package/utils/metadata.d.ts +3 -3
- package/utils/metadata.js +18 -14
- package/utils/parse-shell.js +9 -7
- package/utils/paths.d.ts +4 -1
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +19 -4
- package/utils/text.d.ts +1 -0
- package/utils/text.d.ts.map +1 -1
- package/utils/text.js +3 -0
- package/src/ui/slots.ts +0 -36
- package/ui/slots.d.ts +0 -208
- package/ui/slots.d.ts.map +0 -1
- package/ui/slots.js +0 -19
package/src/testing.ts
CHANGED
|
@@ -17,23 +17,39 @@ import type {
|
|
|
17
17
|
UiPressArgument,
|
|
18
18
|
UiSelectArgument,
|
|
19
19
|
} from 'claude-code'
|
|
20
|
-
import type {
|
|
21
|
-
import type {
|
|
20
|
+
import type { CommandReply } from './jobs/slash-command.js'
|
|
21
|
+
import type { Hook, ModDefinition } from './mod.js'
|
|
22
22
|
import type { Options, OptionValues } from './options.js'
|
|
23
23
|
import { readSteps, storeFolder, type Steps } from './records.js'
|
|
24
24
|
import type { Claude } from './runtime/claude.js'
|
|
25
|
-
import { answerCall, notInstalled } from './runtime/dependencies.js'
|
|
25
|
+
import { answerCall, cmodPluginName, notInstalled } from './runtime/dependencies.js'
|
|
26
|
+
import { foldAnswers, installBefore } from './runtime/before.js'
|
|
26
27
|
import type { RoutedEvent } from './runtime/hooks.js'
|
|
28
|
+
import { createRouter } from './runtime/router.js'
|
|
27
29
|
import { toolInputOf } from './runtime/tool-calls.js'
|
|
28
30
|
import { createLifecycle } from './runtime/lifecycle.js'
|
|
29
31
|
import { MissingOptions } from './runtime/options.js'
|
|
30
|
-
import {
|
|
32
|
+
import { conversationMethods } from './runtime/sessions.js'
|
|
33
|
+
import type { ShownToast } from './runtime/toaster.js'
|
|
34
|
+
import type { Message } from './session/context/turn/message/message.js'
|
|
35
|
+
import { Note } from './session/context/turn/message/note.js'
|
|
36
|
+
import { Prompt } from './session/context/turn/message/prompt.js'
|
|
37
|
+
import { Mod, Person } from './session/context/turn/message/sender.js'
|
|
38
|
+
import type { SessionChange } from './session/conversation.js'
|
|
39
|
+
import type { BeforeEvent } from './session/session.js'
|
|
40
|
+
import { calledAnswer, fakeClaude, type Fakes, type Shown as ShownByClaude, type TestCall } from './testing/fake-claude.js'
|
|
31
41
|
import { elements, findElement, rowsOf } from './testing/fake-elements.js'
|
|
32
42
|
import { fakeFiles } from './testing/fake-files.js'
|
|
33
|
-
import {
|
|
34
|
-
import
|
|
43
|
+
import { fakeSessions, type FakeSession, type FakeSessions, type MessageDraft } from './testing/fake-sessions.js'
|
|
44
|
+
import { listed, messageOf } from './utils/text.js'
|
|
45
|
+
import { componentOf, type ComponentPlace, type PlaceProps } from './ui/places.js'
|
|
35
46
|
|
|
36
|
-
export type { Fakes,
|
|
47
|
+
export type { Fakes, TestCall } from './testing/fake-claude.js'
|
|
48
|
+
export type { FakeSession, MessageDraft } from './testing/fake-sessions.js'
|
|
49
|
+
export type { RoutedEvent } from './runtime/hooks.js'
|
|
50
|
+
export type { ShownToast } from './runtime/toaster.js'
|
|
51
|
+
|
|
52
|
+
export type Shown = ShownByClaude & { readonly toasts: readonly ShownToast[]; readonly sent: FakeSessions['sent'] }
|
|
37
53
|
|
|
38
54
|
export type TestOptions<State extends object, Declared extends Options = Options> = {
|
|
39
55
|
readonly state?: { readonly [Lifetime in keyof State]?: Partial<State[Lifetime]> }
|
|
@@ -45,23 +61,28 @@ export type TestOptions<State extends object, Declared extends Options = Options
|
|
|
45
61
|
readonly dependencies?: { readonly [Name in keyof CmodDependencies]?: CmodDependencies[Name] }
|
|
46
62
|
readonly files?: Readonly<Record<string, string>>
|
|
47
63
|
readonly links?: Readonly<Record<string, string>>
|
|
64
|
+
readonly sessions?: readonly FakeSession[]
|
|
48
65
|
}
|
|
49
66
|
|
|
50
67
|
type FilledField = 'session_id' | 'transcript_path' | 'cwd' | 'hook_event_name' | 'tool_use_id'
|
|
51
68
|
|
|
52
|
-
export type TestInput<
|
|
69
|
+
export type TestInput<H extends Hook> = Omit<ClassicHookInputs[H], FilledField> & Partial<Pick<ClassicHookInputs[H], FilledField & keyof ClassicHookInputs[H]>>
|
|
53
70
|
|
|
54
71
|
export type TestedMod<State extends object> = {
|
|
55
72
|
start(): Promise<void>
|
|
56
|
-
fire<
|
|
73
|
+
fire<H extends Hook>(hook: H, input: TestInput<H>, below?: EventResult<`classic.${H}`>): Promise<EventResult<`classic.${H}`>>
|
|
57
74
|
fire<N extends RoutedEvent>(event: N, input: Args<N>, below?: EventResult<N>): Promise<EventResult<N>>
|
|
58
75
|
settle(): Promise<void>
|
|
76
|
+
add(message: MessageDraft, options?: { readonly task?: string }): Promise<Message[]>
|
|
77
|
+
endTurn(): Promise<void>
|
|
78
|
+
compact(): Promise<void>
|
|
79
|
+
startTask(task: { readonly title: string; readonly agentType?: string; readonly model?: string; readonly openedBy?: string; readonly parent?: string }): Promise<string>
|
|
59
80
|
lines(paneId: string): Promise<string[]>
|
|
60
|
-
lines<
|
|
61
|
-
type(line: string): Promise<
|
|
81
|
+
lines<P extends ComponentPlace>(place: P, props: Omit<PlaceProps<P>, 'Default'>, requestId?: string): Promise<string[]>
|
|
82
|
+
type(line: string): Promise<CommandReply>
|
|
62
83
|
callTool(name: string, input: Record<string, unknown>): Promise<EventResult<'tool.call'>>
|
|
63
84
|
press(paneId: string, key: string, link?: string): Promise<void>
|
|
64
|
-
input(paneId: string, key: string, text: string,
|
|
85
|
+
input(paneId: string, key: string, text: string, type?: UiInputArgument['kind']): Promise<void>
|
|
65
86
|
select(paneId: string, key: string, value: string): Promise<void>
|
|
66
87
|
moveTo(projectRoot: string, cwd?: string): Promise<void>
|
|
67
88
|
readonly state: Readonly<State>
|
|
@@ -76,7 +97,7 @@ const defaultRows = 24
|
|
|
76
97
|
|
|
77
98
|
const presentation: CommandPresentation = { isFullscreen: false, columns: defaultColumns }
|
|
78
99
|
|
|
79
|
-
const toolUseEvents: readonly string[] = ['PreToolUse', 'PostToolUse', 'PostToolUseFailure', 'PermissionDenied'] satisfies readonly
|
|
100
|
+
const toolUseEvents: readonly string[] = ['PreToolUse', 'PostToolUse', 'PostToolUseFailure', 'PermissionDenied'] satisfies readonly Hook[]
|
|
80
101
|
|
|
81
102
|
export function testMod<State extends object, Declared extends Options = Options>(definition: ModDefinition<State, string, Declared>, options: TestOptions<State, Declared> = {}): TestedMod<State> {
|
|
82
103
|
installJsx()
|
|
@@ -93,14 +114,38 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
93
114
|
fake.claude.session.id = async () => sessionId
|
|
94
115
|
fake.claude.session.root = async () => projectRoot
|
|
95
116
|
fake.claude.session.cwd = async () => cwd
|
|
96
|
-
|
|
117
|
+
const conversations = fakeSessions({
|
|
118
|
+
callTool: (args: Parameters<Claude["tool"]["call"]>[0]) => fake.claude.tool.call(args),
|
|
119
|
+
project: () => projectRoot,
|
|
120
|
+
given: options.sessions ?? [],
|
|
121
|
+
push: (mod, session, change) => {
|
|
122
|
+
if (mod !== name) return
|
|
123
|
+
const pushed = { to: mod, method: conversationMethods.changed, input: { session, change }, caller: cmodPluginName } as unknown as Frozen<Args<'cmod.call'>>
|
|
124
|
+
void lifecycle.route('cmod.call', pushed, async () => ({ deny: notInstalled(mod) }))
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
conversations.start(sessionId)
|
|
128
|
+
fake.fakes.cmod.call = fakeDependencies(name, options.dependencies ?? {}, conversations)
|
|
129
|
+
const appendRow = fake.claude.session.append
|
|
130
|
+
fake.claude.session.append = async (args) => {
|
|
131
|
+
const added = await appendRow(args)
|
|
132
|
+
if (added.deny === undefined) conversations.add({ type: Note, text: args.message.content.map((block) => (typeof block['text'] === 'string' ? block['text'] : '')).join(''), from: { type: Mod, name } })
|
|
133
|
+
return added
|
|
134
|
+
}
|
|
135
|
+
const submitRow = fake.claude.prompt.submit
|
|
136
|
+
fake.claude.prompt.submit = async (args) => {
|
|
137
|
+
const submitted = await submitRow(args)
|
|
138
|
+
conversations.add({ type: Prompt, text: args.text, from: { type: Mod, name, ...(args.asUser === true ? { as: Person } : {}) } })
|
|
139
|
+
return submitted
|
|
140
|
+
}
|
|
97
141
|
const firedAgents = new Map<string, string>()
|
|
98
142
|
const listAgents = fake.claude.agent.list
|
|
99
143
|
fake.claude.agent.list = async () => {
|
|
100
144
|
if (fake.fakes.agent.list !== undefined || firedAgents.size === 0) return listAgents()
|
|
101
145
|
return [...firedAgents].map(([id, type]) => ({ id, type, description: '', status: 'running' as const }))
|
|
102
146
|
}
|
|
103
|
-
|
|
147
|
+
const files = fakeFiles(options.files ?? {}, options.links)
|
|
148
|
+
Object.assign(fake.fakes.fs, files)
|
|
104
149
|
const starting = options.state as Record<string, object> | undefined
|
|
105
150
|
const declared = Object.entries(definition.state ?? {}) as [string, object][]
|
|
106
151
|
const state = Object.fromEntries(declared.map(([lifetime, values]) => [lifetime, { ...values, ...starting?.[lifetime] }])) as State
|
|
@@ -122,6 +167,7 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
122
167
|
}
|
|
123
168
|
})
|
|
124
169
|
await started
|
|
170
|
+
await fake.settle()
|
|
125
171
|
const { failure } = lifecycle
|
|
126
172
|
if (failure instanceof MissingOptions) {
|
|
127
173
|
const given = failure.keys.map((key) => `${key}: …`).join(', ')
|
|
@@ -130,9 +176,24 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
130
176
|
if (lifecycle.phase !== 'active') throw failure ?? new Error(`${name} did not start: the lifecycle is ${lifecycle.phase}.`)
|
|
131
177
|
}
|
|
132
178
|
|
|
179
|
+
const cmodPlugin = createRouter()
|
|
180
|
+
const askMod = async (event: BeforeEvent, change: SessionChange) => {
|
|
181
|
+
const asked = { to: name, method: conversationMethods.before, input: { session: sessionId, event, change }, caller: cmodPluginName } as unknown as Frozen<Args<'cmod.call'>>
|
|
182
|
+
return calledAnswer(cmodPluginName, await lifecycle.route('cmod.call', asked, async () => ({ deny: notInstalled(name) })))
|
|
183
|
+
}
|
|
184
|
+
installBefore({
|
|
185
|
+
claude: fake.claude,
|
|
186
|
+
router: cmodPlugin,
|
|
187
|
+
hooks: {
|
|
188
|
+
place: async () => conversations.place(),
|
|
189
|
+
wants: async (event) => conversations.wants(name, event),
|
|
190
|
+
decide: (event, change) => foldAnswers(event, change, [(latest) => askMod(event, latest)], (error) => fake.claude.ui.log(`cmod: passed the ${event} on without ${name}'s answer: ${messageOf(error)}`, { to: 'debug' })),
|
|
191
|
+
},
|
|
192
|
+
})
|
|
193
|
+
|
|
133
194
|
const route = async <N extends RoutedEvent>(event: N, input: unknown, below: unknown): Promise<EventResult<N>> => {
|
|
134
195
|
await start()
|
|
135
|
-
return
|
|
196
|
+
return cmodPlugin.dispatch(event, input as Frozen<Args<N>>, (passed) => lifecycle.route(event, passed, async (e) => (below ?? claudeCodeAnswer(event, e)) as EventResult<N>))
|
|
136
197
|
}
|
|
137
198
|
|
|
138
199
|
const drawPane = async (paneId: string) => {
|
|
@@ -166,14 +227,19 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
166
227
|
|
|
167
228
|
const fire = ((event: string, input: object, below?: unknown) => {
|
|
168
229
|
if (event.includes('.')) return route(event as RoutedEvent, input, below)
|
|
169
|
-
if (event === 'SessionStart')
|
|
230
|
+
if (event === 'SessionStart') {
|
|
231
|
+
sessionId = (input as { session_id?: string }).session_id ?? sessionId
|
|
232
|
+
conversations.start(sessionId)
|
|
233
|
+
}
|
|
170
234
|
if (event === 'UserPromptSubmit') {
|
|
171
235
|
cwd = cwdAfterPrompt ?? cwd
|
|
172
236
|
cwdAfterPrompt = undefined
|
|
173
237
|
}
|
|
174
238
|
const toolUse = toolUseEvents.includes(event) ? { tool_use_id: `toolu_${(toolUses += 1)}` } : {}
|
|
175
|
-
const
|
|
176
|
-
|
|
239
|
+
const transcript = `/test/transcripts/${sessionId}.jsonl`
|
|
240
|
+
const filled = { session_id: sessionId, transcript_path: transcript, cwd, hook_event_name: event, ...toolUse, ...input }
|
|
241
|
+
if (event === 'SessionStart' && !('transcript_path' in input)) return files.write(transcript, '').then(() => route('classic.SessionStart', filled, below))
|
|
242
|
+
if (event !== 'PreToolUse') return route(`classic.${event as Exclude<Hook, 'PreToolUse'>}`, filled, below)
|
|
177
243
|
return firePreToolUse(filled as ClassicHookInputs['PreToolUse'], below as { deny?: string } | undefined)
|
|
178
244
|
}) as TestedMod<State>['fire']
|
|
179
245
|
|
|
@@ -187,7 +253,7 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
187
253
|
const answer = await lifecycle.route('tool.call', envelope, async (e) => {
|
|
188
254
|
reached = e
|
|
189
255
|
check = await lifecycle.route('tool.check', { tool: e.tool, input: toolInputOf(e), tool_use_id: e.tool_use_id, ...(e.agentId === undefined ? {} : { agentId: e.agentId }) }, async () => asks)
|
|
190
|
-
return
|
|
256
|
+
return check.decision === 'deny' ? { deny: check.reason ?? '' } : { result: '' }
|
|
191
257
|
})
|
|
192
258
|
const updatedInput = toolInputOf(reached)
|
|
193
259
|
const decided = check === asks ? {} : check.decision === 'allow' ? { allow: true } : check.decision === 'ask' ? { ask: check.reason ?? '' } : {}
|
|
@@ -198,14 +264,37 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
198
264
|
} as EventResult<'classic.PreToolUse'>
|
|
199
265
|
}
|
|
200
266
|
|
|
267
|
+
const settled = async <Value>(value: Value) => {
|
|
268
|
+
await fake.settle()
|
|
269
|
+
return value
|
|
270
|
+
}
|
|
271
|
+
|
|
201
272
|
return {
|
|
202
273
|
start,
|
|
203
274
|
fire,
|
|
204
275
|
settle: fake.settle,
|
|
205
|
-
|
|
206
|
-
|
|
276
|
+
add: async (message, added) => {
|
|
277
|
+
await start()
|
|
278
|
+
return settled(conversations.add(message, added?.task))
|
|
279
|
+
},
|
|
280
|
+
endTurn: async () => {
|
|
281
|
+
await start()
|
|
282
|
+
await settled(conversations.endTurn())
|
|
283
|
+
},
|
|
284
|
+
compact: async () => {
|
|
285
|
+
await start()
|
|
286
|
+
await settled(conversations.compact())
|
|
287
|
+
},
|
|
288
|
+
startTask: async (task) => {
|
|
289
|
+
await start()
|
|
290
|
+
return settled(conversations.startTask(task))
|
|
291
|
+
},
|
|
292
|
+
lines: async (target: string, props?: object, requestId?: string) => {
|
|
293
|
+
const component = componentOf(target as ComponentPlace)
|
|
294
|
+
if (props !== undefined && component === undefined) throw new Error(`lines takes a pane id, or a place render draws and its props, and ${target} is neither a place such as ToolCall nor called without props.`)
|
|
295
|
+
const { drawing, columns } = props === undefined || component === undefined ? await drawPane(target) : await drawSlot(component, props, requestId)
|
|
207
296
|
return rowsOf(drawing, columns).map((row) => row.trimEnd())
|
|
208
|
-
}
|
|
297
|
+
},
|
|
209
298
|
async type(line) {
|
|
210
299
|
const typed = /^\/(\S+)\s*(.*)$/s.exec(line)
|
|
211
300
|
if (typed === null) throw new Error(`type takes a slash command the way the user types it, such as tested.type('/help'), and "${line}" does not start with /.`)
|
|
@@ -230,16 +319,16 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
230
319
|
const props = await drawnElement(paneId, link === undefined ? 'Button' : 'Markdown', key)
|
|
231
320
|
if (link !== undefined && props['onLinkPress'] === undefined) throw new Error(`The Markdown "${key}" in the pane "${paneId}" of ${name} has no onLinkPress, so Claude Code opens its links itself.`)
|
|
232
321
|
const pressed: UiPressArgument = { ...eventIn(paneId, String(props['key'])), ...(link === undefined ? {} : { link: { href: link } }) }
|
|
233
|
-
await lifecycle.route('ui.press', pressed
|
|
322
|
+
await lifecycle.route('ui.press', pressed, async (e) => {
|
|
234
323
|
if (e.link === undefined) await (props['onPress'] as (e: UiPressArgument) => unknown)(e)
|
|
235
324
|
else await (props['onLinkPress'] as (link: PressedLink, e: UiPressArgument) => unknown)(e.link, e)
|
|
236
325
|
return { element: e.element }
|
|
237
326
|
})
|
|
238
327
|
},
|
|
239
|
-
async input(paneId, key, text,
|
|
328
|
+
async input(paneId, key, text, type = 'submit') {
|
|
240
329
|
const props = await drawnElement(paneId, 'Input', key)
|
|
241
|
-
const typed: UiInputArgument = { ...eventIn(paneId, key), kind, value: text }
|
|
242
|
-
await (props[
|
|
330
|
+
const typed: UiInputArgument = { ...eventIn(paneId, key), kind: type, value: text }
|
|
331
|
+
await (props[type === 'submit' ? 'onSubmit' : 'onInput'] as ((value: string, e: UiInputArgument) => unknown) | undefined)?.(text, typed)
|
|
243
332
|
},
|
|
244
333
|
async select(paneId, key, value) {
|
|
245
334
|
const props = await drawnElement(paneId, 'Select', key)
|
|
@@ -269,21 +358,32 @@ export function testMod<State extends object, Declared extends Options = Options
|
|
|
269
358
|
},
|
|
270
359
|
calls: fake.calls,
|
|
271
360
|
fakes: fake.fakes,
|
|
272
|
-
shown:
|
|
361
|
+
shown: {
|
|
362
|
+
...fake.shown,
|
|
363
|
+
get toasts() {
|
|
364
|
+
return lifecycle.toasts
|
|
365
|
+
},
|
|
366
|
+
sent: conversations.sent,
|
|
367
|
+
},
|
|
273
368
|
}
|
|
274
369
|
}
|
|
275
370
|
|
|
276
|
-
function fakeDependencies(dependencies: NonNullable<TestOptions<object>['dependencies']
|
|
277
|
-
const
|
|
371
|
+
function fakeDependencies(caller: string, dependencies: NonNullable<TestOptions<object>['dependencies']>, conversations: FakeSessions): Claude['cmod']['call'] {
|
|
372
|
+
const cmod = { forkParent: () => null, updateSettings: () => null }
|
|
373
|
+
const apis = { [cmodPluginName]: cmod, ...dependencies } as Readonly<Record<string, Parameters<typeof answerCall>[1] | undefined>>
|
|
278
374
|
return async (call) => {
|
|
375
|
+
const served = call.to === cmodPluginName ? conversations.answer(caller, call.method, call.input) : undefined
|
|
376
|
+
if (served !== undefined) return calledAnswer(caller, served as EventResult<'cmod.call'>)
|
|
279
377
|
const api = apis[call.to]
|
|
280
|
-
|
|
281
|
-
if (answer.deny !== undefined) throw new Error(answer.deny)
|
|
282
|
-
return answer.value
|
|
378
|
+
return calledAnswer(caller, api === undefined ? { deny: notInstalled(call.to) } : await answerCall(call.to, api, call))
|
|
283
379
|
}
|
|
284
380
|
}
|
|
285
381
|
|
|
286
382
|
function claudeCodeAnswer(event: RoutedEvent, e: unknown): unknown {
|
|
383
|
+
if (event === 'agent.spawn') {
|
|
384
|
+
const { model, parentModel } = e as Args<'agent.spawn'>
|
|
385
|
+
return { agentId: 'agent-1', model: model ?? parentModel }
|
|
386
|
+
}
|
|
287
387
|
if (event !== 'prompt.submit') return {}
|
|
288
388
|
const { text, context, origin } = e as Args<'prompt.submit'>
|
|
289
389
|
return { text, ...(context === undefined ? {} : { context }), origin }
|
|
@@ -294,7 +394,7 @@ function plainOutput({ component, props }: Args<'ui.render'>): RenderElement {
|
|
|
294
394
|
return elements.Box({ flexDirection: 'column', children: [elements.Text({ children: component }), elements.Box({ flexDirection: 'column', paddingLeft: 2, children: rows })] })
|
|
295
395
|
}
|
|
296
396
|
|
|
297
|
-
function replyOf({ text, context }: CommandRunResult):
|
|
397
|
+
function replyOf({ text, context }: CommandRunResult): CommandReply {
|
|
298
398
|
return { ...(text === undefined ? {} : { text }), ...(context === undefined ? {} : { context: context.join('\n') }) }
|
|
299
399
|
}
|
|
300
400
|
|
|
@@ -302,7 +402,10 @@ async function modSteps(): Promise<Steps> {
|
|
|
302
402
|
const manifest = '../../../package.json'
|
|
303
403
|
const loaded: unknown = await import(manifest, { with: { type: 'json' } }).then(
|
|
304
404
|
(module: { default: unknown }) => module.default,
|
|
305
|
-
() =>
|
|
405
|
+
(error: { readonly code?: unknown }) => {
|
|
406
|
+
if (error.code === 'ERR_MODULE_NOT_FOUND') return undefined
|
|
407
|
+
throw error
|
|
408
|
+
},
|
|
306
409
|
)
|
|
307
410
|
return readSteps(loaded) ?? {}
|
|
308
411
|
}
|
package/src/ui/components.ts
CHANGED
|
@@ -49,9 +49,9 @@ export function Pagination({ page, pages, onPage }: { readonly page: number; rea
|
|
|
49
49
|
})
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export function
|
|
53
|
-
const filled =
|
|
54
|
-
return Text({ children: [Text({ color: 'claude', children: '█'.repeat(filled) }), Text({ color: 'subtle', children: '░'.repeat(width - filled) })
|
|
52
|
+
export function Progress({ value, width = 24 }: { readonly value: number; readonly width?: number }): RenderElement {
|
|
53
|
+
const filled = Math.round((Math.min(Math.max(value, 0), 100) / 100) * width)
|
|
54
|
+
return Text({ children: [Text({ color: 'claude', children: '█'.repeat(filled) }), Text({ color: 'subtle', children: '░'.repeat(width - filled) })] })
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
const cardColumns = 36
|
package/src/ui/elements.ts
CHANGED
|
@@ -6,25 +6,39 @@ type CompletedTable = Record<ElementName, (props: object) => RenderElement>
|
|
|
6
6
|
|
|
7
7
|
type Drawing = { readonly table: CompletedTable; readonly markdown: ((props: MarkdownProps) => RenderElement) | undefined }
|
|
8
8
|
|
|
9
|
+
const unbuilt = Symbol('an element built outside a render')
|
|
10
|
+
|
|
11
|
+
type Unbuilt = { readonly [unbuilt]: { readonly name: ElementName; readonly props: object } }
|
|
12
|
+
|
|
9
13
|
let drawing: Drawing | undefined
|
|
10
14
|
|
|
11
15
|
export function drawWith<Drawn>(table: ElementTable, draw: () => Drawn, markdown?: (props: MarkdownProps) => RenderElement): Drawn {
|
|
12
16
|
const outer = drawing
|
|
13
|
-
|
|
17
|
+
const current = { table: table as CompletedTable, markdown }
|
|
18
|
+
drawing = current
|
|
14
19
|
try {
|
|
15
|
-
return draw()
|
|
20
|
+
return built(current, draw()) as Drawn
|
|
16
21
|
} finally {
|
|
17
22
|
drawing = outer
|
|
18
23
|
}
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
function element<Name extends keyof Terminal & ElementName>(name: Name): Terminal[Name] {
|
|
22
|
-
const construct = (props: object) => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
const construct = (props: object) => (drawing === undefined ? ({ [unbuilt]: { name, props } } satisfies Unbuilt as unknown as RenderElement) : build(drawing, name, props))
|
|
28
|
+
return construct
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function build(current: Drawing, name: ElementName, props: object): RenderElement {
|
|
32
|
+
const ready = 'children' in props ? { ...props, children: built(current, props.children) } : props
|
|
33
|
+
if (name === 'Markdown' && current.markdown !== undefined) return current.markdown(ready as MarkdownProps)
|
|
34
|
+
return current.table[name](ready)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function built(current: Drawing, value: unknown): unknown {
|
|
38
|
+
if (Array.isArray(value)) return value.map((child: unknown) => built(current, child))
|
|
39
|
+
if (typeof value !== 'object' || value === null || !(unbuilt in value)) return value
|
|
40
|
+
const { name, props } = (value as Unbuilt)[unbuilt]
|
|
41
|
+
return build(current, name, props)
|
|
28
42
|
}
|
|
29
43
|
|
|
30
44
|
export const Box = element('Box')
|
package/src/ui/markdown.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import type { Nodes, RootContent } from 'mdast'
|
|
2
2
|
import { fromMarkdown, gfm, gfmFromMarkdown, toString } from '../vendor-markdown.js'
|
|
3
|
-
import type {
|
|
3
|
+
import type { PlaceProps } from './places.js'
|
|
4
4
|
|
|
5
5
|
type Block = { readonly text: string; readonly source: string }
|
|
6
6
|
|
|
7
|
-
type MarkdownSlot<Props extends object = object> = Slot<Props, { readonly source?: string }, { readonly markdown: MarkdownKind }>
|
|
8
|
-
|
|
9
7
|
type BlockFields = {
|
|
10
8
|
heading: { readonly depth: 1 | 2 | 3 | 4 | 5 | 6 }
|
|
11
9
|
code: { readonly lang: string | undefined; readonly meta: string | undefined; readonly value: string }
|
|
@@ -14,18 +12,31 @@ type BlockFields = {
|
|
|
14
12
|
html: { readonly value: string }
|
|
15
13
|
}
|
|
16
14
|
|
|
17
|
-
type BlockProps<Type extends string> = Type extends keyof BlockFields ? Block & BlockFields[Type] : Block
|
|
15
|
+
export type BlockProps<Type extends string> = Type extends keyof BlockFields ? Block & BlockFields[Type] : Block
|
|
18
16
|
|
|
19
17
|
export type MarkdownPiece<Render> = { readonly text: string } | { readonly render: Render; readonly props: Block }
|
|
20
18
|
|
|
21
19
|
export type MarkdownReader = <Render>(text: string, renderOf: (type: string) => Render | undefined, replyId?: string) => readonly MarkdownPiece<Render>[]
|
|
22
20
|
|
|
23
|
-
export type
|
|
21
|
+
export type MarkdownPlace<Type extends string = string> = {
|
|
24
22
|
readonly name: string
|
|
25
|
-
readonly
|
|
26
|
-
|
|
23
|
+
readonly markdown: Type
|
|
24
|
+
createReader(): MarkdownReader
|
|
27
25
|
}
|
|
28
26
|
|
|
27
|
+
function markdownPlace<const Type extends string>(name: string, markdown: Type): MarkdownPlace<Type> {
|
|
28
|
+
return { name, markdown, createReader }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const Heading = markdownPlace('Heading', 'heading')
|
|
32
|
+
export const Paragraph = markdownPlace('Paragraph', 'paragraph')
|
|
33
|
+
export const CodeBlock = markdownPlace('CodeBlock', 'code')
|
|
34
|
+
export const BlockQuote = markdownPlace('BlockQuote', 'blockquote')
|
|
35
|
+
export const List = markdownPlace('List', 'list')
|
|
36
|
+
export const Table = markdownPlace('Table', 'table')
|
|
37
|
+
export const ThematicBreak = markdownPlace('ThematicBreak', 'thematicBreak')
|
|
38
|
+
export const HtmlBlock = markdownPlace('HtmlBlock', 'html')
|
|
39
|
+
|
|
29
40
|
type Parsed = {
|
|
30
41
|
readonly type: string
|
|
31
42
|
readonly start: number
|
|
@@ -52,7 +63,7 @@ const blankLine = /\n[^\S\n]*\n/
|
|
|
52
63
|
|
|
53
64
|
const fence = /^ {0,3}(`{3}|~{3})/
|
|
54
65
|
|
|
55
|
-
function createReader(): MarkdownReader {
|
|
66
|
+
export function createReader(): MarkdownReader {
|
|
56
67
|
const replies: Reply[] = []
|
|
57
68
|
|
|
58
69
|
const remember = (reply: Reply) => {
|
|
@@ -152,22 +163,7 @@ function plainText(node: Nodes): string {
|
|
|
152
163
|
return node.children.map(plainText).join(node.type === 'tableRow' ? '\t' : '\n')
|
|
153
164
|
}
|
|
154
165
|
|
|
155
|
-
function
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
export const markdownSlots = {
|
|
160
|
-
Heading: markdownSlot('Heading', 'heading'),
|
|
161
|
-
Paragraph: markdownSlot('Paragraph', 'paragraph'),
|
|
162
|
-
CodeBlock: markdownSlot('CodeBlock', 'code'),
|
|
163
|
-
BlockQuote: markdownSlot('BlockQuote', 'blockquote'),
|
|
164
|
-
List: markdownSlot('List', 'list'),
|
|
165
|
-
Table: markdownSlot('Table', 'table'),
|
|
166
|
-
ThematicBreak: markdownSlot('ThematicBreak', 'thematicBreak'),
|
|
167
|
-
HtmlBlock: markdownSlot('HtmlBlock', 'html'),
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export function markdownBlocks<S extends MarkdownSlot>(text: string, slot: S): Omit<SlotProps<S>, 'Default'>[] {
|
|
171
|
-
const blocks = parsed(text, 0).filter((block) => block.type === slot.markdown.type)
|
|
172
|
-
return blocks.map((block) => block.props) as unknown as Omit<SlotProps<S>, 'Default'>[]
|
|
166
|
+
export function markdownBlocks<P extends MarkdownPlace>(text: string, place: P): Omit<PlaceProps<P>, 'Default'>[] {
|
|
167
|
+
const blocks = parsed(text, 0).filter((block) => block.type === place.markdown)
|
|
168
|
+
return blocks.map((block) => block.props) as unknown as Omit<PlaceProps<P>, 'Default'>[]
|
|
173
169
|
}
|
package/src/ui/places.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Frozen, RenderComponent, RenderElement, RenderPropsOf } from 'claude-code'
|
|
2
|
+
import type { BlockProps, MarkdownPlace } from './markdown.js'
|
|
3
|
+
|
|
4
|
+
export const ToolGroup = 'toolGroup'
|
|
5
|
+
export const ToolProgress = 'toolProgress'
|
|
6
|
+
export const CommandOutput = 'commandOutput'
|
|
7
|
+
export const AskUserQuestion = 'askUserQuestion'
|
|
8
|
+
export const Spinner = 'spinner'
|
|
9
|
+
export const TurnDuration = 'turnDuration'
|
|
10
|
+
export const SessionMode = 'sessionMode'
|
|
11
|
+
export const PromptHint = 'promptHint'
|
|
12
|
+
export const AbovePrompt = 'abovePrompt'
|
|
13
|
+
|
|
14
|
+
const components = {
|
|
15
|
+
prompt: 'UserMessage',
|
|
16
|
+
reply: 'AssistantMessage',
|
|
17
|
+
toolCall: 'ToolUse',
|
|
18
|
+
toolResult: 'ToolResult',
|
|
19
|
+
notice: 'InfoNotice',
|
|
20
|
+
[ToolGroup]: 'ToolGroup',
|
|
21
|
+
[ToolProgress]: 'ToolProgress',
|
|
22
|
+
[CommandOutput]: 'CommandOutput',
|
|
23
|
+
[AskUserQuestion]: 'AskUserQuestion',
|
|
24
|
+
[Spinner]: 'Spinner',
|
|
25
|
+
[TurnDuration]: 'TurnDuration',
|
|
26
|
+
[SessionMode]: 'SessionMode',
|
|
27
|
+
[PromptHint]: 'PromptHint',
|
|
28
|
+
[AbovePrompt]: 'AbovePrompt',
|
|
29
|
+
} as const satisfies Readonly<Record<string, Exclude<RenderComponent, 'Pane'>>>
|
|
30
|
+
|
|
31
|
+
export type ComponentPlace = keyof typeof components
|
|
32
|
+
|
|
33
|
+
export type Place = ComponentPlace | MarkdownPlace
|
|
34
|
+
|
|
35
|
+
type ComponentProps = { readonly [P in ComponentPlace]: RenderPropsOf[(typeof components)[P]] }
|
|
36
|
+
|
|
37
|
+
type PropsOf<P extends Place> = P extends ComponentPlace ? ComponentProps[P] : P extends MarkdownPlace<infer Type> ? BlockProps<Type> : never
|
|
38
|
+
|
|
39
|
+
type DefaultOf<P extends Place> = P extends ComponentPlace ? Partial<Omit<ComponentProps[P], 'onScreen'>> : { readonly source?: string }
|
|
40
|
+
|
|
41
|
+
export type PlaceProps<P extends Place> = Frozen<PropsOf<P>> & { readonly Default: (props: Frozen<DefaultOf<P>>) => RenderElement }
|
|
42
|
+
|
|
43
|
+
export function componentOf(place: Place): Exclude<RenderComponent, 'Pane'> | undefined {
|
|
44
|
+
return typeof place === 'string' && Object.hasOwn(components, place) ? components[place] : undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function nameOf(place: Place): string {
|
|
48
|
+
return typeof place === 'string' ? `${place.charAt(0).toUpperCase()}${place.slice(1)}` : place.name
|
|
49
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { parseShell, type ShellCommand } from './parse-shell.js'
|
|
2
|
-
import { expandHome, type FileSystem } from './paths.js'
|
|
2
|
+
import { expandHome, ifMissing, type FileSystem } from './paths.js'
|
|
3
3
|
import { resolve } from '../vendor.js'
|
|
4
4
|
|
|
5
5
|
export type CallBase = { tool: string; input: Readonly<Record<string, unknown>>; agentId?: string; agentType?: string }
|
|
6
6
|
export type CommandCall = CallBase & { commands: [string, ...string[]][]; isFullyParsed: boolean }
|
|
7
7
|
export type FileCall = CallBase & { path: string; content?: string; previousContent?: string }
|
|
8
8
|
export type FetchCall = CallBase & { url: string }
|
|
9
|
-
export type
|
|
9
|
+
export type Call = CommandCall | FileCall | FetchCall | CallBase
|
|
10
10
|
|
|
11
11
|
export type Contents = { content?: string; previousContent?: string }
|
|
12
12
|
export type FileAccess = { path: string; contents: (() => Promise<Contents>) | undefined }
|
|
@@ -18,9 +18,9 @@ export type CallEffects = {
|
|
|
18
18
|
subagent: string | undefined
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export function callEffects(use: CallBase, workspace: { cwd: string; home: string; fs: FileSystem }): CallEffects {
|
|
21
|
+
export function callEffects(use: CallBase, workspace: { cwd: string; home: string; configRoot?: string; fs: FileSystem }): CallEffects {
|
|
22
22
|
const { fs } = workspace
|
|
23
|
-
const absolute = (path: string, folder = workspace.cwd) => resolve(folder, expandHome(path, workspace.home))
|
|
23
|
+
const absolute = (path: string, folder = workspace.cwd) => resolve(folder, expandHome(path, workspace.home, workspace.configRoot))
|
|
24
24
|
const access = (path: string): FileAccess => ({ path: absolute(path), contents: undefined })
|
|
25
25
|
const effects: CallEffects = { shell: undefined, reads: [], writes: [], urls: [], subagent: undefined }
|
|
26
26
|
if (use.tool === 'Bash') {
|
|
@@ -79,7 +79,7 @@ async function editedContents(use: CallBase, path: string, fs: FileSystem): Prom
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
async function previousContentOf(path: string, fs: FileSystem): Promise<string | undefined> {
|
|
82
|
-
const stat = await fs.stat(path).catch((
|
|
82
|
+
const stat = await fs.stat(path).catch(ifMissing(undefined))
|
|
83
83
|
return stat?.kind === 'file' ? fs.read(path) : undefined
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type ChangelogSection = { readonly version: string; readonly date?: string; readonly text: string }
|
|
2
|
+
|
|
3
|
+
export const unreleased = 'Unreleased'
|
|
4
|
+
|
|
5
|
+
const heading = /^##\s+\[?([^\]\s]+)\]?(?:\s+-\s+(\S+))?\s*$/
|
|
6
|
+
|
|
7
|
+
const linkReference = /^\[[^\]]+\]:\s/
|
|
8
|
+
|
|
9
|
+
export function changelogSections(markdown: string): ChangelogSection[] {
|
|
10
|
+
const sections: { readonly version: string; readonly date: string | undefined; readonly lines: string[] }[] = []
|
|
11
|
+
for (const line of markdown.split('\n')) {
|
|
12
|
+
const match = heading.exec(line)
|
|
13
|
+
if (match !== null) sections.push({ version: match[1] as string, date: match[2], lines: [] })
|
|
14
|
+
else if (!linkReference.test(line)) sections.at(-1)?.lines.push(line)
|
|
15
|
+
}
|
|
16
|
+
return sections.map(({ version, date, lines }) => ({ version, ...(date === undefined ? {} : { date }), text: lines.join('\n').trim() }))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function changeLines(text: string): string[] {
|
|
20
|
+
let group: string | undefined
|
|
21
|
+
return text.split('\n').flatMap((line) => {
|
|
22
|
+
const trimmed = line.trim()
|
|
23
|
+
const subheading = /^###\s+(.+)$/.exec(trimmed)?.[1]
|
|
24
|
+
if (subheading !== undefined) {
|
|
25
|
+
group = subheading
|
|
26
|
+
return []
|
|
27
|
+
}
|
|
28
|
+
const item = /^[-*]\s+(.+)$/.exec(trimmed)?.[1] ?? trimmed
|
|
29
|
+
if (item === '') return []
|
|
30
|
+
return [group === undefined ? item : `${group}: ${item}`]
|
|
31
|
+
})
|
|
32
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type Matcher<Value> = Value extends string
|
|
2
|
+
? string | RegExp
|
|
3
|
+
: Value extends number | boolean | null | undefined
|
|
4
|
+
? Value
|
|
5
|
+
: Value extends readonly (infer Item)[]
|
|
6
|
+
? Matcher<Item>
|
|
7
|
+
: Value extends (...args: never[]) => unknown
|
|
8
|
+
? never
|
|
9
|
+
: Value extends object
|
|
10
|
+
? { readonly [Key in keyof Value]?: Matcher<Value[Key]> }
|
|
11
|
+
: Value
|
|
12
|
+
|
|
13
|
+
export type SentMatcher = string | number | boolean | null | { readonly regexp: string; readonly flags: string } | { readonly [key: string]: SentMatcher } | readonly SentMatcher[]
|
|
14
|
+
|
|
15
|
+
export function matches(value: unknown, matcher: unknown): boolean {
|
|
16
|
+
if (matcher instanceof RegExp) return value !== undefined && value !== null && matcher.test(String(value))
|
|
17
|
+
if (Array.isArray(matcher)) return matcher.some((each) => matches(value, each))
|
|
18
|
+
if (typeof matcher !== 'object' || matcher === null) return value === matcher
|
|
19
|
+
if (Array.isArray(value)) return value.some((each) => matches(each, matcher))
|
|
20
|
+
if (typeof value !== 'object' || value === null) return false
|
|
21
|
+
return Object.entries(matcher).every(([key, wanted]) => wanted === undefined || matches((value as Record<string, unknown>)[key], wanted))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function sentMatcher(matcher: unknown): SentMatcher {
|
|
25
|
+
if (matcher instanceof RegExp) return { regexp: matcher.source, flags: matcher.flags }
|
|
26
|
+
if (Array.isArray(matcher)) return matcher.map(sentMatcher)
|
|
27
|
+
if (typeof matcher !== 'object' || matcher === null) return matcher as SentMatcher
|
|
28
|
+
return Object.fromEntries(Object.entries(matcher).filter(([, value]) => value !== undefined).map(([key, value]) => [key, sentMatcher(value)]))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function receivedMatcher(sent: SentMatcher): unknown {
|
|
32
|
+
if (Array.isArray(sent)) return sent.map(receivedMatcher)
|
|
33
|
+
if (typeof sent !== 'object' || sent === null) return sent
|
|
34
|
+
const fields = sent as Readonly<Record<string, unknown>>
|
|
35
|
+
if (typeof fields['regexp'] === 'string' && typeof fields['flags'] === 'string' && Object.keys(fields).length === 2) return new RegExp(fields['regexp'], fields['flags'])
|
|
36
|
+
return Object.fromEntries(Object.entries(fields).map(([key, value]) => [key, receivedMatcher(value as SentMatcher)]))
|
|
37
|
+
}
|