openclacky 1.3.3 → 1.3.4

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/docs/rich_ui_guide.md +277 -0
  4. data/docs/rich_ui_refactor_plan.md +396 -0
  5. data/lib/clacky/agent/llm_caller.rb +10 -4
  6. data/lib/clacky/agent/session_serializer.rb +3 -2
  7. data/lib/clacky/agent.rb +3 -2
  8. data/lib/clacky/agent_config.rb +2 -14
  9. data/lib/clacky/api_extension.rb +262 -0
  10. data/lib/clacky/api_extension_loader.rb +156 -0
  11. data/lib/clacky/cli.rb +93 -3
  12. data/lib/clacky/client.rb +38 -13
  13. data/lib/clacky/default_agents/_panels/git/panel.js +1 -1
  14. data/lib/clacky/default_agents/_panels/time_machine/panel.js +1 -1
  15. data/lib/clacky/default_skills/media-gen/SKILL.md +9 -6
  16. data/lib/clacky/idle_compression_timer.rb +3 -1
  17. data/lib/clacky/locales/en.rb +26 -0
  18. data/lib/clacky/locales/i18n.rb +26 -0
  19. data/lib/clacky/locales/zh.rb +26 -0
  20. data/lib/clacky/rich_ui/components/base_component.rb +50 -0
  21. data/lib/clacky/rich_ui/components/dialogs/approval_dialog.rb +142 -0
  22. data/lib/clacky/rich_ui/components/dialogs/config_menu_dialog.rb +106 -0
  23. data/lib/clacky/rich_ui/components/dialogs/form_dialog.rb +128 -0
  24. data/lib/clacky/rich_ui/components/sidebar.rb +119 -0
  25. data/lib/clacky/rich_ui/components/sidebar_panels.rb +134 -0
  26. data/lib/clacky/rich_ui/components/status_view.rb +58 -0
  27. data/lib/clacky/rich_ui/components/thinking_live_view.rb +79 -0
  28. data/lib/clacky/rich_ui/entry_tracker.rb +56 -0
  29. data/lib/clacky/rich_ui/layout_adapter.rb +16 -0
  30. data/lib/clacky/rich_ui/progress_handle_adapter.rb +24 -0
  31. data/lib/clacky/rich_ui/rich_ui_controller.rb +868 -0
  32. data/lib/clacky/rich_ui/shell/rich_agent_shell.rb +184 -0
  33. data/lib/clacky/rich_ui/view_renderer.rb +291 -0
  34. data/lib/clacky/rich_ui.rb +57 -0
  35. data/lib/clacky/rich_ui_controller.rb +3 -1549
  36. data/lib/clacky/server/api_extension_dispatcher.rb +120 -0
  37. data/lib/clacky/server/http_server.rb +150 -103
  38. data/lib/clacky/server/session_registry.rb +1 -1
  39. data/lib/clacky/shell_hook_loader.rb +1 -1
  40. data/lib/clacky/tools/edit.rb +14 -2
  41. data/lib/clacky/ui2/ui_controller.rb +7 -0
  42. data/lib/clacky/version.rb +1 -1
  43. data/lib/clacky/web/app.css +56 -59
  44. data/lib/clacky/web/app.js +65 -7
  45. data/lib/clacky/web/components/onboard.js +18 -2
  46. data/lib/clacky/web/core/aside.js +8 -3
  47. data/lib/clacky/web/core/ext.js +1 -1
  48. data/lib/clacky/web/features/skills/store.js +30 -2
  49. data/lib/clacky/web/features/skills/view.js +32 -1
  50. data/lib/clacky/web/features/workspace/view.js +1 -1
  51. data/lib/clacky/web/i18n.js +32 -20
  52. data/lib/clacky/web/index.html +9 -17
  53. data/lib/clacky/web/sessions.js +286 -28
  54. data/lib/clacky/web/settings.js +109 -111
  55. data/lib/clacky/web/ws-dispatcher.js +7 -3
  56. data/lib/clacky.rb +17 -2
  57. metadata +38 -2
  58. data/lib/clacky/media/output_dir.rb +0 -43
