openclacky 1.5.13 → 1.5.14

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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -0
  3. data/lib/clacky/agent/chunk_index.rb +83 -0
  4. data/lib/clacky/agent/history_navigation.rb +239 -0
  5. data/lib/clacky/agent/session_serializer.rb +38 -97
  6. data/lib/clacky/agent.rb +281 -276
  7. data/lib/clacky/agent_config.rb +1 -1
  8. data/lib/clacky/billing/billing_store.rb +2 -2
  9. data/lib/clacky/billing/platform_billing.rb +6 -0
  10. data/lib/clacky/brand_config.rb +13 -2
  11. data/lib/clacky/cli.rb +27 -0
  12. data/lib/clacky/client.rb +2 -2
  13. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +70 -134
  14. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +91 -5
  15. data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +179 -577
  16. data/lib/clacky/default_extensions/git/panels/git/view.js +34 -14
  17. data/lib/clacky/default_extensions/preview/ext.yml +20 -0
  18. data/lib/clacky/default_extensions/preview/panels/preview/view.js +475 -0
  19. data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +1 -10
  20. data/lib/clacky/extension/api_extension.rb +1 -1
  21. data/lib/clacky/extension/verifier.rb +1 -1
  22. data/lib/clacky/media/openai_compat.rb +70 -30
  23. data/lib/clacky/message_format/bedrock.rb +6 -1
  24. data/lib/clacky/prompts/base.md +1 -1
  25. data/lib/clacky/providers.rb +38 -3
  26. data/lib/clacky/rich_ui/rich_ui_controller.rb +1 -1
  27. data/lib/clacky/search_config.rb +3 -3
  28. data/lib/clacky/server/channel/channel_manager.rb +8 -5
  29. data/lib/clacky/server/git_panel.rb +10 -2
  30. data/lib/clacky/server/http_server.rb +161 -14
  31. data/lib/clacky/server/preview.rb +351 -0
  32. data/lib/clacky/server/web_ui_controller.rb +1 -1
  33. data/lib/clacky/skill.rb +48 -0
  34. data/lib/clacky/tools/browser.rb +176 -19
  35. data/lib/clacky/tools/web_search.rb +79 -8
  36. data/lib/clacky/ui2/components/command_suggestions.rb +2 -1
  37. data/lib/clacky/ui2/components/input_area.rb +22 -2
  38. data/lib/clacky/ui2/components/modal_component.rb +35 -6
  39. data/lib/clacky/ui2/ui_controller.rb +44 -7
  40. data/lib/clacky/utils/file_processor.rb +19 -7
  41. data/lib/clacky/utils/mac_app_detector.rb +186 -0
  42. data/lib/clacky/utils/model_pricing.rb +114 -66
  43. data/lib/clacky/utils/windows_app_detector.rb +334 -0
  44. data/lib/clacky/version.rb +1 -1
  45. data/lib/clacky/web/app.css +596 -130
  46. data/lib/clacky/web/components/chat-navigator.js +489 -202
  47. data/lib/clacky/web/components/code-editor.js +191 -5
  48. data/lib/clacky/web/components/model-picker.js +2 -1
  49. data/lib/clacky/web/components/quote-select.js +235 -0
  50. data/lib/clacky/web/core/aside.js +157 -8
  51. data/lib/clacky/web/core/ext.js +16 -0
  52. data/lib/clacky/web/features/billing/view.js +75 -21
  53. data/lib/clacky/web/features/skills/store.js +18 -1
  54. data/lib/clacky/web/features/skills/view.js +128 -7
  55. data/lib/clacky/web/features/workspace/store.js +80 -7
  56. data/lib/clacky/web/features/workspace/view.js +392 -40
  57. data/lib/clacky/web/i18n.js +90 -5
  58. data/lib/clacky/web/index.html +15 -6
  59. data/lib/clacky/web/sessions.js +193 -38
  60. data/lib/clacky/web/utils.js +34 -0
  61. data/lib/clacky/web/vendor/codemirror/codemirror.min.js +24 -19
  62. data/lib/clacky/web/vendor/codemirror/entry.js +130 -0
  63. data/lib/clacky/web/vendor/codemirror/package.json +29 -0
  64. data/lib/clacky/web/ws-dispatcher.js +20 -0
  65. data/lib/clacky.rb +2 -0
  66. metadata +11 -1
@@ -1161,7 +1161,7 @@ module Clacky
1161
1161
  # find_by_base_url only matches registered preset URLs, so localhost
1162
1162
  # and other non-openclacky endpoints correctly return nil.
1163
1163
  provider_id = Clacky::Providers.find_by_base_url(m["base_url"])
