ethagent 2.1.1 → 2.3.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/package.json +2 -1
- package/src/app/FirstRun.tsx +1 -7
- package/src/app/FirstRunTimeline.tsx +1 -1
- package/src/auth/openaiOAuth/credentials.ts +47 -0
- package/src/auth/openaiOAuth/crypto.ts +23 -0
- package/src/auth/openaiOAuth/index.ts +238 -0
- package/src/auth/openaiOAuth/landingPage.ts +125 -0
- package/src/auth/openaiOAuth/listener.ts +151 -0
- package/src/auth/openaiOAuth/refresh.ts +70 -0
- package/src/auth/openaiOAuth/shared.ts +115 -0
- package/src/chat/ChatBottomPane.tsx +20 -11
- package/src/chat/ChatScreen.tsx +160 -35
- package/src/chat/ConversationStack.tsx +1 -1
- package/src/chat/MessageList.tsx +185 -72
- package/src/chat/SessionStatus.tsx +3 -1
- package/src/chat/chatScreenUtils.ts +11 -15
- package/src/chat/chatSessionState.ts +3 -2
- package/src/chat/chatTurnOrchestrator.ts +1 -7
- package/src/chat/commands.ts +28 -27
- package/src/chat/display/DiffView.tsx +193 -0
- package/src/chat/display/SyntaxText.tsx +192 -0
- package/src/chat/display/toolCallDisplay.ts +103 -0
- package/src/chat/display/toolResultDisplay.ts +19 -0
- package/src/chat/{ChatInput.tsx → input/ChatInput.tsx} +36 -23
- package/src/chat/{TranscriptView.tsx → transcript/TranscriptView.tsx} +24 -50
- package/src/chat/{transcriptViewport.ts → transcript/transcriptViewport.ts} +12 -30
- package/src/chat/{ContextLimitView.tsx → views/ContextLimitView.tsx} +3 -3
- package/src/chat/{ContinuityEditReviewView.tsx → views/ContinuityEditReviewView.tsx} +11 -3
- package/src/chat/{CopyPicker.tsx → views/CopyPicker.tsx} +4 -5
- package/src/chat/{PermissionPrompt.tsx → views/PermissionPrompt.tsx} +16 -17
- package/src/chat/{PermissionsView.tsx → views/PermissionsView.tsx} +6 -6
- package/src/chat/{PlanApprovalView.tsx → views/PlanApprovalView.tsx} +4 -4
- package/src/chat/{ResumeView.tsx → views/ResumeView.tsx} +35 -35
- package/src/chat/views/RewindView.tsx +410 -0
- package/src/identity/continuity/privateEdit/diff.ts +2 -78
- package/src/identity/ens/agentRecords.ts +5 -19
- package/src/identity/ens/ensAutomation/setup.ts +0 -1
- package/src/identity/ens/ensAutomation/types.ts +0 -1
- package/src/identity/hub/OperationalRoutes.tsx +23 -32
- package/src/identity/hub/Routes.tsx +13 -13
- package/src/identity/hub/{flows/continuity → continuity}/ContinuityDashboardScreen.tsx +9 -9
- package/src/identity/hub/{flows/continuity → continuity}/RebackupStorageScreen.tsx +2 -2
- package/src/identity/hub/{flows/continuity → continuity}/RecoveryConfirmScreen.tsx +5 -5
- package/src/identity/hub/{flows/continuity → continuity}/SavePromptScreen.tsx +5 -5
- package/src/identity/hub/{effects/rebackup/runRebackup.ts → continuity/effects.ts} +19 -19
- package/src/identity/hub/{effects/rebackup → continuity}/index.ts +1 -1
- package/src/identity/hub/{effects/shared → continuity}/snapshot.ts +8 -8
- package/src/identity/hub/{effects/rebackup → continuity}/vault.ts +15 -15
- package/src/identity/hub/{flows/create → create}/CreateFlow.tsx +13 -13
- package/src/identity/hub/{effects/create.ts → create/effects.ts} +4 -4
- package/src/identity/hub/{flows/custody → custody}/CustodyEditFlow.tsx +10 -48
- package/src/identity/hub/{flows/custody/custodyFlowActions.ts → custody/actions.ts} +11 -9
- package/src/identity/hub/{flows/custody/custodyFlowHelpers.ts → custody/helpers.ts} +4 -4
- package/src/identity/hub/{effects/vault → custody}/preflight.ts +5 -5
- package/src/identity/hub/{flows/custody/custodyFlowRoutes.tsx → custody/routes.tsx} +8 -8
- package/src/identity/hub/{flows/custody/custodyEffects.ts → custody/transactions.ts} +9 -9
- package/src/identity/hub/{flows/custody/custodyFlowTypes.ts → custody/types.ts} +6 -6
- package/src/identity/hub/{flows/custody/custodyFlowEffects.ts → custody/useCustodyEffects.ts} +7 -7
- package/src/identity/hub/{flows/custody → custody}/useCustodyFlow.tsx +5 -5
- package/src/identity/hub/ens/EnsEditAdvancedScreens.tsx +241 -0
- package/src/identity/hub/{flows/ens → ens}/EnsEditFlow.tsx +27 -82
- package/src/identity/hub/{flows/ens → ens}/EnsEditMaintenanceScreens.tsx +25 -65
- package/src/identity/hub/{flows/ens → ens}/EnsEditReviewScreens.tsx +12 -30
- package/src/identity/hub/ens/EnsEditRunners.tsx +62 -0
- package/src/identity/hub/{flows/ens → ens}/EnsEditShared.tsx +15 -14
- package/src/identity/hub/{flows/ens → ens}/EnsEditSimpleScreens.tsx +68 -217
- package/src/identity/hub/{flows/ens/IdentityHubEnsFlow.tsx → ens/EnsFlow.tsx} +18 -11
- package/src/identity/hub/{flows/ens/OperatorWalletsScreen.tsx → ens/EnsOperatorWalletsScreen.tsx} +17 -48
- package/src/identity/hub/{advancedEnsValidation.ts → ens/advancedEnsValidation.ts} +2 -2
- package/src/identity/hub/{flows/ens/ensEditCopy.ts → ens/editCopy.ts} +4 -4
- package/src/identity/hub/{effects/ens/flows.ts → ens/effects.ts} +7 -7
- package/src/identity/hub/{effects/ens → ens}/index.ts +1 -1
- package/src/identity/hub/{model/ens.ts → ens/state.ts} +1 -1
- package/src/identity/hub/{effects/ens → ens}/transactions.ts +232 -232
- package/src/identity/hub/{flows/ens/ensEditTypes.ts → ens/types.ts} +12 -26
- package/src/identity/hub/identityHubReducer.ts +3 -3
- package/src/identity/hub/{flows/profile → profile}/EditProfileFlow.tsx +17 -10
- package/src/identity/hub/{effects/publicProfile/runPublicProfileSave.ts → profile/effects.ts} +55 -177
- package/src/identity/hub/{model → profile}/identity.ts +3 -3
- package/src/identity/hub/{effects/profile/profileState.ts → profile/state.ts} +181 -173
- package/src/identity/hub/{flows/restore → restore}/RestoreFlow.tsx +21 -21
- package/src/identity/hub/{effects/restore → restore}/apply.ts +10 -10
- package/src/identity/hub/{effects/restore → restore}/auth.ts +7 -7
- package/src/identity/hub/{effects/restore → restore}/discover.ts +6 -6
- package/src/identity/hub/{effects/restore → restore}/envelopes.ts +2 -2
- package/src/identity/hub/{effects/restore → restore}/fetch.ts +3 -3
- package/src/identity/hub/{effects/restore/shared.ts → restore/helpers.ts} +6 -6
- package/src/identity/hub/{effects/restore → restore}/recovery.ts +10 -10
- package/src/identity/hub/{effects/restore → restore}/resolve.ts +4 -4
- package/src/identity/hub/restore/restoreAdmin.ts +34 -0
- package/src/identity/hub/{flows/restore/useRestoreFlowEffects.ts → restore/useRestoreEffects.ts} +5 -5
- package/src/identity/hub/{flows/settings → settings}/StorageCredentialScreen.tsx +5 -5
- package/src/identity/hub/{components → shared/components}/BusyScreen.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/DetailsScreen.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/ErrorScreen.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/FlowTimeline.tsx +1 -1
- package/src/identity/hub/{components → shared/components}/IdentitySummary.tsx +16 -11
- package/src/identity/hub/{components → shared/components}/MenuScreen.tsx +8 -9
- package/src/identity/hub/{components → shared/components}/NetworkScreen.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/PinataJwtInput.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/UnlinkedIdentityScreen.tsx +5 -5
- package/src/identity/hub/{components → shared/components}/WalletApprovalScreen.tsx +6 -6
- package/src/identity/hub/{components → shared/components}/menuFlagsFromReconciliation.ts +2 -4
- package/src/identity/hub/{effects/shared → shared/effects}/profilePrep.ts +1 -1
- package/src/identity/hub/{effects → shared/effects}/receipts.ts +2 -2
- package/src/identity/hub/{effects/shared → shared/effects}/sync.ts +6 -47
- package/src/identity/hub/{effects → shared/effects}/types.ts +3 -3
- package/src/identity/hub/{model → shared/model}/copy.ts +2 -2
- package/src/identity/hub/{model → shared/model}/errors.ts +5 -5
- package/src/identity/hub/{model → shared/model}/network.ts +3 -3
- package/src/identity/hub/{operatorWallets.ts → shared/operatorWallets.ts} +1 -1
- package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/hook.ts +1 -2
- package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/ownership.ts +2 -2
- package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/run.ts +7 -40
- package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/types.ts +0 -4
- package/src/identity/hub/{reconciliation → shared/reconciliation}/index.ts +0 -7
- package/src/identity/hub/shared/reconciliation/walletSetup.ts +27 -0
- package/src/identity/hub/{utils.ts → shared/utils.ts} +5 -5
- package/src/identity/hub/{flows/token-transfer/IdentityHubTokenTransferFlow.tsx → transfer/TokenTransferFlow.tsx} +8 -8
- package/src/identity/hub/{flows/token-transfer → transfer}/TokenTransferScreens.tsx +14 -14
- package/src/identity/hub/{effects/token-transfer/runTokenTransfer.ts → transfer/effects.ts} +16 -16
- package/src/identity/hub/{effects/token-transfer → transfer}/progress.ts +1 -1
- package/src/identity/hub/useIdentityHubController.ts +11 -11
- package/src/identity/hub/useIdentityHubSideEffects.ts +11 -11
- package/src/identity/wallet/browserWallet/types.ts +0 -5
- package/src/identity/wallet/page/copy.ts +1 -31
- package/src/identity/wallet/walletPurposeCompat.ts +0 -2
- package/src/models/ModelPicker.tsx +248 -8
- package/src/models/catalog.ts +29 -1
- package/src/models/modelPickerOptions.ts +12 -10
- package/src/models/providerDisplay.ts +16 -0
- package/src/providers/errors.ts +6 -4
- package/src/providers/openai-chat.ts +2 -1
- package/src/providers/openai-responses-format.ts +156 -0
- package/src/providers/openai-responses.ts +276 -0
- package/src/providers/registry.ts +85 -8
- package/src/runtime/sessionMode.ts +1 -1
- package/src/runtime/systemPrompt.ts +4 -2
- package/src/runtime/toolExecution.ts +9 -6
- package/src/runtime/turn.ts +29 -1
- package/src/storage/rewind.ts +20 -0
- package/src/storage/secrets.ts +4 -1
- package/src/storage/sessions.ts +2 -1
- package/src/tools/bashSafety.ts +7 -3
- package/src/tools/bashTool.ts +1 -1
- package/src/tools/contracts.ts +3 -0
- package/src/tools/deleteFileTool.ts +8 -3
- package/src/tools/editTool.ts +10 -5
- package/src/tools/fileDiff.ts +261 -0
- package/src/tools/privateContinuityEditTool.ts +11 -1
- package/src/tools/writeFileTool.ts +8 -3
- package/src/ui/Spinner.tsx +25 -3
- package/src/ui/TextInput.tsx +2 -2
- package/src/ui/theme.ts +17 -0
- package/src/utils/clipboard.ts +10 -7
- package/src/utils/openExternal.ts +20 -10
- package/src/chat/RewindView.tsx +0 -386
- package/src/chat/toolResultDisplay.ts +0 -8
- package/src/identity/ens/ensRegistration.ts +0 -199
- package/src/identity/hub/effects/index.ts +0 -74
- package/src/identity/hub/effects/publicProfile/index.ts +0 -5
- package/src/identity/hub/effects/restore/restoreEffects.ts +0 -22
- package/src/identity/hub/effects/restoreAdmin.ts +0 -93
- package/src/identity/hub/effects/token-transfer/index.ts +0 -6
- package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +0 -336
- package/src/identity/hub/flows/ens/EnsEditRunners.tsx +0 -198
- package/src/identity/hub/reconciliation/walletSetup.ts +0 -220
- /package/src/chat/{chatInputState.ts → input/chatInputState.ts} +0 -0
- /package/src/chat/{chatPaste.ts → input/chatPaste.ts} +0 -0
- /package/src/chat/{textCursor.ts → input/textCursor.ts} +0 -0
- /package/src/identity/hub/{model/continuity.ts → continuity/state.ts} +0 -0
- /package/src/identity/hub/{model/custody.ts → custody/state.ts} +0 -0
- /package/src/identity/hub/{effects/restore → restore}/index.ts +0 -0
- /package/src/identity/hub/{model → shared/model}/format.ts +0 -0
- /package/src/identity/hub/{reconciliation → shared/reconciliation}/useAgentReconciliation.ts +0 -0
- /package/src/identity/hub/{txGuard.ts → shared/txGuard.ts} +0 -0
- /package/src/identity/hub/{model/transfer.ts → transfer/state.ts} +0 -0
package/src/chat/commands.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { setCwd } from '../runtime/cwd.js'
|
|
|
19
19
|
import type { SessionMode } from '../runtime/sessionMode.js'
|
|
20
20
|
import type { ContextUsage } from '../runtime/compaction.js'
|
|
21
21
|
import { formatModelDisplayName } from '../models/modelDisplay.js'
|
|
22
|
+
import { providerDisplayName } from '../models/modelPickerOptions.js'
|
|
22
23
|
import type { McpManager } from '../mcp/manager.js'
|
|
23
24
|
|
|
24
25
|
export type IdentityRequestAction =
|
|
@@ -131,9 +132,9 @@ const COMMANDS: CommandSpec[] = [
|
|
|
131
132
|
try {
|
|
132
133
|
const next = setCwd(target, ctx.cwd)
|
|
133
134
|
ctx.onChangeCwd(next)
|
|
134
|
-
return { kind: 'note', text: `
|
|
135
|
+
return { kind: 'note', text: `Cwd: ${next}`, variant: 'dim' }
|
|
135
136
|
} catch (err: unknown) {
|
|
136
|
-
return { kind: 'note', variant: 'error', text: `
|
|
137
|
+
return { kind: 'note', variant: 'error', text: `Cd failed: ${(err as Error).message}` }
|
|
137
138
|
}
|
|
138
139
|
},
|
|
139
140
|
},
|
|
@@ -199,7 +200,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
199
200
|
return {
|
|
200
201
|
kind: 'note',
|
|
201
202
|
variant: 'error',
|
|
202
|
-
text: `'${name}' was not found for ${ctx.config.provider}. use /models to inspect available models.`,
|
|
203
|
+
text: `'${name}' was not found for ${providerDisplayName(ctx.config.provider)}. use /models to inspect available models.`,
|
|
203
204
|
}
|
|
204
205
|
}
|
|
205
206
|
}
|
|
@@ -210,7 +211,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
210
211
|
}
|
|
211
212
|
await saveConfig(next)
|
|
212
213
|
ctx.onReplaceConfig(next)
|
|
213
|
-
return { kind: 'note', text: `Now using ${next.provider} · ${formatModelDisplayName(next.provider, name, { maxLength: 64 })}.` }
|
|
214
|
+
return { kind: 'note', text: `Now using ${providerDisplayName(next.provider)} · ${formatModelDisplayName(next.provider, name, { maxLength: 64 })}.` }
|
|
214
215
|
},
|
|
215
216
|
},
|
|
216
217
|
{
|
|
@@ -243,12 +244,12 @@ const COMMANDS: CommandSpec[] = [
|
|
|
243
244
|
}
|
|
244
245
|
const result = await rewindWorkspaceEdits(ctx.cwd, steps)
|
|
245
246
|
if (result.reverted === 0) {
|
|
246
|
-
return { kind: 'note', variant: 'error', text: '
|
|
247
|
+
return { kind: 'note', variant: 'error', text: 'No managed edits available to rewind in this directory.' }
|
|
247
248
|
}
|
|
248
249
|
const files = result.files.map(file => path.relative(ctx.cwd, file) || path.basename(file))
|
|
249
250
|
return {
|
|
250
251
|
kind: 'note',
|
|
251
|
-
text: `
|
|
252
|
+
text: `Rewound ${result.reverted} edit${result.reverted === 1 ? '' : 's'}.\n${files.join('\n')}`,
|
|
252
253
|
variant: 'dim',
|
|
253
254
|
}
|
|
254
255
|
},
|
|
@@ -275,7 +276,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
275
276
|
run: async (args, ctx) => {
|
|
276
277
|
const assistant = ctx.assistantTurns()
|
|
277
278
|
if (assistant.length === 0) {
|
|
278
|
-
return { kind: 'note', variant: 'error', text: '
|
|
279
|
+
return { kind: 'note', variant: 'error', text: 'Nothing to copy yet.' }
|
|
279
280
|
}
|
|
280
281
|
let offset = 1
|
|
281
282
|
const trimmed = args.trim()
|
|
@@ -288,17 +289,17 @@ const COMMANDS: CommandSpec[] = [
|
|
|
288
289
|
}
|
|
289
290
|
const index = assistant.length - offset
|
|
290
291
|
if (index < 0) {
|
|
291
|
-
return { kind: 'note', variant: 'error', text: `
|
|
292
|
+
return { kind: 'note', variant: 'error', text: `Only ${assistant.length} assistant reply on record.` }
|
|
292
293
|
}
|
|
293
294
|
const text = assistant[index] ?? ''
|
|
294
|
-
const label = offset === 1 ? '
|
|
295
|
+
const label = offset === 1 ? 'Latest reply' : `Reply #${offset} back`
|
|
295
296
|
const segments = parseSegments(text)
|
|
296
297
|
if (segments.length <= 1) {
|
|
297
298
|
const result = await copyToClipboard(text)
|
|
298
299
|
if (!result.ok) {
|
|
299
|
-
return { kind: 'note', variant: 'error', text: `
|
|
300
|
+
return { kind: 'note', variant: 'error', text: `Copy failed: ${result.error}` }
|
|
300
301
|
}
|
|
301
|
-
return { kind: 'note', text:
|
|
302
|
+
return { kind: 'note', text: `${label} copied to clipboard · ${result.chars} chars`, variant: 'dim' }
|
|
302
303
|
}
|
|
303
304
|
ctx.onCopyPickerRequest(text, label)
|
|
304
305
|
return { kind: 'handled' }
|
|
@@ -311,16 +312,16 @@ const COMMANDS: CommandSpec[] = [
|
|
|
311
312
|
run: async (_args, ctx) => {
|
|
312
313
|
const messages = ctx.sessionMessages()
|
|
313
314
|
if (messages.length === 0) {
|
|
314
|
-
return { kind: 'note', variant: 'error', text: '
|
|
315
|
+
return { kind: 'note', variant: 'error', text: 'Nothing to export yet.' }
|
|
315
316
|
}
|
|
316
317
|
try {
|
|
317
318
|
const file = await exportSessionMarkdown(ctx.sessionId, messages, {
|
|
318
319
|
model: ctx.config.model,
|
|
319
320
|
provider: ctx.config.provider,
|
|
320
321
|
})
|
|
321
|
-
return { kind: 'note', text: `
|
|
322
|
+
return { kind: 'note', text: `Exported to ${file}` }
|
|
322
323
|
} catch (err: unknown) {
|
|
323
|
-
return { kind: 'note', variant: 'error', text: `
|
|
324
|
+
return { kind: 'note', variant: 'error', text: `Export failed: ${(err as Error).message}` }
|
|
324
325
|
}
|
|
325
326
|
},
|
|
326
327
|
},
|
|
@@ -436,7 +437,7 @@ async function runMcp(args: string, ctx: SlashContext): Promise<SlashResult> {
|
|
|
436
437
|
return { kind: 'note', text: await ctx.mcp.addJson(name, json, project ? 'project' : 'user'), variant: 'dim' }
|
|
437
438
|
}
|
|
438
439
|
} catch (err: unknown) {
|
|
439
|
-
return { kind: 'note', variant: 'error', text: `
|
|
440
|
+
return { kind: 'note', variant: 'error', text: `MCP failed: ${(err as Error).message}` }
|
|
440
441
|
}
|
|
441
442
|
|
|
442
443
|
return {
|
|
@@ -462,7 +463,7 @@ async function runIdentity(args: string, ctx: SlashContext): Promise<SlashResult
|
|
|
462
463
|
return {
|
|
463
464
|
kind: 'note',
|
|
464
465
|
variant: 'dim',
|
|
465
|
-
text: '
|
|
466
|
+
text: 'No Ethereum identity set. Run /identity create to make one.',
|
|
466
467
|
}
|
|
467
468
|
}
|
|
468
469
|
const lines = [
|
|
@@ -490,16 +491,16 @@ async function runIdentity(args: string, ctx: SlashContext): Promise<SlashResult
|
|
|
490
491
|
return {
|
|
491
492
|
kind: 'note',
|
|
492
493
|
variant: 'error',
|
|
493
|
-
text: '
|
|
494
|
+
text: 'Remove deletes local identity metadata and any legacy stored key. Re-run with: /identity remove confirm',
|
|
494
495
|
}
|
|
495
496
|
}
|
|
496
497
|
const status = await getIdentityStatus(ctx.config)
|
|
497
498
|
if (!status) {
|
|
498
|
-
return { kind: 'note', variant: 'dim', text: '
|
|
499
|
+
return { kind: 'note', variant: 'dim', text: 'No Ethereum identity to remove.' }
|
|
499
500
|
}
|
|
500
501
|
const next = await clearIdentity(ctx.config)
|
|
501
502
|
ctx.onReplaceConfig(next)
|
|
502
|
-
return { kind: 'note', text: `
|
|
503
|
+
return { kind: 'note', text: `Removed identity ${status.address}.`, variant: 'dim' }
|
|
503
504
|
}
|
|
504
505
|
|
|
505
506
|
return {
|
|
@@ -536,7 +537,7 @@ function renderStatus(ctx: SlashContext): string {
|
|
|
536
537
|
const elapsed = minutes > 0 ? `${minutes}m${seconds.toString().padStart(2, '0')}s` : `${seconds}s`
|
|
537
538
|
const displayModel = formatModelDisplayName(ctx.config.provider, ctx.config.model, { maxLength: 72 })
|
|
538
539
|
return [
|
|
539
|
-
`provider ${ctx.config.provider}`,
|
|
540
|
+
`provider ${providerDisplayName(ctx.config.provider)}`,
|
|
540
541
|
`model ${displayModel}`,
|
|
541
542
|
`cwd ${ctx.cwd}`,
|
|
542
543
|
`session ${ctx.sessionId.slice(0, 8)}`,
|
|
@@ -559,7 +560,7 @@ function renderContext(ctx: SlashContext): string {
|
|
|
559
560
|
: 'Context has comfortable room.'
|
|
560
561
|
return [
|
|
561
562
|
'context usage:',
|
|
562
|
-
` model ${ctx.config.provider} · ${formatModelDisplayName(ctx.config.provider, ctx.config.model, { maxLength: 72 })}`,
|
|
563
|
+
` model ${providerDisplayName(ctx.config.provider)} · ${formatModelDisplayName(ctx.config.provider, ctx.config.model, { maxLength: 72 })}`,
|
|
563
564
|
` used ~${usage.usedTokens} / ${usage.windowTokens} tokens (${usage.percent}%)`,
|
|
564
565
|
` free ~${free} tokens`,
|
|
565
566
|
` estimate ${usage.confidence} (${usage.source})`,
|
|
@@ -584,7 +585,7 @@ function renderDoctor(
|
|
|
584
585
|
lines.push(` hf models ${hfModelCount} downloaded`)
|
|
585
586
|
lines.push('')
|
|
586
587
|
lines.push('config:')
|
|
587
|
-
lines.push(` provider ${ctx.config.provider}`)
|
|
588
|
+
lines.push(` provider ${providerDisplayName(ctx.config.provider)}`)
|
|
588
589
|
lines.push(` model ${formatModelDisplayName(ctx.config.provider, ctx.config.model, { maxLength: 72 })}`)
|
|
589
590
|
if (ctx.config.baseUrl) lines.push(` baseUrl ${ctx.config.baseUrl}`)
|
|
590
591
|
if (ctx.config.provider === 'llamacpp') lines.push(` hf cache ${getLocalHfCacheDir()}`)
|
|
@@ -592,7 +593,7 @@ function renderDoctor(
|
|
|
592
593
|
lines.push('')
|
|
593
594
|
lines.push('keys:')
|
|
594
595
|
for (const [provider, present] of keys) {
|
|
595
|
-
lines.push(` ${provider.padEnd(9)} ${present ? 'set' : 'not set'}`)
|
|
596
|
+
lines.push(` ${providerDisplayName(provider).padEnd(9)} ${present ? 'set' : 'not set'}`)
|
|
596
597
|
}
|
|
597
598
|
lines.push('')
|
|
598
599
|
lines.push('identity:')
|
|
@@ -609,8 +610,8 @@ function renderDoctor(
|
|
|
609
610
|
|
|
610
611
|
function renderModelCatalog(catalog: ModelCatalogResult, currentModel: string): string {
|
|
611
612
|
const title = catalog.status === 'fallback'
|
|
612
|
-
? `${catalog.provider} models (fallback${catalog.error ? `: ${catalog.error}` : ''}):`
|
|
613
|
-
: `${catalog.provider} models:`
|
|
613
|
+
? `${providerDisplayName(catalog.provider)} models (fallback${catalog.error ? `: ${catalog.error}` : ''}):`
|
|
614
|
+
: `${providerDisplayName(catalog.provider)} models:`
|
|
614
615
|
const lines = catalog.entries.map(entry => {
|
|
615
616
|
const marker = entry.id === currentModel ? '*' : ' '
|
|
616
617
|
const suffix = entry.source === 'fallback' ? ' fallback' : ''
|
|
@@ -648,9 +649,9 @@ export async function dispatchSlash(input: string, ctx: SlashContext): Promise<S
|
|
|
648
649
|
const promptText = await ctx.mcp?.runPromptSlash(parsed.name, parsed.args)
|
|
649
650
|
if (promptText !== null && promptText !== undefined) return { kind: 'submit', text: promptText }
|
|
650
651
|
} catch (err: unknown) {
|
|
651
|
-
return { kind: 'note', variant: 'error', text: `
|
|
652
|
+
return { kind: 'note', variant: 'error', text: `MCP prompt failed: ${(err as Error).message}` }
|
|
652
653
|
}
|
|
653
|
-
return { kind: 'note', variant: 'error', text: `
|
|
654
|
+
return { kind: 'note', variant: 'error', text: `Unknown command: /${parsed.name}. Try /help` }
|
|
654
655
|
}
|
|
655
656
|
if (ctx.mode === 'plan' && cmd.blockedInPlan) {
|
|
656
657
|
return { kind: 'note', variant: 'error', text: `/${cmd.name} is blocked in plan mode.` }
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
import { theme } from '../../ui/theme.js'
|
|
4
|
+
import { inferLanguageFromPath, SyntaxLine } from './SyntaxText.js'
|
|
5
|
+
|
|
6
|
+
const EMPTY_DIFF_LINE = ' '
|
|
7
|
+
|
|
8
|
+
type DiffLineTone = 'added' | 'removed' | 'added-header' | 'removed-header' | 'hunk' | 'context' | 'marker'
|
|
9
|
+
|
|
10
|
+
type DiffLineStyle = {
|
|
11
|
+
tone: DiffLineTone
|
|
12
|
+
color: string
|
|
13
|
+
backgroundColor?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type NumberedDiffLine = {
|
|
17
|
+
line: string
|
|
18
|
+
oldLine?: number
|
|
19
|
+
newLine?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function diffLineColor(line: string): string {
|
|
23
|
+
return diffLineStyle(line).color
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function diffLineStyle(line: string): DiffLineStyle {
|
|
27
|
+
if (line.startsWith('+++')) return { tone: 'added-header', color: theme.diffAdded }
|
|
28
|
+
if (line.startsWith('---')) return { tone: 'removed-header', color: theme.diffRemoved }
|
|
29
|
+
if (line.startsWith('+')) {
|
|
30
|
+
return { tone: 'added', color: theme.text, backgroundColor: theme.diffAddedBackground }
|
|
31
|
+
}
|
|
32
|
+
if (line.startsWith('-')) {
|
|
33
|
+
return { tone: 'removed', color: theme.text, backgroundColor: theme.diffRemovedBackground }
|
|
34
|
+
}
|
|
35
|
+
if (line.startsWith('@@')) return { tone: 'hunk', color: theme.accentPeriwinkle }
|
|
36
|
+
if (line.startsWith('...')) return { tone: 'marker', color: theme.dim }
|
|
37
|
+
return { tone: 'context', color: theme.textSubtle }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const DiffView: React.FC<{ diff: string }> = ({ diff }) => (
|
|
41
|
+
<DiffLines lines={visibleDiffLines(numberDiffLines(diff))} language={inferDiffLanguage(diff)} />
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const DiffLines: React.FC<{ lines: NumberedDiffLine[]; language: string | null }> = ({ lines, language }) => {
|
|
45
|
+
const width = lineNumberWidth(lines)
|
|
46
|
+
return (
|
|
47
|
+
<Box flexDirection="column">
|
|
48
|
+
{lines.map((line, index) => (
|
|
49
|
+
<DiffLine
|
|
50
|
+
key={index}
|
|
51
|
+
line={line.line}
|
|
52
|
+
oldLine={line.oldLine}
|
|
53
|
+
newLine={line.newLine}
|
|
54
|
+
width={width}
|
|
55
|
+
language={language}
|
|
56
|
+
/>
|
|
57
|
+
))}
|
|
58
|
+
</Box>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const DiffLine: React.FC<{
|
|
63
|
+
line: string
|
|
64
|
+
oldLine?: number
|
|
65
|
+
newLine?: number
|
|
66
|
+
width: number
|
|
67
|
+
language: string | null
|
|
68
|
+
}> = ({
|
|
69
|
+
line,
|
|
70
|
+
oldLine,
|
|
71
|
+
newLine,
|
|
72
|
+
width,
|
|
73
|
+
language,
|
|
74
|
+
}) => {
|
|
75
|
+
const style = diffLineStyle(line)
|
|
76
|
+
const number = renderLineNumber(displayLineNumber(oldLine, newLine, style.tone), width)
|
|
77
|
+
if (line.length === 0) {
|
|
78
|
+
return <Text color={style.color}>{EMPTY_DIFF_LINE}</Text>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (style.tone === 'added' || style.tone === 'removed') {
|
|
82
|
+
const prefixColor = style.tone === 'added' ? theme.diffAdded : theme.diffRemoved
|
|
83
|
+
return (
|
|
84
|
+
<Box width="100%" backgroundColor={style.backgroundColor}>
|
|
85
|
+
<Text>
|
|
86
|
+
{number}
|
|
87
|
+
<Text color={prefixColor}>{line.slice(0, 1)} </Text>
|
|
88
|
+
<SyntaxLine line={line.slice(1)} lang={language} fallbackColor={style.color} />
|
|
89
|
+
</Text>
|
|
90
|
+
</Box>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (style.tone === 'context' && line.startsWith(' ')) {
|
|
95
|
+
return (
|
|
96
|
+
<Text color={style.color}>
|
|
97
|
+
{number}
|
|
98
|
+
<Text>{' '}</Text>
|
|
99
|
+
<SyntaxLine line={line.slice(1)} lang={language} fallbackColor={style.color} />
|
|
100
|
+
</Text>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return <Text color={style.color}>{line}</Text>
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function lineNumberWidth(lines: NumberedDiffLine[]): number {
|
|
108
|
+
return Math.max(
|
|
109
|
+
2,
|
|
110
|
+
...lines.map(line => String(displayLineNumber(line.oldLine, line.newLine, diffLineStyle(line.line).tone) ?? '').length),
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function displayLineNumber(
|
|
115
|
+
oldLine: number | undefined,
|
|
116
|
+
newLine: number | undefined,
|
|
117
|
+
tone: DiffLineTone,
|
|
118
|
+
): number | undefined {
|
|
119
|
+
if (tone === 'removed') return oldLine
|
|
120
|
+
return newLine ?? oldLine
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function renderLineNumber(line: number | undefined, width: number): React.ReactNode {
|
|
124
|
+
if (line === undefined) return null
|
|
125
|
+
return <Text color={theme.dim}>{String(line).padStart(width, ' ')} </Text>
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function visibleDiffLines(lines: NumberedDiffLine[]): NumberedDiffLine[] {
|
|
129
|
+
return lines.filter(line => {
|
|
130
|
+
const tone = diffLineStyle(line.line).tone
|
|
131
|
+
return tone !== 'added-header' && tone !== 'removed-header' && tone !== 'hunk'
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function inferDiffLanguage(diff: string): string | null {
|
|
136
|
+
for (const line of diff.split('\n')) {
|
|
137
|
+
if (!line.startsWith('+++ ') || line === '+++ /dev/null') continue
|
|
138
|
+
return inferLanguageFromPath(line.slice(4).trim())
|
|
139
|
+
}
|
|
140
|
+
for (const line of diff.split('\n')) {
|
|
141
|
+
if (!line.startsWith('--- ') || line === '--- /dev/null') continue
|
|
142
|
+
return inferLanguageFromPath(line.slice(4).trim())
|
|
143
|
+
}
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function numberDiffLines(diff: string): NumberedDiffLine[] {
|
|
148
|
+
const out: NumberedDiffLine[] = []
|
|
149
|
+
let oldLine: number | undefined
|
|
150
|
+
let newLine: number | undefined
|
|
151
|
+
|
|
152
|
+
for (const line of diff.split('\n')) {
|
|
153
|
+
const hunk = parseHunkHeader(line)
|
|
154
|
+
if (hunk) {
|
|
155
|
+
oldLine = hunk.oldStart
|
|
156
|
+
newLine = hunk.newStart
|
|
157
|
+
out.push({ line })
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (oldLine !== undefined && newLine !== undefined) {
|
|
162
|
+
if (line.startsWith(' ')) {
|
|
163
|
+
out.push({ line, oldLine, newLine })
|
|
164
|
+
oldLine += 1
|
|
165
|
+
newLine += 1
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
if (line.startsWith('-') && !line.startsWith('---')) {
|
|
169
|
+
out.push({ line, oldLine })
|
|
170
|
+
oldLine += 1
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
174
|
+
out.push({ line, newLine })
|
|
175
|
+
newLine += 1
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
out.push({ line })
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return out
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function parseHunkHeader(line: string): { oldStart: number; newStart: number } | null {
|
|
187
|
+
const match = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/)
|
|
188
|
+
if (!match) return null
|
|
189
|
+
return {
|
|
190
|
+
oldStart: Number(match[1]),
|
|
191
|
+
newStart: Number(match[2]),
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Text } from 'ink'
|
|
3
|
+
import { styledCharsFromTokens, tokenize } from '@alcalzone/ansi-tokenize'
|
|
4
|
+
import { highlight, supportsLanguage, type Theme } from 'cli-highlight'
|
|
5
|
+
import { theme } from '../../ui/theme.js'
|
|
6
|
+
|
|
7
|
+
type Span = {
|
|
8
|
+
text: string
|
|
9
|
+
color?: string
|
|
10
|
+
bold?: boolean
|
|
11
|
+
italic?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ANSI_RESET_FG = '\x1B[39m'
|
|
15
|
+
const ANSI_RESET_BOLD = '\x1B[22m'
|
|
16
|
+
const ANSI_RESET_ITALIC = '\x1B[23m'
|
|
17
|
+
|
|
18
|
+
const HIGHLIGHT_THEME: Theme = {
|
|
19
|
+
keyword: style(theme.codeKeyword, { bold: true }),
|
|
20
|
+
built_in: style(theme.codeBuiltin),
|
|
21
|
+
type: style(theme.codeType),
|
|
22
|
+
literal: style(theme.codeKeyword),
|
|
23
|
+
number: style(theme.codeNumber),
|
|
24
|
+
regexp: style(theme.codeString),
|
|
25
|
+
string: style(theme.codeString),
|
|
26
|
+
class: style(theme.codeType),
|
|
27
|
+
function: style(theme.codeFunction),
|
|
28
|
+
title: style(theme.codeFunction),
|
|
29
|
+
comment: style(theme.codeComment, { italic: true }),
|
|
30
|
+
doctag: style(theme.codeComment, { italic: true }),
|
|
31
|
+
meta: style(theme.dim),
|
|
32
|
+
tag: style(theme.codeTag, { bold: true }),
|
|
33
|
+
name: style(theme.codeTag, { bold: true }),
|
|
34
|
+
attr: style(theme.codeAttribute),
|
|
35
|
+
attribute: style(theme.codeProperty),
|
|
36
|
+
variable: style(theme.codeProperty),
|
|
37
|
+
addition: style(theme.diffAdded),
|
|
38
|
+
deletion: style(theme.diffRemoved),
|
|
39
|
+
default: style(theme.textSubtle),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const PROGRAMMING_LANGUAGES = new Set([
|
|
43
|
+
'bash',
|
|
44
|
+
'css',
|
|
45
|
+
'go',
|
|
46
|
+
'java',
|
|
47
|
+
'javascript',
|
|
48
|
+
'json',
|
|
49
|
+
'python',
|
|
50
|
+
'rust',
|
|
51
|
+
'solidity',
|
|
52
|
+
'sql',
|
|
53
|
+
'typescript',
|
|
54
|
+
'xml',
|
|
55
|
+
'yaml',
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
const LANGUAGE_BY_EXTENSION: Record<string, string> = {
|
|
59
|
+
cjs: 'javascript',
|
|
60
|
+
css: 'css',
|
|
61
|
+
html: 'xml',
|
|
62
|
+
js: 'javascript',
|
|
63
|
+
json: 'json',
|
|
64
|
+
jsonc: 'json',
|
|
65
|
+
jsx: 'javascript',
|
|
66
|
+
md: 'markdown',
|
|
67
|
+
mjs: 'javascript',
|
|
68
|
+
py: 'python',
|
|
69
|
+
sh: 'bash',
|
|
70
|
+
ts: 'typescript',
|
|
71
|
+
tsx: 'typescript',
|
|
72
|
+
yml: 'yaml',
|
|
73
|
+
yaml: 'yaml',
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const SyntaxLine: React.FC<{ line: string; lang?: string | null; fallbackColor?: string }> = ({
|
|
77
|
+
line,
|
|
78
|
+
lang,
|
|
79
|
+
fallbackColor = theme.textSubtle,
|
|
80
|
+
}) => (
|
|
81
|
+
<>
|
|
82
|
+
{syntaxLineSpans(line, lang, fallbackColor).map((span, index) => (
|
|
83
|
+
<Text key={index} color={span.color} bold={span.bold} italic={span.italic}>
|
|
84
|
+
{span.text}
|
|
85
|
+
</Text>
|
|
86
|
+
))}
|
|
87
|
+
</>
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
export function syntaxLineSpans(line: string, lang?: string | null, fallbackColor: string = theme.textSubtle): Span[] {
|
|
91
|
+
const code = line.length === 0 ? ' ' : line
|
|
92
|
+
const language = supportedLanguage(lang)
|
|
93
|
+
if (!language || !PROGRAMMING_LANGUAGES.has(language)) {
|
|
94
|
+
return [{ text: code, color: fallbackColor }]
|
|
95
|
+
}
|
|
96
|
+
const highlighted = highlight(code, {
|
|
97
|
+
language: language ?? undefined,
|
|
98
|
+
ignoreIllegals: true,
|
|
99
|
+
theme: HIGHLIGHT_THEME,
|
|
100
|
+
})
|
|
101
|
+
const spans = spansFromAnsi(highlighted)
|
|
102
|
+
return spans.length > 0 ? spans : [{ text: code, color: fallbackColor }]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function inferLanguageFromPath(path: string): string | null {
|
|
106
|
+
const clean = path
|
|
107
|
+
.replace(/^["']|["']$/g, '')
|
|
108
|
+
.replace(/^[ab]\//, '')
|
|
109
|
+
.replace(/\\/g, '/')
|
|
110
|
+
.split('/')
|
|
111
|
+
.pop()
|
|
112
|
+
const extension = clean?.match(/\.([A-Za-z0-9]+)$/)?.[1]?.toLowerCase()
|
|
113
|
+
return extension ? LANGUAGE_BY_EXTENSION[extension] ?? null : null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function supportedLanguage(lang?: string | null): string | null {
|
|
117
|
+
if (!lang) return null
|
|
118
|
+
const normalized = LANGUAGE_BY_EXTENSION[lang.toLowerCase()] ?? lang.toLowerCase()
|
|
119
|
+
return supportsLanguage(normalized) ? normalized : null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function style(color: string, options: { bold?: boolean; italic?: boolean } = {}): (text: string) => string {
|
|
123
|
+
return text => [
|
|
124
|
+
colorCode(color),
|
|
125
|
+
options.bold ? '\x1B[1m' : '',
|
|
126
|
+
options.italic ? '\x1B[3m' : '',
|
|
127
|
+
text,
|
|
128
|
+
options.italic ? ANSI_RESET_ITALIC : '',
|
|
129
|
+
options.bold ? ANSI_RESET_BOLD : '',
|
|
130
|
+
ANSI_RESET_FG,
|
|
131
|
+
].join('')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function colorCode(hex: string): string {
|
|
135
|
+
const [r, g, b] = parseHexColor(hex)
|
|
136
|
+
return `\x1B[38;2;${r};${g};${b}m`
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parseHexColor(hex: string): [number, number, number] {
|
|
140
|
+
const clean = hex.replace(/^#/, '')
|
|
141
|
+
return [
|
|
142
|
+
Number.parseInt(clean.slice(0, 2), 16),
|
|
143
|
+
Number.parseInt(clean.slice(2, 4), 16),
|
|
144
|
+
Number.parseInt(clean.slice(4, 6), 16),
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function spansFromAnsi(value: string): Span[] {
|
|
149
|
+
const chars = styledCharsFromTokens(tokenize(value))
|
|
150
|
+
const spans: Span[] = []
|
|
151
|
+
for (const char of chars) {
|
|
152
|
+
const span = spanFromCodes(char.value, char.styles.map(styleCode => styleCode.code))
|
|
153
|
+
const previous = spans[spans.length - 1]
|
|
154
|
+
if (previous && sameStyle(previous, span)) {
|
|
155
|
+
previous.text += span.text
|
|
156
|
+
} else {
|
|
157
|
+
spans.push(span)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return spans
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function spanFromCodes(text: string, codes: string[]): Span {
|
|
164
|
+
return {
|
|
165
|
+
text,
|
|
166
|
+
color: colorFromCodes(codes),
|
|
167
|
+
bold: codes.includes('\x1B[1m') || undefined,
|
|
168
|
+
italic: codes.includes('\x1B[3m') || undefined,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function colorFromCodes(codes: string[]): string | undefined {
|
|
173
|
+
for (let index = codes.length - 1; index >= 0; index -= 1) {
|
|
174
|
+
const match = codes[index]?.match(/\x1B\[38;2;(\d+);(\d+);(\d+)m/)
|
|
175
|
+
if (match) {
|
|
176
|
+
return rgbToHex(Number(match[1]), Number(match[2]), Number(match[3]))
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return undefined
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function rgbToHex(r: number, g: number, b: number): string {
|
|
183
|
+
return `#${hexByte(r)}${hexByte(g)}${hexByte(b)}`
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function hexByte(value: number): string {
|
|
187
|
+
return Math.max(0, Math.min(255, value)).toString(16).padStart(2, '0')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function sameStyle(a: Span, b: Span): boolean {
|
|
191
|
+
return a.color === b.color && a.bold === b.bold && a.italic === b.italic
|
|
192
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const MAX_ARG_SUMMARY = 80
|
|
2
|
+
|
|
3
|
+
export function formatToolCall(
|
|
4
|
+
name: string,
|
|
5
|
+
input: Record<string, unknown> | undefined,
|
|
6
|
+
): { displayName: string; argSummary: string } {
|
|
7
|
+
const safeInput = isRecord(input) ? input : {}
|
|
8
|
+
switch (name) {
|
|
9
|
+
case 'read_file':
|
|
10
|
+
return { displayName: 'Read', argSummary: readArgs(safeInput) }
|
|
11
|
+
case 'list_directory':
|
|
12
|
+
return { displayName: 'List', argSummary: pathArg(safeInput, '.') }
|
|
13
|
+
case 'run_bash':
|
|
14
|
+
return { displayName: 'Bash', argSummary: bashArgs(safeInput) }
|
|
15
|
+
case 'edit_file':
|
|
16
|
+
return { displayName: 'Edit', argSummary: pathArg(safeInput) }
|
|
17
|
+
case 'write_file':
|
|
18
|
+
return { displayName: 'Write', argSummary: pathArg(safeInput) }
|
|
19
|
+
case 'delete_file':
|
|
20
|
+
return { displayName: 'Delete', argSummary: pathArg(safeInput) }
|
|
21
|
+
case 'change_directory':
|
|
22
|
+
return { displayName: 'Cd', argSummary: pathArg(safeInput) }
|
|
23
|
+
case 'read_private_continuity_file':
|
|
24
|
+
return { displayName: 'ReadPrivate', argSummary: privateArgs(safeInput) }
|
|
25
|
+
case 'propose_private_continuity_edit':
|
|
26
|
+
return { displayName: 'EditPrivate', argSummary: stringArg(safeInput, 'file') }
|
|
27
|
+
case 'list_mcp_resources':
|
|
28
|
+
return { displayName: 'ListMcp', argSummary: stringArg(safeInput, 'server') }
|
|
29
|
+
case 'read_mcp_resource':
|
|
30
|
+
return { displayName: 'ReadMcp', argSummary: mcpReadArgs(safeInput) }
|
|
31
|
+
default:
|
|
32
|
+
return { displayName: humanize(name), argSummary: '' }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readArgs(input: Record<string, unknown>): string {
|
|
37
|
+
const filePath = pathArg(input)
|
|
38
|
+
const start = numberArg(input, 'startLine')
|
|
39
|
+
const end = numberArg(input, 'endLine')
|
|
40
|
+
if (start || end) {
|
|
41
|
+
const range = `lines ${start ?? 1}-${end ?? 'end'}`
|
|
42
|
+
return truncate(filePath ? `${filePath} · ${range}` : range)
|
|
43
|
+
}
|
|
44
|
+
return truncate(filePath)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function privateArgs(input: Record<string, unknown>): string {
|
|
48
|
+
const file = stringArg(input, 'file')
|
|
49
|
+
const start = numberArg(input, 'startLine')
|
|
50
|
+
const end = numberArg(input, 'endLine')
|
|
51
|
+
if (start || end) {
|
|
52
|
+
return truncate(`${file} · lines ${start ?? 1}-${end ?? 'end'}`)
|
|
53
|
+
}
|
|
54
|
+
return truncate(file)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function bashArgs(input: Record<string, unknown>): string {
|
|
58
|
+
const command = stringArg(input, 'command')
|
|
59
|
+
if (!command) return ''
|
|
60
|
+
const firstLine = command.split('\n')[0]?.trim() ?? ''
|
|
61
|
+
return truncate(firstLine)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function mcpReadArgs(input: Record<string, unknown>): string {
|
|
65
|
+
const server = stringArg(input, 'server')
|
|
66
|
+
const uri = stringArg(input, 'uri')
|
|
67
|
+
if (server && uri) return truncate(`${server} / ${uri}`)
|
|
68
|
+
return truncate(server || uri)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function pathArg(input: Record<string, unknown>, fallback = ''): string {
|
|
72
|
+
const raw = stringArg(input, 'path')
|
|
73
|
+
if (!raw) return fallback
|
|
74
|
+
return raw.replace(/\\/g, '/')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function stringArg(input: Record<string, unknown>, key: string): string {
|
|
78
|
+
const value = input[key]
|
|
79
|
+
return typeof value === 'string' ? value : ''
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function numberArg(input: Record<string, unknown>, key: string): number | undefined {
|
|
83
|
+
const value = input[key]
|
|
84
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function truncate(text: string): string {
|
|
88
|
+
const flat = text.replace(/\s+/g, ' ').trim()
|
|
89
|
+
if (flat.length <= MAX_ARG_SUMMARY) return flat
|
|
90
|
+
return `${flat.slice(0, MAX_ARG_SUMMARY - 1)}…`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function humanize(name: string): string {
|
|
94
|
+
return name
|
|
95
|
+
.split('_')
|
|
96
|
+
.filter(part => part.length > 0)
|
|
97
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
98
|
+
.join('')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
102
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
103
|
+
}
|