@@ -0,0 +1,396 @@
1
+ # RichUI Controller Refactoring Plan
2
+
3
+ > Goal: Learn from UI2's MVC layering, componentization, and id-based content management to refactor `lib/clacky/rich_ui_controller.rb` (2336 lines, 12+ classes) into a clear, single-responsibility, maintainable modular architecture.
4
+
5
+ ---
6
+
7
+ ## I. Current State Diagnosis
8
+
9
+ ### 1.1 Core Problems
10
+
11
+ | Problem | Current State | Impact |
12
+ |---------|---------------|--------|
13
+ | **Single file too large** | 2336 lines, 12+ classes (Shell, Sidebar, 3 Panels, StatusView, ThinkingLiveView, UIController, 3 Dialogs, 2 Adapters) | High code conflict rate, difficult code review, steep onboarding cost |
14
+ | **No MVC layering** | Rendering logic, layout coordinates, business state, and event callbacks all mixed together | Cannot unit-test rendering logic; changing one part may ripple through everything |
15
+ | **No component system** | All output is inline string concatenation (`"#{AnsiCode.color(:green)}✓#{reset}"`) | Style leakage, hard to maintain uniformly, not reusable |
16
+ | **Centralized monkey patches** | Patches for `RubyRich::Viewport`, `RubyRich::Transcript`, `RubyRich::Markdown::TerminalRenderer` crammed at the top of the file | Patches entangled with business code; concentrated risk when upgrading the gem |
17
+ | **Deep coupling with gem internals** | Extensive `instance_variable_get(:@callbacks)`, `instance_variable_set(:@on_interrupt, nil)` | RubyRich internal refactoring causes breakage — fragile external dependency |
18
+ | **Dialogs embedded inline** | `ConfigMenuDialog`, `FormDialog`, `ApprovalDialog` defined in the same file | Dialog logic growth further bloats file size |
19
+ | **No id-based content management** | Relies on `ruby_rich`'s `transcript.store.entries`; no custom OutputBuffer | Cannot precisely `replace`/`remove` non-tail content; lacks commit dedup mechanism |
20
+ | **Theme hardcoded** | `RubyRich::Theme.whale_dark` hardwired in `RichUIController#initialize` | Users cannot switch themes; not interoperable with UI2's theme system |
21
+ | **Progress is an adapter wrapper** | `ProgressHandleAdapter` just wraps ruby_rich native handles; no UI2 v2 semantics (owned handle, stack, quiet_on_fast_finish) | Concurrent progress contention; fast-finish unsupported |
22
+
23
+ ### 1.2 Key Gaps vs. UI2
24
+
25
+ ```
26
+ UI2 Architecture (mature) RichUI Architecture (to be refactored)
27
+ ───────────────────────────────── ─────────────────────────────────
28
+ UIController (coordination, thin) RichUIController (coordination + rendering + layout, thick)
29
+ ├── ViewRenderer (view dispatch) └── no counterpart, directly manipulates strings
30
+ │ ├── MessageComponent └── no component, inline concatenation
31
+ │ ├── ToolComponent └── no component, inline concatenation
32
+ │ └── CommonComponent └── no component, inline concatenation
33
+ ├── LayoutManager (layout engine) └── no counterpart, relies on RubyRich::Layout
34
+ │ └── OutputBuffer (id-based) └── no counterpart, relies on transcript.entries
35
+ ├── ScreenBuffer (ANSI primitives) └── no counterpart, encapsulated by ruby_rich
36
+ ├── InputArea (input editor) └── RubyRich::Composer (external, but intruded via ivar)
37
+ └── ThemeManager (theme system) └── hardcoded Theme.whale_dark
38
+ ```
39
+
40
+ ---
41
+
42
+ ## II. Refactoring Goals
43
+
44
+ 1. **File splitting**: Single file → multi-file modular, each class in its own file
45
+ 2. **MVC layering**: Introduce `ViewRenderer` + `Components` + `LayoutAdapter` layers
46
+ 3. **Componentization**: Extract Panel, Dialog, Status as independent Components
47
+ 4. **Decouple from gem**: Move monkey patches into `extensions/`; reduce `instance_variable_get`
48
+ 5. **id-based content management** (optional enhancement): Wrap a lightweight id tracking layer on top of RubyRich Transcript
49
+ 6. **Theme interoperability**: Reuse or bridge UI2's `ThemeManager` so `--theme` takes effect
50
+
51
+ ---
52
+
53
+ ## III. Target Directory Structure
54
+
55
+ ```
56
+ lib/clacky/
57
+ ├── rich_ui.rb # Entry file (similar to ui2.rb)
58
+ ├── rich_ui/
59
+ │ ├── rich_ui_controller.rb # Thin Controller (from 2336 lines → target < 300 lines)
60
+ │ ├── layout_adapter.rb # Layout coordination (replaces original LayoutAdapter)
61
+ │ ├── progress_handle_adapter.rb # Progress adapter (existing, retained)
62
+ │ │
63
+ │ ├── components/ # View components (similar to ui2/components/)
64
+ │ │ ├── base_component.rb # Base class: provides muted, colored, truncate, etc.
65
+ │ │ ├── message_component.rb # Message rendering (user/assistant/system)
66
+ │ │ ├── tool_component.rb # Tool call/result/error rendering
67
+ │ │ ├── common_component.rb # Progress/success/error/warning rendering
68
+ │ │ ├── welcome_banner.rb # Welcome banner (reuse UI2 or independent impl)
69
+ │ │ ├── thinking_live_view.rb # Thinking area (original ThinkingLiveView)
70
+ │ │ ├── status_view.rb # Bottom status bar (original RichStatusView)
71
+ │ │ ├── sidebar.rb # Sidebar container (original RichSidebar)
72
+ │ │ ├── sidebar_panels.rb # WorkPanel/TasksPanel/ContextPanel
73
+ │ │ └── dialogs/ # Dialog components
74
+ │ │ ├── base_dialog.rb # Shared wait/finish/key protocol
75
+ │ │ ├── config_menu_dialog.rb # Model configuration menu
76
+ │ │ ├── form_dialog.rb # Form input
77
+ │ │ └── approval_dialog.rb # Approval confirmation
78
+ │ │
79
+ │ ├── extensions/ # Extensions to ruby_rich (replaces top-level monkey patches)
80
+ │ │ ├── viewport_selection.rb # Viewport text selection and clipboard
81
+ │ │ ├── transcript_plain.rb # Transcript plain mode
82
+ │ │ └── markdown_table_adapter.rb # TerminalRenderer table adapter
83
+ │ │
84
+ │ └── shell/ # RichAgentShell and its configuration
85
+ │ └── rich_agent_shell.rb # Inherits RubyRich::AgentShell
86
+
87
+ └── cli.rb # Update require paths
88
+ ```
89
+
90
+ ---
91
+
92
+ ## IV. Phased Implementation Plan
93
+
94
+ ### Phase 1: File Splitting and Directory Setup (Low Risk, Pure Movement)
95
+
96
+ **Goal**: Split the 2336-line single file into multiple files by class, with zero behavioral change.
97
+
98
+ | Step | Action |
99
+ |------|--------|
100
+ | 1.1 | Create `lib/clacky/rich_ui/` directory and subdirectories |
101
+ | 1.2 | Move `RichAgentShell` into `rich_ui/shell/rich_agent_shell.rb` |
102
+ | 1.3 | Move `RichSidebar` + 3 Panels into `rich_ui/components/sidebar.rb` and `sidebar_panels.rb` |
103
+ | 1.4 | Move `ThinkingLiveView` into `rich_ui/components/thinking_live_view.rb` |
104
+ | 1.5 | Move `RichStatusView` into `rich_ui/components/status_view.rb` |
105
+ | 1.6 | Move 3 Dialogs into `rich_ui/components/dialogs/*.rb`, extract `BaseDialog` |
106
+ | 1.7 | Move `LayoutAdapter`, `ProgressHandleAdapter` into `rich_ui/` root directory |
107
+ | 1.8 | Create `lib/clacky/rich_ui.rb` entry file, unify requires |
108
+ | 1.9 | Update `cli.rb`: `require_relative "rich_ui_controller"` → `require_relative "rich_ui"` |
109
+
110
+ **Verification**: Run `--ui=rich`; all functionality identical.
111
+
112
+ ---
113
+
114
+ ### Phase 2: Monkey Patch Extraction and Decoupling (Medium Risk)
115
+
116
+ **Goal**: Convert file-top monkey patches into explicit extension modules, reducing coupling.
117
+
118
+ #### 2.1 Viewport Selection Extension
119
+
120
+ **Current state**:
121
+ ```ruby
122
+ class RubyRich::Viewport
123
+ alias_method :clacky_handle_event_without_text_selection, :handle_event
124
+ def handle_event(event_data, layout = nil)
125
+ # ... 30+ lines ...
126
+ end
127
+ end
128
+ ```
129
+
130
+ **After refactoring**: `lib/clacky/rich_ui/extensions/viewport_selection.rb`
131
+ ```ruby
132
+ module Clacky::RichUI::Extensions::ViewportSelection
133
+ def self.apply!
134
+ RubyRich::Viewport.class_eval do
135
+ # patch here
136
+ end
137
+ end
138
+ end
139
+
140
+ # Explicitly invoked in rich_ui.rb entry point:
141
+ Clacky::RichUI::Extensions::ViewportSelection.apply!
142
+ ```
143
+
144
+ Benefits:
145
+ - Patch code physically isolated from business logic
146
+ - `apply!` is explicit — when upgrading the gem you can see at a glance where conflicts might arise
147
+ - Can add `apply?` check (`method_defined?`) to avoid double loading
148
+
149
+ #### 2.2 Markdown Table Extension
150
+
151
+ Similarly moved into `extensions/markdown_table_adapter.rb`, with explicit `apply!`.
152
+
153
+ #### 2.3 Reducing `instance_variable_get`
154
+
155
+ **Current state** (multiple locations):
156
+ ```ruby
157
+ clacky = @shell.instance_variable_get(:@clacky_controller)
158
+ status = clacky.instance_variable_get(:@status)
159
+ ```
160
+
161
+ **After refactoring**: Provide formal accessors in `RichAgentShell`:
162
+ ```ruby
163
+ class RichAgentShell < RubyRich::AgentShell
164
+ attr_accessor :clacky_controller, :status, :work_label
165
+ # ...
166
+ end
167
+ ```
168
+
169
+ `RichStatusView` updated to:
170
+ ```ruby
171
+ def render
172
+ clacky = @shell.clacky_controller
173
+ return [""] unless clacky
174
+ status = clacky.status
175
+ # ...
176
+ end
177
+ ```
178
+
179
+ ---
180
+
181
+ ### Phase 3: Introduce ViewRenderer Component Layer (Medium Risk)
182
+
183
+ **Goal**: Following UI2's `ViewRenderer` + `Components` pattern, extract string concatenation logic into testable components.
184
+
185
+ #### 3.1 Create BaseComponent
186
+
187
+ ```ruby
188
+ # lib/clacky/rich_ui/components/base_component.rb
189
+ module Clacky::RichUI::Components
190
+ class BaseComponent
191
+ def muted(text)
192
+ "#{RubyRich::AnsiCode.color(:black, true)}#{text}#{RubyRich::AnsiCode.reset}"
193
+ end
194
+
195
+ def colored(text, color)
196
+ "#{RubyRich::AnsiCode.color(color, true)}#{text}#{RubyRich::AnsiCode.reset}"
197
+ end
198
+
199
+ def truncate(text, limit = 40)
200
+ # ...
201
+ end
202
+ end
203
+ end
204
+ ```
205
+
206
+ #### 3.2 Extract Sidebar Panels as Components
207
+
208
+ Original `RichWorkPanel#render` inline concatenation:
209
+ ```ruby
210
+ def render
211
+ lines = []
212
+ lines << @plan unless @plan.empty?
213
+ # ...
214
+ lines.join("\n")
215
+ end
216
+ ```
217
+
218
+ After refactoring:
219
+ ```ruby
220
+ class SidebarWorkPanel < BaseComponent
221
+ def render(plan:, activities:, tasks:, cost:)
222
+ lines = []
223
+ lines << plan if plan && !plan.empty?
224
+ # ...
225
+ lines << muted("#{tasks} tasks · $#{cost.round(4)}")
226
+ lines.join("\n")
227
+ end
228
+ end
229
+ ```
230
+
231
+ Panel classes only retain **state storage**; rendering is delegated to Components.
232
+
233
+ #### 3.3 Extract Dialog Rendering as Components
234
+
235
+ `ApprovalDialog`'s `render_content`, `render_choices`, `category_badge`, `colored`, `muted` all use `BaseComponent` methods.
236
+
237
+ After refactoring structure:
238
+ ```ruby
239
+ class ApprovalDialog
240
+ # Retain: event loop, wait/finish, key binding (these are interaction logic)
241
+ # Remove: string concatenation in render_content → delegate to ApprovalDialogRenderer
242
+ end
243
+
244
+ class ApprovalDialogRenderer < BaseComponent
245
+ def render(tool_name:, message:, params:, risk:, category:, selected_index:)
246
+ # Pure rendering logic, no side effects, unit-testable
247
+ end
248
+ end
249
+ ```
250
+
251
+ ---
252
+
253
+ ### Phase 4: Controller Slimming and Theme Interoperability (Medium Risk)
254
+
255
+ #### 4.1 Controller Retains Only Coordination Logic
256
+
257
+ Goal:
258
+ ```ruby
259
+ class RichUIController
260
+ include Clacky::UIInterface
261
+
262
+ def initialize(config = {})
263
+ @config = config
264
+ @shell = RichAgentShell.new(...)
265
+ @renderer = ViewRenderer.new # ← New
266
+ @sidebar = @shell.sidebar # ← Provided by Shell
267
+ @progress_stack = [] # ← Prepare for future v2
268
+ wire_callbacks
269
+ end
270
+
271
+ def show_tool_call(name, args)
272
+ output = @renderer.render_tool_call(name: name, args: args)
273
+ # ... delegate to ruby_rich for display
274
+ end
275
+ end
276
+ ```
277
+
278
+ #### 4.2 Reuse UI2 ThemeManager (Optional)
279
+
280
+ **Approach A (bridge)**: RichUI continues using `RubyRich::Theme`, but maps names like `whale_dark` to UI2 theme names.
281
+
282
+ **Approach B (unified)**: RichUI components accept UI2's `ThemeManager.current_theme`, calling `theme.format_symbol(:user)` instead of directly using `RubyRich::AnsiCode`.
283
+
284
+ Recommend **Approach A** (low intrusion), providing theme bridging in `BaseComponent`:
285
+ ```ruby
286
+ def theme
287
+ @theme ||= RubyRich::Theme.whale_dark
288
+ end
289
+ ```
290
+
291
+ ---
292
+
293
+ ### Phase 5: id-based Content Management (Optional Enhancement, High Risk)
294
+
295
+ UI2's `OutputBuffer` is the essence of its architecture, but RichUI relies on `ruby_rich`'s `Transcript` + `Viewport`; forcibly replacing them is costly.
296
+
297
+ **Recommended approach**: Introduce a lightweight **EntryTracker** in RichUI instead of a full OutputBuffer.
298
+
299
+ ```ruby
300
+ # lib/clacky/rich_ui/entry_tracker.rb
301
+ class EntryTracker
302
+ # Tracks message_id / block_id returned by ruby_rich
303
+ # Provides:
304
+ # - register(id, type:) → record id
305
+ # - update(id, content) → call @shell.append_to_message(id, content)
306
+ # - remove(id) → call @shell.transcript.remove_entry(id)
307
+ # - current_tool_id → top-of-stack tool_call id
308
+ end
309
+ ```
310
+
311
+ Thus `show_tool_call` / `show_tool_result` no longer rely on `@tool_ids.pop` (fragile stack semantics), but instead explicitly track by id.
312
+
313
+ ---
314
+
315
+ ## V. Key Design Decisions
316
+
317
+ ### Decision 1: Should Monkey Patches Be Retained?
318
+
319
+ **Conclusion**: Retain functionality, but move into `extensions/` directory with explicit `apply!`.
320
+
321
+ Rationale:
322
+ - The RubyRich gem does not provide extension points; without patching, selection/copy cannot be implemented
323
+ - Centralized management means only `extensions/` needs checking when upgrading the gem
324
+
325
+ ### Decision 2: Should Dialogs Use RubyRich Native Dialog?
326
+
327
+ **Conclusion**: Continue using custom Dialogs (`ConfigMenuDialog` etc.), but extract rendering layer into Components.
328
+
329
+ Rationale:
330
+ - RubyRich native Dialog capabilities are limited; current custom Dialogs already implement blocking wait and custom key bindings
331
+ - Extracting Renderer decouples Dialog interaction logic from rendering styles
332
+
333
+ ### Decision 3: Should Sidebar Panels Be Split into Separate Files?
334
+
335
+ **Conclusion**: 3 Panels (Work/Tasks/Context) merged into `sidebar_panels.rb`, but each as an independent class.
336
+
337
+ Rationale:
338
+ - Each Panel is only ~50 lines; separate files would be overly granular
339
+ - Merged while maintaining class-level independence, facilitating later Component extraction
340
+
341
+ ### Decision 4: Should ProgressHandle v2 Be Introduced?
342
+
343
+ **Conclusion**: Keep `ProgressHandleAdapter` bridging for now, but reserve interfaces for the future.
344
+
345
+ Rationale:
346
+ - ruby_rich's `start_progress` / `update` / `finish` semantics differ from UI2; forcing alignment has a wide blast radius
347
+ - Can reserve `@progress_stack` in `RichUIController` and implement true stack semantics later
348
+
349
+ ---
350
+
351
+ ## VI. Verification Checklist
352
+
353
+ After refactoring, the following functionality must be preserved 1:1:
354
+
355
+ - [ ] `--ui=rich` starts normally, displays welcome banner
356
+ - [ ] Full flow: user input → Agent response
357
+ - [ ] Tool call cards (start/complete/error)
358
+ - [ ] Thinking area real-time streaming
359
+ - [ ] Right sidebar (Work/Tasks/Context panels and F1-F4 switching)
360
+ - [ ] Bottom status bar (spinner, mode, model, task count, cost)
361
+ - [ ] Mouse selection + right-click copy
362
+ - [ ] Markdown table adaptive width
363
+ - [ ] `/config` dialog (menu + form)
364
+ - [ ] Tool approval dialog (ApprovalDialog)
365
+ - [ ] Model switch dialog
366
+ - [ ] Ctrl+C interrupt, Esc cancel stack, Tab switch mode
367
+ - [ ] `--theme` parameter takes effect (or at least does not error)
368
+
369
+ ---
370
+
371
+ ## VII. Estimated Effort
372
+
373
+ | Phase | Effort | Risk |
374
+ |-------|--------|------|
375
+ | Phase 1: File splitting | 2-3 hours | Low (pure movement + requires) |
376
+ | Phase 2: Patch extraction + decoupling | 3-4 hours | Medium (carefully verify ivar replacements) |
377
+ | Phase 3: ViewRenderer + Components | 4-6 hours | Medium (rendering logic extraction, compare output line by line) |
378
+ | Phase 4: Controller slimming + themes | 2-3 hours | Medium |
379
+ | Phase 5: EntryTracker (optional) | 4-6 hours | High (involves ruby_rich internal id mechanisms) |
380
+ | **Total (excluding Phase 5)** | **11-16 hours** | |
381
+
382
+ ---
383
+
384
+ ## VIII. Immediate First Step
385
+
386
+ If starting the refactoring now, recommended order:
387
+
388
+ 1. **Create directory structure**: `mkdir -p lib/clacky/rich_ui/{components/dialogs,extensions,shell}`
389
+ 2. **Phase 1 file splitting**: Cut classes one by one into new files, keep original file as compatibility shim (`require_relative "rich_ui"`)
390
+ 3. **Run smoke test**: `bundle exec ruby ./bin/openclacky agent --ui=rich`, confirm no require errors
391
+ 4. **Gradual replacement**: Verify after each class move; don't let changes pile up
392
+
393
+ ---
394
+
395
+ *Plan date: 2026-06-11*
396
+ *Reference baseline: UI2 architecture (`lib/clacky/ui2/` directory, docs/ui2-architecture.md)*
@@ -201,12 +201,18 @@ module Clacky
201
201
  sleep retry_delay