1164
- return nil unless provider_id == "openclacky"
1164
+ return nil unless provider_id == Clacky::Providers::OPENCLACKY_ID
1165
1165
 
1166
1166
  fallback_url = Clacky::Providers.fallback_base_url(provider_id)
1167
1167
  return nil unless fallback_url
@@ -20,7 +20,7 @@ module Clacky
20
20
  # list never drifts from providers.rb as models are added or removed.
21
21
  def openclacky_models
22
22
  @openclacky_models ||= begin
23
- preset = Clacky::Providers.get("openclacky")
23
+ preset = Clacky::Providers.get(Clacky::Providers::OPENCLACKY_ID)
24
24
  return [] unless preset
25
25
  OPENCLACKY_MODEL_KINDS.flat_map { |kind| preset[kind] || [] }.uniq
26
26
  end
@@ -30,7 +30,7 @@ module Clacky
30
30
  # carry an explicit provider id; legacy records (written before the
31
31
  # field existed) are detected by matching the full openclacky model list.
32
32
  def openclacky_record?(record)
33
- return true if record.provider == "openclacky"
33
+ return true if record.provider == Clacky::Providers::OPENCLACKY_ID
34
34
  openclacky_models.include?(record.model.to_s)
35
35
  end
36
36
 
@@ -17,9 +17,15 @@ module Clacky
17
17
  # "primary" id where the gateway can dispatch an alias two ways.
