ethagent 2.4.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/package.json +2 -1
- package/src/app/FirstRun.tsx +155 -15
- package/src/app/FirstRunTimeline.tsx +4 -0
- package/src/app/input/AppInputProvider.tsx +19 -0
- package/src/app/input/appInputParser.ts +19 -4
- package/src/chat/ChatBottomPane.tsx +3 -1
- package/src/chat/ChatScreen.tsx +7 -1
- package/src/chat/ConversationStack.tsx +25 -19
- package/src/chat/MessageList.tsx +194 -53
- package/src/chat/chatSessionState.ts +1 -1
- package/src/chat/chatTurnOrchestrator.ts +59 -0
- package/src/chat/input/ChatInput.tsx +3 -0
- package/src/chat/input/textCursor.ts +13 -3
- package/src/chat/transcript/TranscriptView.tsx +7 -5
- package/src/chat/transcript/transcriptViewport.ts +88 -17
- package/src/chat/views/PermissionPrompt.tsx +26 -26
- package/src/chat/views/PermissionsView.tsx +18 -12
- package/src/chat/views/RewindView.tsx +3 -1
- package/src/cli/ResetConfirmView.tsx +24 -9
- package/src/identity/continuity/editor.ts +27 -2
- package/src/identity/continuity/envelope.ts +125 -0
- package/src/identity/continuity/publicSkills.ts +37 -1
- package/src/identity/continuity/skills/frontmatter.ts +183 -0
- package/src/identity/continuity/skills/loadSkills.ts +609 -0
- package/src/identity/continuity/skills/publicSkillsSync.ts +32 -0
- package/src/identity/continuity/skills/scaffold.ts +52 -0
- package/src/identity/continuity/skills/types.ts +30 -0
- package/src/identity/continuity/storage/defaults.ts +28 -47
- package/src/identity/continuity/storage/files.ts +1 -0
- package/src/identity/continuity/storage/paths.ts +1 -0
- package/src/identity/continuity/storage/scaffold.ts +25 -23
- package/src/identity/continuity/storage/status.ts +34 -5
- package/src/identity/continuity/storage/types.ts +3 -2
- package/src/identity/continuity/storage.ts +3 -0
- package/src/identity/hub/OperationalRoutes.tsx +105 -3
- package/src/identity/hub/Routes.tsx +5 -3
- package/src/identity/hub/continuity/ContinuityDashboardScreen.tsx +5 -51
- package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +1 -1
- package/src/identity/hub/continuity/SavePromptScreen.tsx +1 -0
- package/src/identity/hub/continuity/effects.ts +36 -5
- package/src/identity/hub/continuity/skills/DeleteSkillConfirmScreen.tsx +112 -0
- package/src/identity/hub/continuity/skills/DeleteSkillScreen.tsx +123 -0
- package/src/identity/hub/continuity/skills/NewSkillScreen.tsx +57 -0
- package/src/identity/hub/continuity/skills/NewSkillVisibilityScreen.tsx +52 -0
- package/src/identity/hub/continuity/skills/SkillVisibilityScreen.tsx +171 -0
- package/src/identity/hub/continuity/skills/SkillsTreeScreen.tsx +213 -0
- package/src/identity/hub/continuity/snapshot.ts +3 -0
- package/src/identity/hub/continuity/state.ts +3 -2
- package/src/identity/hub/continuity/vault.ts +42 -10
- package/src/identity/hub/custody/CustodyEditFlow.tsx +3 -3
- package/src/identity/hub/identityHubReducer.ts +21 -0
- package/src/identity/hub/profile/effects.ts +16 -3
- package/src/identity/hub/restore/RestoreFlow.tsx +43 -6
- package/src/identity/hub/restore/apply.ts +12 -1
- package/src/identity/hub/restore/recovery.ts +11 -1
- package/src/identity/hub/restore/resolve.ts +1 -1
- package/src/identity/hub/restore/useRestoreEffects.ts +4 -6
- package/src/identity/hub/shared/components/DetailsScreen.tsx +4 -1
- package/src/identity/hub/shared/components/IdentitySummary.tsx +97 -53
- package/src/identity/hub/shared/components/MenuScreen.tsx +18 -15
- package/src/identity/hub/shared/components/UnlinkedIdentityScreen.tsx +1 -1
- package/src/identity/hub/shared/components/menuFlagsFromReconciliation.ts +8 -12
- package/src/identity/hub/shared/effects/sync.ts +16 -3
- package/src/identity/hub/shared/model/copy.ts +2 -4
- package/src/identity/hub/transfer/effects.ts +15 -2
- package/src/identity/hub/useIdentityHubContinuity.ts +145 -23
- package/src/identity/hub/useIdentityHubController.ts +5 -1
- package/src/identity/hub/useIdentityHubSideEffects.ts +2 -4
- package/src/mcp/manager.ts +1 -1
- package/src/models/ModelPicker.tsx +89 -84
- package/src/models/llamacpp.ts +160 -11
- package/src/models/llamacppPreflight.ts +1 -16
- package/src/models/modelPickerOptions.ts +43 -37
- package/src/providers/contracts.ts +1 -0
- package/src/providers/openai-chat.ts +50 -9
- package/src/providers/openai-responses.ts +19 -4
- package/src/runtime/toolExecution.ts +4 -3
- package/src/runtime/turn.ts +61 -30
- package/src/tools/changeDirectoryTool.ts +1 -1
- package/src/tools/contracts.ts +10 -0
- package/src/tools/deleteFileTool.ts +1 -1
- package/src/tools/editTool.ts +1 -1
- package/src/tools/listDirectoryTool.ts +1 -1
- package/src/tools/listSkillFilesTool.ts +77 -0
- package/src/tools/listSkillsTool.ts +68 -0
- package/src/tools/mcpResourceTools.ts +2 -2
- package/src/tools/privateContinuityReadTool.ts +1 -1
- package/src/tools/readSkillTool.ts +107 -0
- package/src/tools/readTool.ts +1 -1
- package/src/tools/registry.ts +6 -0
- package/src/tools/writeFileTool.ts +22 -2
- package/src/ui/Spinner.tsx +1 -1
- package/src/identity/continuity/localBackup.ts +0 -249
- package/src/identity/continuity/zipWriter.ts +0 -95
- package/src/identity/hub/continuity/index.ts +0 -7
- package/src/identity/hub/ens/index.ts +0 -11
- package/src/identity/hub/restore/index.ts +0 -22
package/src/chat/MessageList.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { DiffView } from './display/DiffView.js'
|
|
|
7
7
|
import { SyntaxLine } from './display/SyntaxText.js'
|
|
8
8
|
import { formatToolCall } from './display/toolCallDisplay.js'
|
|
9
9
|
import { BrandSplash } from '../ui/BrandSplash.js'
|
|
10
|
+
import type { RowSlice } from './transcript/transcriptViewport.js'
|
|
10
11
|
|
|
11
12
|
export type ToolCallResult = {
|
|
12
13
|
content: string
|
|
@@ -48,7 +49,11 @@ export type MessageRow =
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
type MessageListProps = {
|
|
51
|
-
|
|
52
|
+
slices: Array<RowSlice<MessageRow>>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function rowsToFullSlices(rows: MessageRow[]): Array<RowSlice<MessageRow>> {
|
|
56
|
+
return rows.map(row => ({ row, clipStart: 0, clipEnd: Number.MAX_SAFE_INTEGER, rowHeight: Number.MAX_SAFE_INTEGER }))
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
type MarkdownBlock =
|
|
@@ -70,13 +75,13 @@ const ASSISTANT_ACCENT = theme.accentPeriwinkle
|
|
|
70
75
|
const ASSISTANT_MARKER = '• '
|
|
71
76
|
const UNREADABLE_REASONING_TEXT = 'reasoning output was not readable text'
|
|
72
77
|
|
|
73
|
-
const MessageListInner: React.FC<MessageListProps> = ({
|
|
78
|
+
const MessageListInner: React.FC<MessageListProps> = ({ slices }) => (
|
|
74
79
|
<Box flexDirection="column">
|
|
75
|
-
{
|
|
80
|
+
{slices.map((slice, index) => (
|
|
76
81
|
<RowView
|
|
77
|
-
key={row.id}
|
|
78
|
-
|
|
79
|
-
tightTop={row.role === 'tool_call' &&
|
|
82
|
+
key={slice.row.id}
|
|
83
|
+
slice={slice}
|
|
84
|
+
tightTop={slice.row.role === 'tool_call' && slices[index - 1]?.row.role === 'tool_call'}
|
|
80
85
|
/>
|
|
81
86
|
))}
|
|
82
87
|
</Box>
|
|
@@ -121,7 +126,8 @@ export function toggleInspectableRow(rows: MessageRow[], rowId?: string): Messag
|
|
|
121
126
|
return rows
|
|
122
127
|
}
|
|
123
128
|
|
|
124
|
-
const RowViewInner: React.FC<{
|
|
129
|
+
const RowViewInner: React.FC<{ slice: RowSlice<MessageRow>; tightTop?: boolean }> = ({ slice, tightTop }) => {
|
|
130
|
+
const { row, clipStart, clipEnd, rowHeight } = slice
|
|
125
131
|
if (row.role === 'user') {
|
|
126
132
|
const display = clipTextForDisplay(row.content, MAX_RENDERED_MESSAGE_CHARS)
|
|
127
133
|
const lines = display.text.length === 0 ? [''] : display.text.split('\n')
|
|
@@ -141,9 +147,18 @@ const RowViewInner: React.FC<{ row: MessageRow; tightTop?: boolean }> = ({ row,
|
|
|
141
147
|
}
|
|
142
148
|
|
|
143
149
|
if (row.role === 'assistant') {
|
|
150
|
+
const showTopMargin = clipStart === 0
|
|
151
|
+
const bodyClipStart = showTopMargin ? 0 : clipStart - 1
|
|
152
|
+
const bodyClipEnd = clipEnd !== undefined ? Math.max(0, clipEnd - 1) : undefined
|
|
144
153
|
return (
|
|
145
|
-
<Box flexDirection="column" marginTop={1}>
|
|
146
|
-
<AssistantBody
|
|
154
|
+
<Box flexDirection="column" marginTop={showTopMargin ? 1 : 0}>
|
|
155
|
+
<AssistantBody
|
|
156
|
+
content={row.content}
|
|
157
|
+
liveTail={row.liveTail}
|
|
158
|
+
streaming={row.streaming}
|
|
159
|
+
clipStart={bodyClipStart}
|
|
160
|
+
clipEnd={bodyClipEnd}
|
|
161
|
+
/>
|
|
147
162
|
</Box>
|
|
148
163
|
)
|
|
149
164
|
}
|
|
@@ -161,6 +176,9 @@ const RowViewInner: React.FC<{ row: MessageRow; tightTop?: boolean }> = ({ row,
|
|
|
161
176
|
expanded
|
|
162
177
|
active={active}
|
|
163
178
|
showCursor={showCursor}
|
|
179
|
+
clipStart={clipStart}
|
|
180
|
+
clipEnd={clipEnd}
|
|
181
|
+
rowHeight={rowHeight}
|
|
164
182
|
/>
|
|
165
183
|
)
|
|
166
184
|
}
|
|
@@ -170,6 +188,9 @@ const RowViewInner: React.FC<{ row: MessageRow; tightTop?: boolean }> = ({ row,
|
|
|
170
188
|
detail="alt+t inspect"
|
|
171
189
|
active={active}
|
|
172
190
|
showCursor={showCursor}
|
|
191
|
+
clipStart={clipStart}
|
|
192
|
+
clipEnd={clipEnd}
|
|
193
|
+
rowHeight={rowHeight}
|
|
173
194
|
/>
|
|
174
195
|
)
|
|
175
196
|
}
|
|
@@ -307,7 +328,10 @@ const ReasoningBlock: React.FC<{
|
|
|
307
328
|
active?: boolean
|
|
308
329
|
expanded?: boolean
|
|
309
330
|
showCursor?: boolean
|
|
310
|
-
|
|
331
|
+
clipStart?: number
|
|
332
|
+
clipEnd?: number
|
|
333
|
+
rowHeight?: number
|
|
334
|
+
}> = ({ content, detail, active = false, expanded = false, showCursor, clipStart = 0, clipEnd, rowHeight }) => {
|
|
311
335
|
const display = useMemo(
|
|
312
336
|
() => clipTextForDisplay(content, MAX_RENDERED_REASONING_CHARS),
|
|
313
337
|
[content],
|
|
@@ -317,31 +341,54 @@ const ReasoningBlock: React.FC<{
|
|
|
317
341
|
return normalized.length === 0 ? [''] : normalized.split('\n')
|
|
318
342
|
}, [display.text])
|
|
319
343
|
|
|
344
|
+
const omittedVisible = display.omittedChars > 0
|
|
345
|
+
const totalLines = rowHeight ?? (1 + (omittedVisible ? 1 : 0) + 1 + (expanded ? lines.length : 0))
|
|
346
|
+
const effClipEnd = clipEnd ?? totalLines
|
|
347
|
+
|
|
348
|
+
const lineInClip = (n: number) => n >= clipStart && n < effClipEnd
|
|
349
|
+
const showMargin = lineInClip(0)
|
|
350
|
+
const omittedLine = 1
|
|
351
|
+
const headerLine = omittedVisible ? 2 : 1
|
|
352
|
+
const bodyStartLine = headerLine + 1
|
|
353
|
+
|
|
354
|
+
const lastBodyIndex = lines.length - 1
|
|
355
|
+
const visibleBody: Array<{ index: number; text: string }> = []
|
|
356
|
+
if (expanded) {
|
|
357
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
358
|
+
const ln = bodyStartLine + i
|
|
359
|
+
if (ln >= effClipEnd) break
|
|
360
|
+
if (ln < clipStart) continue
|
|
361
|
+
visibleBody.push({ index: i, text: lines[i] ?? '' })
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
320
365
|
return (
|
|
321
|
-
<Box flexDirection="column" marginTop={1}>
|
|
322
|
-
{
|
|
366
|
+
<Box flexDirection="column" marginTop={showMargin ? 1 : 0}>
|
|
367
|
+
{omittedVisible && lineInClip(omittedLine) ? (
|
|
323
368
|
<Text color={theme.dim}>{`${display.omittedChars} earlier reasoning characters omitted`}</Text>
|
|
324
369
|
) : null}
|
|
325
|
-
|
|
326
|
-
<
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
370
|
+
{lineInClip(headerLine) ? (
|
|
371
|
+
<Text>
|
|
372
|
+
<AssistantMarker />
|
|
373
|
+
<ShimmerText
|
|
374
|
+
text={expanded ? 'Thinking…' : 'Thinking'}
|
|
375
|
+
color={theme.accentPeriwinkle}
|
|
376
|
+
active={active}
|
|
377
|
+
bold
|
|
378
|
+
italic
|
|
379
|
+
/>
|
|
380
|
+
<Text color={theme.dim}>{` · ${detail}`}</Text>
|
|
381
|
+
{!expanded && showCursor ? <ThinkingCursor active hasPreview /> : null}
|
|
382
|
+
</Text>
|
|
383
|
+
) : null}
|
|
384
|
+
{expanded && visibleBody.length > 0 ? (
|
|
338
385
|
<Box flexDirection="column" marginLeft={2}>
|
|
339
|
-
{
|
|
386
|
+
{visibleBody.map(({ index, text }) => (
|
|
340
387
|
<Box key={index}>
|
|
341
388
|
<Text color={theme.textSubtle}>
|
|
342
389
|
<Text color={theme.dim}>{' '}</Text>
|
|
343
|
-
{
|
|
344
|
-
{showCursor && index ===
|
|
390
|
+
{text || ' '}
|
|
391
|
+
{showCursor && index === lastBodyIndex ? <ThinkingCursor active hasPreview={text.length > 0} /> : null}
|
|
345
392
|
</Text>
|
|
346
393
|
</Box>
|
|
347
394
|
))}
|
|
@@ -351,39 +398,133 @@ const ReasoningBlock: React.FC<{
|
|
|
351
398
|
)
|
|
352
399
|
}
|
|
353
400
|
|
|
354
|
-
const AssistantBody: React.FC<{
|
|
355
|
-
content
|
|
356
|
-
liveTail
|
|
357
|
-
streaming
|
|
358
|
-
|
|
401
|
+
const AssistantBody: React.FC<{
|
|
402
|
+
content: string
|
|
403
|
+
liveTail?: string
|
|
404
|
+
streaming?: boolean
|
|
405
|
+
clipStart?: number
|
|
406
|
+
clipEnd?: number
|
|
407
|
+
}> = ({ content, liveTail, streaming, clipStart = 0, clipEnd }) => {
|
|
359
408
|
const fullText = liveTail ? content + liveTail : content
|
|
360
|
-
const
|
|
361
|
-
() =>
|
|
362
|
-
[fullText],
|
|
409
|
+
const nodes = useMemo(
|
|
410
|
+
() => flattenAssistantBody(fullText, Boolean(streaming)),
|
|
411
|
+
[fullText, streaming],
|
|
363
412
|
)
|
|
364
|
-
const
|
|
365
|
-
|
|
413
|
+
const effClipEnd = clipEnd ?? nodes.length
|
|
414
|
+
const visible = nodes.slice(clipStart, effClipEnd)
|
|
366
415
|
return (
|
|
367
416
|
<Box flexDirection="column">
|
|
368
|
-
{
|
|
369
|
-
<
|
|
370
|
-
) : null}
|
|
371
|
-
{blocks.map((block, index) => (
|
|
372
|
-
<MarkdownBlockView
|
|
373
|
-
key={index}
|
|
374
|
-
block={block}
|
|
375
|
-
streaming={streaming && index === blocks.length - 1}
|
|
376
|
-
prefix={index === 0 || block.kind === 'code' ? <AssistantMarker /> : null}
|
|
377
|
-
/>
|
|
417
|
+
{visible.map((node, i) => (
|
|
418
|
+
<React.Fragment key={clipStart + i}>{node}</React.Fragment>
|
|
378
419
|
))}
|
|
379
|
-
|
|
420
|
+
</Box>
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function flattenAssistantBody(fullText: string, streaming: boolean): React.ReactNode[] {
|
|
425
|
+
const display = clipTextForDisplay(fullText, MAX_RENDERED_MESSAGE_CHARS)
|
|
426
|
+
const blocks = parseMarkdownBlocks(display.text)
|
|
427
|
+
const nodes: React.ReactNode[] = []
|
|
428
|
+
|
|
429
|
+
if (display.omittedChars > 0) {
|
|
430
|
+
nodes.push(
|
|
431
|
+
<Text color={theme.dim}>{`${display.omittedChars} earlier characters omitted`}</Text>,
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (blocks.length === 0) {
|
|
436
|
+
if (streaming) {
|
|
437
|
+
nodes.push(
|
|
380
438
|
<Text>
|
|
381
439
|
<AssistantMarker />
|
|
382
440
|
<Text color={ASSISTANT_ACCENT}><StreamCursor active /></Text>
|
|
383
|
-
</Text
|
|
384
|
-
)
|
|
385
|
-
|
|
386
|
-
|
|
441
|
+
</Text>,
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
return nodes
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
for (let bi = 0; bi < blocks.length; bi += 1) {
|
|
448
|
+
const block = blocks[bi]!
|
|
449
|
+
const isLastBlock = bi === blocks.length - 1
|
|
450
|
+
const prefix = bi === 0 || block.kind === 'code' ? <AssistantMarker /> : null
|
|
451
|
+
const streamingLast = streaming && isLastBlock
|
|
452
|
+
|
|
453
|
+
nodes.push(<Text> </Text>)
|
|
454
|
+
|
|
455
|
+
if (block.kind === 'heading') {
|
|
456
|
+
nodes.push(
|
|
457
|
+
<Text>
|
|
458
|
+
{prefix}
|
|
459
|
+
<InlineText text={block.text} color={ASSISTANT_ACCENT} bold />
|
|
460
|
+
</Text>,
|
|
461
|
+
)
|
|
462
|
+
continue
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (block.kind === 'quote') {
|
|
466
|
+
block.lines.forEach((line, li) => {
|
|
467
|
+
nodes.push(
|
|
468
|
+
<Text>
|
|
469
|
+
{li === 0 ? prefix : null}
|
|
470
|
+
<Text color={ASSISTANT_ACCENT}>| </Text>
|
|
471
|
+
<InlineText text={line} color={theme.dim} />
|
|
472
|
+
</Text>,
|
|
473
|
+
)
|
|
474
|
+
})
|
|
475
|
+
continue
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (block.kind === 'list') {
|
|
479
|
+
block.items.forEach((item, li) => {
|
|
480
|
+
nodes.push(
|
|
481
|
+
<Text>
|
|
482
|
+
{li === 0 ? prefix : null}
|
|
483
|
+
<Text color={ASSISTANT_ACCENT}>{block.ordered ? `${li + 1}. ` : '- '}</Text>
|
|
484
|
+
<InlineText text={item} color={theme.text} />
|
|
485
|
+
</Text>,
|
|
486
|
+
)
|
|
487
|
+
})
|
|
488
|
+
continue
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (block.kind === 'code') {
|
|
492
|
+
nodes.push(
|
|
493
|
+
<Text>
|
|
494
|
+
{prefix}
|
|
495
|
+
<Text color={theme.accentPeriwinkle} bold>{block.lang ?? 'code'}</Text>
|
|
496
|
+
{block.open ? <Text color={theme.dim}> streaming</Text> : null}
|
|
497
|
+
</Text>,
|
|
498
|
+
)
|
|
499
|
+
const codeLines = block.code.length === 0 ? [''] : block.code.split('\n')
|
|
500
|
+
const isShell = block.lang === 'bash' || block.lang === 'sh'
|
|
501
|
+
codeLines.forEach((line, li) => {
|
|
502
|
+
const isLastCode = li === codeLines.length - 1
|
|
503
|
+
nodes.push(
|
|
504
|
+
<Text>
|
|
505
|
+
<Text color={theme.dim}>{` ${isShell ? '$ ' : ' '}`}</Text>
|
|
506
|
+
<SyntaxLine line={line} lang={block.lang} fallbackColor={theme.textSubtle} />
|
|
507
|
+
{block.open && isLastCode ? <Text color={ASSISTANT_ACCENT}> <StreamCursor active /></Text> : null}
|
|
508
|
+
</Text>,
|
|
509
|
+
)
|
|
510
|
+
})
|
|
511
|
+
continue
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const paragraphLines = block.text.split('\n')
|
|
515
|
+
paragraphLines.forEach((line, li) => {
|
|
516
|
+
const isLastLine = li === paragraphLines.length - 1
|
|
517
|
+
nodes.push(
|
|
518
|
+
<Text>
|
|
519
|
+
{li === 0 ? prefix : null}
|
|
520
|
+
<InlineText text={line} color={theme.text} />
|
|
521
|
+
{streamingLast && isLastLine ? <Text color={ASSISTANT_ACCENT}> <StreamCursor active /></Text> : null}
|
|
522
|
+
</Text>,
|
|
523
|
+
)
|
|
524
|
+
})
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return nodes
|
|
387
528
|
}
|
|
388
529
|
|
|
389
530
|
const AssistantMarker: React.FC = () => (
|
|
@@ -52,7 +52,7 @@ export function resolveModelSelection(
|
|
|
52
52
|
baseUrl,
|
|
53
53
|
localMmprojPath: selection.mmprojPath,
|
|
54
54
|
},
|
|
55
|
-
notice: `
|
|
55
|
+
notice: `llama.cpp ready. Now using ${formatModelDisplayName('llamacpp', selection.model, { maxLength: 64 })}${selection.mmprojPath ? ' · vision' : ''}.`,
|
|
56
56
|
tone: 'info',
|
|
57
57
|
}
|
|
58
58
|
}
|
|
@@ -9,6 +9,8 @@ import type { EthagentConfig } from '../storage/config.js'
|
|
|
9
9
|
import type { SessionMessage } from '../storage/sessions.js'
|
|
10
10
|
import type { SessionPermissionRule, ToolResult } from '../tools/contracts.js'
|
|
11
11
|
import { readContinuityFiles } from '../identity/continuity/storage.js'
|
|
12
|
+
import { listSkillsTree } from '../identity/continuity/skills/loadSkills.js'
|
|
13
|
+
import { isDraftScaffold } from '../identity/continuity/skills/scaffold.js'
|
|
12
14
|
import type { MessageRow } from './MessageList.js'
|
|
13
15
|
import {
|
|
14
16
|
buildBaseMessages,
|
|
@@ -121,6 +123,7 @@ export async function runStreamingTurn(
|
|
|
121
123
|
return [
|
|
122
124
|
...(baseSystem ? [baseSystem] : []),
|
|
123
125
|
...await buildIdentityContinuityContextMessages(getConfig()),
|
|
126
|
+
...await buildSkillsIndexMessage(getConfig()),
|
|
124
127
|
...conversationMessages,
|
|
125
128
|
...mentionContextMessages,
|
|
126
129
|
]
|
|
@@ -399,6 +402,11 @@ async function handleEvent(ev: TurnEvent, ctx: EventHandlerContext): Promise<voi
|
|
|
399
402
|
ctx.flushStreamRows()
|
|
400
403
|
return
|
|
401
404
|
}
|
|
405
|
+
case 'thinking_end': {
|
|
406
|
+
ctx.flushStreamRows(true)
|
|
407
|
+
ctx.stopThinkingCursor()
|
|
408
|
+
return
|
|
409
|
+
}
|
|
402
410
|
case 'retry': {
|
|
403
411
|
return
|
|
404
412
|
}
|
|
@@ -565,6 +573,57 @@ async function buildFileMentionContextMessages(
|
|
|
565
573
|
]
|
|
566
574
|
}
|
|
567
575
|
|
|
576
|
+
const PRIVATE_SKILLS_INDEX_BUDGET = 2048
|
|
577
|
+
|
|
578
|
+
export async function buildSkillsIndexMessage(
|
|
579
|
+
config: EthagentConfig,
|
|
580
|
+
): Promise<Message[]> {
|
|
581
|
+
const identity = config.identity
|
|
582
|
+
if (!identity) return []
|
|
583
|
+
try {
|
|
584
|
+
const { skills, supportingCounts } = await listSkillsTree(identity)
|
|
585
|
+
const entries = skills.filter(entry => !isDraftScaffold(entry))
|
|
586
|
+
if (entries.length === 0) return []
|
|
587
|
+
const header = [
|
|
588
|
+
'<private_skills index="true" visibility="private">',
|
|
589
|
+
'Private skills are owner-authored content packs available for this active identity.',
|
|
590
|
+
'Each line shows skill_name — description. When an entry ends with (+N supporting files), call list_private_skill_files to see relative + absolute paths for each file. Read text content via read_private_skill with file:; run executable supporting scripts with run_bash using the absolute path returned by list_private_skill_files. Call list_private_skills for the full index.',
|
|
591
|
+
]
|
|
592
|
+
const lines: string[] = []
|
|
593
|
+
let charCount = header.reduce((sum, line) => sum + line.length + 1, 0)
|
|
594
|
+
let included = 0
|
|
595
|
+
let dropped = 0
|
|
596
|
+
for (const entry of entries) {
|
|
597
|
+
const displayName = entry.displayName ?? entry.name
|
|
598
|
+
const desc = entry.description ? ` — ${entry.description}` : ''
|
|
599
|
+
const hint = entry.whenToUse ? ` (when: ${entry.whenToUse})` : ''
|
|
600
|
+
const supporting = supportingCounts[entry.name] ?? 0
|
|
601
|
+
const trailer = supporting > 0
|
|
602
|
+
? ` (+${supporting} supporting file${supporting === 1 ? '' : 's'})`
|
|
603
|
+
: ''
|
|
604
|
+
const line = `- ${displayName}${desc}${hint}${trailer}`
|
|
605
|
+
if (charCount + line.length + 1 > PRIVATE_SKILLS_INDEX_BUDGET && included > 0) {
|
|
606
|
+
dropped = entries.length - included
|
|
607
|
+
break
|
|
608
|
+
}
|
|
609
|
+
lines.push(line)
|
|
610
|
+
charCount += line.length + 1
|
|
611
|
+
included++
|
|
612
|
+
}
|
|
613
|
+
const footer: string[] = []
|
|
614
|
+
if (dropped > 0) {
|
|
615
|
+
footer.push(`(${dropped} more — call list_private_skills to enumerate)`)
|
|
616
|
+
}
|
|
617
|
+
footer.push('</private_skills>')
|
|
618
|
+
return [{
|
|
619
|
+
role: 'system',
|
|
620
|
+
content: [...header, ...lines, ...footer].join('\n'),
|
|
621
|
+
}]
|
|
622
|
+
} catch {
|
|
623
|
+
return []
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
568
627
|
export async function buildIdentityContinuityContextMessages(
|
|
569
628
|
config: EthagentConfig,
|
|
570
629
|
): Promise<Message[]> {
|
|
@@ -522,6 +522,9 @@ export const ChatInput: React.FC<PromptInputProps> = ({
|
|
|
522
522
|
}
|
|
523
523
|
if (key.tab || key.escape) return
|
|
524
524
|
if (key.ctrl || key.meta) return
|
|
525
|
+
if (key.return || key.backspace || key.delete) return
|
|
526
|
+
if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow) return
|
|
527
|
+
if (key.pageUp || key.pageDown || key.home || key.end) return
|
|
525
528
|
|
|
526
529
|
if (inputText) {
|
|
527
530
|
insertText(inputText)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import wrapAnsi from 'wrap-ansi'
|
|
2
|
+
|
|
1
3
|
export type TextCursor = {
|
|
2
4
|
value: string
|
|
3
5
|
offset: number
|
|
@@ -60,10 +62,18 @@ export function getVisualLines(value: string, wrapWidth: number): VisualLine[] {
|
|
|
60
62
|
if (start === end) {
|
|
61
63
|
lines.push({ start, end })
|
|
62
64
|
} else {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
const logical = value.slice(start, end)
|
|
66
|
+
const wrapped = wrapAnsi(logical, safeWrapWidth, { trim: false, hard: true, wordWrap: true })
|
|
67
|
+
const wrappedLines = wrapped.split('\n')
|
|
68
|
+
let cursor = start
|
|
69
|
+
let lastEnd = start
|
|
70
|
+
for (const line of wrappedLines) {
|
|
71
|
+
const lineEnd = cursor + line.length
|
|
72
|
+
lines.push({ start: cursor, end: lineEnd })
|
|
73
|
+
cursor = lineEnd
|
|
74
|
+
lastEnd = lineEnd
|
|
65
75
|
}
|
|
66
|
-
if (end === value.length &&
|
|
76
|
+
if (end === value.length && lastEnd === end && wrappedLines[wrappedLines.length - 1]!.length === safeWrapWidth) {
|
|
67
77
|
lines.push({ start: end, end })
|
|
68
78
|
}
|
|
69
79
|
}
|
|
@@ -23,8 +23,8 @@ type TranscriptViewProps = {
|
|
|
23
23
|
onScrollabilityChange?: (canScroll: boolean) => void
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const PROMPT_RESERVED_LINES =
|
|
27
|
-
const OVERLAY_RESERVED_LINES =
|
|
26
|
+
const PROMPT_RESERVED_LINES = 12
|
|
27
|
+
const OVERLAY_RESERVED_LINES = 16
|
|
28
28
|
const MIN_TRANSCRIPT_LINES = 6
|
|
29
29
|
const MAX_TRANSCRIPT_LINES = 240
|
|
30
30
|
|
|
@@ -73,8 +73,10 @@ export const TranscriptView: React.FC<TranscriptViewProps> = ({
|
|
|
73
73
|
)
|
|
74
74
|
const visibleReasoningIds = useMemo(
|
|
75
75
|
() => selection.rows
|
|
76
|
-
.filter((
|
|
77
|
-
|
|
76
|
+
.filter((slice): slice is { row: Extract<MessageRow, { role: 'thinking' }>; clipStart: number; clipEnd: number; rowHeight: number } =>
|
|
77
|
+
slice.row.role === 'thinking',
|
|
78
|
+
)
|
|
79
|
+
.map(slice => slice.row.id),
|
|
78
80
|
[selection.rows],
|
|
79
81
|
)
|
|
80
82
|
|
|
@@ -129,7 +131,7 @@ export const TranscriptView: React.FC<TranscriptViewProps> = ({
|
|
|
129
131
|
{` saves the full transcript`}
|
|
130
132
|
</Text>
|
|
131
133
|
) : null}
|
|
132
|
-
<MessageList
|
|
134
|
+
<MessageList slices={selection.rows} />
|
|
133
135
|
{selection.hiddenAfter > 0 ? (
|
|
134
136
|
<Text color={theme.dim}>
|
|
135
137
|
{` ${selection.hiddenAfter} later message${selection.hiddenAfter === 1 ? '' : 's'} below · `}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MessageRow } from '../MessageList.js'
|
|
2
|
+
import { flattenAssistantBody } from '../MessageList.js'
|
|
2
3
|
|
|
3
4
|
export type TranscriptAnchor = {
|
|
4
5
|
rowId: string
|
|
@@ -11,6 +12,13 @@ export type TranscriptViewportState = {
|
|
|
11
12
|
anchor: TranscriptAnchor | null
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
export type RowSlice<T> = {
|
|
16
|
+
row: T
|
|
17
|
+
clipStart: number
|
|
18
|
+
clipEnd: number
|
|
19
|
+
rowHeight: number
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
export function buildLineOffsets(rowHeights: number[]): number[] {
|
|
15
23
|
const out = new Array<number>(rowHeights.length + 1).fill(0)
|
|
16
24
|
for (let i = 0; i < rowHeights.length; i += 1) {
|
|
@@ -62,12 +70,12 @@ export function clampLine(line: number, maxScrollTop: number): number {
|
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
export type TranscriptTailSelection<T> = {
|
|
65
|
-
rows: T
|
|
73
|
+
rows: Array<RowSlice<T>>
|
|
66
74
|
hiddenCount: number
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
export type TranscriptWindowSelection<T> = {
|
|
70
|
-
rows: T
|
|
78
|
+
rows: Array<RowSlice<T>>
|
|
71
79
|
hiddenBefore: number
|
|
72
80
|
hiddenAfter: number
|
|
73
81
|
totalLines: number
|
|
@@ -94,8 +102,12 @@ export function selectTailRowsForViewport<T>(
|
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
const firstVisible = Math.max(0, start + 1)
|
|
105
|
+
const slice = rows.slice(firstVisible).map(row => {
|
|
106
|
+
const height = Math.max(1, estimateHeight(row))
|
|
107
|
+
return { row, clipStart: 0, clipEnd: height, rowHeight: height }
|
|
108
|
+
})
|
|
97
109
|
return {
|
|
98
|
-
rows:
|
|
110
|
+
rows: slice,
|
|
99
111
|
hiddenCount: firstVisible,
|
|
100
112
|
}
|
|
101
113
|
}
|
|
@@ -118,7 +130,7 @@ export function selectRowsForScrollOffset<T>(
|
|
|
118
130
|
const scrollOffset = clampLine(scrollOffsetFromTail, maxScrollOffset)
|
|
119
131
|
const startLine = Math.max(0, totalLines - budget - scrollOffset)
|
|
120
132
|
|
|
121
|
-
return selectRowsForLineWindow(rows, offsets, budget, startLine, totalLines, maxScrollOffset)
|
|
133
|
+
return selectRowsForLineWindow(rows, heights, offsets, budget, startLine, totalLines, maxScrollOffset)
|
|
122
134
|
}
|
|
123
135
|
|
|
124
136
|
export function selectRowsForScrollTop<T>(
|
|
@@ -138,7 +150,7 @@ export function selectRowsForScrollTop<T>(
|
|
|
138
150
|
const maxScrollOffset = Math.max(0, totalLines - budget)
|
|
139
151
|
const startLine = clampLine(scrollTopLine, maxScrollOffset)
|
|
140
152
|
|
|
141
|
-
return selectRowsForLineWindow(rows, offsets, budget, startLine, totalLines, maxScrollOffset)
|
|
153
|
+
return selectRowsForLineWindow(rows, heights, offsets, budget, startLine, totalLines, maxScrollOffset)
|
|
142
154
|
}
|
|
143
155
|
|
|
144
156
|
export function scrollTopForPageUp(
|
|
@@ -159,11 +171,12 @@ export function scrollTopForPageDown(
|
|
|
159
171
|
|
|
160
172
|
function pageScrollDistance(viewportLines: number): number {
|
|
161
173
|
const viewport = Math.max(1, Math.floor(viewportLines))
|
|
162
|
-
return Math.max(1,
|
|
174
|
+
return Math.max(1, viewport - 2)
|
|
163
175
|
}
|
|
164
176
|
|
|
165
177
|
function selectRowsForLineWindow<T>(
|
|
166
178
|
rows: T[],
|
|
179
|
+
heights: number[],
|
|
167
180
|
offsets: number[],
|
|
168
181
|
budget: number,
|
|
169
182
|
startLine: number,
|
|
@@ -178,8 +191,20 @@ function selectRowsForLineWindow<T>(
|
|
|
178
191
|
? rows.length
|
|
179
192
|
: Math.min(rows.length, findRowIndexAtLine(offsets, lastVisibleLine) + 1)
|
|
180
193
|
|
|
194
|
+
const slices: Array<RowSlice<T>> = []
|
|
195
|
+
for (let i = startIndex; i < endIndex; i += 1) {
|
|
196
|
+
const row = rows[i]
|
|
197
|
+
if (!row) continue
|
|
198
|
+
const rowTop = offsets[i] ?? 0
|
|
199
|
+
const height = heights[i] ?? 1
|
|
200
|
+
const clipStart = Math.max(0, startLine - rowTop)
|
|
201
|
+
const clipEnd = Math.min(height, endLine - rowTop)
|
|
202
|
+
if (clipEnd <= clipStart) continue
|
|
203
|
+
slices.push({ row, clipStart, clipEnd, rowHeight: height })
|
|
204
|
+
}
|
|
205
|
+
|
|
181
206
|
return {
|
|
182
|
-
rows:
|
|
207
|
+
rows: slices,
|
|
183
208
|
hiddenBefore: startIndex,
|
|
184
209
|
hiddenAfter: rows.length - endIndex,
|
|
185
210
|
totalLines,
|
|
@@ -191,13 +216,11 @@ export function estimateMessageRowHeight(row: MessageRow, columns = 80): number
|
|
|
191
216
|
const contentWidth = Math.max(20, columns - 8)
|
|
192
217
|
switch (row.role) {
|
|
193
218
|
case 'user':
|
|
194
|
-
return
|
|
219
|
+
return userRowLineCount(row.content, contentWidth)
|
|
195
220
|
case 'assistant':
|
|
196
|
-
return
|
|
221
|
+
return assistantRowLineCount(row.content, row.liveTail ?? '', contentWidth, Boolean(row.streaming))
|
|
197
222
|
case 'thinking':
|
|
198
|
-
return row
|
|
199
|
-
? 3 + wrappedLineCount([row.content, row.liveTail ?? ''].filter(Boolean).join('\n'), contentWidth)
|
|
200
|
-
: 3 + wrappedLineCount(reasoningPreview(row), contentWidth)
|
|
223
|
+
return thinkingRowLineCount(row, contentWidth)
|
|
201
224
|
case 'tool_call':
|
|
202
225
|
return 1
|
|
203
226
|
case 'note':
|
|
@@ -209,11 +232,59 @@ export function estimateMessageRowHeight(row: MessageRow, columns = 80): number
|
|
|
209
232
|
}
|
|
210
233
|
}
|
|
211
234
|
|
|
212
|
-
function
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
235
|
+
export function userRowLineCount(content: string, contentWidth: number): number {
|
|
236
|
+
const lines = splitLines(content)
|
|
237
|
+
return 1 + lines.reduce((sum, line) => sum + Math.max(1, Math.ceil(line.length / contentWidth)), 0)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function assistantRowLineCount(content: string, liveTail: string, _contentWidth: number, streaming = false): number {
|
|
241
|
+
const fullText = liveTail ? content + liveTail : content
|
|
242
|
+
return 1 + flattenAssistantBody(fullText, streaming).length
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function thinkingRowLineCount(
|
|
246
|
+
row: Extract<MessageRow, { role: 'thinking' }>,
|
|
247
|
+
_contentWidth: number,
|
|
248
|
+
): number {
|
|
249
|
+
const omitted = thinkingDisplayOmittedChars(row)
|
|
250
|
+
const overhead = 1 + (omitted > 0 ? 1 : 0) + 1
|
|
251
|
+
if (!row.expanded) return overhead
|
|
252
|
+
const body = thinkingDisplayBody(row)
|
|
253
|
+
const lines = splitLines(body)
|
|
254
|
+
return overhead + lines.length
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function thinkingDisplayBody(row: Extract<MessageRow, { role: 'thinking' }>): string {
|
|
258
|
+
const text = row.liveTail ? row.content + row.liveTail : row.content
|
|
259
|
+
return clipReasoningForDisplayText(text)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function thinkingDisplayOmittedChars(row: Extract<MessageRow, { role: 'thinking' }>): number {
|
|
263
|
+
const text = row.liveTail ? row.content + row.liveTail : row.content
|
|
264
|
+
return clipReasoningForDisplayOmitted(text)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const MAX_RENDERED_REASONING_CHARS = 10_000
|
|
268
|
+
|
|
269
|
+
function clipReasoningForDisplayText(text: string): string {
|
|
270
|
+
if (text.length <= MAX_RENDERED_REASONING_CHARS) return text
|
|
271
|
+
const rawStart = Math.max(0, text.length - MAX_RENDERED_REASONING_CHARS)
|
|
272
|
+
const newline = text.indexOf('\n', rawStart)
|
|
273
|
+
const start = newline >= 0 && newline - rawStart <= 240 ? newline + 1 : rawStart
|
|
274
|
+
return text.slice(start)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function clipReasoningForDisplayOmitted(text: string): number {
|
|
278
|
+
if (text.length <= MAX_RENDERED_REASONING_CHARS) return 0
|
|
279
|
+
const rawStart = Math.max(0, text.length - MAX_RENDERED_REASONING_CHARS)
|
|
280
|
+
const newline = text.indexOf('\n', rawStart)
|
|
281
|
+
return newline >= 0 && newline - rawStart <= 240 ? newline + 1 : rawStart
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function splitLines(text: string): string[] {
|
|
285
|
+
if (!text) return ['']
|
|
286
|
+
const normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
|
287
|
+
return normalized.split('\n')
|
|
217
288
|
}
|
|
218
289
|
|
|
219
290
|
function wrappedLineCount(text: string, width: number): number {
|