zuzu 0.2.1-java → 0.2.3-java

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.
@@ -0,0 +1,589 @@
1
+ # AGENTS.md — Zuzu App Development Reference
2
+
3
+ Comprehensive guide for AI coding assistants (Claude Code, OpenCode, etc.)
4
+ extending a Zuzu desktop application.
5
+
6
+ ---
7
+
8
+ ## What is Zuzu?
9
+
10
+ Zuzu is a JRuby desktop framework for local-first AI assistants.
11
+ An app consists of:
12
+
13
+ 1. **`app.rb`** — configure + register tools + launch (the only file you normally edit)
14
+ 2. **Glimmer DSL for SWT GUI** — native desktop window (chat area + input row)
15
+ 3. **Agent loop** — prompt-based ReAct loop using `<zuzu_tool_call>` XML tags
16
+ 4. **AgentFS** — SQLite-backed virtual filesystem sandboxed from the host OS
17
+ 5. **llamafile** — local LLM subprocess serving an OpenAI-compatible HTTP API
18
+
19
+ ---
20
+
21
+ ## Runtime requirements
22
+
23
+ | Component | Version |
24
+ |-----------|---------|
25
+ | JRuby | 10.0.3.0 |
26
+ | Java | 21+ |
27
+ | Bundler | any recent |
28
+
29
+ ```bash
30
+ rbenv install jruby-10.0.3.0
31
+ rbenv local jruby-10.0.3.0
32
+ bundle install
33
+ bundle exec zuzu start
34
+ ```
35
+
36
+ ---
37
+
38
+ ## Architecture overview
39
+
40
+ ```
41
+ app.rb
42
+ └── Zuzu.configure(...) # app_name, model path, port, extras
43
+ └── Zuzu::ToolRegistry.register # custom tools
44
+ └── Zuzu::App.launch! # starts llamafile + opens SWT window
45
+
46
+ Zuzu::App (Glimmer::UI::Application)
47
+ └── before_body: creates Store, AgentFS, Memory, LlmClient, Agent, Channels
48
+ └── body: SWT shell (chat display + input row + Admin Panel button)
49
+ └── send_message → Thread.new → Agent#process → async_exec (UI update)
50
+
51
+ Zuzu::Agent#process(user_message)
52
+ └── builds system prompt (BASE_PROMPT + registered tools + config.system_prompt_extras)
53
+ └── LlmClient#chat → parses <zuzu_tool_call> tags → ToolRegistry#execute
54
+ └── loops up to 10 times until no tool calls remain
55
+ └── stores final answer in Memory
56
+
57
+ Zuzu::ToolRegistry
58
+ └── global registry of Tool structs (name, description, schema, block)
59
+ └── tools are auto-listed in system prompt — no manual prompt editing
60
+
61
+ Zuzu::AgentFS
62
+ └── SQLite virtual filesystem (fs_inodes + fs_dentries + kv_store + tool_calls)
63
+ └── completely isolated from host filesystem
64
+
65
+ Zuzu::LlmClient
66
+ └── HTTP POST to http://127.0.0.1:<port>/v1/chat/completions
67
+ └── temperature 0.1 (deterministic, important for tool use)
68
+ └── strips llamafile EOS tokens from response
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Adding a custom tool
74
+
75
+ This is the most common extension point. Tools are Ruby blocks registered
76
+ before `Zuzu::App.launch!` in `app.rb`.
77
+
78
+ ### Minimal example
79
+
80
+ ```ruby
81
+ Zuzu::ToolRegistry.register(
82
+ 'current_time',
83
+ 'Get the current local date and time.',
84
+ { type: 'object', properties: {}, required: [] }
85
+ ) { |_args, _fs| Time.now.strftime('%Y-%m-%d %H:%M:%S %Z') }
86
+ ```
87
+
88
+ ### With arguments
89
+
90
+ ```ruby
91
+ Zuzu::ToolRegistry.register(
92
+ 'calculate',
93
+ 'Evaluate a simple Ruby arithmetic expression safely.',
94
+ {
95
+ type: 'object',
96
+ properties: {
97
+ expression: { type: 'string', description: 'e.g. "2 + 2 * 10"' }
98
+ },
99
+ required: ['expression']
100
+ }
101
+ ) do |args, _fs|
102
+ allowed = args['expression'].to_s.gsub(/[^0-9+\-*\/\(\)\.\s]/, '')
103
+ eval(allowed).to_s # safe: only digits and operators remain
104
+ rescue StandardError => e
105
+ "Error: #{e.message}"
106
+ end
107
+ ```
108
+
109
+ ### Using AgentFS inside a tool
110
+
111
+ ```ruby
112
+ Zuzu::ToolRegistry.register(
113
+ 'save_note',
114
+ 'Save a note to the AgentFS virtual filesystem.',
115
+ {
116
+ type: 'object',
117
+ properties: {
118
+ title: { type: 'string' },
119
+ content: { type: 'string' }
120
+ },
121
+ required: %w[title content]
122
+ }
123
+ ) do |args, fs|
124
+ path = "/notes/#{args['title'].downcase.gsub(/\s+/, '_')}.txt"
125
+ fs.write_file(path, args['content'])
126
+ "Saved note to #{path}"
127
+ end
128
+ ```
129
+
130
+ ### Using the KV store for persistent state
131
+
132
+ ```ruby
133
+ Zuzu::ToolRegistry.register(
134
+ 'remember',
135
+ 'Store a fact for future recall.',
136
+ {
137
+ type: 'object',
138
+ properties: {
139
+ key: { type: 'string' },
140
+ value: { type: 'string' }
141
+ },
142
+ required: %w[key value]
143
+ }
144
+ ) do |args, fs|
145
+ fs.kv_set(args['key'], args['value'])
146
+ "Remembered: #{args['key']} = #{args['value']}"
147
+ end
148
+ ```
149
+
150
+ ### Tool block rules
151
+
152
+ - Block signature is always `|args, fs|` (use `_args` or `_fs` if unused)
153
+ - `args` keys are **strings** (JSON-parsed): `args['name']` not `args[:name]`
154
+ - Return value is converted to String — keep it short and informative
155
+ - Raise or return an error string on failure — do not let exceptions propagate silently
156
+ - Tools are executed synchronously in the agent loop thread (not the UI thread)
157
+
158
+ ---
159
+
160
+ ## Customising the system prompt
161
+
162
+ ```ruby
163
+ Zuzu.configure do |c|
164
+ c.system_prompt_extras = <<~EXTRA
165
+ You are a personal assistant for a Ruby developer named Alex.
166
+ Always use metric units.
167
+ When writing code examples, prefer Ruby unless asked otherwise.
168
+ Never discuss competitor products.
169
+ EXTRA
170
+ end
171
+ ```
172
+
173
+ The agent's full system prompt is assembled at runtime as:
174
+ ```
175
+ BASE_PROMPT
176
+ + list of all registered tools (auto-generated from ToolRegistry)
177
+ + system_prompt_extras (if set)
178
+ ```
179
+
180
+ ---
181
+
182
+ ## AgentFS API reference
183
+
184
+ AgentFS is a sandboxed SQLite virtual filesystem. The agent can only
185
+ access files here — never the host machine's filesystem.
186
+
187
+ ### File operations
188
+
189
+ ```ruby
190
+ fs.write_file('/path/to/file.txt', 'content') # create or overwrite, returns true
191
+ fs.read_file('/path/to/file.txt') # returns String or nil if not found
192
+ fs.list_dir('/') # returns Array of entry names
193
+ fs.list_dir('/subdir') # names only, not full paths
194
+ fs.mkdir('/new/directory') # returns true, false if exists
195
+ fs.delete('/path/to/file.txt') # returns true, false if not found
196
+ fs.exists?('/path/to/file.txt') # returns true/false
197
+ fs.stat('/path/to/file.txt')
198
+ # returns: { 'id'=>1, 'type'=>'file', 'size'=>42,
199
+ # 'created_at'=><ms>, 'updated_at'=><ms> }
200
+ # returns nil if not found
201
+ # type is 'file' or 'dir'
202
+ ```
203
+
204
+ ### Key-value store
205
+
206
+ ```ruby
207
+ fs.kv_set('last_city', 'Tokyo')
208
+ fs.kv_get('last_city') # → 'Tokyo' or nil
209
+ fs.kv_delete('last_city')
210
+ fs.kv_list('prefix_') # → [{'key'=>'...','value'=>'...'}]
211
+ ```
212
+
213
+ ### Direct SQLite access (advanced)
214
+
215
+ ```ruby
216
+ store = Zuzu::Store.new # or access via fs.store
217
+ store.execute("INSERT INTO ...", [param1, param2])
218
+ store.query_all("SELECT * FROM kv_store WHERE key LIKE ?", ['user_%'])
219
+ store.query_one("SELECT value FROM kv_store WHERE key = ?", ['setting'])
220
+ # rows are Hashes with string keys
221
+ ```
222
+
223
+ ---
224
+
225
+ ## Glimmer DSL for SWT — working patterns
226
+
227
+ This section documents what has been tested and confirmed working on
228
+ JRuby 10.0.3.0 + Java 21 macOS. Glimmer DSL has gotchas — follow these patterns.
229
+
230
+ ### Shell layout (the correct full-app pattern)
231
+
232
+ ```ruby
233
+ shell {
234
+ grid_layout 1, false # single column, do not make equal width
235
+ text 'My App'
236
+ size 860, 620
237
+
238
+ scrolled_composite(:v_scroll) {
239
+ layout_data(:fill, :fill, true, true) # fills all available space
240
+ text(:multi, :read_only, :wrap) {
241
+ background :white
242
+ font name: 'Monospace', height: 13
243
+ }
244
+ }
245
+
246
+ composite {
247
+ layout_data(:fill, :fill, true, false) # false = don't grab vertical space
248
+ grid_layout 3, false # columns: input | button | button
249
+ text(:border) {
250
+ layout_data(:fill, :fill, true, false)
251
+ }
252
+ button { text 'Send' }
253
+ button { text 'Other' }
254
+ }
255
+ }
256
+ ```
257
+
258
+ ### layout_data argument form (always use this)
259
+
260
+ ```ruby
261
+ layout_data(:fill, :fill, true, false)
262
+ # args: (h_alignment, v_alignment, grab_excess_horizontal, grab_excess_vertical)
263
+ # :fill = fill available space
264
+ # :left, :right, :center, :beginning, :end = alignment options
265
+ # grab_excess_vertical: true = widget expands vertically. Use false for input rows.
266
+ ```
267
+
268
+ ### layout_data block form — BROKEN, do not use
269
+
270
+ ```ruby
271
+ # This throws InvalidKeywordError on JRuby 10 + Glimmer DSL SWT 4.30:
272
+ layout_data {
273
+ height_hint 28 # DO NOT USE
274
+ width_hint 200 # DO NOT USE
275
+ }
276
+ ```
277
+
278
+ ### Updating the chat display
279
+
280
+ ```ruby
281
+ # Always append, never replace (unless clearing):
282
+ current = @chat_display.swt_widget.get_text
283
+ @chat_display.swt_widget.set_text(current + "\n\nAssistant: " + response)
284
+ @chat_display.swt_widget.set_top_index(@chat_display.swt_widget.get_line_count)
285
+ ```
286
+
287
+ ### Opening a secondary window (Admin Panel pattern)
288
+
289
+ ```ruby
290
+ def open_settings_panel
291
+ panel = shell {
292
+ text 'Settings'
293
+ minimum_size 400, 300
294
+ grid_layout 1, false
295
+
296
+ label { text 'Settings Panel'; font height: 12, style: :bold }
297
+
298
+ button {
299
+ layout_data(:fill, :fill, true, false)
300
+ text 'Do Something'
301
+ on_widget_selected { perform_action }
302
+ }
303
+ }
304
+ panel.open
305
+ end
306
+ ```
307
+
308
+ ### Thread safety — mandatory rule
309
+
310
+ All SWT widget operations MUST happen on the SWT thread.
311
+ Always wrap UI updates from background threads in `async_exec`:
312
+
313
+ ```ruby
314
+ # CORRECT — LLM call on background thread, UI update on SWT thread:
315
+ Thread.new do
316
+ result = @agent.process(user_input)
317
+ async_exec { add_bubble(:assistant, result) }
318
+ end
319
+
320
+ # WRONG — will crash or produce unpredictable behaviour:
321
+ Thread.new do
322
+ result = @agent.process(user_input)
323
+ @chat_display.swt_widget.set_text(result) # never do this
324
+ end
325
+ ```
326
+
327
+ ### Data binding for text input
328
+
329
+ ```ruby
330
+ attr_accessor :user_input # declare in the class
331
+
332
+ text(:border) {
333
+ text <=> [self, :user_input] # two-way binding
334
+ on_key_pressed do |e|
335
+ send_message if e.character == 13 # Enter key
336
+ end
337
+ }
338
+ ```
339
+
340
+ ### Font and color
341
+
342
+ ```ruby
343
+ font name: 'Monospace', height: 13
344
+ font height: 12, style: :bold # styles: :bold, :italic, :normal
345
+ background :white # named color
346
+ background rgb(225, 235, 255) # custom RGB
347
+ foreground rgb(66, 133, 244)
348
+ ```
349
+
350
+ ### What does NOT work — avoid these
351
+
352
+ | Pattern | Problem |
353
+ |---------|---------|
354
+ | `sash_form` | Right pane is invisible on macOS — use plain `composite` instead |
355
+ | `layout_data { height_hint N }` | `InvalidKeywordError` — use argument form |
356
+ | `shell.setSize(w, h)` inside `body {}` | Use `size w, h` DSL method |
357
+ | Calling widget methods outside `async_exec` from a Thread | SWT thread violation, crashes |
358
+ | `require 'glimmer-dsl-swt'` in app.rb | Already loaded by Zuzu — causes conflicts |
359
+
360
+ ---
361
+
362
+ ## Extending the UI
363
+
364
+ ### Changing app name and window size
365
+
366
+ ```ruby
367
+ Zuzu.configure do |c|
368
+ c.app_name = 'My Custom Assistant'
369
+ c.window_width = 1024
370
+ c.window_height = 768
371
+ end
372
+ ```
373
+
374
+ ### Extending Zuzu::App for deeper customisation
375
+
376
+ To override helper methods (e.g. `open_admin_panel`), **reopen `Zuzu::App` directly** —
377
+ do NOT subclass it. Subclassing causes Glimmer to raise
378
+ `Invalid custom widget for having no body!` because `body`, `before_body`, and
379
+ `after_body` are DSL class-level declarations that are not inherited by subclasses.
380
+
381
+ **Correct pattern — reopen the class:**
382
+
383
+ ```ruby
384
+ module Zuzu
385
+ class App
386
+ # Override any private helper method here.
387
+ # @store, @fs, @agent etc. are all available (set up by before_body).
388
+ def open_admin_panel
389
+ panel = shell {
390
+ text 'My Custom Panel'
391
+ minimum_size 400, 400
392
+ grid_layout 1, false
393
+ # ... your widgets here
394
+ }
395
+ panel.open
396
+ end
397
+
398
+ private
399
+
400
+ def my_helper
401
+ # additional private methods are fine here too
402
+ end
403
+ end
404
+ end
405
+
406
+ Zuzu::App.launch!(use_llamafile: true) # always launch on Zuzu::App, not a subclass
407
+ ```
408
+
409
+ **Why not subclass?** `before_body`, `after_body`, and `body` are Glimmer DSL
410
+ declarations stored at the class level. A subclass has none of them, so Glimmer
411
+ refuses to instantiate it with "no body" error. Reopening `Zuzu::App` avoids
412
+ this entirely while still giving full access to all instance variables.
413
+
414
+ ---
415
+
416
+ ## LLM and agent behaviour
417
+
418
+ ### How tool calling works
419
+
420
+ The agent uses prompt-based tool calling — no native function-calling API.
421
+ Any instruction-following model works (not just models with `tool_calls` support).
422
+
423
+ 1. Agent builds system prompt listing all registered tools
424
+ 2. Sends `[{system}, {user message}]` to llamafile
425
+ 3. Model outputs `<zuzu_tool_call>{"name":"...","args":{...}}</zuzu_tool_call>`
426
+ 4. Agent parses tag, calls `ToolRegistry.execute(name, args, fs)`
427
+ 5. Result injected as `<zuzu_tool_result>...</zuzu_tool_result>` user message
428
+ 6. Loops up to 10 times; stops when response has no tool call tags
429
+ 7. Final plain-text response stored in Memory and shown in UI
430
+
431
+ ### Conversation history
432
+
433
+ Memory is persisted in SQLite but **not injected into the LLM context**.
434
+ This is intentional — prior non-tool responses cause models to skip tool use.
435
+ Each request is: `[system_prompt, current_user_message]`.
436
+
437
+ ### Loop detection
438
+
439
+ If the agent calls the same tool with the same args more than twice,
440
+ the loop is broken and the model is told to give its final answer.
441
+ Log line: `[zuzu] loop detected for <tool_name>, breaking`
442
+
443
+ ### LLM not calling tools — common causes
444
+
445
+ 1. `system_prompt_extras` text accidentally overrides the tool-use rules
446
+ 2. Model quality — smaller/quantised models follow instructions less reliably
447
+ 3. User message too ambiguous — tools need clear intent to be triggered
448
+ 4. Tool description unclear — descriptions are shown verbatim to the model
449
+
450
+ ---
451
+
452
+ ## Packaging
453
+
454
+ ### Step 1 — Build the JAR
455
+
456
+ ```bash
457
+ bundle exec zuzu package
458
+ ```
459
+
460
+ - Creates `<app-directory>.jar` in the current directory
461
+ - Warbler auto-detects `bin/app` as the executable entry point (created automatically)
462
+ - `models/` directory is NOT bundled — place model files next to the jar at runtime
463
+ - Path resolution: `__dir__` inside a jar returns `uri:classloader:/` —
464
+ app.rb already handles this with the `base` variable pattern
465
+
466
+ Run the jar (requires Java 21+ installed):
467
+ ```bash
468
+ java -XstartOnFirstThread -jar my_app.jar # macOS (SWT requires first thread)
469
+ java -jar my_app.jar # Linux / Windows
470
+ ```
471
+
472
+ ### Step 2 — Build a native installer (optional, no Java required for users)
473
+
474
+ After the JAR is built, `zuzu package` prompts:
475
+
476
+ ```
477
+ Bundle into a self-contained native executable? (no Java required for users) [y/N]:
478
+ ```
479
+
480
+ Type `y`. Zuzu uses **jpackage** (included with JDK 21+) to bundle a minimal JRE
481
+ via jlink and produce a platform-native installer:
482
+
483
+ | Platform | Output in `dist/` |
484
+ |----------|-------------------|
485
+ | macOS | `.dmg` — drag-to-Applications `.app` bundle |
486
+ | Linux | `.deb` package |
487
+ | Windows | `.exe` installer |
488
+
489
+ The model file cannot be bundled (too large). Zuzu injects the model filename as
490
+ the `-Dzuzu.model` JVM property. At runtime, `Zuzu::Config#llamafile_path` resolves
491
+ it from the platform user-data directory automatically:
492
+
493
+ ```
494
+ macOS: ~/Library/Application Support/<AppName>/models/<model>.llamafile
495
+ Linux: ~/.local/share/<AppName>/models/<model>.llamafile
496
+ Windows: %APPDATA%\<AppName>\models\<model>.llamafile
497
+ ```
498
+
499
+ `zuzu package` prints the exact path — tell your users to place the model there
500
+ before launching the installed app.
501
+
502
+ ---
503
+
504
+ ## Debugging
505
+
506
+ ### Check the LLM is running
507
+
508
+ ```bash
509
+ curl http://127.0.0.1:8080/v1/models
510
+ ```
511
+
512
+ ### Watch agent tool calls in real time
513
+
514
+ Tool calls and results are printed to stderr:
515
+ ```
516
+ [zuzu] tool current_time({}) => 2025-01-15 14:32:00 PST
517
+ [zuzu] tool write_file({"path"=>"/notes/x.txt","content"=>"..."}) => Written 42 bytes
518
+ ```
519
+
520
+ ### Open IRB with the full framework loaded
521
+
522
+ ```bash
523
+ bundle exec zuzu console
524
+ store = Zuzu::Store.new
525
+ fs = Zuzu::AgentFS.new(store)
526
+ fs.write_file('/test.txt', 'hello')
527
+ fs.read_file('/test.txt') # => "hello"
528
+ ```
529
+
530
+ ### Inspect the SQLite database directly
531
+
532
+ ```bash
533
+ sqlite3 .zuzu/zuzu.db
534
+ .tables # fs_inodes, fs_dentries, kv_store, messages, tool_calls
535
+ SELECT * FROM messages ORDER BY id DESC LIMIT 5;
536
+ SELECT key, value FROM kv_store;
537
+ ```
538
+
539
+ ### llamafile startup log
540
+
541
+ ```bash
542
+ tail -f models/llama.log
543
+ ```
544
+
545
+ ### Common errors
546
+
547
+ | Error | Cause | Fix |
548
+ |-------|-------|-----|
549
+ | `llamafile not found: ...` | Model path wrong or file missing | Check `c.llamafile_path` and `chmod +x` |
550
+ | `LoadError: no such file -- zuzu` | Not running with bundler | Use `bundle exec zuzu start` |
551
+ | Blank/invisible UI widgets | Wrong `layout_data` (grab_excess_vertical=true) | Change last arg to `false` |
552
+ | `InvalidKeywordError` in layout | Block-form `layout_data { }` used | Use argument form `layout_data(:fill, :fill, true, false)` |
553
+ | Agent never calls tools | History contamination or bad prompt | Check `system_prompt_extras` isn't overriding rules |
554
+ | `SSL certificate verify failed` | JRuby uses Java SSL store | `verify_mode: OpenSSL::SSL::VERIFY_NONE` (already set in http_get) |
555
+ | `SWT thread access violation` | Widget update from background thread | Wrap in `async_exec { }` |
556
+
557
+ ---
558
+
559
+ ## File structure of a Zuzu app
560
+
561
+ ```
562
+ my_app/
563
+ ├── app.rb # ← your main file: configure + tools + launch
564
+ ├── Gemfile # gem "zuzu"
565
+ ├── warble.rb # packaging config (auto-generated by zuzu package)
566
+ ├── bin/
567
+ │ └── app # thin launcher for warbler (auto-generated)
568
+ ├── models/
569
+ │ └── *.llamafile # local LLM binaries — download separately, not in git
570
+ └── .zuzu/
571
+ └── zuzu.db # SQLite database — not in git
572
+ ```
573
+
574
+ ---
575
+
576
+ ## What the Zuzu gem provides (do not reimplement)
577
+
578
+ - `Zuzu::App` — Glimmer SWT desktop window
579
+ - `Zuzu::Agent` — ReAct loop with `<zuzu_tool_call>` parsing
580
+ - `Zuzu::AgentFS` — SQLite virtual filesystem
581
+ - `Zuzu::Memory` — conversation history
582
+ - `Zuzu::Store` — JDBC SQLite query layer
583
+ - `Zuzu::LlmClient` — HTTP client for llamafile API
584
+ - `Zuzu::LlamafileManager` — llamafile subprocess lifecycle
585
+ - `Zuzu::ToolRegistry` — tool registration and execution
586
+ - Built-in tools: `read_file`, `write_file`, `list_directory`, `run_command`, `http_get`
587
+ - Channels: `InApp` (desktop GUI), `WhatsApp` (opt-in via `c.channels = ['whatsapp']`)
588
+
589
+ All of the above is loaded automatically by `require 'zuzu'` in `app.rb`.
@@ -0,0 +1,82 @@
1
+ # Zuzu App
2
+
3
+ Local-first JRuby desktop AI assistant built with the [Zuzu framework](https://github.com/parolkar/zuzu).
4
+
5
+ **Runtime:** JRuby 10.0.3.0 + Java 21. Always use `bundle exec zuzu` — never `ruby` directly.
6
+
7
+ ## On startup
8
+
9
+ When the developer first opens this project or greets you, respond with this welcome message (adapt the app name from `app.rb`):
10
+
11
+ ---
12
+ Welcome to your **Zuzu app** — a local-first AI desktop assistant that runs entirely on your machine. No cloud, no subscriptions, your data stays private.
13
+
14
+ You're building on the **Zuzu framework** (JRuby + Glimmer DSL for SWT + llamafile), which means your app is:
15
+ - A native desktop window with a built-in AI agent
16
+ - Extensible with custom tools the agent can call during conversations
17
+ - Packageable as a standalone `.jar` for distribution
18
+
19
+ Here's what I can help you with right now:
20
+
21
+ | Command | What it does |
22
+ |---------|-------------|
23
+ | `/setup` | Verify runtime, check model, confirm everything works |
24
+ | `/add-tool` | Add a new capability the agent can call (most common task) |
25
+ | `/customize` | Change app name, persona, window size, or UI |
26
+ | `/debug` | Diagnose issues with tools, LLM, or the UI |
27
+
28
+ What would you like to build today?
29
+
30
+ ---
31
+
32
+ ## Key files
33
+
34
+ | File | Purpose |
35
+ |------|---------|
36
+ | `app.rb` | **Everything lives here** — configure, register tools, launch |
37
+ | `models/*.llamafile` | Local LLM binary — download separately, never commit |
38
+ | `.zuzu/zuzu.db` | SQLite — AgentFS + memory. Don't edit while app is running |
39
+ | `warble.rb` | Warbler config for `zuzu package` |
40
+ | `AGENTS.md` | Full reference: Glimmer DSL patterns, AgentFS API, tool guide |
41
+
42
+ ## Skills
43
+
44
+ | Skill | When to use |
45
+ |-------|-------------|
46
+ | `/setup` | First run — verify runtime, install deps, check model, test launch |
47
+ | `/add-tool` | Add a new tool the agent can call |
48
+ | `/customize` | Change app name, window size, system prompt, or UI |
49
+ | `/debug` | App not responding, tools not firing, LLM issues |
50
+
51
+ ## Commands
52
+
53
+ ```bash
54
+ bundle exec zuzu start # launch the app
55
+ bundle exec zuzu console # IRB with Zuzu loaded — test tools interactively
56
+ bundle exec zuzu package # build standalone .jar, then optionally a native installer
57
+ bundle install # sync gems after Gemfile changes
58
+ ```
59
+
60
+ ### Distribution tiers
61
+
62
+ | Command | Output | User requirement |
63
+ |---------|--------|-----------------|
64
+ | `zuzu package` → JAR only | `<app>.jar` | Java 21+ installed |
65
+ | `zuzu package` → native (type `y` at prompt) | `.dmg` / `.deb` / `.exe` in `dist/` | Nothing — JRE bundled |
66
+
67
+ For the native installer, the model file is NOT bundled. After install, users place it at:
68
+ - **macOS:** `~/Library/Application Support/<AppName>/models/<model>.llamafile`
69
+ - **Linux:** `~/.local/share/<AppName>/models/<model>.llamafile`
70
+ - **Windows:** `%APPDATA%\<AppName>\models\<model>.llamafile`
71
+
72
+ ## Core rules — always enforce these
73
+
74
+ - Tools registered **before** `Zuzu::App.launch!` in `app.rb`
75
+ - Tool block signature is always `|args, fs|` — `args` keys are **strings**
76
+ - Never access the host filesystem from a tool — use `fs` (AgentFS) instead
77
+ - Never call SWT widget methods from a background thread — wrap in `async_exec { }`
78
+ - Never use `layout_data { height_hint N }` block form — use `layout_data(:fill, :fill, true, false)` argument form
79
+ - Never use `sash_form` — it renders invisible on macOS
80
+ - Never subclass `Zuzu::App` to override methods — reopen it with `module Zuzu; class App; end; end` instead. Subclassing causes `Invalid custom widget for having no body!` because Glimmer DSL `body`/`before_body`/`after_body` blocks are not inherited
81
+
82
+ Run commands directly. Don't tell the user to run them unless they require manual action (e.g. entering credentials).