openclacky 1.5.13 → 1.5.15
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 +96 -0
- data/lib/clacky/agent/chunk_index.rb +83 -0
- data/lib/clacky/agent/history_navigation.rb +239 -0
- data/lib/clacky/agent/session_serializer.rb +53 -98
- data/lib/clacky/agent.rb +477 -297
- data/lib/clacky/agent_config.rb +5 -3
- data/lib/clacky/billing/billing_store.rb +2 -2
- data/lib/clacky/billing/platform_billing.rb +7 -0
- data/lib/clacky/brand_config.rb +27 -19
- data/lib/clacky/cli.rb +92 -12
- data/lib/clacky/cli_guidance.rb +48 -0
- data/lib/clacky/client.rb +2 -2
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +70 -134
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +4 -1
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +307 -98
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +179 -577
- data/lib/clacky/default_extensions/git/panels/git/view.js +34 -14
- data/lib/clacky/default_extensions/preview/ext.yml +20 -0
- data/lib/clacky/default_extensions/preview/panels/preview/view.js +475 -0
- data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +1 -10
- data/lib/clacky/extension/api_extension.rb +18 -6
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/json_ui_controller.rb +4 -2
- data/lib/clacky/media/openai_compat.rb +70 -30
- data/lib/clacky/message_format/bedrock.rb +6 -1
- data/lib/clacky/message_format/open_ai.rb +22 -5
- data/lib/clacky/message_format/open_ai_responses.rb +7 -3
- data/lib/clacky/plain_ui_controller.rb +1 -1
- data/lib/clacky/prompts/base.md +1 -1
- data/lib/clacky/providers.rb +44 -13
- data/lib/clacky/rich_ui/components/composer_guidance.rb +49 -0
- data/lib/clacky/rich_ui/rich_ui_controller.rb +24 -4
- data/lib/clacky/rich_ui/shell/rich_agent_shell.rb +13 -0
- data/lib/clacky/search_config.rb +3 -3
- data/lib/clacky/server/channel/channel_manager.rb +25 -7
- data/lib/clacky/server/channel/channel_ui_controller.rb +1 -1
- data/lib/clacky/server/dir_picker.rb +67 -4
- data/lib/clacky/server/git_panel.rb +10 -2
- data/lib/clacky/server/http_server.rb +391 -75
- data/lib/clacky/server/preview.rb +351 -0
- data/lib/clacky/server/session_registry.rb +4 -0
- data/lib/clacky/server/web_ui_controller.rb +10 -5
- data/lib/clacky/session_manager.rb +52 -1
- data/lib/clacky/skill.rb +48 -0
- data/lib/clacky/tools/browser.rb +176 -19
- data/lib/clacky/tools/web_search.rb +91 -8
- data/lib/clacky/ui2/components/command_suggestions.rb +3 -1
- data/lib/clacky/ui2/components/input_area.rb +37 -4
- data/lib/clacky/ui2/components/modal_component.rb +35 -6
- data/lib/clacky/ui2/screen_buffer.rb +1 -0
- data/lib/clacky/ui2/ui_controller.rb +72 -12
- data/lib/clacky/ui_interface.rb +1 -1
- data/lib/clacky/utils/file_processor.rb +19 -7
- data/lib/clacky/utils/mac_app_detector.rb +186 -0
- data/lib/clacky/utils/model_pricing.rb +118 -68
- data/lib/clacky/utils/windows_app_detector.rb +334 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +998 -168
- data/lib/clacky/web/app.js +19 -0
- data/lib/clacky/web/components/chat-navigator.js +489 -202
- data/lib/clacky/web/components/code-editor.js +191 -5
- data/lib/clacky/web/components/composer.js +3 -1
- data/lib/clacky/web/components/datepicker.js +19 -19
- data/lib/clacky/web/components/mentions.js +11 -14
- data/lib/clacky/web/components/model-picker.js +66 -23
- data/lib/clacky/web/components/quote-select.js +341 -0
- data/lib/clacky/web/core/aside.js +157 -8
- data/lib/clacky/web/core/ext.js +16 -0
- data/lib/clacky/web/features/billing/view.js +75 -21
- data/lib/clacky/web/features/extensions/store.js +4 -1
- data/lib/clacky/web/features/model-tester/store.js +37 -4
- data/lib/clacky/web/features/skills/store.js +18 -1
- data/lib/clacky/web/features/skills/view.js +128 -7
- data/lib/clacky/web/features/workspace/store.js +80 -7
- data/lib/clacky/web/features/workspace/view.js +474 -43
- data/lib/clacky/web/i18n.js +158 -23
- data/lib/clacky/web/index.html +57 -29
- data/lib/clacky/web/sessions.js +517 -127
- data/lib/clacky/web/settings.js +155 -6
- data/lib/clacky/web/utils.js +34 -0
- data/lib/clacky/web/vendor/codemirror/codemirror.min.js +24 -19
- data/lib/clacky/web/vendor/codemirror/entry.js +130 -0
- data/lib/clacky/web/vendor/codemirror/package.json +29 -0
- data/lib/clacky/web/ws-dispatcher.js +161 -4
- data/lib/clacky.rb +2 -0
- metadata +13 -1
|
@@ -272,7 +272,7 @@ module Clacky
|
|
|
272
272
|
end
|
|
273
273
|
|
|
274
274
|
# Handle built-in commands
|
|
275
|
-
if text&.match?(KNOWN_COMMAND)
|
|
275
|
+
if text&.match?(KNOWN_COMMAND)
|
|
276
276
|
handle_command(adapter, event, text)
|
|
277
277
|
return
|
|
278
278
|
end
|
|
@@ -294,6 +294,18 @@ module Clacky
|
|
|
294
294
|
sub_count = web_ui_for_session_diag(session_id)
|
|
295
295
|
Clacky::Logger.info("[ChannelManager] Routing to session #{session_id[0, 8]} (status=#{session[:status]}, text=#{text.inspect}, channel_subs=#{sub_count})")
|
|
296
296
|
|
|
297
|
+
if session[:status] == :running && %w[queue steer].include?(@registry.agent_config_input_behavior)
|
|
298
|
+
queued = false
|
|
299
|
+
@registry.with_session(session_id) do |s|
|
|
300
|
+
if s[:status] == :running
|
|
301
|
+
s[:agent].enqueue_input(build_prompt_with_context(event, text), delivery: @registry.agent_config_input_behavior.to_sym, files: files,
|
|
302
|
+
display_text: text, source: :channel, created_at: Time.now.to_f)
|
|
303
|
+
queued = true
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
return if queued
|
|
307
|
+
end
|
|
308
|
+
|
|
297
309
|
# If session is running, interrupt it AND wait for the old thread to
|
|
298
310
|
# actually unwind before starting a new task. Without the join, two
|
|
299
311
|
# threads briefly race on the same agent/history and the old thread
|
|
@@ -301,7 +313,7 @@ module Clacky
|
|
|
301
313
|
# ships to the LLM with no matching tool result — DeepSeek (strict
|
|
302
314
|
# OpenAI-compat) rejects this with HTTP 400 "insufficient tool
|
|
303
315
|
# messages following tool_calls message". CLI already waits via
|
|
304
|
-
# join(2);
|
|
316
|
+
# join(2); if shutdown is slow, retain the input instead of overlapping work.
|
|
305
317
|
if session[:status] == :running
|
|
306
318
|
Clacky::Logger.info("[ChannelManager] Session busy, interrupting previous task")
|
|
307
319
|
old_thread = nil
|
|
@@ -310,7 +322,10 @@ module Clacky
|
|
|
310
322
|
if old_thread&.alive?
|
|
311
323
|
old_thread.join(2)
|
|
312
324
|
if old_thread.alive?
|
|
313
|
-
|
|
325
|
+
session[:agent].enqueue_input(build_prompt_with_context(event, text), files: files,
|
|
326
|
+
display_text: text, source: :channel, created_at: Time.now.to_f)
|
|
327
|
+
adapter.send_text(event[:chat_id], "The current task is still stopping. Your message remains queued.")
|
|
328
|
+
return
|
|
314
329
|
end
|
|
315
330
|
end
|
|
316
331
|
end
|
|
@@ -366,7 +381,7 @@ module Clacky
|
|
|
366
381
|
key = channel_key(event)
|
|
367
382
|
|
|
368
383
|
case text
|
|
369
|
-
when
|
|
384
|
+
when %r{\A/([??]|h|help)\z}i
|
|
370
385
|
adapter.send_text(chat_id, COMMAND_HELP)
|
|
371
386
|
|
|
372
387
|
when "/new", "/clear"
|
|
@@ -506,15 +521,18 @@ module Clacky
|
|
|
506
521
|
list_sessions(adapter, chat_id)
|
|
507
522
|
|
|
508
523
|
else
|
|
509
|
-
adapter.send_text(chat_id, "Unknown command. Type
|
|
524
|
+
adapter.send_text(chat_id, "Unknown command. Type /? for help.")
|
|
510
525
|
end
|
|
511
526
|
end
|
|
512
527
|
|
|
513
|
-
|
|
528
|
+
# "/?" is anchored with \z instead of \b because \b never matches after
|
|
529
|
+
# "?" (not a word character), which would let "/?extra" through.
|
|
530
|
+
# "?" is accepted too since Chinese IMEs produce the full-width form.
|
|
531
|
+
KNOWN_COMMAND = %r{\A/(?:[??]\z|(?:h|help|new|clear|model|skills|bind|stop|unbind|status|list)\b)}i
|
|
514
532
|
|
|
515
533
|
COMMAND_HELP = <<~HELP.strip
|
|
516
534
|
Commands:
|
|
517
|
-
|
|
535
|
+
/? / /h / /help - show this help
|
|
518
536
|
/new / /clear - start a new session
|
|
519
537
|
/model - show current model, cards & quick-switch list
|
|
520
538
|
/model <n> - switch card by number
|
|
@@ -12,15 +12,18 @@ module Clacky
|
|
|
12
12
|
# Translated client-side via `id`. On WSL every favorite targets the
|
|
13
13
|
# Windows profile (/mnt/<drive>/Users/<name>/...) so they match where a
|
|
14
14
|
# Windows browser actually saves files; elsewhere they stay under Dir.home.
|
|
15
|
+
# The Windows shell folders win over the guessed profile when available —
|
|
16
|
+
# they are the folders the user actually sees in Explorer.
|
|
15
17
|
private def dir_picker_places
|
|
16
18
|
home = Dir.home
|
|
17
|
-
|
|
19
|
+
folders = wsl_shell_folders
|
|
20
|
+
win_home = folders["profile"] || wsl_windows_home
|
|
18
21
|
places = []
|
|
19
22
|
|
|
20
23
|
[["home", win_home || home],
|
|
21
|
-
["desktop", File.join(win_home || home, "Desktop")],
|
|
22
|
-
["downloads", File.join(win_home || home, "Downloads")],
|
|
23
|
-
["documents", File.join(win_home || home, "Documents")]].each do |id, path|
|
|
24
|
+
["desktop", folders["desktop"] || File.join(win_home || home, "Desktop")],
|
|
25
|
+
["downloads", folders["downloads"] || File.join(win_home || home, "Downloads")],
|
|
26
|
+
["documents", folders["documents"] || File.join(win_home || home, "Documents")]].each do |id, path|
|
|
24
27
|
places << { id: id, path: path, kind: "favorite" } if Dir.exist?(path)
|
|
25
28
|
end
|
|
26
29
|
|
|
@@ -28,6 +31,66 @@ module Clacky
|
|
|
28
31
|
places
|
|
29
32
|
end
|
|
30
33
|
|
|
34
|
+
# Windows "shell folders" — the profile plus Desktop/Documents/Downloads —
|
|
35
|
+
# are routinely redirected away from C:\Users\<name>: OneDrive's "back up
|
|
36
|
+
# folders", Huawei PC Manager's file migration (which moves a profile to
|
|
37
|
+
# D:\HuaweiMoveData\Users\<name>), group-policy folder redirection. Guessing
|
|
38
|
+
# "<profile>/Desktop" then lands on the empty leftover shell, so the picker
|
|
39
|
+
# shows an empty directory while Explorer is full of files. Ask Windows for
|
|
40
|
+
# the real paths; the profile guess stays as the fallback for every entry.
|
|
41
|
+
# Cached — the spawn costs 0.5–1.5s and these paths never change while the
|
|
42
|
+
# process runs.
|
|
43
|
+
private def wsl_shell_folders
|
|
44
|
+
return @wsl_shell_folders if defined?(@wsl_shell_folders)
|
|
45
|
+
|
|
46
|
+
@wsl_shell_folders = wsl? ? (wsl_shell_folders_from_powershell || {}) : {}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# One PowerShell run for all four paths so the picker pays the process
|
|
50
|
+
# spawn once. Returns nil when interop is unavailable or nothing resolved,
|
|
51
|
+
# so callers fall back to guessing from the profile.
|
|
52
|
+
private def wsl_shell_folders_from_powershell
|
|
53
|
+
script = '$f=[Environment];' \
|
|
54
|
+
'$f::GetFolderPath("UserProfile");' \
|
|
55
|
+
'$f::GetFolderPath("Desktop");' \
|
|
56
|
+
'$f::GetFolderPath("MyDocuments");' \
|
|
57
|
+
'try { (New-Object -ComObject Shell.Application).NameSpace("shell:Downloads").Self.Path } catch { "" }'
|
|
58
|
+
require "open3"
|
|
59
|
+
out, _err, status = Open3.capture3(
|
|
60
|
+
"powershell.exe", "-NoProfile", "-NonInteractive", "-Command", script, binmode: true
|
|
61
|
+
)
|
|
62
|
+
return nil unless status.success?
|
|
63
|
+
|
|
64
|
+
lines = Utils::Encoding.pty_to_utf8(out).split(/\r?\n/)
|
|
65
|
+
folders = {}
|
|
66
|
+
%w[profile desktop documents downloads].each_with_index do |key, index|
|
|
67
|
+
path = wsl_path_of(lines[index])
|
|
68
|
+
folders[key] = path if path
|
|
69
|
+
end
|
|
70
|
+
folders.empty? ? nil : folders
|
|
71
|
+
rescue StandardError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# "D:\Users\leo\Desktop" → "/mnt/d/Users/leo/Desktop". wslpath does the
|
|
76
|
+
# conversion so custom mount roots are honoured. Nil for blank values, UNC
|
|
77
|
+
# shares (WSL can't reach them) and paths not mounted in this distro.
|
|
78
|
+
private def wsl_path_of(win_path)
|
|
79
|
+
value = win_path.to_s.strip
|
|
80
|
+
return nil unless value.match?(%r{\A[A-Za-z]:[\\/]})
|
|
81
|
+
|
|
82
|
+
require "open3"
|
|
83
|
+
out, _err, status = Open3.capture3("wslpath", "-a", "-u", value, binmode: true)
|
|
84
|
+
return nil unless status.success?
|
|
85
|
+
|
|
86
|
+
linux = Utils::Encoding.pty_to_utf8(out).strip
|
|
87
|
+
return nil unless linux.start_with?("/") && Dir.exist?(linux)
|
|
88
|
+
|
|
89
|
+
linux
|
|
90
|
+
rescue StandardError
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
31
94
|
# Drive letters exposed to the picker so Windows users can reach D:/E:/…
|
|
32
95
|
# directly. Under WSL these are the /mnt/<drive> mounts; on native
|
|
33
96
|
# Windows the actual drive letters. Empty on macOS/Linux.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
4
|
|
|
5
|
+
require_relative "../utils/encoding"
|
|
6
|
+
|
|
5
7
|
module Clacky
|
|
6
8
|
module Server
|
|
7
9
|
# Read-mostly git operations scoped to a session's working directory, backing
|
|
@@ -14,9 +16,15 @@ module Clacky
|
|
|
14
16
|
|
|
15
17
|
# Run a git subcommand in `dir` with argv-style args (no shell). Returns
|
|
16
18
|
# [stdout, stderr, success_bool]. Never raises on git failure.
|
|
19
|
+
#
|
|
20
|
+
# `core.quotePath=false` keeps git from octal-escaping non-ASCII paths, so
|
|
21
|
+
# the parsers below (and the path comparisons in `restore`) see the same
|
|
22
|
+
# bytes the caller passed in. Output is then normalised to UTF-8 because
|
|
23
|
+
# git inherits the locale, and under a non-UTF-8 locale the raw bytes come
|
|
24
|
+
# back tagged US-ASCII, where String#split would raise on multibyte paths.
|
|
17
25
|
def git(dir, *args)
|
|
18
|
-
out, err, status = Open3.capture3("git", "-C", dir.to_s, *args)
|
|
19
|
-
[out, err, status.success?]
|
|
26
|
+
out, err, status = Open3.capture3("git", "-C", dir.to_s, "-c", "core.quotePath=false", *args)
|
|
27
|
+
[Utils::Encoding.to_utf8(out), Utils::Encoding.to_utf8(err), status.success?]
|
|
20
28
|
rescue StandardError => e
|
|
21
29
|
["", e.message, false]
|
|
22
30
|
end
|