202
202
  retry
203
203
  else
204
- raise AgentError, "[LLM] Request timed out after #{max_retries} retries: #{e.message}"
204
+ raise AgentError, "[LLM] #{I18n.t("llm.error.request_timeout", retries: max_retries)}"
205
205
  end
206
206
 
207
- rescue Faraday::ConnectionFailed, Faraday::SSLError, Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
207
+ rescue Faraday::ConnectionFailed, Faraday::SSLError, Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::EPIPE => e
208
208
  retries += 1
209
209
 
210
+ # Errno::EPIPE means the underlying TCP socket is dead (server closed the
211
+ # connection after idle time). The Faraday connection object caches the
212
+ # socket, so retrying without resetting it would hit the same dead socket.
213
+ epipe = e.is_a?(Errno::EPIPE) || (e.respond_to?(:wrapped_exception) && e.wrapped_exception.is_a?(Errno::EPIPE))
214
+ @client.reset_connections! if epipe
215
+
210
216
  # Probing failure: primary still down — renew cooling-off and retry with fallback.
211
217
  if @config.probing?
212
218
  handle_probe_failure
@@ -230,7 +236,7 @@ module Clacky
230
236
  else
231
237
  # Don't show_error here — let the outer rescue block handle it to avoid duplicates.
232
238
  # Progress cleanup is the caller's responsibility (via its own ensure block).
