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.
- package/.package.json.bak +44 -0
- package/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +33 -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 +171 -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
package/src/run.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { parse } from '@bomb.sh/args'
|
|
2
|
-
import {
|
|
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
|
-
|
|
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>
|
|
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
|
-
|
|
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>
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
98
|
+
// Field picker (entered via ↓ during name editing)
|
|
99
|
+
if (selectedItem === item) {
|
|
81
100
|
return (
|
|
82
|
-
<
|
|
101
|
+
<AssetFieldPicker
|
|
83
102
|
key={itemId}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
28
|
-
{
|
|
29
|
-
|
|
30
|
-
|
|
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({
|
|
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]: {
|
|
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,
|