18
18
  ALIAS_TO_REAL = {
19
19
  # deepseek (dsk-)
20
+ "dsk-deepseek-flash" => "deepseek-flash",
20
21
  "dsk-deepseek-v4-pro" => "deepseek-v4-pro",
21
22
  "dsk-deepseek-v4-flash" => "deepseek-v4-flash",
22
23
  "dsk-deepseek-v4-flash-vision-exp" => "deepseek-v4-flash-vision-exp",
24
+ # gpt via bedrock (abs-, OpenAI-compatible endpoint)
25
+ "abs-gpt-6-astra" => "global.openai.gpt-6-astra",
26
+ "abs-gpt-5.6-sol" => "global.openai.gpt-5.6-sol",
27
+ "abs-gpt-5.6-terra" => "global.openai.gpt-5.6-terra",
28
+ "abs-gpt-5.6-luna" => "global.openai.gpt-5.6-luna",
23
29
  # claude via bedrock (abs-)
24
30
  "abs-claude-fable-5-1" => "global.anthropic.claude-fable-5-1",
25
31
  "abs-claude-fable-5" => "global.anthropic.claude-fable-5",
@@ -53,7 +53,7 @@ module Clacky
53
53
  attr_reader :product_name, :package_name, :license_key, :license_activated_at,
54
54
  :license_expires_at, :license_last_heartbeat, :device_id,
55
55
  :logo_url, :support_contact, :license_user_id,
56
- :support_qr_url, :theme_color, :homepage_url,
56
+ :support_qr_url, :theme_color,
57
57
  :distribution_last_refreshed_at, :license_last_heartbeat_failure
58
58
 
59
59
  def initialize(attrs = {})
@@ -1649,6 +1649,17 @@ module Clacky
1649
1649
  # Non-fatal — metadata write failure should not break the upload flow
1650
1650
  end
1651
1651
 
1652
+ # Vendors may configure a bare domain ("example.com"). Without a scheme the
1653
+ # browser treats it as a relative path and resolves it against the local
1654
+ # server, so normalize on read to cover every consumer at once.
1655
+ def homepage_url
1656
+ raw = @homepage_url.to_s.strip
1657
+ return nil if raw.empty?
1658
+ return raw if raw.match?(%r{\A[a-z][a-z0-9+.\-]*:}i)
1659
+
1660
+ "https://#{raw}"
1661
+ end
1662
+
1652
1663
  # Returns a hash representation for JSON serialization (e.g. /api/brand).
1653
1664
  def to_h
1654
1665
  {
@@ -1658,7 +1669,7 @@ module Clacky
1658
1669
  support_contact: @support_contact,
1659
1670
  support_qr_url: @support_qr_url,
1660
1671
  theme_color: @theme_color,
1661
- homepage_url: @homepage_url,
1672
+ homepage_url: homepage_url,
1662
1673
  branded: branded?,
1663
1674
  activated: activated?,
1664
1675
  expired: expired?,
data/lib/clacky/cli.rb CHANGED
@@ -345,6 +345,29 @@ module Clacky
345
345
  ui_controller.show_success("Switched to model: #{config.model_name}")
346
346
  end
347
347
 
348
+ # Handle the `/think` slash command — pick the reasoning effort level
349
+ # for the current session. "off" normalizes to nil (provider default),
350
+ # matching the Web UI's reasoning_effort switcher semantics.
351
+ private def handle_think_command(ui_controller, agent, session_manager = nil)
352
+ choice = ui_controller.show_reasoning_effort_menu(agent.reasoning_effort)
353
+ return if choice.nil?
354
+
355
+ agent.reasoning_effort = choice
356
+
357
+ # The override lives in the session file (not config.yml), so persist
358
+ # it now — otherwise it would be lost if the user quits before the
359
+ # next task.
360
+ session_manager&.save(agent.to_session_data(updated_at: Time.now))
361
+
362
+ # Reflect the change in the session bar (appended after the model name)
363
+ ui_controller.config[:reasoning_effort] = agent.reasoning_effort
364
+ ui_controller.update_sessionbar
365
+
366
+ current = agent.reasoning_effort
367
+ message = current ? "Thinking level set to #{current}" : "Thinking level: off (provider default)"
368
+ ui_controller.show_success(message)
369
+ end
370
+
348
371
  private def handle_time_machine_command(ui_controller, agent, session_manager)
349
372
  # Get task history from agent
350
373
  history = agent.get_task_history(limit: 10)
@@ -874,6 +897,7 @@ module Clacky
874
897
  working_dir: working_dir,
875
898
  mode: agent_config.permission_mode.to_s,
876
899
  model: agent_config.model_name,
900
+ reasoning_effort: agent.reasoning_effort,
877
901
  theme: theme_name
878
902
  )
879
903
  end
@@ -997,6 +1021,9 @@ module Clacky
997
1021
  when "/model"
998
1022
  handle_model_command(ui_controller, agent_config, agent, session_manager)
999
1023
  next
1024
+ when "/think"
1025
+ handle_think_command(ui_controller, agent, session_manager)
1026
+ next
1000
1027
  when "/undo"
1001
1028
  handle_time_machine_command(ui_controller, agent, session_manager)
1002
1029
  next
data/lib/clacky/client.rb CHANGED
@@ -720,12 +720,12 @@ module Clacky
720
720
  conn.headers["x-api-key"] = @api_key
721
721
  conn.headers["anthropic-version"] = "2023-06-01"
722
722
  conn.headers["anthropic-dangerous-direct-browser-access"] = "true"
723
- if @provider_id == "openrouter"
723
+ if @provider_id == Clacky::Providers::OPENROUTER_ID
724
724
  conn.headers["Authorization"] = "Bearer #{@api_key}"
725
725
  end
726
726
  # Moonshot's Kimi Code (Coding Plan) endpoint enforces a User-Agent
727
727
  # prefix whitelist limited to first-party coding agents.
728
- if @provider_id == "kimi-coding"
728
+ if @provider_id == Clacky::Providers::KIMI_CODING_ID
729
729
  conn.headers["User-Agent"] = "claude-cli/1.0.51 (external, cli)"
730
730
  end
731
731
  conn.options.timeout = @read_timeout || 300
@@ -1,147 +1,83 @@
1
- You are Extension Developer, an AI expert who helps users build, debug, and (when they
2
- ask) publish OpenClacky extensions through conversation. You drive the whole workflow —
3
- scaffold, edit, verify, reload — so the user never has to memorize commands or file
4
- layouts.
5
-
6
- Your role is to:
7
- - Turn a plain-language idea ("I want an extension that shows the weather") into a
8
- working extension by scaffolding it, wiring the right contributes, and iterating.
9
- - Read and edit extension files directly, then verify and hot-reload to confirm.
10
- - Debug using structured verify errors, fixing manifest and file issues.
11
- - Publish to the marketplace only when the user explicitly wants to share it.
1
+ You are Extension Developer, an AI expert who helps users build, debug, and, only
2
+ when requested, publish OpenClacky extensions through conversation.
12
3
 
13
4
  ## How you work
14
5
 
15
- The `ext-develop` skill holds the authoritative extension model, the contributes-type
16
- map, the verify error codes, the `Clacky.*` WebUI contract, the host APIs a panel can
17
- call, and the publish commands. It is your knowledge base; this prompt is only your
18
- behavior. Actively open the skill and read the relevant section at the start of any
19
- extension work — don't wait for it to surface on its own, and never work from memory when
20
- the skill has the answer. You own the flow and decide when each section applies:
21
-
22
- - **Scaffold** — when the user wants to start a new extension. Clarify the idea in one
23
- question if it's ambiguous (what should it DO, and where — a panel, a skill, an agent,
24
- a backend?), map it to the smallest set of contributes types, then `clacky ext new`.
25
- Don't over-scope: most extensions are one panel + one handler, or one skill.
26
- - **Debug & verify** — when something is broken, `verify` reports errors, or a change
27
- didn't take effect. `clacky ext verify` is your compiler; fix by error code until clean.
28
- - **Publish** — only when the user explicitly asks to share/ship it. Publishing is NOT a
29
- required step; many extensions are built for the user's own use. Never publish on your
30
- own initiative or as a "wrap up."
6
+ First call `invoke_skill` for `ext-develop`, including for an approved repair. It owns
7
+ the workflow and reference index; linked articles own interface contracts. Behavior-only
8
+ discussion needs no API inventory. Read only the current decision's reference, reuse it
9
+ while in context, and never rely on remembered fields, endpoints, or reload behavior.
31
10
 
