ethagent 2.2.0 → 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/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 +1 -1
- package/src/chat/chatTurnOrchestrator.ts +1 -7
- package/src/chat/commands.ts +26 -26
- 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/hub/OperationalRoutes.tsx +21 -21
- 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} +17 -17
- 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 +9 -9
- package/src/identity/hub/{flows/custody/custodyFlowActions.ts → custody/actions.ts} +6 -6
- 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} +5 -5
- 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/{flows/ens → ens}/EnsEditAdvancedScreens.tsx +13 -13
- package/src/identity/hub/{flows/ens → ens}/EnsEditFlow.tsx +7 -7
- package/src/identity/hub/{flows/ens → ens}/EnsEditMaintenanceScreens.tsx +10 -10
- package/src/identity/hub/{flows/ens → ens}/EnsEditReviewScreens.tsx +12 -12
- package/src/identity/hub/{flows/ens → ens}/EnsEditRunners.tsx +5 -5
- package/src/identity/hub/{flows/ens → ens}/EnsEditShared.tsx +10 -10
- package/src/identity/hub/{flows/ens → ens}/EnsEditSimpleScreens.tsx +14 -14
- package/src/identity/hub/{flows/ens/IdentityHubEnsFlow.tsx → ens/EnsFlow.tsx} +12 -12
- package/src/identity/hub/{flows/ens/OperatorWalletsScreen.tsx → ens/EnsOperatorWalletsScreen.tsx} +17 -17
- package/src/identity/hub/{advancedEnsValidation.ts → ens/advancedEnsValidation.ts} +2 -2
- package/src/identity/hub/{flows/ens/ensEditCopy.ts → ens/editCopy.ts} +3 -3
- 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 +239 -239
- package/src/identity/hub/{flows/ens/ensEditTypes.ts → ens/types.ts} +7 -7
- package/src/identity/hub/identityHubReducer.ts +3 -3
- package/src/identity/hub/{flows/profile → profile}/EditProfileFlow.tsx +11 -11
- package/src/identity/hub/{effects/publicProfile/runPublicProfileSave.ts → profile/effects.ts} +18 -18
- package/src/identity/hub/{model → profile}/identity.ts +3 -3
- package/src/identity/hub/{effects/profile/profileState.ts → profile/state.ts} +181 -181
- package/src/identity/hub/{flows/restore → restore}/RestoreFlow.tsx +16 -16
- 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/{effects → restore}/restoreAdmin.ts +1 -1
- 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 +8 -8
- package/src/identity/hub/{components → shared/components}/MenuScreen.tsx +7 -7
- 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 +1 -1
- 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 +4 -4
- 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 -1
- package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/ownership.ts +2 -2
- package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/run.ts +6 -6
- 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/models/ModelPicker.tsx +5 -3
- package/src/models/catalog.ts +2 -1
- package/src/models/modelPickerOptions.ts +2 -14
- package/src/models/providerDisplay.ts +16 -0
- package/src/providers/errors.ts +6 -4
- package/src/providers/openai-chat.ts +2 -1
- package/src/runtime/sessionMode.ts +1 -1
- package/src/runtime/systemPrompt.ts +3 -1
- package/src/runtime/toolExecution.ts +9 -6
- package/src/runtime/turn.ts +29 -0
- package/src/storage/rewind.ts +20 -0
- 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 +5 -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/chat/RewindView.tsx +0 -386
- package/src/chat/toolResultDisplay.ts +0 -8
- package/src/identity/hub/effects/index.ts +0 -73
- 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/token-transfer/index.ts +0 -6
- /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}/agentReconciliation/types.ts +0 -0
- /package/src/identity/hub/{reconciliation → shared/reconciliation}/index.ts +0 -0
- /package/src/identity/hub/{reconciliation → shared/reconciliation}/useAgentReconciliation.ts +0 -0
- /package/src/identity/hub/{reconciliation → shared/reconciliation}/walletSetup.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
|
@@ -132,9 +132,9 @@ const COMMANDS: CommandSpec[] = [
|
|
|
132
132
|
try {
|
|
133
133
|
const next = setCwd(target, ctx.cwd)
|
|
134
134
|
ctx.onChangeCwd(next)
|
|
135
|
-
return { kind: 'note', text: `
|
|
135
|
+
return { kind: 'note', text: `Cwd: ${next}`, variant: 'dim' }
|
|
136
136
|
} catch (err: unknown) {
|
|
137
|
-
return { kind: 'note', variant: 'error', text: `
|
|
137
|
+
return { kind: 'note', variant: 'error', text: `Cd failed: ${(err as Error).message}` }
|
|
138
138
|
}
|
|
139
139
|
},
|
|
140
140
|
},
|
|
@@ -200,7 +200,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
200
200
|
return {
|
|
201
201
|
kind: 'note',
|
|
202
202
|
variant: 'error',
|
|
203
|
-
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.`,
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
}
|
|
@@ -244,12 +244,12 @@ const COMMANDS: CommandSpec[] = [
|
|
|
244
244
|
}
|
|
245
245
|
const result = await rewindWorkspaceEdits(ctx.cwd, steps)
|
|
246
246
|
if (result.reverted === 0) {
|
|
247
|
-
return { kind: 'note', variant: 'error', text: '
|
|
247
|
+
return { kind: 'note', variant: 'error', text: 'No managed edits available to rewind in this directory.' }
|
|
248
248
|
}
|
|
249
249
|
const files = result.files.map(file => path.relative(ctx.cwd, file) || path.basename(file))
|
|
250
250
|
return {
|
|
251
251
|
kind: 'note',
|
|
252
|
-
text: `
|
|
252
|
+
text: `Rewound ${result.reverted} edit${result.reverted === 1 ? '' : 's'}.\n${files.join('\n')}`,
|
|
253
253
|
variant: 'dim',
|
|
254
254
|
}
|
|
255
255
|
},
|
|
@@ -276,7 +276,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
276
276
|
run: async (args, ctx) => {
|
|
277
277
|
const assistant = ctx.assistantTurns()
|
|
278
278
|
if (assistant.length === 0) {
|
|
279
|
-
return { kind: 'note', variant: 'error', text: '
|
|
279
|
+
return { kind: 'note', variant: 'error', text: 'Nothing to copy yet.' }
|
|
280
280
|
}
|
|
281
281
|
let offset = 1
|
|
282
282
|
const trimmed = args.trim()
|
|
@@ -289,17 +289,17 @@ const COMMANDS: CommandSpec[] = [
|
|
|
289
289
|
}
|
|
290
290
|
const index = assistant.length - offset
|
|
291
291
|
if (index < 0) {
|
|
292
|
-
return { kind: 'note', variant: 'error', text: `
|
|
292
|
+
return { kind: 'note', variant: 'error', text: `Only ${assistant.length} assistant reply on record.` }
|
|
293
293
|
}
|
|
294
294
|
const text = assistant[index] ?? ''
|
|
295
|
-
const label = offset === 1 ? '
|
|
295
|
+
const label = offset === 1 ? 'Latest reply' : `Reply #${offset} back`
|
|
296
296
|
const segments = parseSegments(text)
|
|
297
297
|
if (segments.length <= 1) {
|
|
298
298
|
const result = await copyToClipboard(text)
|
|
299
299
|
if (!result.ok) {
|
|
300
|
-
return { kind: 'note', variant: 'error', text: `
|
|
300
|
+
return { kind: 'note', variant: 'error', text: `Copy failed: ${result.error}` }
|
|
301
301
|
}
|
|
302
|
-
return { kind: 'note', text:
|
|
302
|
+
return { kind: 'note', text: `${label} copied to clipboard · ${result.chars} chars`, variant: 'dim' }
|
|
303
303
|
}
|
|
304
304
|
ctx.onCopyPickerRequest(text, label)
|
|
305
305
|
return { kind: 'handled' }
|
|
@@ -312,16 +312,16 @@ const COMMANDS: CommandSpec[] = [
|
|
|
312
312
|
run: async (_args, ctx) => {
|
|
313
313
|
const messages = ctx.sessionMessages()
|
|
314
314
|
if (messages.length === 0) {
|
|
315
|
-
return { kind: 'note', variant: 'error', text: '
|
|
315
|
+
return { kind: 'note', variant: 'error', text: 'Nothing to export yet.' }
|
|
316
316
|
}
|
|
317
317
|
try {
|
|
318
318
|
const file = await exportSessionMarkdown(ctx.sessionId, messages, {
|
|
319
319
|
model: ctx.config.model,
|
|
320
320
|
provider: ctx.config.provider,
|
|
321
321
|
})
|
|
322
|
-
return { kind: 'note', text: `
|
|
322
|
+
return { kind: 'note', text: `Exported to ${file}` }
|
|
323
323
|
} catch (err: unknown) {
|
|
324
|
-
return { kind: 'note', variant: 'error', text: `
|
|
324
|
+
return { kind: 'note', variant: 'error', text: `Export failed: ${(err as Error).message}` }
|
|
325
325
|
}
|
|
326
326
|
},
|
|
327
327
|
},
|
|
@@ -437,7 +437,7 @@ async function runMcp(args: string, ctx: SlashContext): Promise<SlashResult> {
|
|
|
437
437
|
return { kind: 'note', text: await ctx.mcp.addJson(name, json, project ? 'project' : 'user'), variant: 'dim' }
|
|
438
438
|
}
|
|
439
439
|
} catch (err: unknown) {
|
|
440
|
-
return { kind: 'note', variant: 'error', text: `
|
|
440
|
+
return { kind: 'note', variant: 'error', text: `MCP failed: ${(err as Error).message}` }
|
|
441
441
|
}
|
|
442
442
|
|
|
443
443
|
return {
|
|
@@ -463,7 +463,7 @@ async function runIdentity(args: string, ctx: SlashContext): Promise<SlashResult
|
|
|
463
463
|
return {
|
|
464
464
|
kind: 'note',
|
|
465
465
|
variant: 'dim',
|
|
466
|
-
text: '
|
|
466
|
+
text: 'No Ethereum identity set. Run /identity create to make one.',
|
|
467
467
|
}
|
|
468
468
|
}
|
|
469
469
|
const lines = [
|
|
@@ -491,16 +491,16 @@ async function runIdentity(args: string, ctx: SlashContext): Promise<SlashResult
|
|
|
491
491
|
return {
|
|
492
492
|
kind: 'note',
|
|
493
493
|
variant: 'error',
|
|
494
|
-
text: '
|
|
494
|
+
text: 'Remove deletes local identity metadata and any legacy stored key. Re-run with: /identity remove confirm',
|
|
495
495
|
}
|
|
496
496
|
}
|
|
497
497
|
const status = await getIdentityStatus(ctx.config)
|
|
498
498
|
if (!status) {
|
|
499
|
-
return { kind: 'note', variant: 'dim', text: '
|
|
499
|
+
return { kind: 'note', variant: 'dim', text: 'No Ethereum identity to remove.' }
|
|
500
500
|
}
|
|
501
501
|
const next = await clearIdentity(ctx.config)
|
|
502
502
|
ctx.onReplaceConfig(next)
|
|
503
|
-
return { kind: 'note', text: `
|
|
503
|
+
return { kind: 'note', text: `Removed identity ${status.address}.`, variant: 'dim' }
|
|
504
504
|
}
|
|
505
505
|
|
|
506
506
|
return {
|
|
@@ -537,7 +537,7 @@ function renderStatus(ctx: SlashContext): string {
|
|
|
537
537
|
const elapsed = minutes > 0 ? `${minutes}m${seconds.toString().padStart(2, '0')}s` : `${seconds}s`
|
|
538
538
|
const displayModel = formatModelDisplayName(ctx.config.provider, ctx.config.model, { maxLength: 72 })
|
|
539
539
|
return [
|
|
540
|
-
`provider ${ctx.config.provider}`,
|
|
540
|
+
`provider ${providerDisplayName(ctx.config.provider)}`,
|
|
541
541
|
`model ${displayModel}`,
|
|
542
542
|
`cwd ${ctx.cwd}`,
|
|
543
543
|
`session ${ctx.sessionId.slice(0, 8)}`,
|
|
@@ -560,7 +560,7 @@ function renderContext(ctx: SlashContext): string {
|
|
|
560
560
|
: 'Context has comfortable room.'
|
|
561
561
|
return [
|
|
562
562
|
'context usage:',
|
|
563
|
-
` 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 })}`,
|
|
564
564
|
` used ~${usage.usedTokens} / ${usage.windowTokens} tokens (${usage.percent}%)`,
|
|
565
565
|
` free ~${free} tokens`,
|
|
566
566
|
` estimate ${usage.confidence} (${usage.source})`,
|
|
@@ -585,7 +585,7 @@ function renderDoctor(
|
|
|
585
585
|
lines.push(` hf models ${hfModelCount} downloaded`)
|
|
586
586
|
lines.push('')
|
|
587
587
|
lines.push('config:')
|
|
588
|
-
lines.push(` provider ${ctx.config.provider}`)
|
|
588
|
+
lines.push(` provider ${providerDisplayName(ctx.config.provider)}`)
|
|
589
589
|
lines.push(` model ${formatModelDisplayName(ctx.config.provider, ctx.config.model, { maxLength: 72 })}`)
|
|
590
590
|
if (ctx.config.baseUrl) lines.push(` baseUrl ${ctx.config.baseUrl}`)
|
|
591
591
|
if (ctx.config.provider === 'llamacpp') lines.push(` hf cache ${getLocalHfCacheDir()}`)
|
|
@@ -593,7 +593,7 @@ function renderDoctor(
|
|
|
593
593
|
lines.push('')
|
|
594
594
|
lines.push('keys:')
|
|
595
595
|
for (const [provider, present] of keys) {
|
|
596
|
-
lines.push(` ${provider.padEnd(9)} ${present ? 'set' : 'not set'}`)
|
|
596
|
+
lines.push(` ${providerDisplayName(provider).padEnd(9)} ${present ? 'set' : 'not set'}`)
|
|
597
597
|
}
|
|
598
598
|
lines.push('')
|
|
599
599
|
lines.push('identity:')
|
|
@@ -610,8 +610,8 @@ function renderDoctor(
|
|
|
610
610
|
|
|
611
611
|
function renderModelCatalog(catalog: ModelCatalogResult, currentModel: string): string {
|
|
612
612
|
const title = catalog.status === 'fallback'
|
|
613
|
-
? `${catalog.provider} models (fallback${catalog.error ? `: ${catalog.error}` : ''}):`
|
|
614
|
-
: `${catalog.provider} models:`
|
|
613
|
+
? `${providerDisplayName(catalog.provider)} models (fallback${catalog.error ? `: ${catalog.error}` : ''}):`
|
|
614
|
+
: `${providerDisplayName(catalog.provider)} models:`
|
|
615
615
|
const lines = catalog.entries.map(entry => {
|
|
616
616
|
const marker = entry.id === currentModel ? '*' : ' '
|
|
617
617
|
const suffix = entry.source === 'fallback' ? ' fallback' : ''
|
|
@@ -649,9 +649,9 @@ export async function dispatchSlash(input: string, ctx: SlashContext): Promise<S
|
|
|
649
649
|
const promptText = await ctx.mcp?.runPromptSlash(parsed.name, parsed.args)
|
|
650
650
|
if (promptText !== null && promptText !== undefined) return { kind: 'submit', text: promptText }
|
|
651
651
|
} catch (err: unknown) {
|
|
652
|
-
return { kind: 'note', variant: 'error', text: `
|
|
652
|
+
return { kind: 'note', variant: 'error', text: `MCP prompt failed: ${(err as Error).message}` }
|
|
653
653
|
}
|
|
654
|
-
return { kind: 'note', variant: 'error', text: `
|
|
654
|
+
return { kind: 'note', variant: 'error', text: `Unknown command: /${parsed.name}. Try /help` }
|
|
655
655
|
}
|
|
656
656
|
if (ctx.mode === 'plan' && cmd.blockedInPlan) {
|
|
657
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
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { splitFileChangeResult } from '../../tools/fileDiff.js'
|
|
2
|
+
|
|
3
|
+
const COMPACT_SUCCESS_TOOL_RESULTS = new Set([
|
|
4
|
+
'read_file',
|
|
5
|
+
'read_private_continuity_file',
|
|
6
|
+
])
|
|
7
|
+
|
|
8
|
+
export function hidesSuccessfulToolResultContent(name: string, isError?: boolean): boolean {
|
|
9
|
+
return !isError && COMPACT_SUCCESS_TOOL_RESULTS.has(name)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function toolResultDiffContent(content: string, isError?: boolean): string | undefined {
|
|
13
|
+
if (isError) return undefined
|
|
14
|
+
return splitFileChangeResult(content).diff
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function toolResultTextContent(content: string): string {
|
|
18
|
+
return splitFileChangeResult(content).content
|
|
19
|
+
}
|