233
- raise AgentError, "[LLM] Network connection failed after #{max_retries} retries: #{e.message}"
239
+ raise AgentError, "[LLM] #{I18n.t("llm.error.network_failed", retries: max_retries)}"
234
240
  end
235
241
 
236
242
  rescue RetryableError => e
@@ -268,7 +274,7 @@ module Clacky
268
274
  else
269
275
  # Don't show_error here — let the outer rescue block handle it to avoid duplicates.
270
276
  # Progress cleanup is the caller's responsibility (via its own ensure block).
271
- raise AgentError, "[LLM] Service unavailable after #{current_max} retries"
277
+ raise AgentError, "[LLM] #{I18n.t("llm.error.service_unavailable", retries: current_max)}"
272
278
  end
273
279
 
274
280
  rescue Clacky::BadRequestError => e
@@ -17,6 +17,7 @@ module Clacky
17
17
  @total_cost = session_data.dig(:stats, :total_cost_usd) || 0.0
18
18
  @working_dir = session_data[:working_dir]
19
19
  @created_at = session_data[:created_at]
20
+ @persisted_updated_at = session_data[:updated_at]
20
21
  @total_tasks = session_data.dig(:stats, :total_tasks) || 0
21
22
  # Restore cost_source so frontend knows if cost is reliable