32
- ## Working discipline (never break these)
11
+ Discuss the smallest behavior and wait for approval before scaffolding or editing. Before
12
+ approval, call no tool after the skill. Unless internals are requested, send exactly four
13
+ short sections in the user's language—Visible result, Will do, Won't do, Confirm (with at
14
+ most one material choice)—and nothing else. Never include paths, files, commands, fields,
15
+ mount points, APIs, code symbols, ids, frontend/backend (前端/后端), agent/智能体 types,
16
+ parenthetical internal translations, implementation, or verification details.
17
+ After approval, edit real files and verify relevant tests plus the actual result; manifest
18
+ success alone is insufficient. Report missing evidence plainly.
33
19
 
34
- - **Know before you speak, not just before you code.** You are the expert who is supposed
35
- to deeply understand OpenClacky extensions — that expertise comes from *looking things
36
- up*, not from memory. Before you propose a design OR write a line of code, make sure you
37
- actually have the facts: consult the `ext-develop` skill for the model and contracts,
38
- and when a field name, event, adapter method, or WebUI/API detail is anything less than
39
- certain, `web_fetch` the matching reference doc the skill points to. Never invent field
40
- names, endpoints, or behavior from memory. When in doubt, look it up one more time — a
41
- wasted lookup is cheap, a confidently wrong answer is not.
42
- - **Reuse the host before you build.** When a request touches sessions, file recovery
43
- (trash), skills, memories, scheduled tasks, billing/usage, or media, assume the host may
44
- already expose a ready-made API a panel can call — check the host-API reference the skill
45
- points to before you invent a backend. Don't rebuild what the host already provides.
46
- - **Discuss the plan first, act only after the user agrees.** Every time, walk the user
47
- through what you intend to do — what it is, where it lives, and what it will look like —
48
- in plain words, and wait for a clear yes before you scaffold or edit anything. Never
49
- quietly change files mid-conversation or scaffold before the user has signed off.
50
- - **Verify before you claim.** "It should work" is not "it works." Run `verify`, or have
51
- the user reload and confirm, before you say something is done.
52
-
53
- ## Talking to the user
20
+ ## Extension engineering rules
54
21
 
55
- Most users are not programmers. Talk to them like a helpful teammate, not a compiler.
56
- This applies to **everything** you say to them — proposing a plan, reporting what you
57
- changed, or explaining a bug and its fix. The moment you slip into raw code and API names
58
- is exactly the moment the user gets lost, and those "here's what I fixed" updates are
59
- where it happens most.
22
+ These are requirements for every design and implementation, even when the skill or
23
+ online documentation is unavailable. Do not defer them to a later documentation lookup.
60
24
 
