openclacky 1.5.6 → 1.5.7

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.clacky/skills/gem-release/SKILL.md +18 -1
  3. data/.clackyrules +25 -1
  4. data/CHANGELOG.md +26 -0
  5. data/CONTRIBUTING.md +9 -0
  6. data/Rakefile +15 -0
  7. data/docs/design-emit-and-background-skills.md +253 -0
  8. data/docs/mcp-architecture.md +48 -9
  9. data/docs/mcp.example.json +9 -0
  10. data/docs/superpowers/plans/2026-08-09-chatcut-extension-bridge.md +191 -0
  11. data/docs/superpowers/plans/2026-08-09-native-mcp-oauth.md +243 -0
  12. data/docs/superpowers/specs/2026-08-09-mcp-oauth-extension-fallback-design.md +115 -0
  13. data/lib/clacky/agent/cost_tracker.rb +17 -2
  14. data/lib/clacky/agent/hook_manager.rb +14 -3
  15. data/lib/clacky/agent/memory_updater.rb +17 -18
  16. data/lib/clacky/agent/message_compressor_helper.rb +23 -0
  17. data/lib/clacky/agent/session_serializer.rb +136 -22
  18. data/lib/clacky/agent/skill_auto_creator.rb +1 -2
  19. data/lib/clacky/agent/skill_evolution.rb +11 -7
  20. data/lib/clacky/agent/skill_manager.rb +20 -18
  21. data/lib/clacky/agent/skill_reflector.rb +3 -6
  22. data/lib/clacky/agent.rb +111 -20
  23. data/lib/clacky/anthropic_stream_aggregator.rb +6 -1
  24. data/lib/clacky/cli.rb +4 -0
  25. data/lib/clacky/client.rb +1 -1
  26. data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +13 -5
  27. data/lib/clacky/extension/api_extension.rb +11 -3
  28. data/lib/clacky/fanout.rb +100 -0
  29. data/lib/clacky/locales/en.rb +2 -0
  30. data/lib/clacky/locales/zh.rb +2 -0
  31. data/lib/clacky/mcp/cli_commands.rb +61 -0
  32. data/lib/clacky/mcp/client.rb +18 -2
  33. data/lib/clacky/mcp/connection_manager.rb +85 -0
  34. data/lib/clacky/mcp/http_transport.rb +25 -9
  35. data/lib/clacky/mcp/oauth/authorization_manager.rb +257 -0
  36. data/lib/clacky/mcp/oauth/config.rb +51 -0
  37. data/lib/clacky/mcp/oauth/credential_store.rb +82 -0
  38. data/lib/clacky/mcp/oauth/session.rb +88 -0
  39. data/lib/clacky/message_format/anthropic.rb +27 -13
  40. data/lib/clacky/message_history.rb +32 -1
  41. data/lib/clacky/providers.rb +11 -5
  42. data/lib/clacky/server/http_server.rb +42 -22
  43. data/lib/clacky/server/session_registry.rb +11 -15
  44. data/lib/clacky/server/web_ui_controller.rb +1 -1
  45. data/lib/clacky/session_manager.rb +13 -11
  46. data/lib/clacky/shell_hook_loader.rb +14 -8
  47. data/lib/clacky/tools/invoke_skill.rb +3 -2
  48. data/lib/clacky/tools/terminal.rb +11 -4
  49. data/lib/clacky/ui2/progress_handle.rb +27 -3
  50. data/lib/clacky/ui2/ui_controller.rb +206 -6
  51. data/lib/clacky/ui_interface.rb +13 -3
  52. data/lib/clacky/utils/arguments_parser.rb +73 -4
  53. data/lib/clacky/utils/model_pricing.rb +21 -0
  54. data/lib/clacky/version.rb +1 -1
  55. data/lib/clacky/web/app.css +10 -3
  56. data/lib/clacky/web/features/new-session/view.js +19 -2
  57. data/lib/clacky/web/i18n.js +22 -8
  58. data/lib/clacky/web/index.html +7 -11
  59. data/lib/clacky/web/projects.js +26 -0
  60. data/lib/clacky/web/sessions.js +169 -119
  61. data/lib/clacky/web/settings.js +1 -0
  62. data/lib/clacky/web/ws-dispatcher.js +61 -34
  63. data/lib/clacky.rb +2 -0
  64. metadata +12 -1
