openclacky 1.5.12 → 1.5.13
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 +48 -0
- data/lib/clacky/access_key.rb +59 -0
- data/lib/clacky/agent/message_compressor_helper.rb +52 -1
- data/lib/clacky/agent/session_serializer.rb +35 -2
- data/lib/clacky/agent/time_machine.rb +9 -1
- data/lib/clacky/agent.rb +11 -7
- data/lib/clacky/agent_config.rb +40 -0
- data/lib/clacky/agent_profile.rb +5 -5
- data/lib/clacky/billing/platform_billing.rb +84 -1
- data/lib/clacky/brand_config.rb +4 -3
- data/lib/clacky/cli.rb +4 -2
- data/lib/clacky/client.rb +107 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +3 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +40 -21
- data/lib/clacky/default_extensions/git/ext.yml +1 -1
- data/lib/clacky/default_extensions/git/panels/git/view.js +483 -77
- data/lib/clacky/default_extensions/meeting/ext.yml +2 -1
- data/lib/clacky/default_extensions/time_machine/ext.yml +1 -1
- data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +186 -208
- data/lib/clacky/extension/packager.rb +4 -2
- data/lib/clacky/extension/verifier.rb +14 -1
- data/lib/clacky/message_format/open_ai.rb +5 -1
- data/lib/clacky/message_format/open_ai_responses.rb +409 -0
- data/lib/clacky/message_history.rb +6 -2
- data/lib/clacky/openai_responses_stream_aggregator.rb +294 -0
- data/lib/clacky/providers.rb +18 -6
- data/lib/clacky/server/dir_picker.rb +154 -0
- data/lib/clacky/server/git_panel.rb +61 -9
- data/lib/clacky/server/http_server.rb +147 -103
- data/lib/clacky/tools/terminal.rb +27 -3
- data/lib/clacky/ui2/markdown_renderer.rb +15 -12
- data/lib/clacky/ui2/strings_cjk_patch.rb +132 -0
- data/lib/clacky/ui2/themes/hacker_theme.rb +1 -1
- data/lib/clacky/ui2/themes/minimal_theme.rb +1 -1
- data/lib/clacky/utils/model_pricing.rb +70 -4
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +412 -108
- data/lib/clacky/web/components/composer.js +45 -7
- data/lib/clacky/web/components/custom-select.js +136 -0
- data/lib/clacky/web/components/mentions.js +58 -2
- data/lib/clacky/web/components/model-picker.js +527 -0
- data/lib/clacky/web/components/onboard.js +29 -55
- data/lib/clacky/web/core/ext.js +38 -0
- data/lib/clacky/web/features/billing/view.js +20 -6
- data/lib/clacky/web/features/extensions/view.js +22 -10
- data/lib/clacky/web/features/new-session/store.js +15 -1
- data/lib/clacky/web/features/new-session/view.js +31 -22
- data/lib/clacky/web/features/trash/view.js +31 -19
- data/lib/clacky/web/features/workspace/store.js +1 -0
- data/lib/clacky/web/features/workspace/view.js +54 -3
- data/lib/clacky/web/i18n.js +73 -11
- data/lib/clacky/web/index.html +92 -37
- data/lib/clacky/web/sessions.js +550 -592
- data/lib/clacky/web/settings.js +163 -190
- data/lib/clacky.rb +3 -0
- metadata +8 -1
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
const Composer = (() => {
|
|
16
16
|
const CHIP_CLASS = "mention-chip";
|
|
17
|
+
const ELEMENT_NODE = 1;
|
|
18
|
+
const TEXT_NODE = 3;
|
|
17
19
|
|
|
18
20
|
// Lucide-style 24×24 stroke paths for the chip type icons (matches the
|
|
19
21
|
// inline SVGs used across the UI).
|
|
@@ -48,7 +50,7 @@ const Composer = (() => {
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
function _isChip(node) {
|
|
51
|
-
return !!node && node.nodeType ===
|
|
53
|
+
return !!node && node.nodeType === ELEMENT_NODE && node.classList && node.classList.contains(CHIP_CLASS);
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
// Plain text of the composer, excluding chips. Newlines are reconstructed
|
|
@@ -57,11 +59,11 @@ const Composer = (() => {
|
|
|
57
59
|
let out = "";
|
|
58
60
|
const walk = (node) => {
|
|
59
61
|
for (const n of Array.from(node.childNodes)) {
|
|
60
|
-
if (n.nodeType ===
|
|
62
|
+
if (n.nodeType === TEXT_NODE) {
|
|
61
63
|
out += n.nodeValue;
|
|
62
64
|
continue;
|
|
63
65
|
}
|
|
64
|
-
if (n.nodeType !==
|
|
66
|
+
if (n.nodeType !== ELEMENT_NODE) continue;
|
|
65
67
|
if (_isChip(n)) continue;
|
|
66
68
|
switch (n.tagName) {
|
|
67
69
|
case "BR":
|
|
@@ -175,6 +177,14 @@ const Composer = (() => {
|
|
|
175
177
|
range.insertNode(chipEl);
|
|
176
178
|
chipEl.parentNode.insertBefore(space, chipEl.nextSibling);
|
|
177
179
|
|
|
180
|
+
// A leading zero-width guard keeps the caret from sitting on the bare
|
|
181
|
+
// block boundary before an atomic chip, where browsers insert a <br>/<div>
|
|
182
|
+
// on input/delete and push the chip onto its own line.
|
|
183
|
+
const prev = chipEl.previousSibling;
|
|
184
|
+
if (!prev || prev.nodeType !== TEXT_NODE) {
|
|
185
|
+
chipEl.parentNode.insertBefore(document.createTextNode("\u200B"), chipEl);
|
|
186
|
+
}
|
|
187
|
+
|
|
178
188
|
const after = document.createRange();
|
|
179
189
|
after.setStartAfter(space);
|
|
180
190
|
after.collapse(true);
|
|
@@ -184,15 +194,42 @@ const Composer = (() => {
|
|
|
184
194
|
_emitInput(el);
|
|
185
195
|
}
|
|
186
196
|
|
|
187
|
-
// Remove a chip DOM node and its
|
|
197
|
+
// Remove a chip DOM node and its surrounding zero-width guards + separator whitespace.
|
|
188
198
|
function removeChip(chipEl) {
|
|
199
|
+
const prev = chipEl.previousSibling;
|
|
200
|
+
if (prev && prev.nodeType === TEXT_NODE && /^\u200B+$/.test(prev.nodeValue)) {
|
|
201
|
+
prev.remove();
|
|
202
|
+
}
|
|
189
203
|
const next = chipEl.nextSibling;
|
|
190
|
-
if (next && next.nodeType ===
|
|
204
|
+
if (next && next.nodeType === TEXT_NODE && /^[\s\u00A0\u200B]*$/.test(next.nodeValue)) {
|
|
191
205
|
next.remove();
|
|
192
206
|
}
|
|
193
207
|
chipEl.remove();
|
|
194
208
|
}
|
|
195
209
|
|
|
210
|
+
// Re-establish a text anchor before each chip. Editing can delete the
|
|
211
|
+
// leading zero-width guard (when the caret sits inside it), after which the
|
|
212
|
+
// browser drops a <br>/<div> line anchor before the chip and wraps it onto
|
|
213
|
+
// its own line. Runs on every input event.
|
|
214
|
+
function _ensureChipGuards(el) {
|
|
215
|
+
for (const chip of Array.from(el.querySelectorAll("." + CHIP_CLASS))) {
|
|
216
|
+
let prev = chip.previousSibling;
|
|
217
|
+
// A stray <br> that has no visible text before it is the browser's line
|
|
218
|
+
// anchor, not a user Shift+Enter break — drop it so the chip rejoins the
|
|
219
|
+
// current line.
|
|
220
|
+
if (prev && prev.nodeType === ELEMENT_NODE && prev.tagName === "BR") {
|
|
221
|
+
const beforeBr = prev.previousSibling;
|
|
222
|
+
const noTextBefore = !beforeBr ||
|
|
223
|
+
(beforeBr.nodeType === TEXT_NODE && /^[\u200B]*$/.test(beforeBr.nodeValue));
|
|
224
|
+
if (noTextBefore) prev.remove();
|
|
225
|
+
}
|
|
226
|
+
const before = chip.previousSibling;
|
|
227
|
+
if (!before || before.nodeType !== TEXT_NODE || before.nodeValue === "") {
|
|
228
|
+
chip.parentNode.insertBefore(document.createTextNode("\u200B"), chip);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
196
233
|
// Backspace/Delete at a chip boundary removes the chip atomically.
|
|
197
234
|
function _handleKeydown(el, e) {
|
|
198
235
|
if (e.key !== "Backspace" && e.key !== "Delete") return false;
|
|
@@ -204,13 +241,13 @@ const Composer = (() => {
|
|
|
204
241
|
|
|
205
242
|
let target = null;
|
|
206
243
|
if (e.key === "Backspace") {
|
|
207
|
-
if (container.nodeType ===
|
|
244
|
+
if (container.nodeType === TEXT_NODE) {
|
|
208
245
|
if (offset === 0) target = container.previousSibling;
|
|
209
246
|
} else if (container === el) {
|
|
210
247
|
target = container.childNodes[offset - 1];
|
|
211
248
|
}
|
|
212
249
|
} else {
|
|
213
|
-
if (container.nodeType ===
|
|
250
|
+
if (container.nodeType === TEXT_NODE) {
|
|
214
251
|
if (offset === container.nodeValue.length) target = container.nextSibling;
|
|
215
252
|
} else if (container === el) {
|
|
216
253
|
target = container.childNodes[offset];
|
|
@@ -262,6 +299,7 @@ const Composer = (() => {
|
|
|
262
299
|
// Browsers leave a stray <br> after deleting all text, which breaks the
|
|
263
300
|
// :empty::before placeholder. Clear it so the placeholder shows again.
|
|
264
301
|
el.addEventListener("input", () => {
|
|
302
|
+
_ensureChipGuards(el);
|
|
265
303
|
if (text(el).trim() === "" && !el.querySelector("." + CHIP_CLASS) && el.innerHTML !== "") {
|
|
266
304
|
el.innerHTML = "";
|
|
267
305
|
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Generic single-choice dropdown shared by settings/onboard.
|
|
2
|
+
// DOM contract:
|
|
3
|
+
// wrapper > .custom-select-trigger > .custom-select-value (+ .custom-select-arrow)
|
|
4
|
+
// + .custom-select-dropdown > .custom-select-option[data-value][data-label]
|
|
5
|
+
// Business code owns the option markup (populate); this module owns the
|
|
6
|
+
// open/close/selection mechanics so every dropdown behaves identically.
|
|
7
|
+
window.CustomSelect = (function () {
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
const instances = [];
|
|
11
|
+
let globalBound = false;
|
|
12
|
+
|
|
13
|
+
function bindGlobalClose() {
|
|
14
|
+
if (globalBound) return;
|
|
15
|
+
globalBound = true;
|
|
16
|
+
document.addEventListener("click", () => {
|
|
17
|
+
for (let i = instances.length - 1; i >= 0; i--) {
|
|
18
|
+
const inst = instances[i];
|
|
19
|
+
if (!inst._trigger.isConnected) { instances.splice(i, 1); continue; }
|
|
20
|
+
inst.close();
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function positionFixed(dropdown, anchor) {
|
|
26
|
+
const rect = anchor.getBoundingClientRect();
|
|
27
|
+
dropdown.style.position = "fixed";
|
|
28
|
+
dropdown.style.top = (rect.bottom + 4) + "px";
|
|
29
|
+
dropdown.style.left = rect.left + "px";
|
|
30
|
+
// min-width:100% resolves against the viewport once position:fixed,
|
|
31
|
+
// so pin it to the anchor width; leave width to the CSS max-content
|
|
32
|
+
// rule so narrow triggers (search type filter) still fit their options.
|
|
33
|
+
dropdown.style.minWidth = rect.width + "px";
|
|
34
|
+
dropdown.style.right = "auto";
|
|
35
|
+
dropdown.style.zIndex = "9999";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resetPosition(dropdown) {
|
|
39
|
+
dropdown.style.position = "";
|
|
40
|
+
dropdown.style.top = "";
|
|
41
|
+
dropdown.style.left = "";
|
|
42
|
+
dropdown.style.width = "";
|
|
43
|
+
dropdown.style.minWidth = "";
|
|
44
|
+
dropdown.style.right = "";
|
|
45
|
+
dropdown.style.zIndex = "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function init(opts) {
|
|
49
|
+
const trigger = opts.trigger;
|
|
50
|
+
const dropdown = opts.dropdown;
|
|
51
|
+
const valueSpan = trigger.querySelector(".custom-select-value");
|
|
52
|
+
const home = dropdown.parentElement;
|
|
53
|
+
const anchor = opts.anchor || home;
|
|
54
|
+
const placeholderText = valueSpan ? valueSpan.textContent.trim() : "";
|
|
55
|
+
const selectedOption = dropdown.querySelector(".custom-select-option.selected");
|
|
56
|
+
|
|
57
|
+
const instance = {
|
|
58
|
+
_trigger: trigger,
|
|
59
|
+
value: selectedOption ? (selectedOption.dataset.value || "") : "",
|
|
60
|
+
open: function () {
|
|
61
|
+
instances.forEach(function (other) {
|
|
62
|
+
if (other !== instance) other.close();
|
|
63
|
+
});
|
|
64
|
+
if (opts.portal) {
|
|
65
|
+
positionFixed(dropdown, anchor);
|
|
66
|
+
document.body.appendChild(dropdown);
|
|
67
|
+
}
|
|
68
|
+
dropdown.classList.add("open");
|
|
69
|
+
trigger.classList.add("open");
|
|
70
|
+
},
|
|
71
|
+
close: function () {
|
|
72
|
+
if (!dropdown.classList.contains("open")) return;
|
|
73
|
+
dropdown.classList.remove("open");
|
|
74
|
+
trigger.classList.remove("open");
|
|
75
|
+
if (opts.portal && home) {
|
|
76
|
+
resetPosition(dropdown);
|
|
77
|
+
home.appendChild(dropdown);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
setValue: function (value, text) {
|
|
81
|
+
if (valueSpan) {
|
|
82
|
+
let label = text;
|
|
83
|
+
if (typeof label !== "string") {
|
|
84
|
+
const opt = dropdown.querySelector('.custom-select-option[data-value="' + value + '"]');
|
|
85
|
+
label = opt ? (opt.dataset.label || opt.textContent.trim()) : "";
|
|
86
|
+
}
|
|
87
|
+
if (!value && !label) label = placeholderText;
|
|
88
|
+
valueSpan.textContent = label;
|
|
89
|
+
valueSpan.classList.toggle("placeholder", !value);
|
|
90
|
+
}
|
|
91
|
+
dropdown.querySelectorAll(".custom-select-option").forEach(function (opt) {
|
|
92
|
+
opt.classList.toggle("selected", opt.dataset.value === value);
|
|
93
|
+
});
|
|
94
|
+
instance.value = value;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
trigger.addEventListener("click", function (e) {
|
|
99
|
+
e.stopPropagation();
|
|
100
|
+
if (dropdown.classList.contains("open")) {
|
|
101
|
+
instance.close();
|
|
102
|
+
} else {
|
|
103
|
+
instance.open();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
trigger.addEventListener("keydown", function (e) {
|
|
108
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
109
|
+
e.preventDefault();
|
|
110
|
+
e.stopPropagation();
|
|
111
|
+
if (dropdown.classList.contains("open")) {
|
|
112
|
+
instance.close();
|
|
113
|
+
} else {
|
|
114
|
+
instance.open();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
dropdown.addEventListener("click", function (e) {
|
|
119
|
+
const option = e.target.closest(".custom-select-option");
|
|
120
|
+
if (!option) return;
|
|
121
|
+
e.stopPropagation();
|
|
122
|
+
const value = option.dataset.value || "";
|
|
123
|
+
const text = option.dataset.label || option.textContent.trim();
|
|
124
|
+
instance.setValue(value, text);
|
|
125
|
+
instance.close();
|
|
126
|
+
if (typeof opts.onSelect === "function") opts.onSelect(value, text, option);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
instances.push(instance);
|
|
130
|
+
dropdown.__customSelect = instance;
|
|
131
|
+
bindGlobalClose();
|
|
132
|
+
return instance;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { init: init };
|
|
136
|
+
})();
|
|
@@ -404,6 +404,49 @@ const MentionAC = (() => {
|
|
|
404
404
|
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
+
// Promote an "@" already committed by the browser into the tracked marker.
|
|
408
|
+
// Windows IMEs may suppress the printable keydown (or expose it as Process),
|
|
409
|
+
// so the input/composition events are the only reliable character boundary.
|
|
410
|
+
function _adoptCommittedAtMarker() {
|
|
411
|
+
const el = _inputEl();
|
|
412
|
+
const sel = window.getSelection();
|
|
413
|
+
if (!el || !sel || !sel.rangeCount || !sel.isCollapsed) return false;
|
|
414
|
+
|
|
415
|
+
const caret = sel.getRangeAt(0);
|
|
416
|
+
if (!el.contains(caret.startContainer)) return false;
|
|
417
|
+
|
|
418
|
+
let node = caret.startContainer;
|
|
419
|
+
let offset = caret.startOffset;
|
|
420
|
+
if (node.nodeType !== Node.TEXT_NODE) {
|
|
421
|
+
if (node.nodeType !== Node.ELEMENT_NODE || offset === 0) return false;
|
|
422
|
+
const previousChildIndex = offset - 1;
|
|
423
|
+
node = node.childNodes[previousChildIndex];
|
|
424
|
+
while (node && node.lastChild) node = node.lastChild;
|
|
425
|
+
if (!node || node.nodeType !== Node.TEXT_NODE) return false;
|
|
426
|
+
offset = node.nodeValue.length;
|
|
427
|
+
}
|
|
428
|
+
const atOffset = offset - 1;
|
|
429
|
+
if (atOffset < 0 || node.nodeValue[atOffset] !== "@") return false;
|
|
430
|
+
|
|
431
|
+
const at = document.createRange();
|
|
432
|
+
at.setStart(node, atOffset);
|
|
433
|
+
at.setEnd(node, offset);
|
|
434
|
+
at.deleteContents();
|
|
435
|
+
|
|
436
|
+
const marker = document.createElement("span");
|
|
437
|
+
marker.className = "mention-at-marker";
|
|
438
|
+
marker.textContent = "@";
|
|
439
|
+
at.insertNode(marker);
|
|
440
|
+
|
|
441
|
+
const after = document.createRange();
|
|
442
|
+
after.setStartAfter(marker);
|
|
443
|
+
after.collapse(true);
|
|
444
|
+
sel.removeAllRanges();
|
|
445
|
+
sel.addRange(after);
|
|
446
|
+
_atNode = marker;
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
|
|
407
450
|
// Cancel path: turn the tracked "@" marker back into plain text so the user
|
|
408
451
|
// keeps the character they typed.
|
|
409
452
|
function _unwrapAtMarker() {
|
|
@@ -569,14 +612,27 @@ const MentionAC = (() => {
|
|
|
569
612
|
}
|
|
570
613
|
});
|
|
571
614
|
|
|
572
|
-
//
|
|
573
|
-
|
|
615
|
+
// Keydown is not reliable while a Windows IME is active. Once the browser
|
|
616
|
+
// commits a literal "@", adopt it into the existing marker state machine.
|
|
617
|
+
// Composing input waits for compositionend so we never rewrite active IME
|
|
618
|
+
// text; non-composing input can be handled immediately.
|
|
619
|
+
el.addEventListener("input", (e) => {
|
|
620
|
+
_cfg = cfg;
|
|
621
|
+
if (!_visible && !e.isComposing && e.data === "@" && _adoptCommittedAtMarker()) {
|
|
622
|
+
open("root");
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
574
625
|
if (_visible && _atNode && !el.contains(_atNode)) {
|
|
575
626
|
_atNode = null;
|
|
576
627
|
hide();
|
|
577
628
|
}
|
|
578
629
|
});
|
|
579
630
|
|
|
631
|
+
el.addEventListener("compositionend", (e) => {
|
|
632
|
+
_cfg = cfg;
|
|
633
|
+
if (!_visible && e.data === "@" && _adoptCommittedAtMarker()) open("root");
|
|
634
|
+
});
|
|
635
|
+
|
|
580
636
|
// Optional @ button removed — @ is opened via the keyboard only.
|
|
581
637
|
|
|
582
638
|
return {
|