61
- - **One language at a time.** In a Chinese conversation, speak Chinese; in an English one,
62
- speak English. Don't sprinkle the other language's technical jargon through your
63
- sentences. When a technical term is unavoidable, add a short plain-language gloss the
64
- first time it appears (e.g. "a handler — the small backend file that answers requests").
65
- - **Translate the jargon.** Words like *contributes*, *slot*, *manifest*, *handler* mean
66
- nothing to most users. Say what they DO: a panel is "a screen inside the app," a slot is
67
- "a spot in the UI where your thing shows up," `ext.yml` is "the extension's settings
68
- file."
69
- - **Report in outcomes, not code.** When you tell the user what you did or what broke,
70
- describe it in terms of what they can SEE or what behavior changed — not the code you
71
- touched. Keep symbol names (`ui.mount`, `container.appendChild`, `handler.rb`,
72
- `sidebar.nav`), library names, and file internals out of your message unless the user is
73
- clearly technical or explicitly asks. If a detail matters, say it in plain words.
74
- - ❌ "Fixed it — the `saved city` branch had an early `return` so the DOM never mounted;
75
- added `container.appendChild(root)`."
76
- - ✅ "Found it — when a saved city was remembered, the panel built its content but never
77
- showed it. Fixed, refresh and it'll appear."
78
- - ❌ "Changed the architecture: frontend → own backend → Open-Meteo; added a `daily`
79
- param to the handler."
80
- - ✅ "Reworked it so weather still loads where the direct connection was blocked, and
81
- added a 5-day forecast. Give it a refresh."
82
- - **Map vague locations to real mount points.** Users describe UI by rough position
83
- ("put a button in the top-right", "add something to the left sidebar"). Internally,
84
- translate that to the actual slot below and build against it — but when you talk to the
85
- user, keep saying "top-right" or "middle of the left sidebar," not the slot name. The
86
- host renders exactly these named slots:
25
+ - **Security and scope.** Refuse credential theft or unauthorized private-data access/
26
+ export; never bypass host permissions or expose secrets in client code/logs. Limit file
27
+ and session access to the approved purpose. Destruction and private-data transfer need
28
+ explicit approval of data and destination. Read-only/no-file-change also forbids helper
29
+ files and scripts, including `/tmp`; a tool cache grants no write permission.
30
+ - **Performance.** OpenClacky's host is single-process, so request volume is a safety
31
+ boundary: prefer events and cached reads, and never create unbounded polling, retries,
32
+ workers, or overlapping requests.
33
+ - **Lifecycle.** Clean up listeners, timers, and requests across rerenders; hidden panels
34
+ may stay mounted, so use the documented visibility and unsubscribe behavior instead of
35
+ assuming a tab or session switch disposes them. Ignore stale async results, and never
36
+ replay external sends, destructive changes, or paid operations.
37
+ - **Cost.** Model tasks and media generation/transcription can spend the user's money.
38
+ Require explicit approval for the action and any recurring scope before invoking them;
39
+ never trigger them merely by mounting, refreshing, or replaying a panel.
40
+ - **Host integration.** Reuse host capabilities before adding a backend. Preserve
41
+ generated callback signatures/lifecycle wiring unless the matching reference says
42
+ otherwise. Reuse `btn-*`, `form-*`, `form-input`, `form-textarea`, `Clacky.Modal`, and
43
+ verified `var(--color-*)` names; omit unverified color overrides rather than inventing
44
+ tokens or fallbacks. Prefix tab ids/classes with the extension id, scope DOM/styles to
45
+ its mount, and store persistent data outside the package.
46
+ - **Privileged changes and release.** Develop local extensions, never installed/builtin
47
+ packages or gem implementation source; version metadata is allowed. Hooks/patches,
48
+ scope expansion, service restarts, publication and removal each require an explicit
49
+ request, not general implementation approval.
50
+ - **Missing contracts.** If the relevant reference does not verify an interface, report
51
+ the missing detail and stop. Do not infer the contract from other extensions or a
52
+ running host; ask for the reference or permission for the documented fallback.
53
+ - **Browser scope.** Approval to implement or verify an extension does not authorize
54
+ browser control. Before browser calls, agree on the purpose and a new test/docs tab;
55
+ do not enumerate, reuse, navigate, or inspect existing tabs without specific consent.
56
+ If browser use is declined, leave UI verification to the user. Do not enable browser
57
+ access or substitute console probing for the approved documentation fallback.
87
58
 
88
- | What the user might say | Real slot |
89
- | ---------------------------------- | -------------------- |
90
- | top bar, left / right | `header.left` / `header.right` |
91
- | left sidebar — top / middle / bottom | `sidebar.nav.top` / `sidebar.nav` / `sidebar.nav.bottom` |
92
- | bottom of the left sidebar | `sidebar.footer` |
93
- | the main area / a full page | `main.workspace` |
94
- | a banner at the top of a chat | `session.banner` |
95
- | near the message input box | `session.composer` |
96
- | the right-hand panel of a chat | `session.aside` (tabbed) |
97
- | a settings tab / its body | `settings.tabs` / `settings.body` |
59
+ ## Talking to the user
98
60
 
99
- Mounting into any other name silently does nothing — always use one of these.
61
+ Match the user's language. Lead updates, problems, and handoffs with the visible result
62
+ or needed choice; keep tools and internals private unless asked. Be concise about what was
63
+ found, changed, and remains to verify. Ask only when ambiguity changes the result, and do
64
+ not turn “should work” into “works.” At handoff, follow the skill's refresh/aside flow,
65
+ say what was actually verified, and apply the same nontechnical rewrite as before
66
+ approval. Local completion does not imply publication.
100
67
 
101
- ## Extension engineering rules
68
+ ## Evidence gate
102
69
 
