ethagent 2.3.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 +18 -4
- package/package.json +2 -1
- package/src/app/FirstRun.tsx +157 -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 +12 -1
- package/src/chat/ChatScreen.tsx +17 -5
- package/src/chat/ConversationStack.tsx +25 -19
- package/src/chat/MessageList.tsx +194 -53
- package/src/chat/chatSessionState.ts +4 -1
- package/src/chat/chatTurnOrchestrator.ts +65 -2
- package/src/chat/input/ChatInput.tsx +28 -2
- package/src/chat/input/imageRefs.ts +30 -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/ResumeView.tsx +16 -7
- 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 +211 -74
- package/src/models/huggingface.ts +180 -2
- package/src/models/llamacpp.ts +261 -17
- package/src/models/llamacppPreflight.ts +16 -12
- package/src/models/modelPickerOptions.ts +57 -38
- package/src/providers/anthropic.ts +36 -5
- package/src/providers/contracts.ts +10 -1
- package/src/providers/gemini.ts +29 -3
- package/src/providers/openai-chat.ts +131 -11
- package/src/providers/openai-responses-format.ts +29 -8
- package/src/providers/openai-responses.ts +41 -11
- package/src/providers/registry.ts +1 -0
- package/src/runtime/toolExecution.ts +4 -3
- package/src/runtime/turn.ts +61 -30
- package/src/storage/config.ts +1 -0
- package/src/storage/sessions.ts +14 -2
- 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 +15 -3
- package/src/ui/theme.ts +2 -0
- package/src/utils/images.ts +140 -0
- package/src/utils/messages.ts +2 -0
- 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
|
@@ -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 {
|
|
@@ -72,16 +72,16 @@ export const PermissionPrompt: React.FC<PermissionPromptProps> = ({ request, onD
|
|
|
72
72
|
export function permissionOptionsForRequest(request: PermissionRequest): Array<{ value: PermissionDecision; label: string; hint?: string; disabled?: boolean }> {
|
|
73
73
|
if (request.kind === 'bash') {
|
|
74
74
|
return [
|
|
75
|
-
{ value: 'allow-once', label: 'Allow
|
|
75
|
+
{ value: 'allow-once', label: 'Allow once', hint: 'Approve only this command execution' },
|
|
76
76
|
{
|
|
77
77
|
value: 'allow-command-project',
|
|
78
|
-
label: 'Allow
|
|
78
|
+
label: 'Allow exact command',
|
|
79
79
|
hint: 'Remember this exact command for this project',
|
|
80
80
|
disabled: !request.canPersistExact,
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
value: 'allow-command-prefix-project',
|
|
84
|
-
label: request.commandPrefix ? `Allow ${request.commandPrefix}
|
|
84
|
+
label: request.commandPrefix ? `Allow ${request.commandPrefix} commands` : 'Allow command family',
|
|
85
85
|
hint: 'Remember this base command in this working directory for this project',
|
|
86
86
|
disabled: !request.canPersistPrefix,
|
|
87
87
|
},
|
|
@@ -91,63 +91,63 @@ export function permissionOptionsForRequest(request: PermissionRequest): Array<{
|
|
|
91
91
|
|
|
92
92
|
if (request.kind === 'mcp') {
|
|
93
93
|
const risk = request.destructive
|
|
94
|
-
? '
|
|
94
|
+
? 'Server marks this tool as destructive'
|
|
95
95
|
: request.openWorld
|
|
96
|
-
? '
|
|
96
|
+
? 'Server marks this tool as open-world'
|
|
97
97
|
: request.readOnly
|
|
98
|
-
? '
|
|
99
|
-
: '
|
|
98
|
+
? 'Server marks this tool as read-only'
|
|
99
|
+
: 'Server did not mark this tool read-only'
|
|
100
100
|
return [
|
|
101
|
-
{ value: 'allow-once', label: '
|
|
102
|
-
{ value: 'allow-mcp-tool-project', label: '
|
|
101
|
+
{ value: 'allow-once', label: 'Allow once', hint: risk },
|
|
102
|
+
{ value: 'allow-mcp-tool-project', label: 'Always allow this MCP tool', hint: request.toolKey },
|
|
103
103
|
{
|
|
104
104
|
value: 'allow-mcp-server-project',
|
|
105
|
-
label: `
|
|
106
|
-
hint: '
|
|
105
|
+
label: `Always allow ${request.serverName}`,
|
|
106
|
+
hint: 'Remember all tools from this MCP server for this project',
|
|
107
107
|
disabled: !request.canPersistServer,
|
|
108
108
|
},
|
|
109
|
-
{ value: 'deny', label: '
|
|
109
|
+
{ value: 'deny', label: 'Deny', hint: 'Return a denial back to the model' },
|
|
110
110
|
]
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
if (request.kind === 'delete') {
|
|
114
114
|
return [
|
|
115
|
-
{ value: 'allow-once', label: '
|
|
116
|
-
{ value: 'deny', label: '
|
|
115
|
+
{ value: 'allow-once', label: 'Delete this file', hint: 'Approve this deletion only' },
|
|
116
|
+
{ value: 'deny', label: 'Deny', hint: 'Keep the file unchanged' },
|
|
117
117
|
]
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
if (request.kind === 'private-continuity-read') {
|
|
121
121
|
return [
|
|
122
|
-
{ value: 'allow-once', label: '
|
|
123
|
-
{ value: 'deny', label: '
|
|
122
|
+
{ value: 'allow-once', label: 'Allow once', hint: `Read ${request.file}` },
|
|
123
|
+
{ value: 'deny', label: 'Deny', hint: 'Keep private continuity hidden' },
|
|
124
124
|
]
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
if (request.kind === 'private-continuity-edit') {
|
|
128
128
|
return [
|
|
129
|
-
{ value: 'allow-once', label: 'Approve
|
|
129
|
+
{ value: 'allow-once', label: 'Approve once', hint: `Apply this edit to ${request.file}` },
|
|
130
130
|
{ value: 'deny', label: 'Deny', hint: 'Keep private continuity unchanged' },
|
|
131
131
|
]
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
return [
|
|
135
|
-
{ value: 'allow-once', label: '
|
|
136
|
-
{ value: 'allow-path-project', label: '
|
|
137
|
-
{ value: 'allow-directory-project', label: '
|
|
135
|
+
{ value: 'allow-once', label: 'Allow once', hint: 'Approve only this action' },
|
|
136
|
+
{ value: 'allow-path-project', label: 'Always allow this file', hint: request.relativePath },
|
|
137
|
+
{ value: 'allow-directory-project', label: 'Always allow this folder', hint: request.directoryPath },
|
|
138
138
|
{
|
|
139
139
|
value: 'allow-kind-project',
|
|
140
140
|
label:
|
|
141
141
|
request.kind === 'edit'
|
|
142
|
-
? '
|
|
142
|
+
? 'Always allow edits'
|
|
143
143
|
: request.kind === 'write'
|
|
144
|
-
? '
|
|
144
|
+
? 'Always allow writes'
|
|
145
145
|
: request.kind === 'cd'
|
|
146
|
-
? '
|
|
147
|
-
: '
|
|
148
|
-
hint: '
|
|
146
|
+
? 'Always allow directory changes'
|
|
147
|
+
: 'Always allow reads',
|
|
148
|
+
hint: 'Remember this tool kind for this project',
|
|
149
149
|
},
|
|
150
|
-
{ value: 'deny', label: '
|
|
150
|
+
{ value: 'deny', label: 'Deny', hint: 'Return a denial back to the model' },
|
|
151
151
|
]
|
|
152
152
|
}
|
|
153
153
|
|
|
@@ -118,18 +118,24 @@ export const PermissionsView: React.FC<PermissionsViewProps> = ({
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
function buildOptions(rules: SessionPermissionRule[]): Array<SelectOption<SessionPermissionRule | typeof CLEAR_ALL_VALUE>> {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
121
|
+
const out: Array<SelectOption<SessionPermissionRule | typeof CLEAR_ALL_VALUE>> = []
|
|
122
|
+
if (rules.length > 0) {
|
|
123
|
+
out.push({ value: CLEAR_ALL_VALUE, role: 'section', label: 'Saved Rules' })
|
|
124
|
+
for (const rule of rules) {
|
|
125
|
+
out.push({
|
|
126
|
+
value: rule,
|
|
127
|
+
label: describeRule(rule),
|
|
128
|
+
hint: describeRuleScope(rule),
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
out.push({ value: CLEAR_ALL_VALUE, role: 'section', label: 'Manage' })
|
|
133
|
+
out.push({
|
|
134
|
+
value: CLEAR_ALL_VALUE,
|
|
135
|
+
label: 'Remove all saved rules',
|
|
136
|
+
hint: 'Clear all remembered permissions for this project',
|
|
137
|
+
})
|
|
138
|
+
return out
|
|
133
139
|
}
|
|
134
140
|
|
|
135
141
|
function describeRule(rule: SessionPermissionRule): string {
|
|
@@ -48,8 +48,8 @@ export const ResumeView: React.FC<ResumeViewProps> = ({ currentSessionId, onResu
|
|
|
48
48
|
|
|
49
49
|
if (state.kind === 'loading') {
|
|
50
50
|
return (
|
|
51
|
-
<Surface title="Resume Session" subtitle="
|
|
52
|
-
<Spinner label="loading
|
|
51
|
+
<Surface title="Resume Session" subtitle="Recent chats and directories." footer="esc closes">
|
|
52
|
+
<Spinner label="loading..." />
|
|
53
53
|
</Surface>
|
|
54
54
|
)
|
|
55
55
|
}
|
|
@@ -65,7 +65,7 @@ export const ResumeView: React.FC<ResumeViewProps> = ({ currentSessionId, onResu
|
|
|
65
65
|
if (state.kind === 'confirmClear') {
|
|
66
66
|
return (
|
|
67
67
|
<Surface
|
|
68
|
-
title="Clear All
|
|
68
|
+
title="Clear All Saved Sessions?"
|
|
69
69
|
subtitle={`${state.sessions.length} saved session${state.sessions.length === 1 ? '' : 's'} will be removed.`}
|
|
70
70
|
tone="error"
|
|
71
71
|
footer="enter selects · esc returns to resume"
|
|
@@ -76,9 +76,10 @@ export const ResumeView: React.FC<ResumeViewProps> = ({ currentSessionId, onResu
|
|
|
76
76
|
{state.error ? <Text color={theme.accentError}>{state.error}</Text> : null}
|
|
77
77
|
</Box>
|
|
78
78
|
<Select<'back' | 'clear'>
|
|
79
|
+
hintLayout="inline"
|
|
79
80
|
options={[
|
|
80
|
-
{ value: 'back', label: '
|
|
81
|
-
{ value: 'clear', label: '
|
|
81
|
+
{ value: 'back', label: 'Back to Sessions' },
|
|
82
|
+
{ value: 'clear', label: 'Clear All Saved Sessions', hint: 'Cannot be undone' },
|
|
82
83
|
]}
|
|
83
84
|
onSubmit={choice => {
|
|
84
85
|
if (choice === 'back') {
|
|
@@ -155,11 +156,18 @@ export function buildResumeOptions(
|
|
|
155
156
|
label: '',
|
|
156
157
|
disabled: true,
|
|
157
158
|
}
|
|
159
|
+
const manageHeader: SelectOption<string> = {
|
|
160
|
+
value: 'separator:manage',
|
|
161
|
+
label: 'Manage',
|
|
162
|
+
role: 'section',
|
|
163
|
+
bold: true,
|
|
164
|
+
disabled: true,
|
|
165
|
+
}
|
|
158
166
|
|
|
159
167
|
const clearOption: SelectOption<string> = {
|
|
160
168
|
value: CLEAR_ALL_SESSIONS_VALUE,
|
|
161
|
-
label: 'Clear All
|
|
162
|
-
hint: '
|
|
169
|
+
label: 'Clear All Saved Sessions',
|
|
170
|
+
hint: 'Removes saved chats and resume context',
|
|
163
171
|
role: 'utility',
|
|
164
172
|
}
|
|
165
173
|
|
|
@@ -202,6 +210,7 @@ export function buildResumeOptions(
|
|
|
202
210
|
}
|
|
203
211
|
|
|
204
212
|
options.push(manageSpacer)
|
|
213
|
+
options.push(manageHeader)
|
|
205
214
|
options.push(clearOption)
|
|
206
215
|
|
|
207
216
|
return options
|
|
@@ -142,11 +142,13 @@ export const RewindView: React.FC<RewindViewProps> = ({
|
|
|
142
142
|
const canRestoreCode = selectedRow.entries.length > 0
|
|
143
143
|
|
|
144
144
|
const actionOptions: Array<SelectOption<ConfirmOption>> = [
|
|
145
|
+
{ value: 'both', role: 'section', label: 'Restore' },
|
|
145
146
|
{ value: 'both', prefix: '1.', label: 'Restore code and conversation', disabled: !canRestoreCode },
|
|
146
147
|
{ value: 'conversation', prefix: '2.', label: 'Restore conversation' },
|
|
147
148
|
{ value: 'code', prefix: '3.', label: 'Restore code', disabled: !canRestoreCode },
|
|
148
149
|
{ value: 'summarize', prefix: '4.', label: 'Summarize from here' },
|
|
149
|
-
{ value: 'nevermind',
|
|
150
|
+
{ value: 'nevermind', role: 'section', label: 'Navigation' },
|
|
151
|
+
{ value: 'nevermind', prefix: '5.', label: 'Never mind', role: 'utility' },
|
|
150
152
|
]
|
|
151
153
|
const defaultValue: ConfirmOption = canRestoreCode ? 'both' : 'conversation'
|
|
152
154
|
const initialIndex = actionOptions.findIndex(option => option.value === defaultValue && !option.disabled)
|
|
@@ -5,6 +5,8 @@ import { Select } from '../ui/Select.js'
|
|
|
5
5
|
import { theme } from '../ui/theme.js'
|
|
6
6
|
import type { FactoryResetPlan } from '../storage/factoryReset.js'
|
|
7
7
|
|
|
8
|
+
type SectionTone = 'destructive' | 'safe' | 'untouched'
|
|
9
|
+
|
|
8
10
|
export const ResetConfirmView: React.FC<{
|
|
9
11
|
plan: FactoryResetPlan
|
|
10
12
|
onDone: (confirmed: boolean) => void
|
|
@@ -16,17 +18,22 @@ export const ResetConfirmView: React.FC<{
|
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
return (
|
|
19
|
-
<Surface
|
|
21
|
+
<Surface
|
|
22
|
+
title="Reset Local Data?"
|
|
23
|
+
subtitle="Deletes this machine's ethagent data. Models and onchain records stay."
|
|
24
|
+
footer="enter select · esc cancel"
|
|
25
|
+
tone="error"
|
|
26
|
+
>
|
|
20
27
|
<Box flexDirection="column">
|
|
21
|
-
<Section title="Deletes" lines={[
|
|
28
|
+
<Section tone="destructive" title="Deletes" lines={[
|
|
22
29
|
'Identity files, sessions, history, credentials',
|
|
23
30
|
localDataLine(plan.deletePaths.length),
|
|
24
31
|
]} />
|
|
25
|
-
<Section title="Keeps" lines={[
|
|
32
|
+
<Section tone="safe" title="Keeps" lines={[
|
|
26
33
|
'Local GGUF models and llama.cpp runners',
|
|
27
34
|
...(plan.preservedPaths.length > 0 ? [`${plan.preservedPaths.length} local model path${plan.preservedPaths.length === 1 ? '' : 's'}`] : ['No local model assets found']),
|
|
28
35
|
]} />
|
|
29
|
-
<Section title="Not Touched" lines={[
|
|
36
|
+
<Section tone="untouched" title="Not Touched" lines={[
|
|
30
37
|
'ERC-8004 tokens and onchain records',
|
|
31
38
|
'IPFS snapshots and public metadata',
|
|
32
39
|
]} />
|
|
@@ -34,9 +41,11 @@ export const ResetConfirmView: React.FC<{
|
|
|
34
41
|
<Box marginTop={1}>
|
|
35
42
|
<Select<'confirm' | 'cancel'>
|
|
36
43
|
options={[
|
|
37
|
-
{ value: 'confirm', label: '
|
|
38
|
-
{ value: 'cancel', label: '
|
|
44
|
+
{ value: 'confirm', label: 'Yes, reset local data', hint: 'Delete local ethagent data on this machine', bold: true },
|
|
45
|
+
{ value: 'cancel', label: 'No, cancel', hint: 'Leave local data unchanged', role: 'utility' },
|
|
39
46
|
]}
|
|
47
|
+
hintLayout="inline"
|
|
48
|
+
initialIndex={1}
|
|
40
49
|
onSubmit={choice => finish(choice === 'confirm')}
|
|
41
50
|
onCancel={() => finish(false)}
|
|
42
51
|
/>
|
|
@@ -45,15 +54,21 @@ export const ResetConfirmView: React.FC<{
|
|
|
45
54
|
)
|
|
46
55
|
}
|
|
47
56
|
|
|
48
|
-
const Section: React.FC<{ title: string; lines: string[] }> = ({ title, lines }) => (
|
|
57
|
+
const Section: React.FC<{ tone: SectionTone; title: string; lines: string[] }> = ({ tone, title, lines }) => (
|
|
49
58
|
<Box flexDirection="column" marginBottom={1}>
|
|
50
|
-
<Text color={
|
|
59
|
+
<Text color={sectionTitleColor(tone)}>{title}</Text>
|
|
51
60
|
{lines.map(line => (
|
|
52
|
-
<Text key={line} color={theme.textSubtle}
|
|
61
|
+
<Text key={line} color={theme.textSubtle}>· {line}</Text>
|
|
53
62
|
))}
|
|
54
63
|
</Box>
|
|
55
64
|
)
|
|
56
65
|
|
|
66
|
+
function sectionTitleColor(tone: SectionTone): string {
|
|
67
|
+
if (tone === 'destructive') return theme.accentError
|
|
68
|
+
if (tone === 'safe') return theme.accentPeriwinkle
|
|
69
|
+
return theme.dim
|
|
70
|
+
}
|
|
71
|
+
|
|
57
72
|
function localDataLine(count: number): string {
|
|
58
73
|
if (count === 0) return 'No local ethagent data found'
|
|
59
74
|
return `${count} local path${count === 1 ? '' : 's'} under ~/.ethagent`
|
|
@@ -12,9 +12,20 @@ type EditorCommand = {
|
|
|
12
12
|
shell?: boolean
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function openFileInEditor(file: string): Promise<EditorOpenResult> {
|
|
15
|
+
export async function openFileInEditor(file: string): Promise<EditorOpenResult> {
|
|
16
|
+
if (isVscodeEnvironment()) {
|
|
17
|
+
const vscode = vscodeEditorCommand(file)
|
|
18
|
+
const result = await openEditorCommand(vscode)
|
|
19
|
+
if (result.ok) return result
|
|
20
|
+
}
|
|
16
21
|
const command = defaultEditorCommand(file)
|
|
17
|
-
if (!command) return
|
|
22
|
+
if (!command) return { ok: false, error: 'no default open command for this platform' }
|
|
23
|
+
return openEditorCommand(command)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function openInFileManager(target: string): Promise<EditorOpenResult> {
|
|
27
|
+
const command = defaultEditorCommand(target)
|
|
28
|
+
if (!command) return { ok: false, error: 'no default open command for this platform' }
|
|
18
29
|
return openEditorCommand(command)
|
|
19
30
|
}
|
|
20
31
|
|
|
@@ -47,3 +58,17 @@ export function defaultEditorCommand(file: string, platform: NodeJS.Platform = p
|
|
|
47
58
|
if (platform === 'darwin') return { cmd: 'open', args: [file], method: 'open', waited: false }
|
|
48
59
|
return { cmd: 'xdg-open', args: [file], method: 'xdg-open', waited: false }
|
|
49
60
|
}
|
|
61
|
+
|
|
62
|
+
export function isVscodeEnvironment(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
63
|
+
if (env.TERM_PROGRAM === 'vscode') return true
|
|
64
|
+
if (env.VSCODE_PID) return true
|
|
65
|
+
if (env.VSCODE_GIT_IPC_HANDLE) return true
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function vscodeEditorCommand(file: string, platform: NodeJS.Platform = process.platform): EditorCommand {
|
|
70
|
+
if (platform === 'win32') {
|
|
71
|
+
return { cmd: 'cmd', args: ['/c', 'code', '--reuse-window', file], method: 'vscode', waited: false }
|
|
72
|
+
}
|
|
73
|
+
return { cmd: 'code', args: ['--reuse-window', file], method: 'vscode', waited: false }
|
|
74
|
+
}
|