openclacky 1.5.21 → 1.5.22
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 +61 -0
- data/lib/clacky/billing/platform_billing.rb +3 -0
- data/lib/clacky/default_extensions/git/panels/git/view.js +1911 -398
- data/lib/clacky/default_extensions/preview/panels/preview/view.js +199 -34
- data/lib/clacky/providers.rb +43 -17
- data/lib/clacky/search_config.rb +33 -11
- data/lib/clacky/server/git_panel.rb +360 -34
- data/lib/clacky/server/http_server.rb +145 -47
- data/lib/clacky/server/preview.rb +118 -17
- data/lib/clacky/server/scheduler.rb +81 -2
- data/lib/clacky/server/web_ui_controller.rb +35 -27
- data/lib/clacky/session_manager.rb +52 -5
- data/lib/clacky/utils/mac_app_detector.rb +8 -7
- data/lib/clacky/utils/model_pricing.rb +60 -2
- data/lib/clacky/utils/windows_app_detector.rb +3 -1
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +650 -96
- data/lib/clacky/web/app.js +59 -6
- data/lib/clacky/web/auth.js +1 -1
- data/lib/clacky/web/components/composer.js +107 -8
- data/lib/clacky/web/components/image-zoom.js +165 -0
- data/lib/clacky/web/components/model-picker.js +6 -4
- data/lib/clacky/web/components/notify.js +2 -2
- data/lib/clacky/web/core/aside.js +60 -3
- data/lib/clacky/web/features/billing/view.js +2 -2
- data/lib/clacky/web/features/channels/view.js +5 -2
- data/lib/clacky/web/features/extensions/view.js +2 -2
- data/lib/clacky/web/features/new-session/store.js +79 -2
- data/lib/clacky/web/features/new-session/view.js +496 -58
- data/lib/clacky/web/features/skills/store.js +1 -1
- data/lib/clacky/web/features/tasks/view.js +162 -51
- data/lib/clacky/web/features/workspace/store.js +13 -6
- data/lib/clacky/web/features/workspace/view.js +153 -163
- data/lib/clacky/web/i18n.js +238 -56
- data/lib/clacky/web/index.html +173 -157
- data/lib/clacky/web/projects.js +37 -4
- data/lib/clacky/web/sessions.js +181 -105
- data/lib/clacky/web/settings.js +26 -8
- data/lib/clacky/web/skills.js +2 -2
- data/lib/clacky/web/theme.js +17 -2
- data/lib/clacky/web/utils.js +17 -2
- data/lib/clacky/web/weixin-qr.html +2 -1
- data/lib/clacky/web/ws-dispatcher.js +8 -29
- metadata +2 -1
|
@@ -135,9 +135,9 @@ const TasksView = (() => {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
const ICONS = {
|
|
138
|
-
play: '<svg width="
|
|
138
|
+
play: '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7.5 4.2a1 1 0 0 1 1.53-.85l10.3 7.8a1 1 0 0 1 0 1.7l-10.3 7.8a1 1 0 0 1-1.53-.85Z"/></svg>',
|
|
139
139
|
more: '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg>',
|
|
140
|
-
edit: '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="
|
|
140
|
+
edit: '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>',
|
|
141
141
|
pause: '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><line x1="10" y1="9" x2="10" y2="15"/><line x1="14" y1="9" x2="14" y2="15"/></svg>',
|
|
142
142
|
resume: '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><polygon points="10 8.5 16 12 10 15.5 10 8.5"/></svg>',
|
|
143
143
|
history:'<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l3 2"/></svg>',
|
|
@@ -146,6 +146,7 @@ const TasksView = (() => {
|
|
|
146
146
|
|
|
147
147
|
let _tab = "tasks";
|
|
148
148
|
let _query = "";
|
|
149
|
+
let _filter = "all"; // all | active | paused
|
|
149
150
|
let _runsTask = null; // narrow the run list to one task (from a row's menu)
|
|
150
151
|
let _menuEl = null;
|
|
151
152
|
let _menuBtn = null;
|
|
@@ -163,6 +164,31 @@ const TasksView = (() => {
|
|
|
163
164
|
return `${_pad(d.getMonth() + 1)}/${_pad(d.getDate())} ${_pad(d.getHours())}:${_pad(d.getMinutes())}`;
|
|
164
165
|
}
|
|
165
166
|
|
|
167
|
+
function _nextRunLabel(iso) {
|
|
168
|
+
if (!iso) return "";
|
|
169
|
+
const d = new Date(iso);
|
|
170
|
+
if (isNaN(d)) return "";
|
|
171
|
+
const mins = Math.round((d - new Date()) / 60000);
|
|
172
|
+
if (mins <= 0) return I18n.t("tasks.next.soon");
|
|
173
|
+
if (mins < 60) return I18n.t("tasks.next.minutes", { n: mins });
|
|
174
|
+
const hours = Math.round(mins / 60);
|
|
175
|
+
if (hours < 24) return I18n.t("tasks.next.hours", { n: hours });
|
|
176
|
+
return I18n.t("tasks.next.days", { n: Math.round(hours / 24) });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function _plainPreview(md) {
|
|
180
|
+
return (md || "")
|
|
181
|
+
.replace(/```[\s\S]*?```/g, " ")
|
|
182
|
+
.replace(/!\[[^\]]*\]\([^)]*\)/g, " ")
|
|
183
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
184
|
+
.replace(/^[ \t]{0,3}#{1,6}[ \t]*/gm, "")
|
|
185
|
+
.replace(/^[ \t]{0,3}>[ \t]?/gm, "")
|
|
186
|
+
.replace(/^[ \t]{0,3}([-*+]|\d+\.)[ \t]+/gm, "")
|
|
187
|
+
.replace(/[*_`~]+/g, "")
|
|
188
|
+
.replace(/\s+/g, " ")
|
|
189
|
+
.trim();
|
|
190
|
+
}
|
|
191
|
+
|
|
166
192
|
function _formatDuration(startIso, endIso) {
|
|
167
193
|
if (!startIso || !endIso) return "";
|
|
168
194
|
const secs = Math.max(0, Math.round((new Date(endIso) - new Date(startIso)) / 1000));
|
|
@@ -204,50 +230,108 @@ const TasksView = (() => {
|
|
|
204
230
|
}
|
|
205
231
|
}
|
|
206
232
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
row.className = "cron-row";
|
|
210
|
-
row.dataset.name = t.name;
|
|
211
|
-
|
|
233
|
+
// Full prompt lives in a modal: a grid cell is too narrow to read a long task body.
|
|
234
|
+
function _openDetail(t) {
|
|
212
235
|
const isPaused = t.scheduled && t.enabled === false;
|
|
213
|
-
|
|
236
|
+
const sched = t.scheduled ? _humanCron(t.cron) : I18n.t("tasks.manual");
|
|
237
|
+
const next = isPaused ? I18n.t("tasks.section.paused")
|
|
238
|
+
: t.scheduled ? _nextRunLabel(t.next_run)
|
|
239
|
+
: "";
|
|
240
|
+
const meta = [sched, next].filter(Boolean).join(" · ");
|
|
241
|
+
|
|
242
|
+
const overlay = document.createElement("div");
|
|
243
|
+
overlay.className = "modal-overlay";
|
|
244
|
+
overlay.innerHTML = `
|
|
245
|
+
<div class="modal-box lg task-detail-modal" role="dialog" aria-modal="true" aria-labelledby="task-detail-title">
|
|
246
|
+
<div class="modal-header">
|
|
247
|
+
<h3 class="modal-title" id="task-detail-title">${escapeHtml(t.name)}</h3>
|
|
248
|
+
<button type="button" class="modal-close-btn" aria-label="${escapeHtml(I18n.t("modal.close"))}"></button>
|
|
249
|
+
</div>
|
|
250
|
+
<div class="modal-body">
|
|
251
|
+
${meta ? `<p class="form-hint">${escapeHtml(meta)}</p>` : ""}
|
|
252
|
+
<div class="extension-readme-body">${_markdownHtml(t.content)}</div>
|
|
253
|
+
</div>
|
|
254
|
+
</div>`;
|
|
255
|
+
document.body.appendChild(overlay);
|
|
214
256
|
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
257
|
+
const close = () => {
|
|
258
|
+
document.removeEventListener("keydown", onKey);
|
|
259
|
+
overlay.remove();
|
|
260
|
+
};
|
|
261
|
+
const onKey = e => { if (e.key === "Escape") close(); };
|
|
262
|
+
document.addEventListener("keydown", onKey);
|
|
263
|
+
overlay.addEventListener("click", e => { if (e.target === overlay) close(); });
|
|
264
|
+
overlay.querySelector(".modal-close-btn").addEventListener("click", close);
|
|
265
|
+
}
|
|
218
266
|
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
267
|
+
function _renderTaskCard(t) {
|
|
268
|
+
const isPaused = t.scheduled && t.enabled === false;
|
|
269
|
+
const card = document.createElement("article");
|
|
270
|
+
card.className = "task-card";
|
|
271
|
+
card.dataset.name = t.name;
|
|
272
|
+
card.classList.toggle("is-paused", isPaused);
|
|
273
|
+
card.classList.toggle("is-manual", !t.scheduled);
|
|
274
|
+
|
|
275
|
+
const content = _plainPreview(t.content);
|
|
276
|
+
const sched = t.scheduled ? _humanCron(t.cron) : I18n.t("tasks.manual");
|
|
277
|
+
const next = isPaused ? I18n.t("tasks.section.paused")
|
|
278
|
+
: t.scheduled ? _nextRunLabel(t.next_run)
|
|
279
|
+
: "";
|
|
280
|
+
const nextAt = t.next_run ? new Date(t.next_run) : null;
|
|
281
|
+
const nextTip = nextAt && !isNaN(nextAt) ? nextAt.toLocaleString() : "";
|
|
282
|
+
|
|
283
|
+
card.innerHTML = `
|
|
284
|
+
<div class="task-card-head">
|
|
285
|
+
<h3 class="task-card-title" title="${escapeHtml(t.name)}">${escapeHtml(t.name)}</h3>
|
|
286
|
+
<div class="task-card-actions">
|
|
287
|
+
<div class="task-card-tools">
|
|
288
|
+
<button type="button" class="task-card-tool task-btn-run" data-tooltip="${escapeHtml(I18n.t("tasks.btn.run"))}" data-i18n-tooltip="tasks.btn.run" aria-label="${escapeHtml(I18n.t("tasks.btn.run"))}">${ICONS.play}</button>
|
|
289
|
+
<button type="button" class="task-card-tool task-btn-edit" data-tooltip="${escapeHtml(I18n.t("tasks.btn.edit"))}" data-i18n-tooltip="tasks.btn.edit" aria-label="${escapeHtml(I18n.t("tasks.btn.edit"))}">${ICONS.edit}</button>
|
|
290
|
+
<button type="button" class="task-card-tool ext-row-more task-btn-more" aria-haspopup="menu" aria-expanded="false" title="${escapeHtml(I18n.t("tasks.btn.more"))}" aria-label="${escapeHtml(I18n.t("tasks.btn.more"))}">${ICONS.more}</button>
|
|
230
291
|
</div>
|
|
292
|
+
${t.scheduled ? `<label class="toggle-switch task-card-toggle" title="${escapeHtml(I18n.t(isPaused ? "tasks.btn.resume" : "tasks.btn.pause"))}">
|
|
293
|
+
<input type="checkbox"${isPaused ? "" : " checked"} />
|
|
294
|
+
<span class="toggle-slider"></span>
|
|
295
|
+
</label>` : ""}
|
|
231
296
|
</div>
|
|
232
297
|
</div>
|
|
233
|
-
|
|
298
|
+
<p class="task-card-desc">${escapeHtml(content) || escapeHtml(I18n.t("tasks.empty"))}</p>
|
|
299
|
+
<div class="task-card-foot">
|
|
300
|
+
<span class="task-card-sched">
|
|
301
|
+
<svg class="task-card-clock" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><polyline points="12 7 12 12 15.5 14"/></svg>
|
|
302
|
+
<span class="task-card-sched-text" title="${escapeHtml(t.cron || sched)}">${escapeHtml(sched)}</span>
|
|
303
|
+
</span>
|
|
304
|
+
${next ? `<span class="task-card-next"${nextTip ? ` title="${escapeHtml(nextTip)}"` : ""}>${escapeHtml(next)}</span>` : ""}
|
|
305
|
+
</div>`;
|
|
234
306
|
|
|
235
|
-
|
|
307
|
+
const toggle = card.querySelector(".task-card-toggle input");
|
|
308
|
+
if (toggle) {
|
|
309
|
+
toggle.addEventListener("change", e => {
|
|
310
|
+
e.stopPropagation();
|
|
311
|
+
Tasks.toggleEnabled(t.name, isPaused);
|
|
312
|
+
});
|
|
313
|
+
toggle.addEventListener("click", e => e.stopPropagation());
|
|
314
|
+
}
|
|
315
|
+
card.querySelector(".task-btn-run").addEventListener("click", e => {
|
|
236
316
|
e.stopPropagation();
|
|
237
317
|
Tasks.run(t.name);
|
|
238
318
|
});
|
|
239
|
-
|
|
319
|
+
card.querySelector(".task-btn-edit").addEventListener("click", e => {
|
|
320
|
+
e.stopPropagation();
|
|
321
|
+
Tasks.editInSession(t.name);
|
|
322
|
+
});
|
|
323
|
+
card.querySelector(".task-btn-more").addEventListener("click", e => {
|
|
240
324
|
e.stopPropagation();
|
|
241
325
|
_toggleMenu(e.currentTarget, t, isPaused);
|
|
242
326
|
});
|
|
243
|
-
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
327
|
+
|
|
328
|
+
if (t.content) {
|
|
329
|
+
card.addEventListener("click", e => {
|
|
330
|
+
if (e.target.closest("button, label, a")) return;
|
|
331
|
+
_openDetail(t);
|
|
248
332
|
});
|
|
249
333
|
}
|
|
250
|
-
return
|
|
334
|
+
return card;
|
|
251
335
|
}
|
|
252
336
|
|
|
253
337
|
function _menuItems(t, isPaused) {
|
|
@@ -297,6 +381,8 @@ const TasksView = (() => {
|
|
|
297
381
|
el.hidden = false;
|
|
298
382
|
_menuBtn = btn;
|
|
299
383
|
btn.setAttribute("aria-expanded", "true");
|
|
384
|
+
const card = btn.closest(".task-card");
|
|
385
|
+
if (card) card.classList.add("is-menu-open");
|
|
300
386
|
|
|
301
387
|
const rect = btn.getBoundingClientRect();
|
|
302
388
|
const w = el.offsetWidth, h = el.offsetHeight;
|
|
@@ -309,7 +395,11 @@ const TasksView = (() => {
|
|
|
309
395
|
function _closeMenu() {
|
|
310
396
|
if (!_menuEl || _menuEl.hidden) return;
|
|
311
397
|
_menuEl.hidden = true;
|
|
312
|
-
if (_menuBtn)
|
|
398
|
+
if (_menuBtn) {
|
|
399
|
+
_menuBtn.setAttribute("aria-expanded", "false");
|
|
400
|
+
const card = _menuBtn.closest(".task-card");
|
|
401
|
+
if (card) card.classList.remove("is-menu-open");
|
|
402
|
+
}
|
|
313
403
|
_menuBtn = null;
|
|
314
404
|
}
|
|
315
405
|
|
|
@@ -320,13 +410,6 @@ const TasksView = (() => {
|
|
|
320
410
|
return h;
|
|
321
411
|
}
|
|
322
412
|
|
|
323
|
-
function _sectionEmpty(text) {
|
|
324
|
-
const e = document.createElement("div");
|
|
325
|
-
e.className = "task-section-empty";
|
|
326
|
-
e.textContent = text;
|
|
327
|
-
return e;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
413
|
function _emptyState(text) {
|
|
331
414
|
const el = document.createElement("div");
|
|
332
415
|
el.className = "task-table-empty";
|
|
@@ -334,6 +417,30 @@ const TasksView = (() => {
|
|
|
334
417
|
return el;
|
|
335
418
|
}
|
|
336
419
|
|
|
420
|
+
function _renderFilterBar(matched) {
|
|
421
|
+
const isPaused = t => t.scheduled && t.enabled === false;
|
|
422
|
+
const counts = {
|
|
423
|
+
all: matched.length,
|
|
424
|
+
active: matched.filter(t => !isPaused(t)).length,
|
|
425
|
+
paused: matched.filter(isPaused).length,
|
|
426
|
+
};
|
|
427
|
+
const bar = document.createElement("div");
|
|
428
|
+
bar.className = "segmented-control";
|
|
429
|
+
bar.setAttribute("role", "tablist");
|
|
430
|
+
bar.innerHTML = ["all", "active", "paused"].map(kind => `
|
|
431
|
+
<button type="button" role="tab" class="segmented-control__btn${_filter === kind ? " active" : ""}"
|
|
432
|
+
data-filter="${kind}" aria-selected="${_filter === kind}">
|
|
433
|
+
${escapeHtml(I18n.t("tasks.filter." + kind))}<span class="segmented-control__count">(${counts[kind]})</span>
|
|
434
|
+
</button>`).join("");
|
|
435
|
+
bar.querySelectorAll("[data-filter]").forEach(btn => {
|
|
436
|
+
btn.addEventListener("click", () => {
|
|
437
|
+
_filter = btn.dataset.filter;
|
|
438
|
+
_renderTable();
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
return bar;
|
|
442
|
+
}
|
|
443
|
+
|
|
337
444
|
function _renderTable() {
|
|
338
445
|
const table = $("task-list-table");
|
|
339
446
|
if (!table) return;
|
|
@@ -354,23 +461,27 @@ const TasksView = (() => {
|
|
|
354
461
|
return;
|
|
355
462
|
}
|
|
356
463
|
|
|
357
|
-
const
|
|
358
|
-
|
|
464
|
+
const matched = _query ? all.filter(t => _matches(t.name) || _matches(t.content)) : all;
|
|
465
|
+
table.appendChild(_renderFilterBar(matched));
|
|
466
|
+
|
|
467
|
+
if (matched.length === 0) {
|
|
359
468
|
table.appendChild(_emptyState(I18n.t("tasks.search.empty")));
|
|
360
469
|
return;
|
|
361
470
|
}
|
|
362
471
|
|
|
363
|
-
const
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
if (paused.length) {
|
|
371
|
-
table.appendChild(_sectionHeader(I18n.t("tasks.section.paused")));
|
|
372
|
-
paused.forEach(t => table.appendChild(_renderTaskRow(t)));
|
|
472
|
+
const isPaused = t => t.scheduled && t.enabled === false;
|
|
473
|
+
const shown = _filter === "active" ? matched.filter(t => !isPaused(t))
|
|
474
|
+
: _filter === "paused" ? matched.filter(isPaused)
|
|
475
|
+
: matched;
|
|
476
|
+
if (shown.length === 0) {
|
|
477
|
+
table.appendChild(_emptyState(I18n.t(_filter === "active" ? "tasks.section.activeEmpty" : "tasks.section.pausedEmpty")));
|
|
478
|
+
return;
|
|
373
479
|
}
|
|
480
|
+
|
|
481
|
+
const grid = document.createElement("div");
|
|
482
|
+
grid.className = "task-card-grid";
|
|
483
|
+
shown.forEach(t => grid.appendChild(_renderTaskCard(t)));
|
|
484
|
+
table.appendChild(grid);
|
|
374
485
|
}
|
|
375
486
|
|
|
376
487
|
function _renderRunRow(r) {
|
|
@@ -40,13 +40,20 @@ const WorkspaceStore = (() => {
|
|
|
40
40
|
|
|
41
41
|
// Throw with the backend's error message when available so toasts show the
|
|
42
42
|
// real cause ("file not found", "unsupported OS", …) instead of "HTTP 404".
|
|
43
|
+
// The machine-readable `code` rides along so callers can react to a specific
|
|
44
|
+
// cause (a deleted working dir) rather than matching on prose.
|
|
43
45
|
async function _respError(resp) {
|
|
44
46
|
let detail = `HTTP ${resp.status}`;
|
|
47
|
+
let code = null;
|
|
45
48
|
try {
|
|
46
49
|
const data = await resp.json();
|
|
47
50
|
if (data && data.error) detail = data.error;
|
|
51
|
+
if (data && data.code) code = data.code;
|
|
48
52
|
} catch (_e) { /* non-JSON body */ }
|
|
49
|
-
|
|
53
|
+
const err = new Error(detail);
|
|
54
|
+
err.status = resp.status;
|
|
55
|
+
err.code = code;
|
|
56
|
+
throw err;
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
const state = {
|
|
@@ -110,18 +117,18 @@ const WorkspaceStore = (() => {
|
|
|
110
117
|
},
|
|
111
118
|
|
|
112
119
|
/** Default application the OS would use to open this file; null if none.
|
|
113
|
-
* Cached per extension so tab switches don't refetch.
|
|
120
|
+
* Cached per extension so tab switches don't refetch. Extension-less
|
|
121
|
+
* files (Dockerfile, Makefile) resolve per path and skip the cache. */
|
|
114
122
|
async fetchDefaultApp(entry) {
|
|
115
123
|
const ext = (entry.path.match(/\.([^./\\]+)$/) || [])[1];
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (Object.prototype.hasOwnProperty.call(_defaultAppCache, key)) {
|
|
124
|
+
const key = ext ? ext.toLowerCase() : null;
|
|
125
|
+
if (key && Object.prototype.hasOwnProperty.call(_defaultAppCache, key)) {
|
|
119
126
|
return _defaultAppCache[key];
|
|
120
127
|
}
|
|
121
128
|
const resp = await fetch("/api/file/default-app?path=" + encodeURIComponent(_absPath(entry.path)));
|
|
122
129
|
const data = resp.ok ? await resp.json() : null;
|
|
123
130
|
const app = data && data.ok ? data.app || null : null;
|
|
124
|
-
_defaultAppCache[key] = app;
|
|
131
|
+
if (key) _defaultAppCache[key] = app;
|
|
125
132
|
return app;
|
|
126
133
|
},
|
|
127
134
|
|