103
- These are requirements, not suggestions. Hold the same engineering bar you would for the
104
- main product, and apply every rule below whenever you propose or write code:
70
+ Before answering or using an interface, choose Verified or Unverified. Every reference
71
+ lookup may fetch one matching official article and optionally search its cache once;
72
+ then stop—no other search, cache read, terminal, fetch, or browser. A failed manual test
73
+ does not relax this budget: afterward inspect only that extension, never another
74
+ extension or the running host.
105
75
 
106
- - **Performance.** Keep the panel light and the UI responsive. Do NOT spin up extra
107
- threads unless there is truly no other way — default to none. The real danger is request
108
- volume: don't hammer the host with tight loops, sub-second polling, or requests that
109
- never stop, and don't re-fetch the same data over and over — fetch once and cache what
110
- you can. Polling is fine when there's genuinely no push channel for the data you need,
111
- but keep the interval coarse (seconds, not milliseconds), prefer the host's events if
112
- they exist, and stop polling when the panel is hidden or the work is done. Runaway
113
- request volume can hit the host's limits, block its own request handling, and bring the
114
- whole of OpenClacky down — treat this as a hard safety concern, not a nicety.
115
- - **Security.** Never help build a malicious extension. If a user asks for something that
116
- steals or exfiltrates data — other people's API keys, credentials, private session
117
- content, files outside the extension's scope — refuse plainly and explain why. Respect
118
- the host's auth boundaries; an extension acts on behalf of its own user, nothing more.
119
- - **Cost.** Billing and usage endpoints cost the user real money. Call them only on an
120
- explicit user action, never automatically and never in a loop.
121
- - **UI.** Default to the host's CSS classes - `btn-*` buttons, `form-*` inputs,
122
- the `modal-*` dialog system, `Clacky.Modal.toast/confirm` feedback, and
123
- `var(--color-*)` colors (raw hex breaks the dark theme) - so the extension
124
- inherits the theme for free. Anything the host has no class for, build freely
125
- with your own prefixed classes.
76
+ - **Verified:** the article body or generated scaffold states the contract. Site chrome,
77
+ styles, and scripts are not extension examples. State only supported behavior/source.
78
+ - **Unverified:** the reference is unavailable or omits the detail. End with the missing
79
+ detail, reference checked, and needed evidence/permission. Never repeat a declined
80
+ permission request; incomplete lookup is not proof of nonexistence.
126
81
 
127
- ## Guidance
128
- - Prefer editing real files over describing what to do (once the user has agreed to the
129
- plan). You are hands-on.
130
- - Never scaffold `patches` or `hooks` unless the user explicitly asks; they run arbitrary
131
- Ruby and carry supply-chain risk.- After editing `view.js`, `handler.rb`, or a `SKILL.md`, tell the user to reload the
132
- WebUI page — hot reload is per-request, no restart needed.
133
- - After you finish editing extension files, call this once at the very end to trigger
134
- a reload button in the UI (if it doesn't appear, the user can manually refresh the browser):
135
- ```
136
- curl -s --noproxy '*' -X POST "http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/ui/show_ext_refresh" \
137
- -H "Content-Type: application/json" \
138
- -d "{\"session_id\": \"${CLACKY_SESSION_ID}\"}"
139
- ```
140
- (`--noproxy '*'` prevents shell proxy env vars from silently intercepting this loopback call.)
141
- - If the extension contributes a `session.aside` panel, also call this right after the above
142
- to open the panel automatically:
143
- ```
144
- curl -s --noproxy '*' -X POST "http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/ui/open_aside" \
145
- -H "Content-Type: application/json" \
146
- -d "{\"session_id\": \"${CLACKY_SESSION_ID}\"}"
147
- ```
82
+ Say “I could not verify this method from this page; please provide its source,” never
83
+ infer absent/private/unsupported. A blocked handoff beats invention or expanded scope.
@@ -147,6 +147,7 @@
147
147
  "extlist.btn.disable": "Disable",
148
148
  "extlist.btn.enable": "Enable",
149
149
  "extlist.btn.delete": "Delete",
150
+ "extlist.btn.more": "More actions",
150
151
  "extlist.badge.disabled": "Disabled",
151
152
  "extlist.delete.confirm": "Delete local extension \"{{id}}\"? This cannot be undone.",
152
153
  "extlist.iterate.seed": "Iterate on extension {{id}}",
@@ -345,6 +346,7 @@
345
346
  "extlist.btn.disable": "禁止",
346
347
  "extlist.btn.enable": "启用",
347
348
  "extlist.btn.delete": "删除",
349
+ "extlist.btn.more": "更多操作",
348
350
  "extlist.badge.disabled": "已禁用",
349
351
  "extlist.delete.confirm": "确定要删除本地扩展「{{id}}」吗?此操作不可恢复。",
350
352
  "extlist.iterate.seed": "迭代扩展 {{id}}",
@@ -501,6 +503,76 @@
501
503
  return node;
502
504
  }
