agent-facets 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/.package.json.bak +44 -0
  2. package/.turbo/turbo-build.log +2 -2
  3. package/CHANGELOG.md +33 -0
  4. package/dist/facet +0 -0
  5. package/package.json +7 -4
  6. package/src/__tests__/cli.test.ts +69 -26
  7. package/src/__tests__/create-build.test.ts +32 -12
  8. package/src/__tests__/edit-integration.test.ts +171 -0
  9. package/src/__tests__/resolve-dir.test.ts +95 -0
  10. package/src/commands/build.ts +17 -4
  11. package/src/commands/create/index.ts +51 -5
  12. package/src/commands/create/wizard.tsx +66 -15
  13. package/src/commands/create-scaffold.ts +14 -10
  14. package/src/commands/edit/index.ts +144 -0
  15. package/src/commands/edit/wizard.tsx +74 -0
  16. package/src/commands/resolve-dir.ts +98 -0
  17. package/src/commands.ts +11 -2
  18. package/src/help.ts +17 -10
  19. package/src/index.ts +2 -1
  20. package/src/run.ts +32 -5
  21. package/src/tui/components/asset-description.tsx +17 -0
  22. package/src/tui/components/asset-field-picker.tsx +78 -0
  23. package/src/tui/components/asset-inline-input.tsx +13 -1
  24. package/src/tui/components/asset-item.tsx +3 -7
  25. package/src/tui/components/asset-section.tsx +72 -26
  26. package/src/tui/components/reconciliation-item.tsx +129 -0
  27. package/src/tui/components/stage-row.tsx +16 -4
  28. package/src/tui/context/focus-order-context.ts +8 -2
  29. package/src/tui/context/form-state-context.ts +34 -3
  30. package/src/tui/editor.ts +40 -0
  31. package/src/tui/views/build/build-view.tsx +43 -44
  32. package/src/tui/views/create/create-view.tsx +17 -13
  33. package/src/tui/views/create/wizard.tsx +35 -6
  34. package/src/tui/views/edit/edit-confirm-view.tsx +93 -0
  35. package/src/tui/views/edit/edit-types.ts +34 -0
  36. package/src/tui/views/edit/edit-view.tsx +140 -0
  37. package/src/tui/views/edit/manifest-to-form.ts +38 -0
  38. package/src/tui/views/edit/reconciliation-view.tsx +170 -0
  39. package/src/tui/views/edit/use-edit-session.ts +125 -0
  40. package/src/tui/views/edit/wizard.tsx +129 -0
package/src/run.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { parse } from '@bomb.sh/args'
2
- import { commands } from './commands.ts'
2
+ import type { Command } from './commands.ts'
3
3
  import { printCommandHelp, printGlobalHelp } from './help.ts'
4
4
  import { findClosestCommand } from './suggest.ts'
5
5
  import { version } from './version.ts'
6
6
 
