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
|
@@ -17,24 +17,208 @@
|
|
|
17
17
|
const LANG_MAP = {
|
|
18
18
|
markdown: () => CM.markdown({ base: CM.markdownLanguage }),
|
|
19
19
|
md: () => CM.markdown({ base: CM.markdownLanguage }),
|
|
20
|
+
python: () => CM.python(),
|
|
21
|
+
javascript: () => CM.javascript(),
|
|
22
|
+
jsx: () => CM.jsx(),
|
|
23
|
+
typescript: () => CM.typescript(),
|
|
24
|
+
tsx: () => CM.tsx(),
|
|
25
|
+
html: () => CM.html(),
|
|
26
|
+
css: () => CM.css(),
|
|
27
|
+
json: () => CM.json(),
|
|
28
|
+
sql: () => CM.sql(),
|
|
29
|
+
xml: () => CM.xml(),
|
|
30
|
+
rust: () => CM.rust(),
|
|
31
|
+
java: () => CM.java(),
|
|
32
|
+
cpp: () => CM.cpp(),
|
|
33
|
+
php: () => CM.php(),
|
|
34
|
+
ruby: () => CM.ruby(),
|
|
35
|
+
go: () => CM.go(),
|
|
36
|
+
shell: () => CM.shell(),
|
|
37
|
+
yaml: () => CM.yaml(),
|
|
38
|
+
toml: () => CM.toml(),
|
|
39
|
+
dockerfile: () => CM.dockerfile(),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Extension → language id. Keyed by lowercase extension (no leading dot).
|
|
43
|
+
const EXT_LANG = {
|
|
44
|
+
md: "markdown", markdown: "markdown", mdx: "markdown",
|
|
45
|
+
py: "python", pyw: "python",
|
|
46
|
+
js: "javascript", mjs: "javascript", cjs: "javascript",
|
|
47
|
+
jsx: "jsx",
|
|
48
|
+
ts: "typescript", mts: "typescript", cts: "typescript",
|
|
49
|
+
tsx: "tsx",
|
|
50
|
+
html: "html", htm: "html",
|
|
51
|
+
css: "css",
|
|
52
|
+
json: "json", jsonc: "json",
|
|
53
|
+
sql: "sql",
|
|
54
|
+
xml: "xml", svg: "xml", xsd: "xml", xsl: "xml", plist: "xml",
|
|
55
|
+
rs: "rust",
|
|
56
|
+
java: "java",
|
|
57
|
+
c: "cpp", h: "cpp", cpp: "cpp", cc: "cpp", cxx: "cpp", hpp: "cpp", hxx: "cpp", hh: "cpp",
|
|
58
|
+
php: "php", php3: "php", php4: "php", php5: "php", phtml: "php",
|
|
59
|
+
rb: "ruby", rbw: "ruby", gemspec: "ruby", rake: "ruby", ru: "ruby",
|
|
60
|
+
go: "go",
|
|
61
|
+
sh: "shell", bash: "shell", zsh: "shell", ksh: "shell",
|
|
62
|
+
yaml: "yaml", yml: "yaml",
|
|
63
|
+
toml: "toml",
|
|
64
|
+
vue: "html", svelte: "html",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Whole filename (no extension) → language id.
|
|
68
|
+
const FILENAME_LANG = {
|
|
69
|
+
dockerfile: "dockerfile",
|
|
70
|
+
gemfile: "ruby", rakefile: "ruby", vagrantfile: "ruby", guardfile: "ruby",
|
|
71
|
+
podfile: "ruby", berksfile: "ruby", brewfile: "ruby",
|
|
20
72
|
};
|
|
21
73
|
|
|
22
74
|
const IMAGE_EXTS = new Set(["png","jpg","jpeg","gif","bmp","webp","svg","ico","tiff","tif","avif"]);
|
|
23
|
-
const
|
|
75
|
+
const PDF_EXTS = new Set(["pdf"]);
|
|
76
|
+
const BINARY_EXTS = new Set([
|
|
77
|
+
// archives
|
|
78
|
+
"zip","gz","7z","tar","dmg","rar","tgz","iso","jar","war",
|
|
79
|
+
// office docs (previewed via the "open with app" fallback instead)
|
|
80
|
+
"xls","xlsx","doc","docx","ppt","pptx","pot","pps","ppsx","dot",
|
|
81
|
+
"odt","ods","odp","wps","et","dps","key","numbers","pages",
|
|
82
|
+
// media
|
|
83
|
+
"mov","mp4","mp3","avi","mkv","wmv","weba","flac","wav","m4a","aac","aiff",
|
|
84
|
+
// fonts
|
|
85
|
+
"ttf","otf","woff","woff2","eot",
|
|
86
|
+
// system / misc
|
|
87
|
+
"exe","msi","db","db3","sqlite","sqlite3","dat","wasm","bin","so","dylib","dll",
|
|
88
|
+
"class","pyc","psd","ai","eps","indd","sketch","fig",
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
// Sample size matches the server-side 512-byte sniff; the ratio tolerates a
|
|
92
|
+
// few stray replacement chars in otherwise valid text.
|
|
93
|
+
const SNIFF_SAMPLE_CHARS = 512;
|
|
94
|
+
const SNIFF_REPLACEMENT_RATIO = 0.1;
|
|
95
|
+
|
|
96
|
+
const TABLE_EXTS = new Set(["csv","tsv"]);
|
|
97
|
+
|
|
98
|
+
// Rendering a full <table> forces the browser to lay out every cell to resolve
|
|
99
|
+
// column widths, so cost grows with cells rather than lines. Measured in Chrome:
|
|
100
|
+
// 1000x20 (20k cells) ~73ms, 3000x20 (60k cells) ~231ms.
|
|
101
|
+
const TABLE_MAX_ROWS = 1000;
|
|
102
|
+
const TABLE_MAX_CELLS = 50000;
|
|
24
103
|
|
|
25
104
|
function _fileKind(filename) {
|
|
26
105
|
if (!filename) return "text";
|
|
27
106
|
const ext = filename.split(".").pop().toLowerCase();
|
|
28
107
|
if (IMAGE_EXTS.has(ext)) return "image";
|
|
108
|
+
if (PDF_EXTS.has(ext)) return "pdf";
|
|
29
109
|
if (BINARY_EXTS.has(ext)) return "binary";
|
|
110
|
+
if (TABLE_EXTS.has(ext)) return "table";
|
|
30
111
|
return "text";
|
|
31
112
|
}
|
|
32
113
|
|
|
114
|
+
// BINARY_EXTS can only list what we thought of, so unlisted binaries still
|
|
115
|
+
// reach the text branch and render as noise. Mirrors the server-side sniff in
|
|
116
|
+
// Utils::FileProcessor.binary_file_path?: a NUL byte means "not text". Decoding
|
|
117
|
+
// already turned invalid sequences into U+FFFD, so a high density of those is
|
|
118
|
+
// the same signal for encodings that carry no NUL.
|
|
119
|
+
function _looksBinary(text) {
|
|
120
|
+
if (!text) return false;
|
|
121
|
+
const head = text.slice(0, SNIFF_SAMPLE_CHARS);
|
|
122
|
+
if (head.indexOf("\u0000") >= 0) return true;
|
|
123
|
+
let replacements = 0;
|
|
124
|
+
for (let i = 0; i < head.length; i++) {
|
|
125
|
+
if (head.charCodeAt(i) === 0xFFFD) replacements++;
|
|
126
|
+
}
|
|
127
|
+
return replacements / head.length > SNIFF_REPLACEMENT_RATIO;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function _tableDelimiter(filename) {
|
|
131
|
+
const ext = (filename || "").split(".").pop().toLowerCase();
|
|
132
|
+
return ext === "tsv" ? "\t" : ",";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// RFC 4180: quoted fields may contain the delimiter, newlines and "" escapes.
|
|
136
|
+
function _parseDelimited(text, delimiter, maxRows) {
|
|
137
|
+
const rows = [];
|
|
138
|
+
let row = [];
|
|
139
|
+
let field = "";
|
|
140
|
+
let quoted = false;
|
|
141
|
+
let i = 0;
|
|
142
|
+
const limit = typeof maxRows === "number" ? maxRows : Infinity;
|
|
143
|
+
|
|
144
|
+
const endRow = () => {
|
|
145
|
+
row.push(field);
|
|
146
|
+
field = "";
|
|
147
|
+
rows.push(row);
|
|
148
|
+
row = [];
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
while (i < text.length) {
|
|
152
|
+
const ch = text[i];
|
|
153
|
+
if (quoted) {
|
|
154
|
+
if (ch === '"') {
|
|
155
|
+
if (text[i + 1] === '"') { field += '"'; i += 2; continue; }
|
|
156
|
+
quoted = false; i++; continue;
|
|
157
|
+
}
|
|
158
|
+
field += ch; i++; continue;
|
|
159
|
+
}
|
|
160
|
+
if (ch === '"' && field === "") { quoted = true; i++; continue; }
|
|
161
|
+
if (ch === delimiter) { row.push(field); field = ""; i++; continue; }
|
|
162
|
+
if (ch === "\r") { i += text[i + 1] === "\n" ? 2 : 1; endRow(); if (rows.length >= limit) return rows; continue; }
|
|
163
|
+
if (ch === "\n") { i++; endRow(); if (rows.length >= limit) return rows; continue; }
|
|
164
|
+
field += ch; i++;
|
|
165
|
+
}
|
|
166
|
+
if (field !== "" || row.length) endRow();
|
|
167
|
+
return rows;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Returns { table, rows, cols, truncated }. Cells are set via textContent so
|
|
171
|
+
// untrusted file data can never be interpreted as markup.
|
|
172
|
+
function _renderTable(text, filename) {
|
|
173
|
+
const delimiter = _tableDelimiter(filename);
|
|
174
|
+
const rows = _parseDelimited(text, delimiter, TABLE_MAX_ROWS + 1);
|
|
175
|
+
|
|
176
|
+
let truncated = rows.length > TABLE_MAX_ROWS;
|
|
177
|
+
if (truncated) rows.length = TABLE_MAX_ROWS;
|
|
178
|
+
|
|
179
|
+
const cols = rows.reduce((max, r) => (r.length > max ? r.length : max), 0);
|
|
180
|
+
if (cols > 0) {
|
|
181
|
+
const maxRowsByCells = Math.max(1, Math.floor(TABLE_MAX_CELLS / cols));
|
|
182
|
+
if (rows.length > maxRowsByCells) {
|
|
183
|
+
rows.length = maxRowsByCells;
|
|
184
|
+
truncated = true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const table = document.createElement("table");
|
|
189
|
+
if (rows.length) {
|
|
190
|
+
const thead = document.createElement("thead");
|
|
191
|
+
const headTr = document.createElement("tr");
|
|
192
|
+
rows[0].forEach((cell) => {
|
|
193
|
+
const th = document.createElement("th");
|
|
194
|
+
th.textContent = cell;
|
|
195
|
+
headTr.appendChild(th);
|
|
196
|
+
});
|
|
197
|
+
thead.appendChild(headTr);
|
|
198
|
+
table.appendChild(thead);
|
|
199
|
+
|
|
200
|
+
const tbody = document.createElement("tbody");
|
|
201
|
+
for (let r = 1; r < rows.length; r++) {
|
|
202
|
+
const tr = document.createElement("tr");
|
|
203
|
+
for (let c = 0; c < cols; c++) {
|
|
204
|
+
const td = document.createElement("td");
|
|
205
|
+
td.textContent = rows[r][c] == null ? "" : rows[r][c];
|
|
206
|
+
tr.appendChild(td);
|
|
207
|
+
}
|
|
208
|
+
tbody.appendChild(tr);
|
|
209
|
+
}
|
|
210
|
+
table.appendChild(tbody);
|
|
211
|
+
}
|
|
212
|
+
return { table: table, rows: rows.length, cols: cols, truncated: truncated };
|
|
213
|
+
}
|
|
214
|
+
|
|
33
215
|
function _detectLanguage(filename) {
|
|
34
216
|
if (!filename) return null;
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
217
|
+
const base = filename.split(/[\\/]/).pop().toLowerCase();
|
|
218
|
+
if (FILENAME_LANG[base]) return FILENAME_LANG[base];
|
|
219
|
+
const dot = base.lastIndexOf(".");
|
|
220
|
+
if (dot < 0) return null;
|
|
221
|
+
return EXT_LANG[base.slice(dot + 1)] || null;
|
|
38
222
|
}
|
|
39
223
|
|
|
40
224
|
function _isDark() {
|
|
@@ -76,7 +260,7 @@
|
|
|
76
260
|
extensions.push(CM.oneDark);
|
|
77
261
|
}
|
|
78
262
|
|
|
79
|
-
const langFn = LANG_MAP[opts.language
|
|
263
|
+
const langFn = opts.language ? LANG_MAP[opts.language] : null;
|
|
80
264
|
if (langFn) extensions.push(langFn());
|
|
81
265
|
|
|
82
266
|
if (opts.readOnly) {
|
|
@@ -261,7 +445,9 @@
|
|
|
261
445
|
open,
|
|
262
446
|
createInline,
|
|
263
447
|
fileKind: _fileKind,
|
|
448
|
+
looksBinary: _looksBinary,
|
|
264
449
|
detectLanguage: _detectLanguage,
|
|
265
450
|
renderMarkdown: _renderMarkdownHtml,
|
|
451
|
+
renderTable: _renderTable,
|
|
266
452
|
};
|
|
267
453
|
})(window);
|
|
@@ -124,8 +124,10 @@ const Composer = (() => {
|
|
|
124
124
|
|
|
125
125
|
// Extract mention chips as plain data objects, in document order.
|
|
126
126
|
// file/directory → { type, name, path }; session → { type, name, session_id }.
|
|
127
|
+
// Quote chips ride on the same chip mechanics (atomic, skipped by text()) but
|
|
128
|
+
// are not mentions — quote-select.js reads them off the DOM itself.
|
|
127
129
|
function chips(el) {
|
|
128
|
-
return Array.from(el.querySelectorAll("." + CHIP_CLASS)).map((c) => {
|
|
130
|
+
return Array.from(el.querySelectorAll("." + CHIP_CLASS + ":not(.quote-chip)")).map((c) => {
|
|
129
131
|
const type = c.dataset.mentionType || "";
|
|
130
132
|
const out = { type: type, name: c.dataset.name || "" };
|
|
131
133
|
if (type === "session") {
|
|
@@ -45,12 +45,12 @@ const DatePicker = (() => {
|
|
|
45
45
|
function _ensurePopup() {
|
|
46
46
|
if (_popup) return;
|
|
47
47
|
_popup = document.createElement("div");
|
|
48
|
-
_popup.className = "
|
|
48
|
+
_popup.className = "dpk-popup";
|
|
49
49
|
_popup.hidden = true;
|
|
50
50
|
document.body.appendChild(_popup);
|
|
51
51
|
|
|
52
52
|
_popup.addEventListener("click", (e) => {
|
|
53
|
-
const nav = e.target.closest(".
|
|
53
|
+
const nav = e.target.closest(".dpk-nav");
|
|
54
54
|
if (nav) {
|
|
55
55
|
e.stopPropagation();
|
|
56
56
|
_month += parseInt(nav.dataset.dir);
|
|
@@ -61,10 +61,10 @@ const DatePicker = (() => {
|
|
|
61
61
|
}
|
|
62
62
|
const day = e.target.closest("[data-date]");
|
|
63
63
|
if (day) { _pick(day.dataset.date); return; }
|
|
64
|
-
if (e.target.closest(".
|
|
64
|
+
if (e.target.closest(".dpk-btn-clear")) {
|
|
65
65
|
clear(_anchor); _dispatch(); _close(); return;
|
|
66
66
|
}
|
|
67
|
-
if (e.target.closest(".
|
|
67
|
+
if (e.target.closest(".dpk-btn-today")) {
|
|
68
68
|
const t = new Date();
|
|
69
69
|
_pick(_toISO(t.getFullYear(), t.getMonth(), t.getDate()));
|
|
70
70
|
}
|
|
@@ -80,31 +80,31 @@ const DatePicker = (() => {
|
|
|
80
80
|
const firstWeekday = new Date(_year, _month, 1).getDay();
|
|
81
81
|
const daysInMonth = new Date(_year, _month + 1, 0).getDate();
|
|
82
82
|
|
|
83
|
-
let html = `<div class="
|
|
84
|
-
<button class="
|
|
85
|
-
<button class="
|
|
86
|
-
<span class="
|
|
87
|
-
<button class="
|
|
88
|
-
<button class="
|
|
83
|
+
let html = `<div class="dpk-header">
|
|
84
|
+
<button class="dpk-nav" data-dir="-12" type="button">≪</button>
|
|
85
|
+
<button class="dpk-nav" data-dir="-1" type="button">‹</button>
|
|
86
|
+
<span class="dpk-title">${loc.title(_year, _month, loc.months)}</span>
|
|
87
|
+
<button class="dpk-nav" data-dir="1" type="button">›</button>
|
|
88
|
+
<button class="dpk-nav" data-dir="12" type="button">≫</button>
|
|
89
89
|
</div>
|
|
90
|
-
<div class="
|
|
90
|
+
<div class="dpk-grid">`;
|
|
91
91
|
|
|
92
|
-
loc.days.forEach(d => { html += `<div class="
|
|
92
|
+
loc.days.forEach(d => { html += `<div class="dpk-dow">${d}</div>`; });
|
|
93
93
|
for (let i = 0; i < firstWeekday; i++) html += `<div></div>`;
|
|
94
94
|
|
|
95
95
|
for (let d = 1; d <= daysInMonth; d++) {
|
|
96
96
|
const iso = _toISO(_year, _month, d);
|
|
97
|
-
const cls = ["
|
|
98
|
-
iso === selVal ? "
|
|
99
|
-
(_year === tY && _month === tM && d === tD) ? "
|
|
97
|
+
const cls = ["dpk-day",
|
|
98
|
+
iso === selVal ? "dpk-day--selected" : "",
|
|
99
|
+
(_year === tY && _month === tM && d === tD) ? "dpk-day--today" : "",
|
|
100
100
|
].filter(Boolean).join(" ");
|
|
101
101
|
html += `<button class="${cls}" data-date="${iso}" type="button">${d}</button>`;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
html += `</div>
|
|
105
|
-
<div class="
|
|
106
|
-
<button class="
|
|
107
|
-
<button class="
|
|
105
|
+
<div class="dpk-footer">
|
|
106
|
+
<button class="dpk-btn-clear" type="button">${loc.clear}</button>
|
|
107
|
+
<button class="dpk-btn-today" type="button">${loc.today}</button>
|
|
108
108
|
</div>`;
|
|
109
109
|
|
|
110
110
|
_popup.innerHTML = html;
|
|
@@ -171,7 +171,7 @@ const DatePicker = (() => {
|
|
|
171
171
|
_open(trigger);
|
|
172
172
|
return;
|
|
173
173
|
}
|
|
174
|
-
if (_popup && !_popup.hidden && !e.target.closest(".
|
|
174
|
+
if (_popup && !_popup.hidden && !e.target.closest(".dpk-popup")) _close();
|
|
175
175
|
});
|
|
176
176
|
|
|
177
177
|
window.addEventListener("scroll", _position, true);
|
|
@@ -528,18 +528,15 @@ const MentionAC = (() => {
|
|
|
528
528
|
|
|
529
529
|
// ── show / hide ────────────────────────────────────────────────────────
|
|
530
530
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
function _removeOverlay() {
|
|
541
|
-
const overlay = document.getElementById("mention-overlay");
|
|
542
|
-
if (overlay) overlay.remove();
|
|
531
|
+
// Dismiss on outside press. A DOM overlay would have to out-stack the menu,
|
|
532
|
+
// which breaks whenever an ancestor forms a stacking context (the mobile
|
|
533
|
+
// #welcome.new-session-page is position:fixed), so listen on document.
|
|
534
|
+
function _onOutsidePointerDown(e) {
|
|
535
|
+
const menu = _menuEl();
|
|
536
|
+
const input = _inputEl();
|
|
537
|
+
if (menu && menu.contains(e.target)) return;
|
|
538
|
+
if (input && input.contains(e.target)) return;
|
|
539
|
+
hide();
|
|
543
540
|
}
|
|
544
541
|
|
|
545
542
|
function open(mode) {
|
|
@@ -552,7 +549,7 @@ const MentionAC = (() => {
|
|
|
552
549
|
else if (_mode === "files") _renderFiles();
|
|
553
550
|
else if (_mode === "chats") _renderChats();
|
|
554
551
|
_visible = true;
|
|
555
|
-
|
|
552
|
+
document.addEventListener("pointerdown", _onOutsidePointerDown, true);
|
|
556
553
|
}
|
|
557
554
|
|
|
558
555
|
function hide() {
|
|
@@ -564,7 +561,7 @@ const MentionAC = (() => {
|
|
|
564
561
|
const menu = _menuEl();
|
|
565
562
|
if (menu) menu.style.display = "none";
|
|
566
563
|
_unwrapAtMarker();
|
|
567
|
-
|
|
564
|
+
document.removeEventListener("pointerdown", _onOutsidePointerDown, true);
|
|
568
565
|
}
|
|
569
566
|
|
|
570
567
|
function _toggle() {
|
|
@@ -36,7 +36,8 @@ const ModelPicker = (() => {
|
|
|
36
36
|
svg: '<path d="M16.5376 0.0307374L14.3326 3.13223C14.1617 3.37577 13.9331 3.57415 13.6667 3.71001C13.4004 3.84586 13.1045 3.91504 12.8048 3.9115H0.787598V0.015152H16.5376V0.0307374Z"/><path d="M31.5 0.0321655L12.6 26.5273H0L18.9 0.0321655H31.5Z"/><path d="M14.9624 26.5272L17.1832 23.4101C17.3564 23.1689 17.5856 22.9723 17.8513 22.8368C18.1171 22.7012 18.4119 22.6306 18.7109 22.6308H30.7124V26.5272H14.9624Z"/>' },
|
|
37
37
|
{ re: /gemini/i, label: "G", color: "#3B78E7",
|
|
38
38
|
svg: '<path d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81"/>' },
|
|
39
|
-
{ re: /gpt|openai/i, label: "
|
|
39
|
+
{ re: /gpt|openai/i, label: "G", color: "#000",
|
|
40
|
+
svg: '<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/>' },
|
|
40
41
|
{ re: /minimax/i, label: "M", color: "#F2402A",
|
|
41
42
|
svg: '<path style="fill:none;stroke:currentColor;stroke-width:2.4;stroke-linecap:round" d="M4 9.5V14.5M8 6V18M12 3V21M16 6V18M20 9.5V14.5"/>' },
|
|
42
43
|
{ re: /qwen|tongyi/i, label: "Q", color: "#615CED" },
|
|
@@ -67,6 +68,21 @@ const ModelPicker = (() => {
|
|
|
67
68
|
return el;
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
// Disclosure caret shared by the clickable status-bar fields (model name,
|
|
72
|
+
// working dir, reasoning effort). Those render as plain text, so a hover
|
|
73
|
+
// tint alone stays invisible to anyone who does not already know they open
|
|
74
|
+
// a picker — this is the only persistent affordance.
|
|
75
|
+
function caret() {
|
|
76
|
+
const el = document.createElement("span");
|
|
77
|
+
el.className = "sib-caret";
|
|
78
|
+
el.setAttribute("aria-hidden", "true");
|
|
79
|
+
el.innerHTML =
|
|
80
|
+
'<svg viewBox="0 0 16 16" width="9" height="9" aria-hidden="true">' +
|
|
81
|
+
'<path d="M4 6.5L8 10.5L12 6.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' +
|
|
82
|
+
'</svg>';
|
|
83
|
+
return el;
|
|
84
|
+
}
|
|
85
|
+
|
|
70
86
|
// Fetches ratios for names not yet cached. Rows simply stay blank when
|
|
71
87
|
// the request fails (offline, unknown model).
|
|
72
88
|
async function loadPriceRatios(names) {
|
|
@@ -89,6 +105,20 @@ const ModelPicker = (() => {
|
|
|
89
105
|
return _priceCache.get(name) || null;
|
|
90
106
|
}
|
|
91
107
|
|
|
108
|
+
// Provider label for a row: the preset name resolved server-side (same
|
|
109
|
+
// rules the rest of the app uses), localised when the preset ships a
|
|
110
|
+
// name_key. Returns "" for endpoints with no known provider — those rows
|
|
111
|
+
// fall back to the raw host.
|
|
112
|
+
function providerLabel(m) {
|
|
113
|
+
if (!m || !m.provider_name) return "";
|
|
114
|
+
const key = m.provider_name_key;
|
|
115
|
+
if (key && typeof I18n !== "undefined") {
|
|
116
|
+
const translated = I18n.t(key);
|
|
117
|
+
if (translated && translated !== key) return translated;
|
|
118
|
+
}
|
|
119
|
+
return m.provider_name;
|
|
120
|
+
}
|
|
121
|
+
|
|
92
122
|
// Render one latency cell based on a cached result.
|
|
93
123
|
// undefined → empty slot
|
|
94
124
|
// { ok:true } → "812ms" in green/amber/red per threshold
|
|
@@ -239,7 +269,7 @@ const ModelPicker = (() => {
|
|
|
239
269
|
panel.style.visibility = "hidden";
|
|
240
270
|
|
|
241
271
|
const dropRect = container.getBoundingClientRect();
|
|
242
|
-
const btnRect = btn.getBoundingClientRect();
|
|
272
|
+
const btnRect = (btn || anchorRow).getBoundingClientRect();
|
|
243
273
|
const panelRect = panel.getBoundingClientRect();
|
|
244
274
|
const gap = 6;
|
|
245
275
|
const margin = 8;
|
|
@@ -265,7 +295,16 @@ const ModelPicker = (() => {
|
|
|
265
295
|
|
|
266
296
|
_activeSubmodelAnchor = anchorRow;
|
|
267
297
|
anchorRow.classList.add("submodel-open");
|
|
268
|
-
btn.setAttribute("aria-expanded", "true");
|
|
298
|
+
if (btn) btn.setAttribute("aria-expanded", "true");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Open the quick-switch panel anchored to any element. The session info bar
|
|
302
|
+
// caret uses this to reach the same panel without opening the full picker.
|
|
303
|
+
function toggleSubmodelPanel(anchorEl, subInfo, onSwitchSubModel) {
|
|
304
|
+
const panel = document.getElementById("sib-submodel-panel");
|
|
305
|
+
if (!panel) return false;
|
|
306
|
+
_toggleSubmodelPanel(anchorEl, anchorEl, null, subInfo, onSwitchSubModel);
|
|
307
|
+
return panel.style.display !== "none";
|
|
269
308
|
}
|
|
270
309
|
|
|
271
310
|
// ── Benchmark runner ────────────────────────────────────────────────────
|
|
@@ -396,11 +435,6 @@ const ModelPicker = (() => {
|
|
|
396
435
|
}
|
|
397
436
|
|
|
398
437
|
// Model rows.
|
|
399
|
-
const _nameCounts = models.reduce((acc, m) => {
|
|
400
|
-
acc[m.model] = (acc[m.model] || 0) + 1;
|
|
401
|
-
return acc;
|
|
402
|
-
}, {});
|
|
403
|
-
|
|
404
438
|
models.forEach(m => {
|
|
405
439
|
const opt = document.createElement("div");
|
|
406
440
|
opt.className = "sib-model-option";
|
|
@@ -436,22 +470,27 @@ const ModelPicker = (() => {
|
|
|
436
470
|
nameLine.appendChild(vis);
|
|
437
471
|
}
|
|
438
472
|
|
|
439
|
-
//
|
|
473
|
+
// Provider line: which service the model runs through (plus the user's own
|
|
474
|
+
// remark). Rendered on every row so two rows with the same model name are
|
|
475
|
+
// never ambiguous. The endpoint host and the masked key are technical
|
|
476
|
+
// details: they stay out of the line and live in the tooltip instead.
|
|
440
477
|
const remark = (m.remark || "").trim();
|
|
441
|
-
|
|
478
|
+
const host = (() => {
|
|
479
|
+
try { return new URL(m.base_url).host; } catch { return m.base_url || ""; }
|
|
480
|
+
})();
|
|
481
|
+
const provider = providerLabel(m);
|
|
482
|
+
// Unknown endpoints (no matching provider preset) fall back to the host
|
|
483
|
+
// so a row never renders an empty second line.
|
|
484
|
+
const identityBits = [provider || host, remark].filter(Boolean);
|
|
485
|
+
if (identityBits.length) {
|
|
442
486
|
left.classList.add("has-sub");
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if (subBits.length) {
|
|
448
|
-
const subLine = document.createElement("span");
|
|
449
|
-
subLine.className = "sib-model-name-sub";
|
|
450
|
-
subLine.textContent = subBits.join(" · ");
|
|
451
|
-
left.appendChild(subLine);
|
|
452
|
-
opt.title = `${m.model} · ${subBits.join(" · ")}`;
|
|
453
|
-
}
|
|
487
|
+
const subLine = document.createElement("span");
|
|
488
|
+
subLine.className = "sib-model-name-sub";
|
|
489
|
+
subLine.textContent = identityBits.join(" · ");
|
|
490
|
+
left.appendChild(subLine);
|
|
454
491
|
}
|
|
492
|
+
const tipBits = [provider, host, remark, m.api_key_masked].filter(Boolean);
|
|
493
|
+
if (tipBits.length) opt.title = `${m.model} · ${tipBits.join(" · ")}`;
|
|
455
494
|
|
|
456
495
|
opt.appendChild(left);
|
|
457
496
|
|
|
@@ -468,10 +507,12 @@ const ModelPicker = (() => {
|
|
|
468
507
|
right.appendChild(check);
|
|
469
508
|
}
|
|
470
509
|
|
|
471
|
-
if (m.type === "default") {
|
|
510
|
+
if (m.type === "default" || m.type === "lite") {
|
|
472
511
|
const badge = document.createElement("span");
|
|
473
512
|
badge.className = `model-badge ${m.type}`;
|
|
474
|
-
badge
|
|
513
|
+
// Reuses the Settings badge labels so both surfaces stay in sync.
|
|
514
|
+
const labelKey = `settings.models.badge.${m.type}`;
|
|
515
|
+
badge.textContent = (typeof I18n !== "undefined") ? I18n.t(labelKey) : m.type;
|
|
475
516
|
right.appendChild(badge);
|
|
476
517
|
}
|
|
477
518
|
|
|
@@ -519,9 +560,11 @@ const ModelPicker = (() => {
|
|
|
519
560
|
|
|
520
561
|
return {
|
|
521
562
|
vendorBadge,
|
|
563
|
+
caret,
|
|
522
564
|
loadPriceRatios,
|
|
523
565
|
getPrice,
|
|
524
566
|
populate,
|
|
525
567
|
closeSubmodelPanel,
|
|
568
|
+
toggleSubmodelPanel,
|
|
526
569
|
};
|
|
527
570
|
})();
|