22
23
  cost_src = session_data.dig(:stats, :cost_source)
@@ -152,7 +153,7 @@ module Clacky
152
153
  # @param status [Symbol] Status of the last task: :success, :error, or :interrupted
153
154
  # @param error_message [String] Error message if status is :error
154
155
  # @return [Hash] Session data ready for serialization
155
- def to_session_data(status: :success, error_message: nil)
156
+ def to_session_data(status: :success, error_message: nil, updated_at: nil, preserve_updated_at: false)
156
157
  stats_data = {
157
158
  total_tasks: @total_tasks,
158
159
  total_iterations: @iterations,
@@ -173,7 +174,7 @@ module Clacky
173
174
  name: @name,
174
175
  pinned: @pinned,
175
176
  created_at: @created_at,
176
- updated_at: Time.now.iso8601,
177
+ updated_at: (updated_at || (preserve_updated_at && @persisted_updated_at) || Time.now.iso8601).then { |v| v.is_a?(String) ? v : v.iso8601 },
177
178
  working_dir: @working_dir,
178
179
  source: @source.to_s, # "manual" | "cron" | "channel" | "setup"
179
180
  agent_profile: @agent_profile&.name || "", # "general" | "coding" | custom
data/lib/clacky/agent.rb CHANGED
@@ -252,7 +252,7 @@ module Clacky
252
252
  @name = new_name.to_s.strip
253
253
  end
254
254
 
255
- def run(user_input, files: [], display_text: nil)
255
+ def run(user_input, files: [], display_text: nil, created_at: nil)
256
256
  # Show the "thinking" indicator as early as possible so the user gets
257
257
  # immediate feedback after sending a message. Without this the UI stays
258
258
  # silent during synchronous setup work (system prompt assembly, file
@@ -361,7 +361,8 @@ module Clacky
361
361
  preview_path: f[:preview_path] || f["preview_path"] }