7
- export async function run(argv: string[]): Promise<number> {
7
+ export async function run(argv: string[], commands: Record<string, Command>): Promise<number> {
8
8
  const args = parse(argv, {
9
9
  boolean: ['help', 'version'],
10
10
  })
@@ -18,7 +18,7 @@ export async function run(argv: string[]): Promise<number> {
18
18
 
19
19
  // No command given — show global help
20
20
  if (!commandName) {
21
- printGlobalHelp()
21
+ printGlobalHelp(commands)
22
22
  return 0
23
23
  }
24
24
 
@@ -29,7 +29,7 @@ export async function run(argv: string[]): Promise<number> {
29
29
  if (subCommand) {
30
30
  printCommandHelp(subCommand)
31
31
  } else {
32
- printGlobalHelp()
32
+ printGlobalHelp(commands)
33
33
  }
34
34
  return 0
35
35
  }
@@ -51,5 +51,32 @@ export async function run(argv: string[]): Promise<number> {
51
51
  return 0
52
52
  }
53
53
 
54
- return command.run(argv.slice(1))
54
+ // Build per-command flag parsing config
55
+ const booleanFlags: string[] = []
56
+ const stringFlags: string[] = []
57
+
58
+ if (command.flags) {
59
+ for (const [name, def] of Object.entries(command.flags)) {
60
+ if (def.type === 'boolean') booleanFlags.push(name)
61
+ else if (def.type === 'string') stringFlags.push(name)
62
+ }
63
+ }
64
+
65
+ // Parse with per-command config
66
+ const parsed = parse(argv.slice(1), {
67
+ boolean: booleanFlags,
68
+ string: stringFlags,
69
+ })
70
+
71
+ // Build positional args and flags
72
+ const positionalArgs = parsed._.map(String)
73
+ const flags: Record<string, unknown> = {}
74
+
75
+ for (const name of [...booleanFlags, ...stringFlags]) {
76
+ if (parsed[name] !== undefined) {
77
+ flags[name] = parsed[name]
78
+ }
79
+ }
80
+
81
+ return command.run(positionalArgs, flags)
55
82
  }
@@ -0,0 +1,17 @@
1
+ import { Box, Text } from 'ink'
2
+
3
+ export function truncateDescription(text: string, maxLen = 50): string {
4
+ const lines = text.split(/\r?\n/)
5
+ const firstLine = lines[0] ?? text
6
+ const hasMore = lines.length > 1
7
+ if (firstLine.length <= maxLen) return hasMore ? `${firstLine}...` : firstLine
8
+ return `${firstLine.slice(0, maxLen)}...`
9
+ }
10
+
11
+ export function AssetDescription({ description }: { description: string }) {
12
+ return (
13
+ <Box marginLeft={4}>
14
+ <Text dimColor>{truncateDescription(description)}</Text>
15
+ </Box>
16
+ )
17
+ }
@@ -0,0 +1,78 @@
1
+ import { Box, Text, useInput } from 'ink'
2
+ import { useEffect, useState } from 'react'
3
+ import { useFocusMode } from '../context/focus-mode-context.ts'
4
+ import { THEME } from '../theme.ts'
5
+
6
+ export type AssetField = 'name' | 'description'
7
+
8
+ export function AssetFieldPicker({
9
+ name,
10
+ description,
11
+ initialField,
12
+ onChoose,
13
+ onCancel,
14
+ }: {
15
+ name: string
16
+ description: string
17
+ initialField?: AssetField
18
+ onChoose: (field: AssetField) => void
19
+ onCancel: () => void
20
+ }) {
21
+ const { setMode } = useFocusMode()
22
+ const [field, setField] = useState<AssetField>(initialField ?? 'name')
23
+
24
+ useEffect(() => {
25
+ setMode('field-revision')
26
+ return () => setMode('form-navigation')
27
+ }, [setMode])
28
+
29
+ useInput((_input, key) => {
30
+ if (key.upArrow) setField('name')
31
+ if (key.downArrow) setField('description')
32
+ if (key.escape) onCancel()
33
+ if (key.return) onChoose(field)
34
+ })
35
+
36
+ return (
37
+ <Box flexDirection="column">
38
+ <Box gap={1} marginLeft={2}>
39
+ {field === 'name' ? (
40
+ <>
41
+ <Text color={THEME.primary} bold>
42
+
43
+ </Text>
44
+ <Text color={THEME.primary}>{name}</Text>
45
+ <Text color={THEME.hint}>
46
+ <Text color={THEME.keyword}>↑↓</Text> select · <Text color={THEME.keyword}>Enter</Text> edit ·{' '}
47
+ <Text color={THEME.keyword}>Esc</Text> back
48
+ </Text>
49
+ </>
50
+ ) : (
51
+ <>
52
+ <Text color={THEME.success}>•</Text>
53
+ <Text>{name}</Text>
54
+ </>
55
+ )}
56
+ </Box>
57
+ <Box gap={1} marginLeft={2}>
58
+ {field === 'description' ? (
59
+ <>
60
+ <Text color={THEME.primary} bold>
61
+
62
+ </Text>
63
+ <Text color={THEME.primary}>{description}</Text>
64
+ <Text color={THEME.hint}>
65
+ <Text color={THEME.keyword}>↑↓</Text> select · <Text color={THEME.keyword}>Enter</Text> edit ·{' '}
66
+ <Text color={THEME.keyword}>Esc</Text> back
67
+ </Text>
68
+ </>
69
+ ) : (
70
+ <>
71
+ <Text> </Text>
72
+ <Text dimColor>{description}</Text>
73
+ </>
74
+ )}
75
+ </Box>
76
+ </Box>
77
+ )
78
+ }
@@ -12,6 +12,7 @@ export function AssetInlineInput({
12
12
  onError,
13
13
  onSubmit,
14
14
  onCancel,
15
+ onDownArrow,
15
16
  }: {
16
17
  id: string
17
18
  value: string
@@ -23,6 +24,7 @@ export function AssetInlineInput({
23
24
  onError: (error: string) => void
24
25
  onSubmit: (name: string) => void
25
26
  onCancel: () => void
27
+ onDownArrow?: () => void
26
28
  }) {
27
29
  useInput(
28
30
  (_input, key) => {
@@ -47,6 +49,10 @@ export function AssetInlineInput({
47
49
  onCancel()
48
50
  return
49
51
  }
52
+ if (key.downArrow && onDownArrow) {
53
+ onDownArrow()
54
+ return
55
+ }
50
56
  if (key.tab && !value && placeholder) {
51
57
  onChange(placeholder)
52
58
  return
@@ -63,7 +69,13 @@ export function AssetInlineInput({
63
69
  <Text color={THEME.warning}>· {error}</Text>
64
70
  ) : value ? (
65
71
  <Text color={THEME.hint}>
66
- · <Text color={THEME.keyword}>Enter</Text> to save · <Text color={THEME.keyword}>Escape</Text> to revert
72
+ · <Text color={THEME.keyword}>Enter</Text> save ·{' '}
73
+ {onDownArrow && (
74
+ <>
75
+ <Text color={THEME.keyword}>↓</Text> description ·{' '}
76
+ </>
77
+ )}
78
+ <Text color={THEME.keyword}>Esc</Text> revert
67
79
  </Text>
68
80
  ) : placeholder ? (
69
81
  <Text color={THEME.hint}>
@@ -15,12 +15,8 @@ export function AssetItem({
15
15
  }) {
16
16
  useInput(
17
17
  (_input, key) => {
18
- if (key.return) {
19
- onEdit()
20
- }
21
- if (key.delete || key.backspace) {
22
- onRemove()
23
- }
18
+ if (key.return) onEdit()
19
+ if (key.delete || key.backspace) onRemove()
24
20
  },
25
21
  { isActive: isFocused },
26
22
  )
@@ -34,7 +30,7 @@ export function AssetItem({
34
30
  </Text>
35
31
  <Text color={THEME.primary}>{name}</Text>
36
32
  <Text color={THEME.hint}>
37
- <Text color={THEME.keyword}>Enter</Text> to edit · <Text color={THEME.keyword}>Delete</Text> to remove
33
+ <Text color={THEME.keyword}>Enter</Text> edit name · <Text color={THEME.keyword}>Del</Text> remove
38
34
  </Text>
39
35
  </>
40
36
  ) : (
@@ -4,6 +4,9 @@ import { useFocusMode } from '../context/focus-mode-context.ts'
4
4
  import { useFocusOrder } from '../context/focus-order-context.ts'
5
5
  import type { AssetSectionKey } from '../context/form-state-context.ts'
6
6
  import { useFormState } from '../context/form-state-context.ts'
7
+ import { AssetDescription, truncateDescription } from './asset-description.tsx'
8
+ import type { AssetField } from './asset-field-picker.tsx'
9
+ import { AssetFieldPicker } from './asset-field-picker.tsx'
7
10
  import { AssetInlineInput } from './asset-inline-input.tsx'
8
11
  import { AssetItem } from './asset-item.tsx'
9
12
  import { Button } from './button.tsx'
@@ -14,19 +17,22 @@ export function AssetSection({
14
17
  defaultName,
15
18
  dimmed,
16
19
  validate,
20
+ onEditDescription,
17
21
  }: {
18
22
  section: AssetSectionKey
19
23
  label: string
20
24
  defaultName?: string
21
25
  dimmed?: boolean
22
26
  validate?: (value: string) => string | undefined
27
+ onEditDescription?: (section: AssetSectionKey, name: string) => void
23
28
  }) {
24
29
  const { form, addAsset, removeAsset, renameAsset, setAssetAdding, setAssetEditing } = useFormState()
25
- const { items, editing, adding } = form.assets[section]
30
+ const { items, descriptions, editing, adding } = form.assets[section]
26
31
  const { setMode } = useFocusMode()
27
32
  const { focusedId, focus } = useFocusOrder()
28
33
  const [inputValue, setInputValue] = useState('')
29
34
  const [error, setError] = useState('')
35
+ const [selectedItem, setSelectedItem] = useState<string | null>(null)
30
36
 
31
37
  const startAdding = () => {
32
38
  setAssetAdding(section, true)
@@ -42,13 +48,24 @@ export function AssetSection({
42
48
  setMode('field-revision')
43
49
  }
44
50
 
45
- const closeInput = () => {
51
+ const closeInput = (focusTarget?: string | false) => {
46
52
  setAssetAdding(section, false)
47
53
  setAssetEditing(section, undefined)
48
54
  setInputValue('')
49
55
  setError('')
50
- setMode('form-navigation')
51
- focus(`add-${section}`)
56
+ if (focusTarget !== false) {
57
+ setMode('form-navigation')
58
+ focus(focusTarget ?? `add-${section}`)
59
+ }
60
+ }
61
+
62
+ const handleFieldChoice = (name: string, field: AssetField) => {
63
+ setSelectedItem(null)
64
+ if (field === 'name') {
65
+ startEditing(name)
66
+ } else {
67
+ onEditDescription?.(section, name)
68
+ }
52
69
  }
53
70
 
54
71
  const handleRemove = (name: string) => {
@@ -76,37 +93,66 @@ export function AssetSection({
76
93
  {items.map((item, i) => {
77
94
  const itemId = `item-${section}-${i}`
78
95
  const isFocusedItem = focusedId === itemId
96
+ const description = descriptions[item] ?? `A ${item} ${section}`
79
97
 
80
- if (editing === item) {
98
+ // Field picker (entered via ↓ during name editing)
99
+ if (selectedItem === item) {
81
100
  return (
82
- <AssetInlineInput
101
+ <AssetFieldPicker
83
102
  key={itemId}
84
- id={itemId}
85
- value={inputValue}
86
- placeholder={item}
87
- error={error}
88
- isFocused={isFocusedItem}
89
- onChange={setInputValue}
90
- validate={validate}
91
- onError={setError}
92
- onSubmit={(newName) => {
93
- renameAsset(section, item, newName)
94
- closeInput()
103
+ name={item}
104
+ description={truncateDescription(description)}
105
+ initialField="description"
106
+ onChoose={(field) => handleFieldChoice(item, field)}
107
+ onCancel={() => {
108
+ setSelectedItem(null)
109
+ setMode('form-navigation')
110
+ focus(itemId)
95
111
  }}
96
- onCancel={closeInput}
97
112
  />
98
113
  )
99
114
  }
100
115
 
116
+ // Inline name editing
117
+ if (editing === item) {
118
+ return (
119
+ <Box key={itemId} flexDirection="column">
120
+ <AssetInlineInput
121
+ id={itemId}
122
+ value={inputValue}
123
+ placeholder={item}
124
+ error={error}
125
+ isFocused={isFocusedItem}
126
+ onChange={setInputValue}
127
+ validate={validate}
128
+ onError={setError}
129
+ onSubmit={(newName) => {
130
+ renameAsset(section, item, newName)
131
+ closeInput(itemId)
132
+ }}
133
+ onCancel={() => closeInput(itemId)}
134
+ onDownArrow={() => {
135
+ closeInput(false)
136
+ setSelectedItem(item)
137
+ }}
138
+ />
139
+ <AssetDescription description={description} />
140
+ </Box>
141
+ )
142
+ }
143
+
144
+ // Normal display (level 1)
101
145
  return (
102
- <AssetItem
103
- key={itemId}
104
- id={itemId}
105
- name={item}
106
- isFocused={isFocusedItem}
107
- onEdit={() => startEditing(item)}
108
- onRemove={() => handleRemove(item)}
109
- />
146
+ <Box key={itemId} flexDirection="column">
147
+ <AssetItem
148
+ id={itemId}
149
+ name={item}
150
+ isFocused={isFocusedItem}
151
+ onEdit={() => startEditing(item)}
152
+ onRemove={() => handleRemove(item)}
153
+ />
154
+ <AssetDescription description={description} />
155
+ </Box>
110
156
  )
111
157
  })}
112
158
 
@@ -0,0 +1,129 @@
1
+ import { Box, Text, useInput } from 'ink'
2
+ import Gradient from 'ink-gradient'
3
+ import { useEffect, useState } from 'react'
4
+ import { useFocusOrder } from '../context/focus-order-context.ts'
5
+ import { GRADIENT_STOPS, getAnimatedGradient } from '../gradient.ts'
6
+ import { THEME } from '../theme.ts'
7
+
8
+ const ANIMATION_INTERVAL_MS = 75
9
+
10
+ export interface ReconciliationOption {
11
+ label: string
12
+ }
13
+
14
+ export function ReconciliationItemRow({
15
+ id,
16
+ description,
17
+ detail,
18
+ options,
19
+ selectedIndex,
20
+ onSelect,
21
+ }: {
22
+ id: string
23
+ /** Primary text for this item (e.g., file path or asset name) */
24
+ description: string
25
+ /** Optional secondary detail line below the description */
26
+ detail?: string
27
+ /** The two action options */
28
+ options: [ReconciliationOption, ReconciliationOption]
29
+ /** Index of currently selected option, or null if unresolved */
30
+ selectedIndex: number | null
31
+ /** Called when the user locks in an option */
32
+ onSelect: (index: number) => void
33
+ }) {
34
+ const { focusedId } = useFocusOrder()
35
+ const isFocused = focusedId === id
36
+
37
+ // Which option is highlighted (cursor position)
38
+ const [highlightedIndex, setHighlightedIndex] = useState(selectedIndex ?? 0)
39
+ const [offset, setOffset] = useState(0)
40
+
41
+ // Animate gradient when focused
42
+ useEffect(() => {
43
+ if (!isFocused) return
44
+ const interval = setInterval(() => {
45
+ setOffset((prev) => (prev + 1) % GRADIENT_STOPS.length)
46
+ }, ANIMATION_INTERVAL_MS)
47
+ return () => clearInterval(interval)
48
+ }, [isFocused])
49
+
50
+ useInput(
51
+ (_input, key) => {
52
+ if (key.leftArrow) setHighlightedIndex(0)
53
+ if (key.rightArrow) setHighlightedIndex(1)
54
+ if (key.return) onSelect(highlightedIndex)
55
+ },
56
+ { isActive: isFocused },
57
+ )
58
+
59
+ const animatedColors = getAnimatedGradient(offset)
60
+
61
+ return (
62
+ <Box flexDirection="column">
63
+ <Box gap={2}>
64
+ <Box gap={1}>
65
+ {isFocused ? (
66
+ <Text color={THEME.primary} bold>
67
+
68
+ </Text>
69
+ ) : (
70
+ <Text> </Text>
71
+ )}
72
+ <Text color={isFocused ? THEME.primary : undefined}>{description}</Text>
73
+ </Box>
74
+
75
+ <Box gap={2}>
76
+ {options.map((opt, i) => {
77
+ const isSelected = selectedIndex === i
78
+ const isHighlighted = isFocused && highlightedIndex === i
79
+ const isOther = selectedIndex !== null && !isSelected
80
+
81
+ if (isHighlighted) {
82
+ return (
83
+ <Box key={opt.label} gap={1}>
84
+ {isSelected && <Text color={THEME.success}>✓</Text>}
85
+ <Gradient colors={animatedColors}>
86
+ <Text bold>{opt.label}</Text>
87
+ </Gradient>
88
+ </Box>
89
+ )
90
+ }
91
+
92
+ if (isSelected && !isFocused) {
93
+ return (
94
+ <Box key={opt.label} gap={1}>
95
+ <Text color={THEME.success}>✓</Text>
96
+ <Gradient colors={[...THEME.gradient]}>
97
+ <Text bold>{opt.label}</Text>
98
+ </Gradient>
99
+ </Box>
100
+ )
101
+ }
102
+
103
+ if (isOther) {
104
+ return (
105
+ <Text key={opt.label} dimColor>
106
+ {opt.label}
107
+ </Text>
108
+ )
109
+ }
110
+
111
+ // Unresolved, unfocused
112
+ return (
113
+ <Box key={opt.label} gap={1}>
114
+ {isSelected && <Text color={THEME.success}>✓</Text>}
115
+ <Text>{opt.label}</Text>
116
+ </Box>
117
+ )
118
+ })}
119
+ </Box>
120
+ </Box>
121
+
122
+ {detail && (
123
+ <Box marginLeft={2}>
124
+ <Text dimColor>{detail}</Text>
125
+ </Box>
126
+ )}
127
+ </Box>
128
+ )
129
+ }
@@ -9,6 +9,7 @@ export interface Stage {
9
9
  label: string
10
10
  status: StageStatus
11
11
  detail?: string
12
+ errors?: string[]
12
13
  }
13
14
 
14
15
  const ICONS: Record<StageStatus, ReactNode> = {
@@ -24,10 +25,21 @@ const ICONS: Record<StageStatus, ReactNode> = {
24
25
 
25
26
  export function StageRow({ stage }: { stage: Stage }) {
26
27
  return (
27
- <Box gap={1}>
28
- {ICONS[stage.status]}
29
- <Text dimColor={stage.status === 'pending'}>{stage.label}</Text>
30
- {stage.detail && <Text color={THEME.hint}>{stage.detail}</Text>}
28
+ <Box flexDirection="column">
29
+ <Box gap={1}>
30
+ {ICONS[stage.status]}
31
+ <Text dimColor={stage.status === 'pending'}>{stage.label}</Text>
32
+ {stage.detail && <Text color={THEME.hint}> — {stage.detail}</Text>}
33
+ </Box>
34
+ {stage.errors && stage.errors.length > 0 && (
35
+ <Box flexDirection="column" marginLeft={3}>
36
+ {stage.errors.map((e) => (
37
+ <Text key={e} color={THEME.warning}>
38
+ {e}
39
+ </Text>
40
+ ))}
41
+ </Box>
42
+ )}
31
43
  </Box>
32
44
  )
33
45
  }
@@ -21,9 +21,15 @@ const FocusOrderContext = createContext<FocusOrderState>({
21
21
  focus: () => {},
22
22
  })
23
23
 
24
- export function FocusOrderProvider({ children }: { children: ReactNode }) {
24
+ export function FocusOrderProvider({
25
+ children,
26
+ initialFocusId,
27
+ }: {
28
+ children: ReactNode
29
+ initialFocusId?: string | null
30
+ }) {
25
31
  const [focusIds, setFocusIds] = useState<string[]>([])
26
- const [focusedId, setFocusedId] = useState<string | null>(null)
32
+ const [focusedId, setFocusedId] = useState<string | null>(initialFocusId ?? null)
27
33
 
28
34
  const focusNext = useCallback(() => {
29
35
  setFocusedId((current) => {
@@ -16,6 +16,7 @@ export interface FieldState {
16
16
 
17
17
  export interface AssetSectionState {
18
18
  items: string[]
19
+ descriptions: Record<string, string>
19
20
  editing?: string
20
21
  adding: boolean
21
22
  }
@@ -46,6 +47,7 @@ interface FormStateContextValue {
46
47
  addAsset: (section: AssetSectionKey, name: string) => void
47
48
  removeAsset: (section: AssetSectionKey, name: string) => void
48
49
  renameAsset: (section: AssetSectionKey, oldName: string, newName: string) => void
50
+ setAssetDescription: (section: AssetSectionKey, name: string, description: string) => void
49
51
  setAssetAdding: (section: AssetSectionKey, adding: boolean) => void
50
52
  setAssetEditing: (section: AssetSectionKey, name?: string) => void
51
53
 
@@ -57,6 +59,7 @@ interface FormStateContextValue {
57
59
 
58
60
  const defaultAssetSection: AssetSectionState = {
59
61
  items: [],
62
+ descriptions: {},
60
63
  editing: undefined,
61
64
  adding: false,
62
65
  }
@@ -81,6 +84,7 @@ const FormStateContext = createContext<FormStateContextValue>({
81
84
  addAsset: () => {},
82
85
  removeAsset: () => {},
83
86
  renameAsset: () => {},
87
+ setAssetDescription: () => {},
84
88
  setAssetAdding: () => {},
85
89
  setAssetEditing: () => {},
86
90
  toCreateOptions: () => ({ name: '', version: '', description: '', skills: [], commands: [], agents: [] }),
@@ -88,8 +92,8 @@ const FormStateContext = createContext<FormStateContextValue>({
88
92
 
89
93
  // --- Provider ---
90
94
 
91
- export function FormStateProvider({ children }: { children: ReactNode }) {
92
- const [form, setForm] = useState<FormState>(defaultForm)
95
+ export function FormStateProvider({ children, initialState }: { children: ReactNode; initialState?: FormState }) {
96
+ const [form, setForm] = useState<FormState>(initialState ?? defaultForm)
93
97
 
94
98
  const setFieldValue = useCallback((field: RequiredFieldKey, value: string) => {
95
99
  setForm((prev) => ({
@@ -116,11 +120,16 @@ export function FormStateProvider({ children }: { children: ReactNode }) {
116
120
  setForm((prev) => {
117
121
  const current = prev.assets[section]
118
122
  if (current.items.includes(name)) return prev
123
+ const defaultDesc = `The ${name} ${section} description`
119
124
  return {
120
125
  ...prev,
121
126
  assets: {
122
127
  ...prev.assets,
123
- [section]: { ...current, items: [...current.items, name] },
128
+ [section]: {
129
+ ...current,
130
+ items: [...current.items, name],
131
+ descriptions: { ...current.descriptions, [name]: defaultDesc },
132
+ },
124
133
  },
125
134
  }
126
135
  })
@@ -129,6 +138,7 @@ export function FormStateProvider({ children }: { children: ReactNode }) {
129
138
  const removeAsset = useCallback((section: AssetSectionKey, name: string) => {
130
139
  setForm((prev) => {
131
140
  const current = prev.assets[section]
141
+ const { [name]: _, ...remainingDescs } = current.descriptions
132
142
  return {
133
143
  ...prev,
134
144
  assets: {
@@ -136,6 +146,7 @@ export function FormStateProvider({ children }: { children: ReactNode }) {
136
146
  [section]: {
137
147
  ...current,
138
148
  items: current.items.filter((item) => item !== name),
149
+ descriptions: remainingDescs,
139
150
  editing: current.editing === name ? undefined : current.editing,
140
151
  },
141
152
  },
@@ -148,6 +159,7 @@ export function FormStateProvider({ children }: { children: ReactNode }) {
148
159
  setForm((prev) => {
149
160
  const current = prev.assets[section]
150
161
  if (current.items.includes(newName)) return prev
162
+ const { [oldName]: desc, ...restDescs } = current.descriptions
151
163
  return {
152
164
  ...prev,
153
165
  assets: {
@@ -155,6 +167,7 @@ export function FormStateProvider({ children }: { children: ReactNode }) {
155
167
  [section]: {
156
168
  ...current,
157
169
  items: current.items.map((item) => (item === oldName ? newName : item)),
170
+ descriptions: { ...restDescs, [newName]: desc ?? `A ${newName} ${section}` },
158
171
  editing: current.editing === oldName ? newName : current.editing,
159
172
  },
160
173
  },
@@ -162,6 +175,22 @@ export function FormStateProvider({ children }: { children: ReactNode }) {
162
175
  })
163
176
  }, [])
164
177
 
178
+ const setAssetDescription = useCallback((section: AssetSectionKey, name: string, description: string) => {
179
+ setForm((prev) => {
180
+ const current = prev.assets[section]
181
+ return {
182
+ ...prev,
183
+ assets: {
184
+ ...prev.assets,
185
+ [section]: {
186
+ ...current,
187
+ descriptions: { ...current.descriptions, [name]: description },
188
+ },
189
+ },
190
+ }
191
+ })
192
+ }, [])
193
+
165
194
  const setAssetAdding = useCallback((section: AssetSectionKey, adding: boolean) => {
166
195
  setForm((prev) => ({
167
196
  ...prev,
@@ -202,6 +231,7 @@ export function FormStateProvider({ children }: { children: ReactNode }) {
202
231
  addAsset,
203
232
  removeAsset,
204
233
  renameAsset,
234
+ setAssetDescription,
205
235
  setAssetAdding,
206
236
  setAssetEditing,
207
237
  toCreateOptions,
@@ -213,6 +243,7 @@ export function FormStateProvider({ children }: { children: ReactNode }) {
213
243
  addAsset,
214
244
  removeAsset,
215
245
  renameAsset,
246
+ setAssetDescription,
216
247
  setAssetAdding,
217
248
  setAssetEditing,
218
249
  toCreateOptions,