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.
Files changed (110) hide show
  1. package/README.md +18 -4
  2. package/package.json +2 -1
  3. package/src/app/FirstRun.tsx +157 -15
  4. package/src/app/FirstRunTimeline.tsx +4 -0
  5. package/src/app/input/AppInputProvider.tsx +19 -0
  6. package/src/app/input/appInputParser.ts +19 -4
  7. package/src/chat/ChatBottomPane.tsx +12 -1
  8. package/src/chat/ChatScreen.tsx +17 -5
  9. package/src/chat/ConversationStack.tsx +25 -19
  10. package/src/chat/MessageList.tsx +194 -53
  11. package/src/chat/chatSessionState.ts +4 -1
  12. package/src/chat/chatTurnOrchestrator.ts +65 -2
  13. package/src/chat/input/ChatInput.tsx +28 -2
  14. package/src/chat/input/imageRefs.ts +30 -0
  15. package/src/chat/input/textCursor.ts +13 -3
  16. package/src/chat/transcript/TranscriptView.tsx +7 -5
  17. package/src/chat/transcript/transcriptViewport.ts +88 -17
  18. package/src/chat/views/PermissionPrompt.tsx +26 -26
  19. package/src/chat/views/PermissionsView.tsx +18 -12
  20. package/src/chat/views/ResumeView.tsx +16 -7
  21. package/src/chat/views/RewindView.tsx +3 -1
  22. package/src/cli/ResetConfirmView.tsx +24 -9
  23. package/src/identity/continuity/editor.ts +27 -2
  24. package/src/identity/continuity/envelope.ts +125 -0
  25. package/src/identity/continuity/publicSkills.ts +37 -1
  26. package/src/identity/continuity/skills/frontmatter.ts +183 -0
  27. package/src/identity/continuity/skills/loadSkills.ts +609 -0
  28. package/src/identity/continuity/skills/publicSkillsSync.ts +32 -0
  29. package/src/identity/continuity/skills/scaffold.ts +52 -0
  30. package/src/identity/continuity/skills/types.ts +30 -0
  31. package/src/identity/continuity/storage/defaults.ts +28 -47
  32. package/src/identity/continuity/storage/files.ts +1 -0
  33. package/src/identity/continuity/storage/paths.ts +1 -0
  34. package/src/identity/continuity/storage/scaffold.ts +25 -23
  35. package/src/identity/continuity/storage/status.ts +34 -5
  36. package/src/identity/continuity/storage/types.ts +3 -2
  37. package/src/identity/continuity/storage.ts +3 -0
  38. package/src/identity/hub/OperationalRoutes.tsx +105 -3
  39. package/src/identity/hub/Routes.tsx +5 -3
  40. package/src/identity/hub/continuity/ContinuityDashboardScreen.tsx +5 -51
  41. package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +1 -1
  42. package/src/identity/hub/continuity/SavePromptScreen.tsx +1 -0
  43. package/src/identity/hub/continuity/effects.ts +36 -5
  44. package/src/identity/hub/continuity/skills/DeleteSkillConfirmScreen.tsx +112 -0
  45. package/src/identity/hub/continuity/skills/DeleteSkillScreen.tsx +123 -0
  46. package/src/identity/hub/continuity/skills/NewSkillScreen.tsx +57 -0
  47. package/src/identity/hub/continuity/skills/NewSkillVisibilityScreen.tsx +52 -0
  48. package/src/identity/hub/continuity/skills/SkillVisibilityScreen.tsx +171 -0
  49. package/src/identity/hub/continuity/skills/SkillsTreeScreen.tsx +213 -0
  50. package/src/identity/hub/continuity/snapshot.ts +3 -0
  51. package/src/identity/hub/continuity/state.ts +3 -2
  52. package/src/identity/hub/continuity/vault.ts +42 -10
  53. package/src/identity/hub/custody/CustodyEditFlow.tsx +3 -3
  54. package/src/identity/hub/identityHubReducer.ts +21 -0
  55. package/src/identity/hub/profile/effects.ts +16 -3
  56. package/src/identity/hub/restore/RestoreFlow.tsx +43 -6
  57. package/src/identity/hub/restore/apply.ts +12 -1
  58. package/src/identity/hub/restore/recovery.ts +11 -1
  59. package/src/identity/hub/restore/resolve.ts +1 -1
  60. package/src/identity/hub/restore/useRestoreEffects.ts +4 -6
  61. package/src/identity/hub/shared/components/DetailsScreen.tsx +4 -1
  62. package/src/identity/hub/shared/components/IdentitySummary.tsx +97 -53
  63. package/src/identity/hub/shared/components/MenuScreen.tsx +18 -15
  64. package/src/identity/hub/shared/components/UnlinkedIdentityScreen.tsx +1 -1
  65. package/src/identity/hub/shared/components/menuFlagsFromReconciliation.ts +8 -12
  66. package/src/identity/hub/shared/effects/sync.ts +16 -3
  67. package/src/identity/hub/shared/model/copy.ts +2 -4
  68. package/src/identity/hub/transfer/effects.ts +15 -2
  69. package/src/identity/hub/useIdentityHubContinuity.ts +145 -23
  70. package/src/identity/hub/useIdentityHubController.ts +5 -1
  71. package/src/identity/hub/useIdentityHubSideEffects.ts +2 -4
  72. package/src/mcp/manager.ts +1 -1
  73. package/src/models/ModelPicker.tsx +211 -74
  74. package/src/models/huggingface.ts +180 -2
  75. package/src/models/llamacpp.ts +261 -17
  76. package/src/models/llamacppPreflight.ts +16 -12
  77. package/src/models/modelPickerOptions.ts +57 -38
  78. package/src/providers/anthropic.ts +36 -5
  79. package/src/providers/contracts.ts +10 -1
  80. package/src/providers/gemini.ts +29 -3
  81. package/src/providers/openai-chat.ts +131 -11
  82. package/src/providers/openai-responses-format.ts +29 -8
  83. package/src/providers/openai-responses.ts +41 -11
  84. package/src/providers/registry.ts +1 -0
  85. package/src/runtime/toolExecution.ts +4 -3
  86. package/src/runtime/turn.ts +61 -30
  87. package/src/storage/config.ts +1 -0
  88. package/src/storage/sessions.ts +14 -2
  89. package/src/tools/changeDirectoryTool.ts +1 -1
  90. package/src/tools/contracts.ts +10 -0
  91. package/src/tools/deleteFileTool.ts +1 -1
  92. package/src/tools/editTool.ts +1 -1
  93. package/src/tools/listDirectoryTool.ts +1 -1
  94. package/src/tools/listSkillFilesTool.ts +77 -0
  95. package/src/tools/listSkillsTool.ts +68 -0
  96. package/src/tools/mcpResourceTools.ts +2 -2
  97. package/src/tools/privateContinuityReadTool.ts +1 -1
  98. package/src/tools/readSkillTool.ts +107 -0
  99. package/src/tools/readTool.ts +1 -1
  100. package/src/tools/registry.ts +6 -0
  101. package/src/tools/writeFileTool.ts +22 -2
  102. package/src/ui/Spinner.tsx +15 -3
  103. package/src/ui/theme.ts +2 -0
  104. package/src/utils/images.ts +140 -0
  105. package/src/utils/messages.ts +2 -0
  106. package/src/identity/continuity/localBackup.ts +0 -249
  107. package/src/identity/continuity/zipWriter.ts +0 -95
  108. package/src/identity/hub/continuity/index.ts +0 -7
  109. package/src/identity/hub/ens/index.ts +0 -11
  110. 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: rows.slice(firstVisible),
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, Math.floor(viewport / 2))
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: rows.slice(startIndex, endIndex),
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 1 + wrappedLineCount(row.content, contentWidth)
219
+ return userRowLineCount(row.content, contentWidth)
195
220
  case 'assistant':
