agent-facets 0.2.2 → 0.3.3
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/bin/facet +181 -0
- package/bin/package.json +3 -0
- package/package.json +17 -37
- package/postinstall.mjs +210 -0
- package/.package.json.bak +0 -44
- package/.turbo/turbo-build.log +0 -3
- package/CHANGELOG.md +0 -85
- package/bunfig.toml +0 -2
- package/dist/facet +0 -0
- package/src/__tests__/cli.test.ts +0 -195
- package/src/__tests__/create-build.test.ts +0 -227
- package/src/__tests__/edit-integration.test.ts +0 -171
- package/src/__tests__/resolve-dir.test.ts +0 -95
- package/src/commands/build.ts +0 -58
- package/src/commands/create/index.ts +0 -76
- package/src/commands/create/types.ts +0 -9
- package/src/commands/create/wizard.tsx +0 -75
- package/src/commands/create-scaffold.ts +0 -184
- package/src/commands/edit/index.ts +0 -144
- package/src/commands/edit/wizard.tsx +0 -74
- package/src/commands/resolve-dir.ts +0 -98
- package/src/commands.ts +0 -40
- package/src/help.ts +0 -43
- package/src/index.ts +0 -10
- package/src/run.ts +0 -82
- package/src/suggest.ts +0 -35
- package/src/tui/components/asset-description.tsx +0 -17
- package/src/tui/components/asset-field-picker.tsx +0 -78
- package/src/tui/components/asset-inline-input.tsx +0 -91
- package/src/tui/components/asset-item.tsx +0 -44
- package/src/tui/components/asset-section.tsx +0 -191
- package/src/tui/components/button.tsx +0 -92
- package/src/tui/components/editable-field.tsx +0 -172
- package/src/tui/components/exit-toast.tsx +0 -20
- package/src/tui/components/reconciliation-item.tsx +0 -129
- package/src/tui/components/stage-row.tsx +0 -45
- package/src/tui/components/version-selector.tsx +0 -79
- package/src/tui/context/focus-mode-context.ts +0 -36
- package/src/tui/context/focus-order-context.ts +0 -68
- package/src/tui/context/form-state-context.ts +0 -260
- package/src/tui/editor.ts +0 -40
- package/src/tui/gradient.ts +0 -1
- package/src/tui/hooks/use-exit-keys.ts +0 -75
- package/src/tui/hooks/use-navigation-keys.ts +0 -34
- package/src/tui/layouts/wizard-layout.tsx +0 -41
- package/src/tui/theme.ts +0 -1
- package/src/tui/views/build/build-view.tsx +0 -152
- package/src/tui/views/create/confirm-view.tsx +0 -74
- package/src/tui/views/create/create-view.tsx +0 -158
- package/src/tui/views/create/wizard.tsx +0 -97
- package/src/tui/views/edit/edit-confirm-view.tsx +0 -93
- package/src/tui/views/edit/edit-types.ts +0 -34
- package/src/tui/views/edit/edit-view.tsx +0 -140
- package/src/tui/views/edit/manifest-to-form.ts +0 -38
- package/src/tui/views/edit/reconciliation-view.tsx +0 -170
- package/src/tui/views/edit/use-edit-session.ts +0 -125
- package/src/tui/views/edit/wizard.tsx +0 -129
- package/src/version.ts +0 -3
- package/tsconfig.json +0 -4
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from 'ink'
|
|
2
|
-
import { useState } from 'react'
|
|
3
|
-
import { useFocusMode } from '../context/focus-mode-context.ts'
|
|
4
|
-
import { useFocusOrder } from '../context/focus-order-context.ts'
|
|
5
|
-
import type { AssetSectionKey } from '../context/form-state-context.ts'
|
|
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'
|
|
10
|
-
import { AssetInlineInput } from './asset-inline-input.tsx'
|
|
11
|
-
import { AssetItem } from './asset-item.tsx'
|
|
12
|
-
import { Button } from './button.tsx'
|
|
13
|
-
|
|
14
|
-
export function AssetSection({
|
|
15
|
-
section,
|
|
16
|
-
label,
|
|
17
|
-
defaultName,
|
|
18
|
-
dimmed,
|
|
19
|
-
validate,
|
|
20
|
-
onEditDescription,
|
|
21
|
-
}: {
|
|
22
|
-
section: AssetSectionKey
|
|
23
|
-
label: string
|
|
24
|
-
defaultName?: string
|
|
25
|
-
dimmed?: boolean
|
|
26
|
-
validate?: (value: string) => string | undefined
|
|
27
|
-
onEditDescription?: (section: AssetSectionKey, name: string) => void
|
|
28
|
-
}) {
|
|
29
|
-
const { form, addAsset, removeAsset, renameAsset, setAssetAdding, setAssetEditing } = useFormState()
|
|
30
|
-
const { items, descriptions, editing, adding } = form.assets[section]
|
|
31
|
-
const { setMode } = useFocusMode()
|
|
32
|
-
const { focusedId, focus } = useFocusOrder()
|
|
33
|
-
const [inputValue, setInputValue] = useState('')
|
|
34
|
-
const [error, setError] = useState('')
|
|
35
|
-
const [selectedItem, setSelectedItem] = useState<string | null>(null)
|
|
36
|
-
|
|
37
|
-
const startAdding = () => {
|
|
38
|
-
setAssetAdding(section, true)
|
|
39
|
-
setInputValue('')
|
|
40
|
-
setError('')
|
|
41
|
-
setMode('field-revision')
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const startEditing = (name: string) => {
|
|
45
|
-
setAssetEditing(section, name)
|
|
46
|
-
setInputValue(name)
|
|
47
|
-
setError('')
|
|
48
|
-
setMode('field-revision')
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const closeInput = (focusTarget?: string | false) => {
|
|
52
|
-
setAssetAdding(section, false)
|
|
53
|
-
setAssetEditing(section, undefined)
|
|
54
|
-
setInputValue('')
|
|
55
|
-
setError('')
|
|
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
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const handleRemove = (name: string) => {
|
|
72
|
-
const index = items.indexOf(name)
|
|
73
|
-
removeAsset(section, name)
|
|
74
|
-
|
|
75
|
-
if (index < items.length - 1) {
|
|
76
|
-
focus(`item-${section}-${index}`)
|
|
77
|
-
} else if (index > 0) {
|
|
78
|
-
focus(`item-${section}-${index - 1}`)
|
|
79
|
-
} else {
|
|
80
|
-
focus(`add-${section}`)
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return (
|
|
85
|
-
<Box flexDirection="column" gap={0}>
|
|
86
|
-
<Box gap={1}>
|
|
87
|
-
<Text bold dimColor={dimmed}>
|
|
88
|
-
{label}
|
|
89
|
-
</Text>
|
|
90
|
-
{items.length === 0 && !adding && <Text dimColor>(none)</Text>}
|
|
91
|
-
</Box>
|
|
92
|
-
|
|
93
|
-
{items.map((item, i) => {
|
|
94
|
-
const itemId = `item-${section}-${i}`
|
|
95
|
-
const isFocusedItem = focusedId === itemId
|
|
96
|
-
const description = descriptions[item] ?? `A ${item} ${section}`
|
|
97
|
-
|
|
98
|
-
// Field picker (entered via ↓ during name editing)
|
|
99
|
-
if (selectedItem === item) {
|
|
100
|
-
return (
|
|
101
|
-
<AssetFieldPicker
|
|
102
|
-
key={itemId}
|
|
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)
|
|
111
|
-
}}
|
|
112
|
-
/>
|
|
113
|
-
)
|
|
114
|
-
}
|
|
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)
|
|
145
|
-
return (
|
|
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>
|
|
156
|
-
)
|
|
157
|
-
})}
|
|
158
|
-
|
|
159
|
-
{adding ? (
|
|
160
|
-
<AssetInlineInput
|
|
161
|
-
id={`add-${section}`}
|
|
162
|
-
value={inputValue}
|
|
163
|
-
placeholder={defaultName}
|
|
164
|
-
error={error}
|
|
165
|
-
isFocused={focusedId === `add-${section}`}
|
|
166
|
-
onChange={setInputValue}
|
|
167
|
-
validate={validate}
|
|
168
|
-
onError={setError}
|
|
169
|
-
onSubmit={(name) => {
|
|
170
|
-
addAsset(section, name)
|
|
171
|
-
closeInput()
|
|
172
|
-
}}
|
|
173
|
-
onCancel={closeInput}
|
|
174
|
-
/>
|
|
175
|
-
) : (
|
|
176
|
-
<Box marginLeft={2}>
|
|
177
|
-
<Button
|
|
178
|
-
id={`add-${section}`}
|
|
179
|
-
label="+ Add"
|
|
180
|
-
hint={
|
|
181
|
-
<Text dimColor>
|
|
182
|
-
<Text>Enter</Text> to add
|
|
183
|
-
</Text>
|
|
184
|
-
}
|
|
185
|
-
onPress={startAdding}
|
|
186
|
-
/>
|
|
187
|
-
</Box>
|
|
188
|
-
)}
|
|
189
|
-
</Box>
|
|
190
|
-
)
|
|
191
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { Box, Text, useInput } from 'ink'
|
|
2
|
-
import Gradient from 'ink-gradient'
|
|
3
|
-
import type { ReactNode } from 'react'
|
|
4
|
-
import { useEffect, useState } from 'react'
|
|
5
|
-
import { useFocusOrder } from '../context/focus-order-context.ts'
|
|
6
|
-
import { GRADIENT_STOPS, getAnimatedGradient } from '../gradient.ts'
|
|
7
|
-
import { THEME } from '../theme.ts'
|
|
8
|
-
|
|
9
|
-
const ANIMATION_INTERVAL_MS = 75
|
|
10
|
-
|
|
11
|
-
export function Button({
|
|
12
|
-
id,
|
|
13
|
-
label,
|
|
14
|
-
hint,
|
|
15
|
-
onPress,
|
|
16
|
-
disabled,
|
|
17
|
-
color,
|
|
18
|
-
gradient: showGradient,
|
|
19
|
-
animateGradient,
|
|
20
|
-
}: {
|
|
21
|
-
id: string
|
|
22
|
-
label: string
|
|
23
|
-
hint?: ReactNode
|
|
24
|
-
onPress: () => void
|
|
25
|
-
disabled?: boolean
|
|
26
|
-
color?: string
|
|
27
|
-
autoFocus?: boolean
|
|
28
|
-
gradient?: boolean
|
|
29
|
-
animateGradient?: boolean
|
|
30
|
-
}) {
|
|
31
|
-
const { focusedId } = useFocusOrder()
|
|
32
|
-
const isFocused = focusedId === id && !disabled
|
|
33
|
-
const [offset, setOffset] = useState(0)
|
|
34
|
-
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
if (!animateGradient) return
|
|
37
|
-
const interval = setInterval(() => {
|
|
38
|
-
setOffset((prev) => (prev + 1) % GRADIENT_STOPS.length)
|
|
39
|
-
}, ANIMATION_INTERVAL_MS)
|
|
40
|
-
return () => clearInterval(interval)
|
|
41
|
-
}, [animateGradient])
|
|
42
|
-
|
|
43
|
-
useInput(
|
|
44
|
-
(_input, key) => {
|
|
45
|
-
if (key.return) {
|
|
46
|
-
onPress()
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
{ isActive: isFocused },
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
const prefix = isFocused ? '▸ ' : ' '
|
|
53
|
-
|
|
54
|
-
const focusHint = isFocused && hint ? <Text> {hint}</Text> : null
|
|
55
|
-
|
|
56
|
-
if (disabled) {
|
|
57
|
-
return (
|
|
58
|
-
<Box gap={0}>
|
|
59
|
-
<Text color="gray" dimColor>
|
|
60
|
-
{prefix}
|
|
61
|
-
{label}
|
|
62
|
-
</Text>
|
|
63
|
-
</Box>
|
|
64
|
-
)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (showGradient) {
|
|
68
|
-
const colors = animateGradient ? getAnimatedGradient(offset) : [...THEME.gradient]
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<Box gap={0}>
|
|
72
|
-
<Gradient colors={colors}>
|
|
73
|
-
<Text bold>
|
|
74
|
-
{prefix}
|
|
75
|
-
{label}
|
|
76
|
-
</Text>
|
|
77
|
-
</Gradient>
|
|
78
|
-
{focusHint}
|
|
79
|
-
</Box>
|
|
80
|
-
)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<Box gap={0}>
|
|
85
|
-
<Text color={isFocused ? (color ?? THEME.primary) : undefined} bold={isFocused}>
|
|
86
|
-
{prefix}
|
|
87
|
-
{label}
|
|
88
|
-
</Text>
|
|
89
|
-
{focusHint}
|
|
90
|
-
</Box>
|
|
91
|
-
)
|
|
92
|
-
}
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import { Box, Text, useInput } from 'ink'
|
|
2
|
-
import TextInput from 'ink-text-input'
|
|
3
|
-
import { useCallback, useEffect, useState } from 'react'
|
|
4
|
-
import { useFocusMode } from '../context/focus-mode-context.ts'
|
|
5
|
-
import { useFocusOrder } from '../context/focus-order-context.ts'
|
|
6
|
-
import type { RequiredFieldKey } from '../context/form-state-context.ts'
|
|
7
|
-
import { useFormState } from '../context/form-state-context.ts'
|
|
8
|
-
import { THEME } from '../theme.ts'
|
|
9
|
-
|
|
10
|
-
export function EditableField({
|
|
11
|
-
field,
|
|
12
|
-
label,
|
|
13
|
-
placeholder,
|
|
14
|
-
hint,
|
|
15
|
-
defaultValue,
|
|
16
|
-
dimmed,
|
|
17
|
-
validate,
|
|
18
|
-
onConfirm,
|
|
19
|
-
}: {
|
|
20
|
-
field: RequiredFieldKey
|
|
21
|
-
label: string
|
|
22
|
-
placeholder?: string
|
|
23
|
-
hint?: string
|
|
24
|
-
defaultValue?: string
|
|
25
|
-
dimmed?: boolean
|
|
26
|
-
validate?: (value: string) => string | undefined
|
|
27
|
-
onConfirm?: () => void
|
|
28
|
-
}) {
|
|
29
|
-
const id = `field-${field}`
|
|
30
|
-
const { focusedId } = useFocusOrder()
|
|
31
|
-
const { mode, setMode } = useFocusMode()
|
|
32
|
-
const { form, setFieldValue, setFieldStatus } = useFormState()
|
|
33
|
-
const { value, status } = form.fields[field]
|
|
34
|
-
const isFocused = focusedId === id
|
|
35
|
-
const editing = status === 'editing'
|
|
36
|
-
const [error, setError] = useState('')
|
|
37
|
-
const [didAutofill, setDidAutofill] = useState(false)
|
|
38
|
-
const [previousValue, setPreviousValue] = useState<string | undefined>(undefined)
|
|
39
|
-
|
|
40
|
-
const startEditing = useCallback(() => {
|
|
41
|
-
setPreviousValue(value || undefined)
|
|
42
|
-
setFieldStatus(field, 'editing')
|
|
43
|
-
// Revising an existing value: Escape reverts without triggering exit modal
|
|
44
|
-
// Initial entry: Escape passes through to the exit hook
|
|
45
|
-
setMode(value ? 'field-revision' : 'field-initial-entry')
|
|
46
|
-
}, [field, value, setFieldStatus, setMode])
|
|
47
|
-
|
|
48
|
-
const stopEditing = useCallback(() => {
|
|
49
|
-
setPreviousValue(undefined)
|
|
50
|
-
setFieldStatus(field, value ? 'confirmed' : 'empty')
|
|
51
|
-
setMode('form-navigation')
|
|
52
|
-
}, [field, value, setFieldStatus, setMode])
|
|
53
|
-
|
|
54
|
-
const cancelEditing = useCallback(() => {
|
|
55
|
-
if (previousValue) {
|
|
56
|
-
setFieldValue(field, previousValue)
|
|
57
|
-
setPreviousValue(undefined)
|
|
58
|
-
setFieldStatus(field, 'confirmed')
|
|
59
|
-
setMode('form-navigation')
|
|
60
|
-
setError('')
|
|
61
|
-
}
|
|
62
|
-
}, [previousValue, field, setFieldValue, setFieldStatus, setMode])
|
|
63
|
-
|
|
64
|
-
// Handle Enter to edit, or 'c' to clear and edit
|
|
65
|
-
useInput(
|
|
66
|
-
(input, key) => {
|
|
67
|
-
if (!editing) {
|
|
68
|
-
if (key.return) {
|
|
69
|
-
startEditing()
|
|
70
|
-
}
|
|
71
|
-
if (input === 'c' && value) {
|
|
72
|
-
setFieldValue(field, '')
|
|
73
|
-
startEditing()
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
},
|
|
77
|
-
{ isActive: isFocused && !editing && mode === 'form-navigation' },
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
// Handle Enter to confirm / Escape to revert (if previously had value)
|
|
81
|
-
useInput(
|
|
82
|
-
(_input, key) => {
|
|
83
|
-
if (key.return) {
|
|
84
|
-
if (validate) {
|
|
85
|
-
const err = validate(value)
|
|
86
|
-
if (err) {
|
|
87
|
-
setError(err)
|
|
88
|
-
return
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
if (!value) {
|
|
92
|
-
setError(`${label} is required`)
|
|
93
|
-
return
|
|
94
|
-
}
|
|
95
|
-
setError('')
|
|
96
|
-
stopEditing()
|
|
97
|
-
onConfirm?.()
|
|
98
|
-
}
|
|
99
|
-
if (key.escape && previousValue) {
|
|
100
|
-
cancelEditing()
|
|
101
|
-
}
|
|
102
|
-
},
|
|
103
|
-
{ isActive: isFocused && editing },
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
// Auto-start editing when focused on an empty required field
|
|
107
|
-
useEffect(() => {
|
|
108
|
-
if (isFocused && !value && !editing && mode === 'form-navigation') {
|
|
109
|
-
startEditing()
|
|
110
|
-
}
|
|
111
|
-
}, [isFocused, value, editing, mode, startEditing])
|
|
112
|
-
|
|
113
|
-
// Autofill defaultValue when editing starts on an empty field
|
|
114
|
-
useEffect(() => {
|
|
115
|
-
if (editing && !value && defaultValue && !didAutofill) {
|
|
116
|
-
setFieldValue(field, defaultValue)
|
|
117
|
-
setDidAutofill(true)
|
|
118
|
-
}
|
|
119
|
-
}, [editing, value, defaultValue, didAutofill, field, setFieldValue])
|
|
120
|
-
|
|
121
|
-
const isEditing = editing && isFocused
|
|
122
|
-
|
|
123
|
-
return (
|
|
124
|
-
<Box flexDirection="column" gap={0}>
|
|
125
|
-
<Box gap={1}>
|
|
126
|
-
<Text color={isFocused ? THEME.primary : undefined} bold={isFocused} dimColor={dimmed && !isFocused}>
|
|
127
|
-
{label}:
|
|
128
|
-
</Text>
|
|
129
|
-
{isEditing && error ? (
|
|
130
|
-
<Text color={THEME.warning}>{error}</Text>
|
|
131
|
-
) : isEditing ? (
|
|
132
|
-
<>
|
|
133
|
-
{hint && <Text color={THEME.hint}>({hint})</Text>}
|
|
134
|
-
{previousValue && (
|
|
135
|
-
<Text color={THEME.hint}>
|
|
136
|
-
<Text color={THEME.keyword}>Escape</Text> to revert to{' '}
|
|
137
|
-
<Text color={THEME.keyword}>"{previousValue}"</Text>
|
|
138
|
-
</Text>
|
|
139
|
-
)}
|
|
140
|
-
</>
|
|
141
|
-
) : !isEditing && value ? (
|
|
142
|
-
<Text color={dimmed && !isFocused ? undefined : THEME.success} dimColor={dimmed && !isFocused}>
|
|
143
|
-
✓
|
|
144
|
-
</Text>
|
|
145
|
-
) : !isEditing ? (
|
|
146
|
-
<Text dimColor>(not set)</Text>
|
|
147
|
-
) : null}
|
|
148
|
-
</Box>
|
|
149
|
-
<Box marginLeft={2}>
|
|
150
|
-
{isEditing ? (
|
|
151
|
-
<Box gap={1}>
|
|
152
|
-
<Text color={THEME.tertiary}>{'> '}</Text>
|
|
153
|
-
<TextInput value={value} onChange={(v) => setFieldValue(field, v)} placeholder={placeholder} focus />
|
|
154
|
-
<Text color={THEME.hint}>
|
|
155
|
-
· <Text color={THEME.keyword}>Enter</Text> to save
|
|
156
|
-
</Text>
|
|
157
|
-
</Box>
|
|
158
|
-
) : (
|
|
159
|
-
<Box gap={1}>
|
|
160
|
-
<Text dimColor={dimmed && !isFocused}>{value || ' '}</Text>
|
|
161
|
-
{isFocused && value && (
|
|
162
|
-
<Text color={THEME.hint}>
|
|
163
|
-
· <Text color={THEME.keyword}>Enter</Text> to edit · <Text color={THEME.keyword}>c</Text> to clear and
|
|
164
|
-
edit
|
|
165
|
-
</Text>
|
|
166
|
-
)}
|
|
167
|
-
</Box>
|
|
168
|
-
)}
|
|
169
|
-
</Box>
|
|
170
|
-
</Box>
|
|
171
|
-
)
|
|
172
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from 'ink'
|
|
2
|
-
import { useFocusMode } from '../context/focus-mode-context.ts'
|
|
3
|
-
import { THEME } from '../theme.ts'
|
|
4
|
-
|
|
5
|
-
export function ExitFooter() {
|
|
6
|
-
const { mode, exitSecondsLeft } = useFocusMode()
|
|
7
|
-
const visible = mode === 'exit-modal' && exitSecondsLeft > 0
|
|
8
|
-
|
|
9
|
-
return (
|
|
10
|
-
<Box marginTop={1}>
|
|
11
|
-
{visible ? (
|
|
12
|
-
<Text color={THEME.warning} bold>
|
|
13
|
-
Press Escape again to exit ({exitSecondsLeft}s) — any other key to cancel
|
|
14
|
-
</Text>
|
|
15
|
-
) : (
|
|
16
|
-
<Text> </Text>
|
|
17
|
-
)}
|
|
18
|
-
</Box>
|
|
19
|
-
)
|
|
20
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from 'ink'
|
|
2
|
-
import Spinner from 'ink-spinner'
|
|
3
|
-
import type { ReactNode } from 'react'
|
|
4
|
-
import { THEME } from '../theme.ts'
|
|
5
|
-
|
|
6
|
-
export type StageStatus = 'pending' | 'running' | 'done' | 'failed'
|
|
7
|
-
|
|
8
|
-
export interface Stage {
|
|
9
|
-
label: string
|
|
10
|
-
status: StageStatus
|
|
11
|
-
detail?: string
|
|
12
|
-
errors?: string[]
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const ICONS: Record<StageStatus, ReactNode> = {
|
|
16
|
-
pending: <Text color={THEME.hint}>○</Text>,
|
|
17
|
-
running: (
|
|
18
|
-
<Text color={THEME.secondary}>
|
|
19
|
-
<Spinner type="dots" />
|
|
20
|
-
</Text>
|
|
21
|
-
),
|
|
22
|
-
done: <Text color={THEME.success}>●</Text>,
|
|
23
|
-
failed: <Text color={THEME.warning}>✕</Text>,
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function StageRow({ stage }: { stage: Stage }) {
|
|
27
|
-
return (
|
|
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
|
-
)}
|
|
43
|
-
</Box>
|
|
44
|
-
)
|
|
45
|
-
}
|