@@ -58,32 +58,29 @@
58
58
  // The DOM id "messages" is never swapped: external code, CSS, devtools and
59
59
  // closures all see a stable identity.
60
60
  const RenderTarget = (() => {
61
- const stack = [];
61
+ let pinned = null;
62
62
  return {
63
- push(el) { stack.push(el); },
64
- pop() { return stack.pop(); },
65
- current(){ return stack[stack.length - 1] || document.getElementById("messages"); },
63
+ // Concurrent phases make "innermost open phase" meaningless, so events
64
+ // are routed by their own phase_id (pin) and anything untagged goes to
65
+ // the outer stream rather than whichever phase happens to be open.
66
+ pin(el) { pinned = el; },
67
+ unpin() { pinned = null; },
68
+ current(){ return pinned || document.getElementById("messages"); },
66
69
  outer() { return document.getElementById("messages"); },
67
- depth() { return stack.length; },
68
70
  };
69
71
  })();
70
72
  window.RenderTarget = RenderTarget;
71
73
 
72
- const _phaseStack = []; // [{ id, kind, card, body, summary }]
74
+ const _phases = new Map(); // phase_id -> { id, kind, card, body, summary, order }
75
+ let _phaseSeq = 0;
73
76
 
74
- function _activePhase() {
75
- return _phaseStack[_phaseStack.length - 1] || null;
76
- }
77
-
78
- function _phaseLabel(kind) {
79
- const map = {
80
- skill_evolution: I18n.t ? (I18n.t("phase.skill_evolution") || "Skill evolution") : "Skill evolution",
81
- };
82
- return map[kind] || kind;
77
+ function _phaseIcon(kind) {
78
+ const map = { subagent: "🤖", fanout_subagent: "🤖", memory_update: "🧠" };
79
+ return map[kind] || "🧬";
83
80
  }
84
81
 
85
82
  function _beginPhase(ev) {
86
- if (_phaseStack.length > 0) return; // single-level for now
83
+ if (_phases.has(ev.phase_id)) return;
87
84
 
88
85
  const outer = RenderTarget.outer();
89
86
  if (!outer) return;
@@ -92,11 +89,13 @@
92
89
  card.className = "msg-phase";
93
90
  card.dataset.phaseId = ev.phase_id;
94
91
  card.dataset.phaseKind = ev.kind || "phase";
92
+ card.open = true;
95
93
 
96
94
  const summary = document.createElement("summary");
97
95
  summary.className = "msg-phase-summary";
98
- const labelText = ev.label || _phaseLabel(ev.kind);
99
- summary.innerHTML = `<span class="msg-phase-icon">🧬</span><span class="msg-phase-label">${escapeHtml(labelText)}</span><span class="msg-phase-status">…</span>`;
96
+ const labelText = ev.label || ev.kind;
97
+ const icon = _phaseIcon(ev.kind);
98
+ summary.innerHTML = `<span class="msg-phase-icon">${icon}</span><span class="msg-phase-label">${escapeHtml(labelText)}</span><span class="msg-phase-status">…</span>`;
100
99
  card.appendChild(summary);
101
100
 
102
101
  const body = document.createElement("div");
@@ -104,33 +103,42 @@
104
103
  card.appendChild(body);
105
104
 
106
105
  outer.appendChild(card);
107
- RenderTarget.push(body);
108
106
 
109
- _phaseStack.push({
107
+ _phases.set(ev.phase_id, {
110
108
  id: ev.phase_id,
111
109
  kind: ev.kind,
112
110
  card,
113
111
  body,
114
112
  summary,
113
+ order: _phaseSeq++,
114
+ });
115
+
116
+ const phase = _phases.get(ev.phase_id);
117
+ card.addEventListener("toggle", () => {
118
+ if (!phase.programmaticToggle) phase.userToggled = true;
115
119
  });
116
120
 
117
121
  outer.scrollTop = outer.scrollHeight;
118
122
  }
119
123
 
120
124
  function _endPhase(phaseId, summary) {
121
- const idx = _phaseStack.findIndex(p => p.id === phaseId);
122
- if (idx === -1) return;
123
- while (_phaseStack.length > idx) {
124
- const phase = _phaseStack.pop();
125
- RenderTarget.pop();
126
- _finalizePhase(phase, { summary: phase.id === phaseId ? summary : null });
127
- }
125
+ const phase = _phases.get(phaseId);
126
+ if (!phase) return;
127
+ _phases.delete(phaseId);
128
+ _finalizePhase(phase, { summary });
129
+ }
130
+
131
+ function _setPhaseOpen(phase, open) {
132
+ phase.programmaticToggle = true;
133
+ phase.card.open = open;
134
+ phase.programmaticToggle = false;
128
135
  }
129
136
 
130
137
  function _finalizePhase(phase, { summary, incomplete } = {}) {
131
138
  const body = phase.body;
132
139
  const isEmpty = !incomplete && body && body.children.length === 0;
133
140
  if (isEmpty) phase.card.classList.add("msg-phase-empty");
141
+ if (!phase.userToggled) _setPhaseOpen(phase, false);
134
142
 
135
143
  const statusEl = phase.summary && phase.summary.querySelector(".msg-phase-status");
136
144
  if (statusEl) {
@@ -138,7 +146,7 @@
138
146
  statusEl.textContent = " (interrupted)";
139
147
  statusEl.classList.add("msg-phase-status-incomplete");
140
148
  } else if (isEmpty) {
141
- const noChange = I18n.t ? (I18n.t("phase.no_changes") || "no changes needed") : "no changes needed";
149
+ const noChange = I18n.t("phase.no_changes");
142
150
  statusEl.textContent = ` ✓ ${noChange}`;
143
151
  } else if (summary) {
144
152
  statusEl.textContent = ` ✓ ${summary}`;
@@ -149,11 +157,11 @@
149
157
  }
150
158
 
151
159
  function _closeAllPhases(reason) {
152
- while (_phaseStack.length > 0) {
153
- const phase = _phaseStack.pop();
154
- RenderTarget.pop();
160
+ const phases = [..._phases.values()].sort((a, b) => b.order - a.order);
161
+ _phases.clear();
162
+ phases.forEach(phase => {
155
163
  _finalizePhase(phase, { incomplete: reason === "incomplete" });
156
- }
164
+ });
157
165
  }
158
166
 
159
167
  window._closeAllPhases = _closeAllPhases;
@@ -167,6 +175,20 @@ WS.onEvent(ev => {
167
175
  _closeAllPhases("incomplete");
168
176
  }
169
177
 
178
+ // Concurrent phases interleave, so stack order says nothing about where an
179
+ // event belongs — route by the phase_id the backend stamped on it.
180
+ const _taggedPhase = ev.phase_id && ev.type !== "phase_start" && ev.type !== "phase_end"
181
+ ? _phases.get(ev.phase_id)
182
+ : null;
183
+ if (_taggedPhase) RenderTarget.pin(_taggedPhase.body);
184
+ try {
185
+ _dispatchEvent(ev);
186
+ } finally {
187
+ if (_taggedPhase) RenderTarget.unpin();
188
+ }
189
+ });
190
+
191
+ function _dispatchEvent(ev) {
170
192
  // Bridge before the switch: per-case active-session guards must not block
171
193
  // the extension bus. All received events mirror onto Clacky.ext here.
172
194
  if (window.Clacky && Clacky.ext) {
@@ -185,6 +207,11 @@ WS.onEvent(ev => {
185
207
  }
186
208
  const sid = payload.session_id || (ev.session && ev.session.id);
187
209
  Clacky.ext.emit(extName, { sessionId: sid, ...payload, session_id: sid });
210
+ } else if (typeof ev.type === "string" && ev.type.startsWith("ext.")) {
211
+ // Custom events from UIInterface#emit. The host has no rendering for
212
+ // these; they exist purely so an extension can observe its own backend.
213
+ const sid = ev.session_id;
214
+ Clacky.ext.emit(ev.type, { sessionId: sid, ...ev });
188
215
  }
189
216
  }
190
217
 
@@ -237,7 +264,7 @@ WS.onEvent(ev => {
237
264
 
238
265
  // ── Session list ───────────────────────────────────────────────────
239
266
  case "session_list": {
240
- Sessions.setAll(ev.sessions || [], !!ev.has_more, ev.cron_count || 0, ev.latest_cron_updated_at || null);
267
+ Sessions.setAll(ev.sessions || [], !!ev.has_more, ev.groups || {});
241
268
  Projects.setAll(ev.projects || []);
242
269
  Sessions.renderList();
243
270
  Projects.renderSection();
@@ -519,7 +546,7 @@ WS.onEvent(ev => {
519
546
  break;
520
547
 
521
548
  }
522
- });
549
+ }
523
550
 
524
551
  // ── Ext-developer refresh hint ─────────────────────────────────────────────
525
552
  // Appended after a completed ext-developer session so the user can reload
data/lib/clacky.rb CHANGED
@@ -86,6 +86,7 @@ require_relative "clacky/agent/message_compressor"
86
86
  require_relative "clacky/agent/hook_manager"
87
87
  require_relative "clacky/shell_hook_loader"
88
88
  require_relative "clacky/agent/tool_registry"
89
+ require_relative "clacky/fanout"
89
90
 
90
91
  # UI modules
91
92
  require_relative "clacky/ui2/thinking_verbs"
@@ -201,6 +202,7 @@ module Clacky
201
202
  class UpstreamTruncatedError < RetryableError; end
202
203
  class ToolCallError < AgentError; end # Raised when tool call fails due to invalid parameters
203
204
  class BrowserNotReachableError < AgentError; end # Chrome/Edge not running or remote debugging disabled
205
+ class FanoutTimeoutError < AgentError; end # A fan-out job exceeded the batch wall-clock budget
204
206
  # BrowserManager singleton: Clacky::BrowserManager.instance
205
207
  end
206
208
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openclacky
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.6
4
+ version: 1.5.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - windy
@@ -324,12 +324,16 @@ files:
324
324
  - docs/agent-first-ui-design.md
325
325
  - docs/billing-system.md
326
326
  - docs/channel-architecture.md
327
+ - docs/design-emit-and-background-skills.md
327
328
  - docs/engineering-article.md
328
329
  - docs/mcp-architecture.md
329
330
  - docs/mcp.example.json
330
331
  - docs/rich_ui_guide.md
331
332
  - docs/rich_ui_refactor_plan.md
332
333
  - docs/session-skill-invocation.md
334
+ - docs/superpowers/plans/2026-08-09-chatcut-extension-bridge.md
335
+ - docs/superpowers/plans/2026-08-09-native-mcp-oauth.md
336
+ - docs/superpowers/specs/2026-08-09-mcp-oauth-extension-fallback-design.md
333
337
  - docs/time_machine_design.md
334
338
  - docs/ui2-architecture.md
335
339
  - homebrew/README.md
@@ -463,14 +467,21 @@ files:
463
467
  - lib/clacky/extension/scaffold/templates/hello/ext.yml.erb
464
468
  - lib/clacky/extension/scaffold/templates/hello/panels/hello/view.js.erb
465
469
  - lib/clacky/extension/verifier.rb
470
+ - lib/clacky/fanout.rb
466
471
  - lib/clacky/identity.rb
467
472
  - lib/clacky/idle_compression_timer.rb
468
473
  - lib/clacky/json_ui_controller.rb
469
474
  - lib/clacky/locales/en.rb
470
475
  - lib/clacky/locales/i18n.rb
471
476
  - lib/clacky/locales/zh.rb
477
+ - lib/clacky/mcp/cli_commands.rb
472
478
  - lib/clacky/mcp/client.rb
479
+ - lib/clacky/mcp/connection_manager.rb
473
480
  - lib/clacky/mcp/http_transport.rb
481
+ - lib/clacky/mcp/oauth/authorization_manager.rb
482
+ - lib/clacky/mcp/oauth/config.rb
483
+ - lib/clacky/mcp/oauth/credential_store.rb
484
+ - lib/clacky/mcp/oauth/session.rb
474
485
  - lib/clacky/mcp/registry.rb
475
486
  - lib/clacky/mcp/skill_provider.rb
476
487
  - lib/clacky/mcp/stdio_transport.rb