196
- return 1 + wrappedLineCount([row.content, row.liveTail ?? ''].filter(Boolean).join('\n'), contentWidth)
221
+ return assistantRowLineCount(row.content, row.liveTail ?? '', contentWidth, Boolean(row.streaming))
197
222
  case 'thinking':
198
- return row.expanded
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 reasoningPreview(row: Extract<MessageRow, { role: 'thinking' }>): string {
213
- const normalized = [row.content, row.liveTail ?? ''].filter(Boolean).join('').replace(/\s+/g, ' ').trim()
214
- if (!normalized) return 'thinking...'
215
- if (normalized.length <= 120) return normalized
216
- return `${normalized.slice(0, 117)}...`
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 Once', hint: 'Approve only this command execution' },
75
+ { value: 'allow-once', label: 'Allow once', hint: 'Approve only this command execution' },
76
76
  {
77
77
  value: 'allow-command-project',
78
- label: 'Allow Exact Command',
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} Commands` : 'Allow Command Family',
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
- ? 'server marks this tool as destructive'
94
+ ? 'Server marks this tool as destructive'
95
95
  : request.openWorld
96
- ? 'server marks this tool as open-world'
96
+ ? 'Server marks this tool as open-world'
97
97
  : request.readOnly
98
- ? 'server marks this tool as read-only'
99
- : 'server did not mark this tool read-only'
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: 'allow once', hint: risk },
102
- { value: 'allow-mcp-tool-project', label: 'always allow this MCP tool', hint: request.toolKey },
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: `always allow ${request.serverName}`,
106
- hint: 'remember all tools from this MCP server for this project',
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: 'deny', hint: 'return a denial back to the model' },
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: 'delete this file', hint: 'approve this deletion only' },
116
- { value: 'deny', label: 'deny', hint: 'keep the file unchanged' },
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: 'allow once', hint: `read ${request.file}` },
123
- { value: 'deny', label: 'deny', hint: 'keep private continuity hidden' },
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 Once', hint: `Apply this edit to ${request.file}` },
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: '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 },
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
- ? 'always allow edits'
142
+ ? 'Always allow edits'
143
143
  : request.kind === 'write'
144
- ? 'always allow writes'
144
+ ? 'Always allow writes'
145
145
  : request.kind === 'cd'
146
- ? 'always allow directory changes'
147
- : 'always allow reads',
148
- hint: 'remember this tool kind for this project',
146
+ ? 'Always allow directory changes'
147
+ : 'Always allow reads',
148
+ hint: 'Remember this tool kind for this project',
149
149
  },
150
- { value: 'deny', label: 'deny', hint: 'return a denial back to the model' },
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
- return [
122
- ...rules.map(rule => ({
123
- value: rule,
124
- label: describeRule(rule),
125
- hint: describeRuleScope(rule),
126
- })),
127
- {
128
- value: CLEAR_ALL_VALUE,
129
- label: 'Remove all saved rules',
130
- hint: 'Clear all remembered permissions for this project',
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="Loading projects and directories...">
52
- <Spinner label="loading sessions..." />
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 Chat Logs?"
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: 'back to sessions' },
81
- { value: 'clear', label: 'clear all chat logs', hint: 'cannot be undone' },
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 Chat Logs',
162
- hint: 'removes saved chats and resume context',
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', prefix: '5.', label: 'Never mind' },
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 title="Reset Local Data?" subtitle="Deletes this machine's ethagent data. Models and onchain records stay." footer="enter select · esc cancel">
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: 'Reset Local Data', hint: 'Delete local ethagent data now' },
38
- { value: 'cancel', label: 'Cancel', hint: 'Leave local data unchanged' },
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={theme.accentPeriwinkle}>{title}</Text>
59
+ <Text color={sectionTitleColor(tone)}>{title}</Text>
51
60
  {lines.map(line => (
52
- <Text key={line} color={theme.textSubtle}>- {line}</Text>
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 Promise.resolve({ ok: false, error: 'no default open command for this platform' })
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
+ }