1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
|
local micro = import("micro")
local config = import("micro/config")
local filepath = import("filepath")
local shell = import("micro/shell")
local util = import("micro/util")
local os = import("os")
-- Store the last Claude edit position we navigated to
-- Format: {path = "/path/to/file", line = 42}
local lastClaudePosition = nil
-- Store the last selected Claude session
local lastClaudeSession = nil
function claudePrevPosition(bp, args)
-- Use last tracked position if available, otherwise use current cursor position
local queryPath, queryLine
if lastClaudePosition then
queryPath = lastClaudePosition.path
queryLine = lastClaudePosition.line
else
queryPath = bp.Buf.Path
queryLine = bp.Cursor.Y + 1 -- Convert to 1-based
end
if not queryPath or queryPath == "" then
micro.InfoBar():Error("No file path - save the buffer first")
return
end
-- Call claude-last-position prev with the tracked position
local cmd = string.format("claude-last-position prev '%s' %d 2>&1", queryPath, queryLine)
local handle = io.popen(cmd)
if not handle then
micro.InfoBar():Error("Failed to run claude-last-position")
return
end
local output = handle:read("*a")
local success = handle:close()
-- If command failed (non-zero exit), we're at the beginning of history
if not success then
micro.InfoBar():Message("⏮️ At beginning of edit history")
return
end
-- Parse path:line format
local trimmed = output:match("^%s*(.-)%s*$")
-- Empty output means no history
if not trimmed or trimmed == "" then
micro.InfoBar():Message("⏮️ At beginning of edit history")
return
end
local path, line = trimmed:match("^(.+):(%d+)$")
if not path or not line then
micro.InfoBar():Error("Invalid response format: " .. output)
return
end
-- Update tracked position before navigating
lastClaudePosition = {path = path, line = tonumber(line)}
-- Navigate to the previous position
local newTab = OpenTabFromFile(bp, path, tonumber(line))
if newTab then
micro.InfoBar():Message(string.format("⏮️ Previous: %s:%d", filepath.Base(path), tonumber(line)))
else
micro.InfoBar():Error("Failed to open: " .. path)
end
end
function claudeNextPosition(bp, args)
-- Use last tracked position if available, otherwise use current cursor position
local queryPath, queryLine
if lastClaudePosition then
queryPath = lastClaudePosition.path
queryLine = lastClaudePosition.line
else
queryPath = bp.Buf.Path
queryLine = bp.Cursor.Y + 1 -- Convert to 1-based
end
if not queryPath or queryPath == "" then
micro.InfoBar():Error("No file path - save the buffer first")
return
end
-- Call claude-last-position next with the tracked position
local cmd = string.format("claude-last-position next '%s' %d 2>&1", queryPath, queryLine)
local handle = io.popen(cmd)
if not handle then
micro.InfoBar():Error("Failed to run claude-last-position")
return
end
local output = handle:read("*a")
local success = handle:close()
-- If command failed (non-zero exit), we're at the end of history
if not success then
micro.InfoBar():Message("⏭️ At end of edit history")
return
end
-- Parse path:line format
local trimmed = output:match("^%s*(.-)%s*$")
-- Empty output means no history
if not trimmed or trimmed == "" then
micro.InfoBar():Message("⏭️ At end of edit history")
return
end
local path, line = trimmed:match("^(.+):(%d+)$")
if not path or not line then
micro.InfoBar():Error("Invalid response format: " .. output)
return
end
-- Update tracked position before navigating
lastClaudePosition = {path = path, line = tonumber(line)}
-- Navigate to the next position
local newTab = OpenTabFromFile(bp, path, tonumber(line))
if newTab then
micro.InfoBar():Message(string.format("⏭️ Next: %s:%d", filepath.Base(path), tonumber(line)))
else
micro.InfoBar():Error("Failed to open: " .. path)
end
end
function claudeClearSession(bp, args)
if lastClaudeSession then
local oldSession = lastClaudeSession
lastClaudeSession = nil
micro.InfoBar():Message("Cleared session selection: " .. oldSession)
else
micro.InfoBar():Message("No session was selected")
end
end
function sendToClaude(bp, args)
-- Get all abduco sessions that start with "claude-"
local handle = io.popen("abduco 2>/dev/null | awk '{print $NF}' | grep '^claude-'")
if not handle then
micro.InfoBar():Error("Failed to list abduco sessions")
return
end
local sessions = {}
for line in handle:lines() do
table.insert(sessions, line)
end
handle:close()
if #sessions == 0 then
micro.InfoBar():Error("No Claude abduco sessions found (sessions must start with 'claude-')")
return
end
-- Select session
local selectedSession
-- If we have a last selected session and it's still in the list, use it
if lastClaudeSession then
local found = false
for _, sess in ipairs(sessions) do
if sess == lastClaudeSession then
found = true
break
end
end
if found then
selectedSession = lastClaudeSession
micro.InfoBar():Message("Using last Claude session: " .. selectedSession)
else
-- Session no longer exists, clear it and inform user
micro.InfoBar():Message("Session '" .. lastClaudeSession .. "' no longer exists, selecting new session...")
lastClaudeSession = nil
end
end
-- If we don't have a selection yet, prompt for one
if not selectedSession then
if #sessions == 1 then
selectedSession = sessions[1]
micro.InfoBar():Message("Using Claude session: " .. selectedSession)
else
-- Multiple sessions - use gum to select
-- Check if gum exists
local gumCheck = io.popen("command -v gum 2>/dev/null")
local gumPath = gumCheck:read("*l")
gumCheck:close()
if not gumPath or gumPath == "" then
micro.InfoBar():Error("Multiple Claude sessions found, but gum is not installed")
return
end
-- Build gum command with sessions as arguments
local sessionArgs = table.concat(sessions, " ")
local gumCmd = string.format("gum choose --header 'Select Claude session:' %s", sessionArgs)
-- Use RunInteractiveShell for gum
local output, err = shell.RunInteractiveShell(gumCmd, false, true)
if err ~= nil then
micro.InfoBar():Error("Failed to run gum: " .. tostring(err))
return
end
selectedSession = output:match("^%s*(.-)%s*$") -- trim whitespace
if not selectedSession or selectedSession == "" then
micro.InfoBar():Message("No session selected")
return
end
end
end
-- Remember this selection for next time
lastClaudeSession = selectedSession
-- Build the initial message
local message
local cursor = bp.Cursor
local filePath = bp.Buf.Path
if cursor:HasSelection() then
-- Get selected text
local selectionBytes = cursor:GetSelection()
local selection = util.String(selectionBytes)
-- Get the line range of the selection
local startLine = cursor.CurSelection[1].Y + 1 -- Convert to 1-based
local endLine = cursor.CurSelection[2].Y + 1
local lineCount = endLine - startLine + 1
-- Build prompt with selection context
-- Only include text for short selections (< 10 lines)
if lineCount < 10 then
message = string.format("Look at %s lines %d–%d, I have the following comments:\n\n%s",
filePath, startLine, endLine, selection)
else
message = string.format("Look at %s lines %d–%d, I have the following comments:\n\n",
filePath, startLine, endLine)
end
else
-- No selection - use args if provided, otherwise error
if #args > 0 then
message = table.concat(args, " ")
else
micro.InfoBar():Error("No selection and no message provided")
return
end
end
-- Write message to temp file with unique name
-- Use _G.os.time() to access standard Lua os library (local 'os' is Micro's module)
local tmpfile = string.format("%s/micro-claude-msg-%d.txt", os.TempDir(), _G.os.time())
local f = io.open(tmpfile, "w")
if not f then
micro.InfoBar():Error("Failed to create temp file")
return
end
-- Add two empty lines at the end for cursor positioning
local messageWithEmptyLines = message .. "\n\n"
f:write(messageWithEmptyLines)
f:close()
-- Count lines in message to position cursor at the last empty line
local lineCount = 1
for _ in message:gmatch("\n") do
lineCount = lineCount + 1
end
local cursorLine = lineCount + 2 -- Position at the second empty line we added
-- Spawn micro to edit the message with cursor at the end
micro.InfoBar():Message("Opening message in micro for editing...")
local editCmd = string.format("micro '%s:%d'", tmpfile, cursorLine)
local output, err = shell.RunInteractiveShell(editCmd, false, false)
if err ~= nil then
micro.InfoBar():Error("Failed to edit message: " .. tostring(err))
os.Remove(tmpfile)
return
end
-- Read the edited message back
local readFile = io.open(tmpfile, "r")
if not readFile then
micro.InfoBar():Error("Failed to read edited message")
os.Remove(tmpfile)
return
end
local editedMessage = readFile:read("*a")
readFile:close()
if not editedMessage or editedMessage == "" then
micro.InfoBar():Message("Empty message, cancelled")
os.Remove(tmpfile)
return
end
-- Send the message to abduco session using -p (pass-through) flag
-- We use printf to send the message followed by literal 0x0D byte (CR/Enter)
local sendCmd = string.format("(cat '%s'; printf '\\x0d') | abduco -p '%s'", tmpfile, selectedSession)
local sendOutput, sendErr = shell.ExecCommand("sh", "-c", sendCmd)
-- Clean up temp file
os.Remove(tmpfile)
if sendErr ~= nil then
micro.InfoBar():Error("Failed to send message: " .. tostring(sendErr))
else
micro.InfoBar():Message("✓ Sent to " .. selectedSession)
end
end
|