ethagent 1.1.0 → 1.1.2
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/README.md +19 -80
- package/package.json +10 -8
- package/src/app/FirstRun.tsx +2 -0
- package/src/chat/ChatScreen.tsx +8 -3
- package/src/chat/ContinuityEditReviewView.tsx +6 -0
- package/src/chat/ConversationStack.tsx +3 -0
- package/src/chat/TranscriptView.tsx +6 -0
- package/src/chat/chatTurnOrchestrator.ts +1 -1
- package/src/cli/updateNotice.ts +1 -1
- package/src/identity/hub/IdentityHub.tsx +4 -2
- package/src/identity/hub/identityHubModel.ts +80 -0
- package/src/identity/hub/identityHubReducer.ts +2 -2
- package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +14 -2
- package/src/identity/hub/screens/EditProfileFlow.tsx +1 -0
- package/src/identity/hub/screens/ErrorScreen.tsx +1 -1
- package/src/identity/hub/screens/IdentitySummary.tsx +36 -20
- package/src/identity/hub/screens/MenuScreen.tsx +2 -2
- package/src/identity/hub/screens/RecoveryConfirmScreen.tsx +24 -16
- package/src/identity/hub/screens/StorageCredentialScreen.tsx +3 -0
- package/src/identity/wallet/wallet-page/wallet.html +370 -379
- package/src/ui/Select.tsx +1 -1
- package/src/ui/Surface.tsx +1 -1
- package/src/ui/TextInput.tsx +142 -23
package/src/ui/Select.tsx
CHANGED
|
@@ -73,7 +73,7 @@ export function Select<T>({
|
|
|
73
73
|
else if (key.return) {
|
|
74
74
|
const selected = options[index]
|
|
75
75
|
if (isSelectableOption(selected)) onSubmit(selected.value)
|
|
76
|
-
} else if (key.escape) {
|
|
76
|
+
} else if (key.escape || (key.ctrl && input === 'c')) {
|
|
77
77
|
onCancel?.()
|
|
78
78
|
}
|
|
79
79
|
})
|
package/src/ui/Surface.tsx
CHANGED
|
@@ -27,7 +27,7 @@ export const Surface: React.FC<SurfaceProps> = ({
|
|
|
27
27
|
}) => {
|
|
28
28
|
const borderColor = toneColor[tone]
|
|
29
29
|
return (
|
|
30
|
-
<Box flexDirection="column" borderStyle="round" borderColor={borderColor} paddingX={2} paddingY={0}>
|
|
30
|
+
<Box flexDirection="column" borderStyle="round" borderColor={borderColor} paddingX={2} paddingY={0} width="100%">
|
|
31
31
|
<Box flexDirection="column">
|
|
32
32
|
<Text color={borderColor} bold>{title}</Text>
|
|
33
33
|
{subtitle ? (
|
package/src/ui/TextInput.tsx
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
2
|
-
import { Box, Text } from 'ink'
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react'
|
|
2
|
+
import { Box, Text, useStdout } from 'ink'
|
|
3
3
|
import { theme } from './theme.js'
|
|
4
4
|
import { useAppInput } from '../app/input/AppInputProvider.js'
|
|
5
|
+
import { moveVerticalVisual } from '../chat/chatInputState.js'
|
|
6
|
+
import {
|
|
7
|
+
getVisualLineIndex,
|
|
8
|
+
getVisualLines,
|
|
9
|
+
} from '../chat/textCursor.js'
|
|
10
|
+
|
|
11
|
+
// ConversationStack padding=1 (2) + Surface border (2) + Surface paddingX=2 (4) + '> ' prefix (2) = 10
|
|
12
|
+
const DEFAULT_CHROME_WIDTH = 10
|
|
5
13
|
|
|
6
14
|
type TextInputProps = {
|
|
7
15
|
label?: string
|
|
@@ -9,62 +17,116 @@ type TextInputProps = {
|
|
|
9
17
|
isSecret?: boolean
|
|
10
18
|
initialValue?: string
|
|
11
19
|
allowEmpty?: boolean
|
|
20
|
+
multiline?: boolean
|
|
21
|
+
chromeWidth?: number
|
|
12
22
|
maxLength?: number
|
|
13
23
|
validate?: (value: string) => string | null
|
|
14
24
|
onSubmit: (value: string) => void
|
|
15
25
|
onCancel?: () => void
|
|
16
26
|
}
|
|
17
27
|
|
|
28
|
+
type RenderedTextInputLine = {
|
|
29
|
+
visualLineIndex: number
|
|
30
|
+
node: React.ReactNode
|
|
31
|
+
}
|
|
32
|
+
|
|
18
33
|
export function TextInput({
|
|
19
34
|
label,
|
|
20
35
|
placeholder,
|
|
21
36
|
isSecret,
|
|
22
37
|
initialValue = '',
|
|
23
38
|
allowEmpty = false,
|
|
39
|
+
multiline = false,
|
|
40
|
+
chromeWidth = DEFAULT_CHROME_WIDTH,
|
|
24
41
|
maxLength = 4096,
|
|
25
42
|
validate,
|
|
26
43
|
onSubmit,
|
|
27
44
|
onCancel,
|
|
28
45
|
}: TextInputProps) {
|
|
46
|
+
const { stdout } = useStdout()
|
|
29
47
|
const [value, setValue] = useState(initialValue)
|
|
48
|
+
const [cursor, setCursor] = useState(initialValue.length)
|
|
49
|
+
const [preferredColumn, setPreferredColumn] = useState<number | null>(null)
|
|
30
50
|
const [error, setError] = useState<string | null>(null)
|
|
31
51
|
|
|
52
|
+
// Keep a columns state updated via resize, matching ChatInput's pattern exactly
|
|
53
|
+
const [columns, setColumns] = useState<number>(() => Math.floor(stdout?.columns ?? 80))
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!stdout) return
|
|
56
|
+
const handleResize = () => setColumns(Math.floor(stdout.columns ?? 80))
|
|
57
|
+
stdout.on('resize', handleResize)
|
|
58
|
+
return () => { stdout.off('resize', handleResize) }
|
|
59
|
+
}, [stdout])
|
|
60
|
+
|
|
61
|
+
const wrapWidth = textInputWrapWidth(columns, chromeWidth)
|
|
62
|
+
|
|
63
|
+
// Sync refs during render so the input handler always reads fresh values,
|
|
64
|
+
// even if AppInputProvider fires before the next useEffect cycle updates handlerRef.
|
|
65
|
+
const stateRef = useRef({ value, cursor, preferredColumn, wrapWidth })
|
|
66
|
+
stateRef.current = { value, cursor, preferredColumn, wrapWidth }
|
|
67
|
+
|
|
32
68
|
useAppInput((input, key) => {
|
|
69
|
+
const { value: val, cursor: cur, preferredColumn: prefCol, wrapWidth: ww } = stateRef.current
|
|
70
|
+
|
|
33
71
|
if (key.return) {
|
|
34
|
-
if (!allowEmpty &&
|
|
72
|
+
if (!allowEmpty && val.trim().length === 0) {
|
|
35
73
|
setError('value cannot be empty')
|
|
36
74
|
return
|
|
37
75
|
}
|
|
38
|
-
const validationError = validate?.(
|
|
76
|
+
const validationError = validate?.(val) ?? null
|
|
39
77
|
if (validationError) {
|
|
40
78
|
setError(validationError)
|
|
41
79
|
return
|
|
42
80
|
}
|
|
43
81
|
setError(null)
|
|
44
|
-
onSubmit(
|
|
82
|
+
onSubmit(val)
|
|
45
83
|
return
|
|
46
84
|
}
|
|
47
|
-
if (key.escape) {
|
|
85
|
+
if (key.escape || (key.ctrl && input === 'c')) {
|
|
48
86
|
onCancel?.()
|
|
49
87
|
return
|
|
50
88
|
}
|
|
89
|
+
if (key.leftArrow) {
|
|
90
|
+
setCursor(Math.max(0, cur - 1))
|
|
91
|
+
setPreferredColumn(null)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
if (key.rightArrow) {
|
|
95
|
+
setCursor(Math.min(val.length, cur + 1))
|
|
96
|
+
setPreferredColumn(null)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
if (multiline && (key.upArrow || key.downArrow)) {
|
|
100
|
+
const result = moveVerticalVisual(val, cur, key.upArrow ? -1 : 1, ww, prefCol)
|
|
101
|
+
if (result.kind === 'moved') setCursor(result.cursor)
|
|
102
|
+
setPreferredColumn(result.preferredColumn)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
51
105
|
if (key.backspace || key.delete) {
|
|
52
|
-
|
|
106
|
+
if (cur === 0) return
|
|
107
|
+
setValue(val.slice(0, cur - 1) + val.slice(cur))
|
|
108
|
+
setCursor(cur - 1)
|
|
109
|
+
setPreferredColumn(null)
|
|
53
110
|
if (error) setError(null)
|
|
54
111
|
return
|
|
55
112
|
}
|
|
56
113
|
if (key.ctrl && input === 'u') {
|
|
57
114
|
setValue('')
|
|
115
|
+
setCursor(0)
|
|
116
|
+
setPreferredColumn(null)
|
|
58
117
|
if (error) setError(null)
|
|
59
118
|
return
|
|
60
119
|
}
|
|
61
|
-
if (key.ctrl || key.meta || key.
|
|
120
|
+
if (key.ctrl || key.meta || key.upArrow || key.downArrow || key.tab) {
|
|
62
121
|
return
|
|
63
122
|
}
|
|
64
123
|
if (input) {
|
|
65
124
|
const clean = input.replace(/[\r\n]/g, '')
|
|
66
125
|
if (clean) {
|
|
67
|
-
|
|
126
|
+
const next = (val.slice(0, cur) + clean + val.slice(cur)).slice(0, maxLength)
|
|
127
|
+
setValue(next)
|
|
128
|
+
setCursor(Math.min(cur + clean.length, maxLength))
|
|
129
|
+
setPreferredColumn(null)
|
|
68
130
|
if (error) setError(null)
|
|
69
131
|
}
|
|
70
132
|
}
|
|
@@ -72,26 +134,83 @@ export function TextInput({
|
|
|
72
134
|
|
|
73
135
|
const display = isSecret ? '*'.repeat(value.length) : value
|
|
74
136
|
const showPlaceholder = value.length === 0 && placeholder
|
|
137
|
+
const renderedLines = multiline
|
|
138
|
+
? renderTextInputLines(display, cursor, true, wrapWidth)
|
|
139
|
+
: []
|
|
75
140
|
|
|
76
141
|
return (
|
|
77
142
|
<Box flexDirection="column">
|
|
78
143
|
{label ? <Text color={theme.dim}>{label}</Text> : null}
|
|
79
|
-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
144
|
+
{multiline && !showPlaceholder ? (
|
|
145
|
+
<Box flexDirection="column">
|
|
146
|
+
{renderedLines.map(line => (
|
|
147
|
+
<Box key={line.visualLineIndex} flexDirection="row">
|
|
148
|
+
<Text color={line.visualLineIndex === 0 ? theme.accentPrimary : theme.dim}>
|
|
149
|
+
{line.visualLineIndex === 0 ? '> ' : ' '}
|
|
150
|
+
</Text>
|
|
151
|
+
<Box width={wrapWidth}>{line.node}</Box>
|
|
152
|
+
</Box>
|
|
153
|
+
))}
|
|
154
|
+
</Box>
|
|
155
|
+
) : (
|
|
156
|
+
<Box flexDirection="row">
|
|
157
|
+
<Text color={theme.accentPrimary}>{'> '}</Text>
|
|
158
|
+
<Box width={wrapWidth}>
|
|
159
|
+
{showPlaceholder ? (
|
|
160
|
+
<Text wrap={multiline ? 'wrap' : 'truncate-end'}>
|
|
161
|
+
<Text backgroundColor={theme.accentMint} color="#08110c">{' '}</Text>
|
|
162
|
+
<Text color={theme.dim}>{placeholder}</Text>
|
|
163
|
+
</Text>
|
|
164
|
+
) : (
|
|
165
|
+
<Text color={theme.text} wrap="truncate-end">
|
|
166
|
+
{display.slice(0, cursor)}
|
|
167
|
+
<Text backgroundColor={theme.accentMint} color="#08110c">{display[cursor] ?? ' '}</Text>
|
|
168
|
+
{display.slice(cursor + 1)}
|
|
169
|
+
</Text>
|
|
170
|
+
)}
|
|
171
|
+
</Box>
|
|
172
|
+
</Box>
|
|
173
|
+
)}
|
|
93
174
|
{error ? <Text color="#e87070">{error}</Text> : null}
|
|
94
175
|
</Box>
|
|
95
176
|
)
|
|
96
177
|
}
|
|
97
178
|
|
|
179
|
+
export function textInputWrapWidth(columns: number, chromeWidth = DEFAULT_CHROME_WIDTH): number {
|
|
180
|
+
return Math.max(1, Math.floor(columns) - Math.max(0, Math.floor(chromeWidth)))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function renderTextInputLines(
|
|
184
|
+
value: string,
|
|
185
|
+
cursor: number,
|
|
186
|
+
showCursor: boolean,
|
|
187
|
+
wrapWidth: number,
|
|
188
|
+
): RenderedTextInputLine[] {
|
|
189
|
+
const lines = getVisualLines(value, wrapWidth)
|
|
190
|
+
const cursorLine = getVisualLineIndex(lines, cursor)
|
|
191
|
+
|
|
192
|
+
return lines.map((line, visualLineIndex) => {
|
|
193
|
+
const text = value.slice(line.start, line.end)
|
|
194
|
+
if (!showCursor || visualLineIndex !== cursorLine) {
|
|
195
|
+
return {
|
|
196
|
+
visualLineIndex,
|
|
197
|
+
node: <Text color={theme.text} wrap="wrap">{text || ' '}</Text>,
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const column = Math.max(0, Math.min(cursor - line.start, text.length))
|
|
202
|
+
const before = text.slice(0, column)
|
|
203
|
+
const atChar = text[column] ?? ' '
|
|
204
|
+
const after = text.slice(column + 1)
|
|
205
|
+
return {
|
|
206
|
+
visualLineIndex,
|
|
207
|
+
node: (
|
|
208
|
+
<Text color={theme.text} wrap="wrap">
|
|
209
|
+
{before}
|
|
210
|
+
<Text backgroundColor={theme.accentMint} color="#08110c">{atChar}</Text>
|
|
211
|
+
{after}
|
|
212
|
+
</Text>
|
|
213
|
+
),
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
}
|