openclacky 0.9.34 → 0.9.36
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 +42 -0
- data/lib/clacky/agent/cost_tracker.rb +1 -1
- data/lib/clacky/agent/llm_caller.rb +14 -10
- data/lib/clacky/agent/memory_updater.rb +1 -1
- data/lib/clacky/agent/session_serializer.rb +2 -0
- data/lib/clacky/agent/skill_manager.rb +21 -9
- data/lib/clacky/agent/tool_executor.rb +13 -16
- data/lib/clacky/agent/tool_registry.rb +0 -3
- data/lib/clacky/agent.rb +85 -55
- data/lib/clacky/agent_config.rb +170 -40
- data/lib/clacky/brand_config.rb +11 -27
- data/lib/clacky/cli.rb +68 -13
- data/lib/clacky/default_skills/channel-setup/SKILL.md +1 -1
- data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +1 -1
- data/lib/clacky/default_skills/new/SKILL.md +1 -1
- data/lib/clacky/default_skills/product-help/SKILL.md +1 -1
- data/lib/clacky/default_skills/recall-memory/SKILL.md +1 -1
- data/lib/clacky/default_skills/skill-creator/SKILL.md +1 -1
- data/lib/clacky/idle_compression_timer.rb +8 -0
- data/lib/clacky/json_ui_controller.rb +2 -1
- data/lib/clacky/plain_ui_controller.rb +10 -3
- data/lib/clacky/platform_http_client.rb +161 -1
- data/lib/clacky/server/channel/channel_manager.rb +5 -3
- data/lib/clacky/server/channel/channel_ui_controller.rb +6 -2
- data/lib/clacky/server/http_server.rb +376 -99
- data/lib/clacky/server/scheduler.rb +17 -16
- data/lib/clacky/server/session_registry.rb +1 -5
- data/lib/clacky/server/web_ui_controller.rb +7 -6
- data/lib/clacky/session_manager.rb +22 -0
- data/lib/clacky/skill.rb +19 -3
- data/lib/clacky/skill_loader.rb +5 -59
- data/lib/clacky/tools/browser.rb +25 -73
- data/lib/clacky/tools/security.rb +326 -0
- data/lib/clacky/tools/terminal/output_cleaner.rb +63 -0
- data/lib/clacky/tools/terminal/persistent_session.rb +247 -0
- data/lib/clacky/tools/terminal/session_manager.rb +208 -0
- data/lib/clacky/tools/terminal.rb +892 -0
- data/lib/clacky/tools/todo_manager.rb +6 -16
- data/lib/clacky/tools/trash_manager.rb +2 -2
- data/lib/clacky/ui2/components/input_area.rb +11 -2
- data/lib/clacky/ui2/layout_manager.rb +438 -488
- data/lib/clacky/ui2/output_buffer.rb +310 -0
- data/lib/clacky/ui2/ui_controller.rb +88 -28
- data/lib/clacky/ui_interface.rb +1 -1
- data/lib/clacky/utils/encoding.rb +1 -1
- data/lib/clacky/utils/environment_detector.rb +43 -0
- data/lib/clacky/utils/model_pricing.rb +3 -3
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +506 -279
- data/lib/clacky/web/app.js +169 -21
- data/lib/clacky/web/auth.js +101 -0
- data/lib/clacky/web/i18n.js +45 -1
- data/lib/clacky/web/index.html +13 -16
- data/lib/clacky/web/sessions.js +294 -26
- data/lib/clacky/web/skills.js +20 -6
- data/lib/clacky/web/tasks.js +54 -2
- data/lib/clacky/web/theme.js +58 -20
- data/lib/clacky/web/ws.js +11 -2
- data/lib/clacky.rb +2 -2
- metadata +8 -3
- data/lib/clacky/tools/safe_shell.rb +0 -608
- data/lib/clacky/tools/shell.rb +0 -522
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8a7f74cba226bc2332676ed9f0a72c27f3fb6b45bafacd60198f7ec9b59a84c7
|
|
4
|
+
data.tar.gz: 7396d2258a38e203a5ffe217f0409fb101487bdb74dad8f224649a09b8adb775
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7f10c234d4a22d825e3623bff7e0a0faf55af4cea34dc15a939fbde3e6e3792bf1a11cd97d68e0c6890dd37c011814019cfed7dea70c724fcb2fc11b49cff15a
|
|
7
|
+
data.tar.gz: 12dcd062214b42191f33e4b2e09ec62f24cb9f91206451ff23861672a2cc8b680d89e5498cf0da6c53b74d4da2933bd53863666cee608539edb398f016226df5
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.36] - 2026-04-24
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Session deletion now works correctly**: fixed disk-based session deletion that was failing with proper error handling in the Web UI (C-9d1ea93)
|
|
14
|
+
- **Model switching improved**: better model ID validation and normalization when switching models in Web UI — handles various ID formats correctly (C-b61e22e)
|
|
15
|
+
- **Terminal tool word wrapping**: fixed terminal output word wrapping issues that could break long command outputs (C-5989d02)
|
|
16
|
+
- **Heartbeat mechanism stability**: improved async heartbeat logic in server mode for more reliable connection status tracking (C-5989d02)
|
|
17
|
+
|
|
18
|
+
### Improved
|
|
19
|
+
- **UI polish**: removed session topbar clutter and added empty state messages for better first-time user experience (C-003d613)
|
|
20
|
+
- **Cleaner logging**: reduced noisy debug logs in skill manager for quieter operation (C-c27bbec)
|
|
21
|
+
|
|
22
|
+
## [0.9.35] - 2026-04-23
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- **Unified Terminal tool**: merged the old `safe_shell` and `shell` tools into a single `terminal` tool with persistent PTY sessions — the agent can now keep a shell session alive across tool calls, send input to running prompts, poll long-running commands, and safely interrupt them (`Ctrl+C` / `Ctrl+D`). Replaces 1000+ lines of duplicated shell-handling logic with a cleaner, better-tested implementation.
|
|
26
|
+
- **Access key authentication for server mode**: start the Web UI server with `--access-key <key>` (or `CLACKY_ACCESS_KEY` env var) to require a login before anyone can open sessions — safe to expose the Web UI over the network or to share a running instance
|
|
27
|
+
- **Session debug download**: from the Web UI you can now download a full session bundle (messages, tool calls, config) as a zip for debugging or sharing — useful for bug reports and post-mortems
|
|
28
|
+
- **Scheduler now saves session state**: scheduled/cron tasks now persist their session after each run, so you can inspect what the scheduled task actually did from the Web UI just like a normal session
|
|
29
|
+
- **Web UI visual redesign**: substantial redesign of the sidebar, session list, settings panel, and theme — cleaner spacing, better contrast in both light and dark modes, smoother transitions
|
|
30
|
+
- **Web UI & channel message interrupt**: you can now cancel an in-progress agent reply from the Web UI or from an IM channel (Feishu/WeCom/WeChat) mid-flight instead of waiting for it to finish
|
|
31
|
+
- **Terminal tool UI tips**: the Web UI now surfaces helpful inline tips when the agent is running a terminal command (e.g. "waiting for input", "process still running"), making long-running commands easier to follow
|
|
32
|
+
|
|
33
|
+
### Improved
|
|
34
|
+
- **Smaller tool descriptions**: trimmed the system-prompt footprint of `terminal`, `browser`, and `todo_manager` tool descriptions by ~40% — fewer tokens burned on every API call, slightly faster startup, and meaningfully cheaper sessions over time
|
|
35
|
+
- **Download fallback for skills & brand assets**: when the primary platform download host is unreachable (common in certain regions), the client now automatically falls back to a secondary URL — skill installs and brand asset fetches succeed in more network environments
|
|
36
|
+
- **Session cost shows "N/A" for unknown-price models**: instead of displaying `$0.00` when a model's pricing isn't registered, sessions now show "N/A" so you can tell the difference between "free call" and "we don't know the cost"
|
|
37
|
+
- **Faster, more accurate progress updates**: removed a delay in the progress spinner so the "Agent is thinking..." and tool-running indicators update immediately on state changes instead of a second later
|
|
38
|
+
- **No Claude-specific skill auto-loading**: removed legacy logic that special-cased loading `.claude/` skills at startup — skill loading is now uniform regardless of provider, reducing surprise behavior and confusing "unknown skill" errors
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
- **`file://` links now render and open correctly** (C-5552, C-5553): file:// links are no longer stripped during streaming in the Web UI, and clicking them now opens the file via the backend (including proper foreground focus on WSL via `cmd.exe /c start`). Also fixes silent drop of `file://` links in the CLI.
|
|
42
|
+
- **Idle `Ctrl+C` no longer crashes the CLI**: pressing Ctrl+C while the CLI is idle (no task running) now exits cleanly instead of raising an error
|
|
43
|
+
- **Session pinned status persists correctly** (C-5556): pinning a session in the Web UI now survives server restarts and is correctly restored from disk
|
|
44
|
+
- **Brand skill names follow language switch**: brand-supplied skill names in the Web UI sidebar now update immediately when you toggle the UI language (previously stuck in the initial language until reload)
|
|
45
|
+
- **New sessions get the default model**: fixed a case where newly created sessions could end up on a different model than the configured default; the "lite UI" mode is no longer automatically forced either
|
|
46
|
+
|
|
47
|
+
### More
|
|
48
|
+
- Large refactor of the UI2 `LayoutManager` + new `OutputBuffer` for cleaner CLI output line handling
|
|
49
|
+
- Agent progress-emission refactor for more consistent spinner/tool state reporting across Web, CLI, and channel UIs
|
|
50
|
+
- Removed the `safe_shell_spec` and `shell_spec` suites; replaced with a single, comprehensive `terminal_spec` (500+ lines of coverage)
|
|
51
|
+
|
|
10
52
|
## [0.9.34] - 2026-04-21
|
|
11
53
|
|
|
12
54
|
### Added
|
|
@@ -41,7 +41,7 @@ module Clacky
|
|
|
41
41
|
token_data = collect_iteration_tokens(usage, iteration_cost)
|
|
42
42
|
|
|
43
43
|
# Update session bar cost in real-time (don't wait for agent.run to finish)
|
|
44
|
-
@ui&.update_sessionbar(cost: @total_cost)
|
|
44
|
+
@ui&.update_sessionbar(cost: @total_cost, cost_source: @cost_source)
|
|
45
45
|
|
|
46
46
|
# Track cache usage statistics (global)
|
|
47
47
|
@cache_stats[:total_requests] += 1
|
|
@@ -35,12 +35,20 @@ module Clacky
|
|
|
35
35
|
# user experiences no extra delay.
|
|
36
36
|
#
|
|
37
37
|
# @return [Hash] API response with :content, :tool_calls, :usage, etc.
|
|
38
|
+
# NOTE on progress lifecycle:
|
|
39
|
+
# call_llm intentionally does NOT start or stop the progress indicator.
|
|
40
|
+
# Ownership lives with the caller (Agent#think for normal/compression
|
|
41
|
+
# paths, Agent#trigger_idle_compression for idle compression). This
|
|
42
|
+
# avoids nested active/done pairs clobbering each other — a bug that
|
|
43
|
+
# silently dropped the idle-compression summary line.
|
|
44
|
+
#
|
|
45
|
+
# Inside call_llm we only *update in place* during retries, so the
|
|
46
|
+
# already-live progress slot shows meaningful transient status
|
|
47
|
+
# ("Network failed… attempt 2/10", etc.).
|
|
38
48
|
private def call_llm
|
|
39
49
|
# Transition :fallback_active → :probing if cooling-off has expired.
|
|
40
50
|
@config.maybe_start_probing
|
|
41
51
|
|
|
42
|
-
@ui&.show_progress
|
|
43
|
-
|
|
44
52
|
tools_to_send = @tool_registry.all_definitions
|
|
45
53
|
|
|
46
54
|
max_retries = 10
|
|
@@ -68,7 +76,6 @@ module Clacky
|
|
|
68
76
|
handle_probe_success if @config.probing?
|
|
69
77
|
|
|
70
78
|
rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Faraday::SSLError, Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
|
|
71
|
-
@ui&.show_progress(phase: "done")
|
|
72
79
|
retries += 1
|
|
73
80
|
|
|
74
81
|
# Probing failure: primary still down — renew cooling-off and retry with fallback.
|
|
@@ -90,13 +97,12 @@ module Clacky
|
|
|
90
97
|
sleep retry_delay
|
|
91
98
|
retry
|
|
92
99
|
else
|
|
93
|
-
|
|
94
|
-
#
|
|
100
|
+
# Don't show_error here — let the outer rescue block handle it to avoid duplicates.
|
|
101
|
+
# Progress cleanup is the caller's responsibility (via its own ensure block).
|
|
95
102
|
raise AgentError, "[LLM] Network connection failed after #{max_retries} retries: #{e.message}"
|
|
96
103
|
end
|
|
97
104
|
|
|
98
105
|
rescue RetryableError => e
|
|
99
|
-
@ui&.show_progress(phase: "done")
|
|
100
106
|
retries += 1
|
|
101
107
|
|
|
102
108
|
# Probing failure: primary still down — renew cooling-off and retry with fallback.
|
|
@@ -127,12 +133,10 @@ module Clacky
|
|
|
127
133
|
sleep retry_delay
|
|
128
134
|
retry
|
|
129
135
|
else
|
|
130
|
-
|
|
131
|
-
#
|
|
136
|
+
# Don't show_error here — let the outer rescue block handle it to avoid duplicates.
|
|
137
|
+
# Progress cleanup is the caller's responsibility (via its own ensure block).
|
|
132
138
|
raise AgentError, "[LLM] Service unavailable after #{current_max} retries"
|
|
133
139
|
end
|
|
134
|
-
ensure
|
|
135
|
-
@ui&.show_progress(phase: "done")
|
|
136
140
|
end
|
|
137
141
|
|
|
138
142
|
# Track cost and collect token usage data.
|
|
@@ -130,7 +130,7 @@ module Clacky
|
|
|
130
130
|
For each qualifying topic:
|
|
131
131
|
a. If a matching file exists → read it with `file_reader(path: "~/.clacky/memories/<filename>")`, then write an updated version (merge new + old, drop stale)
|
|
132
132
|
b. If no matching file → create a new one at `~/.clacky/memories/<new-filename>.md`
|
|
133
|
-
Use the `write` tool to save each file. Do NOT use `
|
|
133
|
+
Use the `write` tool to save each file. Do NOT use `terminal` or `file_reader` to list the directory.
|
|
134
134
|
|
|
135
135
|
## Hard constraints (CRITICAL)
|
|
136
136
|
- Each file MUST stay under 4000 characters of content (after the frontmatter)
|
|
@@ -10,6 +10,7 @@ module Clacky
|
|
|
10
10
|
def restore_session(session_data)
|
|
11
11
|
@session_id = session_data[:session_id]
|
|
12
12
|
@name = session_data[:name] || ""
|
|
13
|
+
@pinned = session_data[:pinned] || false
|
|
13
14
|
@history = MessageHistory.new(session_data[:messages] || [])
|
|
14
15
|
@todos = session_data[:todos] || [] # Restore todos from session
|
|
15
16
|
@iterations = session_data.dig(:stats, :total_iterations) || 0
|
|
@@ -77,6 +78,7 @@ module Clacky
|
|
|
77
78
|
{
|
|
78
79
|
session_id: @session_id,
|
|
79
80
|
name: @name,
|
|
81
|
+
pinned: @pinned,
|
|
80
82
|
created_at: @created_at,
|
|
81
83
|
updated_at: Time.now.iso8601,
|
|
82
84
|
working_dir: @working_dir,
|
|
@@ -68,15 +68,27 @@ module Clacky
|
|
|
68
68
|
all_skills = all_skills.reject(&:invalid?)
|
|
69
69
|
auto_invocable = all_skills.select(&:model_invocation_allowed?)
|
|
70
70
|
|
|
71
|
-
# Enforce system prompt injection limit to control token usage
|
|
71
|
+
# Enforce system prompt injection limit to control token usage.
|
|
72
|
+
# Warn only when the set of dropped skills *changes* — this message
|
|
73
|
+
# is otherwise emitted once per agent turn (build_skill_context is
|
|
74
|
+
# called during every system prompt assembly) and floods the log.
|
|
72
75
|
if auto_invocable.size > MAX_CONTEXT_SKILLS
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
kept = auto_invocable.first(MAX_CONTEXT_SKILLS)
|
|
77
|
+
dropped = auto_invocable.drop(MAX_CONTEXT_SKILLS)
|
|
78
|
+
dropped_names = dropped.map(&:identifier)
|
|
79
|
+
signature = dropped_names.sort.join(",")
|
|
80
|
+
|
|
81
|
+
if @skill_limit_warned_signature != signature
|
|
82
|
+
@skill_limit_warned_signature = signature
|
|
83
|
+
Clacky::Logger.warn(
|
|
84
|
+
"Skill context limit: #{auto_invocable.size} auto-invocable skills found, " \
|
|
85
|
+
"only injecting first #{MAX_CONTEXT_SKILLS} " \
|
|
86
|
+
"(#{dropped.size} dropped — will NOT be auto-discovered by the agent: " \
|
|
87
|
+
"#{dropped_names.join(", ")}). " \
|
|
88
|
+
"Remove unused skills to restore full visibility."
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
auto_invocable = kept
|
|
80
92
|
end
|
|
81
93
|
|
|
82
94
|
return "" if auto_invocable.empty?
|
|
@@ -456,7 +468,7 @@ module Clacky
|
|
|
456
468
|
# the real cumulative spend across all subagents
|
|
457
469
|
subagent_cost = result[:total_cost_usd] || 0.0
|
|
458
470
|
@total_cost += subagent_cost
|
|
459
|
-
@ui&.update_sessionbar(cost: @total_cost)
|
|
471
|
+
@ui&.update_sessionbar(cost: @total_cost, cost_source: @cost_source)
|
|
460
472
|
|
|
461
473
|
# Log completion
|
|
462
474
|
@ui&.show_info("Subagent completed: #{result[:iterations]} iterations, $#{subagent_cost.round(4)} (total: $#{@total_cost.round(4)})")
|
|
@@ -21,7 +21,7 @@ module Clacky
|
|
|
21
21
|
# confirm_all → human present, truly wait for user input
|
|
22
22
|
true
|
|
23
23
|
when :confirm_safes
|
|
24
|
-
# Use
|
|
24
|
+
# Use Security module to check auto-execution safety
|
|
25
25
|
is_safe_operation?(tool_name, tool_params)
|
|
26
26
|
else
|
|
27
27
|
false
|
|
@@ -33,13 +33,14 @@ module Clacky
|
|
|
33
33
|
# @param tool_params [Hash, String] Tool parameters
|
|
34
34
|
# @return [Boolean] true if safe operation
|
|
35
35
|
def is_safe_operation?(tool_name, tool_params = {})
|
|
36
|
-
# For
|
|
37
|
-
if tool_name.to_s.downcase == '
|
|
36
|
+
# For terminal commands, defer to Security layer for the verdict.
|
|
37
|
+
if tool_name.to_s.downcase == 'terminal'
|
|
38
38
|
params = tool_params.is_a?(String) ? JSON.parse(tool_params) : tool_params
|
|
39
39
|
command = params[:command] || params['command']
|
|
40
|
-
|
|
40
|
+
# No command = session_id continuation / kill / action → safe by default.
|
|
41
|
+
return true unless command
|
|
41
42
|
|
|
42
|
-
return Tools::
|
|
43
|
+
return Clacky::Tools::Security.command_safe_for_auto_execution?(command)
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
if tool_name.to_s.downcase == 'edit' || tool_name.to_s.downcase == 'write'
|
|
@@ -140,10 +141,10 @@ module Clacky
|
|
|
140
141
|
else
|
|
141
142
|
"Write(#{filename}) - create new"
|
|
142
143
|
end
|
|
143
|
-
when "
|
|
144
|
+
when "terminal"
|
|
144
145
|
cmd = args[:command] || ''
|
|
145
146
|
display_cmd = cmd.length > 30 ? "#{cmd[0..27]}..." : cmd
|
|
146
|
-
"
|
|
147
|
+
"terminal(\"#{display_cmd}\")"
|
|
147
148
|
else
|
|
148
149
|
"Allow #{call[:name]}"
|
|
149
150
|
end
|
|
@@ -241,7 +242,7 @@ module Clacky
|
|
|
241
242
|
# @return [Boolean] true if tool is potentially slow
|
|
242
243
|
private def potentially_slow_tool?(tool_name, args)
|
|
243
244
|
case tool_name.to_s.downcase
|
|
244
|
-
when '
|
|
245
|
+
when 'terminal'
|
|
245
246
|
# Check if the command is a slow command
|
|
246
247
|
command = args[:command] || args['command']
|
|
247
248
|
return false unless command
|
|
@@ -257,24 +258,20 @@ module Clacky
|
|
|
257
258
|
/make\s+(test|build)/,
|
|
258
259
|
/pytest/,
|
|
259
260
|
/jest/,
|
|
260
|
-
/sleep\s+\d+/
|
|
261
|
+
/sleep\s+\d+/
|
|
261
262
|
]
|
|
262
263
|
|
|
263
264
|
slow_patterns.any? { |pattern| command.match?(pattern) }
|
|
264
265
|
when 'web_fetch', 'web_search'
|
|
265
|
-
true
|
|
266
|
+
true
|
|
266
267
|
else
|
|
267
|
-
false
|
|
268
|
+
false
|
|
268
269
|
end
|
|
269
270
|
end
|
|
270
271
|
|
|
271
|
-
# Build progress message for tool execution
|
|
272
|
-
# @param tool_name [String] Name of the tool
|
|
273
|
-
# @param args [Hash] Tool arguments
|
|
274
|
-
# @return [String] Progress message
|
|
275
272
|
private def build_tool_progress_message(tool_name, args)
|
|
276
273
|
case tool_name.to_s.downcase
|
|
277
|
-
when '
|
|
274
|
+
when 'terminal'
|
|
278
275
|
"Running command"
|
|
279
276
|
when 'web_fetch'
|
|
280
277
|
"Fetching web page"
|
|
@@ -11,9 +11,6 @@ module Clacky
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def get(name)
|
|
14
|
-
# Handle shell alias to safe_shell for backward compatibility
|
|
15
|
-
name = 'safe_shell' if name == 'shell' && @tools.key?('safe_shell') && !@tools.key?('shell')
|
|
16
|
-
|
|
17
14
|
@tools[name] || raise(Clacky::ToolCallError, "Tool not found: #{name}")
|
|
18
15
|
end
|
|
19
16
|
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -43,6 +43,7 @@ module Clacky
|
|
|
43
43
|
attr_reader :session_id, :name, :history, :iterations, :total_cost, :working_dir, :created_at, :total_tasks, :todos,
|
|
44
44
|
:cache_stats, :cost_source, :ui, :skill_loader, :agent_profile,
|
|
45
45
|
:status, :error, :updated_at, :source
|
|
46
|
+
attr_accessor :pinned
|
|
46
47
|
|
|
47
48
|
def permission_mode
|
|
48
49
|
@config&.permission_mode&.to_s || ""
|
|
@@ -57,6 +58,7 @@ module Clacky
|
|
|
57
58
|
@hooks = HookManager.new
|
|
58
59
|
@session_id = session_id
|
|
59
60
|
@name = ""
|
|
61
|
+
@pinned = false
|
|
60
62
|
@history = MessageHistory.new
|
|
61
63
|
@todos = [] # Store todos in memory
|
|
62
64
|
@iterations = 0
|
|
@@ -129,14 +131,21 @@ module Clacky
|
|
|
129
131
|
@hooks.add(event, &block)
|
|
130
132
|
end
|
|
131
133
|
|
|
132
|
-
# Switch to a different model by
|
|
133
|
-
#
|
|
134
|
+
# Switch this session to a different model, identified by its stable
|
|
135
|
+
# runtime id. Ids survive list reorders, additions, and field edits,
|
|
136
|
+
# which is why we no longer expose an index-based API.
|
|
137
|
+
# @param id [String] Model id (see AgentConfig#parse_models)
|
|
134
138
|
# @return [Boolean] true if switched successfully, false otherwise
|
|
135
|
-
def
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
def switch_model_by_id(id)
|
|
140
|
+
return false unless @config.switch_model_by_id(id)
|
|
141
|
+
|
|
142
|
+
rebuild_client_for_current_model!
|
|
143
|
+
true
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Rebuild the underlying Client (and dependent components) to pick up
|
|
147
|
+
# credentials/model name from the currently-selected model in @config.
|
|
148
|
+
private def rebuild_client_for_current_model!
|
|
140
149
|
@client = Clacky::Client.new(
|
|
141
150
|
@config.api_key,
|
|
142
151
|
base_url: @config.base_url,
|
|
@@ -145,11 +154,9 @@ module Clacky
|
|
|
145
154
|
)
|
|
146
155
|
# Update message compressor with new client and model
|
|
147
156
|
@message_compressor = MessageCompressor.new(@client, model: current_model)
|
|
148
|
-
|
|
157
|
+
|
|
149
158
|
# Inject a new session context to notify the AI of the model switch
|
|
150
159
|
inject_session_context
|
|
151
|
-
|
|
152
|
-
true
|
|
153
160
|
end
|
|
154
161
|
|
|
155
162
|
# Change the working directory for this session
|
|
@@ -188,6 +195,14 @@ module Clacky
|
|
|
188
195
|
end
|
|
189
196
|
|
|
190
197
|
def run(user_input, files: [])
|
|
198
|
+
# Show the "thinking" indicator as early as possible so the user gets
|
|
199
|
+
# immediate feedback after sending a message. Without this the UI stays
|
|
200
|
+
# silent during synchronous setup work (system prompt assembly, file
|
|
201
|
+
# parsing, history compression checks) before the first LLM call. The
|
|
202
|
+
# subsequent `think` call will re-emit show_progress, which is an
|
|
203
|
+
# idempotent update on the same progress UI element.
|
|
204
|
+
@ui&.show_progress
|
|
205
|
+
|
|
191
206
|
# Start new task for Time Machine
|
|
192
207
|
task_id = start_new_task
|
|
193
208
|
|
|
@@ -477,6 +492,11 @@ module Clacky
|
|
|
477
492
|
# Always clean up memory update messages, even if interrupted or error occurred
|
|
478
493
|
cleanup_memory_messages
|
|
479
494
|
|
|
495
|
+
# Safety net: ensure any lingering progress spinner is stopped.
|
|
496
|
+
# Normal paths close their own spinners; this guards against exceptions
|
|
497
|
+
# raised between a progress slot's active/done pair.
|
|
498
|
+
@ui&.show_progress(phase: "done")
|
|
499
|
+
|
|
480
500
|
# Shred any decrypted-script tmpdirs created during this run for encrypted brand skills.
|
|
481
501
|
# This covers the inline-injection path; the subagent path shreds immediately after
|
|
482
502
|
# subagent.run returns (see execute_skill_with_subagent).
|
|
@@ -491,6 +511,12 @@ module Clacky
|
|
|
491
511
|
raise AgentError, "API key is not configured"
|
|
492
512
|
end
|
|
493
513
|
|
|
514
|
+
# Ensure a thinking progress indicator is live for the duration of this
|
|
515
|
+
# LLM turn. This is idempotent — if `run` already started one at task
|
|
516
|
+
# entry (or a previous iteration left one running), the UI recognizes
|
|
517
|
+
# the bare reentry and preserves the existing spinner.
|
|
518
|
+
@ui&.show_progress
|
|
519
|
+
|
|
494
520
|
# Check if compression is needed
|
|
495
521
|
compression_context = compress_messages_if_needed(force: false)
|
|
496
522
|
|
|
@@ -500,6 +526,14 @@ module Clacky
|
|
|
500
526
|
@ui&.show_info(
|
|
501
527
|
"Message history compression starting (~#{compression_context[:original_token_count]} tokens, #{compression_context[:original_message_count]} messages) - Level #{compression_context[:compression_level]}"
|
|
502
528
|
)
|
|
529
|
+
# Take over the progress slot with a compression-specific message.
|
|
530
|
+
# handle_compression_response will close it with the summary line on
|
|
531
|
+
# success; on failure the outer ensure below finalizes it.
|
|
532
|
+
@ui&.show_progress(
|
|
533
|
+
"Compressing message history...",
|
|
534
|
+
progress_type: "idle_compress",
|
|
535
|
+
phase: "active"
|
|
536
|
+
)
|
|
503
537
|
compression_message = compression_context[:compression_message]
|
|
504
538
|
@history.append(compression_message)
|
|
505
539
|
compression_handled = false
|
|
@@ -518,13 +552,24 @@ module Clacky
|
|
|
518
552
|
# (with the user's new message as the last entry), producing consecutive user messages
|
|
519
553
|
# that confuse the LLM into echoing compression instructions.
|
|
520
554
|
@compression_level -= 1
|
|
555
|
+
# Close the compression progress slot so the spinner does not linger.
|
|
556
|
+
@ui&.show_progress(phase: "done")
|
|
521
557
|
end
|
|
522
558
|
end
|
|
523
559
|
return nil
|
|
524
560
|
end
|
|
525
561
|
|
|
526
|
-
# Normal LLM call
|
|
527
|
-
|
|
562
|
+
# Normal LLM call. call_llm no longer manages the progress lifecycle;
|
|
563
|
+
# we keep the spinner live across the call and finalize it here so the
|
|
564
|
+
# UI transitions cleanly to the assistant message that follows.
|
|
565
|
+
response = nil
|
|
566
|
+
begin
|
|
567
|
+
response = call_llm
|
|
568
|
+
rescue
|
|
569
|
+
# Ensure the spinner is stopped on any error path before it bubbles up.
|
|
570
|
+
@ui&.show_progress(phase: "done")
|
|
571
|
+
raise
|
|
572
|
+
end
|
|
528
573
|
|
|
529
574
|
# Handle truncated responses (when max_tokens limit is reached)
|
|
530
575
|
if response[:finish_reason] == "length"
|
|
@@ -533,6 +578,7 @@ module Clacky
|
|
|
533
578
|
|
|
534
579
|
if @task_truncation_count >= 3
|
|
535
580
|
# Too many truncations - task is too complex
|
|
581
|
+
@ui&.show_progress(phase: "done")
|
|
536
582
|
@ui&.show_error("Response truncated multiple times. Task is too complex.")
|
|
537
583
|
|
|
538
584
|
# Create a response that tells the user to break down the task
|
|
@@ -578,6 +624,9 @@ module Clacky
|
|
|
578
624
|
truncated: true
|
|
579
625
|
})
|
|
580
626
|
|
|
627
|
+
# Close the current spinner so the warning appears cleanly;
|
|
628
|
+
# the recursive think() call below will reopen a new one.
|
|
629
|
+
@ui&.show_progress(phase: "done")
|
|
581
630
|
@ui&.show_warning("Response truncated (#{@task_truncation_count}/3). Retrying with smaller steps...")
|
|
582
631
|
|
|
583
632
|
# Recursively retry
|
|
@@ -600,6 +649,11 @@ module Clacky
|
|
|
600
649
|
msg[:reasoning_content] = response[:reasoning_content] if response[:reasoning_content]
|
|
601
650
|
@history.append(msg)
|
|
602
651
|
|
|
652
|
+
# Close the thinking spinner before returning. The caller (run loop)
|
|
653
|
+
# is about to render the assistant message and/or tool invocations,
|
|
654
|
+
# which should appear after the spinner disappears.
|
|
655
|
+
@ui&.show_progress(phase: "done")
|
|
656
|
+
|
|
603
657
|
response
|
|
604
658
|
end
|
|
605
659
|
|
|
@@ -686,46 +740,24 @@ module Clacky
|
|
|
686
740
|
args[:agent] = self
|
|
687
741
|
end
|
|
688
742
|
|
|
689
|
-
# For safe_shell, skip safety check if user has already confirmed
|
|
690
|
-
if call[:name] == "safe_shell" || call[:name] == "shell"
|
|
691
|
-
args[:skip_safety_check] = true
|
|
692
|
-
end
|
|
693
|
-
|
|
694
743
|
# Inject working_dir so tools don't rely on Dir.chdir global state
|
|
695
744
|
args[:working_dir] = @working_dir if @working_dir
|
|
696
745
|
|
|
697
|
-
#
|
|
746
|
+
# Show progress immediately for every tool execution so the user
|
|
747
|
+
# always knows the agent is working. (Previously we deferred this by
|
|
748
|
+
# 2 seconds to avoid flicker in the legacy CLI TUI; that trade-off is
|
|
749
|
+
# no longer desirable now that progress is a first-class UI state in
|
|
750
|
+
# the Web UI and structured JSON UIs.)
|
|
698
751
|
progress_shown = false
|
|
699
|
-
progress_timer = nil
|
|
700
|
-
|
|
701
752
|
if @ui
|
|
702
753
|
progress_message = build_tool_progress_message(call[:name], args)
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
# The callback fires immediately on each read_nonblock chunk — no polling delay.
|
|
706
|
-
if (call[:name] == "shell" || call[:name] == "safe_shell") &&
|
|
707
|
-
@ui.respond_to?(:show_tool_stdout)
|
|
708
|
-
args[:on_output] = ->(stream, data) {
|
|
709
|
-
@ui.show_tool_stdout([data]) if stream == :stdout
|
|
710
|
-
}
|
|
711
|
-
end
|
|
712
|
-
|
|
713
|
-
progress_timer = Thread.new do
|
|
714
|
-
sleep 2
|
|
715
|
-
@ui.show_progress(progress_message, prefix_newline: false)
|
|
716
|
-
progress_shown = true
|
|
717
|
-
# Streaming is handled by on_output callback — no polling loop needed here
|
|
718
|
-
end
|
|
754
|
+
@ui.show_progress(progress_message, prefix_newline: false)
|
|
755
|
+
progress_shown = true
|
|
719
756
|
end
|
|
720
757
|
|
|
721
758
|
begin
|
|
722
759
|
result = tool.execute(**args)
|
|
723
760
|
ensure
|
|
724
|
-
# Cancel timer and clear progress if shown
|
|
725
|
-
if progress_timer
|
|
726
|
-
progress_timer.kill
|
|
727
|
-
progress_timer.join
|
|
728
|
-
end
|
|
729
761
|
@ui&.show_progress(phase: "done") if progress_shown
|
|
730
762
|
end
|
|
731
763
|
|
|
@@ -927,7 +959,7 @@ module Clacky
|
|
|
927
959
|
end
|
|
928
960
|
|
|
929
961
|
private def register_builtin_tools
|
|
930
|
-
@tool_registry.register(Tools::
|
|
962
|
+
@tool_registry.register(Tools::Terminal.new)
|
|
931
963
|
@tool_registry.register(Tools::FileReader.new)
|
|
932
964
|
@tool_registry.register(Tools::Write.new)
|
|
933
965
|
@tool_registry.register(Tools::Edit.new)
|
|
@@ -936,7 +968,6 @@ module Clacky
|
|
|
936
968
|
@tool_registry.register(Tools::WebSearch.new)
|
|
937
969
|
@tool_registry.register(Tools::WebFetch.new)
|
|
938
970
|
@tool_registry.register(Tools::TodoManager.new)
|
|
939
|
-
# @tool_registry.register(Tools::RunProject.new) # temporarily disabled
|
|
940
971
|
@tool_registry.register(Tools::RequestUserFeedback.new)
|
|
941
972
|
@tool_registry.register(Tools::InvokeSkill.new)
|
|
942
973
|
@tool_registry.register(Tools::UndoTask.new)
|
|
@@ -961,16 +992,16 @@ module Clacky
|
|
|
961
992
|
if model == "lite"
|
|
962
993
|
# Special keyword: use lite model if available, otherwise fall back to default
|
|
963
994
|
lite_model = subagent_config.lite_model
|
|
964
|
-
if lite_model
|
|
965
|
-
|
|
966
|
-
subagent_config.switch_model(model_index) if model_index
|
|
995
|
+
if lite_model && lite_model["id"]
|
|
996
|
+
subagent_config.switch_model_by_id(lite_model["id"])
|
|
967
997
|
end
|
|
968
998
|
# If no lite model, just use current (default) model
|
|
969
999
|
else
|
|
970
|
-
# Regular model name lookup
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1000
|
+
# Regular model name lookup — find the first model with a matching
|
|
1001
|
+
# name and switch by its stable id.
|
|
1002
|
+
target = subagent_config.models.find { |m| m["model"] == model }
|
|
1003
|
+
if target && target["id"]
|
|
1004
|
+
subagent_config.switch_model_by_id(target["id"])
|
|
974
1005
|
else
|
|
975
1006
|
raise AgentError, "Model '#{model}' not found in config. Available models: #{subagent_config.model_names.join(', ')}"
|
|
976
1007
|
end
|
|
@@ -1250,13 +1281,13 @@ module Clacky
|
|
|
1250
1281
|
# [Download report](file:///path/to/file.pdf)
|
|
1251
1282
|
# 
|
|
1252
1283
|
#
|
|
1253
|
-
# Returns { text: String
|
|
1254
|
-
#
|
|
1284
|
+
# Returns { text: String (original content, unmodified),
|
|
1285
|
+
# files: Array<{name:, path:, inline:}> }
|
|
1255
1286
|
private def parse_file_links(content)
|
|
1256
1287
|
return { text: content, files: [] } if content.nil? || content.empty?
|
|
1257
1288
|
|
|
1258
1289
|
files = []
|
|
1259
|
-
|
|
1290
|
+
content.scan(/(!?)\[([^\]]*)\]\(file:\/\/([^)]+)\)/) do
|
|
1260
1291
|
inline = $1 == "!"
|
|
1261
1292
|
# URL-decode percent-encoded characters (e.g. Chinese filenames encoded by AI)
|
|
1262
1293
|
raw_path = CGI.unescape($3)
|
|
@@ -1264,9 +1295,8 @@ module Clacky
|
|
|
1264
1295
|
path = File.expand_path(raw_path)
|
|
1265
1296
|
Clacky::Logger.info("[parse_file_links] raw=#{$3.inspect} expanded=#{path.inspect} exist=#{File.exist?(path)}")
|
|
1266
1297
|
files << { name: name, path: path, inline: inline }
|
|
1267
|
-
""
|
|
1268
1298
|
end
|
|
1269
|
-
{ text:
|
|
1299
|
+
{ text: content, files: files }
|
|
1270
1300
|
end
|
|
1271
1301
|
|
|
1272
1302
|
# Emit assistant message to UI, parsing any embedded file:// links first.
|