503
505
 
506
+ // ── Shared: overflow menu ──────────────────────────────────────────────
507
+ // Mirrors the session list's "⋯" menu so buttons collapsed on narrow screens
508
+ // stay reachable. Items proxy the real buttons rather than duplicating them,
509
+ // which keeps transient label/disabled state (e.g. "Packing…") in sync.
510
+ const OVERFLOW_ICON = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="2.5" cy="7" r="1.2" fill="currentColor"/><circle cx="7" cy="7" r="1.2" fill="currentColor"/><circle cx="11.5" cy="7" r="1.2" fill="currentColor"/></svg>`;
511
+
512
+ let overflowDismiss = null;
513
+ let overflowAnchor = null;
514
+
515
+ function closeOverflowMenu() {
516
+ if (overflowDismiss) {
517
+ document.removeEventListener("click", overflowDismiss, true);
518
+ overflowDismiss = null;
519
+ }
520
+ overflowAnchor = null;
521
+ const existing = document.querySelector(".studio-overflow-menu");
522
+ if (existing) existing.remove();
523
+ }
524
+
525
+ function openOverflowMenu(anchor, buttons) {
526
+ // Re-tapping the same "⋯" toggles; tapping a different card must switch to it
527
+ // rather than merely dismissing the menu that is already open.
528
+ if (overflowAnchor === anchor) { closeOverflowMenu(); return; }
529
+ closeOverflowMenu();
530
+ overflowAnchor = anchor;
531
+ const menu = el("div", { class: "session-actions-menu studio-overflow-menu" });
532
+ buttons.forEach((btn) => {
533
+ const item = el("div", { class: "session-actions-menu-item" }, [
534
+ el("span", { class: "session-actions-menu-label", text: btn.textContent }),
535
+ ]);
536
+ if (btn.classList.contains("studio-btn-danger")) item.classList.add("session-actions-menu-item--danger");
537
+ if (btn.disabled) {
538
+ item.classList.add("is-disabled");
539
+ const tip = btn.getAttribute("data-tooltip");
540
+ if (tip) item.setAttribute("data-tooltip", tip);
541
+ } else {
542
+ item.addEventListener("click", () => { closeOverflowMenu(); btn.click(); });
543
+ }
544
+ menu.appendChild(item);
545
+ });
546
+
547
+ // Take the menu out of normal flow before measuring: as a static block child
548
+ // of <body> it would stretch to the full body width, so mw would be the
549
+ // viewport width and the clamp below would fling the menu to the far edge.
550
+ menu.style.visibility = "hidden";
551
+ menu.style.position = "fixed";
552
+ menu.style.top = "0px";
553
+ menu.style.left = "0px";
554
+ document.body.appendChild(menu);
555
+ const rect = anchor.getBoundingClientRect();
556
+ const mw = menu.offsetWidth;
557
+ const mh = menu.offsetHeight;
558
+ let top = rect.bottom + 4;
559
+ let left = rect.left;
560
+ if (top + mh > window.innerHeight - 8) top = Math.max(8, rect.top - mh - 4);
561
+ if (left + mw > window.innerWidth - 8) left = Math.max(8, window.innerWidth - mw - 8);
562
+ menu.style.top = top + "px";
563
+ menu.style.left = left + "px";
564
+ menu.style.visibility = "";
565
+
566
+ // Capture phase runs before the anchor's own handler, so the anchor has to be
567
+ // excluded here or reopening would immediately close and reopen the menu.
568
+ overflowDismiss = (e) => {
569
+ const node = e.target instanceof Element ? e.target : null;
570
+ if (node && (node.closest(".studio-overflow-menu") || node.closest(".studio-actions-overflow"))) return;
571
+ closeOverflowMenu();
572
+ };
573
+ document.addEventListener("click", overflowDismiss, true);
574
+ }
575
+
504
576
  // ── Shared: modal dialog ───────────────────────────────────────────────
505
577
  // A minimal overlay modal used by the publish-confirm and device-binding
506
578
  // flows. Returns the body element so callers can drive it live (bind flow),
@@ -1636,7 +1708,7 @@
1636
1708
 
1637
1709
  if (typeof Brand !== "undefined" && Brand.userLicensed) {
1638
1710
  const brandEntry = brandCloud.find((c) => c.id === ext.id);
1639
- const brandPub = el("button", { class: "studio-btn", text: t("extlist.btn.publishBrand") });
1711
+ const brandPub = el("button", { class: "studio-btn studio-btn-collapsible", text: t("extlist.btn.publishBrand") });
1640
1712
  brandPub.disabled = !!ext.error_count;
1641
1713
  if (ext.error_count) brandPub.setAttribute("data-tooltip", t("extlist.verify.errors", { n: ext.error_count }));
1642
1714
  brandPub.addEventListener("click", () => doPublish(ext, brandEntry ? brandEntry.version : null, "self"));
@@ -1644,12 +1716,12 @@
1644
1716
  }
1645
1717
 
1646
1718
  actions.appendChild(el("button", { class: "studio-btn", text: t("extlist.btn.iterate"), onclick: () => createExtension(t("extlist.iterate.seed", { id: ext.id })) }));
1647
- const packBtn = el("button", { class: "studio-btn studio-btn-ghost", text: t("extlist.btn.pack") });
1719
+ const packBtn = el("button", { class: "studio-btn studio-btn-ghost studio-btn-collapsible", text: t("extlist.btn.pack") });
1648
1720
  packBtn.addEventListener("click", () => doPack(ext, packBtn));
1649
1721
  actions.appendChild(packBtn);
1650
1722
 
1651
1723
  const toggleBtn = el("button", {
1652
- class: "studio-btn studio-btn-ghost",
1724
+ class: "studio-btn studio-btn-ghost studio-btn-collapsible",
1653
1725
  text: ext.disabled ? t("extlist.btn.enable") : t("extlist.btn.disable"),
1654
1726
  });
1655
1727
  toggleBtn.addEventListener("click", () => doToggleDisabled(ext, toggleBtn));
@@ -1657,10 +1729,18 @@
1657
1729
  card.appendChild(actions);
1658
1730
 
1659
1731
  if (!published) {
1660
- const delBtn = el("button", { class: "studio-btn studio-btn-danger", text: t("extlist.btn.delete") });
1732
+ const delBtn = el("button", { class: "studio-btn studio-btn-danger studio-btn-collapsible", text: t("extlist.btn.delete") });
1661
1733
  delBtn.addEventListener("click", () => doDelete(ext, delBtn));
1662
1734
  actions.appendChild(delBtn);
1663
1735
  }
1736
+
1737
+ const overflow = el("button", { class: "studio-btn studio-actions-overflow", "aria-label": t("extlist.btn.more"), title: t("extlist.btn.more") });
1738
+ overflow.innerHTML = OVERFLOW_ICON;
1739
+ overflow.addEventListener("click", (e) => {
1740
+ e.stopPropagation();
1741
+ openOverflowMenu(overflow, Array.from(actions.querySelectorAll(".studio-btn-collapsible")));
1742
+ });
1743
+ actions.appendChild(overflow);
1664
1744
  return card;
1665
1745
  }
1666
1746
 
@@ -2041,7 +2121,7 @@
2041
2121
  .studio-detail-desc { margin: 4px 0 0; font-size: 12px; color: var(--color-text-secondary); line-height: 1.5; }
2042
2122
  .studio-units { display: flex; flex-wrap: wrap; gap: 6px; }
2043
2123
  .studio-unit-chip { display: inline-flex; background: var(--color-bg-hover); color: var(--color-text-primary); border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm); padding: 2px 8px; font-size: 12px; }
2044
- .studio-actions { display: flex; align-items: center; gap: 8px; margin: 12px 0 0; }
2124
+ .studio-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin: 12px 0 0; }
2045
2125
  .studio-skill-promo .studio-actions { margin: 12px 0 0; }
2046
2126
  .studio-btn { padding: 7px 14px; border-radius: var(--radius-sm); border: 1px solid var(--color-border-primary); background: transparent; color: var(--color-text-secondary); cursor: pointer; font-size: 13px; font-weight: 500; }
2047
2127
  .studio-btn:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
@@ -2150,6 +2230,12 @@
2150
2230
  .studio-readme-preview pre code, .extension-readme-body pre code { background: none; padding: 0; }
2151
2231
  .studio-link-btn { background: none; border: none; padding: 0; font-size: 12px; color: var(--color-accent-primary); cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
2152
2232
  .studio-link-btn:hover { color: var(--color-accent-hover); }
2233
+ .studio-actions-overflow { display: none; align-items: center; justify-content: center; align-self: stretch; box-sizing: border-box; padding: 7px 12px; }
2234
+ .studio-overflow-menu .session-actions-menu-item.is-disabled { opacity: 0.5; cursor: default; }
2235
+ @media (max-width: 768px) {
2236
+ .studio-actions > .studio-btn-collapsible { display: none; }
2237
+ .studio-actions > .studio-actions-overflow { display: inline-flex; }
2238
+ }
2153
2239
  `;
2154
2240
  document.head.appendChild(style);
2155
2241
  })();