zuzu 0.2.1-java → 0.2.2-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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 30a4b0d6a387e3fcd5f11779007cca23c75cfc8022e0efaf7b59a51a774f8e9c
4
- data.tar.gz: 85912884d1c9a34aea94735f909f9702ecb5e732b09f695e842c5edebab5ccd2
3
+ metadata.gz: b6098f883dec29f87b5dcece9c69f8e115608cfd89b9cf045fd442355fbf48c9
4
+ data.tar.gz: d9ffeb971f19158a6262401bdcadb7791bdbd70523c843dd6b7ac26cc3a4c536
5
5
  SHA512:
6
- metadata.gz: 7df32882674e79a21e55e8f81857eb264c88f168090619ce9af3bda6c356858fd6fda0db17618e808c8b67fd6f3c81e31e14c26857837a96858284b29533ce0b
7
- data.tar.gz: 571984d3abb3f76e8afa169fb88257c18441cac52f5e2a085067b2a189af0772e354e0b0c999a73be09cb13365eb3d6c52a98b772717803b5a27fc553fcf099c
6
+ metadata.gz: b7d6f2d65abfc635d0dceec2122a788d7d2cc03159969c9e06a1b03a45e73eb9c03bf430d5f1a6170a7aaa8c717961bc5c51311336cdf80ab4a3b28feb1dc8b4
7
+ data.tar.gz: b10535955c154eee0eaade46edb4ee5b1b9049e8e224cea1d49da58553d1bbd1ae263045977f9a1ac6d87dbb5bf0c7da62e26e73eb5ec0c4b3bf8152ba699f58
data/README.md CHANGED
@@ -1,9 +1,22 @@
1
1
  # Zuzu
2
2
 
3
- **Local-first agentic desktop apps with JRuby.**
3
+ **Build AI-native desktop apps that run entirely on the user's machine.**
4
4
 
