fantasy-cli 1.2.14 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +8 -1
- data/lib/gsd/agents/communication.rb +260 -0
- data/lib/gsd/agents/swarm.rb +341 -0
- data/lib/gsd/lsp/client.rb +394 -0
- data/lib/gsd/lsp/completion.rb +266 -0
- data/lib/gsd/lsp/diagnostics.rb +259 -0
- data/lib/gsd/lsp/hover.rb +244 -0
- data/lib/gsd/lsp/protocol.rb +434 -0
- data/lib/gsd/lsp/server_manager.rb +290 -0
- data/lib/gsd/lsp/symbols.rb +368 -0
- data/lib/gsd/plugins/api.rb +340 -0
- data/lib/gsd/plugins/hooks.rb +117 -95
- data/lib/gsd/plugins/hot_reload.rb +293 -0
- data/lib/gsd/plugins/registry.rb +273 -0
- data/lib/gsd/tui/agent_panel.rb +182 -0
- data/lib/gsd/tui/animations.rb +320 -0
- data/lib/gsd/tui/app.rb +442 -2
- data/lib/gsd/tui/colors.rb +15 -0
- data/lib/gsd/tui/effects.rb +263 -0
- data/lib/gsd/tui/header.rb +13 -5
- data/lib/gsd/tui/input_box.rb +10 -7
- data/lib/gsd/tui/mouse.rb +388 -0
- data/lib/gsd/tui/persistence.rb +192 -0
- data/lib/gsd/tui/session.rb +273 -0
- data/lib/gsd/tui/status_bar.rb +63 -15
- data/lib/gsd/tui/tab.rb +112 -0
- data/lib/gsd/tui/tab_manager.rb +191 -0
- data/lib/gsd/tui/transitions.rb +262 -0
- data/lib/gsd/version.rb +1 -1
- metadata +22 -1
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsd
|
|
4
|
+
module TUI
|
|
5
|
+
# Mouse - Suporte a mouse e eventos de clique
|
|
6
|
+
#
|
|
7
|
+
# Suporta:
|
|
8
|
+
# - Cliques (left, right, middle)
|
|
9
|
+
# - Scroll (up, down)
|
|
10
|
+
# - Drag
|
|
11
|
+
# - Hover
|
|
12
|
+
# - Seleção de texto
|
|
13
|
+
class Mouse
|
|
14
|
+
attr_reader :x, :y, :button, :event_type
|
|
15
|
+
|
|
16
|
+
BUTTONS = {
|
|
17
|
+
0 => :left,
|
|
18
|
+
1 => :middle,
|
|
19
|
+
2 => :right,
|
|
20
|
+
64 => :scroll_up,
|
|
21
|
+
65 => :scroll_down
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
def initialize
|
|
25
|
+
@enabled = false
|
|
26
|
+
@x = 0
|
|
27
|
+
@y = 0
|
|
28
|
+
@button = nil
|
|
29
|
+
@event_type = nil
|
|
30
|
+
@handlers = {}
|
|
31
|
+
@drag_start = nil
|
|
32
|
+
@selection = nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Habilita suporte ao mouse
|
|
36
|
+
def enable
|
|
37
|
+
return if @enabled
|
|
38
|
+
|
|
39
|
+
@enabled = true
|
|
40
|
+
# Habilita modo mouse do terminal (1000, 1002, 1006)
|
|
41
|
+
print "\e[?1000h\e[?1002h\e[?1006h" unless Gem.win_platform?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Desabilita suporte ao mouse
|
|
45
|
+
def disable
|
|
46
|
+
return unless @enabled
|
|
47
|
+
|
|
48
|
+
@enabled = false
|
|
49
|
+
print "\e[?1000l\e[?1002l\e[?1006l" unless Gem.win_platform?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Processa input de mouse
|
|
53
|
+
# Formato: \e[<button;x;yM (press) ou \e[<button;x;ym (release)
|
|
54
|
+
def parse(input)
|
|
55
|
+
return nil unless input.start_with?("\e[<")
|
|
56
|
+
|
|
57
|
+
# Extrai dados do formato SGR (1006)
|
|
58
|
+
match = input.match(/\e\[<(\d+);(\d+);(\d+)([Mm])/)
|
|
59
|
+
return nil unless match
|
|
60
|
+
|
|
61
|
+
button_code = match[1].to_i
|
|
62
|
+
@x = match[2].to_i
|
|
63
|
+
@y = match[3].to_i
|
|
64
|
+
action = match[4] == 'M' ? :press : :release
|
|
65
|
+
|
|
66
|
+
@button = BUTTONS[button_code] || :unknown
|
|
67
|
+
@event_type = determine_event_type(button_code, action)
|
|
68
|
+
|
|
69
|
+
MouseEvent.new(
|
|
70
|
+
x: @x,
|
|
71
|
+
y: @y,
|
|
72
|
+
button: @button,
|
|
73
|
+
type: @event_type,
|
|
74
|
+
raw: input
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Registra handler para evento
|
|
79
|
+
def on(event, &block)
|
|
80
|
+
@handlers[event] ||= []
|
|
81
|
+
@handlers[event] << block
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Processa evento
|
|
85
|
+
def handle(event)
|
|
86
|
+
return unless event.is_a?(MouseEvent)
|
|
87
|
+
|
|
88
|
+
# Atualiza estado de drag
|
|
89
|
+
update_drag_state(event)
|
|
90
|
+
|
|
91
|
+
# Chama handlers
|
|
92
|
+
handlers = @handlers[event.type] || []
|
|
93
|
+
handlers.each { |h| h.call(event) }
|
|
94
|
+
|
|
95
|
+
# Handler genérico
|
|
96
|
+
generic = @handlers[:any] || []
|
|
97
|
+
generic.each { |h| h.call(event) }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Verifica se coordenada está dentro de uma região
|
|
101
|
+
def in_region?(x, y, width, height)
|
|
102
|
+
@x >= x && @x < x + width && @y >= y && @y < y + height
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Verifica se está em uma linha específica
|
|
106
|
+
def on_line?(y)
|
|
107
|
+
@y == y
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Seleção atual
|
|
111
|
+
def selection
|
|
112
|
+
@selection
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Inicia seleção
|
|
116
|
+
def start_selection(x, y)
|
|
117
|
+
@selection = { start_x: x, start_y: y, end_x: x, end_y: y }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Atualiza seleção
|
|
121
|
+
def update_selection(x, y)
|
|
122
|
+
return unless @selection
|
|
123
|
+
@selection[:end_x] = x
|
|
124
|
+
@selection[:end_y] = y
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Finaliza seleção
|
|
128
|
+
def end_selection
|
|
129
|
+
sel = @selection
|
|
130
|
+
@selection = nil
|
|
131
|
+
sel
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Estado de drag
|
|
135
|
+
def dragging?
|
|
136
|
+
@drag_start && !@drag_start[:ended]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Posição inicial do drag
|
|
140
|
+
def drag_start
|
|
141
|
+
@drag_start
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def determine_event_type(button_code, action)
|
|
147
|
+
case button_code
|
|
148
|
+
when 64
|
|
149
|
+
:scroll_up
|
|
150
|
+
when 65
|
|
151
|
+
:scroll_down
|
|
152
|
+
else
|
|
153
|
+
action == :press ? :click : :release
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def update_drag_state(event)
|
|
158
|
+
case event.type
|
|
159
|
+
when :click
|
|
160
|
+
@drag_start = { x: event.x, y: event.y, button: event.button, ended: false }
|
|
161
|
+
when :release
|
|
162
|
+
@drag_start[:ended] = true if @drag_start
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# MouseEvent - Representa um evento de mouse
|
|
168
|
+
class MouseEvent
|
|
169
|
+
attr_reader :x, :y, :button, :type, :raw
|
|
170
|
+
|
|
171
|
+
def initialize(x:, y:, button:, type:, raw: nil)
|
|
172
|
+
@x = x
|
|
173
|
+
@y = y
|
|
174
|
+
@button = button
|
|
175
|
+
@type = type
|
|
176
|
+
@raw = raw
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Verifica se é clique esquerdo
|
|
180
|
+
def left_click?
|
|
181
|
+
@button == :left && @type == :click
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Verifica se é clique direito
|
|
185
|
+
def right_click?
|
|
186
|
+
@button == :right && @type == :click
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Verifica se é scroll
|
|
190
|
+
def scroll?
|
|
191
|
+
@type == :scroll_up || @type == :scroll_down
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Direção do scroll
|
|
195
|
+
def scroll_direction
|
|
196
|
+
return nil unless scroll?
|
|
197
|
+
@type == :scroll_up ? :up : :down
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Converte para string legível
|
|
201
|
+
def to_s
|
|
202
|
+
"#{type} at (#{x},#{y}) with #{button}"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# ClickableRegion - Região clicável na tela
|
|
207
|
+
class ClickableRegion
|
|
208
|
+
attr_reader :x, :y, :width, :height, :id, :data
|
|
209
|
+
|
|
210
|
+
def initialize(x:, y:, width:, height:, id:, data: nil, &block)
|
|
211
|
+
@x = x
|
|
212
|
+
@y = y
|
|
213
|
+
@width = width
|
|
214
|
+
@height = height
|
|
215
|
+
@id = id
|
|
216
|
+
@data = data
|
|
217
|
+
@callback = block
|
|
218
|
+
@hover = false
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Verifica se ponto está dentro da região
|
|
222
|
+
def contains?(px, py)
|
|
223
|
+
px >= @x && px < @x + @width && py >= @y && py < @y + @height
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Handler de clique
|
|
227
|
+
def on_click(event)
|
|
228
|
+
@callback&.call(event, self)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Handler de hover
|
|
232
|
+
def on_hover(event)
|
|
233
|
+
@hover = true
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Handler de leave
|
|
237
|
+
def on_leave
|
|
238
|
+
@hover = false
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Está com hover?
|
|
242
|
+
def hover?
|
|
243
|
+
@hover
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Renderiza com highlight se hover
|
|
247
|
+
def render(content, theme: nil)
|
|
248
|
+
t = theme || Colors.theme
|
|
249
|
+
if @hover
|
|
250
|
+
"#{t[:accent]}#{content}#{Colors::RESET}"
|
|
251
|
+
else
|
|
252
|
+
content
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# RegionManager - Gerencia regiões clicáveis
|
|
258
|
+
class RegionManager
|
|
259
|
+
def initialize
|
|
260
|
+
@regions = []
|
|
261
|
+
@mutex = Mutex.new
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Adiciona região
|
|
265
|
+
def add(region)
|
|
266
|
+
@mutex.synchronize { @regions << region }
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Remove região
|
|
270
|
+
def remove(id)
|
|
271
|
+
@mutex.synchronize { @regions.delete_if { |r| r.id == id } }
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Limpa todas
|
|
275
|
+
def clear
|
|
276
|
+
@mutex.synchronize { @regions.clear }
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Encontra região em coordenada
|
|
280
|
+
def find_at(x, y)
|
|
281
|
+
@mutex.synchronize do
|
|
282
|
+
@regions.find { |r| r.contains?(x, y) }
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Processa evento de mouse
|
|
287
|
+
def handle_event(event)
|
|
288
|
+
region = find_at(event.x, event.y)
|
|
289
|
+
|
|
290
|
+
case event.type
|
|
291
|
+
when :click
|
|
292
|
+
region&.on_click(event)
|
|
293
|
+
when :scroll_up, :scroll_down
|
|
294
|
+
region&.on_scroll(event)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Atualiza hover states
|
|
298
|
+
update_hover_states(event)
|
|
299
|
+
|
|
300
|
+
region
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Lista todas regiões
|
|
304
|
+
def all
|
|
305
|
+
@mutex.synchronize { @regions.dup }
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
private
|
|
309
|
+
|
|
310
|
+
def update_hover_states(event)
|
|
311
|
+
@regions.each do |r|
|
|
312
|
+
was_hover = r.hover?
|
|
313
|
+
is_hover = r.contains?(event.x, event.y)
|
|
314
|
+
|
|
315
|
+
if !was_hover && is_hover
|
|
316
|
+
r.on_hover(event)
|
|
317
|
+
elsif was_hover && !is_hover
|
|
318
|
+
r.on_leave
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# MouseIntegration - Integração com App
|
|
325
|
+
class MouseIntegration
|
|
326
|
+
def initialize(app)
|
|
327
|
+
@app = app
|
|
328
|
+
@mouse = Mouse.new
|
|
329
|
+
@regions = RegionManager.new
|
|
330
|
+
setup_handlers
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def enable
|
|
334
|
+
@mouse.enable
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def disable
|
|
338
|
+
@mouse.disable
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def process(input)
|
|
342
|
+
event = @mouse.parse(input)
|
|
343
|
+
return nil unless event
|
|
344
|
+
|
|
345
|
+
@mouse.handle(event)
|
|
346
|
+
@regions.handle_event(event)
|
|
347
|
+
|
|
348
|
+
# Integração específica com App
|
|
349
|
+
handle_app_interaction(event)
|
|
350
|
+
|
|
351
|
+
event
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def add_clickable_region(x, y, width, height, id, data = nil, &block)
|
|
355
|
+
@regions.add(ClickableRegion.new(
|
|
356
|
+
x: x, y: y, width: width, height: height,
|
|
357
|
+
id: id, data: data, &block
|
|
358
|
+
))
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
private
|
|
362
|
+
|
|
363
|
+
def setup_handlers
|
|
364
|
+
@mouse.on(:scroll_up) do |event|
|
|
365
|
+
# Scroll up no output
|
|
366
|
+
@app.scroll_output(-3)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
@mouse.on(:scroll_down) do |event|
|
|
370
|
+
# Scroll down no output
|
|
371
|
+
@app.scroll_output(3)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
@mouse.on(:click) do |event|
|
|
375
|
+
# Verifica se clicou em elemento específico
|
|
376
|
+
if event.y == 0
|
|
377
|
+
# Header area - toggle palette
|
|
378
|
+
@app.toggle_palette if event.x < 20
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def handle_app_interaction(event)
|
|
384
|
+
# Delega para regiões ou comportamentos padrão
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module Gsd
|
|
7
|
+
module TUI
|
|
8
|
+
# Persistence - Camada de persistência para o TUI
|
|
9
|
+
#
|
|
10
|
+
# Gerencia:
|
|
11
|
+
# - Save/restore de estado do TUI
|
|
12
|
+
# - Persistência de conversas
|
|
13
|
+
# - Checkpoints
|
|
14
|
+
# - Auto-save
|
|
15
|
+
class Persistence
|
|
16
|
+
SESSIONS_DIR = File.expand_path('~/.gsd/sessions').freeze
|
|
17
|
+
|
|
18
|
+
attr_reader :sessions_dir
|
|
19
|
+
|
|
20
|
+
def initialize(sessions_dir: SESSIONS_DIR)
|
|
21
|
+
@sessions_dir = sessions_dir
|
|
22
|
+
ensure_directory
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Salva um estado de sessão
|
|
26
|
+
#
|
|
27
|
+
# @param session_id [String] ID da sessão
|
|
28
|
+
# @param data [Hash] Dados a serem salvos
|
|
29
|
+
# @return [String] Caminho do arquivo salvo
|
|
30
|
+
def save(session_id:, data:)
|
|
31
|
+
filepath = session_path(session_id)
|
|
32
|
+
|
|
33
|
+
File.write(filepath, JSON.pretty_generate(data))
|
|
34
|
+
filepath
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Carrega um estado de sessão
|
|
38
|
+
#
|
|
39
|
+
# @param session_id [String] ID da sessão
|
|
40
|
+
# @return [Hash, nil] Dados da sessão ou nil se não existir
|
|
41
|
+
def load(session_id:)
|
|
42
|
+
filepath = session_path(session_id)
|
|
43
|
+
return nil unless File.exist?(filepath)
|
|
44
|
+
|
|
45
|
+
JSON.parse(File.read(filepath), symbolize_names: true)
|
|
46
|
+
rescue JSON::ParserError => e
|
|
47
|
+
puts "Error loading session #{session_id}: #{e.message}"
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Lista todas as sessões disponíveis
|
|
52
|
+
#
|
|
53
|
+
# @return [Array<Hash>] Lista de metadados das sessões
|
|
54
|
+
def list_sessions
|
|
55
|
+
return [] unless Dir.exist?(@sessions_dir)
|
|
56
|
+
|
|
57
|
+
Dir.glob(File.join(@sessions_dir, '*.json')).map do |filepath|
|
|
58
|
+
begin
|
|
59
|
+
data = JSON.parse(File.read(filepath), symbolize_names: true)
|
|
60
|
+
{
|
|
61
|
+
id: File.basename(filepath, '.json'),
|
|
62
|
+
name: data[:name] || 'Unnamed Session',
|
|
63
|
+
created_at: data[:created_at],
|
|
64
|
+
updated_at: data[:updated_at],
|
|
65
|
+
message_count: data[:output]&.length || 0,
|
|
66
|
+
agent_count: data[:swarm_stats]&.dig(:total_agents) || 0
|
|
67
|
+
}
|
|
68
|
+
rescue => e
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
end.compact.sort_by { |s| s[:updated_at] || s[:created_at] || '' }.reverse
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Deleta uma sessão
|
|
75
|
+
#
|
|
76
|
+
# @param session_id [String] ID da sessão
|
|
77
|
+
def delete(session_id:)
|
|
78
|
+
filepath = session_path(session_id)
|
|
79
|
+
File.delete(filepath) if File.exist?(filepath)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Cria um checkpoint (snapshot) da sessão
|
|
83
|
+
#
|
|
84
|
+
# @param session_id [String] ID da sessão
|
|
85
|
+
# @param checkpoint_name [String] Nome do checkpoint
|
|
86
|
+
def create_checkpoint(session_id:, checkpoint_name:)
|
|
87
|
+
data = load(session_id: session_id)
|
|
88
|
+
return unless data
|
|
89
|
+
|
|
90
|
+
checkpoint_id = "#{session_id}_#{checkpoint_name}_#{Time.now.to_i}"
|
|
91
|
+
checkpoint_path = File.join(@sessions_dir, 'checkpoints', "#{checkpoint_id}.json")
|
|
92
|
+
|
|
93
|
+
FileUtils.mkdir_p(File.dirname(checkpoint_path))
|
|
94
|
+
File.write(checkpoint_path, JSON.pretty_generate(data))
|
|
95
|
+
|
|
96
|
+
checkpoint_id
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Restaura de um checkpoint
|
|
100
|
+
#
|
|
101
|
+
# @param checkpoint_id [String] ID do checkpoint
|
|
102
|
+
# @return [Hash, nil] Dados do checkpoint
|
|
103
|
+
def restore_checkpoint(checkpoint_id:)
|
|
104
|
+
checkpoint_path = File.join(@sessions_dir, 'checkpoints', "#{checkpoint_id}.json")
|
|
105
|
+
return nil unless File.exist?(checkpoint_path)
|
|
106
|
+
|
|
107
|
+
JSON.parse(File.read(checkpoint_path), symbolize_names: true)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Lista checkpoints de uma sessão
|
|
111
|
+
#
|
|
112
|
+
# @param session_id [String] ID da sessão
|
|
113
|
+
# @return [Array<Hash>] Lista de checkpoints
|
|
114
|
+
def list_checkpoints(session_id:)
|
|
115
|
+
pattern = File.join(@sessions_dir, 'checkpoints', "#{session_id}_*.json")
|
|
116
|
+
|
|
117
|
+
Dir.glob(pattern).map do |filepath|
|
|
118
|
+
filename = File.basename(filepath, '.json')
|
|
119
|
+
parts = filename.split('_')
|
|
120
|
+
|
|
121
|
+
{
|
|
122
|
+
id: filename,
|
|
123
|
+
name: parts[1..-2].join('_'),
|
|
124
|
+
timestamp: parts.last.to_i,
|
|
125
|
+
created_at: Time.at(parts.last.to_i).to_s
|
|
126
|
+
}
|
|
127
|
+
end.sort_by { |c| c[:timestamp] }.reverse
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Exporta sessão para formato legível (Markdown)
|
|
131
|
+
#
|
|
132
|
+
# @param session_id [String] ID da sessão
|
|
133
|
+
# @param format [Symbol] Formato (:markdown, :json)
|
|
134
|
+
# @return [String] Conteúdo exportado
|
|
135
|
+
def export(session_id:, format: :markdown)
|
|
136
|
+
data = load(session_id: session_id)
|
|
137
|
+
return nil unless data
|
|
138
|
+
|
|
139
|
+
case format
|
|
140
|
+
when :markdown
|
|
141
|
+
export_to_markdown(data)
|
|
142
|
+
when :json
|
|
143
|
+
JSON.pretty_generate(data)
|
|
144
|
+
else
|
|
145
|
+
JSON.pretty_generate(data)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def ensure_directory
|
|
152
|
+
FileUtils.mkdir_p(@sessions_dir)
|
|
153
|
+
FileUtils.mkdir_p(File.join(@sessions_dir, 'checkpoints'))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def session_path(session_id)
|
|
157
|
+
File.join(@sessions_dir, "#{session_id}.json")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def export_to_markdown(data)
|
|
161
|
+
lines = []
|
|
162
|
+
lines << "# Session: #{data[:name] || 'Unnamed'}"
|
|
163
|
+
lines << ""
|
|
164
|
+
lines << "**Created:** #{data[:created_at]}"
|
|
165
|
+
lines << "**Updated:** #{data[:updated_at]}"
|
|
166
|
+
lines << ""
|
|
167
|
+
lines << "## Conversation"
|
|
168
|
+
lines << ""
|
|
169
|
+
|
|
170
|
+
data[:output]&.each do |msg|
|
|
171
|
+
case msg[:type]
|
|
172
|
+
when :user
|
|
173
|
+
lines << "### User"
|
|
174
|
+
lines << ""
|
|
175
|
+
lines << msg[:text]
|
|
176
|
+
lines << ""
|
|
177
|
+
when :assistant
|
|
178
|
+
lines << "### Assistant"
|
|
179
|
+
lines << ""
|
|
180
|
+
lines << msg[:text]
|
|
181
|
+
lines << ""
|
|
182
|
+
when :system
|
|
183
|
+
lines << "*#{msg[:text]}*"
|
|
184
|
+
lines << ""
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
lines.join("\n")
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|