ethagent 2.4.0 → 3.0.0
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 +7 -4
- package/package.json +2 -1
- package/src/app/FirstRun.tsx +155 -15
- package/src/app/FirstRunTimeline.tsx +4 -0
- package/src/app/input/AppInputProvider.tsx +19 -0
- package/src/app/input/appInputParser.ts +19 -4
- package/src/chat/ChatBottomPane.tsx +3 -1
- package/src/chat/ChatScreen.tsx +7 -1
- package/src/chat/ConversationStack.tsx +25 -19
- package/src/chat/MessageList.tsx +194 -53
- package/src/chat/chatSessionState.ts +1 -1
- package/src/chat/chatTurnOrchestrator.ts +59 -0
- package/src/chat/input/ChatInput.tsx +3 -0
- package/src/chat/input/textCursor.ts +13 -3
- package/src/chat/transcript/TranscriptView.tsx +7 -5
- package/src/chat/transcript/transcriptViewport.ts +88 -17
- package/src/chat/views/PermissionPrompt.tsx +26 -26
- package/src/chat/views/PermissionsView.tsx +18 -12
- package/src/chat/views/RewindView.tsx +3 -1
- package/src/cli/ResetConfirmView.tsx +24 -9
- package/src/identity/continuity/editor.ts +27 -2
- package/src/identity/continuity/envelope.ts +125 -0
- package/src/identity/continuity/publicSkills.ts +37 -1
- package/src/identity/continuity/skills/frontmatter.ts +183 -0
- package/src/identity/continuity/skills/loadSkills.ts +609 -0
- package/src/identity/continuity/skills/publicSkillsSync.ts +32 -0
- package/src/identity/continuity/skills/scaffold.ts +52 -0
- package/src/identity/continuity/skills/types.ts +30 -0
- package/src/identity/continuity/storage/defaults.ts +28 -47
- package/src/identity/continuity/storage/files.ts +1 -0
- package/src/identity/continuity/storage/paths.ts +1 -0
- package/src/identity/continuity/storage/scaffold.ts +25 -23
- package/src/identity/continuity/storage/status.ts +34 -5
- package/src/identity/continuity/storage/types.ts +3 -2
- package/src/identity/continuity/storage.ts +3 -0
- package/src/identity/hub/OperationalRoutes.tsx +105 -3
- package/src/identity/hub/Routes.tsx +5 -3
- package/src/identity/hub/continuity/ContinuityDashboardScreen.tsx +5 -51
- package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +1 -1
- package/src/identity/hub/continuity/SavePromptScreen.tsx +1 -0
- package/src/identity/hub/continuity/effects.ts +36 -5
- package/src/identity/hub/continuity/skills/DeleteSkillConfirmScreen.tsx +112 -0
- package/src/identity/hub/continuity/skills/DeleteSkillScreen.tsx +123 -0
- package/src/identity/hub/continuity/skills/NewSkillScreen.tsx +57 -0
- package/src/identity/hub/continuity/skills/NewSkillVisibilityScreen.tsx +52 -0
- package/src/identity/hub/continuity/skills/SkillVisibilityScreen.tsx +171 -0
- package/src/identity/hub/continuity/skills/SkillsTreeScreen.tsx +213 -0
- package/src/identity/hub/continuity/snapshot.ts +3 -0
- package/src/identity/hub/continuity/state.ts +3 -2
- package/src/identity/hub/continuity/vault.ts +42 -10
- package/src/identity/hub/custody/CustodyEditFlow.tsx +3 -3
- package/src/identity/hub/identityHubReducer.ts +21 -0
- package/src/identity/hub/profile/effects.ts +16 -3
- package/src/identity/hub/restore/RestoreFlow.tsx +43 -6
- package/src/identity/hub/restore/apply.ts +12 -1
- package/src/identity/hub/restore/recovery.ts +11 -1
- package/src/identity/hub/restore/resolve.ts +1 -1
- package/src/identity/hub/restore/useRestoreEffects.ts +4 -6
- package/src/identity/hub/shared/components/DetailsScreen.tsx +4 -1
- package/src/identity/hub/shared/components/IdentitySummary.tsx +97 -53
- package/src/identity/hub/shared/components/MenuScreen.tsx +18 -15
- package/src/identity/hub/shared/components/UnlinkedIdentityScreen.tsx +1 -1
- package/src/identity/hub/shared/components/menuFlagsFromReconciliation.ts +8 -12
- package/src/identity/hub/shared/effects/sync.ts +16 -3
- package/src/identity/hub/shared/model/copy.ts +2 -4
- package/src/identity/hub/transfer/effects.ts +15 -2
- package/src/identity/hub/useIdentityHubContinuity.ts +145 -23
- package/src/identity/hub/useIdentityHubController.ts +5 -1
- package/src/identity/hub/useIdentityHubSideEffects.ts +2 -4
- package/src/mcp/manager.ts +1 -1
- package/src/models/ModelPicker.tsx +89 -84
- package/src/models/llamacpp.ts +160 -11
- package/src/models/llamacppPreflight.ts +1 -16
- package/src/models/modelPickerOptions.ts +43 -37
- package/src/providers/contracts.ts +1 -0
- package/src/providers/openai-chat.ts +50 -9
- package/src/providers/openai-responses.ts +19 -4
- package/src/runtime/toolExecution.ts +4 -3
- package/src/runtime/turn.ts +61 -30
- package/src/tools/changeDirectoryTool.ts +1 -1
- package/src/tools/contracts.ts +10 -0
- package/src/tools/deleteFileTool.ts +1 -1
- package/src/tools/editTool.ts +1 -1
- package/src/tools/listDirectoryTool.ts +1 -1
- package/src/tools/listSkillFilesTool.ts +77 -0
- package/src/tools/listSkillsTool.ts +68 -0
- package/src/tools/mcpResourceTools.ts +2 -2
- package/src/tools/privateContinuityReadTool.ts +1 -1
- package/src/tools/readSkillTool.ts +107 -0
- package/src/tools/readTool.ts +1 -1
- package/src/tools/registry.ts +6 -0
- package/src/tools/writeFileTool.ts +22 -2
- package/src/ui/Spinner.tsx +1 -1
- package/src/identity/continuity/localBackup.ts +0 -249
- package/src/identity/continuity/zipWriter.ts +0 -95
- package/src/identity/hub/continuity/index.ts +0 -7
- package/src/identity/hub/ens/index.ts +0 -11
- package/src/identity/hub/restore/index.ts +0 -22
|
@@ -37,6 +37,7 @@ export const SavePromptScreen: React.FC<SavePromptScreenProps> = ({ workingStatu
|
|
|
37
37
|
options={[
|
|
38
38
|
{ value: 'save-now', role: 'section', label: 'Save' },
|
|
39
39
|
{ value: 'save-now', label: 'Save now', hint: 'Sign once and save the encrypted snapshot' },
|
|
40
|
+
{ value: 'later', role: 'section', label: 'Defer' },
|
|
40
41
|
{ value: 'later', label: 'Not now', hint: 'Ask again on the next ethagent launch', role: 'utility' },
|
|
41
42
|
]}
|
|
42
43
|
hintLayout="inline"
|
|
@@ -4,14 +4,25 @@ import type { EthagentIdentity } from '../../../storage/config.js'
|
|
|
4
4
|
import {
|
|
5
5
|
continuityVaultStatus,
|
|
6
6
|
prepareSyncedIdentityMarkdownScaffold,
|
|
7
|
+
prepareSyncedSkillsTree,
|
|
7
8
|
readContinuityFiles,
|
|
8
9
|
readPublicSkillsFile,
|
|
9
10
|
writeIdentityMarkdownScaffold,
|
|
10
11
|
type IdentityMarkdownScaffold,
|
|
11
12
|
} from '../../continuity/storage.js'
|
|
13
|
+
import {
|
|
14
|
+
appendPublicSkillEntries,
|
|
15
|
+
defaultPublicSkillsProfile as basePublicSkillsProfile,
|
|
16
|
+
} from '../../continuity/publicSkills.js'
|
|
17
|
+
import {
|
|
18
|
+
derivePublicSkillEntries,
|
|
19
|
+
syncPublicSkillsManifest,
|
|
20
|
+
} from '../../continuity/skills/publicSkillsSync.js'
|
|
12
21
|
import {
|
|
13
22
|
createWalletRestoreAccessChallenge,
|
|
14
23
|
serializeContinuitySnapshotEnvelope,
|
|
24
|
+
type ContinuityFiles,
|
|
25
|
+
type ContinuitySkillsTree,
|
|
15
26
|
type WalletChallengePurpose,
|
|
16
27
|
} from '../../continuity/envelope.js'
|
|
17
28
|
import {
|
|
@@ -78,6 +89,11 @@ type RebackupPreparedTransaction = {
|
|
|
78
89
|
publicSkills: PublicSkillsMetadata
|
|
79
90
|
identity: EthagentIdentity
|
|
80
91
|
markdownScaffold?: IdentityMarkdownScaffold
|
|
92
|
+
publishedSources: {
|
|
93
|
+
privateFiles: ContinuityFiles
|
|
94
|
+
publicSkills: string
|
|
95
|
+
skills: ContinuitySkillsTree
|
|
96
|
+
}
|
|
81
97
|
}
|
|
82
98
|
|
|
83
99
|
export async function runRebackupPreflight(
|
|
@@ -224,18 +240,22 @@ async function runRebackupSigningInner(
|
|
|
224
240
|
const continuityFiles = markdownScaffold
|
|
225
241
|
? { 'SOUL.md': markdownScaffold['SOUL.md'], 'MEMORY.md': markdownScaffold['MEMORY.md'] }
|
|
226
242
|
: await readContinuityFiles(nextIdentityForFiles)
|
|
227
|
-
const publicSkillsJson =
|
|
228
|
-
? markdownScaffold['skills.json']
|
|
229
|
-
: await readPublicSkillsFile(nextIdentityForFiles)
|
|
243
|
+
const publicSkillsJson = await syncPublicSkillsManifest(nextIdentityForFiles)
|
|
230
244
|
const publicSkillsPin = await addToIpfs(DEFAULT_IPFS_API_URL, publicSkillsJson, fetch, { pinataJwt: step.pinataJwt })
|
|
231
245
|
assertVerifiedPin(publicSkillsPin)
|
|
246
|
+
const publicSkillEntries = await derivePublicSkillEntries(nextIdentityForFiles)
|
|
247
|
+
const augmentedPublicProfile = appendPublicSkillEntries(
|
|
248
|
+
basePublicSkillsProfile(nextIdentityForFiles),
|
|
249
|
+
publicSkillEntries,
|
|
250
|
+
)
|
|
232
251
|
const agentCardPin = await addToIpfs(
|
|
233
252
|
DEFAULT_IPFS_API_URL,
|
|
234
|
-
serializeAgentCard(createAgentCard(
|
|
253
|
+
serializeAgentCard(createAgentCard(augmentedPublicProfile)),
|
|
235
254
|
fetch,
|
|
236
255
|
{ pinataJwt: step.pinataJwt },
|
|
237
256
|
)
|
|
238
257
|
assertVerifiedPin(agentCardPin)
|
|
258
|
+
const skillsTree = await prepareSyncedSkillsTree(nextIdentityForFiles)
|
|
239
259
|
const envelope = createContinuityEnvelopeForSave({
|
|
240
260
|
identity: nextIdentityForFiles,
|
|
241
261
|
registry: step.registry,
|
|
@@ -244,6 +264,7 @@ async function runRebackupSigningInner(
|
|
|
244
264
|
walletSignature: wallet.signature,
|
|
245
265
|
state,
|
|
246
266
|
files: continuityFiles,
|
|
267
|
+
skills: skillsTree,
|
|
247
268
|
walletAccess,
|
|
248
269
|
...(challengePurpose ? { challengePurpose } : {}),
|
|
249
270
|
})
|
|
@@ -310,6 +331,11 @@ async function runRebackupSigningInner(
|
|
|
310
331
|
publicSkills,
|
|
311
332
|
identity: { ...step.identity, state },
|
|
312
333
|
...(markdownScaffold ? { markdownScaffold } : {}),
|
|
334
|
+
publishedSources: {
|
|
335
|
+
privateFiles: continuityFiles,
|
|
336
|
+
publicSkills: publicSkillsJson,
|
|
337
|
+
skills: skillsTree,
|
|
338
|
+
},
|
|
313
339
|
},
|
|
314
340
|
}
|
|
315
341
|
}
|
|
@@ -330,6 +356,11 @@ async function runRebackupSigningInner(
|
|
|
330
356
|
publicSkills,
|
|
331
357
|
identity: { ...step.identity, state },
|
|
332
358
|
...(markdownScaffold ? { markdownScaffold } : {}),
|
|
359
|
+
publishedSources: {
|
|
360
|
+
privateFiles: continuityFiles,
|
|
361
|
+
publicSkills: publicSkillsJson,
|
|
362
|
+
skills: skillsTree,
|
|
363
|
+
},
|
|
333
364
|
},
|
|
334
365
|
}
|
|
335
366
|
},
|
|
@@ -359,7 +390,7 @@ async function runRebackupSigningInner(
|
|
|
359
390
|
await writeIdentityMarkdownScaffold(nextIdentity, result.prepared.markdownScaffold)
|
|
360
391
|
}
|
|
361
392
|
await recordPublishedContinuitySnapshot({ identity: nextIdentity, label: 'published encrypted snapshot' }).catch(() => null)
|
|
362
|
-
await markCurrentContinuityFilesPublished(nextIdentity).catch(() => null)
|
|
393
|
+
await markCurrentContinuityFilesPublished(nextIdentity, result.prepared.publishedSources).catch(() => null)
|
|
363
394
|
const resolverSyncWarning = await syncVaultOperatorsAfterOwnerSave({
|
|
364
395
|
beforeIdentity: step.identity,
|
|
365
396
|
afterIdentity: nextIdentity,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
import { Surface } from '../../../../ui/Surface.js'
|
|
4
|
+
import { Select } from '../../../../ui/Select.js'
|
|
5
|
+
import { theme } from '../../../../ui/theme.js'
|
|
6
|
+
import type { EthagentIdentity } from '../../../../storage/config.js'
|
|
7
|
+
import {
|
|
8
|
+
listSkills,
|
|
9
|
+
listSkillFiles,
|
|
10
|
+
type SkillFileEntry,
|
|
11
|
+
} from '../../../continuity/skills/loadSkills.js'
|
|
12
|
+
import { continuityVaultRef } from '../../../continuity/storage.js'
|
|
13
|
+
import type { SkillIndexEntry } from '../../../continuity/skills/types.js'
|
|
14
|
+
|
|
15
|
+
type ConfirmChoice = 'delete' | 'cancel'
|
|
16
|
+
|
|
17
|
+
type DeleteTarget = { kind: 'skill'; relativePath: string }
|
|
18
|
+
|
|
19
|
+
interface DeleteSkillConfirmScreenProps {
|
|
20
|
+
identity?: EthagentIdentity
|
|
21
|
+
target: DeleteTarget
|
|
22
|
+
footer: React.ReactNode
|
|
23
|
+
onConfirm: () => void
|
|
24
|
+
onCancel: () => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const MAX_LISTED_FILES = 10
|
|
28
|
+
|
|
29
|
+
export const DeleteSkillConfirmScreen: React.FC<DeleteSkillConfirmScreenProps> = ({
|
|
30
|
+
identity,
|
|
31
|
+
target,
|
|
32
|
+
footer,
|
|
33
|
+
onConfirm,
|
|
34
|
+
onCancel,
|
|
35
|
+
}) => {
|
|
36
|
+
const [entries, setEntries] = useState<SkillIndexEntry[] | null>(null)
|
|
37
|
+
const [files, setFiles] = useState<SkillFileEntry[] | null>(null)
|
|
38
|
+
const skillName = target.relativePath.split('/')[0] ?? ''
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
let cancelled = false
|
|
42
|
+
if (!identity) { setEntries([]); setFiles([]); return () => { cancelled = true } }
|
|
43
|
+
setEntries(null)
|
|
44
|
+
setFiles(null)
|
|
45
|
+
listSkills(identity)
|
|
46
|
+
.then(result => { if (!cancelled) setEntries(result) })
|
|
47
|
+
.catch(() => { if (!cancelled) setEntries([]) })
|
|
48
|
+
listSkillFiles(identity, skillName)
|
|
49
|
+
.then(result => { if (!cancelled) setFiles(result) })
|
|
50
|
+
.catch(() => { if (!cancelled) setFiles([]) })
|
|
51
|
+
return () => { cancelled = true }
|
|
52
|
+
}, [identity, target.relativePath, skillName])
|
|
53
|
+
|
|
54
|
+
const ref = identity ? continuityVaultRef(identity) : undefined
|
|
55
|
+
const skillMissing = entries !== null && !entries.some(entry => entry.relativePath === target.relativePath)
|
|
56
|
+
const supportingCount = (files ?? []).filter(f => f.relativePath !== 'SKILL.md').length
|
|
57
|
+
const subtitle = skillMissing
|
|
58
|
+
? `Skill ${skillName} was already removed elsewhere. Cancel and refresh.`
|
|
59
|
+
: supportingCount > 0
|
|
60
|
+
? `Removes ${skillName}/ and ${supportingCount} supporting file${supportingCount === 1 ? '' : 's'}. This cannot be undone.`
|
|
61
|
+
: `Removes ${skillName}/. This cannot be undone.`
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Surface title={`Delete ${skillName}?`} subtitle={subtitle} footer={footer} tone="error">
|
|
65
|
+
<Box marginTop={1} flexDirection="column">
|
|
66
|
+
<Text color={theme.dim}>This will remove:</Text>
|
|
67
|
+
{ref && (
|
|
68
|
+
<Box marginLeft={2} flexDirection="column">
|
|
69
|
+
<Text color={theme.text}>{ref.skillsDir}/{skillName}/</Text>
|
|
70
|
+
{files === null ? (
|
|
71
|
+
<Text color={theme.dim}>(loading folder contents...)</Text>
|
|
72
|
+
) : (
|
|
73
|
+
<FolderContents files={files} />
|
|
74
|
+
)}
|
|
75
|
+
</Box>
|
|
76
|
+
)}
|
|
77
|
+
</Box>
|
|
78
|
+
<Box marginTop={1}>
|
|
79
|
+
<Select<ConfirmChoice>
|
|
80
|
+
options={[
|
|
81
|
+
{ value: 'delete', label: 'Yes, delete', hint: skillMissing ? 'Skill already missing on disk' : 'Permanent. Will be reflected in the next snapshot.', bold: true, disabled: skillMissing },
|
|
82
|
+
{ value: 'cancel', label: 'No, back', hint: 'Return to the delete picker', role: 'utility' },
|
|
83
|
+
]}
|
|
84
|
+
hintLayout="inline"
|
|
85
|
+
initialIndex={1}
|
|
86
|
+
onSubmit={choice => {
|
|
87
|
+
if (choice === 'delete') return onConfirm()
|
|
88
|
+
return onCancel()
|
|
89
|
+
}}
|
|
90
|
+
onCancel={onCancel}
|
|
91
|
+
/>
|
|
92
|
+
</Box>
|
|
93
|
+
</Surface>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const FolderContents: React.FC<{ files: SkillFileEntry[] }> = ({ files }) => {
|
|
98
|
+
if (files.length === 0) {
|
|
99
|
+
return <Text color={theme.dim}>(empty folder)</Text>
|
|
100
|
+
}
|
|
101
|
+
const shown = files.slice(0, MAX_LISTED_FILES)
|
|
102
|
+
const extra = files.length - shown.length
|
|
103
|
+
return (
|
|
104
|
+
<Box flexDirection="column" marginTop={1}>
|
|
105
|
+
<Text color={theme.dim}>Files to be removed:</Text>
|
|
106
|
+
{shown.map(f => (
|
|
107
|
+
<Text key={f.relativePath} color={theme.text}> · {f.relativePath}</Text>
|
|
108
|
+
))}
|
|
109
|
+
{extra > 0 && <Text color={theme.dim}> · ...and {extra} more</Text>}
|
|
110
|
+
</Box>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
import { Surface } from '../../../../ui/Surface.js'
|
|
4
|
+
import { Select, type SelectOption } from '../../../../ui/Select.js'
|
|
5
|
+
import { theme } from '../../../../ui/theme.js'
|
|
6
|
+
import type { EthagentIdentity } from '../../../../storage/config.js'
|
|
7
|
+
import { listSkills } from '../../../continuity/skills/loadSkills.js'
|
|
8
|
+
import type { SkillIndexEntry } from '../../../continuity/skills/types.js'
|
|
9
|
+
|
|
10
|
+
type DeleteTarget =
|
|
11
|
+
| { kind: 'skill'; relativePath: string }
|
|
12
|
+
| { kind: 'cancel' }
|
|
13
|
+
|
|
14
|
+
interface DeleteSkillScreenProps {
|
|
15
|
+
identity?: EthagentIdentity
|
|
16
|
+
notice?: string
|
|
17
|
+
footer: React.ReactNode
|
|
18
|
+
onPick: (target: { kind: 'skill'; relativePath: string }) => void
|
|
19
|
+
onCancel: () => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const DeleteSkillScreen: React.FC<DeleteSkillScreenProps> = ({
|
|
23
|
+
identity,
|
|
24
|
+
notice,
|
|
25
|
+
footer,
|
|
26
|
+
onPick,
|
|
27
|
+
onCancel,
|
|
28
|
+
}) => {
|
|
29
|
+
const [entries, setEntries] = useState<SkillIndexEntry[] | null>(null)
|
|
30
|
+
const [error, setError] = useState<string | null>(null)
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
let cancelled = false
|
|
34
|
+
if (!identity) {
|
|
35
|
+
setEntries([])
|
|
36
|
+
return () => { cancelled = true }
|
|
37
|
+
}
|
|
38
|
+
listSkills(identity)
|
|
39
|
+
.then(list => { if (!cancelled) { setEntries(list); setError(null) } })
|
|
40
|
+
.catch(err => {
|
|
41
|
+
if (cancelled) return
|
|
42
|
+
setEntries([])
|
|
43
|
+
setError(String((err as Error).message ?? err))
|
|
44
|
+
})
|
|
45
|
+
return () => { cancelled = true }
|
|
46
|
+
}, [identity])
|
|
47
|
+
|
|
48
|
+
const subtitle = notice ?? 'Pick a skill to delete. The whole folder will be removed.'
|
|
49
|
+
const isLoading = entries === null
|
|
50
|
+
const skills = entries ?? []
|
|
51
|
+
const hasAnything = skills.length > 0
|
|
52
|
+
|
|
53
|
+
const options = buildOptions(skills, isLoading)
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Surface title="Delete Skill" subtitle={subtitle} footer={footer} tone="error">
|
|
57
|
+
{error && (
|
|
58
|
+
<Box marginTop={1}>
|
|
59
|
+
<Text color={theme.accentError}>{error}</Text>
|
|
60
|
+
</Box>
|
|
61
|
+
)}
|
|
62
|
+
{!isLoading && !hasAnything && (
|
|
63
|
+
<Box marginTop={1}>
|
|
64
|
+
<Text color={theme.dim}>Nothing to delete. The skills tree is empty.</Text>
|
|
65
|
+
</Box>
|
|
66
|
+
)}
|
|
67
|
+
<Box marginTop={1}>
|
|
68
|
+
<Select<DeleteTarget>
|
|
69
|
+
options={options}
|
|
70
|
+
hintLayout="inline"
|
|
71
|
+
onSubmit={choice => {
|
|
72
|
+
if (choice.kind === 'cancel') return onCancel()
|
|
73
|
+
return onPick(choice)
|
|
74
|
+
}}
|
|
75
|
+
onCancel={onCancel}
|
|
76
|
+
/>
|
|
77
|
+
</Box>
|
|
78
|
+
</Surface>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildOptions(
|
|
83
|
+
entries: SkillIndexEntry[],
|
|
84
|
+
isLoading: boolean,
|
|
85
|
+
): Array<SelectOption<DeleteTarget>> {
|
|
86
|
+
const rows: Array<SelectOption<DeleteTarget>> = []
|
|
87
|
+
const noopValue: DeleteTarget = { kind: 'cancel' }
|
|
88
|
+
|
|
89
|
+
if (isLoading) {
|
|
90
|
+
rows.push({
|
|
91
|
+
value: noopValue,
|
|
92
|
+
role: 'notice',
|
|
93
|
+
label: 'Loading...',
|
|
94
|
+
labelColor: theme.dim,
|
|
95
|
+
indent: 0,
|
|
96
|
+
})
|
|
97
|
+
} else if (entries.length > 0) {
|
|
98
|
+
rows.push({ value: noopValue, role: 'section', label: 'Targets' })
|
|
99
|
+
rows.push({ value: noopValue, role: 'notice', label: 'skills/', labelColor: theme.dim, indent: 3 })
|
|
100
|
+
const sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name))
|
|
101
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
102
|
+
const skill = sorted[i]
|
|
103
|
+
if (!skill) continue
|
|
104
|
+
const isLast = i === sorted.length - 1
|
|
105
|
+
const branch = isLast ? '└── ' : '├── '
|
|
106
|
+
rows.push({
|
|
107
|
+
value: { kind: 'skill', relativePath: skill.relativePath },
|
|
108
|
+
label: `${branch}${skill.name}/`,
|
|
109
|
+
indent: 3,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
rows.push({ value: noopValue, role: 'section', label: 'Navigation' })
|
|
115
|
+
rows.push({
|
|
116
|
+
value: { kind: 'cancel' },
|
|
117
|
+
label: 'Back',
|
|
118
|
+
hint: 'Return to Skills',
|
|
119
|
+
role: 'utility',
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
return rows
|
|
123
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
import { Surface } from '../../../../ui/Surface.js'
|
|
4
|
+
import { TextInput } from '../../../../ui/TextInput.js'
|
|
5
|
+
import { theme } from '../../../../ui/theme.js'
|
|
6
|
+
|
|
7
|
+
interface NewSkillScreenProps {
|
|
8
|
+
error?: string
|
|
9
|
+
footer: React.ReactNode
|
|
10
|
+
onSubmit: (name: string) => void
|
|
11
|
+
onCancel: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const NewSkillScreen: React.FC<NewSkillScreenProps> = ({
|
|
15
|
+
error,
|
|
16
|
+
footer,
|
|
17
|
+
onSubmit,
|
|
18
|
+
onCancel,
|
|
19
|
+
}) => (
|
|
20
|
+
<Surface
|
|
21
|
+
title="New Skill"
|
|
22
|
+
subtitle="Folder name. The skill activates by this name."
|
|
23
|
+
footer={footer}
|
|
24
|
+
>
|
|
25
|
+
<Box marginTop={1} flexDirection="column">
|
|
26
|
+
<TextInput
|
|
27
|
+
label="Folder name"
|
|
28
|
+
placeholder="<folder>"
|
|
29
|
+
validate={value => validateSegment(value)}
|
|
30
|
+
onSubmit={raw => {
|
|
31
|
+
const trimmed = raw.trim()
|
|
32
|
+
if (validateSegment(trimmed) === null) onSubmit(trimmed)
|
|
33
|
+
}}
|
|
34
|
+
onCancel={onCancel}
|
|
35
|
+
/>
|
|
36
|
+
<Box marginTop={1}>
|
|
37
|
+
<Text color={theme.dim}>
|
|
38
|
+
Saved as skills/<folder>/SKILL.md. Add supporting files later by dropping them into the same folder.
|
|
39
|
+
</Text>
|
|
40
|
+
</Box>
|
|
41
|
+
{error && (
|
|
42
|
+
<Box marginTop={1}>
|
|
43
|
+
<Text color={theme.accentError}>{error}</Text>
|
|
44
|
+
</Box>
|
|
45
|
+
)}
|
|
46
|
+
</Box>
|
|
47
|
+
</Surface>
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
export function validateSegment(value: string): string | null {
|
|
51
|
+
const trimmed = value.trim()
|
|
52
|
+
if (!trimmed) return 'Enter a name.'
|
|
53
|
+
if (trimmed.includes('/') || trimmed.includes('\\')) return 'One segment only, no slashes.'
|
|
54
|
+
if (trimmed === '.' || trimmed === '..') return 'Reserved name.'
|
|
55
|
+
if (!/^[A-Za-z0-9._-]+$/.test(trimmed)) return 'Only letters, digits, dashes, underscores, dots.'
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
import { Surface } from '../../../../ui/Surface.js'
|
|
4
|
+
import { Select } from '../../../../ui/Select.js'
|
|
5
|
+
import { theme } from '../../../../ui/theme.js'
|
|
6
|
+
import type { SkillVisibility } from '../../../continuity/skills/types.js'
|
|
7
|
+
|
|
8
|
+
interface NewSkillVisibilityScreenProps {
|
|
9
|
+
name: string
|
|
10
|
+
error?: string
|
|
11
|
+
footer: React.ReactNode
|
|
12
|
+
onSelect: (visibility: SkillVisibility) => void
|
|
13
|
+
onCancel: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const NewSkillVisibilityScreen: React.FC<NewSkillVisibilityScreenProps> = ({
|
|
17
|
+
name,
|
|
18
|
+
error,
|
|
19
|
+
footer,
|
|
20
|
+
onSelect,
|
|
21
|
+
onCancel,
|
|
22
|
+
}) => (
|
|
23
|
+
<Surface
|
|
24
|
+
title={`Visibility · ${name}`}
|
|
25
|
+
subtitle="Discoverable is the default. You can change it later from Change Visibility."
|
|
26
|
+
footer={footer}
|
|
27
|
+
>
|
|
28
|
+
{error && (
|
|
29
|
+
<Box marginTop={1}>
|
|
30
|
+
<Text color={theme.accentError}>{error}</Text>
|
|
31
|
+
</Box>
|
|
32
|
+
)}
|
|
33
|
+
<Box marginTop={1}>
|
|
34
|
+
<Select<SkillVisibility | 'back'>
|
|
35
|
+
options={[
|
|
36
|
+
{ value: 'private', label: 'Private', hint: 'Local-only. Not in skills.json.' },
|
|
37
|
+
{ value: 'discoverable', label: 'Discoverable', hint: 'Default. Indexed in skills.json with description.' },
|
|
38
|
+
{ value: 'public', label: 'Public', hint: 'Indexed in skills.json and Agent Card.' },
|
|
39
|
+
{ value: 'back', role: 'section', label: 'Navigation' },
|
|
40
|
+
{ value: 'back', label: 'Back', hint: 'Return to the name step', role: 'utility' },
|
|
41
|
+
]}
|
|
42
|
+
hintLayout="inline"
|
|
43
|
+
initialIndex={1}
|
|
44
|
+
onSubmit={choice => {
|
|
45
|
+
if (choice === 'back') return onCancel()
|
|
46
|
+
return onSelect(choice)
|
|
47
|
+
}}
|
|
48
|
+
onCancel={onCancel}
|
|
49
|
+
/>
|
|
50
|
+
</Box>
|
|
51
|
+
</Surface>
|
|
52
|
+
)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
import { Surface } from '../../../../ui/Surface.js'
|
|
4
|
+
import { Select, type SelectOption } from '../../../../ui/Select.js'
|
|
5
|
+
import { theme } from '../../../../ui/theme.js'
|
|
6
|
+
import type { EthagentIdentity } from '../../../../storage/config.js'
|
|
7
|
+
import { listSkills } from '../../../continuity/skills/loadSkills.js'
|
|
8
|
+
import type { SkillIndexEntry, SkillVisibility } from '../../../continuity/skills/types.js'
|
|
9
|
+
|
|
10
|
+
interface SkillVisibilityListProps {
|
|
11
|
+
identity?: EthagentIdentity
|
|
12
|
+
notice?: string
|
|
13
|
+
footer: React.ReactNode
|
|
14
|
+
onPick: (relativePath: string) => void
|
|
15
|
+
onCancel: () => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type PickAction =
|
|
19
|
+
| { kind: 'skill'; relativePath: string }
|
|
20
|
+
| { kind: 'cancel' }
|
|
21
|
+
|
|
22
|
+
export const SkillVisibilityListScreen: React.FC<SkillVisibilityListProps> = ({
|
|
23
|
+
identity,
|
|
24
|
+
notice,
|
|
25
|
+
footer,
|
|
26
|
+
onPick,
|
|
27
|
+
onCancel,
|
|
28
|
+
}) => {
|
|
29
|
+
const [entries, setEntries] = useState<SkillIndexEntry[] | null>(null)
|
|
30
|
+
const [error, setError] = useState<string | null>(null)
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
let cancelled = false
|
|
34
|
+
if (!identity) {
|
|
35
|
+
setEntries([])
|
|
36
|
+
return () => { cancelled = true }
|
|
37
|
+
}
|
|
38
|
+
listSkills(identity)
|
|
39
|
+
.then(list => { if (!cancelled) { setEntries(list); setError(null) } })
|
|
40
|
+
.catch(err => {
|
|
41
|
+
if (cancelled) return
|
|
42
|
+
setEntries([])
|
|
43
|
+
setError(String((err as Error).message ?? err))
|
|
44
|
+
})
|
|
45
|
+
return () => { cancelled = true }
|
|
46
|
+
}, [identity])
|
|
47
|
+
|
|
48
|
+
const subtitle = notice ?? 'Pick a skill to change its visibility in the public manifest.'
|
|
49
|
+
const isLoading = entries === null
|
|
50
|
+
const skills = entries ?? []
|
|
51
|
+
|
|
52
|
+
const options: Array<SelectOption<PickAction>> = []
|
|
53
|
+
if (isLoading) {
|
|
54
|
+
options.push({ value: { kind: 'cancel' }, role: 'notice', label: 'Loading...', labelColor: theme.dim, indent: 0 })
|
|
55
|
+
} else if (skills.length === 0) {
|
|
56
|
+
options.push({ value: { kind: 'cancel' }, role: 'notice', label: 'No skills yet. Create one first.', labelColor: theme.dim, indent: 0 })
|
|
57
|
+
} else {
|
|
58
|
+
options.push({ value: { kind: 'cancel' }, role: 'section', label: 'Skills' })
|
|
59
|
+
for (const skill of skills) {
|
|
60
|
+
options.push({
|
|
61
|
+
value: { kind: 'skill', relativePath: skill.relativePath },
|
|
62
|
+
label: skill.displayName ?? skill.name,
|
|
63
|
+
hint: visibilityBadge(skill.visibility),
|
|
64
|
+
hintColor: visibilityColor(skill.visibility),
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
options.push({ value: { kind: 'cancel' }, role: 'section', label: 'Navigation' })
|
|
69
|
+
options.push({ value: { kind: 'cancel' }, label: 'Back', hint: 'Return to Skills', role: 'utility' })
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Surface title="Skill Visibility" subtitle={subtitle} footer={footer}>
|
|
73
|
+
{error && (
|
|
74
|
+
<Box marginTop={1}>
|
|
75
|
+
<Text color={theme.accentError}>{error}</Text>
|
|
76
|
+
</Box>
|
|
77
|
+
)}
|
|
78
|
+
<Box marginTop={1}>
|
|
79
|
+
<Select<PickAction>
|
|
80
|
+
options={options}
|
|
81
|
+
hintLayout="inline"
|
|
82
|
+
onSubmit={choice => {
|
|
83
|
+
if (choice.kind === 'cancel') return onCancel()
|
|
84
|
+
return onPick(choice.relativePath)
|
|
85
|
+
}}
|
|
86
|
+
onCancel={onCancel}
|
|
87
|
+
/>
|
|
88
|
+
</Box>
|
|
89
|
+
</Surface>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface SkillVisibilityPickProps {
|
|
94
|
+
identity?: EthagentIdentity
|
|
95
|
+
relativePath: string
|
|
96
|
+
footer: React.ReactNode
|
|
97
|
+
onSelect: (visibility: SkillVisibility) => void
|
|
98
|
+
onCancel: () => void
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const SkillVisibilityPickScreen: React.FC<SkillVisibilityPickProps> = ({
|
|
102
|
+
identity,
|
|
103
|
+
relativePath,
|
|
104
|
+
footer,
|
|
105
|
+
onSelect,
|
|
106
|
+
onCancel,
|
|
107
|
+
}) => {
|
|
108
|
+
const [current, setCurrent] = useState<SkillVisibility | null>(null)
|
|
109
|
+
const [displayName, setDisplayName] = useState<string>(relativePath)
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
let cancelled = false
|
|
113
|
+
if (!identity) return () => { cancelled = true }
|
|
114
|
+
listSkills(identity)
|
|
115
|
+
.then(list => {
|
|
116
|
+
if (cancelled) return
|
|
117
|
+
const match = list.find(entry => entry.relativePath === relativePath)
|
|
118
|
+
if (match) {
|
|
119
|
+
setCurrent(match.visibility)
|
|
120
|
+
setDisplayName(match.displayName ?? match.name)
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
.catch(() => null)
|
|
124
|
+
return () => { cancelled = true }
|
|
125
|
+
}, [identity, relativePath])
|
|
126
|
+
|
|
127
|
+
const subtitle = current
|
|
128
|
+
? `Current: ${current}. Pick a new value.`
|
|
129
|
+
: 'Pick a visibility level.'
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<Surface title={`Visibility · ${displayName}`} subtitle={subtitle} footer={footer}>
|
|
133
|
+
<Box marginTop={1}>
|
|
134
|
+
<Select<SkillVisibility | 'cancel'>
|
|
135
|
+
options={[
|
|
136
|
+
{ value: 'private', label: 'Private', hint: 'Local-only. Not in skills.json.' },
|
|
137
|
+
{ value: 'discoverable', label: 'Discoverable', hint: 'Default. Indexed in skills.json with description.' },
|
|
138
|
+
{ value: 'public', label: 'Public', hint: 'Indexed in skills.json and Agent Card.' },
|
|
139
|
+
{ value: 'cancel', role: 'section', label: 'Navigation' },
|
|
140
|
+
{ value: 'cancel', label: 'Back', hint: 'Return to skill list', role: 'utility' },
|
|
141
|
+
]}
|
|
142
|
+
hintLayout="inline"
|
|
143
|
+
initialIndex={current ? indexForVisibility(current) : 0}
|
|
144
|
+
onSubmit={choice => {
|
|
145
|
+
if (choice === 'cancel') return onCancel()
|
|
146
|
+
return onSelect(choice)
|
|
147
|
+
}}
|
|
148
|
+
onCancel={onCancel}
|
|
149
|
+
/>
|
|
150
|
+
</Box>
|
|
151
|
+
</Surface>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function visibilityBadge(visibility: SkillVisibility): string {
|
|
156
|
+
if (visibility === 'public') return '[public]'
|
|
157
|
+
if (visibility === 'discoverable') return '[discoverable]'
|
|
158
|
+
return '[private]'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function visibilityColor(visibility: SkillVisibility): string {
|
|
162
|
+
if (visibility === 'public') return theme.accentPeriwinkle
|
|
163
|
+
if (visibility === 'discoverable') return theme.textSubtle
|
|
164
|
+
return theme.dim
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function indexForVisibility(visibility: SkillVisibility): number {
|
|
168
|
+
if (visibility === 'private') return 0
|
|
169
|
+
if (visibility === 'discoverable') return 1
|
|
170
|
+
return 2
|
|
171
|
+
}
|