@cmodjs/core 0.1.8 → 0.1.9
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 +2 -2
- package/docs/hooks.md +62 -6
- package/docs/index.md +6 -4
- package/docs/install-steps.md +3 -3
- package/docs/jobs.md +5 -4
- package/docs/mod.md +39 -7
- package/docs/testing.md +25 -9
- package/docs/ui.md +27 -3
- package/jobs/check.js +2 -2
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +2 -0
- package/jobs/permissions/decide-permission.js +2 -2
- 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 +9 -9
- package/jobs/permissions/match-target.d.ts.map +1 -1
- package/jobs/permissions/match-target.js +4 -3
- package/mod.d.ts +15 -5
- package/mod.d.ts.map +1 -1
- package/package.json +2 -1
- package/path.d.ts +2 -0
- package/path.d.ts.map +1 -0
- package/path.js +1 -0
- package/register.d.ts.map +1 -1
- package/register.js +8 -2
- package/runtime/claude.d.ts +4 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/hooks.d.ts +10 -2
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +56 -18
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +12 -2
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +38 -8
- package/shell.d.ts +2 -0
- package/shell.d.ts.map +1 -0
- package/shell.js +1 -0
- package/src/jobs/check.ts +2 -2
- package/src/jobs/context.ts +2 -0
- package/src/jobs/permissions/decide-permission.ts +2 -2
- package/src/jobs/permissions/find-project-scope.ts +11 -10
- package/src/jobs/permissions/match-target.ts +4 -3
- package/src/mod.ts +22 -3
- package/src/path.ts +1 -0
- package/src/register.ts +8 -2
- package/src/runtime/claude.ts +11 -1
- package/src/runtime/hooks.ts +62 -23
- package/src/runtime/lifecycle.ts +11 -4
- package/src/runtime/ui.ts +35 -8
- package/src/shell.ts +1 -0
- package/src/testing/fake-claude.ts +14 -5
- package/src/testing/fake-elements.ts +5 -7
- package/src/testing/fake-files.ts +5 -5
- package/src/testing.ts +83 -11
- package/src/ui/define-pane.ts +5 -1
- package/src/utils/call-effects.ts +12 -11
- package/src/utils/parse-shell.ts +28 -36
- package/src/utils/paths.ts +4 -19
- package/src/vendor.ts +1 -0
- package/testing/fake-claude.d.ts +5 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +13 -3
- package/testing/fake-elements.d.ts +5 -8
- package/testing/fake-elements.d.ts.map +1 -1
- package/testing/fake-elements.js +6 -6
- package/testing/fake-files.js +5 -5
- package/testing.d.ts +4 -2
- package/testing.d.ts.map +1 -1
- package/testing.js +65 -9
- package/ui/define-pane.d.ts +5 -1
- package/ui/define-pane.d.ts.map +1 -1
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +12 -11
- package/utils/parse-shell.d.ts +5 -9
- package/utils/parse-shell.d.ts.map +1 -1
- package/utils/parse-shell.js +5 -10
- package/utils/paths.d.ts +0 -3
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +3 -18
- package/vendor.d.ts +1 -0
- package/vendor.d.ts.map +1 -1
- package/vendor.js +209 -12
|
@@ -2,8 +2,6 @@ import type { ElementTable, RenderElement, RenderNode } from 'claude-code'
|
|
|
2
2
|
|
|
3
3
|
type Drawn = { readonly type: string; readonly props: Record<string, unknown>; readonly children?: readonly RenderNode[] }
|
|
4
4
|
|
|
5
|
-
type Button = { readonly type: 'Button'; readonly props: { readonly key: string; readonly label: string; readonly onPress: () => unknown } }
|
|
6
|
-
|
|
7
5
|
const boxInsides = new WeakMap<object, Drawn>()
|
|
8
6
|
|
|
9
7
|
function element(type: string) {
|
|
@@ -31,12 +29,12 @@ export const elements = {
|
|
|
31
29
|
Image: element('Image'),
|
|
32
30
|
} as unknown as ElementTable
|
|
33
31
|
|
|
34
|
-
export function
|
|
32
|
+
export function findElement(node: RenderNode, type: string, key: string): Drawn['props'] | undefined {
|
|
35
33
|
if (typeof node === 'string') return undefined
|
|
36
|
-
const
|
|
37
|
-
if (type ===
|
|
38
|
-
for (const child of children) {
|
|
39
|
-
const found =
|
|
34
|
+
const drawn = insideOf(node)
|
|
35
|
+
if (drawn.type === type && (drawn.props['key'] === key || (type === 'Button' && drawn.props['label'] === key))) return drawn.props
|
|
36
|
+
for (const child of drawn.children ?? []) {
|
|
37
|
+
const found = findElement(child, type, key)
|
|
40
38
|
if (found !== undefined) return found
|
|
41
39
|
}
|
|
42
40
|
return undefined
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Claude } from '../runtime/claude.js'
|
|
2
|
-
import {
|
|
2
|
+
import { resolve } from '../vendor.js'
|
|
3
3
|
|
|
4
4
|
export function fakeFiles(files: Readonly<Record<string, string>>, links: Readonly<Record<string, string>> = {}): Claude['fs'] {
|
|
5
|
-
const contents = new Map(Object.entries(files).map(([path, text]) => [
|
|
5
|
+
const contents = new Map(Object.entries(files).map(([path, text]) => [resolve('/', path), text]))
|
|
6
6
|
const realPathOf = (path: string): string => {
|
|
7
|
-
const absolute =
|
|
7
|
+
const absolute = resolve('/', path)
|
|
8
8
|
for (const [link, target] of Object.entries(links)) {
|
|
9
9
|
if (absolute === link || absolute.startsWith(`${link}/`)) return realPathOf(`${target}${absolute.slice(link.length)}`)
|
|
10
10
|
}
|
|
@@ -35,7 +35,7 @@ export function fakeFiles(files: Readonly<Record<string, string>>, links: Readon
|
|
|
35
35
|
if (kindOf(folder) !== 'dir') throw new Error(`ENOENT: no such directory, list '${path}'`)
|
|
36
36
|
const names = new Set(below(folder).map((file) => file.split('/')[0] as string))
|
|
37
37
|
return [...names].map((name) => {
|
|
38
|
-
const entry =
|
|
38
|
+
const entry = resolve(folder, name)
|
|
39
39
|
if (Object.hasOwn(links, entry)) return { name, kind: 'other', size: 0, mtimeMs: 0, isLink: true }
|
|
40
40
|
return { name, kind: contents.has(entry) ? 'file' : 'dir', size: sizeOf(entry), mtimeMs: 0, isLink: false }
|
|
41
41
|
})
|
|
@@ -44,7 +44,7 @@ export function fakeFiles(files: Readonly<Record<string, string>>, links: Readon
|
|
|
44
44
|
stat: async (path, options) => {
|
|
45
45
|
const realPath = realPathOf(path)
|
|
46
46
|
const kind = kindOf(realPath)
|
|
47
|
-
const isLink = Object.hasOwn(links,
|
|
47
|
+
const isLink = Object.hasOwn(links, resolve('/', path))
|
|
48
48
|
if (kind === undefined && isLink) return { kind: 'other', size: 0, mtimeMs: 0, isLink }
|
|
49
49
|
if (kind === undefined) throw new Error(`ENOENT: no such file or directory, stat '${path}'`)
|
|
50
50
|
return { kind, size: sizeOf(realPath), mtimeMs: 0, isLink, ...(options?.resolve === true ? { realPath } : {}) }
|
package/src/testing.ts
CHANGED
|
@@ -1,13 +1,31 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Args,
|
|
3
|
+
ClassicHookInputs,
|
|
4
|
+
CmodDependencies,
|
|
5
|
+
CommandPresentation,
|
|
6
|
+
CommandRunResult,
|
|
7
|
+
EventResult,
|
|
8
|
+
Frozen,
|
|
9
|
+
PaneOpenArgs,
|
|
10
|
+
PressedLink,
|
|
11
|
+
RenderChildren,
|
|
12
|
+
RenderComponent,
|
|
13
|
+
RenderElement,
|
|
14
|
+
RenderPropsOf,
|
|
15
|
+
UiInputArgument,
|
|
16
|
+
UiPressArgument,
|
|
17
|
+
UiSelectArgument,
|
|
18
|
+
} from 'claude-code'
|
|
2
19
|
import type { Reply } from './jobs/slash-command.js'
|
|
3
20
|
import type { ModDefinition, ModEvent } from './mod.js'
|
|
4
21
|
import { storeFolder } from './records.js'
|
|
5
22
|
import type { Claude } from './runtime/claude.js'
|
|
6
23
|
import { answerCall, notInstalled } from './runtime/dependencies.js'
|
|
7
24
|
import type { RoutedEvent } from './runtime/hooks.js'
|
|
25
|
+
import { toolInputOf } from './runtime/tool-calls.js'
|
|
8
26
|
import { createLifecycle } from './runtime/lifecycle.js'
|
|
9
27
|
import { fakeClaude, type Fakes, type Shown, type TestCall } from './testing/fake-claude.js'
|
|
10
|
-
import { elements,
|
|
28
|
+
import { elements, findElement, rowsOf } from './testing/fake-elements.js'
|
|
11
29
|
import { fakeFiles } from './testing/fake-files.js'
|
|
12
30
|
import type { Slot, SlotProps } from './ui/slots.js'
|
|
13
31
|
|
|
@@ -35,7 +53,9 @@ export type TestedMod<State extends object> = {
|
|
|
35
53
|
lines<S extends Slot<object, object, { readonly component: RenderComponent }>>(slot: S, props: Omit<SlotProps<S>, 'Default'>, requestId?: string): Promise<string[]>
|
|
36
54
|
type(line: string): Promise<Reply>
|
|
37
55
|
callTool(name: string, input: Record<string, unknown>): Promise<EventResult<'tool.call'>>
|
|
38
|
-
press(paneId: string, key: string): Promise<void>
|
|
56
|
+
press(paneId: string, key: string, link?: string): Promise<void>
|
|
57
|
+
input(paneId: string, key: string, text: string, kind?: UiInputArgument['kind']): Promise<void>
|
|
58
|
+
select(paneId: string, key: string, value: string): Promise<void>
|
|
39
59
|
moveTo(projectRoot: string, cwd?: string): Promise<void>
|
|
40
60
|
readonly state: Readonly<State>
|
|
41
61
|
readonly calls: readonly TestCall[]
|
|
@@ -67,6 +87,12 @@ export function testMod<State extends object>(definition: ModDefinition<State>,
|
|
|
67
87
|
fake.claude.session.root = async () => projectRoot
|
|
68
88
|
fake.claude.session.cwd = async () => cwd
|
|
69
89
|
fake.fakes.cmod.call = fakeDependencies(options.dependencies ?? {})
|
|
90
|
+
const firedAgents = new Map<string, string>()
|
|
91
|
+
const listAgents = fake.claude.agent.list
|
|
92
|
+
fake.claude.agent.list = async () => {
|
|
93
|
+
if (fake.fakes.agent.list !== undefined || firedAgents.size === 0) return listAgents()
|
|
94
|
+
return [...firedAgents].map(([id, type]) => ({ id, type, description: '', status: 'running' as const }))
|
|
95
|
+
}
|
|
70
96
|
Object.assign(fake.fakes.fs, fakeFiles(options.files ?? {}))
|
|
71
97
|
const starting = options.state as Record<string, object> | undefined
|
|
72
98
|
const declared = Object.entries(definition.state ?? {}) as [string, object][]
|
|
@@ -100,11 +126,20 @@ export function testMod<State extends object>(definition: ModDefinition<State>,
|
|
|
100
126
|
}
|
|
101
127
|
const opened = fake.calls.findLast((call) => call.call === 'ui.open' && (call.args[0] as PaneOpenArgs).id === paneId)?.args[0] as PaneOpenArgs | undefined
|
|
102
128
|
const columns = opened?.columns ?? defaultColumns
|
|
103
|
-
const
|
|
129
|
+
const isFocused = (await fake.claude.ui.panes()).some((open) => open.id === paneId && open.isFocused)
|
|
130
|
+
const props: RenderPropsOf['Pane'] = { title: opened?.title ?? paneId, isFocused, bodyColumns: columns, placement: 'dock', scroll: { offset: 0, bodyRows: defaultRows }, view: {} }
|
|
104
131
|
const drawing = await route('ui.render', { surface: 'terminal', component: 'Pane', requestId: paneId, props }, elements.Box({}))
|
|
105
132
|
return { drawing, columns }
|
|
106
133
|
}
|
|
107
134
|
|
|
135
|
+
const drawnElement = async (paneId: string, type: 'Button' | 'Input' | 'Select' | 'Markdown', key: string) => {
|
|
136
|
+
const props = findElement((await drawPane(paneId)).drawing, type, key)
|
|
137
|
+
if (props === undefined) throw new Error(`The pane "${paneId}" of ${name} draws no ${type} with the ${type === 'Button' ? 'key or label' : 'key'} "${key}".`)
|
|
138
|
+
return props
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const eventIn = (paneId: string, key: string) => ({ plugin: name, element: key, component: 'Pane' as const, requestId: paneId, surface: 'terminal' as const })
|
|
142
|
+
|
|
108
143
|
const drawSlot = async (component: RenderComponent, props: object, requestId = `${component}_${(slotDraws += 1)}`) => {
|
|
109
144
|
await start()
|
|
110
145
|
const input = { surface: 'terminal', component, requestId, props } as Frozen<Args<'ui.render'>>
|
|
@@ -121,11 +156,31 @@ export function testMod<State extends object>(definition: ModDefinition<State>,
|
|
|
121
156
|
}
|
|
122
157
|
const toolUse = toolUseEvents.includes(event) ? { tool_use_id: `toolu_${(toolUses += 1)}` } : {}
|
|
123
158
|
const filled = { session_id: sessionId, transcript_path: '/test/transcript.jsonl', cwd, hook_event_name: event, ...toolUse, ...input }
|
|
124
|
-
if (event !== 'PreToolUse') return route(`classic.${event as ModEvent}`, filled, below)
|
|
125
|
-
|
|
126
|
-
return route('classic.PreToolUse', { ...(tool_input as object), tool: tool_name, tool_use_id }, below)
|
|
159
|
+
if (event !== 'PreToolUse') return route(`classic.${event as Exclude<ModEvent, 'PreToolUse'>}`, filled, below)
|
|
160
|
+
return firePreToolUse(filled as ClassicHookInputs['PreToolUse'], below as { deny?: string } | undefined)
|
|
127
161
|
}) as TestedMod<State>['fire']
|
|
128
162
|
|
|
163
|
+
const firePreToolUse = async ({ tool_name, tool_input, tool_use_id, agent_id, agent_type }: ClassicHookInputs['PreToolUse'], below: { deny?: string } | undefined) => {
|
|
164
|
+
await start()
|
|
165
|
+
if (agent_id !== undefined && agent_type !== undefined) firedAgents.set(agent_id, agent_type)
|
|
166
|
+
const envelope = { ...(tool_input as object), tool: tool_name, tool_use_id, ...(agent_id === undefined ? {} : { agentId: agent_id }) } as Frozen<Args<'tool.call'>>
|
|
167
|
+
const asks: EventResult<'tool.check'> = below?.deny === undefined ? { decision: 'ask' } : { decision: 'deny', reason: below.deny }
|
|
168
|
+
let reached = envelope
|
|
169
|
+
let check = asks
|
|
170
|
+
const answer = await lifecycle.route('tool.call', envelope, async (e) => {
|
|
171
|
+
reached = e
|
|
172
|
+
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)
|
|
173
|
+
return (check.decision === 'deny' ? { deny: check.reason ?? '' } : { result: '' }) as EventResult<'tool.call'>
|
|
174
|
+
})
|
|
175
|
+
const updatedInput = toolInputOf(reached)
|
|
176
|
+
const decided = check === asks ? {} : check.decision === 'allow' ? { allow: true } : check.decision === 'ask' ? { ask: check.reason ?? '' } : {}
|
|
177
|
+
return {
|
|
178
|
+
...(answer.deny === undefined ? decided : { deny: answer.deny }),
|
|
179
|
+
...(JSON.stringify(updatedInput) === JSON.stringify(tool_input) ? {} : { updatedInput }),
|
|
180
|
+
...(answer.context === undefined || answer.context.length === 0 ? {} : { additionalContext: [...answer.context] }),
|
|
181
|
+
} as EventResult<'classic.PreToolUse'>
|
|
182
|
+
}
|
|
183
|
+
|
|
129
184
|
return {
|
|
130
185
|
start,
|
|
131
186
|
fire,
|
|
@@ -154,10 +209,27 @@ export function testMod<State extends object>(definition: ModDefinition<State>,
|
|
|
154
209
|
if (answer === unanswered) throw new Error(`${name} has no tool ${toolName}. Add it in setup with mod.use(tool({ name: '${toolName}', … })).`)
|
|
155
210
|
return answer
|
|
156
211
|
},
|
|
157
|
-
async press(paneId, key) {
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
160
|
-
|
|
212
|
+
async press(paneId, key, link) {
|
|
213
|
+
const props = await drawnElement(paneId, link === undefined ? 'Button' : 'Markdown', key)
|
|
214
|
+
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.`)
|
|
215
|
+
const pressed: UiPressArgument = { ...eventIn(paneId, String(props['key'])), ...(link === undefined ? {} : { link: { href: link } }) }
|
|
216
|
+
await lifecycle.route('ui.press', pressed as Frozen<Args<'ui.press'>>, async (e) => {
|
|
217
|
+
if (e.link === undefined) await (props['onPress'] as (e: UiPressArgument) => unknown)(e)
|
|
218
|
+
else await (props['onLinkPress'] as (link: PressedLink, e: UiPressArgument) => unknown)(e.link, e)
|
|
219
|
+
return { element: e.element }
|
|
220
|
+
})
|
|
221
|
+
},
|
|
222
|
+
async input(paneId, key, text, kind = 'submit') {
|
|
223
|
+
const props = await drawnElement(paneId, 'Input', key)
|
|
224
|
+
const typed: UiInputArgument = { ...eventIn(paneId, key), kind, value: text }
|
|
225
|
+
await (props[kind === 'submit' ? 'onSubmit' : 'onInput'] as ((value: string, e: UiInputArgument) => unknown) | undefined)?.(text, typed)
|
|
226
|
+
},
|
|
227
|
+
async select(paneId, key, value) {
|
|
228
|
+
const props = await drawnElement(paneId, 'Select', key)
|
|
229
|
+
const options = (props['options'] as readonly { readonly value: string }[]).map((option) => option.value)
|
|
230
|
+
if (!options.includes(value)) throw new Error(`The Select "${key}" in the pane "${paneId}" of ${name} has no option "${value}". Its options are ${options.map((option) => `"${option}"`).join(', ')}.`)
|
|
231
|
+
const picked: UiSelectArgument = { ...eventIn(paneId, key), value }
|
|
232
|
+
await (props['onSelect'] as (value: string, e: UiSelectArgument) => unknown)(value, picked)
|
|
161
233
|
},
|
|
162
234
|
async moveTo(nextRoot, nextCwd = nextRoot) {
|
|
163
235
|
await start()
|
package/src/ui/define-pane.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Frozen, RenderElement, RenderPropsOf } from 'claude-code'
|
|
1
|
+
import type { Frozen, PaneCloseInput, RenderElement, RenderPropsOf, UiScrollInput } from 'claude-code'
|
|
2
2
|
import type { Mod } from '../mod.js'
|
|
3
3
|
|
|
4
4
|
export type Pane<State extends object = Record<never, never>> = {
|
|
@@ -6,7 +6,11 @@ export type Pane<State extends object = Record<never, never>> = {
|
|
|
6
6
|
readonly title: string
|
|
7
7
|
readonly columns?: number | ((state: State) => number | undefined)
|
|
8
8
|
readonly rows?: number | ((state: State) => number | undefined)
|
|
9
|
+
readonly closeOnEscape?: true
|
|
10
|
+
readonly holdToasts?: true
|
|
9
11
|
render(mod: Mod<State>, props: Frozen<RenderPropsOf['Pane']>): RenderElement
|
|
12
|
+
onScroll?(mod: Mod<State>, e: Frozen<UiScrollInput>): void | Promise<void>
|
|
13
|
+
onClose?(mod: Mod<State>, e: Frozen<PaneCloseInput>): void | Promise<void>
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
const paneId = /^[A-Za-z0-9_-]{1,64}$/
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { expandHome,
|
|
1
|
+
import { parseShell, type ShellCommand } from './parse-shell.js'
|
|
2
|
+
import { expandHome, type FileSystem } from './paths.js'
|
|
3
|
+
import { resolve } from '../vendor.js'
|
|
3
4
|
|
|
4
5
|
export type ToolUse = { tool: string; input: unknown; agentId?: string; agentType?: string }
|
|
5
6
|
|
|
@@ -21,13 +22,13 @@ export type CallEffects = {
|
|
|
21
22
|
|
|
22
23
|
export function callEffects(use: ToolUse, workspace: { cwd: string; home: string; fs: FileSystem }): CallEffects {
|
|
23
24
|
const { fs } = workspace
|
|
24
|
-
const
|
|
25
|
-
const access = (path: string): FileAccess => ({ path:
|
|
25
|
+
const absolute = (path: string, folder = workspace.cwd) => resolve(folder, expandHome(path, workspace.home))
|
|
26
|
+
const access = (path: string): FileAccess => ({ path: absolute(path), contents: undefined })
|
|
26
27
|
const effects: CallEffects = { shell: undefined, reads: [], writes: [], urls: [], subagent: undefined }
|
|
27
28
|
if (use.tool === 'Bash') {
|
|
28
29
|
const line = textField(use, 'command')
|
|
29
|
-
const parsed =
|
|
30
|
-
const commands = parsed.commands.map(({ argv, folder }) => ({ argv, folder:
|
|
30
|
+
const parsed = parseShell(line)
|
|
31
|
+
const commands = parsed.commands.map(({ argv, folder }) => ({ argv, folder: absolute(folder) }))
|
|
31
32
|
effects.shell = { line, commands, isFullyParsed: parsed.isFullyParsed }
|
|
32
33
|
effects.reads = parsed.reads.map(access)
|
|
33
34
|
effects.writes = parsed.writes.map(access)
|
|
@@ -35,22 +36,22 @@ export function callEffects(use: ToolUse, workspace: { cwd: string; home: string
|
|
|
35
36
|
} else if (use.tool === 'PowerShell') {
|
|
36
37
|
effects.shell = { line: textField(use, 'command'), commands: [], isFullyParsed: false }
|
|
37
38
|
} else if (use.tool === 'Edit') {
|
|
38
|
-
const path =
|
|
39
|
+
const path = absolute(textField(use, 'file_path'))
|
|
39
40
|
effects.writes = [{ path, contents: once(() => editedContents(use, path, fs)) }]
|
|
40
41
|
} else if (use.tool === 'Write') {
|
|
41
|
-
const path =
|
|
42
|
+
const path = absolute(textField(use, 'file_path'))
|
|
42
43
|
const content = textField(use, 'content')
|
|
43
44
|
effects.writes = [{ path, contents: once(async () => contentsOf(content, await previousContentOf(path, fs))) }]
|
|
44
45
|
} else if (use.tool === 'NotebookEdit') {
|
|
45
|
-
const path =
|
|
46
|
+
const path = absolute(textField(use, 'notebook_path'))
|
|
46
47
|
effects.writes = [{ path, contents: once(async () => contentsOf(undefined, await previousContentOf(path, fs))) }]
|
|
47
48
|
} else if (use.tool === 'Read') {
|
|
48
49
|
effects.reads = [access(textField(use, 'file_path'))]
|
|
49
50
|
} else if (use.tool === 'Grep') {
|
|
50
51
|
effects.reads = [access(optionalTextField(use, 'path') ?? '')]
|
|
51
52
|
} else if (use.tool === 'Glob') {
|
|
52
|
-
const folder =
|
|
53
|
-
effects.reads = [{ path:
|
|
53
|
+
const folder = absolute(optionalTextField(use, 'path') ?? '')
|
|
54
|
+
effects.reads = [{ path: absolute(textField(use, 'pattern'), folder), contents: undefined }]
|
|
54
55
|
} else if (use.tool === 'WebFetch') {
|
|
55
56
|
effects.urls = [textField(use, 'url')]
|
|
56
57
|
} else if (use.tool === 'Agent') {
|
package/src/utils/parse-shell.ts
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { basename, parse, type GlobPattern, type ParseEntry } from '../vendor.js'
|
|
2
|
+
|
|
3
|
+
export type ShellCommand = { argv: [string, ...string[]]; folder: string }
|
|
3
4
|
|
|
4
5
|
export type ParsedShell = {
|
|
5
|
-
commands: [
|
|
6
|
+
commands: ShellCommand[]
|
|
6
7
|
writes: string[]
|
|
7
8
|
reads: string[]
|
|
8
9
|
fetches: string[]
|
|
9
10
|
isFullyParsed: boolean
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
export type ShellCommand = { argv: [string, ...string[]]; folder: string }
|
|
13
|
-
|
|
14
|
-
export type ParsedShellCommands = Omit<ParsedShell, 'commands'> & { commands: ShellCommand[] }
|
|
15
|
-
|
|
16
13
|
type Word = { text: string; isGlob: boolean; isDynamic: boolean }
|
|
17
14
|
type Marker = { raw: string; code: string[] }
|
|
18
15
|
type Expansion = Marker & { end: number }
|
|
@@ -200,17 +197,12 @@ const inlineCodeFlags = new Map<string, readonly string[]>([
|
|
|
200
197
|
])
|
|
201
198
|
|
|
202
199
|
export function parseShell(line: string): ParsedShell {
|
|
203
|
-
const
|
|
204
|
-
return { ...parsed, commands: parsed.commands.map((command) => command.argv) }
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export function parseShellCommands(line: string): ParsedShellCommands {
|
|
208
|
-
const result: ParsedShellCommands = { commands: [], writes: [], reads: [], fetches: [], isFullyParsed: true }
|
|
200
|
+
const result: ParsedShell = { commands: [], writes: [], reads: [], fetches: [], isFullyParsed: true }
|
|
209
201
|
parseLine(line, '', result)
|
|
210
202
|
return result
|
|
211
203
|
}
|
|
212
204
|
|
|
213
|
-
function parseLine(line: string, folder: string, result:
|
|
205
|
+
function parseLine(line: string, folder: string, result: ParsedShell): void {
|
|
214
206
|
if (line.includes('\u0001')) {
|
|
215
207
|
result.isFullyParsed = false
|
|
216
208
|
return
|
|
@@ -475,7 +467,7 @@ function readHeredocBodies(source: string, from: number, heredocs: Heredoc[]): n
|
|
|
475
467
|
return index
|
|
476
468
|
}
|
|
477
469
|
|
|
478
|
-
function walk(tokens: ParseEntry[], markers: Marker[], start: string, result:
|
|
470
|
+
function walk(tokens: ParseEntry[], markers: Marker[], start: string, result: ParsedShell): void {
|
|
479
471
|
let folder = start
|
|
480
472
|
const subshells: string[] = []
|
|
481
473
|
let command = newCommand(false)
|
|
@@ -533,7 +525,7 @@ function isWordToken(token: ParseEntry): token is string | GlobPattern {
|
|
|
533
525
|
return typeof token === 'string' || ('op' in token && token.op === 'glob')
|
|
534
526
|
}
|
|
535
527
|
|
|
536
|
-
function wordOf(token: string | GlobPattern, markers: Marker[], folder: string, result:
|
|
528
|
+
function wordOf(token: string | GlobPattern, markers: Marker[], folder: string, result: ParsedShell): Word {
|
|
537
529
|
const raw = typeof token === 'string' ? token : token.pattern
|
|
538
530
|
for (const match of raw.matchAll(markerPattern)) {
|
|
539
531
|
for (const code of markers[Number(match[1])]!.code) parseLine(code, folder, result)
|
|
@@ -541,11 +533,11 @@ function wordOf(token: string | GlobPattern, markers: Marker[], folder: string,
|
|
|
541
533
|
return {
|
|
542
534
|
text: raw.replace(markerPattern, (_, index: string) => markers[Number(index)]!.raw),
|
|
543
535
|
isGlob: typeof token !== 'string',
|
|
544
|
-
isDynamic: /[$\u0001]/.test(
|
|
536
|
+
isDynamic: /[$\u0001]/.test(basename(raw)),
|
|
545
537
|
}
|
|
546
538
|
}
|
|
547
539
|
|
|
548
|
-
function finish(command: Command, operator: string, folder: string, result:
|
|
540
|
+
function finish(command: Command, operator: string, folder: string, result: ParsedShell): string {
|
|
549
541
|
for (const redirect of command.redirects) addRedirect(redirect, folder, result)
|
|
550
542
|
const words = withoutPrefixes(command.words)
|
|
551
543
|
const [first, ...args] = words
|
|
@@ -569,14 +561,14 @@ function withoutPrefixes(words: Word[]): Word[] {
|
|
|
569
561
|
return words.slice(start)
|
|
570
562
|
}
|
|
571
563
|
|
|
572
|
-
function addRedirect({ operator, target }: Redirect, folder: string, result:
|
|
564
|
+
function addRedirect({ operator, target }: Redirect, folder: string, result: ParsedShell): void {
|
|
573
565
|
if (operator === '<' || operator === '<>') addPath(result, 'reads', target, folder)
|
|
574
566
|
if (writeRedirects.has(operator) || (operator === '>&' && !/^([0-9]+|-)$/.test(target.text))) addPath(result, 'writes', target, folder)
|
|
575
567
|
}
|
|
576
568
|
|
|
577
|
-
function addCommand(first: Word, args: Word[], folder: string, stdin: Stdin | undefined, result:
|
|
569
|
+
function addCommand(first: Word, args: Word[], folder: string, stdin: Stdin | undefined, result: ParsedShell): string | undefined {
|
|
578
570
|
if (first.isDynamic || first.isGlob) result.isFullyParsed = false
|
|
579
|
-
const program =
|
|
571
|
+
const program = basename(first.text)
|
|
580
572
|
if (program === 'git') addGit(first, args, folder, result)
|
|
581
573
|
else if (program === 'find') addFind(first, args, folder, result)
|
|
582
574
|
else if (wrappers.has(program)) addWrapper(first, program, args, folder, stdin, result)
|
|
@@ -589,7 +581,7 @@ function addCommand(first: Word, args: Word[], folder: string, stdin: Stdin | un
|
|
|
589
581
|
return undefined
|
|
590
582
|
}
|
|
591
583
|
|
|
592
|
-
function addGit(first: Word, args: Word[], folder: string, result:
|
|
584
|
+
function addGit(first: Word, args: Word[], folder: string, result: ParsedShell): void {
|
|
593
585
|
let index = 0
|
|
594
586
|
let gitFolder = folder
|
|
595
587
|
for (let text = args[0]?.text; text?.startsWith('-') === true; text = args[index]?.text) {
|
|
@@ -600,7 +592,7 @@ function addGit(first: Word, args: Word[], folder: string, result: ParsedShellCo
|
|
|
600
592
|
result.commands.push({ argv: [first.text, ...expandFlags('git', args.slice(index))], folder: gitFolder })
|
|
601
593
|
}
|
|
602
594
|
|
|
603
|
-
function addFind(first: Word, args: Word[], folder: string, result:
|
|
595
|
+
function addFind(first: Word, args: Word[], folder: string, result: ParsedShell): void {
|
|
604
596
|
const own: string[] = []
|
|
605
597
|
const actions: Word[][] = []
|
|
606
598
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -628,7 +620,7 @@ function findStarts(args: Word[]): Word[] {
|
|
|
628
620
|
return starts.length > 0 ? starts : [literal('.')]
|
|
629
621
|
}
|
|
630
622
|
|
|
631
|
-
function addWrapper(first: Word, program: string, args: Word[], folder: string, stdin: Stdin | undefined, result:
|
|
623
|
+
function addWrapper(first: Word, program: string, args: Word[], folder: string, stdin: Stdin | undefined, result: ParsedShell): void {
|
|
632
624
|
const wrapper = wrappers.get(program)!
|
|
633
625
|
if (program === 'xargs' && stdin?.kind !== 'text') result.isFullyParsed = false
|
|
634
626
|
const { options, operands } = readArguments(args, wrapper, true)
|
|
@@ -651,7 +643,7 @@ function addWrapper(first: Word, program: string, args: Word[], folder: string,
|
|
|
651
643
|
if (innerFirst !== undefined) addCommand(innerFirst, innerArgs, wrappedFolder, stdin, result)
|
|
652
644
|
}
|
|
653
645
|
|
|
654
|
-
function addEffects(program: string, args: Word[], flags: string[], folder: string, stdin: Stdin | undefined, result:
|
|
646
|
+
function addEffects(program: string, args: Word[], flags: string[], folder: string, stdin: Stdin | undefined, result: ParsedShell): void {
|
|
655
647
|
const fileProgram = fileReaders.get(program)
|
|
656
648
|
const fileWriter = fileWriters.get(program)
|
|
657
649
|
const copyProgram = copyPrograms.get(program)
|
|
@@ -675,7 +667,7 @@ function addEffects(program: string, args: Word[], flags: string[], folder: stri
|
|
|
675
667
|
}
|
|
676
668
|
}
|
|
677
669
|
|
|
678
|
-
function addShell(args: Word[], folder: string, stdin: Stdin | undefined, result:
|
|
670
|
+
function addShell(args: Word[], folder: string, stdin: Stdin | undefined, result: ParsedShell): void {
|
|
679
671
|
const { options, operands } = readArguments(args, { valued: 'oO', longValued: ['--rcfile', '--init-file'] }, true)
|
|
680
672
|
const script = operands[0]
|
|
681
673
|
if (options.some((option) => option.name === '-c')) {
|
|
@@ -683,35 +675,35 @@ function addShell(args: Word[], folder: string, stdin: Stdin | undefined, result
|
|
|
683
675
|
} else if (script === undefined || options.some((option) => option.name === '-s')) addStdinCode(stdin, folder, result)
|
|
684
676
|
}
|
|
685
677
|
|
|
686
|
-
function addSsh(args: Word[], stdin: Stdin | undefined, result:
|
|
678
|
+
function addSsh(args: Word[], stdin: Stdin | undefined, result: ParsedShell): void {
|
|
687
679
|
const { operands } = readArguments(args, { valued: 'BbcDEeFIiJLlmOoPpQRSWw' }, true)
|
|
688
680
|
const remote = operands.slice(1)
|
|
689
681
|
if (remote.length > 0) parseLine(remote.map((word) => word.text).join(' '), '~', result)
|
|
690
682
|
else if (operands.length > 0) addStdinCode(stdin, '~', result)
|
|
691
683
|
}
|
|
692
684
|
|
|
693
|
-
function addSu(args: Word[], folder: string, result:
|
|
685
|
+
function addSu(args: Word[], folder: string, result: ParsedShell): void {
|
|
694
686
|
const code = readArguments(args, su, false).options.findLast((option) => suCodeOptions.includes(option.name))?.value
|
|
695
687
|
if (code !== undefined) parseLine(code.text, folder, result)
|
|
696
688
|
}
|
|
697
689
|
|
|
698
|
-
function addFetches(fetcher: OptionSpec, args: Word[], result:
|
|
690
|
+
function addFetches(fetcher: OptionSpec, args: Word[], result: ParsedShell): void {
|
|
699
691
|
const { options, operands } = readArguments(args, fetcher, false)
|
|
700
692
|
const urls = [...options.filter((option) => option.name === '--url').flatMap((option) => (option.value === undefined ? [] : [option.value])), ...operands]
|
|
701
693
|
for (const url of urls) result.fetches.push(/^[a-z][a-z0-9+.-]*:\/\//i.test(url.text) ? url.text : `http://${url.text}`)
|
|
702
694
|
}
|
|
703
695
|
|
|
704
|
-
function addStdinCode(stdin: Stdin | undefined, folder: string, result:
|
|
696
|
+
function addStdinCode(stdin: Stdin | undefined, folder: string, result: ParsedShell): void {
|
|
705
697
|
if (stdin?.kind === 'text') parseLine(stdin.text, folder, result)
|
|
706
698
|
if (stdin?.kind === 'pipe') result.isFullyParsed = false
|
|
707
699
|
}
|
|
708
700
|
|
|
709
|
-
function addCopy(program: CopyProgram, args: Word[], folder: string, result:
|
|
701
|
+
function addCopy(program: CopyProgram, args: Word[], folder: string, result: ParsedShell): void {
|
|
710
702
|
const { options, operands } = readArguments(args, program, false)
|
|
711
703
|
const target = options.findLast((option) => program.targetOptions?.includes(option.name) === true)?.value
|
|
712
704
|
const only = operands[0]
|
|
713
705
|
if (program.linksHere === true && target === undefined && operands.length === 1 && only !== undefined) {
|
|
714
|
-
addPath(result, 'writes', { ...only, text:
|
|
706
|
+
addPath(result, 'writes', { ...only, text: basename(only.text) }, folder)
|
|
715
707
|
return
|
|
716
708
|
}
|
|
717
709
|
const destination = target ?? operands.at(-1)
|
|
@@ -720,12 +712,12 @@ function addCopy(program: CopyProgram, args: Word[], folder: string, result: Par
|
|
|
720
712
|
const isLocal = (word: Word) => program.skipsRemote !== true || !/^[^/]*:/.test(word.text)
|
|
721
713
|
for (const source of sources.filter(isLocal)) {
|
|
722
714
|
if (program.sources !== undefined) addPath(result, program.sources, source, folder)
|
|
723
|
-
if (isLocal(destination) && !source.isGlob && !destination.isGlob) addPathText(result, 'writes', `${destination.text.replace(/\/+$/, '')}/${
|
|
715
|
+
if (isLocal(destination) && !source.isGlob && !destination.isGlob) addPathText(result, 'writes', `${destination.text.replace(/\/+$/, '')}/${basename(source.text)}`, folder)
|
|
724
716
|
}
|
|
725
717
|
if (isLocal(destination)) addPath(result, 'writes', destination, folder)
|
|
726
718
|
}
|
|
727
719
|
|
|
728
|
-
function addFileArguments(program: FileProgram, args: Word[], folder: string, result:
|
|
720
|
+
function addFileArguments(program: FileProgram, args: Word[], folder: string, result: ParsedShell): void {
|
|
729
721
|
const { options, operands } = readArguments(args, program, false)
|
|
730
722
|
const inPlace = options.find((option) => program.inPlaceOptions?.includes(option.name) === true && (program !== awk || option.value?.text === 'inplace'))
|
|
731
723
|
let files = operands
|
|
@@ -812,11 +804,11 @@ function changedFolder(args: Word[], folder: string): string {
|
|
|
812
804
|
return joinFolder(folder, target.text === '-' ? '$OLDPWD' : target.text)
|
|
813
805
|
}
|
|
814
806
|
|
|
815
|
-
function addPath(result:
|
|
807
|
+
function addPath(result: ParsedShell, access: Access, word: Word | undefined, folder: string): void {
|
|
816
808
|
if (word !== undefined && !word.isGlob) addPathText(result, access, word.text, folder)
|
|
817
809
|
}
|
|
818
810
|
|
|
819
|
-
function addPathText(result:
|
|
811
|
+
function addPathText(result: ParsedShell, access: Access, path: string, folder: string): void {
|
|
820
812
|
if (path === '' || path.startsWith('/dev/')) return
|
|
821
813
|
const joined = joinFolder(folder, path)
|
|
822
814
|
if (dynamicPattern.test(joined.replace(homePrefix, ''))) result.isFullyParsed = false
|
package/src/utils/paths.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { basename, dirname, resolve } from '../vendor.js'
|
|
2
|
+
|
|
1
3
|
export type FileSystem = {
|
|
2
4
|
read: (path: string) => Promise<string>
|
|
3
5
|
stat: (path: string) => Promise<{ kind: 'file' | 'dir' | 'other'; realPath?: string }>
|
|
@@ -9,23 +11,6 @@ export function expandHome(path: string, home: string): string {
|
|
|
9
11
|
return prefix === undefined ? path : `${home}${path.slice(prefix.length)}`
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
export function resolvePath(path: string, folder: string): string {
|
|
13
|
-
const parts: string[] = []
|
|
14
|
-
for (const part of (path.startsWith('/') ? path : `${folder}/${path}`).split('/')) {
|
|
15
|
-
if (part === '..') parts.pop()
|
|
16
|
-
else if (part !== '' && part !== '.') parts.push(part)
|
|
17
|
-
}
|
|
18
|
-
return `/${parts.join('/')}`
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function parentOf(path: string): string {
|
|
22
|
-
return resolvePath('..', path)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function nameOf(path: string): string {
|
|
26
|
-
return path.slice(path.lastIndexOf('/') + 1)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
14
|
export function relativePath(base: string, path: string): string | undefined {
|
|
30
15
|
if (path === base) return ''
|
|
31
16
|
const prefix = base === '/' ? '/' : `${base}/`
|
|
@@ -35,6 +20,6 @@ export function relativePath(base: string, path: string): string | undefined {
|
|
|
35
20
|
export async function realPathOf(path: string, fs: FileSystem): Promise<string | undefined> {
|
|
36
21
|
const stat = await fs.stat(path).catch(() => undefined)
|
|
37
22
|
if (stat !== undefined || path === '/') return stat?.realPath
|
|
38
|
-
const folder = await realPathOf(
|
|
39
|
-
return folder === undefined ? undefined :
|
|
23
|
+
const folder = await realPathOf(dirname(path), fs)
|
|
24
|
+
return folder === undefined ? undefined : resolve(folder, basename(path))
|
|
40
25
|
}
|
package/src/vendor.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { Validator } from '@cfworker/json-schema'
|
|
2
|
+
export { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve } from 'pathe'
|
|
2
3
|
export { default as picomatch } from 'picomatch/posix'
|
|
3
4
|
export { parse, quote, type GlobPattern, type ParseEntry } from 'shell-quote'
|
package/testing/fake-claude.d.ts
CHANGED
|
@@ -24,9 +24,14 @@ export type Fakes = {
|
|
|
24
24
|
ui: {
|
|
25
25
|
ask?: Claude['ui']['ask'];
|
|
26
26
|
open?: Claude['ui']['open'];
|
|
27
|
+
scroll?: Claude['ui']['scroll'];
|
|
28
|
+
};
|
|
29
|
+
session: {
|
|
30
|
+
messages?: Claude['session']['messages'];
|
|
27
31
|
};
|
|
28
32
|
agent: {
|
|
29
33
|
list?: Claude['agent']['list'];
|
|
34
|
+
spawn?: Claude['agent']['spawn'];
|
|
30
35
|
};
|
|
31
36
|
clock: {
|
|
32
37
|
after?: Claude['clock']['after'];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fake-claude.d.ts","sourceRoot":"","sources":["../src/testing/fake-claude.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAGlD,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,CAAA;CAClC,CAAA;AAED,MAAM,MAAM,KAAK,GAAG;IAClB,OAAO,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;IAC/E,EAAE,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IAC7J,IAAI,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;IACzC,QAAQ,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IAC/C,EAAE,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"fake-claude.d.ts","sourceRoot":"","sources":["../src/testing/fake-claude.ts"],"names":[],"mappings":"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"}
|
package/testing/fake-claude.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { elements } from './fake-elements.js';
|
|
2
2
|
export function fakeClaude(plugin) {
|
|
3
3
|
const calls = [];
|
|
4
|
-
const fakes = { process: {}, fs: {}, http: {}, settings: {}, ui: {}, agent: {}, clock: {}, cmod: {} };
|
|
4
|
+
const fakes = { process: {}, fs: {}, http: {}, settings: {}, ui: {}, session: {}, agent: {}, clock: {}, cmod: {} };
|
|
5
5
|
const shown = { toasts: [], logs: [], debug: [], statuses: [], openPanes: new Set(), commands: [], tools: [] };
|
|
6
6
|
const unplacedPanes = new Set();
|
|
7
|
+
let focusedPane;
|
|
7
8
|
const store = new Map();
|
|
8
9
|
const faked = (call, answer) => {
|
|
9
10
|
return (...args) => {
|
|
@@ -41,6 +42,8 @@ export function fakeClaude(plugin) {
|
|
|
41
42
|
if (answer.isPlaced) {
|
|
42
43
|
shown.openPanes.add(pane.id);
|
|
43
44
|
unplacedPanes.delete(pane.id);
|
|
45
|
+
if (pane.focus === true)
|
|
46
|
+
focusedPane = pane.id;
|
|
44
47
|
}
|
|
45
48
|
else if (!shown.openPanes.has(pane.id)) {
|
|
46
49
|
unplacedPanes.add(pane.id);
|
|
@@ -51,11 +54,14 @@ export function fakeClaude(plugin) {
|
|
|
51
54
|
calls.push({ call: 'ui.close', args: [pane] });
|
|
52
55
|
shown.openPanes.delete(pane.id);
|
|
53
56
|
unplacedPanes.delete(pane.id);
|
|
57
|
+
if (focusedPane === pane.id)
|
|
58
|
+
focusedPane = undefined;
|
|
54
59
|
},
|
|
55
60
|
panes: async () => [
|
|
56
|
-
...[...shown.openPanes].map((id) => ({ id, title: id, isShown: true, isFocused:
|
|
61
|
+
...[...shown.openPanes].map((id) => ({ id, title: id, isShown: true, isFocused: id === focusedPane, isPlaced: true })),
|
|
57
62
|
...[...unplacedPanes].map((id) => ({ id, title: id, isShown: false, isFocused: false, isPlaced: false })),
|
|
58
63
|
],
|
|
64
|
+
scroll: rejected('ui.scroll', () => fakes.ui.scroll ?? (async () => ({}))),
|
|
59
65
|
resolve: () => elements,
|
|
60
66
|
},
|
|
61
67
|
process: {
|
|
@@ -71,7 +77,10 @@ export function fakeClaude(plugin) {
|
|
|
71
77
|
},
|
|
72
78
|
http: { fetch: rejected('http.fetch', () => fakes.http.fetch) },
|
|
73
79
|
settings: { read: rejected('settings.read', () => fakes.settings.read) },
|
|
74
|
-
agent: {
|
|
80
|
+
agent: {
|
|
81
|
+
list: rejected('agent.list', () => fakes.agent.list),
|
|
82
|
+
spawn: rejected('agent.spawn', () => fakes.agent.spawn),
|
|
83
|
+
},
|
|
75
84
|
store: {
|
|
76
85
|
get: async (key) => store.get(key),
|
|
77
86
|
set: async (key, value) => {
|
|
@@ -94,6 +103,7 @@ export function fakeClaude(plugin) {
|
|
|
94
103
|
model: async () => 'test-model',
|
|
95
104
|
usage: async () => ({ context: { window: 200000 }, rateLimits: {}, cost: { usd: 0 } }),
|
|
96
105
|
surfaces: async () => ['terminal'],
|
|
106
|
+
messages: rejected('session.messages', () => fakes.session.messages),
|
|
97
107
|
},
|
|
98
108
|
command: {
|
|
99
109
|
register: async (command) => {
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import type { ElementTable, RenderNode } from 'claude-code';
|
|
2
|
-
type
|
|
3
|
-
readonly type:
|
|
4
|
-
readonly props:
|
|
5
|
-
|
|
6
|
-
readonly label: string;
|
|
7
|
-
readonly onPress: () => unknown;
|
|
8
|
-
};
|
|
2
|
+
type Drawn = {
|
|
3
|
+
readonly type: string;
|
|
4
|
+
readonly props: Record<string, unknown>;
|
|
5
|
+
readonly children?: readonly RenderNode[];
|
|
9
6
|
};
|
|
10
7
|
export declare const elements: ElementTable;
|
|
11
|
-
export declare function
|
|
8
|
+
export declare function findElement(node: RenderNode, type: string, key: string): Drawn['props'] | undefined;
|
|
12
9
|
export declare function rowsOf(node: RenderNode, width: number): string[];
|
|
13
10
|
export declare function textOf(node: RenderNode): string;
|
|
14
11
|
export {};
|