ethagent 1.1.2 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +124 -32
- package/package.json +8 -3
- package/src/app/FirstRun.tsx +190 -146
- package/src/app/FirstRunTimeline.tsx +47 -0
- package/src/app/input/AppInputProvider.tsx +1 -1
- package/src/app/keybindings/KeybindingProvider.tsx +1 -1
- package/src/chat/ChatBottomPane.tsx +0 -1
- package/src/chat/ChatInput.tsx +6 -6
- package/src/chat/ChatScreen.tsx +35 -15
- package/src/chat/ContextLimitView.tsx +4 -4
- package/src/chat/ContinuityEditReviewView.tsx +10 -22
- package/src/chat/CopyPicker.tsx +0 -1
- package/src/chat/MessageList.tsx +62 -45
- package/src/chat/PermissionPrompt.tsx +13 -9
- package/src/chat/PlanApprovalView.tsx +3 -3
- package/src/chat/ResumeView.tsx +1 -4
- package/src/chat/RewindView.tsx +2 -2
- package/src/chat/chatInputState.ts +1 -1
- package/src/chat/chatScreenUtils.ts +22 -11
- package/src/chat/chatSessionState.ts +2 -2
- package/src/chat/chatTurnOrchestrator.ts +16 -81
- package/src/chat/commands.ts +1 -1
- package/src/chat/textCursor.ts +1 -1
- package/src/chat/transcriptViewport.ts +2 -7
- package/src/cli/ResetConfirmView.tsx +1 -1
- package/src/cli/main.tsx +9 -3
- package/src/cli/preview.tsx +0 -5
- package/src/cli/updateNotice.ts +4 -2
- package/src/identity/continuity/editor.ts +7 -107
- package/src/identity/continuity/envelope.ts +1048 -40
- package/src/identity/continuity/history.ts +4 -4
- package/src/identity/continuity/localBackup.ts +249 -0
- package/src/identity/continuity/privateEdit/apply.ts +170 -0
- package/src/identity/continuity/privateEdit/diff.ts +82 -0
- package/src/identity/continuity/privateEdit/files.ts +23 -0
- package/src/identity/continuity/privateEdit/types.ts +28 -0
- package/src/identity/continuity/privateEdit.ts +10 -298
- package/src/identity/continuity/publicSkills.ts +8 -9
- package/src/identity/continuity/snapshots.ts +17 -6
- package/src/identity/continuity/storage/defaults.ts +111 -0
- package/src/identity/continuity/storage/files.ts +72 -0
- package/src/identity/continuity/storage/markdown.ts +81 -0
- package/src/identity/continuity/storage/paths.ts +24 -0
- package/src/identity/continuity/storage/scaffold.ts +124 -0
- package/src/identity/continuity/storage/status.ts +86 -0
- package/src/identity/continuity/storage/types.ts +27 -0
- package/src/identity/continuity/storage.ts +32 -507
- package/src/identity/continuity/zipWriter.ts +95 -0
- package/src/identity/crypto/backupEnvelope.ts +14 -247
- package/src/identity/crypto/eth.ts +7 -7
- package/src/identity/ens/agentRecords.ts +96 -0
- package/src/identity/ens/ensAutomation/contracts.ts +38 -0
- package/src/identity/ens/ensAutomation/delete.ts +80 -0
- package/src/identity/ens/ensAutomation/names.ts +14 -0
- package/src/identity/ens/ensAutomation/operators.ts +29 -0
- package/src/identity/ens/ensAutomation/read.ts +114 -0
- package/src/identity/ens/ensAutomation/root.ts +63 -0
- package/src/identity/ens/ensAutomation/setup.ts +284 -0
- package/src/identity/ens/ensAutomation/transactions.ts +107 -0
- package/src/identity/ens/ensAutomation/types.ts +126 -0
- package/src/identity/ens/ensAutomation.ts +29 -0
- package/src/identity/ens/ensLookup/client.ts +43 -0
- package/src/identity/ens/ensLookup/constants.ts +26 -0
- package/src/identity/ens/ensLookup/discovery.ts +70 -0
- package/src/identity/ens/ensLookup/names.ts +34 -0
- package/src/identity/ens/ensLookup/records.ts +45 -0
- package/src/identity/ens/ensLookup/resolve.ts +75 -0
- package/src/identity/ens/ensLookup/tokenReference.ts +17 -0
- package/src/identity/ens/ensLookup/types.ts +38 -0
- package/src/identity/ens/ensLookup/validation.ts +72 -0
- package/src/identity/ens/ensLookup.ts +19 -0
- package/src/identity/ens/ensRegistration.ts +199 -0
- package/src/identity/ens/resolverDelegation.ts +48 -0
- package/src/identity/hub/IdentityHub.tsx +13 -817
- package/src/identity/hub/OperationalRoutes.tsx +370 -0
- package/src/identity/hub/Routes.tsx +361 -0
- package/src/identity/hub/advancedEnsValidation.ts +45 -0
- package/src/identity/hub/{screens → components}/DetailsScreen.tsx +14 -8
- package/src/identity/hub/{screens → components}/ErrorScreen.tsx +15 -5
- package/src/identity/hub/components/FlowTimeline.tsx +27 -0
- package/src/identity/hub/components/IdentitySummary.tsx +190 -0
- package/src/identity/hub/components/MenuScreen.tsx +237 -0
- package/src/identity/hub/{screens → components}/NetworkScreen.tsx +3 -3
- package/src/identity/hub/{screens/RebackupStorageScreen.tsx → components/PinataJwtInput.tsx} +21 -18
- package/src/identity/hub/components/UnlinkedIdentityScreen.tsx +76 -0
- package/src/identity/hub/{screens → components}/WalletApprovalScreen.tsx +9 -8
- package/src/identity/hub/components/menuFlagsFromReconciliation.ts +68 -0
- package/src/identity/hub/effects/create.ts +310 -0
- package/src/identity/hub/effects/ens/flows.ts +218 -0
- package/src/identity/hub/effects/ens/index.ts +11 -0
- package/src/identity/hub/effects/ens/transactions.ts +239 -0
- package/src/identity/hub/effects/index.ts +74 -0
- package/src/identity/hub/effects/profile/profileState.ts +173 -0
- package/src/identity/hub/effects/publicProfile/index.ts +5 -0
- package/src/identity/hub/effects/publicProfile/runPublicProfileSave.ts +646 -0
- package/src/identity/hub/effects/rebackup/index.ts +7 -0
- package/src/identity/hub/effects/rebackup/operatorVault.ts +378 -0
- package/src/identity/hub/effects/rebackup/runRebackup.ts +451 -0
- package/src/identity/hub/effects/receipts.ts +46 -0
- package/src/identity/hub/effects/restore/apply.ts +112 -0
- package/src/identity/hub/effects/restore/auth.ts +159 -0
- package/src/identity/hub/effects/restore/discover.ts +86 -0
- package/src/identity/hub/effects/restore/envelopes.ts +21 -0
- package/src/identity/hub/effects/restore/fetch.ts +25 -0
- package/src/identity/hub/effects/restore/index.ts +22 -0
- package/src/identity/hub/effects/restore/recovery.ts +135 -0
- package/src/identity/hub/effects/restore/resolve.ts +102 -0
- package/src/identity/hub/effects/restore/restoreEffects.ts +22 -0
- package/src/identity/hub/effects/restore/shared.ts +91 -0
- package/src/identity/hub/effects/restoreAdmin.ts +93 -0
- package/src/identity/hub/effects/shared/profilePrep.ts +139 -0
- package/src/identity/hub/effects/shared/snapshot.ts +336 -0
- package/src/identity/hub/effects/shared/sync.ts +190 -0
- package/src/identity/hub/effects/token-transfer/index.ts +6 -0
- package/src/identity/hub/effects/token-transfer/progress.ts +59 -0
- package/src/identity/hub/effects/token-transfer/runTokenTransfer.ts +299 -0
- package/src/identity/hub/effects/types.ts +53 -0
- package/src/identity/hub/effects/vault/preflight.ts +50 -0
- package/src/identity/hub/flows/continuity/ContinuityDashboardScreen.tsx +170 -0
- package/src/identity/hub/flows/continuity/RebackupStorageScreen.tsx +28 -0
- package/src/identity/hub/{screens → flows/continuity}/RecoveryConfirmScreen.tsx +28 -19
- package/src/identity/hub/flows/continuity/SavePromptScreen.tsx +49 -0
- package/src/identity/hub/{screens → flows/create}/CreateFlow.tsx +61 -62
- package/src/identity/hub/flows/custody/CustodyEditFlow.tsx +347 -0
- package/src/identity/hub/flows/custody/custodyEffects.ts +321 -0
- package/src/identity/hub/flows/custody/custodyFlowActions.ts +236 -0
- package/src/identity/hub/flows/custody/custodyFlowEffects.ts +163 -0
- package/src/identity/hub/flows/custody/custodyFlowHelpers.ts +25 -0
- package/src/identity/hub/flows/custody/custodyFlowRoutes.tsx +239 -0
- package/src/identity/hub/flows/custody/custodyFlowTypes.ts +45 -0
- package/src/identity/hub/flows/custody/useCustodyFlow.tsx +25 -0
- package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +336 -0
- package/src/identity/hub/flows/ens/EnsEditFlow.tsx +397 -0
- package/src/identity/hub/flows/ens/EnsEditMaintenanceScreens.tsx +332 -0
- package/src/identity/hub/flows/ens/EnsEditReviewScreens.tsx +471 -0
- package/src/identity/hub/flows/ens/EnsEditRunners.tsx +198 -0
- package/src/identity/hub/flows/ens/EnsEditShared.tsx +162 -0
- package/src/identity/hub/flows/ens/EnsEditSimpleScreens.tsx +518 -0
- package/src/identity/hub/flows/ens/IdentityHubEnsFlow.tsx +299 -0
- package/src/identity/hub/flows/ens/OperatorWalletsScreen.tsx +398 -0
- package/src/identity/hub/flows/ens/ensEditCopy.ts +117 -0
- package/src/identity/hub/flows/ens/ensEditTypes.ts +91 -0
- package/src/identity/hub/flows/profile/EditProfileFlow.tsx +271 -0
- package/src/identity/hub/flows/restore/RestoreFlow.tsx +324 -0
- package/src/identity/hub/flows/restore/useRestoreFlowEffects.ts +77 -0
- package/src/identity/hub/{screens → flows/settings}/StorageCredentialScreen.tsx +23 -44
- package/src/identity/hub/flows/token-transfer/IdentityHubTokenTransferFlow.tsx +162 -0
- package/src/identity/hub/flows/token-transfer/TokenTransferScreens.tsx +256 -0
- package/src/identity/hub/identityHubReducer.ts +164 -99
- package/src/identity/hub/model/continuity.ts +94 -0
- package/src/identity/hub/model/copy.ts +35 -0
- package/src/identity/hub/model/custody.ts +54 -0
- package/src/identity/hub/model/ens.ts +49 -0
- package/src/identity/hub/model/errors.ts +140 -0
- package/src/identity/hub/model/format.ts +15 -0
- package/src/identity/hub/model/identity.ts +94 -0
- package/src/identity/hub/model/network.ts +32 -0
- package/src/identity/hub/model/transfer.ts +57 -0
- package/src/identity/hub/operatorWallets.ts +131 -0
- package/src/identity/hub/reconciliation/agentReconciliation/hook.ts +46 -0
- package/src/identity/hub/reconciliation/agentReconciliation/ownership.ts +129 -0
- package/src/identity/hub/reconciliation/agentReconciliation/run.ts +302 -0
- package/src/identity/hub/reconciliation/agentReconciliation/types.ts +17 -0
- package/src/identity/hub/reconciliation/index.ts +21 -0
- package/src/identity/hub/reconciliation/useAgentReconciliation.ts +10 -0
- package/src/identity/hub/reconciliation/walletSetup.ts +220 -0
- package/src/identity/hub/txGuard.ts +51 -0
- package/src/identity/hub/types.ts +17 -0
- package/src/identity/hub/useIdentityHubContinuity.ts +136 -0
- package/src/identity/hub/useIdentityHubController.ts +396 -0
- package/src/identity/hub/useIdentityHubSideEffects.ts +309 -0
- package/src/identity/hub/utils.ts +79 -0
- package/src/identity/identityCompat.ts +34 -0
- package/src/identity/profile/agentIcon.ts +61 -0
- package/src/identity/profile/imagePicker.ts +12 -12
- package/src/identity/registry/erc8004/abi.ts +14 -0
- package/src/identity/registry/erc8004/chains.ts +150 -0
- package/src/identity/registry/erc8004/client.ts +11 -0
- package/src/identity/registry/erc8004/discovery.ts +511 -0
- package/src/identity/registry/erc8004/metadata.ts +335 -0
- package/src/identity/registry/erc8004/ownership.ts +121 -0
- package/src/identity/registry/erc8004/preflight.ts +123 -0
- package/src/identity/registry/erc8004/transactions.ts +77 -0
- package/src/identity/registry/erc8004/types.ts +88 -0
- package/src/identity/registry/erc8004/uri.ts +59 -0
- package/src/identity/registry/erc8004/utils.ts +58 -0
- package/src/identity/registry/erc8004.ts +53 -1106
- package/src/identity/registry/fieldParsers.ts +28 -0
- package/src/identity/registry/operatorVault/bytecode.ts +98 -0
- package/src/identity/registry/operatorVault/constants.ts +38 -0
- package/src/identity/registry/operatorVault/read.ts +246 -0
- package/src/identity/registry/operatorVault/transactions.ts +81 -0
- package/src/identity/registry/operatorVault.ts +44 -0
- package/src/identity/storage/ipfs.ts +26 -24
- package/src/identity/wallet/browserWallet/gas.ts +41 -0
- package/src/identity/wallet/browserWallet/html.ts +106 -0
- package/src/identity/wallet/browserWallet/http.ts +28 -0
- package/src/identity/wallet/browserWallet/requestServer.ts +106 -0
- package/src/identity/wallet/browserWallet/requests.ts +191 -0
- package/src/identity/wallet/browserWallet/session.ts +325 -0
- package/src/identity/wallet/browserWallet/types.ts +192 -0
- package/src/identity/wallet/browserWallet/validation.ts +74 -0
- package/src/identity/wallet/browserWallet.ts +30 -393
- package/src/identity/wallet/page/constants.ts +5 -0
- package/src/identity/wallet/page/controller.ts +251 -0
- package/src/identity/wallet/page/copy.ts +340 -0
- package/src/identity/wallet/page/grainient.ts +278 -0
- package/src/identity/wallet/page/html.ts +28 -0
- package/src/identity/wallet/page/markup.ts +50 -0
- package/src/identity/wallet/page/state.ts +9 -0
- package/src/identity/wallet/page/styles/base.ts +259 -0
- package/src/identity/wallet/page/styles/components.ts +262 -0
- package/src/identity/wallet/page/styles/index.ts +5 -0
- package/src/identity/wallet/page/styles/responsive.ts +247 -0
- package/src/identity/wallet/page/types.ts +47 -0
- package/src/identity/wallet/page/view.ts +535 -0
- package/src/identity/wallet/page/walletProvider.ts +70 -0
- package/src/identity/wallet/page.tsx +38 -0
- package/src/identity/wallet/walletPurposeCompat.ts +27 -0
- package/src/mcp/manager.ts +0 -1
- package/src/models/ModelPicker.tsx +36 -30
- package/src/models/catalog.ts +5 -2
- package/src/models/huggingface.ts +9 -9
- package/src/models/llamacpp.ts +13 -13
- package/src/models/modelDisplay.ts +75 -0
- package/src/models/modelPickerOptions.ts +16 -3
- package/src/models/modelRecommendation.ts +0 -1
- package/src/providers/errors.ts +16 -0
- package/src/providers/gemini.ts +252 -39
- package/src/providers/registry.ts +2 -2
- package/src/providers/retry.ts +1 -1
- package/src/runtime/sessionMode.ts +1 -1
- package/src/runtime/systemPrompt.ts +2 -0
- package/src/runtime/toolExecution.ts +18 -22
- package/src/runtime/toolIntent.ts +0 -20
- package/src/runtime/turn.ts +0 -92
- package/src/storage/atomicWrite.ts +4 -1
- package/src/storage/config.ts +181 -5
- package/src/storage/identity.ts +9 -3
- package/src/storage/secrets.ts +2 -2
- package/src/tools/bashSafety.ts +8 -0
- package/src/tools/changeDirectoryTool.ts +1 -1
- package/src/tools/deleteFileTool.ts +4 -4
- package/src/tools/editTool.ts +4 -4
- package/src/tools/editUtils.ts +5 -5
- package/src/tools/privateContinuityEditTool.ts +4 -5
- package/src/tools/privateContinuityReadTool.ts +1 -2
- package/src/tools/registry.ts +30 -0
- package/src/tools/writeFileTool.ts +5 -5
- package/src/ui/BrandSplash.tsx +20 -85
- package/src/ui/ProgressBar.tsx +3 -5
- package/src/ui/Select.tsx +20 -8
- package/src/ui/Spinner.tsx +38 -3
- package/src/ui/Surface.tsx +2 -2
- package/src/ui/TextInput.tsx +63 -20
- package/src/ui/theme.ts +7 -34
- package/src/utils/openExternal.ts +21 -0
- package/src/utils/withRetry.ts +47 -3
- package/src/identity/hub/identityHubEffects.ts +0 -937
- package/src/identity/hub/identityHubModel.ts +0 -371
- package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +0 -156
- package/src/identity/hub/screens/EditProfileFlow.tsx +0 -146
- package/src/identity/hub/screens/IdentitySummary.tsx +0 -106
- package/src/identity/hub/screens/MenuScreen.tsx +0 -117
- package/src/identity/hub/screens/RestoreFlow.tsx +0 -206
- package/src/identity/wallet/wallet-page/wallet.html +0 -1202
- /package/src/identity/hub/{screens → components}/BusyScreen.tsx +0 -0
|
@@ -3,7 +3,6 @@ import { z } from 'zod'
|
|
|
3
3
|
import {
|
|
4
4
|
continuityVaultRef,
|
|
5
5
|
ensureContinuityFiles,
|
|
6
|
-
type PrivateContinuityFile,
|
|
7
6
|
} from '../identity/continuity/storage.js'
|
|
8
7
|
import type { Tool } from './contracts.js'
|
|
9
8
|
|
|
@@ -71,7 +70,7 @@ function preparePrivateContinuityRead(
|
|
|
71
70
|
) {
|
|
72
71
|
const identity = config?.identity
|
|
73
72
|
if (!identity) {
|
|
74
|
-
throw new Error('
|
|
73
|
+
throw new Error('No active identity; create or load an identity before reading private continuity files')
|
|
75
74
|
}
|
|
76
75
|
const ref = continuityVaultRef(identity)
|
|
77
76
|
const fullPath = input.file === 'SOUL.md' ? ref.soulPath : ref.memoryPath
|
package/src/tools/registry.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AnthropicToolDefinition } from '../providers/anthropic.js'
|
|
2
|
+
import type { GeminiToolDefinition } from '../providers/gemini.js'
|
|
2
3
|
import type { OpenAIToolDefinition } from '../providers/openai-chat.js'
|
|
3
4
|
import type { Tool } from './contracts.js'
|
|
4
5
|
import { modePolicy, type SessionMode } from '../runtime/sessionMode.js'
|
|
@@ -65,3 +66,32 @@ export function openAITools(mode: SessionMode = 'chat', context: ToolAvailabilit
|
|
|
65
66
|
},
|
|
66
67
|
}))
|
|
67
68
|
}
|
|
69
|
+
|
|
70
|
+
const GEMINI_DROP_KEYS = new Set([
|
|
71
|
+
'additionalProperties',
|
|
72
|
+
'$schema',
|
|
73
|
+
'$ref',
|
|
74
|
+
'$defs',
|
|
75
|
+
'definitions',
|
|
76
|
+
])
|
|
77
|
+
|
|
78
|
+
function sanitizeForGemini(schema: unknown): unknown {
|
|
79
|
+
if (Array.isArray(schema)) return schema.map(sanitizeForGemini)
|
|
80
|
+
if (schema && typeof schema === 'object') {
|
|
81
|
+
const out: Record<string, unknown> = {}
|
|
82
|
+
for (const [k, v] of Object.entries(schema as Record<string, unknown>)) {
|
|
83
|
+
if (GEMINI_DROP_KEYS.has(k)) continue
|
|
84
|
+
out[k] = sanitizeForGemini(v)
|
|
85
|
+
}
|
|
86
|
+
return out
|
|
87
|
+
}
|
|
88
|
+
return schema
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function geminiTools(mode: SessionMode = 'chat', context: ToolAvailabilityContext = {}): GeminiToolDefinition[] {
|
|
92
|
+
return toolsForMode(mode, context).map(tool => ({
|
|
93
|
+
name: tool.name,
|
|
94
|
+
description: tool.description,
|
|
95
|
+
parameters: sanitizeForGemini(tool.inputSchemaJson) as GeminiToolDefinition['parameters'],
|
|
96
|
+
}))
|
|
97
|
+
}
|
|
@@ -78,7 +78,7 @@ async function prepareWrite(
|
|
|
78
78
|
assertSafeWritePath(input.path)
|
|
79
79
|
assertNotPrivateContinuityWorkspacePath(input.path, context.config, 'write_file')
|
|
80
80
|
if (input.content.length === 0) {
|
|
81
|
-
throw new Error('write_file content is empty; provide non-empty file contents')
|
|
81
|
+
throw new Error('Tool write_file content is empty; provide non-empty file contents')
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
const fullPath = resolveWorkspacePath(context.workspaceRoot, input.path)
|
|
@@ -91,7 +91,7 @@ async function prepareWrite(
|
|
|
91
91
|
async function readExistingFile(fullPath: string): Promise<{ before: string; existedBefore: boolean }> {
|
|
92
92
|
try {
|
|
93
93
|
const stats = await fs.stat(fullPath)
|
|
94
|
-
if (stats.isDirectory()) throw new Error('write_file path points to a directory; provide a file path')
|
|
94
|
+
if (stats.isDirectory()) throw new Error('Tool write_file path points to a directory; provide a file path')
|
|
95
95
|
return { before: await fs.readFile(fullPath, 'utf8'), existedBefore: true }
|
|
96
96
|
} catch (error: unknown) {
|
|
97
97
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { before: '', existedBefore: false }
|
|
@@ -113,13 +113,13 @@ async function tryRecordRewindSnapshot(
|
|
|
113
113
|
function assertSafeWritePath(requestedPath: string): void {
|
|
114
114
|
const trimmed = requestedPath.trim()
|
|
115
115
|
if (trimmed !== requestedPath || trimmed.length === 0) {
|
|
116
|
-
throw new Error('write_file path must be a clean workspace-relative file path')
|
|
116
|
+
throw new Error('Tool write_file path must be a clean workspace-relative file path')
|
|
117
117
|
}
|
|
118
118
|
if (/[|;&<>`]/.test(trimmed)) {
|
|
119
|
-
throw new Error('write_file path must not contain shell operators')
|
|
119
|
+
throw new Error('Tool write_file path must not contain shell operators')
|
|
120
120
|
}
|
|
121
121
|
if (/^(?:rm|del|erase|rmdir|remove-item|mkdir|type|cat|echo|copy|move|mv|cp)\b/i.test(trimmed)) {
|
|
122
|
-
throw new Error('write_file path looks like a shell command; pass only the file path')
|
|
122
|
+
throw new Error('Tool write_file path looks like a shell command; pass only the file path')
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
|
package/src/ui/BrandSplash.tsx
CHANGED
|
@@ -1,56 +1,14 @@
|
|
|
1
1
|
import React, { useEffect, useState } from 'react'
|
|
2
2
|
import { Text, Box } from 'ink'
|
|
3
|
-
import {
|
|
3
|
+
import { theme } from './theme.js'
|
|
4
4
|
|
|
5
5
|
const glyphs = {
|
|
6
|
-
ethagent:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
╚══════╝ ╚═╝ ╚═╝ ╚═╝`,
|
|
13
|
-
a: [
|
|
14
|
-
` █████╗ `,
|
|
15
|
-
`██╔══██╗`,
|
|
16
|
-
`███████║`,
|
|
17
|
-
`██╔══██║`,
|
|
18
|
-
`██║ ██║`,
|
|
19
|
-
`╚═╝ ╚═╝`,
|
|
20
|
-
].join('\n'),
|
|
21
|
-
g: [
|
|
22
|
-
` ██████╗ `,
|
|
23
|
-
`██╔════╝ `,
|
|
24
|
-
`██║ ███╗`,
|
|
25
|
-
`██║ ██║`,
|
|
26
|
-
`╚██████╔╝`,
|
|
27
|
-
` ╚═════╝ `,
|
|
28
|
-
].join('\n'),
|
|
29
|
-
e: [
|
|
30
|
-
`███████╗`,
|
|
31
|
-
`██╔════╝`,
|
|
32
|
-
`█████╗ `,
|
|
33
|
-
`██╔══╝ `,
|
|
34
|
-
`███████╗`,
|
|
35
|
-
`╚══════╝`,
|
|
36
|
-
].join('\n'),
|
|
37
|
-
n: [
|
|
38
|
-
`███╗ ██╗`,
|
|
39
|
-
`████╗ ██║`,
|
|
40
|
-
`██╔██╗ ██║`,
|
|
41
|
-
`██║╚██╗██║`,
|
|
42
|
-
`██║ ╚████║`,
|
|
43
|
-
`╚═╝ ╚═══╝`,
|
|
44
|
-
].join('\n'),
|
|
45
|
-
t: [
|
|
46
|
-
`████████╗`,
|
|
47
|
-
`╚══██╔══╝`,
|
|
48
|
-
` ██║ `,
|
|
49
|
-
` ██║ `,
|
|
50
|
-
` ██║ `,
|
|
51
|
-
` ╚═╝ `,
|
|
52
|
-
].join('\n'),
|
|
53
|
-
},
|
|
6
|
+
ethagent: `░░░░░░░╗░░░░░░░░╗░░╗ ░░╗ █████╗ ██████╗ ███████╗███╗ ██╗████████╗
|
|
7
|
+
░░╔════╝╚══░░╔══╝░░║ ░░║██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝
|
|
8
|
+
░░░░░╗ ░░║ ░░░░░░░║███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║
|
|
9
|
+
░░╔══╝ ░░║ ░░╔══░░║██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║
|
|
10
|
+
░░░░░░░╗ ░░║ ░░║ ░░║██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║
|
|
11
|
+
╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ `,
|
|
54
12
|
eyes: `
|
|
55
13
|
-+:
|
|
56
14
|
:=- -%@@@%.
|
|
@@ -79,33 +37,13 @@ const glyphs = {
|
|
|
79
37
|
},
|
|
80
38
|
} as const
|
|
81
39
|
|
|
82
|
-
const ethagentGlyphOrder = ['eth', 'a', 'g', 'e', 'n', 't'] as const
|
|
83
|
-
|
|
84
40
|
const Eyes = () => {
|
|
85
41
|
const lines = glyphs.eyes.split('\n')
|
|
86
42
|
return (
|
|
87
43
|
<Box flexDirection="column">
|
|
88
|
-
{lines.map((line, li) =>
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
.filter(entry => entry.char.trim().length > 0)
|
|
92
|
-
.map(entry => entry.index)
|
|
93
|
-
const firstGlyph = glyphPositions[0] ?? 0
|
|
94
|
-
const lastGlyph = glyphPositions[glyphPositions.length - 1] ?? firstGlyph
|
|
95
|
-
const span = Math.max(lastGlyph - firstGlyph, 1)
|
|
96
|
-
|
|
97
|
-
return (
|
|
98
|
-
<Text key={li}>
|
|
99
|
-
{[...line].map((char, ci) => {
|
|
100
|
-
if (!char.trim()) {
|
|
101
|
-
return <Text key={ci}>{char}</Text>
|
|
102
|
-
}
|
|
103
|
-
const t = (ci - firstGlyph) / span
|
|
104
|
-
return <Text key={ci} color={eyeGradientColor(t)}>{char}</Text>
|
|
105
|
-
})}
|
|
106
|
-
</Text>
|
|
107
|
-
)
|
|
108
|
-
})}
|
|
44
|
+
{lines.map((line, li) => (
|
|
45
|
+
<Text key={li} color={theme.text}>{line}</Text>
|
|
46
|
+
))}
|
|
109
47
|
</Box>
|
|
110
48
|
)
|
|
111
49
|
}
|
|
@@ -135,19 +73,18 @@ export const BrandSplash: React.FC<SplashProps> = ({ contextLine, tipLine, updat
|
|
|
135
73
|
return (
|
|
136
74
|
<Box flexDirection="column" alignSelf="flex-start" padding={1}>
|
|
137
75
|
<Eyes />
|
|
138
|
-
<Text bold color={theme.
|
|
139
|
-
<Text color={theme.dim}>{
|
|
76
|
+
<Text bold color={theme.accentWhite}>ethagent</Text>
|
|
77
|
+
<Text color={theme.dim}>privacy-first AI agent with a portable <Text color={theme.accentPeriwinkle}>Ethereum</Text> identity</Text>
|
|
140
78
|
{contextLine ? <Text color={theme.dim}>{contextLine}</Text> : null}
|
|
141
79
|
{tipLine ? <Text color={theme.dim}>{tipLine}</Text> : null}
|
|
142
|
-
{updateNotice ? <Text color={theme.
|
|
80
|
+
{updateNotice ? <Text color={theme.accentPeriwinkle}>{updateNotice}</Text> : null}
|
|
143
81
|
</Box>
|
|
144
82
|
)
|
|
145
83
|
}
|
|
146
84
|
|
|
147
|
-
const logoLines = ethagentGlyphOrder.map(key => glyphs.ethagent[key].split('\n'))
|
|
148
|
-
const rowCount = Math.max(...logoLines.map(lines => lines.length))
|
|
149
|
-
|
|
150
85
|
const w = 69
|
|
86
|
+
const logoLines = glyphs.ethagent.split('\n').map(line => line.padEnd(w, ' '))
|
|
87
|
+
|
|
151
88
|
const topPad = Math.max(0, w - glyphs.tagline.length - 1)
|
|
152
89
|
|
|
153
90
|
const bottomInline = contextLine ? ` ${truncateToFit(contextLine, w - 4)} ` : ''
|
|
@@ -158,22 +95,20 @@ export const BrandSplash: React.FC<SplashProps> = ({ contextLine, tipLine, updat
|
|
|
158
95
|
<Eyes />
|
|
159
96
|
<Text>
|
|
160
97
|
<Text color={theme.border}>{glyphs.frame.topLeft}</Text>
|
|
161
|
-
<Text color={theme.dim}>{
|
|
98
|
+
<Text color={theme.dim}>{' privacy-first AI agent with a portable '}<Text color={theme.accentPeriwinkle}>Ethereum</Text>{' identity '}</Text>
|
|
162
99
|
<Text color={theme.border}>{glyphs.frame.horizontal.repeat(topPad)}{glyphs.frame.topRight}</Text>
|
|
163
100
|
</Text>
|
|
164
|
-
{
|
|
101
|
+
{logoLines.map((line, i) => (
|
|
165
102
|
<Box key={i}>
|
|
166
103
|
<Text color={theme.border}>{glyphs.frame.side}</Text>
|
|
167
|
-
{
|
|
168
|
-
<Text key={ethagentGlyphOrder[index]} color={theme.border}>{lines[i] ?? ''}</Text>
|
|
169
|
-
))}
|
|
104
|
+
<Text color={theme.border}>{line}</Text>
|
|
170
105
|
<Text color={theme.border}>{glyphs.frame.side}</Text>
|
|
171
106
|
</Box>
|
|
172
107
|
))}
|
|
173
108
|
{bottomInline ? (
|
|
174
109
|
<Text>
|
|
175
110
|
<Text color={theme.border}>{glyphs.frame.bottomLeft}</Text>
|
|
176
|
-
<Text color={theme.
|
|
111
|
+
<Text color={theme.accentPeriwinkle}>{bottomInline}</Text>
|
|
177
112
|
<Text color={theme.border}>{glyphs.frame.horizontal.repeat(bottomPad)}{glyphs.frame.bottomRight}</Text>
|
|
178
113
|
</Text>
|
|
179
114
|
) : (
|
|
@@ -182,7 +117,7 @@ export const BrandSplash: React.FC<SplashProps> = ({ contextLine, tipLine, updat
|
|
|
182
117
|
{tipLine || updateNotice ? (
|
|
183
118
|
<Box marginTop={1} flexDirection="column">
|
|
184
119
|
{tipLine ? <Text color={theme.dim}>{tipLine}</Text> : null}
|
|
185
|
-
{updateNotice ? <Text color={theme.
|
|
120
|
+
{updateNotice ? <Text color={theme.accentPeriwinkle}>{updateNotice}</Text> : null}
|
|
186
121
|
</Box>
|
|
187
122
|
) : null}
|
|
188
123
|
</Box>
|
package/src/ui/ProgressBar.tsx
CHANGED
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { Box, Text } from 'ink'
|
|
3
|
-
import { theme, gradientColor
|
|
3
|
+
import { theme, gradientColor } from './theme.js'
|
|
4
4
|
|
|
5
5
|
type ProgressBarProps = {
|
|
6
6
|
progress: number
|
|
7
7
|
width?: number
|
|
8
8
|
label?: string
|
|
9
9
|
suffix?: string
|
|
10
|
-
variant?: 'default' | 'rainbow'
|
|
11
10
|
}
|
|
12
11
|
|
|
13
|
-
export const ProgressBar: React.FC<ProgressBarProps> = ({ progress, width = 40, label, suffix
|
|
12
|
+
export const ProgressBar: React.FC<ProgressBarProps> = ({ progress, width = 40, label, suffix }) => {
|
|
14
13
|
const p = Math.max(0, Math.min(1, progress))
|
|
15
14
|
const filled = Math.round(p * width)
|
|
16
15
|
const empty = Math.max(0, width - filled)
|
|
17
|
-
const colorFor = variant === 'rainbow' ? eyeGradientColor : gradientColor
|
|
18
16
|
const cells: React.ReactElement[] = []
|
|
19
17
|
for (let i = 0; i < filled; i++) {
|
|
20
18
|
cells.push(
|
|
21
|
-
<Text key={`f-${i}`} color={
|
|
19
|
+
<Text key={`f-${i}`} color={gradientColor(i / Math.max(width - 1, 1))}>█</Text>,
|
|
22
20
|
)
|
|
23
21
|
}
|
|
24
22
|
for (let i = 0; i < empty; i++) {
|
package/src/ui/Select.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
1
|
+
import React, { useEffect, useMemo, useState } from 'react'
|
|
2
2
|
import { Box, Text } from 'ink'
|
|
3
3
|
import { theme } from './theme.js'
|
|
4
4
|
import { useAppInput } from '../app/input/AppInputProvider.js'
|
|
@@ -39,9 +39,18 @@ export function Select<T>({
|
|
|
39
39
|
onCancel,
|
|
40
40
|
onHighlight,
|
|
41
41
|
}: SelectProps<T>) {
|
|
42
|
+
const optionsSignature = useMemo(
|
|
43
|
+
() => options.map(option => `${String(option.value)}:${option.disabled ? 'disabled' : 'enabled'}:${option.role ?? 'option'}`).join('|'),
|
|
44
|
+
[options],
|
|
45
|
+
)
|
|
42
46
|
const firstEnabled = Math.max(0, options.findIndex(isSelectableOption))
|
|
43
47
|
const start = isSelectableOption(options[initialIndex]) ? initialIndex : firstEnabled
|
|
44
48
|
const [index, setIndex] = useState(start === -1 ? 0 : start)
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
setIndex(start === -1 ? 0 : start)
|
|
52
|
+
}, [optionsSignature, start])
|
|
53
|
+
|
|
45
54
|
const visibleCount = Math.max(1, maxVisible ?? options.length)
|
|
46
55
|
const windowStart = Math.max(0, Math.min(
|
|
47
56
|
index - Math.floor(visibleCount / 2),
|
|
@@ -88,24 +97,27 @@ export function Select<T>({
|
|
|
88
97
|
const absoluteIndex = windowStart + visibleIndex
|
|
89
98
|
const isActive = absoluteIndex === index
|
|
90
99
|
const selectable = isSelectableOption(option)
|
|
100
|
+
const disabled = !!option.disabled
|
|
91
101
|
const cursor = !selectable ? ' ' : isActive ? '>' : ' '
|
|
92
102
|
const isSection = option.role === 'section' || option.role === 'group'
|
|
93
103
|
const prefix = option.prefix && !isSection ? `${option.prefix} ` : ''
|
|
94
104
|
const rowIndent = option.indent ?? (usesInlineSections ? isSection ? 1 : 3 : 0)
|
|
95
|
-
const prefixColor =
|
|
105
|
+
const prefixColor = disabled
|
|
96
106
|
? option.labelColor ?? theme.border
|
|
97
107
|
: isActive && selectable
|
|
98
|
-
? theme.
|
|
108
|
+
? theme.accentPeriwinkle
|
|
99
109
|
: option.labelColor ?? theme.dim
|
|
100
110
|
const labelColor = isSection
|
|
101
|
-
? option.labelColor ?? theme.
|
|
111
|
+
? option.labelColor ?? theme.textSubtle
|
|
102
112
|
: isActive && selectable
|
|
103
|
-
? theme.
|
|
104
|
-
: option.labelColor ?? (
|
|
113
|
+
? theme.accentPeriwinkle
|
|
114
|
+
: option.labelColor ?? (disabled ? theme.dim : theme.text)
|
|
105
115
|
const hintColor = isActive && selectable
|
|
106
116
|
? theme.textSubtle
|
|
107
|
-
:
|
|
108
|
-
|
|
117
|
+
: disabled
|
|
118
|
+
? theme.border
|
|
119
|
+
: option.hintColor ?? theme.dim
|
|
120
|
+
const subtextColor = disabled ? theme.border : option.subtextColor ?? theme.dim
|
|
109
121
|
const bold = option.bold ?? (isSection || (isActive && selectable))
|
|
110
122
|
const inlineHint = Boolean(option.hint && hintLayout === 'inline' && !isSection)
|
|
111
123
|
const belowHint = Boolean(option.hint && (!inlineHint || isSection))
|
package/src/ui/Spinner.tsx
CHANGED
|
@@ -193,6 +193,15 @@ export function pickVerb(): string {
|
|
|
193
193
|
return SPINNER_VERBS[idx] ?? 'thinking'
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
export function spinnerText(value: string): string {
|
|
197
|
+
const text = restoreSpinnerTerms(value.toLowerCase())
|
|
198
|
+
return text.replace(/^(\s*)([a-z])/, (_match, prefix: string, letter: string) => `${prefix}${letter.toUpperCase()}`)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function spinnerHintText(value: string): string {
|
|
202
|
+
return restoreSpinnerTerms(value.toLowerCase())
|
|
203
|
+
}
|
|
204
|
+
|
|
196
205
|
type SpinnerProps = {
|
|
197
206
|
active?: boolean
|
|
198
207
|
hint?: string
|
|
@@ -210,7 +219,7 @@ export const Spinner: React.FC<SpinnerProps> = ({
|
|
|
210
219
|
hint: rawHint,
|
|
211
220
|
label,
|
|
212
221
|
verb,
|
|
213
|
-
color = theme.
|
|
222
|
+
color = theme.accentPeriwinkle,
|
|
214
223
|
startedAt,
|
|
215
224
|
showElapsed = true,
|
|
216
225
|
}) => {
|
|
@@ -246,11 +255,11 @@ export const Spinner: React.FC<SpinnerProps> = ({
|
|
|
246
255
|
if (!active) return null
|
|
247
256
|
|
|
248
257
|
const autoLabel = stickyVerbRef.current ?? verb ?? 'thinking'
|
|
249
|
-
const text = label ?? `${autoLabel}…`
|
|
258
|
+
const text = spinnerText(label ?? `${autoLabel}…`)
|
|
250
259
|
const glyph = FRAMES[frame] ?? 'o'
|
|
251
260
|
const elapsed = showElapsed ? formatElapsedSeconds(Date.now() - (startedAt ?? internalStartedAtRef.current)) : null
|
|
252
261
|
const renderedHint = [rawHint, elapsed].filter(Boolean).join(' · ')
|
|
253
|
-
const hint = renderedHint
|
|
262
|
+
const hint = renderedHint ? spinnerHintText(renderedHint) : ''
|
|
254
263
|
|
|
255
264
|
return (
|
|
256
265
|
<Text>
|
|
@@ -267,3 +276,29 @@ function formatElapsedSeconds(milliseconds: number): string {
|
|
|
267
276
|
const minutes = Math.floor(seconds / 60)
|
|
268
277
|
return `${minutes}:${(seconds % 60).toString().padStart(2, '0')}`
|
|
269
278
|
}
|
|
279
|
+
|
|
280
|
+
function restoreSpinnerTerms(value: string): string {
|
|
281
|
+
return value
|
|
282
|
+
.replace(/\bapi\b/g, 'API')
|
|
283
|
+
.replace(/\bens\b/g, 'ENS')
|
|
284
|
+
.replace(/\berc-8004\b/g, 'ERC-8004')
|
|
285
|
+
.replace(/\bgguf\b/g, 'GGUF')
|
|
286
|
+
.replace(/\bhugging face\b/g, 'Hugging Face')
|
|
287
|
+
.replace(/\bipfs\b/g, 'IPFS')
|
|
288
|
+
.replace(/\bjson\b/g, 'JSON')
|
|
289
|
+
.replace(/\bjwt\b/g, 'JWT')
|
|
290
|
+
.replace(/\bmemory\.md\b/g, 'MEMORY.md')
|
|
291
|
+
.replace(/\bopenai\b/g, 'OpenAI')
|
|
292
|
+
.replace(/\banthropic\b/g, 'Anthropic')
|
|
293
|
+
.replace(/\bgemini\b/g, 'Gemini')
|
|
294
|
+
.replace(/\bos\b/g, 'OS')
|
|
295
|
+
.replace(/\brpc\b/g, 'RPC')
|
|
296
|
+
.replace(/\bsoul\.md\b/g, 'SOUL.md')
|
|
297
|
+
.replace(/\buri\b/g, 'URI')
|
|
298
|
+
.replace(/\burl\b/g, 'URL')
|
|
299
|
+
.replace(/\bbase\b/g, 'Base')
|
|
300
|
+
.replace(/\bethereum mainnet\b/g, 'Ethereum Mainnet')
|
|
301
|
+
.replace(/\bethereum\b/g, 'Ethereum')
|
|
302
|
+
.replace(/\bsepolia\b/g, 'Sepolia')
|
|
303
|
+
.replace(/\bbase sepolia\b/g, 'Base Sepolia')
|
|
304
|
+
}
|
package/src/ui/Surface.tsx
CHANGED
|
@@ -13,9 +13,9 @@ type SurfaceProps = {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const toneColor: Record<SurfaceTone, string> = {
|
|
16
|
-
primary: theme.
|
|
16
|
+
primary: theme.accentPeriwinkle,
|
|
17
17
|
muted: theme.border,
|
|
18
|
-
error:
|
|
18
|
+
error: theme.accentError,
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export const Surface: React.FC<SurfaceProps> = ({
|
package/src/ui/TextInput.tsx
CHANGED
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
getVisualLines,
|
|
9
9
|
} from '../chat/textCursor.js'
|
|
10
10
|
|
|
11
|
-
// ConversationStack padding=1 (2) + Surface border (2) + Surface paddingX=2 (4) + '> ' prefix (2) = 10
|
|
12
11
|
const DEFAULT_CHROME_WIDTH = 10
|
|
13
12
|
|
|
14
13
|
type TextInputProps = {
|
|
@@ -23,6 +22,8 @@ type TextInputProps = {
|
|
|
23
22
|
validate?: (value: string) => string | null
|
|
24
23
|
onSubmit: (value: string) => void
|
|
25
24
|
onCancel?: () => void
|
|
25
|
+
onNavigateLeft?: () => void
|
|
26
|
+
onNavigateRight?: (value: string) => void
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
type RenderedTextInputLine = {
|
|
@@ -42,6 +43,8 @@ export function TextInput({
|
|
|
42
43
|
validate,
|
|
43
44
|
onSubmit,
|
|
44
45
|
onCancel,
|
|
46
|
+
onNavigateLeft,
|
|
47
|
+
onNavigateRight,
|
|
45
48
|
}: TextInputProps) {
|
|
46
49
|
const { stdout } = useStdout()
|
|
47
50
|
const [value, setValue] = useState(initialValue)
|
|
@@ -49,7 +52,6 @@ export function TextInput({
|
|
|
49
52
|
const [preferredColumn, setPreferredColumn] = useState<number | null>(null)
|
|
50
53
|
const [error, setError] = useState<string | null>(null)
|
|
51
54
|
|
|
52
|
-
// Keep a columns state updated via resize, matching ChatInput's pattern exactly
|
|
53
55
|
const [columns, setColumns] = useState<number>(() => Math.floor(stdout?.columns ?? 80))
|
|
54
56
|
useEffect(() => {
|
|
55
57
|
if (!stdout) return
|
|
@@ -60,26 +62,37 @@ export function TextInput({
|
|
|
60
62
|
|
|
61
63
|
const wrapWidth = textInputWrapWidth(columns, chromeWidth)
|
|
62
64
|
|
|
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
65
|
const stateRef = useRef({ value, cursor, preferredColumn, wrapWidth })
|
|
66
66
|
stateRef.current = { value, cursor, preferredColumn, wrapWidth }
|
|
67
67
|
|
|
68
68
|
useAppInput((input, key) => {
|
|
69
69
|
const { value: val, cursor: cur, preferredColumn: prefCol, wrapWidth: ww } = stateRef.current
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
const submitValue = (submit: (value: string) => void) => {
|
|
72
72
|
if (!allowEmpty && val.trim().length === 0) {
|
|
73
73
|
setError('value cannot be empty')
|
|
74
|
-
return
|
|
74
|
+
return false
|
|
75
75
|
}
|
|
76
76
|
const validationError = validate?.(val) ?? null
|
|
77
77
|
if (validationError) {
|
|
78
78
|
setError(validationError)
|
|
79
|
-
return
|
|
79
|
+
return false
|
|
80
80
|
}
|
|
81
81
|
setError(null)
|
|
82
|
-
|
|
82
|
+
submit(val)
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (multiline && isTextInputSoftBreak(key)) {
|
|
87
|
+
const next = insertTextInputText(val, cur, '\n', maxLength)
|
|
88
|
+
setValue(next.value)
|
|
89
|
+
setCursor(next.cursor)
|
|
90
|
+
setPreferredColumn(null)
|
|
91
|
+
if (error) setError(null)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
if (key.return) {
|
|
95
|
+
submitValue(onSubmit)
|
|
83
96
|
return
|
|
84
97
|
}
|
|
85
98
|
if (key.escape || (key.ctrl && input === 'c')) {
|
|
@@ -87,11 +100,19 @@ export function TextInput({
|
|
|
87
100
|
return
|
|
88
101
|
}
|
|
89
102
|
if (key.leftArrow) {
|
|
103
|
+
if (onNavigateLeft && cur === 0) {
|
|
104
|
+
onNavigateLeft()
|
|
105
|
+
return
|
|
106
|
+
}
|
|
90
107
|
setCursor(Math.max(0, cur - 1))
|
|
91
108
|
setPreferredColumn(null)
|
|
92
109
|
return
|
|
93
110
|
}
|
|
94
111
|
if (key.rightArrow) {
|
|
112
|
+
if (onNavigateRight && cur === val.length) {
|
|
113
|
+
submitValue(onNavigateRight)
|
|
114
|
+
return
|
|
115
|
+
}
|
|
95
116
|
setCursor(Math.min(val.length, cur + 1))
|
|
96
117
|
setPreferredColumn(null)
|
|
97
118
|
return
|
|
@@ -111,8 +132,15 @@ export function TextInput({
|
|
|
111
132
|
return
|
|
112
133
|
}
|
|
113
134
|
if (key.ctrl && input === 'u') {
|
|
114
|
-
|
|
115
|
-
|
|
135
|
+
const lineStart = val.lastIndexOf('\n', cur - 1) + 1
|
|
136
|
+
if (lineStart === cur) {
|
|
137
|
+
if (!multiline || cur === 0) return
|
|
138
|
+
setValue(val.slice(0, cur - 1) + val.slice(cur))
|
|
139
|
+
setCursor(cur - 1)
|
|
140
|
+
} else {
|
|
141
|
+
setValue(val.slice(0, lineStart) + val.slice(cur))
|
|
142
|
+
setCursor(lineStart)
|
|
143
|
+
}
|
|
116
144
|
setPreferredColumn(null)
|
|
117
145
|
if (error) setError(null)
|
|
118
146
|
return
|
|
@@ -121,11 +149,13 @@ export function TextInput({
|
|
|
121
149
|
return
|
|
122
150
|
}
|
|
123
151
|
if (input) {
|
|
124
|
-
const clean =
|
|
152
|
+
const clean = multiline
|
|
153
|
+
? input.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
|
154
|
+
: input.replace(/[\r\n]/g, '')
|
|
125
155
|
if (clean) {
|
|
126
|
-
const next = (val
|
|
127
|
-
setValue(next)
|
|
128
|
-
setCursor(
|
|
156
|
+
const next = insertTextInputText(val, cur, clean, maxLength)
|
|
157
|
+
setValue(next.value)
|
|
158
|
+
setCursor(next.cursor)
|
|
129
159
|
setPreferredColumn(null)
|
|
130
160
|
if (error) setError(null)
|
|
131
161
|
}
|
|
@@ -145,7 +175,7 @@ export function TextInput({
|
|
|
145
175
|
<Box flexDirection="column">
|
|
146
176
|
{renderedLines.map(line => (
|
|
147
177
|
<Box key={line.visualLineIndex} flexDirection="row">
|
|
148
|
-
<Text color={line.visualLineIndex === 0 ? theme.
|
|
178
|
+
<Text color={line.visualLineIndex === 0 ? theme.accentPeriwinkle : theme.dim}>
|
|
149
179
|
{line.visualLineIndex === 0 ? '> ' : ' '}
|
|
150
180
|
</Text>
|
|
151
181
|
<Box width={wrapWidth}>{line.node}</Box>
|
|
@@ -154,24 +184,24 @@ export function TextInput({
|
|
|
154
184
|
</Box>
|
|
155
185
|
) : (
|
|
156
186
|
<Box flexDirection="row">
|
|
157
|
-
<Text color={theme.
|
|
187
|
+
<Text color={theme.accentPeriwinkle}>{'> '}</Text>
|
|
158
188
|
<Box width={wrapWidth}>
|
|
159
189
|
{showPlaceholder ? (
|
|
160
190
|
<Text wrap={multiline ? 'wrap' : 'truncate-end'}>
|
|
161
|
-
<Text backgroundColor={theme.
|
|
191
|
+
<Text backgroundColor={theme.accentPeriwinkle} color="#0c0c1f">{' '}</Text>
|
|
162
192
|
<Text color={theme.dim}>{placeholder}</Text>
|
|
163
193
|
</Text>
|
|
164
194
|
) : (
|
|
165
195
|
<Text color={theme.text} wrap="truncate-end">
|
|
166
196
|
{display.slice(0, cursor)}
|
|
167
|
-
<Text backgroundColor={theme.
|
|
197
|
+
<Text backgroundColor={theme.accentPeriwinkle} color="#0c0c1f">{display[cursor] ?? ' '}</Text>
|
|
168
198
|
{display.slice(cursor + 1)}
|
|
169
199
|
</Text>
|
|
170
200
|
)}
|
|
171
201
|
</Box>
|
|
172
202
|
</Box>
|
|
173
203
|
)}
|
|
174
|
-
{error ? <Text color=
|
|
204
|
+
{error ? <Text color={theme.accentError}>{error}</Text> : null}
|
|
175
205
|
</Box>
|
|
176
206
|
)
|
|
177
207
|
}
|
|
@@ -180,6 +210,19 @@ export function textInputWrapWidth(columns: number, chromeWidth = DEFAULT_CHROME
|
|
|
180
210
|
return Math.max(1, Math.floor(columns) - Math.max(0, Math.floor(chromeWidth)))
|
|
181
211
|
}
|
|
182
212
|
|
|
213
|
+
export function insertTextInputText(value: string, cursor: number, input: string, maxLength = 4096): { value: string; cursor: number } {
|
|
214
|
+
const cleanCursor = Math.max(0, Math.min(cursor, value.length))
|
|
215
|
+
const next = (value.slice(0, cleanCursor) + input + value.slice(cleanCursor)).slice(0, maxLength)
|
|
216
|
+
return {
|
|
217
|
+
value: next,
|
|
218
|
+
cursor: Math.min(cleanCursor + input.length, next.length),
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function isTextInputSoftBreak(key: { return: boolean; shift?: boolean; meta?: boolean }): boolean {
|
|
223
|
+
return key.return && Boolean(key.shift || key.meta)
|
|
224
|
+
}
|
|
225
|
+
|
|
183
226
|
export function renderTextInputLines(
|
|
184
227
|
value: string,
|
|
185
228
|
cursor: number,
|
|
@@ -207,7 +250,7 @@ export function renderTextInputLines(
|
|
|
207
250
|
node: (
|
|
208
251
|
<Text color={theme.text} wrap="wrap">
|
|
209
252
|
{before}
|
|
210
|
-
<Text backgroundColor={theme.
|
|
253
|
+
<Text backgroundColor={theme.accentPeriwinkle} color="#0c0c1f">{atChar}</Text>
|
|
211
254
|
{after}
|
|
212
255
|
</Text>
|
|
213
256
|
),
|