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,79 +0,0 @@
|
|
|
1
|
-
import { Box, Text, useInput } from 'ink'
|
|
2
|
-
import { useState } from 'react'
|
|
3
|
-
|
|
4
|
-
export function VersionSelector({
|
|
5
|
-
value,
|
|
6
|
-
onChange,
|
|
7
|
-
onSubmit,
|
|
8
|
-
active,
|
|
9
|
-
}: {
|
|
10
|
-
value: string
|
|
11
|
-
onChange: (v: string) => void
|
|
12
|
-
onSubmit: () => void
|
|
13
|
-
active: boolean
|
|
14
|
-
}) {
|
|
15
|
-
const parts = value.split('.').map(Number)
|
|
16
|
-
const [cursor, setCursor] = useState(0) // 0=major, 1=minor, 2=patch
|
|
17
|
-
|
|
18
|
-
useInput(
|
|
19
|
-
(input, key) => {
|
|
20
|
-
if (!active) return
|
|
21
|
-
|
|
22
|
-
if (key.return) {
|
|
23
|
-
onSubmit()
|
|
24
|
-
return
|
|
25
|
-
}
|
|
26
|
-
// Tab and left/right all move between segments
|
|
27
|
-
if (key.leftArrow || (key.shift && key.tab)) {
|
|
28
|
-
setCursor((c) => Math.max(0, c - 1))
|
|
29
|
-
return
|
|
30
|
-
}
|
|
31
|
-
if (key.rightArrow || key.tab) {
|
|
32
|
-
setCursor((c) => Math.min(2, c + 1))
|
|
33
|
-
return
|
|
34
|
-
}
|
|
35
|
-
if (key.upArrow) {
|
|
36
|
-
const next = [...parts]
|
|
37
|
-
next[cursor] = (next[cursor] ?? 0) + 1
|
|
38
|
-
onChange(next.join('.'))
|
|
39
|
-
return
|
|
40
|
-
}
|
|
41
|
-
if (key.downArrow) {
|
|
42
|
-
const next = [...parts]
|
|
43
|
-
next[cursor] = Math.max(0, (next[cursor] ?? 0) - 1)
|
|
44
|
-
onChange(next.join('.'))
|
|
45
|
-
return
|
|
46
|
-
}
|
|
47
|
-
// Allow typing a full version directly
|
|
48
|
-
if (key.backspace) {
|
|
49
|
-
onChange(value.slice(0, -1))
|
|
50
|
-
return
|
|
51
|
-
}
|
|
52
|
-
if (/[0-9.]/.test(input)) {
|
|
53
|
-
onChange(value + input)
|
|
54
|
-
return
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
{ isActive: active },
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
const segments = value.split('.')
|
|
61
|
-
return (
|
|
62
|
-
<Box gap={0}>
|
|
63
|
-
{segments.map((seg, i) => (
|
|
64
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: version segments are always positional (major.minor.patch)
|
|
65
|
-
<Box key={i}>
|
|
66
|
-
{i > 0 && <Text dimColor>.</Text>}
|
|
67
|
-
<Text
|
|
68
|
-
bold={active && cursor === i}
|
|
69
|
-
color={active && cursor === i ? 'cyan' : undefined}
|
|
70
|
-
inverse={active && cursor === i}
|
|
71
|
-
>
|
|
72
|
-
{seg}
|
|
73
|
-
</Text>
|
|
74
|
-
</Box>
|
|
75
|
-
))}
|
|
76
|
-
{active && <Text dimColor>← → to select, ↑ ↓ to change, Enter to confirm</Text>}
|
|
77
|
-
</Box>
|
|
78
|
-
)
|
|
79
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import type { Dispatch, ReactNode, SetStateAction } from 'react'
|
|
2
|
-
import { createContext, createElement, useContext, useMemo, useState } from 'react'
|
|
3
|
-
|
|
4
|
-
export type FocusMode =
|
|
5
|
-
| 'form-navigation'
|
|
6
|
-
| 'field-initial-entry'
|
|
7
|
-
| 'field-revision'
|
|
8
|
-
| 'exit-modal'
|
|
9
|
-
| 'form-confirmation'
|
|
10
|
-
|
|
11
|
-
interface FocusModeState {
|
|
12
|
-
mode: FocusMode
|
|
13
|
-
setMode: (mode: FocusMode) => void
|
|
14
|
-
exitSecondsLeft: number
|
|
15
|
-
setExitSecondsLeft: Dispatch<SetStateAction<number>>
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const FocusModeContext = createContext<FocusModeState>({
|
|
19
|
-
mode: 'form-navigation',
|
|
20
|
-
setMode: () => {},
|
|
21
|
-
exitSecondsLeft: 0,
|
|
22
|
-
setExitSecondsLeft: () => {},
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
export function FocusModeProvider({ children }: { children: ReactNode }) {
|
|
26
|
-
const [mode, setMode] = useState<FocusMode>('form-navigation')
|
|
27
|
-
const [exitSecondsLeft, setExitSecondsLeft] = useState(0)
|
|
28
|
-
|
|
29
|
-
const value = useMemo(() => ({ mode, setMode, exitSecondsLeft, setExitSecondsLeft }), [mode, exitSecondsLeft])
|
|
30
|
-
|
|
31
|
-
return createElement(FocusModeContext.Provider, { value }, children)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function useFocusMode() {
|
|
35
|
-
return useContext(FocusModeContext)
|
|
36
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { Dispatch, ReactNode, SetStateAction } from 'react'
|
|
2
|
-
import { createContext, createElement, useCallback, useContext, useMemo, useState } from 'react'
|
|
3
|
-
|
|
4
|
-
interface FocusOrderState {
|
|
5
|
-
focusedId: string | null
|
|
6
|
-
focusIds: string[]
|
|
7
|
-
setFocusIds: (ids: string[]) => void
|
|
8
|
-
setFocusedId: Dispatch<SetStateAction<string | null>>
|
|
9
|
-
focusNext: () => void
|
|
10
|
-
focusPrevious: () => void
|
|
11
|
-
focus: (id: string) => void
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const FocusOrderContext = createContext<FocusOrderState>({
|
|
15
|
-
focusedId: null,
|
|
16
|
-
focusIds: [],
|
|
17
|
-
setFocusIds: () => {},
|
|
18
|
-
setFocusedId: () => {},
|
|
19
|
-
focusNext: () => {},
|
|
20
|
-
focusPrevious: () => {},
|
|
21
|
-
focus: () => {},
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
export function FocusOrderProvider({
|
|
25
|
-
children,
|
|
26
|
-
initialFocusId,
|
|
27
|
-
}: {
|
|
28
|
-
children: ReactNode
|
|
29
|
-
initialFocusId?: string | null
|
|
30
|
-
}) {
|
|
31
|
-
const [focusIds, setFocusIds] = useState<string[]>([])
|
|
32
|
-
const [focusedId, setFocusedId] = useState<string | null>(initialFocusId ?? null)
|
|
33
|
-
|
|
34
|
-
const focusNext = useCallback(() => {
|
|
35
|
-
setFocusedId((current) => {
|
|
36
|
-
if (!current || focusIds.length === 0) return focusIds[0] ?? null
|
|
37
|
-
const idx = focusIds.indexOf(current)
|
|
38
|
-
if (idx === -1) return focusIds[0] ?? null
|
|
39
|
-
if (idx === focusIds.length - 1) return current
|
|
40
|
-
return focusIds[idx + 1] ?? null
|
|
41
|
-
})
|
|
42
|
-
}, [focusIds])
|
|
43
|
-
|
|
44
|
-
const focusPrevious = useCallback(() => {
|
|
45
|
-
setFocusedId((current) => {
|
|
46
|
-
if (!current || focusIds.length === 0) return focusIds[0] ?? null
|
|
47
|
-
const idx = focusIds.indexOf(current)
|
|
48
|
-
if (idx === -1) return focusIds[0] ?? null
|
|
49
|
-
if (idx === 0) return current
|
|
50
|
-
return focusIds[idx - 1] ?? null
|
|
51
|
-
})
|
|
52
|
-
}, [focusIds])
|
|
53
|
-
|
|
54
|
-
const focus = useCallback((id: string) => {
|
|
55
|
-
setFocusedId(id)
|
|
56
|
-
}, [])
|
|
57
|
-
|
|
58
|
-
const value = useMemo(
|
|
59
|
-
() => ({ focusedId, focusIds, setFocusIds, setFocusedId, focusNext, focusPrevious, focus }),
|
|
60
|
-
[focusedId, focusIds, focusNext, focusPrevious, focus],
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
return createElement(FocusOrderContext.Provider, { value }, children)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function useFocusOrder() {
|
|
67
|
-
return useContext(FocusOrderContext)
|
|
68
|
-
}
|
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from 'react'
|
|
2
|
-
import { createContext, createElement, useCallback, useContext, useMemo, useState } from 'react'
|
|
3
|
-
import type { CreateOptions } from '../../commands/create-scaffold.ts'
|
|
4
|
-
import { isValidKebabCase } from '../../commands/create-scaffold.ts'
|
|
5
|
-
|
|
6
|
-
// --- Types ---
|
|
7
|
-
|
|
8
|
-
export type FieldStatus = 'empty' | 'editing' | 'confirmed'
|
|
9
|
-
export type RequiredFieldKey = 'name' | 'description' | 'version'
|
|
10
|
-
export type AssetSectionKey = 'skill' | 'command' | 'agent'
|
|
11
|
-
|
|
12
|
-
export interface FieldState {
|
|
13
|
-
value: string
|
|
14
|
-
status: FieldStatus
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface AssetSectionState {
|
|
18
|
-
items: string[]
|
|
19
|
-
descriptions: Record<string, string>
|
|
20
|
-
editing?: string
|
|
21
|
-
adding: boolean
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface FormState {
|
|
25
|
-
fields: {
|
|
26
|
-
name: FieldState
|
|
27
|
-
description: FieldState
|
|
28
|
-
version: FieldState
|
|
29
|
-
}
|
|
30
|
-
assets: {
|
|
31
|
-
skill: AssetSectionState
|
|
32
|
-
command: AssetSectionState
|
|
33
|
-
agent: AssetSectionState
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// --- Context value ---
|
|
38
|
-
|
|
39
|
-
interface FormStateContextValue {
|
|
40
|
-
form: FormState
|
|
41
|
-
|
|
42
|
-
// Field operations
|
|
43
|
-
setFieldValue: (field: RequiredFieldKey, value: string) => void
|
|
44
|
-
setFieldStatus: (field: RequiredFieldKey, status: FieldStatus) => void
|
|
45
|
-
|
|
46
|
-
// Asset operations
|
|
47
|
-
addAsset: (section: AssetSectionKey, name: string) => void
|
|
48
|
-
removeAsset: (section: AssetSectionKey, name: string) => void
|
|
49
|
-
renameAsset: (section: AssetSectionKey, oldName: string, newName: string) => void
|
|
50
|
-
setAssetDescription: (section: AssetSectionKey, name: string, description: string) => void
|
|
51
|
-
setAssetAdding: (section: AssetSectionKey, adding: boolean) => void
|
|
52
|
-
setAssetEditing: (section: AssetSectionKey, name?: string) => void
|
|
53
|
-
|
|
54
|
-
// Build CreateOptions for scaffold
|
|
55
|
-
toCreateOptions: () => CreateOptions
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// --- Defaults ---
|
|
59
|
-
|
|
60
|
-
const defaultAssetSection: AssetSectionState = {
|
|
61
|
-
items: [],
|
|
62
|
-
descriptions: {},
|
|
63
|
-
editing: undefined,
|
|
64
|
-
adding: false,
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const defaultForm: FormState = {
|
|
68
|
-
fields: {
|
|
69
|
-
name: { value: '', status: 'empty' },
|
|
70
|
-
description: { value: '', status: 'empty' },
|
|
71
|
-
version: { value: '', status: 'empty' },
|
|
72
|
-
},
|
|
73
|
-
assets: {
|
|
74
|
-
skill: { ...defaultAssetSection },
|
|
75
|
-
command: { ...defaultAssetSection },
|
|
76
|
-
agent: { ...defaultAssetSection },
|
|
77
|
-
},
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const FormStateContext = createContext<FormStateContextValue>({
|
|
81
|
-
form: defaultForm,
|
|
82
|
-
setFieldValue: () => {},
|
|
83
|
-
setFieldStatus: () => {},
|
|
84
|
-
addAsset: () => {},
|
|
85
|
-
removeAsset: () => {},
|
|
86
|
-
renameAsset: () => {},
|
|
87
|
-
setAssetDescription: () => {},
|
|
88
|
-
setAssetAdding: () => {},
|
|
89
|
-
setAssetEditing: () => {},
|
|
90
|
-
toCreateOptions: () => ({ name: '', version: '', description: '', skills: [], commands: [], agents: [] }),
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
// --- Provider ---
|
|
94
|
-
|
|
95
|
-
export function FormStateProvider({ children, initialState }: { children: ReactNode; initialState?: FormState }) {
|
|
96
|
-
const [form, setForm] = useState<FormState>(initialState ?? defaultForm)
|
|
97
|
-
|
|
98
|
-
const setFieldValue = useCallback((field: RequiredFieldKey, value: string) => {
|
|
99
|
-
setForm((prev) => ({
|
|
100
|
-
...prev,
|
|
101
|
-
fields: {
|
|
102
|
-
...prev.fields,
|
|
103
|
-
[field]: { ...prev.fields[field], value },
|
|
104
|
-
},
|
|
105
|
-
}))
|
|
106
|
-
}, [])
|
|
107
|
-
|
|
108
|
-
const setFieldStatus = useCallback((field: RequiredFieldKey, status: FieldStatus) => {
|
|
109
|
-
setForm((prev) => ({
|
|
110
|
-
...prev,
|
|
111
|
-
fields: {
|
|
112
|
-
...prev.fields,
|
|
113
|
-
[field]: { ...prev.fields[field], status },
|
|
114
|
-
},
|
|
115
|
-
}))
|
|
116
|
-
}, [])
|
|
117
|
-
|
|
118
|
-
const addAsset = useCallback((section: AssetSectionKey, name: string) => {
|
|
119
|
-
if (!isValidKebabCase(name)) return
|
|
120
|
-
setForm((prev) => {
|
|
121
|
-
const current = prev.assets[section]
|
|
122
|
-
if (current.items.includes(name)) return prev
|
|
123
|
-
const defaultDesc = `The ${name} ${section} description`
|
|
124
|
-
return {
|
|
125
|
-
...prev,
|
|
126
|
-
assets: {
|
|
127
|
-
...prev.assets,
|
|
128
|
-
[section]: {
|
|
129
|
-
...current,
|
|
130
|
-
items: [...current.items, name],
|
|
131
|
-
descriptions: { ...current.descriptions, [name]: defaultDesc },
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
}
|
|
135
|
-
})
|
|
136
|
-
}, [])
|
|
137
|
-
|
|
138
|
-
const removeAsset = useCallback((section: AssetSectionKey, name: string) => {
|
|
139
|
-
setForm((prev) => {
|
|
140
|
-
const current = prev.assets[section]
|
|
141
|
-
const { [name]: _, ...remainingDescs } = current.descriptions
|
|
142
|
-
return {
|
|
143
|
-
...prev,
|
|
144
|
-
assets: {
|
|
145
|
-
...prev.assets,
|
|
146
|
-
[section]: {
|
|
147
|
-
...current,
|
|
148
|
-
items: current.items.filter((item) => item !== name),
|
|
149
|
-
descriptions: remainingDescs,
|
|
150
|
-
editing: current.editing === name ? undefined : current.editing,
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
}
|
|
154
|
-
})
|
|
155
|
-
}, [])
|
|
156
|
-
|
|
157
|
-
const renameAsset = useCallback((section: AssetSectionKey, oldName: string, newName: string) => {
|
|
158
|
-
if (!isValidKebabCase(newName)) return
|
|
159
|
-
setForm((prev) => {
|
|
160
|
-
const current = prev.assets[section]
|
|
161
|
-
if (current.items.includes(newName)) return prev
|
|
162
|
-
const { [oldName]: desc, ...restDescs } = current.descriptions
|
|
163
|
-
return {
|
|
164
|
-
...prev,
|
|
165
|
-
assets: {
|
|
166
|
-
...prev.assets,
|
|
167
|
-
[section]: {
|
|
168
|
-
...current,
|
|
169
|
-
items: current.items.map((item) => (item === oldName ? newName : item)),
|
|
170
|
-
descriptions: { ...restDescs, [newName]: desc ?? `A ${newName} ${section}` },
|
|
171
|
-
editing: current.editing === oldName ? newName : current.editing,
|
|
172
|
-
},
|
|
173
|
-
},
|
|
174
|
-
}
|
|
175
|
-
})
|
|
176
|
-
}, [])
|
|
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
|
-
|
|
194
|
-
const setAssetAdding = useCallback((section: AssetSectionKey, adding: boolean) => {
|
|
195
|
-
setForm((prev) => ({
|
|
196
|
-
...prev,
|
|
197
|
-
assets: {
|
|
198
|
-
...prev.assets,
|
|
199
|
-
[section]: { ...prev.assets[section], adding },
|
|
200
|
-
},
|
|
201
|
-
}))
|
|
202
|
-
}, [])
|
|
203
|
-
|
|
204
|
-
const setAssetEditing = useCallback((section: AssetSectionKey, name?: string) => {
|
|
205
|
-
setForm((prev) => ({
|
|
206
|
-
...prev,
|
|
207
|
-
assets: {
|
|
208
|
-
...prev.assets,
|
|
209
|
-
[section]: { ...prev.assets[section], editing: name },
|
|
210
|
-
},
|
|
211
|
-
}))
|
|
212
|
-
}, [])
|
|
213
|
-
|
|
214
|
-
const toCreateOptions = useCallback(
|
|
215
|
-
(): CreateOptions => ({
|
|
216
|
-
name: form.fields.name.value,
|
|
217
|
-
version: form.fields.version.value,
|
|
218
|
-
description: form.fields.description.value,
|
|
219
|
-
skills: form.assets.skill.items,
|
|
220
|
-
commands: form.assets.command.items,
|
|
221
|
-
agents: form.assets.agent.items,
|
|
222
|
-
}),
|
|
223
|
-
[form],
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
const value = useMemo<FormStateContextValue>(
|
|
227
|
-
() => ({
|
|
228
|
-
form,
|
|
229
|
-
setFieldValue,
|
|
230
|
-
setFieldStatus,
|
|
231
|
-
addAsset,
|
|
232
|
-
removeAsset,
|
|
233
|
-
renameAsset,
|
|
234
|
-
setAssetDescription,
|
|
235
|
-
setAssetAdding,
|
|
236
|
-
setAssetEditing,
|
|
237
|
-
toCreateOptions,
|
|
238
|
-
}),
|
|
239
|
-
[
|
|
240
|
-
form,
|
|
241
|
-
setFieldValue,
|
|
242
|
-
setFieldStatus,
|
|
243
|
-
addAsset,
|
|
244
|
-
removeAsset,
|
|
245
|
-
renameAsset,
|
|
246
|
-
setAssetDescription,
|
|
247
|
-
setAssetAdding,
|
|
248
|
-
setAssetEditing,
|
|
249
|
-
toCreateOptions,
|
|
250
|
-
],
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
return createElement(FormStateContext.Provider, { value }, children)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// --- Hook ---
|
|
257
|
-
|
|
258
|
-
export function useFormState() {
|
|
259
|
-
return useContext(FormStateContext)
|
|
260
|
-
}
|
package/src/tui/editor.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process'
|
|
2
|
-
import { readFileSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
3
|
-
import { tmpdir } from 'node:os'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Opens the user's preferred terminal editor with the given content.
|
|
8
|
-
* Returns the edited content after the editor closes.
|
|
9
|
-
*
|
|
10
|
-
* This is synchronous — it blocks the event loop while the editor is open.
|
|
11
|
-
* Callers must ensure the TUI is in a safe state before calling.
|
|
12
|
-
*
|
|
13
|
-
* Uses $VISUAL, $EDITOR, or falls back to 'vi'.
|
|
14
|
-
*/
|
|
15
|
-
export function openInEditorSync(content: string, filename = 'description.md'): string | null {
|
|
16
|
-
const editor = process.env.VISUAL || process.env.EDITOR || 'vi'
|
|
17
|
-
const tmpFile = join(tmpdir(), `facet-${Date.now()}-${filename}`)
|
|
18
|
-
|
|
19
|
-
writeFileSync(tmpFile, content, 'utf-8')
|
|
20
|
-
|
|
21
|
-
// Split editor command to support args (e.g., "code --wait")
|
|
22
|
-
const [cmd, ...args] = editor.split(' ')
|
|
23
|
-
if (!cmd) return null
|
|
24
|
-
|
|
25
|
-
const result = spawnSync(cmd, [...args, tmpFile], {
|
|
26
|
-
stdio: 'inherit',
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
if (result.status !== 0) return null
|
|
30
|
-
|
|
31
|
-
const edited = readFileSync(tmpFile, 'utf-8')
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
unlinkSync(tmpFile)
|
|
35
|
-
} catch {
|
|
36
|
-
// Ignore cleanup failures
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return edited
|
|
40
|
-
}
|
package/src/tui/gradient.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { GRADIENT_STOPS, getAnimatedGradient } from '@agent-facets/brand'
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { useInput } from 'ink'
|
|
2
|
-
import { useCallback, useEffect, useRef } from 'react'
|
|
3
|
-
import { useFocusMode } from '../context/focus-mode-context.ts'
|
|
4
|
-
|
|
5
|
-
const EXIT_WINDOW_MS = 3000
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Handles exit via double-tap Escape with a visible countdown toast.
|
|
9
|
-
*
|
|
10
|
-
* First Escape (when not editing): sets mode to 'exit-modal', starts countdown.
|
|
11
|
-
* Second Escape (while in exit-modal): exits.
|
|
12
|
-
* After 3 seconds: returns to 'form-navigation'.
|
|
13
|
-
* Any other key while in exit-modal: returns to previous mode.
|
|
14
|
-
*/
|
|
15
|
-
export function useExitKeys(onExit: () => void) {
|
|
16
|
-
const { mode, setMode, setExitSecondsLeft } = useFocusMode()
|
|
17
|
-
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
18
|
-
const countdownRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
19
|
-
const previousModeRef = useRef(mode)
|
|
20
|
-
|
|
21
|
-
const clearTimers = useCallback(() => {
|
|
22
|
-
if (timerRef.current) {
|
|
23
|
-
clearTimeout(timerRef.current)
|
|
24
|
-
timerRef.current = null
|
|
25
|
-
}
|
|
26
|
-
if (countdownRef.current) {
|
|
27
|
-
clearInterval(countdownRef.current)
|
|
28
|
-
countdownRef.current = null
|
|
29
|
-
}
|
|
30
|
-
}, [])
|
|
31
|
-
|
|
32
|
-
const dismiss = useCallback(() => {
|
|
33
|
-
setExitSecondsLeft(0)
|
|
34
|
-
clearTimers()
|
|
35
|
-
setMode(previousModeRef.current === 'exit-modal' ? 'form-navigation' : previousModeRef.current)
|
|
36
|
-
}, [clearTimers, setMode, setExitSecondsLeft])
|
|
37
|
-
|
|
38
|
-
const startExitWindow = useCallback(() => {
|
|
39
|
-
previousModeRef.current = mode
|
|
40
|
-
setMode('exit-modal')
|
|
41
|
-
setExitSecondsLeft(3)
|
|
42
|
-
|
|
43
|
-
clearTimers()
|
|
44
|
-
|
|
45
|
-
countdownRef.current = setInterval(() => {
|
|
46
|
-
setExitSecondsLeft((prev: number) => {
|
|
47
|
-
if (prev <= 1) return 0
|
|
48
|
-
return prev - 1
|
|
49
|
-
})
|
|
50
|
-
}, 1000)
|
|
51
|
-
|
|
52
|
-
timerRef.current = setTimeout(() => {
|
|
53
|
-
dismiss()
|
|
54
|
-
}, EXIT_WINDOW_MS)
|
|
55
|
-
}, [clearTimers, dismiss, mode, setMode, setExitSecondsLeft])
|
|
56
|
-
|
|
57
|
-
useEffect(() => clearTimers, [clearTimers])
|
|
58
|
-
|
|
59
|
-
useInput((_input, key) => {
|
|
60
|
-
if (key.escape) {
|
|
61
|
-
if (mode === 'exit-modal') {
|
|
62
|
-
clearTimers()
|
|
63
|
-
onExit()
|
|
64
|
-
} else if (mode !== 'field-revision') {
|
|
65
|
-
startExitWindow()
|
|
66
|
-
}
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Any other key while in exit-modal dismisses it
|
|
71
|
-
if (mode === 'exit-modal') {
|
|
72
|
-
dismiss()
|
|
73
|
-
}
|
|
74
|
-
})
|
|
75
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { useInput } from 'ink'
|
|
2
|
-
import { useFocusMode } from '../context/focus-mode-context.ts'
|
|
3
|
-
import { useFocusOrder } from '../context/focus-order-context.ts'
|
|
4
|
-
|
|
5
|
-
export function useNavigationKeys() {
|
|
6
|
-
const { mode } = useFocusMode()
|
|
7
|
-
const { focusNext, focusPrevious } = useFocusOrder()
|
|
8
|
-
|
|
9
|
-
const isActive = mode === 'form-navigation' || mode === 'form-confirmation'
|
|
10
|
-
|
|
11
|
-
useInput(
|
|
12
|
-
(_input, key) => {
|
|
13
|
-
if (key.downArrow || key.tab) {
|
|
14
|
-
focusNext()
|
|
15
|
-
return
|
|
16
|
-
}
|
|
17
|
-
if (key.upArrow || (key.shift && key.tab)) {
|
|
18
|
-
focusPrevious()
|
|
19
|
-
return
|
|
20
|
-
}
|
|
21
|
-
if (mode === 'form-confirmation') {
|
|
22
|
-
if (key.rightArrow) {
|
|
23
|
-
focusNext()
|
|
24
|
-
return
|
|
25
|
-
}
|
|
26
|
-
if (key.leftArrow) {
|
|
27
|
-
focusPrevious()
|
|
28
|
-
return
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
{ isActive },
|
|
33
|
-
)
|
|
34
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from 'ink'
|
|
2
|
-
import Gradient from 'ink-gradient'
|
|
3
|
-
import type { ReactNode } from 'react'
|
|
4
|
-
import { useEffect, useState } from 'react'
|
|
5
|
-
import { ExitFooter } from '../components/exit-toast.tsx'
|
|
6
|
-
import { GRADIENT_STOPS, getAnimatedGradient } from '../gradient.ts'
|
|
7
|
-
import { THEME } from '../theme.ts'
|
|
8
|
-
|
|
9
|
-
const ANIMATION_INTERVAL_MS = 75
|
|
10
|
-
|
|
11
|
-
function AnimatedGradientText({ text }: { text: string }) {
|
|
12
|
-
const [offset, setOffset] = useState(0)
|
|
13
|
-
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
const interval = setInterval(() => {
|
|
16
|
-
setOffset((prev) => (prev + 1) % GRADIENT_STOPS.length)
|
|
17
|
-
}, ANIMATION_INTERVAL_MS)
|
|
18
|
-
return () => clearInterval(interval)
|
|
19
|
-
}, [])
|
|
20
|
-
|
|
21
|
-
return (
|
|
22
|
-
<Gradient colors={getAnimatedGradient(offset)}>
|
|
23
|
-
<Text bold>{text}</Text>
|
|
24
|
-
</Gradient>
|
|
25
|
-
)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function WizardLayout({ children }: { children: ReactNode }) {
|
|
29
|
-
return (
|
|
30
|
-
<Box flexDirection="column" padding={1} gap={1}>
|
|
31
|
-
<Box borderStyle="round" borderColor={THEME.brand} paddingX={2} gap={1}>
|
|
32
|
-
<Text bold color={THEME.brand}>
|
|
33
|
-
Create a new
|
|
34
|
-
</Text>
|
|
35
|
-
<AnimatedGradientText text="FACET" />
|
|
36
|
-
</Box>
|
|
37
|
-
{children}
|
|
38
|
-
<ExitFooter />
|
|
39
|
-
</Box>
|
|
40
|
-
)
|
|
41
|
-
}
|
package/src/tui/theme.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { THEME } from '@agent-facets/brand'
|