5
- Zuzu is a framework for building privacy-respecting AI desktop assistants that
6
- run entirely on your machine. No cloud APIs required — your data stays local.
5
+ Every application you install on an operating system does the same fundamental thing: it translates human intent into OS system calls. A text editor writes bytes to disk. A browser opens network connections. At its core, every installed app is an orchestrator of operating system capabilities.
6
+
7
+ LLMs are simply a more expressive interface for exactly that orchestration. Zuzu is a framework built on this premise — for developers who want to ship installable, AI-native desktop apps where the intelligence runs on the user's hardware, not in a data center.
8
+
9
+ **Why does this matter?**
10
+
11
+ - **Privacy by architecture.** The agent operates inside AgentFS — a sandboxed virtual filesystem backed by a single SQLite file. It cannot touch the host OS unless you explicitly open that door. There is no network call to make, no token to rotate, no terms of service that changes next quarter.
12
+
13
+ - **Deployable like software, not like a service.** Package your app as a single `.jar`. Users pay, download, double-click, and run. No Docker. No cloud subscription. No infrastructure to maintain. The JVM handles cross-platform distribution the way it has for 30 years.
14
+
15
+ - **Built for regulated environments.** A therapist keeping session notes, an auditor running confidential analysis, a corporate team in a restricted environment — these are exactly the users who benefit most from powerful AI but are currently blocked by cloud dependency. A bundled LLM in a self-contained Java application needs no external approval to run.
16
+
17
+ - **Developer experience that matches how software is actually built today.** `zuzu new my_app` scaffolds a project pre-wired for Claude Code: CLAUDE.md, skills for `/setup`, `/add-tool`, `/customize`, and `/debug` — all enforcing Zuzu's patterns. Open the folder, start your coding agent, describe what you want to build.
18
+
19
+ → [Why Zuzu exists](docs/why.md) · [Quick Demo](https://raw.githubusercontent.com/parolkar/zuzu/refs/heads/main/docs/demo/zuzu_quick_demo_01.mp4)
7
20
 
8
21
  <video src="https://raw.githubusercontent.com/parolkar/zuzu/refs/heads/main/docs/demo/zuzu_quick_demo_01.mp4" controls width="100%"></video>
9
22
  [Quick Demo](https://raw.githubusercontent.com/parolkar/zuzu/refs/heads/main/docs/demo/zuzu_quick_demo_01.mp4)
@@ -32,7 +45,7 @@ cd zuzu
32
45
  bin/setup
33
46
  ```
34
47
 
35
- `bin/setup` installs Java 21, JRuby 10.0.4.0, and all gem dependencies
48
+ `bin/setup` installs Java 21, JRuby 10.0.3.0, and all gem dependencies
36
49
  automatically. If you prefer manual setup, see [Manual Setup](#manual-setup)
37
50
  below.
38
51
 
@@ -61,9 +74,11 @@ Edit `app.rb` to point at your model:
61
74
 
62
75
  ```ruby
63
76
  Zuzu.configure do |c|
64
- c.app_name = 'My Assistant'
65
- c.llamafile_path = File.expand_path('models/llava-v1.5-7b-q4.llamafile', __dir__)
66
- c.db_path = File.expand_path('.zuzu/zuzu.db', __dir__)
77
+ c.app_name = 'My Assistant'
78
+ # Works both when run directly and from a packaged .jar
79
+ base = __dir__.to_s.start_with?('uri:classloader:') ? Dir.pwd : __dir__
80
+ c.llamafile_path = File.join(base, 'models', 'llava-v1.5-7b-q4.llamafile')
81
+ c.db_path = File.join(base, '.zuzu', 'zuzu.db')
67
82
  c.port = 8080
68
83
  end
69
84
  ```
@@ -71,16 +86,11 @@ end
71
86
  Launch:
72
87
 
73
88
  ```bash
74
- # macOS (SWT requires first-thread access):
75
- JRUBY_OPTS="-J-XstartOnFirstThread -J--enable-native-access=ALL-UNNAMED" bundle exec ruby app.rb
76
-
77
- # Linux:
78
- JRUBY_OPTS="-J--enable-native-access=ALL-UNNAMED" bundle exec ruby app.rb
89
+ bundle exec zuzu start
79
90
  ```
80
91
 
81
- You'll see a native desktop window with a file browser on the left and a chat
82
- interface on the right. The llamafile model starts automatically in the
83
- background.
92
+ You'll see a native desktop chat window. The llamafile model starts automatically
93
+ in the background. Click **Admin Panel** to browse the AgentFS virtual filesystem.
84
94
 
85
95
  ---
86
96
 
@@ -90,18 +100,14 @@ If `bin/setup` doesn't suit your workflow, follow these steps.
90
100
 
91
101
  ### 1. Install Java 21+
92
102
 
93
- **macOS (Homebrew):**
103
+ **macOS (Homebrew — recommended):**
94
104
 
95
105
  ```bash
96
- brew install openjdk@21
106
+ brew install --cask temurin@21
97
107
  ```
98
108
 
99
- Add to your shell profile (`~/.zshrc` or `~/.bash_profile`):
100
-
101
- ```bash
102
- export JAVA_HOME=$(/usr/libexec/java_home -v 21)
103
- export PATH="$JAVA_HOME/bin:$PATH"
104
- ```
109
+ Temurin is the Eclipse/Adoptium OpenJDK distribution. The cask sets up
110
+ `JAVA_HOME` automatically — no manual PATH changes needed.
105
111
 
106
112
  **macOS (SDKMAN):**
107
113
 
@@ -125,17 +131,17 @@ java -version
125
131
  # → openjdk version "21.x.x" ...
126
132
  ```
127
133
 
128
- ### 2. Install JRuby 10.0.4.0 via rbenv
134
+ ### 2. Install JRuby 10.0.3.0 via rbenv
129
135
 
130
136
  ```bash
131
137
  # Install rbenv if needed
132
138
  brew install rbenv ruby-build # macOS
133
139
  # or: https://github.com/rbenv/rbenv#installation
134
140
 
135
- rbenv install jruby-10.0.4.0
136
- rbenv local jruby-10.0.4.0
141
+ rbenv install jruby-10.0.3.0
142
+ rbenv local jruby-10.0.3.0
137
143
  ruby -v
138
- # → jruby 10.0.4.0 (ruby 3.x.x) ...
144
+ # → jruby 10.0.3.0 (ruby 3.x.x) ...
139
145
  ```
140
146
 
141
147
  ### 3. Install Gems
@@ -289,15 +295,23 @@ fs.kv_get('last_query') # → "weather in Tokyo"
289
295
 
290
296
  ### Packaging as .jar
291
297
 
292
- Use Warbler to create a standalone Java archive:
298
+ Package your app as a standalone Java archive with a single command:
299
+
300
+ ```bash
301
+ bundle exec zuzu package
302
+ ```
303
+
304
+ This auto-installs Warbler if needed, generates the necessary launcher, and
305
+ produces a `.jar` named after your app directory. Run it with:
293
306
 
294
307
  ```bash
295
- gem install warbler
296
- warble jar
297
- java -XstartOnFirstThread -jar zuzu-app.jar # macOS
298
- java -jar zuzu-app.jar # Linux
308
+ java -XstartOnFirstThread -jar my_app.jar # macOS
309
+ java -jar my_app.jar # Linux / Windows
299
310
  ```
300
311
 
312
+ > **Note:** Place your llamafile model in a `models/` directory alongside the
313
+ > `.jar` — models are not bundled into the archive.
314
+
301
315
  ---
302
316
 
303
317
  ## Configuration Reference
@@ -323,6 +337,7 @@ end
323
337
  ```
324
338
  zuzu new APP_NAME Scaffold a new Zuzu application
325
339
  zuzu start Launch the Zuzu app in the current directory
340
+ zuzu package Package the app as a standalone .jar
326
341
  zuzu console Open an IRB session with Zuzu loaded
327
342
  zuzu version Print the Zuzu version
328
343
  zuzu help Show this message
data/bin/setup CHANGED
@@ -20,12 +20,9 @@ if java -version 2>&1 | grep -q 'version "2[1-9]\|version "3'; then
20
20
  else
21
21
  warn "Java 21+ not found."
22
22
  if [[ "$OSTYPE" == darwin* ]]; then
23
- step "Installing Java 21 via Homebrew"
24
- brew install openjdk@21
25
- echo 'export JAVA_HOME=$(/usr/libexec/java_home -v 21)' >> ~/.zshrc
26
- export JAVA_HOME=$(/usr/libexec/java_home -v 21)
27
- export PATH="$JAVA_HOME/bin:$PATH"
28
- echo " Installed. Restart your shell or run: source ~/.zshrc"
23
+ step "Installing Java 21 via Homebrew (Temurin)"
24
+ brew install --cask temurin@21
25
+ echo " Installed. Restart your shell to pick up JAVA_HOME."
29
26
  elif [[ "$OSTYPE" == linux-gnu* ]]; then
30
27
  step "Installing Java 21 via apt"
31
28
  sudo apt-get update -qq && sudo apt-get install -y openjdk-21-jdk
data/bin/zuzu CHANGED
@@ -33,10 +33,13 @@ when 'new'
33
33
  app_dir = File.expand_path(app_name)
34
34
  abort "Directory '#{app_name}' already exists." if File.exist?(app_dir)
35
35
 
36
- template_src = File.expand_path('../templates/app.rb', __dir__)
36
+ templates_dir = File.expand_path('../templates', __dir__)
37
37
 
38
38
  FileUtils.mkdir_p(app_dir)
39
- FileUtils.cp(template_src, File.join(app_dir, 'app.rb'))
39
+ FileUtils.cp(File.join(templates_dir, 'app.rb'), File.join(app_dir, 'app.rb'))
40
+ FileUtils.cp(File.join(templates_dir, 'AGENTS.md'), File.join(app_dir, 'AGENTS.md'))
41
+ FileUtils.cp(File.join(templates_dir, 'CLAUDE.md'), File.join(app_dir, 'CLAUDE.md'))
42
+ FileUtils.cp_r(File.join(templates_dir, '.claude'), File.join(app_dir, '.claude'))
40
43
 
41
44
  # Generate Gemfile — uses local path when running from source tree,
42
45
  # published gem when installed via `gem install zuzu`.
@@ -55,6 +58,9 @@ when 'new'
55
58
  puts "Created new Zuzu app: #{app_name}/"
56
59
  puts " #{app_name}/app.rb"
57
60
  puts " #{app_name}/Gemfile"
61
+ puts " #{app_name}/AGENTS.md"
62
+ puts " #{app_name}/CLAUDE.md"
63
+ puts " #{app_name}/.claude/skills/ (setup, add-tool, customize, debug)"
58
64
  puts ''
59
65
  if DEV_MODE
60
66
  puts " (dev mode: Gemfile points to #{ZUZU_ROOT})"
@@ -69,6 +75,15 @@ when 'new'
69
75
  when 'start'
70
76
  entry = ['app.rb', 'lib/app.rb'].find { |f| File.exist?(f) }
71
77
  abort 'No app.rb found. Run `zuzu start` from your app directory.' unless entry
78
+
79
+ # SWT on Java 21+ requires --enable-native-access=ALL-UNNAMED.
80
+ # JVM flags must be set before the JVM starts, so re-exec via bundle
81
+ # with the flag injected into JRUBY_OPTS if it isn't already present.
82
+ unless ENV['JRUBY_OPTS'].to_s.include?('enable-native-access')
83
+ ENV['JRUBY_OPTS'] = "#{ENV['JRUBY_OPTS']} -J--enable-native-access=ALL-UNNAMED".strip
84
+ exec('bundle', 'exec', 'zuzu', 'start')
85
+ end
86
+
72
87
  load File.expand_path(entry)
73
88
 
74
89
  when 'package'
data/lib/zuzu/agent.rb CHANGED
@@ -11,22 +11,13 @@ module Zuzu
11
11
  TOOL_CALL_RE = /<zuzu_tool_call>(.*?)<\/zuzu_tool_call>/m
12
12
  TOOL_RESULT_RE = /<zuzu_tool_result>.*?<\/zuzu_tool_result>/m
13
13
 
14
- SYSTEM_PROMPT = <<~PROMPT.strip
14
+ BASE_PROMPT = <<~PROMPT
15
15
  You are Zuzu, a helpful desktop AI assistant.
16
16
 
17
17
  You have access to a sandboxed virtual filesystem called AgentFS. It is completely
18
18
  separate from the host computer's filesystem. All file paths refer to AgentFS only.
19
19
  You cannot access or modify any files on the host system.
20
20
 
21
- Available tools — use the tag format shown below:
22
-
23
- - write_file : Write text to an AgentFS file. Args: path (string), content (string)
24
- - read_file : Read an AgentFS file. Args: path (string)
25
- - list_directory : List an AgentFS directory. Args: path (string, default "/")
26
- - run_command : Run a sandboxed command against AgentFS. Args: command (string)
27
- Supported: ls [path], cat <path>, pwd, echo <text>
28
- - http_get : Fetch a public URL from the internet. Args: url (string)
29
-
30
21
  To call a tool, output exactly this on its own line:
31
22
  <zuzu_tool_call>{"name":"TOOL_NAME","args":{"key":"value"}}</zuzu_tool_call>
32
23
 
@@ -51,7 +42,7 @@ module Zuzu
51
42
  # Only system prompt + current message — no history injected into agent context.
52
43
  # Prior non-tool-call responses cause models to skip tool use.
53
44
  messages = [
54
- { 'role' => 'system', 'content' => SYSTEM_PROMPT },
45
+ { 'role' => 'system', 'content' => build_system_prompt },
55
46
  { 'role' => 'user', 'content' => user_message }
56
47
  ]
57
48
 
@@ -100,6 +91,22 @@ module Zuzu
100
91
 
101
92
  private
102
93
 
94
+ def build_system_prompt
95
+ tool_lines = ToolRegistry.tools.map do |t|
96
+ args = t.schema[:properties]&.keys&.map(&:to_s)&.join(', ')
97
+ line = "- #{t.name} : #{t.description}"
98
+ line += " Args: #{args}" if args && !args.empty?
99
+ line
100
+ end.join("\n")
101
+
102
+ extras = Zuzu.config.system_prompt_extras.to_s.strip
103
+
104
+ prompt = BASE_PROMPT.dup
105
+ prompt << "\nAvailable tools:\n#{tool_lines}\n"
106
+ prompt << "\n#{extras}" unless extras.empty?
107
+ prompt.strip
108
+ end
109
+
103
110
  def extract_tool_calls(content)
104
111
  content.scan(TOOL_CALL_RE).filter_map do |match|
105
112
  data = JSON.parse(match[0].strip)
data/lib/zuzu/config.rb CHANGED
@@ -11,7 +11,7 @@ module Zuzu
11
11
  #
12
12
  class Config
13
13
  attr_accessor :port, :model, :channels, :log_level, :app_name,
14
- :window_width, :window_height
14
+ :window_width, :window_height, :system_prompt_extras
15
15
 
16
16
  attr_reader :db_path, :llamafile_path
17
17
 
@@ -20,8 +20,9 @@ module Zuzu
20
20
  @model = 'LLaMA_CPP'
21
21
  @db_path = File.join('.zuzu', 'zuzu.db')
22
22
  @llamafile_path = nil
23
- @channels = []
24
- @log_level = :info
23
+ @channels = []
24
+ @log_level = :info
25
+ @system_prompt_extras = nil
25
26
  @app_name = 'Zuzu'
26
27
  @window_width = 860
27
28
  @window_height = 620
data/lib/zuzu/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zuzu
4
- VERSION = "0.2.1"
4
+ VERSION = "0.2.2"
5
5
  end
@@ -0,0 +1,162 @@
1
+ ---
2
+ name: add-tool
3
+ description: Add a new tool that the Zuzu agent can call during conversations. Guides through naming, arguments, implementation, and registers it correctly in app.rb. Use when the developer wants the AI assistant to gain a new capability (e.g. check weather, query a database, read a file, call an API).
4
+ ---
5
+
6
+ # Add a Zuzu Tool
7
+
8
+ A tool is a Ruby block the agent calls using `<zuzu_tool_call>` tags. Once registered, it is **automatically listed in the agent's system prompt** — no manual prompt editing needed.
9
+
10
+ ## Step 1 — Understand what the tool should do
11
+
12
+ AskUserQuestion: "What should this tool do? Describe it in one sentence — this description will be shown to the AI agent."
13
+
14
+ AskUserQuestion: "What arguments does it need? For each: name, type (string/number/boolean), and what it represents. Or 'none' if no arguments."
15
+
16
+ AskUserQuestion: "Does it need to read/write files (use AgentFS), call an external API, query a database, or just compute something locally?"
17
+
18
+ ## Step 2 — Choose a tool name
19
+
20
+ - Must be snake_case, descriptive, unambiguous
21
+ - Examples: `get_weather`, `search_notes`, `send_email`, `calculate_tax`, `lookup_stock_price`
22
+ - Check `app.rb` for existing tool names — avoid duplicates
23
+ - AskUserQuestion: "I'll name this tool `<suggested_name>`. Does that work, or would you prefer a different name?"
24
+
25
+ ## Step 3 — Implement the tool
26
+
27
+ Read `app.rb` to find the insertion point — tools go **between the `Zuzu.configure` block and `Zuzu::App.launch!`**.
28
+
29
+ ### Pattern A — No arguments, no AgentFS
30
+
31
+ ```ruby
32
+ Zuzu::ToolRegistry.register(
33
+ 'tool_name',
34
+ 'One-sentence description shown to the agent.',
35
+ { type: 'object', properties: {}, required: [] }
36
+ ) { |_args, _fs| "result as a string" }
37
+ ```
38
+
39
+ ### Pattern B — With arguments
40
+
41
+ ```ruby
42
+ Zuzu::ToolRegistry.register(
43
+ 'tool_name',
44
+ 'Description shown to the agent.',
45
+ {
46
+ type: 'object',
47
+ properties: {
48
+ param_one: { type: 'string', description: 'what it is' },
49
+ param_two: { type: 'integer', description: 'what it is' }
50
+ },
51
+ required: ['param_one'] # list only truly required params
52
+ }
53
+ ) do |args, _fs|
54
+ # args keys are STRINGS: args['param_one'] not args[:param_one]
55
+ value = args['param_one'].to_s
56
+ "result: #{value}"
57
+ end
58
+ ```
59
+
60
+ ### Pattern C — Using AgentFS (sandboxed file/KV access)
61
+
62
+ ```ruby
63
+ Zuzu::ToolRegistry.register(
64
+ 'save_note',
65
+ 'Save a note to the virtual filesystem.',
66
+ {
67
+ type: 'object',
68
+ properties: {
69
+ title: { type: 'string', description: 'Note title' },
70
+ content: { type: 'string', description: 'Note content' }
71
+ },
72
+ required: %w[title content]
73
+ }
74
+ ) do |args, fs|
75
+ # fs is Zuzu::AgentFS — sandboxed, NOT the host filesystem
76
+ path = "/notes/#{args['title'].downcase.gsub(/\s+/, '_')}.txt"
77
+ fs.write_file(path, args['content'])
78
+ "Saved to #{path}"
79
+ end
80
+ ```
81
+
82
+ ### Pattern D — Calling an external HTTP API
83
+
84
+ ```ruby
85
+ require 'net/http'
86
+ require 'json'
87
+
88
+ Zuzu::ToolRegistry.register(
89
+ 'get_weather',
90
+ 'Get current weather for a city using wttr.in.',
91
+ {
92
+ type: 'object',
93
+ properties: {
94
+ city: { type: 'string', description: 'City name' }
95
+ },
96
+ required: ['city']
97
+ }
98
+ ) do |args, _fs|
99
+ uri = URI("https://wttr.in/#{URI.encode_uri_component(args['city'])}?format=3")
100
+ Net::HTTP.get(uri).strip
101
+ rescue StandardError => e
102
+ "Error fetching weather: #{e.message}"
103
+ end
104
+ ```
105
+
106
+ ## Step 4 — Enforce these rules before writing
107
+
108
+ Before inserting code, verify:
109
+
110
+ - [ ] Tool is placed **before** `Zuzu::App.launch!`
111
+ - [ ] Block signature uses `|args, fs|` or `|args, _fs|` (never zero args)
112
+ - [ ] `args` keys use **string** form: `args['name']` not `args[:name]`
113
+ - [ ] Return value is a **String** (or will be `.to_s`'d automatically)
114
+ - [ ] No `File.read` / `File.write` / `Dir` calls — use `fs` for file access
115
+ - [ ] External HTTP calls have a rescue block returning an error string
116
+ - [ ] Description is one clear sentence (the agent sees this verbatim)
117
+
118
+ Write the tool to `app.rb` now.
119
+
120
+ ## Step 5 — Verify it loads
121
+
122
+ ```bash
123
+ bundle exec ruby -e "
124
+ require 'zuzu'
125
+ load 'app.rb' rescue nil
126
+ tool = Zuzu::ToolRegistry.find('<tool_name>')
127
+ if tool
128
+ puts 'Tool registered: ' + tool.name
129
+ puts 'Description: ' + tool.description
130
+ else
131
+ puts 'ERROR: tool not found'
132
+ end
133
+ " 2>&1
134
+ ```
135
+
136
+ - If "tool not found": check for syntax errors, verify placement before `launch!`, retry.
137
+ - If syntax error printed: fix it, retry.
138
+
139
+ ## Step 6 — Test in console (if possible without side effects)
140
+
141
+ ```bash
142
+ bundle exec zuzu console
143
+ ```
144
+
145
+ Then in the console:
146
+ ```ruby
147
+ store = Zuzu::Store.new
148
+ fs = Zuzu::AgentFS.new(store)
149
+ tool = Zuzu::ToolRegistry.find('<tool_name>')
150
+ puts tool.block.call({'param' => 'test_value'}, fs)
151
+ ```
152
+
153
+ - If it returns a sensible result: done.
154
+ - If error: fix and re-verify.
155
+
156
+ ## Step 7 — Tell the user
157
+
158
+ Show the registered tool code and confirm:
159
+ > ✅ Tool `<name>` registered. The agent will now automatically list it in its system prompt and call it when relevant. Restart the app (`bundle exec zuzu start`) to pick up the change.
160
+
161
+ If the tool calls an external service that needs configuration (API key, URL, etc.):
162
+ > ⚠️ Remember to set `ENV['YOUR_API_KEY']` or add the value to your configuration before running.
@@ -0,0 +1,192 @@
1
+ ---
2
+ name: customize
3
+ description: Customize the Zuzu app — change the app name, window size, system prompt persona, or extend the UI. Use when the developer wants to rebrand the app, add personality to the assistant, or make visual/behavioral changes.
4
+ ---
5
+
6
+ # Customize Zuzu App
7
+
8
+ Ask what the developer wants to change, then make the changes directly.
9
+
10
+ ## Step 1 — Understand the request
11
+
12
+ AskUserQuestion: "What would you like to customize? Choose one or more:
13
+ 1. App name / window title
14
+ 2. Window size
15
+ 3. Assistant persona / system prompt instructions
16
+ 4. Add a new button or panel to the UI
17
+ 5. Something else — describe it"
18
+
19
+ Route to the relevant section below based on the answer.
20
+
21
+ ---
22
+
23
+ ## Route A — App name and window title
24
+
25
+ Read the current value from `app.rb`:
26
+
27
+ ```ruby
28
+ Zuzu.configure do |c|
29
+ c.app_name = 'Current Name'
30
+ ```
31
+
32
+ AskUserQuestion: "What should the app be called?"
33
+
34
+ Edit `app.rb` — update `c.app_name`. Done. Tell the user to restart the app.
35
+
36
+ ---
37
+
38
+ ## Route B — Window size
39
+
40
+ Read current values (`c.window_width`, `c.window_height`).
41
+
42
+ AskUserQuestion: "What size should the window be? (e.g. 1024 × 768, 1280 × 800)"
43
+
44
+ Edit `app.rb`:
45
+ ```ruby
46
+ c.window_width = 1024
47
+ c.window_height = 768
48
+ ```
49
+
50
+ Done. Tell the user to restart.
51
+
52
+ ---
53
+
54
+ ## Route C — Assistant persona and system prompt
55
+
56
+ Read the current `c.system_prompt_extras` from `app.rb` (may be nil/absent).
57
+
58
+ AskUserQuestion: "Describe the assistant's persona or any extra rules you want it to follow. Examples:
59
+ - 'You are a Ruby developer assistant. Always use Ruby in code examples.'
60
+ - 'You are a cooking assistant. Only discuss food, recipes, and nutrition.'
61
+ - 'Always respond concisely in bullet points.'"
62
+
63
+ AskUserQuestion: "Should this replace the current instructions or be added to them?"
64
+
65
+ Edit `app.rb` — set or update `c.system_prompt_extras`:
66
+
67
+ ```ruby
68
+ Zuzu.configure do |c|
69
+ # ...
70
+ c.system_prompt_extras = <<~EXTRA
71
+ <the persona instructions here>
72
+ EXTRA
73
+ end
74
+ ```
75
+
76
+ **Important rules to preserve** — always keep these in `system_prompt_extras` if the developer's text doesn't already cover them:
77
+ - Do not override the tool-calling rules (those come from the base prompt automatically)
78
+ - Keep instructions concise — the model sees this verbatim on every request
79
+
80
+ Verify it loads:
81
+ ```bash
82
+ bundle exec ruby -e "require 'zuzu'; load 'app.rb' rescue nil; puts Zuzu.config.system_prompt_extras" 2>&1
83
+ ```
84
+
85
+ Done. Tell the user to restart.
86
+
87
+ ---
88
+
89
+ ## Route D — UI: add a button to the Admin Panel
90
+
91
+ The Admin Panel is the popup window opened by the "Admin Panel" button.
92
+ The easiest UI extension is adding a button there.
93
+
94
+ AskUserQuestion: "What should the button do when clicked?"
95
+ AskUserQuestion: "What label should the button have?"
96
+
97
+ This requires subclassing `Zuzu::App`. Check if `app.rb` already subclasses it.
98
+
99
+ **If not subclassing yet**, add this pattern to `app.rb` before `Zuzu::App.launch!`:
100
+
101
+ ```ruby
102
+ class MyApp < Zuzu::App
103
+ private
104
+
105
+ def open_admin_panel
106
+ file_list_widget = nil
107
+
108
+ admin = shell {
109
+ text 'Admin Panel'
110
+ minimum_size 380, 500
111
+ grid_layout 1, false
112
+
113
+ label {
114
+ layout_data(:fill, :fill, true, false)
115
+ text 'AgentFS — Virtual File Browser'
116
+ font height: 12, style: :bold
117
+ }
118
+
119
+ file_list_widget = list(:single, :v_scroll, :border) {
120
+ layout_data(:fill, :fill, true, true)
121
+ font name: 'Monospace', height: 11
122
+ }
123
+
124
+ # ── Default buttons (keep these) ─────────────────────────
125
+ button {
126
+ layout_data(:fill, :fill, true, false)
127
+ text 'Create Test File'
128
+ on_widget_selected {
129
+ @fs.write_file('/test.txt', "Hello!\nCreated at: #{Time.now}")
130
+ populate_file_list(file_list_widget)
131
+ }
132
+ }
133
+
134
+ button {
135
+ layout_data(:fill, :fill, true, false)
136
+ text 'Clear Chat History'
137
+ on_widget_selected {
138
+ @memory.clear
139
+ message_box { text 'Done'; message 'History cleared.' }.open
140
+ }
141
+ }
142
+
143
+ button {
144
+ layout_data(:fill, :fill, true, false)
145
+ text 'Refresh'
146
+ on_widget_selected { populate_file_list(file_list_widget) }
147
+ }
148
+
149
+ # ── New custom button ─────────────────────────────────────
150
+ button {
151
+ layout_data(:fill, :fill, true, false)
152
+ text '<Button Label>'
153
+ on_widget_selected {
154
+ # your action here
155
+ message_box { text 'Done'; message 'Action completed.' }.open
156
+ }
157
+ }
158
+ }
159
+
160
+ populate_file_list(file_list_widget)
161
+ admin.open
162
+ end
163
+ end
164
+ ```
165
+
166
+ Then change the launch line to:
167
+ ```ruby
168
+ MyApp.launch!(use_llamafile: true)
169
+ ```
170
+
171
+ **Glimmer DSL rules to enforce:**
172
+ - `layout_data(:fill, :fill, true, false)` — always use argument form, never block form
173
+ - Never call widget methods from a background thread — wrap in `async_exec { }`
174
+ - Never use `sash_form` — invisible on macOS
175
+
176
+ Verify syntax:
177
+ ```bash
178
+ bundle exec ruby -e "require 'zuzu'; load 'app.rb' rescue puts $!.message" 2>&1
179
+ ```
180
+
181
+ Done. Tell the user to restart.
182
+
183
+ ---
184
+
185
+ ## Route E — Something else
186
+
187
+ Read `AGENTS.md` for the relevant section and implement accordingly.
188
+ Always verify with a syntax check after making changes:
189
+
190
+ ```bash
191
+ bundle exec ruby -e "require 'zuzu'; load 'app.rb' rescue puts $!.message" 2>&1
192
+ ```