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
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// ── Official panel: time_machine ──────────────────────────────────────────
|
|
2
2
|
//
|
|
3
3
|
// Vertical timeline of the session's tasks (mounted in session.aside tab slot).
|
|
4
|
-
// Clicking a past row
|
|
5
|
-
//
|
|
6
|
-
// the
|
|
7
|
-
//
|
|
8
|
-
|
|
4
|
+
// Clicking a past row expands an inline card (top files + actions). "View
|
|
5
|
+
// details" swaps the whole panel to a two-pane detail view - file list on the
|
|
6
|
+
// left, unified diff on the right - mirroring the Git tab layout inside the
|
|
7
|
+
// aside (the aside widens to fit the diff, same as the Files/Git tabs).
|
|
8
|
+
//// Backed by:
|
|
9
9
|
// GET /api/sessions/:id/time_machine — task list
|
|
10
10
|
// GET /api/sessions/:id/time_machine/:tid/diff — files this task touched
|
|
11
11
|
// GET /api/sessions/:id/time_machine/:tid/diff?path=… — unified diff for one file
|
|
@@ -43,6 +43,11 @@
|
|
|
43
43
|
return (v && v !== k) ? v : fallback;
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
const DIFF_ASIDE_WIDTH = 860;
|
|
47
|
+
const MAX_DIFF_LINES = 3000;
|
|
48
|
+
const ICON_BACK = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>';
|
|
49
|
+
const ICON_CLOSE = '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>';
|
|
50
|
+
|
|
46
51
|
if (!document.getElementById("tm-panel-style")) {
|
|
47
52
|
const style = document.createElement("style");
|
|
48
53
|
style.id = "tm-panel-style";
|
|
@@ -109,7 +114,7 @@
|
|
|
109
114
|
.tm-mini-btn { padding: 4px 10px; font-size: 11px; line-height: 16px; cursor: pointer; border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm); background: var(--color-bg-primary); color: var(--color-text-secondary); }
|
|
110
115
|
.tm-mini-btn:hover { background: var(--color-bg-hover); }
|
|
111
116
|
.tm-mini-btn.primary { background: var(--color-accent-primary); color: var(--color-text-inverse); border-color: var(--color-accent-primary); }
|
|
112
|
-
.tm-mini-btn.primary:hover { background: var(--color-
|
|
117
|
+
.tm-mini-btn.primary:hover { background: var(--color-button-primary-hover); border-color: var(--color-button-primary-hover); }
|
|
113
118
|
.tm-mini-btn:disabled { opacity: 0.5; cursor: default; }
|
|
114
119
|
|
|
115
120
|
.tm-mini-confirm {
|
|
@@ -126,44 +131,33 @@
|
|
|
126
131
|
|
|
127
132
|
.tm-foot { flex: none; padding: 8px 14px; font-size: 11px; color: var(--color-text-tertiary); border-top: 1px solid var(--color-border-secondary); }
|
|
128
133
|
|
|
129
|
-
/* ──
|
|
130
|
-
.tm-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
.tm-
|
|
135
|
-
.tm-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
.tm-
|
|
147
|
-
|
|
148
|
-
.tm-
|
|
149
|
-
.tm-
|
|
150
|
-
|
|
151
|
-
.tm-
|
|
152
|
-
.tm-
|
|
153
|
-
.tm-
|
|
154
|
-
.tm-
|
|
155
|
-
.tm-
|
|
156
|
-
.tm-restore-btn:disabled { opacity: 0.5; cursor: default; }
|
|
157
|
-
|
|
158
|
-
.tm-confirm-row { flex: none; display: flex; gap: 8px; padding: 10px 18px; align-items: center; background: var(--color-bg-secondary); border-bottom: 1px solid var(--color-border-secondary); }
|
|
159
|
-
.tm-confirm-msg { flex: 1; font-size: 12px; color: var(--color-text-secondary); }
|
|
160
|
-
.tm-confirm-btn { padding: 4px 12px; cursor: pointer; border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm); background: var(--color-bg-secondary); font-size: 12px; }
|
|
161
|
-
.tm-confirm-btn.go { background: var(--color-accent-primary); color: var(--color-text-inverse); border-color: transparent; }
|
|
162
|
-
.tm-confirm-btn:disabled { opacity: 0.5; cursor: default; }
|
|
163
|
-
|
|
164
|
-
.tm-drawer-body { flex: 1; min-height: 0; display: flex; }
|
|
165
|
-
.tm-files { flex: 0 0 220px; overflow: auto; border-right: 1px solid var(--color-border-secondary); padding: 6px 0; }
|
|
166
|
-
.tm-file { padding: 7px 14px; font-size: 12px; cursor: pointer; display: flex; align-items: center; gap: 8px; }
|
|
134
|
+
/* ── Detail view (in-aside, mirrors the Git tab layout) ───────────── */
|
|
135
|
+
.tm-detail { display: none; flex: 1; min-height: 0; flex-direction: column; }
|
|
136
|
+
.tm-panel.in-detail .tm-list { display: none; }
|
|
137
|
+
.tm-panel.in-detail .tm-detail { display: flex; }
|
|
138
|
+
|
|
139
|
+
.tm-detail-head { flex: none; display: flex; align-items: center; gap: 8px; padding: 10px 10px 10px 6px; border-bottom: 1px solid var(--color-border-secondary); }
|
|
140
|
+
.tm-detail-back { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border: none; border-radius: var(--radius-sm); background: transparent; color: var(--color-text-secondary); cursor: pointer; }
|
|
141
|
+
.tm-detail-back:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
|
|
142
|
+
.tm-detail-title-wrap { flex: 1; min-width: 0; }
|
|
143
|
+
.tm-detail-title { font-size: 13px; color: var(--color-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
|
|
144
|
+
.tm-detail-time { font-size: 11px; color: var(--color-text-tertiary); margin-top: 2px; }
|
|
145
|
+
.tm-detail-restore { flex: none; padding: 5px 12px; font-size: 12px; cursor: pointer; border: 1px solid transparent; border-radius: var(--radius-sm); background: var(--color-accent-primary); color: var(--color-text-inverse); white-space: nowrap; }
|
|
146
|
+
.tm-detail-restore:hover { background: var(--color-button-primary-hover); }
|
|
147
|
+
.tm-detail-restore:disabled { opacity: 0.5; cursor: default; }
|
|
148
|
+
.tm-detail-restore.armed { background: var(--color-error, #b03a3a); }
|
|
149
|
+
|
|
150
|
+
.tm-detail-body { flex: 1; min-height: 0; display: flex; }
|
|
151
|
+
.tm-detail-files { flex: 1; min-width: 0; overflow: auto; padding: 6px 6px; }
|
|
152
|
+
.tm-panel.has-detail-diff .tm-detail-files { flex: 0 0 232px; border-right: 1px solid var(--color-border-primary); }
|
|
153
|
+
.tm-detail-diff { display: none; flex: 1; min-width: 0; min-height: 0; flex-direction: column; }
|
|
154
|
+
.tm-panel.has-detail-diff .tm-detail-diff { display: flex; }
|
|
155
|
+
|
|
156
|
+
.tm-detail-diff-head { flex: none; display: flex; align-items: center; gap: 8px; padding: 6px 8px 6px 12px; border-bottom: 1px solid var(--color-border-secondary); }
|
|
157
|
+
.tm-detail-diff-path { flex: 1; min-width: 0; font-family: ui-monospace, monospace; font-size: 11.5px; color: var(--color-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
158
|
+
.tm-detail-diff-close { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; border: none; border-radius: var(--radius-sm); background: transparent; color: var(--color-text-tertiary); cursor: pointer; }
|
|
159
|
+
.tm-detail-diff-close:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
|
|
160
|
+
.tm-file { padding: 6px 8px; font-size: 12px; cursor: pointer; display: flex; align-items: center; gap: 8px; border-radius: var(--radius-sm); }
|
|
167
161
|
.tm-file:hover { background: var(--color-bg-hover); }
|
|
168
162
|
.tm-file.active { background: var(--color-accent-soft); }
|
|
169
163
|
.tm-file-tag { flex: none; font-size: 9px; padding: 1px 6px; border-radius: var(--radius-sm); }
|
|
@@ -172,10 +166,7 @@
|
|
|
172
166
|
.tm-file-tag.deleted { background: var(--color-error-soft, #b03a3a33); color: var(--color-error); }
|
|
173
167
|
.tm-file-path { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--color-text-secondary); }
|
|
174
168
|
|
|
175
|
-
.tm-diff
|
|
176
|
-
.tm-diff-path { flex: none; padding: 8px 16px; font-size: 11px; color: var(--color-text-tertiary); border-bottom: 1px solid var(--color-border-secondary); font-family: ui-monospace, monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; direction: rtl; text-align: left; }
|
|
177
|
-
.tm-diff-path:empty { display: none; }
|
|
178
|
-
.tm-diff { flex: 1; min-width: 0; overflow: auto; padding: 10px 0; font-family: ui-monospace, monospace; font-size: 12px; line-height: 1.55; }
|
|
169
|
+
.tm-diff { flex: 1; min-height: 0; overflow: auto; padding: 10px 0; font-family: ui-monospace, monospace; font-size: 12px; line-height: 1.55; }
|
|
179
170
|
.tm-diff-stub, .tm-diff-loading { color: var(--color-text-tertiary); padding: 20px; text-align: center; }
|
|
180
171
|
.tm-diff-line { white-space: pre; padding: 0 16px; min-width: max-content; }
|
|
181
172
|
.tm-diff-line.add { background: var(--color-success-soft, #1f6e2c33); color: var(--color-success, #4eb965); }
|
|
@@ -217,6 +208,21 @@
|
|
|
217
208
|
return dt.toLocaleString();
|
|
218
209
|
}
|
|
219
210
|
|
|
211
|
+
// The diff pane needs horizontal room; widen the aside once when a diff is
|
|
212
|
+
// first opened (same trick as the Files/Git tabs, still user-resizable).
|
|
213
|
+
function ensureAsideWidth() {
|
|
214
|
+
const aside = document.getElementById("session-aside");
|
|
215
|
+
if (!aside) return;
|
|
216
|
+
let w = 0;
|
|
217
|
+
try {
|
|
218
|
+
w = parseFloat(getComputedStyle(aside).getPropertyValue("--session-aside-width")) || 0;
|
|
219
|
+
} catch (_e) { w = 0; }
|
|
220
|
+
if (w < DIFF_ASIDE_WIDTH) {
|
|
221
|
+
aside.style.setProperty("--session-aside-width", DIFF_ASIDE_WIDTH + "px");
|
|
222
|
+
try { localStorage.setItem("clacky.aside.width", String(DIFF_ASIDE_WIDTH)); } catch (_e) { /* non-fatal */ }
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
220
226
|
function renderTimeline(state) {
|
|
221
227
|
const { listEl, tasks } = state;
|
|
222
228
|
listEl.replaceChildren();
|
|
@@ -277,7 +283,7 @@
|
|
|
277
283
|
const actions = el("div", { class: "tm-mini-actions" }, detailsBtn, restoreBtn);
|
|
278
284
|
const card = el("div", { class: `tm-mini ${isUndone ? "undone" : ""}` }, filesWrap, actions);
|
|
279
285
|
|
|
280
|
-
detailsBtn.addEventListener("click", (e) => { e.stopPropagation();
|
|
286
|
+
detailsBtn.addEventListener("click", (e) => { e.stopPropagation(); openDetail(state, task); });
|
|
281
287
|
restoreBtn.addEventListener("click", (e) => {
|
|
282
288
|
e.stopPropagation();
|
|
283
289
|
openConfirm(state, task, actions);
|
|
@@ -396,6 +402,7 @@
|
|
|
396
402
|
if (res.ok) {
|
|
397
403
|
state.footEl.textContent = res.message || t("tm.restored", "已恢复");
|
|
398
404
|
state.expanded = null;
|
|
405
|
+
closeDetail(state);
|
|
399
406
|
await loadHistory(state);
|
|
400
407
|
} else {
|
|
401
408
|
state.footEl.textContent = res.error || t("tm.restoreFailed", "恢复失败");
|
|
@@ -411,72 +418,123 @@
|
|
|
411
418
|
wrap.appendChild(el("div", { class: "tm-diff-stub", text: t("tm.diff.same", "这一步没有改动这个文件的内容。") }));
|
|
412
419
|
return wrap;
|
|
413
420
|
}
|
|
414
|
-
patch.split("\n")
|
|
421
|
+
const lines = patch.split("\n");
|
|
422
|
+
const shown = Math.min(lines.length, MAX_DIFF_LINES);
|
|
423
|
+
for (let i = 0; i < shown; i++) {
|
|
424
|
+
const line = lines[i];
|
|
415
425
|
let cls = "tm-diff-line";
|
|
416
426
|
if (line.startsWith("+++") || line.startsWith("---")) cls += " meta";
|
|
417
427
|
else if (line.startsWith("@@")) cls += " hunk";
|
|
418
428
|
else if (line.startsWith("+")) cls += " add";
|
|
419
429
|
else if (line.startsWith("-")) cls += " del";
|
|
420
430
|
wrap.appendChild(el("div", { class: cls, text: line || " " }));
|
|
421
|
-
}
|
|
431
|
+
}
|
|
432
|
+
if (lines.length > shown) {
|
|
433
|
+
wrap.appendChild(el("div", { class: "tm-diff-stub",
|
|
434
|
+
text: t("tm.diff.truncated", "差异过大,仅显示前 %d 行").replace("%d", String(shown)) }));
|
|
435
|
+
}
|
|
422
436
|
return wrap;
|
|
423
437
|
}
|
|
424
438
|
|
|
425
|
-
async function loadFileDiff(state, rel) {
|
|
426
|
-
state.
|
|
439
|
+
async function loadFileDiff(state, taskId, rel) {
|
|
440
|
+
state.detailDiffEl.replaceChildren(el("div", { class: "tm-diff-loading", text: t("tm.diff.loading", "正在读取差异…") }));
|
|
427
441
|
try {
|
|
428
|
-
const res = await api(state.sessionId, `/${
|
|
442
|
+
const res = await api(state.sessionId, `/${taskId}/diff?path=${encodeURIComponent(rel)}`);
|
|
443
|
+
if (state.detailSelected !== rel) return;
|
|
429
444
|
if (!res.ok) {
|
|
430
|
-
state.
|
|
445
|
+
state.detailDiffEl.replaceChildren(el("div", { class: "tm-diff-stub", text: res.error || t("tm.diff.fail", "读取差异失败") }));
|
|
431
446
|
return;
|
|
432
447
|
}
|
|
433
448
|
if (res.binary) {
|
|
434
|
-
state.
|
|
449
|
+
state.detailDiffEl.replaceChildren(el("div", { class: "tm-diff-stub", text: t("tm.diff.binary", "二进制文件,跳过逐行对比。") }));
|
|
435
450
|
return;
|
|
436
451
|
}
|
|
437
|
-
state.
|
|
452
|
+
state.detailDiffEl.replaceChildren(renderDiffText(res.patch));
|
|
438
453
|
} catch (_e) {
|
|
439
|
-
state.
|
|
454
|
+
state.detailDiffEl.replaceChildren(el("div", { class: "tm-diff-stub", text: t("tm.diff.fail", "读取差异失败") }));
|
|
440
455
|
}
|
|
441
456
|
}
|
|
442
457
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
state.confirmRow.style.display = "none";
|
|
449
|
-
state.restoreBtn.disabled = false;
|
|
450
|
-
state.filesEl.replaceChildren(el("div", { class: "tm-diff-loading", text: t("tm.diff.loading", "正在读取改动…") }));
|
|
451
|
-
state.diffEl.replaceChildren();
|
|
452
|
-
state.diffPathEl.textContent = "";
|
|
453
|
-
|
|
454
|
-
state.maskEl.style.display = "block";
|
|
455
|
-
state.drawerEl.style.display = "flex";
|
|
456
|
-
requestAnimationFrame(() => {
|
|
457
|
-
state.maskEl.classList.add("open");
|
|
458
|
-
state.drawerEl.classList.add("open");
|
|
459
|
-
});
|
|
458
|
+
// ── Detail view (in-aside) ────────────────────────────────────────────────
|
|
459
|
+
// Mirrors the Git tab: file list on the left, diff on the right. The restore
|
|
460
|
+
// button uses the same two-step armed confirm as the Git tab's revert.
|
|
461
|
+
|
|
462
|
+
let detailRestoreTimer = null;
|
|
460
463
|
|
|
461
|
-
|
|
464
|
+
function disarmDetailRestore(state) {
|
|
465
|
+
if (detailRestoreTimer) { clearTimeout(detailRestoreTimer); detailRestoreTimer = null; }
|
|
466
|
+
const btn = state.detailRestoreEl;
|
|
467
|
+
if (!btn) return;
|
|
468
|
+
btn.classList.remove("armed");
|
|
469
|
+
btn.textContent = t("tm.restore.go", "回到这里");
|
|
462
470
|
}
|
|
463
471
|
|
|
464
|
-
|
|
472
|
+
function armDetailRestore(state) {
|
|
473
|
+
disarmDetailRestore(state);
|
|
474
|
+
state.detailRestoreEl.classList.add("armed");
|
|
475
|
+
state.detailRestoreEl.textContent = t("tm.restore.confirm", "确认回到这里");
|
|
476
|
+
detailRestoreTimer = setTimeout(() => disarmDetailRestore(state), 4000);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function onDetailRestoreClick(state) {
|
|
480
|
+
if (!state.detailTask) return;
|
|
481
|
+
if (state.detailRestoreEl.classList.contains("armed")) {
|
|
482
|
+
disarmDetailRestore(state);
|
|
483
|
+
performRestoreInline(state, state.detailTask.task_id);
|
|
484
|
+
} else {
|
|
485
|
+
armDetailRestore(state);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function openDetail(state, task) {
|
|
490
|
+
state.view = "detail";
|
|
491
|
+
state.detailTask = task;
|
|
492
|
+
state.detailSelected = null;
|
|
493
|
+
disarmDetailRestore(state);
|
|
494
|
+
state.panelEl.classList.add("in-detail");
|
|
495
|
+
state.panelEl.classList.remove("has-detail-diff");
|
|
496
|
+
state.detailTitleEl.textContent = task.summary;
|
|
497
|
+
state.detailTimeEl.textContent = task.started_at ? relTime(task.started_at) : "";
|
|
498
|
+
state.detailRestoreEl.disabled = false;
|
|
499
|
+
state.detailFilesEl.replaceChildren(el("div", { class: "tm-diff-loading", text: t("tm.diff.loading", "正在读取改动…") }));
|
|
500
|
+
state.detailDiffPathEl.textContent = "";
|
|
501
|
+
state.detailDiffEl.replaceChildren();
|
|
502
|
+
loadDetailFiles(state, task);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function closeDetail(state) {
|
|
506
|
+
state.view = "timeline";
|
|
507
|
+
state.detailTask = null;
|
|
508
|
+
state.detailSelected = null;
|
|
509
|
+
disarmDetailRestore(state);
|
|
510
|
+
state.panelEl.classList.remove("in-detail", "has-detail-diff");
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function closeDetailDiff(state) {
|
|
514
|
+
state.detailSelected = null;
|
|
515
|
+
state.panelEl.classList.remove("has-detail-diff");
|
|
516
|
+
state.detailDiffPathEl.textContent = "";
|
|
517
|
+
state.detailDiffEl.replaceChildren();
|
|
518
|
+
state.detailFilesEl.querySelectorAll(".tm-file.active").forEach((n) => n.classList.remove("active"));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async function loadDetailFiles(state, task) {
|
|
522
|
+
const taskId = task.task_id;
|
|
465
523
|
let res;
|
|
466
524
|
try {
|
|
467
525
|
res = await api(state.sessionId, `/${taskId}/diff`);
|
|
468
526
|
} catch (_e) {
|
|
469
|
-
state.
|
|
527
|
+
state.detailFilesEl.replaceChildren(el("div", { class: "tm-diff-stub", text: t("tm.diff.fail", "读取改动失败") }));
|
|
470
528
|
return;
|
|
471
529
|
}
|
|
472
|
-
if (state.
|
|
530
|
+
if (state.view !== "detail" || !state.detailTask || state.detailTask.task_id !== taskId) return;
|
|
473
531
|
if (!res.ok) {
|
|
474
|
-
state.
|
|
532
|
+
state.detailFilesEl.replaceChildren(el("div", { class: "tm-diff-stub", text: res.error || t("tm.diff.fail", "读取改动失败") }));
|
|
475
533
|
return;
|
|
476
534
|
}
|
|
477
535
|
const files = res.files || [];
|
|
478
536
|
if (files.length === 0) {
|
|
479
|
-
state.
|
|
537
|
+
state.detailFilesEl.replaceChildren(el("div", { class: "tm-diff-stub", text: t("tm.diff.noFiles", "没有文件改动。") }));
|
|
480
538
|
return;
|
|
481
539
|
}
|
|
482
540
|
const tagText = { added: t("tm.tag.added", "新增"), modified: t("tm.tag.modified", "修改"), deleted: t("tm.tag.deleted", "删除") };
|
|
@@ -486,52 +544,24 @@
|
|
|
486
544
|
el("span", { class: `tm-file-tag ${f.status}`, text: tagText[f.status] || f.status }),
|
|
487
545
|
el("span", { class: "tm-file-path", text: basename }),
|
|
488
546
|
);
|
|
489
|
-
node.addEventListener("click", () =>
|
|
490
|
-
state.filesEl.querySelectorAll(".tm-file.active").forEach((n) => n.classList.remove("active"));
|
|
491
|
-
node.classList.add("active");
|
|
492
|
-
state.diffPathEl.textContent = f.path;
|
|
493
|
-
if (f.binary) {
|
|
494
|
-
state.diffEl.replaceChildren(el("div", { class: "tm-diff-stub", text: t("tm.diff.binary", "二进制文件,跳过逐行对比。") }));
|
|
495
|
-
} else {
|
|
496
|
-
loadFileDiff(state, f.path);
|
|
497
|
-
}
|
|
498
|
-
});
|
|
547
|
+
node.addEventListener("click", () => selectDetailFile(state, task, f, node));
|
|
499
548
|
return node;
|
|
500
549
|
});
|
|
501
|
-
state.
|
|
502
|
-
fileNodes[0]
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function closeDrawer(state) {
|
|
506
|
-
state.maskEl.classList.remove("open");
|
|
507
|
-
state.drawerEl.classList.remove("open");
|
|
508
|
-
setTimeout(() => {
|
|
509
|
-
state.maskEl.style.display = "none";
|
|
510
|
-
state.drawerEl.style.display = "none";
|
|
511
|
-
}, 250);
|
|
512
|
-
state.selected = null;
|
|
550
|
+
state.detailFilesEl.replaceChildren(...fileNodes);
|
|
551
|
+
selectDetailFile(state, task, files[0], fileNodes[0]);
|
|
513
552
|
}
|
|
514
553
|
|
|
515
|
-
|
|
516
|
-
state.
|
|
517
|
-
state.
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
closeDrawer(state);
|
|
527
|
-
await loadHistory(state);
|
|
528
|
-
} else {
|
|
529
|
-
state.footEl.textContent = res.error || t("tm.restoreFailed", "恢复失败");
|
|
530
|
-
state.restoreBtn.disabled = false;
|
|
531
|
-
}
|
|
532
|
-
} catch (_e) {
|
|
533
|
-
state.footEl.textContent = t("tm.restoreFailed", "恢复失败");
|
|
534
|
-
state.restoreBtn.disabled = false;
|
|
554
|
+
function selectDetailFile(state, task, f, node) {
|
|
555
|
+
state.detailSelected = f.path;
|
|
556
|
+
state.detailFilesEl.querySelectorAll(".tm-file.active").forEach((n) => n.classList.remove("active"));
|
|
557
|
+
node.classList.add("active");
|
|
558
|
+
state.panelEl.classList.add("has-detail-diff");
|
|
559
|
+
ensureAsideWidth();
|
|
560
|
+
state.detailDiffPathEl.textContent = f.path;
|
|
561
|
+
if (f.binary) {
|
|
562
|
+
state.detailDiffEl.replaceChildren(el("div", { class: "tm-diff-stub", text: t("tm.diff.binary", "二进制文件,跳过逐行对比。") }));
|
|
563
|
+
} else {
|
|
564
|
+
loadFileDiff(state, task.task_id, f.path);
|
|
535
565
|
}
|
|
536
566
|
}
|
|
537
567
|
|
|
@@ -552,99 +582,50 @@
|
|
|
552
582
|
renderTimeline(state);
|
|
553
583
|
}
|
|
554
584
|
|
|
555
|
-
// The drawer is global — only one can be open at a time across mounts, and
|
|
556
|
-
// it lives on document.body so it can escape the narrow aside column.
|
|
557
|
-
function buildDrawer() {
|
|
558
|
-
if (document.getElementById("tm-drawer-root")) {
|
|
559
|
-
return {
|
|
560
|
-
mask: document.querySelector(".tm-drawer-mask"),
|
|
561
|
-
drawer: document.getElementById("tm-drawer-root"),
|
|
562
|
-
title: document.querySelector(".tm-drawer-title"),
|
|
563
|
-
time: document.querySelector(".tm-drawer-time"),
|
|
564
|
-
restore: document.querySelector(".tm-restore-btn"),
|
|
565
|
-
close: document.querySelector(".tm-drawer-close"),
|
|
566
|
-
confirmRow: document.querySelector(".tm-confirm-row"),
|
|
567
|
-
confirmYes: document.querySelector(".tm-confirm-btn.go"),
|
|
568
|
-
confirmNo: document.querySelector(".tm-confirm-btn:not(.go)"),
|
|
569
|
-
files: document.querySelector(".tm-files"),
|
|
570
|
-
diff: document.querySelector(".tm-diff"),
|
|
571
|
-
diffPath: document.querySelector(".tm-diff-path"),
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
const mask = el("div", { class: "tm-drawer-mask" });
|
|
576
|
-
const titleEl = el("div", { class: "tm-drawer-title" });
|
|
577
|
-
const timeEl = el("div", { class: "tm-drawer-time" });
|
|
578
|
-
const restoreBtn = el("button", { class: "tm-restore-btn", type: "button", text: t("tm.restore.go", "回到这里") });
|
|
579
|
-
const closeBtn = el("button", { class: "tm-drawer-close", type: "button", text: t("tm.detail.close", "关闭") });
|
|
580
|
-
const head = el("div", { class: "tm-drawer-head" },
|
|
581
|
-
el("div", { class: "tm-drawer-title-wrap" }, titleEl, timeEl),
|
|
582
|
-
restoreBtn, closeBtn,
|
|
583
|
-
);
|
|
584
|
-
|
|
585
|
-
const confirmYes = el("button", { class: "tm-confirm-btn go", type: "button", text: t("tm.restore.confirm", "确认恢复") });
|
|
586
|
-
const confirmNo = el("button", { class: "tm-confirm-btn", type: "button", text: t("tm.restore.cancel", "取消") });
|
|
587
|
-
const confirmRow = el("div", { class: "tm-confirm-row" },
|
|
588
|
-
el("span", { class: "tm-confirm-msg", text: t("tm.restore.msg", "回到这一步会把文件恢复到当时的状态。") }),
|
|
589
|
-
confirmYes, confirmNo,
|
|
590
|
-
);
|
|
591
|
-
confirmRow.style.display = "none";
|
|
592
|
-
|
|
593
|
-
const filesEl = el("div", { class: "tm-files" });
|
|
594
|
-
const diffPathEl = el("div", { class: "tm-diff-path" });
|
|
595
|
-
const diffEl = el("div", { class: "tm-diff" });
|
|
596
|
-
const diffWrap = el("div", { class: "tm-diff-wrap" }, diffPathEl, diffEl);
|
|
597
|
-
const body = el("div", { class: "tm-drawer-body" }, filesEl, diffWrap);
|
|
598
|
-
|
|
599
|
-
const drawer = el("div", { id: "tm-drawer-root", class: "tm-drawer" }, head, confirmRow, body);
|
|
600
|
-
drawer.style.display = "none";
|
|
601
|
-
mask.style.display = "none";
|
|
602
|
-
|
|
603
|
-
document.body.appendChild(mask);
|
|
604
|
-
document.body.appendChild(drawer);
|
|
605
|
-
|
|
606
|
-
return { mask, drawer, title: titleEl, time: timeEl, restore: restoreBtn, close: closeBtn,
|
|
607
|
-
confirmRow, confirmYes, confirmNo, files: filesEl, diff: diffEl, diffPath: diffPathEl };
|
|
608
|
-
}
|
|
609
|
-
|
|
610
585
|
Clacky.ext.ui.mount("session.aside", {
|
|
611
586
|
create(ctx) {
|
|
612
587
|
const list = el("div", { class: "tm-list" });
|
|
613
588
|
const foot = el("div", { class: "tm-foot", text: t("tm.foot", "每完成一步会自动存档。点击想回到的版本即可恢复。") });
|
|
614
|
-
const root = el("div", { class: "tm-panel", "data-panel": "tm" }, list, foot);
|
|
615
589
|
|
|
616
|
-
const
|
|
590
|
+
const backBtn = el("button", { class: "tm-detail-back", type: "button", title: t("tm.detail.back", "返回") });
|
|
591
|
+
backBtn.innerHTML = ICON_BACK;
|
|
592
|
+
const detailTitleEl = el("div", { class: "tm-detail-title" });
|
|
593
|
+
const detailTimeEl = el("div", { class: "tm-detail-time" });
|
|
594
|
+
const detailRestoreEl = el("button", { class: "tm-detail-restore", type: "button", text: t("tm.restore.go", "回到这里") });
|
|
595
|
+
const detailHead = el("div", { class: "tm-detail-head" },
|
|
596
|
+
backBtn,
|
|
597
|
+
el("div", { class: "tm-detail-title-wrap" }, detailTitleEl, detailTimeEl),
|
|
598
|
+
detailRestoreEl,
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
const detailFilesEl = el("div", { class: "tm-detail-files" });
|
|
602
|
+
const diffCloseBtn = el("button", { class: "tm-detail-diff-close", type: "button", title: t("tm.diff.close", "关闭") });
|
|
603
|
+
diffCloseBtn.innerHTML = ICON_CLOSE;
|
|
604
|
+
const detailDiffPathEl = el("span", { class: "tm-detail-diff-path" });
|
|
605
|
+
const detailDiffEl = el("div", { class: "tm-diff" });
|
|
606
|
+
const detailDiff = el("div", { class: "tm-detail-diff" },
|
|
607
|
+
el("div", { class: "tm-detail-diff-head" }, detailDiffPathEl, diffCloseBtn),
|
|
608
|
+
detailDiffEl,
|
|
609
|
+
);
|
|
610
|
+
const detailBody = el("div", { class: "tm-detail-body" }, detailFilesEl, detailDiff);
|
|
611
|
+
const detail = el("div", { class: "tm-detail" }, detailHead, detailBody);
|
|
612
|
+
|
|
613
|
+
const root = el("div", { class: "tm-panel", "data-panel": "tm" }, list, detail, foot);
|
|
617
614
|
|
|
618
615
|
const state = {
|
|
619
616
|
sessionId: ctx.sessionId,
|
|
620
617
|
tasks: [],
|
|
621
|
-
selected: null,
|
|
622
618
|
expanded: null,
|
|
619
|
+
view: "timeline",
|
|
620
|
+
detailTask: null,
|
|
621
|
+
detailSelected: null,
|
|
623
622
|
panelEl: root, listEl: list, footEl: foot,
|
|
624
|
-
|
|
625
|
-
drawerTitleEl: d.title, drawerTimeEl: d.time,
|
|
626
|
-
restoreBtn: d.restore, filesEl: d.files, diffEl: d.diff, diffPathEl: d.diffPath, confirmRow: d.confirmRow,
|
|
627
|
-
};
|
|
628
|
-
|
|
629
|
-
const onClose = () => closeDrawer(state);
|
|
630
|
-
const onRestoreClick = () => {
|
|
631
|
-
d.confirmRow.style.display = "flex";
|
|
632
|
-
d.restore.disabled = true;
|
|
633
|
-
};
|
|
634
|
-
const onCancelClick = () => {
|
|
635
|
-
d.confirmRow.style.display = "none";
|
|
636
|
-
d.restore.disabled = false;
|
|
623
|
+
detailTitleEl, detailTimeEl, detailRestoreEl, detailFilesEl, detailDiffPathEl, detailDiffEl,
|
|
637
624
|
};
|
|
638
|
-
const onGoClick = () => performRestore(state);
|
|
639
|
-
const onMaskClick = () => closeDrawer(state);
|
|
640
|
-
const onKey = (e) => { if (e.key === "Escape" && d.drawer.classList.contains("open")) closeDrawer(state); };
|
|
641
625
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
d.confirmYes.onclick = onGoClick;
|
|
646
|
-
d.mask.onclick = onMaskClick;
|
|
647
|
-
document.addEventListener("keydown", onKey);
|
|
626
|
+
backBtn.addEventListener("click", () => closeDetail(state));
|
|
627
|
+
detailRestoreEl.addEventListener("click", () => onDetailRestoreClick(state));
|
|
628
|
+
diffCloseBtn.addEventListener("click", () => closeDetailDiff(state));
|
|
648
629
|
|
|
649
630
|
_activeState = state;
|
|
650
631
|
_hookWs();
|
|
@@ -655,9 +636,7 @@
|
|
|
655
636
|
state,
|
|
656
637
|
root,
|
|
657
638
|
dispose() {
|
|
658
|
-
|
|
659
|
-
try { d.mask.remove(); } catch (_) {}
|
|
660
|
-
try { d.drawer.remove(); } catch (_) {}
|
|
639
|
+
if (detailRestoreTimer) { clearTimeout(detailRestoreTimer); detailRestoreTimer = null; }
|
|
661
640
|
if (_activeState === state) _activeState = null;
|
|
662
641
|
},
|
|
663
642
|
};
|
|
@@ -666,8 +645,7 @@
|
|
|
666
645
|
container.appendChild(runtime.root);
|
|
667
646
|
_activeState = runtime.state;
|
|
668
647
|
},
|
|
669
|
-
}, {
|
|
670
|
-
order: 20,
|
|
648
|
+
}, { order: 40,
|
|
671
649
|
tab: { id: "tm", label: () => t("tm.tab") },
|
|
672
650
|
});
|
|
673
651
|
})();
|
|
@@ -300,10 +300,12 @@ module Clacky
|
|
|
300
300
|
) do |io| # rubocop:disable Security/Open
|
|
301
301
|
File.open(dest, "wb") { |out| out.write(io.read) }
|
|
302
302
|
end
|
|
303
|
-
rescue OpenURI::HTTPError, SocketError => e
|
|
304
|
-
raise Error, "failed to download #{url}: #{e.message}"
|
|
305
303
|
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
306
304
|
raise Error, "download timed out: #{e.message}"
|
|
305
|
+
# Catch-all so any transport-layer failure triggers download()'s
|
|
306
|
+
# secondary-host fallback instead of surfacing a raw error.
|
|
307
|
+
rescue StandardError => e
|
|
308
|
+
raise Error, "failed to download #{url}: #{e.message}"
|
|
307
309
|
end
|
|
308
310
|
|
|
309
311
|
private def extract_zip(zip_path, dest_root)
|
|
@@ -16,7 +16,7 @@ module Clacky
|
|
|
16
16
|
module ExtensionVerifier
|
|
17
17
|
Issue = Struct.new(:ext, :unit, :level, :code, :message, :file, :hint, keyword_init: true)
|
|
18
18
|
|
|
19
|
-
KNOWN_TOP_KEYS = %w[id name name_zh display_name display_name_zh title description description_zh emoji version origin author homepage license public license_required keywords contributes].freeze
|
|
19
|
+
KNOWN_TOP_KEYS = %w[id name name_zh display_name display_name_zh title description description_zh emoji version origin author homepage license public license_required keywords config contributes].freeze
|
|
20
20
|
KNOWN_CONTRIBUTES = %w[panels api skills agents channels patches hooks tools].freeze
|
|
21
21
|
|
|
22
22
|
PANEL_KEYS = %w[id title title_zh description description_zh view order attach entry_points].freeze
|
|
@@ -29,6 +29,7 @@ module Clacky
|
|
|
29
29
|
TOOL_KEYS = %w[id file].freeze
|
|
30
30
|
|
|
31
31
|
ATTACH_TOKEN_RE = /\A(\*|[\w\-]+)\z/.freeze
|
|
32
|
+
VERSION_RE = /\A[0-9]+\.[0-9]+\.[0-9]+\z/.freeze
|
|
32
33
|
|
|
33
34
|
class << self
|
|
34
35
|
# Run all checks against a fully-loaded ExtensionLoader::Result. Returns
|
|
@@ -43,6 +44,10 @@ module Clacky
|
|
|
43
44
|
issues
|
|
44
45
|
end
|
|
45
46
|
|
|
47
|
+
def valid_version?(version)
|
|
48
|
+
version.to_s.match?(VERSION_RE)
|
|
49
|
+
end
|
|
50
|
+
|
|
46
51
|
private def loader_errors_as_issues(result)
|
|
47
52
|
Array(result.errors).map do |err|
|
|
48
53
|
Issue.new(
|
|
@@ -78,6 +83,14 @@ module Clacky
|
|
|
78
83
|
)
|
|
79
84
|
end
|
|
80
85
|
|
|
86
|
+
if manifest.key?("version") && !valid_version?(manifest["version"])
|
|
87
|
+
issues << Issue.new(
|
|
88
|
+
ext: ext_id, unit: nil, level: :error, code: "schema.invalid_version",
|
|
89
|
+
message: "version must contain exactly three numeric segments (for example, 1.0.0)",
|
|
90
|
+
file: File.join(dir, "ext.yml"), hint: "Use the format x.x.x with digits only."
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
81
94
|
contributes = manifest["contributes"]
|
|
82
95
|
next unless contributes.is_a?(Hash)
|
|
83
96
|
|
|
@@ -215,7 +215,11 @@ module Clacky
|
|
|
215
215
|
#
|
|
216
216
|
# Zero side-effects: when reasoning_effort is nil/empty the body is
|
|
217
217
|
# unchanged, preserving the provider default for all models.
|
|
218
|
-
|
|
218
|
+
#
|
|
219
|
+
# Single source of truth for reasoning param mapping — shared with
|
|
220
|
+
# MessageFormat::OpenAIResponses (the Responses API accepts the same
|
|
221
|
+
# top-level fields).
|
|
222
|
+
def self.apply_reasoning_params(body, model, reasoning_effort)
|
|
219
223
|
effort_str = reasoning_effort.to_s
|
|
220
224
|
|
|
221
225
|
if model.to_s.match?(/\Aglm-5[-.]3/i)
|