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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/docs/rich_ui_guide.md +277 -0
- data/docs/rich_ui_refactor_plan.md +396 -0
- data/lib/clacky/agent/llm_caller.rb +10 -4
- data/lib/clacky/agent/session_serializer.rb +3 -2
- data/lib/clacky/agent.rb +3 -2
- data/lib/clacky/agent_config.rb +2 -14
- data/lib/clacky/api_extension.rb +262 -0
- data/lib/clacky/api_extension_loader.rb +156 -0
- data/lib/clacky/cli.rb +93 -3
- data/lib/clacky/client.rb +38 -13
- data/lib/clacky/default_agents/_panels/git/panel.js +1 -1
- data/lib/clacky/default_agents/_panels/time_machine/panel.js +1 -1
- data/lib/clacky/default_skills/media-gen/SKILL.md +9 -6
- data/lib/clacky/idle_compression_timer.rb +3 -1
- data/lib/clacky/locales/en.rb +26 -0
- data/lib/clacky/locales/i18n.rb +26 -0
- data/lib/clacky/locales/zh.rb +26 -0
- data/lib/clacky/rich_ui/components/base_component.rb +50 -0
- data/lib/clacky/rich_ui/components/dialogs/approval_dialog.rb +142 -0
- data/lib/clacky/rich_ui/components/dialogs/config_menu_dialog.rb +106 -0
- data/lib/clacky/rich_ui/components/dialogs/form_dialog.rb +128 -0
- data/lib/clacky/rich_ui/components/sidebar.rb +119 -0
- data/lib/clacky/rich_ui/components/sidebar_panels.rb +134 -0
- data/lib/clacky/rich_ui/components/status_view.rb +58 -0
- data/lib/clacky/rich_ui/components/thinking_live_view.rb +79 -0
- data/lib/clacky/rich_ui/entry_tracker.rb +56 -0
- data/lib/clacky/rich_ui/layout_adapter.rb +16 -0
- data/lib/clacky/rich_ui/progress_handle_adapter.rb +24 -0
- data/lib/clacky/rich_ui/rich_ui_controller.rb +868 -0
- data/lib/clacky/rich_ui/shell/rich_agent_shell.rb +184 -0
- data/lib/clacky/rich_ui/view_renderer.rb +291 -0
- data/lib/clacky/rich_ui.rb +57 -0
- data/lib/clacky/rich_ui_controller.rb +3 -1549
- data/lib/clacky/server/api_extension_dispatcher.rb +120 -0
- data/lib/clacky/server/http_server.rb +150 -103
- data/lib/clacky/server/session_registry.rb +1 -1
- data/lib/clacky/shell_hook_loader.rb +1 -1
- data/lib/clacky/tools/edit.rb +14 -2
- data/lib/clacky/ui2/ui_controller.rb +7 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +56 -59
- data/lib/clacky/web/app.js +65 -7
- data/lib/clacky/web/components/onboard.js +18 -2
- data/lib/clacky/web/core/aside.js +8 -3
- data/lib/clacky/web/core/ext.js +1 -1
- data/lib/clacky/web/features/skills/store.js +30 -2
- data/lib/clacky/web/features/skills/view.js +32 -1
- data/lib/clacky/web/features/workspace/view.js +1 -1
- data/lib/clacky/web/i18n.js +32 -20
- data/lib/clacky/web/index.html +9 -17
- data/lib/clacky/web/sessions.js +286 -28
- data/lib/clacky/web/settings.js +109 -111
- data/lib/clacky/web/ws-dispatcher.js +7 -3
- data/lib/clacky.rb +17 -2
- metadata +38 -2
- 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]
|
|
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]
|
|
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]
|
|
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
|
-
|
|
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
|
data/lib/clacky/agent_config.rb
CHANGED
|
@@ -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
|