agent-facets 0.1.2 → 0.1.4
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/.package.json.bak +44 -0
- package/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +16 -0
- package/dist/facet +0 -0
- package/package.json +7 -4
- package/src/__tests__/cli.test.ts +69 -26
- package/src/__tests__/create-build.test.ts +32 -12
- package/src/__tests__/edit-integration.test.ts +155 -0
- package/src/__tests__/resolve-dir.test.ts +95 -0
- package/src/commands/build.ts +17 -4
- package/src/commands/create/index.ts +51 -5
- package/src/commands/create/wizard.tsx +66 -15
- package/src/commands/create-scaffold.ts +14 -10
- package/src/commands/edit/index.ts +144 -0
- package/src/commands/edit/wizard.tsx +74 -0
- package/src/commands/resolve-dir.ts +98 -0
- package/src/commands.ts +11 -2
- package/src/help.ts +17 -10
- package/src/index.ts +2 -1
- package/src/run.ts +32 -5
- package/src/tui/components/asset-description.tsx +17 -0
- package/src/tui/components/asset-field-picker.tsx +78 -0
- package/src/tui/components/asset-inline-input.tsx +13 -1
- package/src/tui/components/asset-item.tsx +3 -7
- package/src/tui/components/asset-section.tsx +72 -26
- package/src/tui/components/reconciliation-item.tsx +129 -0
- package/src/tui/components/stage-row.tsx +16 -4
- package/src/tui/context/focus-order-context.ts +8 -2
- package/src/tui/context/form-state-context.ts +34 -3
- package/src/tui/editor.ts +40 -0
- package/src/tui/views/build/build-view.tsx +43 -44
- package/src/tui/views/create/create-view.tsx +17 -13
- package/src/tui/views/create/wizard.tsx +35 -6
- package/src/tui/views/edit/edit-confirm-view.tsx +93 -0
- package/src/tui/views/edit/edit-types.ts +34 -0
- package/src/tui/views/edit/edit-view.tsx +140 -0
- package/src/tui/views/edit/manifest-to-form.ts +38 -0
- package/src/tui/views/edit/reconciliation-view.tsx +170 -0
- package/src/tui/views/edit/use-edit-session.ts +125 -0
- package/src/tui/views/edit/wizard.tsx +129 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Box, Text } from 'ink'
|
|
2
|
+
import { useCallback, useEffect, useMemo } from 'react'
|
|
3
|
+
import { Button } from '../../components/button.tsx'
|
|
4
|
+
import { ReconciliationItemRow } from '../../components/reconciliation-item.tsx'
|
|
5
|
+
import { useFocusOrder } from '../../context/focus-order-context.ts'
|
|
6
|
+
import { THEME } from '../../theme.ts'
|
|
7
|
+
import type { ReconciliationItem, ReconciliationResolution } from './edit-types.ts'
|
|
8
|
+
|
|
9
|
+
/** Maps a reconciliation item to a unique key. */
|
|
10
|
+
function itemKey(item: ReconciliationItem): string {
|
|
11
|
+
return `${item.kind}:${item.type}:${item.name}`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Returns the two action options for a reconciliation item kind. */
|
|
15
|
+
function optionsForKind(kind: ReconciliationItem['kind']): [{ label: string }, { label: string }] {
|
|
16
|
+
switch (kind) {
|
|
17
|
+
case 'addition':
|
|
18
|
+
return [{ label: 'Add to manifest' }, { label: 'Ignore for now' }]
|
|
19
|
+
case 'missing':
|
|
20
|
+
return [{ label: 'Scaffold template' }, { label: 'Remove from manifest' }]
|
|
21
|
+
case 'front-matter':
|
|
22
|
+
return [{ label: 'Strip front matter' }, { label: 'Remove from manifest' }]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Converts a selected option index to a resolution for the given item kind. */
|
|
27
|
+
function indexToResolution(kind: ReconciliationItem['kind'], index: number): ReconciliationResolution {
|
|
28
|
+
switch (kind) {
|
|
29
|
+
case 'addition':
|
|
30
|
+
return index === 0 ? { action: 'add-to-manifest' } : { action: 'ignore' }
|
|
31
|
+
case 'missing':
|
|
32
|
+
return index === 0 ? { action: 'scaffold-template' } : { action: 'remove-from-manifest' }
|
|
33
|
+
case 'front-matter':
|
|
34
|
+
return index === 0 ? { action: 'strip-front-matter' } : { action: 'remove-from-manifest' }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Returns the selected option index for a resolution, or null. */
|
|
39
|
+
function resolutionToIndex(
|
|
40
|
+
kind: ReconciliationItem['kind'],
|
|
41
|
+
resolution: ReconciliationResolution | undefined,
|
|
42
|
+
): number | null {
|
|
43
|
+
if (!resolution) return null
|
|
44
|
+
switch (kind) {
|
|
45
|
+
case 'addition':
|
|
46
|
+
return resolution.action === 'add-to-manifest' ? 0 : resolution.action === 'ignore' ? 1 : null
|
|
47
|
+
case 'missing':
|
|
48
|
+
return resolution.action === 'scaffold-template' ? 0 : resolution.action === 'remove-from-manifest' ? 1 : null
|
|
49
|
+
case 'front-matter':
|
|
50
|
+
return resolution.action === 'strip-front-matter' ? 0 : resolution.action === 'remove-from-manifest' ? 1 : null
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function ReconciliationView({
|
|
55
|
+
items,
|
|
56
|
+
resolutions,
|
|
57
|
+
onResolve,
|
|
58
|
+
onContinue,
|
|
59
|
+
}: {
|
|
60
|
+
items: ReconciliationItem[]
|
|
61
|
+
resolutions: Map<string, ReconciliationResolution>
|
|
62
|
+
onResolve: (key: string, resolution: ReconciliationResolution) => void
|
|
63
|
+
onContinue: () => void
|
|
64
|
+
}) {
|
|
65
|
+
const { setFocusIds, focusedId, focus } = useFocusOrder()
|
|
66
|
+
|
|
67
|
+
const allResolved = items.every((item) => resolutions.has(itemKey(item)))
|
|
68
|
+
|
|
69
|
+
// Group items by kind
|
|
70
|
+
const additions = items.filter((i) => i.kind === 'addition')
|
|
71
|
+
const missing = items.filter((i) => i.kind === 'missing')
|
|
72
|
+
const frontMatter = items.filter((i) => i.kind === 'front-matter')
|
|
73
|
+
|
|
74
|
+
// Build focus order: all items then continue button
|
|
75
|
+
const focusIds = useMemo(() => {
|
|
76
|
+
const ids = items.map((item) => `recon-${itemKey(item)}`)
|
|
77
|
+
ids.push('recon-continue')
|
|
78
|
+
return ids
|
|
79
|
+
}, [items])
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
setFocusIds(focusIds)
|
|
83
|
+
if (!focusedId) {
|
|
84
|
+
focus(focusIds[0] ?? '')
|
|
85
|
+
}
|
|
86
|
+
}, [focusIds, setFocusIds, focusedId, focus])
|
|
87
|
+
|
|
88
|
+
const handleSelect = useCallback(
|
|
89
|
+
(item: ReconciliationItem, optionIndex: number) => {
|
|
90
|
+
const key = itemKey(item)
|
|
91
|
+
const resolution = indexToResolution(item.kind, optionIndex)
|
|
92
|
+
onResolve(key, resolution)
|
|
93
|
+
|
|
94
|
+
// Auto-advance to next unresolved item
|
|
95
|
+
const currentIdx = items.findIndex((i) => itemKey(i) === key)
|
|
96
|
+
for (let i = currentIdx + 1; i < items.length; i++) {
|
|
97
|
+
const nextItem = items[i]
|
|
98
|
+
if (!nextItem) continue
|
|
99
|
+
const nextKey = itemKey(nextItem)
|
|
100
|
+
if (!resolutions.has(nextKey)) {
|
|
101
|
+
focus(`recon-${nextKey}`)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// All resolved — focus continue button
|
|
106
|
+
focus('recon-continue')
|
|
107
|
+
},
|
|
108
|
+
[items, resolutions, onResolve, focus],
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
const renderGroup = (label: string, groupItems: ReconciliationItem[]) => {
|
|
112
|
+
if (groupItems.length === 0) return null
|
|
113
|
+
return (
|
|
114
|
+
<Box flexDirection="column" key={label}>
|
|
115
|
+
<Box marginBottom={0}>
|
|
116
|
+
<Text bold color={THEME.warning}>
|
|
117
|
+
{label}
|
|
118
|
+
</Text>
|
|
119
|
+
</Box>
|
|
120
|
+
{groupItems.map((item) => {
|
|
121
|
+
const key = itemKey(item)
|
|
122
|
+
const description =
|
|
123
|
+
item.kind === 'missing'
|
|
124
|
+
? `${item.name} (${item.type}) — ${item.expectedPath}`
|
|
125
|
+
: 'path' in item
|
|
126
|
+
? item.path
|
|
127
|
+
: ''
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<ReconciliationItemRow
|
|
131
|
+
key={key}
|
|
132
|
+
id={`recon-${key}`}
|
|
133
|
+
description={description}
|
|
134
|
+
options={optionsForKind(item.kind)}
|
|
135
|
+
selectedIndex={resolutionToIndex(item.kind, resolutions.get(key))}
|
|
136
|
+
onSelect={(index) => handleSelect(item, index)}
|
|
137
|
+
/>
|
|
138
|
+
)
|
|
139
|
+
})}
|
|
140
|
+
</Box>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<Box flexDirection="column" padding={1} gap={1}>
|
|
146
|
+
<Text bold color={THEME.brand}>
|
|
147
|
+
Reconciliation — {items.length} item{items.length !== 1 ? 's' : ''} to resolve
|
|
148
|
+
</Text>
|
|
149
|
+
|
|
150
|
+
{renderGroup('New files on disk:', additions)}
|
|
151
|
+
{renderGroup('Missing from disk:', missing)}
|
|
152
|
+
{renderGroup('Front matter detected:', frontMatter)}
|
|
153
|
+
|
|
154
|
+
<Box marginTop={1}>
|
|
155
|
+
<Button
|
|
156
|
+
id="recon-continue"
|
|
157
|
+
label="[ Continue to edit ]"
|
|
158
|
+
disabled={!allResolved}
|
|
159
|
+
gradient={allResolved}
|
|
160
|
+
animateGradient={allResolved && focusedId === 'recon-continue'}
|
|
161
|
+
onPress={onContinue}
|
|
162
|
+
/>
|
|
163
|
+
</Box>
|
|
164
|
+
|
|
165
|
+
<Box>
|
|
166
|
+
<Text dimColor>↑ ↓ navigate · ← → switch option · Enter select · Esc Esc exit</Text>
|
|
167
|
+
</Box>
|
|
168
|
+
</Box>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { FacetManifest } from '@agent-facets/core'
|
|
2
|
+
import { useCallback, useState } from 'react'
|
|
3
|
+
import type { AssetSectionKey, FormState } from '../../context/form-state-context.ts'
|
|
4
|
+
import type { EditContext, EditOperation, EditResult, ReconciliationResolution } from './edit-types.ts'
|
|
5
|
+
|
|
6
|
+
/** Maps form section keys to manifest asset keys. */
|
|
7
|
+
const FORM_TO_MANIFEST: Record<AssetSectionKey, 'skills' | 'agents' | 'commands'> = {
|
|
8
|
+
skill: 'skills',
|
|
9
|
+
agent: 'agents',
|
|
10
|
+
command: 'commands',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Builds a manifest from form state, preserving non-asset fields from the original. */
|
|
14
|
+
function buildManifest(original: FacetManifest, form: FormState): FacetManifest {
|
|
15
|
+
const manifest: FacetManifest = {
|
|
16
|
+
...original,
|
|
17
|
+
name: form.fields.name.value,
|
|
18
|
+
version: form.fields.version.value,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (form.fields.description.value) {
|
|
22
|
+
manifest.description = form.fields.description.value
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const [formKey, manifestKey] of Object.entries(FORM_TO_MANIFEST) as [
|
|
26
|
+
AssetSectionKey,
|
|
27
|
+
'skills' | 'agents' | 'commands',
|
|
28
|
+
][]) {
|
|
29
|
+
const items = form.assets[formKey].items
|
|
30
|
+
if (items.length > 0) {
|
|
31
|
+
const section: Record<string, { description: string }> = {}
|
|
32
|
+
for (const name of items) {
|
|
33
|
+
section[name] = { description: form.assets[formKey].descriptions[name] ?? '' }
|
|
34
|
+
}
|
|
35
|
+
manifest[manifestKey] = section
|
|
36
|
+
} else {
|
|
37
|
+
delete manifest[manifestKey]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return manifest
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Builds the list of file operations from resolutions + form changes. */
|
|
45
|
+
function buildOperations(
|
|
46
|
+
context: EditContext,
|
|
47
|
+
form: FormState,
|
|
48
|
+
resolutions: Map<string, ReconciliationResolution>,
|
|
49
|
+
): EditOperation[] {
|
|
50
|
+
const operations: EditOperation[] = [{ op: 'write-manifest' }]
|
|
51
|
+
|
|
52
|
+
// Operations from reconciliation resolutions
|
|
53
|
+
for (const [key, resolution] of resolutions) {
|
|
54
|
+
const parts = key.split(':')
|
|
55
|
+
const kind = parts[0]
|
|
56
|
+
const assetType = parts[1] as 'skills' | 'agents' | 'commands'
|
|
57
|
+
const name = parts[2]
|
|
58
|
+
if (!kind || !assetType || !name) continue
|
|
59
|
+
|
|
60
|
+
if (resolution.action === 'scaffold-template') {
|
|
61
|
+
operations.push({ op: 'scaffold', type: assetType, name })
|
|
62
|
+
} else if (resolution.action === 'remove-from-manifest' && kind === 'front-matter') {
|
|
63
|
+
operations.push({ op: 'delete-file', type: assetType, name })
|
|
64
|
+
} else if (resolution.action === 'strip-front-matter') {
|
|
65
|
+
const item = context.reconciliationItems.find(
|
|
66
|
+
(i) => i.kind === 'front-matter' && i.type === assetType && i.name === name,
|
|
67
|
+
)
|
|
68
|
+
if (item && 'path' in item) {
|
|
69
|
+
operations.push({ op: 'strip-front-matter', type: assetType, name, path: item.path })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// New assets added during editing (not from reconciliation)
|
|
75
|
+
for (const [formKey, manifestKey] of Object.entries(FORM_TO_MANIFEST) as [
|
|
76
|
+
AssetSectionKey,
|
|
77
|
+
'skills' | 'agents' | 'commands',
|
|
78
|
+
][]) {
|
|
79
|
+
const originalSection = context.manifest[manifestKey]
|
|
80
|
+
const originalNames =
|
|
81
|
+
originalSection && typeof originalSection === 'object' && !Array.isArray(originalSection)
|
|
82
|
+
? Object.keys(originalSection)
|
|
83
|
+
: []
|
|
84
|
+
|
|
85
|
+
for (const name of form.assets[formKey].items) {
|
|
86
|
+
const isFromReconciliation = resolutions.has(`addition:${manifestKey}:${name}`)
|
|
87
|
+
if (!originalNames.includes(name) && !isFromReconciliation) {
|
|
88
|
+
operations.push({ op: 'scaffold', type: manifestKey, name })
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Removed assets
|
|
93
|
+
for (const name of originalNames) {
|
|
94
|
+
if (!form.assets[formKey].items.includes(name)) {
|
|
95
|
+
operations.push({ op: 'delete-file', type: manifestKey, name })
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return operations
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function useEditSession(context: EditContext) {
|
|
104
|
+
const [resolutions, setResolutions] = useState<Map<string, ReconciliationResolution>>(new Map())
|
|
105
|
+
|
|
106
|
+
const resolve = useCallback((key: string, resolution: ReconciliationResolution) => {
|
|
107
|
+
setResolutions((prev) => {
|
|
108
|
+
const next = new Map(prev)
|
|
109
|
+
next.set(key, resolution)
|
|
110
|
+
return next
|
|
111
|
+
})
|
|
112
|
+
}, [])
|
|
113
|
+
|
|
114
|
+
/** Builds the final edit result from current form state and resolutions. */
|
|
115
|
+
const buildResult = useCallback(
|
|
116
|
+
(form: FormState): EditResult => {
|
|
117
|
+
const manifest = buildManifest(context.manifest, form)
|
|
118
|
+
const operations = buildOperations(context, form, resolutions)
|
|
119
|
+
return { outcome: 'applied', manifest, operations }
|
|
120
|
+
},
|
|
121
|
+
[context, resolutions],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return { resolutions, resolve, buildResult }
|
|
125
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { useApp } from 'ink'
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
3
|
+
import { FocusModeProvider, useFocusMode } from '../../context/focus-mode-context.ts'
|
|
4
|
+
import { FocusOrderProvider, useFocusOrder } from '../../context/focus-order-context.ts'
|
|
5
|
+
import type { AssetSectionKey, FormState } from '../../context/form-state-context.ts'
|
|
6
|
+
import { FormStateProvider, useFormState } from '../../context/form-state-context.ts'
|
|
7
|
+
import { useExitKeys } from '../../hooks/use-exit-keys.ts'
|
|
8
|
+
import { useNavigationKeys } from '../../hooks/use-navigation-keys.ts'
|
|
9
|
+
import { EditConfirmView } from './edit-confirm-view.tsx'
|
|
10
|
+
import type { EditContext, EditResult, ReconciliationResolution } from './edit-types.ts'
|
|
11
|
+
import { EditView } from './edit-view.tsx'
|
|
12
|
+
import { manifestToFormState } from './manifest-to-form.ts'
|
|
13
|
+
import { ReconciliationView } from './reconciliation-view.tsx'
|
|
14
|
+
import { useEditSession } from './use-edit-session.ts'
|
|
15
|
+
|
|
16
|
+
type EditPhase = 'reconciliation' | 'editing' | 'confirmation'
|
|
17
|
+
|
|
18
|
+
export interface EditWizardSnapshot {
|
|
19
|
+
phase: EditPhase
|
|
20
|
+
formState?: FormState
|
|
21
|
+
focusedId?: string | null
|
|
22
|
+
resolutions: Map<string, ReconciliationResolution>
|
|
23
|
+
selectedItem?: {
|
|
24
|
+
section: AssetSectionKey
|
|
25
|
+
name: string
|
|
26
|
+
field: 'name' | 'description'
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface EditWizardProps {
|
|
31
|
+
context: EditContext
|
|
32
|
+
snapshot?: EditWizardSnapshot
|
|
33
|
+
onComplete: (result: EditResult) => void
|
|
34
|
+
onSnapshot?: (snapshot: EditWizardSnapshot) => void
|
|
35
|
+
onRequestEditor?: (section: AssetSectionKey, name: string, description: string) => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function EditWizardInner({ context, snapshot, onComplete, onSnapshot, onRequestEditor }: EditWizardProps) {
|
|
39
|
+
const { exit } = useApp()
|
|
40
|
+
const { setMode } = useFocusMode()
|
|
41
|
+
const { form } = useFormState()
|
|
42
|
+
const { focusedId, focus } = useFocusOrder()
|
|
43
|
+
const hasReconciliation = context.reconciliationItems.length > 0
|
|
44
|
+
|
|
45
|
+
const initialPhase = snapshot?.phase ?? (hasReconciliation ? 'reconciliation' : 'editing')
|
|
46
|
+
const [phase, setPhase] = useState<EditPhase>(initialPhase)
|
|
47
|
+
const { resolutions, resolve, buildResult } = useEditSession(context)
|
|
48
|
+
|
|
49
|
+
// Report snapshot to parent for editor round-trips
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
onSnapshot?.({ phase, formState: form, focusedId, resolutions })
|
|
52
|
+
}, [phase, form, focusedId, resolutions, onSnapshot])
|
|
53
|
+
|
|
54
|
+
const cancel = useCallback(() => {
|
|
55
|
+
onComplete({ outcome: 'cancelled' })
|
|
56
|
+
exit()
|
|
57
|
+
}, [onComplete, exit])
|
|
58
|
+
|
|
59
|
+
useExitKeys(cancel)
|
|
60
|
+
useNavigationKeys()
|
|
61
|
+
|
|
62
|
+
const handleEditDescription = useCallback(
|
|
63
|
+
(section: AssetSectionKey, name: string) => {
|
|
64
|
+
const description = form.assets[section].descriptions[name] ?? ''
|
|
65
|
+
onRequestEditor?.(section, name, description)
|
|
66
|
+
},
|
|
67
|
+
[form, onRequestEditor],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const handleConfirm = useCallback(() => {
|
|
71
|
+
onComplete(buildResult(form))
|
|
72
|
+
exit()
|
|
73
|
+
}, [form, buildResult, onComplete, exit])
|
|
74
|
+
|
|
75
|
+
if (phase === 'reconciliation') {
|
|
76
|
+
return (
|
|
77
|
+
<ReconciliationView
|
|
78
|
+
items={context.reconciliationItems}
|
|
79
|
+
resolutions={resolutions}
|
|
80
|
+
onResolve={resolve}
|
|
81
|
+
onContinue={() => setPhase('editing')}
|
|
82
|
+
/>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (phase === 'editing') {
|
|
87
|
+
return (
|
|
88
|
+
<EditView
|
|
89
|
+
onSubmit={() => {
|
|
90
|
+
setPhase('confirmation')
|
|
91
|
+
setMode('form-confirmation')
|
|
92
|
+
}}
|
|
93
|
+
onEditDescription={handleEditDescription}
|
|
94
|
+
/>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (phase === 'confirmation') {
|
|
99
|
+
return (
|
|
100
|
+
<EditConfirmView
|
|
101
|
+
onConfirm={handleConfirm}
|
|
102
|
+
onBack={() => {
|
|
103
|
+
setPhase('editing')
|
|
104
|
+
setMode('form-navigation')
|
|
105
|
+
focus('edit-confirm-btn')
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function EditWizard(props: EditWizardProps) {
|
|
115
|
+
const initialFormState = useMemo(
|
|
116
|
+
() => props.snapshot?.formState ?? manifestToFormState(props.context.manifest),
|
|
117
|
+
[props.snapshot?.formState, props.context.manifest],
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<FocusModeProvider>
|
|
122
|
+
<FocusOrderProvider initialFocusId={props.snapshot?.focusedId}>
|
|
123
|
+
<FormStateProvider initialState={initialFormState}>
|
|
124
|
+
<EditWizardInner {...props} />
|
|
125
|
+
</FormStateProvider>
|
|
126
|
+
</FocusOrderProvider>
|
|
127
|
+
</FocusModeProvider>
|
|
128
|
+
)
|
|
129
|
+
}
|