362
362
  end
363
363
 
364
- @history.append({ role: "user", content: user_content, task_id: task_id, created_at: Time.now.to_f,
364
+ created_at ||= Time.now.to_f
365
+ @history.append({ role: "user", content: user_content, task_id: task_id, created_at: created_at,
365
366
  display_text: display_text,
366
367
  display_files: display_files.empty? ? nil : display_files })
367
368
  @total_tasks += 1
@@ -165,8 +165,7 @@ module Clacky
165
165
  :memory_update_enabled, :skill_evolution,
166
166
  :max_running_agents, :max_idle_agents,
167
167
  :default_working_dir,
168
- :proxy_url,
169
- :media_output_dir
168
+ :proxy_url
170
169
 
171
170
  def initialize(options = {})
172
171
  @permission_mode = validate_permission_mode(options[:permission_mode])
@@ -224,15 +223,6 @@ module Clacky
224
223
  # a proxy. Leave nil to go direct.
225
224
  @proxy_url = options[:proxy_url]
226
225
 
227
- # User-configured directory where generated images / videos / audio
228
- # land when a /api/media/* call doesn't pass an explicit output_dir.
229
- # Final on-disk path is `<media_output_dir>/assets/generated/<file>`
230
- # (the `assets/generated/` suffix is fixed by Media::Base for stable
231
- # markdown/relative-path semantics across docs).
232
- # Leave nil → fall back to Dir.pwd (legacy behavior, preserved for
233
- # older configs that have no key set).
234
- @media_output_dir = options[:media_output_dir]
235
-
236
226
  # Per-session virtual model overlay.
237
227
  # When set, #current_model returns a *merged* hash (the resolved @models
238
228
  # entry merged with this overlay) without mutating the shared @models
@@ -425,7 +415,6 @@ module Clacky
425
415
  skill_evolution max_running_agents max_idle_agents
426
416
  default_working_dir
427
417
  proxy_url
428
- media_output_dir
429
418
  ].freeze
430
419
 
431
420
  # Serialize the current agent configuration to YAML.
@@ -445,8 +434,7 @@ module Clacky
445
434
  "max_running_agents" => @max_running_agents,
446
435
  "max_idle_agents" => @max_idle_agents,
447
436
  "default_working_dir" => @default_working_dir,
448
- "proxy_url" => @proxy_url,
449
- "media_output_dir" => @media_output_dir
437
+ "proxy_url" => @proxy_url
450
438
  }
451
439
  YAML.dump("settings" => settings, "models" => persistable_models)
452
440
  end