cline-kit 0.1.0
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.
- package/CHANGELOG.md +113 -0
- package/LICENSE +21 -0
- package/NOTICE +60 -0
- package/README.md +188 -0
- package/README.zh-CN.md +168 -0
- package/SECURITY.md +53 -0
- package/ckit.cmd +12 -0
- package/dictionaries/ja.json +622 -0
- package/dictionaries/ko.json +622 -0
- package/dictionaries/vi.json +622 -0
- package/dictionaries/zh-CN.json +623 -0
- package/dictionaries/zh-TW.json +623 -0
- package/package.json +66 -0
- package/src/audit.js +127 -0
- package/src/cdp.js +85 -0
- package/src/cli.js +236 -0
- package/src/config.js +108 -0
- package/src/detect.js +118 -0
- package/src/dict.js +171 -0
- package/src/doctor.js +121 -0
- package/src/engine.js +148 -0
- package/src/features/index.js +83 -0
- package/src/features/sidebar-groups.js +376 -0
- package/src/features/sidebar-groups.logic.js +145 -0
- package/src/injector.js +96 -0
- package/src/install-win.js +155 -0
- package/src/launcher.js +123 -0
- package/src/payload.js +80 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
// feature: sidebar-groups
|
|
2
|
+
// Shows every registered Cline workspace in the sidebar's project grouping, not just the ones that
|
|
3
|
+
// already have sessions. Native Cline hides empty projects; this appends them with the same styling
|
|
4
|
+
// and switches project by driving Cline's own workspace picker (chip -> search -> result row).
|
|
5
|
+
//
|
|
6
|
+
// Runs inside the webview. Config arrives as window.__clineKitFeature["sidebar-groups"].
|
|
7
|
+
(function () {
|
|
8
|
+
var ID = "sidebar-groups";
|
|
9
|
+
var VER = 6; // human-readable; hot-swap keys off CFG.__build instead
|
|
10
|
+
var CFG = (window.__clineKitFeature && window.__clineKitFeature[ID]) || {};
|
|
11
|
+
var st = window.__clineKitFeatureState = window.__clineKitFeatureState || {};
|
|
12
|
+
var BUILD = String(CFG.__build || "v" + VER);
|
|
13
|
+
if (st[ID + "_build"] === BUILD) return;
|
|
14
|
+
if (st[ID + "_observer"]) { try { st[ID + "_observer"].disconnect(); } catch (e) { } }
|
|
15
|
+
if (st[ID + "_timer"]) clearInterval(st[ID + "_timer"]);
|
|
16
|
+
st[ID + "_build"] = BUILD;
|
|
17
|
+
st[ID] = VER;
|
|
18
|
+
|
|
19
|
+
// Pure path/filtering helpers live in a sibling file that is prepended to this script by
|
|
20
|
+
// src/features/index.js and unit tested in scripts/selftest.js.
|
|
21
|
+
var L = window.__ckitSidebarLogic;
|
|
22
|
+
if (!L) {
|
|
23
|
+
try { console.warn("[cline-kit:" + ID + "] logic module missing, feature disabled"); } catch (e) { }
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
var INSTALL = (CFG.installDir || "");
|
|
27
|
+
var HIDE = (CFG.hide || []);
|
|
28
|
+
var MAX_ROWS = CFG.maxRows || 80;
|
|
29
|
+
var TEXT = CFG.text || {};
|
|
30
|
+
// Fallbacks are English on purpose: English is the app source language, so a feature stays
|
|
31
|
+
// readable when a locale has no featureText block yet. Translations arrive via CFG.text,
|
|
32
|
+
// which the registry fills from dictionaries/<locale>.json -> featureText[<id>].
|
|
33
|
+
|
|
34
|
+
var CHEVRON = "lucide lucide-chevron-down size-3.5 shrink-0 transition-transform";
|
|
35
|
+
var HEAD_CLS = "flex h-8 w-full min-w-0 items-center gap-1.5 rounded-md px-1 text-left text-sm font-medium text-sidebar-foreground hover:bg-surface-hover-lighter focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring";
|
|
36
|
+
var ACT_CLS = "flex h-7 w-full min-w-0 items-center gap-1.5 rounded-md px-1 text-left text-xs text-muted-foreground hover:bg-surface-hover-lighter hover:text-sidebar-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring";
|
|
37
|
+
|
|
38
|
+
function t(key, fallback) { return (TEXT && TEXT[key]) || fallback; }
|
|
39
|
+
|
|
40
|
+
var norm = L.norm;
|
|
41
|
+
var base = L.base;
|
|
42
|
+
var strip = L.strip;
|
|
43
|
+
var KEY = CFG.storageKey || L.registryKey(storageKeys()) || "cline.code.workspace-selection.v2";
|
|
44
|
+
|
|
45
|
+
function storageKeys() {
|
|
46
|
+
var out = [];
|
|
47
|
+
for (var i = 0; i < localStorage.length; i++) {
|
|
48
|
+
var k = localStorage.key(i);
|
|
49
|
+
if (k) out.push(k);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function registry() {
|
|
55
|
+
var parsed = L.parseRegistry(localStorage.getItem(KEY));
|
|
56
|
+
parsed.key = KEY;
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function listRoot() {
|
|
61
|
+
var vp = document.querySelector("[data-radix-scroll-area-viewport]");
|
|
62
|
+
if (!vp) return null;
|
|
63
|
+
var box = vp.querySelector('div[class*="flex-col"]');
|
|
64
|
+
return box && box.querySelector('button[aria-expanded]') ? box : (box || null);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Labels of the groups Cline already renders; used so we never add a row for a project that is
|
|
68
|
+
// already on screen.
|
|
69
|
+
function nativeLabels(box) {
|
|
70
|
+
var out = [];
|
|
71
|
+
if (!box) return out;
|
|
72
|
+
var bs = box.querySelectorAll('button[aria-expanded]');
|
|
73
|
+
for (var i = 0; i < bs.length; i++) {
|
|
74
|
+
var b = bs[i];
|
|
75
|
+
if (!/h-8 w-full/.test(b.className)) continue;
|
|
76
|
+
if (b.closest("[data-ckit-feat]")) continue; // our own rows must not hide our own rows
|
|
77
|
+
var sp = b.querySelector("span");
|
|
78
|
+
var name = norm(sp ? sp.textContent : b.textContent);
|
|
79
|
+
if (name && out.indexOf(name) < 0) out.push(name);
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function waitFor(fn, ms) {
|
|
85
|
+
return new Promise(function (resolve) {
|
|
86
|
+
var t0 = Date.now();
|
|
87
|
+
(function loop() {
|
|
88
|
+
var v = null;
|
|
89
|
+
try { v = fn(); } catch (e) { }
|
|
90
|
+
if (v) return resolve(v);
|
|
91
|
+
if (Date.now() - t0 > (ms || 2500)) return resolve(null);
|
|
92
|
+
setTimeout(loop, 80);
|
|
93
|
+
})();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function setNativeValue(inp, val) {
|
|
98
|
+
var d = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value");
|
|
99
|
+
if (d && d.set) d.set.call(inp, val); else inp.value = val;
|
|
100
|
+
inp.dispatchEvent(new Event("input", { bubbles: true }));
|
|
101
|
+
inp.dispatchEvent(new Event("change", { bubbles: true }));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// The workspace chip lives in the title bar, outside the sidebar list. Match structurally rather
|
|
105
|
+
// than by pixel offset: the sidebar can be resized, docked or narrowed to icons.
|
|
106
|
+
function inSidebar(el) {
|
|
107
|
+
return !!(el.closest && (el.closest('[class*="bg-sidebar"]') || el.closest("nav")));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function findChip(labels) {
|
|
111
|
+
var wanted = {};
|
|
112
|
+
(labels || []).forEach(function (l) { wanted[l] = 1; });
|
|
113
|
+
var btns = [].slice.call(document.querySelectorAll("button")).filter(function (e) {
|
|
114
|
+
var r = e.getBoundingClientRect();
|
|
115
|
+
return r.width > 0 && !inSidebar(e) && wanted[norm(e.textContent)] && !!e.querySelector("svg");
|
|
116
|
+
});
|
|
117
|
+
return btns[0] || null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function findRow(path) {
|
|
121
|
+
var want = norm(path);
|
|
122
|
+
var all = [].slice.call(document.querySelectorAll("button")).filter(function (e) {
|
|
123
|
+
return norm(e.textContent) === want;
|
|
124
|
+
});
|
|
125
|
+
if (!all.length) return null;
|
|
126
|
+
var inList = all.filter(function (e) {
|
|
127
|
+
return /max-h-\d+/.test((e.parentElement || {}).className || "");
|
|
128
|
+
});
|
|
129
|
+
return inList[inList.length - 1] || all[all.length - 1];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
var switching = false;
|
|
133
|
+
function switchWorkspace(path) {
|
|
134
|
+
if (switching) return Promise.resolve();
|
|
135
|
+
switching = true;
|
|
136
|
+
var prev = document.activeElement;
|
|
137
|
+
var chip = findChip(L.labelize(registry().workspaces).map(function (e) { return e.label; }));
|
|
138
|
+
var done = function (msg) {
|
|
139
|
+
switching = false;
|
|
140
|
+
setTimeout(render, 250);
|
|
141
|
+
setTimeout(render, 1200);
|
|
142
|
+
if (prev && prev.focus) { try { prev.focus(); } catch (e) { } }
|
|
143
|
+
if (msg) flash(msg);
|
|
144
|
+
return Promise.resolve();
|
|
145
|
+
};
|
|
146
|
+
if (!chip) return done(t("errNoChip", "Could not find the workspace button"));
|
|
147
|
+
chip.click();
|
|
148
|
+
return waitFor(function () {
|
|
149
|
+
var ins = document.querySelectorAll("input");
|
|
150
|
+
for (var i = 0; i < ins.length; i++) {
|
|
151
|
+
if (/搜索工作区|search workspace|enter a folder path/i.test(ins[i].getAttribute("placeholder") || "")) return ins[i];
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}, 2000).then(function (inp) {
|
|
155
|
+
if (!inp) { chip.click(); return done(t("errNoSearch", "Could not find the workspace search box")); }
|
|
156
|
+
setNativeValue(inp, path);
|
|
157
|
+
return waitFor(function () { return findRow(path); }, 2200).then(function (row) {
|
|
158
|
+
if (!row) {
|
|
159
|
+
["keydown", "keyup"].forEach(function (type) {
|
|
160
|
+
inp.dispatchEvent(new KeyboardEvent(type, { key: "Escape", code: "Escape", bubbles: true }));
|
|
161
|
+
});
|
|
162
|
+
return done(t("errNotListed", "That project is not in the list"));
|
|
163
|
+
}
|
|
164
|
+
row.click();
|
|
165
|
+
return done(null);
|
|
166
|
+
});
|
|
167
|
+
}).catch(function () { return done(t("errSwitch", "Switch failed")); });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
var flashMsg = "";
|
|
171
|
+
function flash(msg) {
|
|
172
|
+
flashMsg = msg;
|
|
173
|
+
render();
|
|
174
|
+
setTimeout(function () { flashMsg = ""; render(); }, 2600);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
var SVG_CHEVRON = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"></path></svg>';
|
|
178
|
+
var SVG_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"></path></svg>';
|
|
179
|
+
|
|
180
|
+
// static markup only; every dynamic value goes through textContent
|
|
181
|
+
function svgInto(el, markup, cls) {
|
|
182
|
+
el.innerHTML = markup;
|
|
183
|
+
var node = el.firstElementChild;
|
|
184
|
+
if (node && cls) node.setAttribute("class", cls);
|
|
185
|
+
return node;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function span(cls, text) {
|
|
189
|
+
var s = document.createElement("span");
|
|
190
|
+
if (cls) s.className = cls;
|
|
191
|
+
s.textContent = text;
|
|
192
|
+
return s;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function buildRow(ws, label, isCurrent) {
|
|
196
|
+
var wrap = document.createElement("div");
|
|
197
|
+
wrap.className = "mb-1 min-w-0";
|
|
198
|
+
wrap.setAttribute("data-ckit-feat", ID);
|
|
199
|
+
wrap.dataset.wsPath = ws;
|
|
200
|
+
|
|
201
|
+
var head = document.createElement("button");
|
|
202
|
+
head.type = "button";
|
|
203
|
+
head.className = HEAD_CLS;
|
|
204
|
+
head.setAttribute("aria-expanded", "false");
|
|
205
|
+
head.title = ws; // full path: the label may be shortened
|
|
206
|
+
svgInto(head, SVG_CHEVRON, CHEVRON + " -rotate-90");
|
|
207
|
+
head.appendChild(span("block min-w-0 truncate", label));
|
|
208
|
+
if (isCurrent) head.appendChild(span("ml-auto shrink-0 text-[11px] text-muted-foreground", t("current", "current")));
|
|
209
|
+
wrap.appendChild(head);
|
|
210
|
+
|
|
211
|
+
var body = document.createElement("div");
|
|
212
|
+
body.className = "hidden pl-4";
|
|
213
|
+
// A bare label means this path is definitely not on screen already (Cline headers show bare
|
|
214
|
+
// folder names), so "no sessions" is a true statement. A qualified label was disambiguated
|
|
215
|
+
// against a same-named sibling, and one of them may be the native group above - claim nothing.
|
|
216
|
+
if (label === base(ws)) {
|
|
217
|
+
body.appendChild(span("px-1 py-1 text-xs text-muted-foreground", t("noSessions", "No sessions yet")));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
var act = document.createElement("button");
|
|
221
|
+
act.type = "button";
|
|
222
|
+
act.className = ACT_CLS;
|
|
223
|
+
svgInto(act, SVG_FOLDER, "size-3.5 shrink-0");
|
|
224
|
+
act.appendChild(span(null, t("switchAndNew", "Open this project and start a session")));
|
|
225
|
+
act.addEventListener("click", function (ev) { ev.stopPropagation(); switchWorkspace(ws); });
|
|
226
|
+
body.appendChild(act);
|
|
227
|
+
|
|
228
|
+
if (flashMsg) {
|
|
229
|
+
var err = document.createElement("div");
|
|
230
|
+
err.className = "px-1 py-1 text-xs text-destructive";
|
|
231
|
+
err.textContent = flashMsg;
|
|
232
|
+
body.appendChild(err);
|
|
233
|
+
}
|
|
234
|
+
wrap.appendChild(body);
|
|
235
|
+
|
|
236
|
+
head.addEventListener("click", function () {
|
|
237
|
+
var open = head.getAttribute("aria-expanded") === "true";
|
|
238
|
+
head.setAttribute("aria-expanded", open ? "false" : "true");
|
|
239
|
+
body.classList.toggle("hidden", open);
|
|
240
|
+
head.firstElementChild.classList.toggle("-rotate-90", open);
|
|
241
|
+
});
|
|
242
|
+
return wrap;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function projectMode() {
|
|
246
|
+
var btns = document.querySelectorAll("button");
|
|
247
|
+
var headerLabels = {};
|
|
248
|
+
headerLabels[t("projects", "Projects")] = true;
|
|
249
|
+
headerLabels["Projects"] = true;
|
|
250
|
+
var sawSortControl = false;
|
|
251
|
+
for (var i = 0; i < btns.length; i++) {
|
|
252
|
+
var a = btns[i].getAttribute("aria-label") || "";
|
|
253
|
+
// Prefer the sort control's own label: it states the current mode and survives restyling.
|
|
254
|
+
if (/会话排序|Sort sessions/i.test(a)) {
|
|
255
|
+
sawSortControl = true;
|
|
256
|
+
if (/项目|Project/i.test(a)) return true;
|
|
257
|
+
if (/时间|Time/i.test(a)) return false;
|
|
258
|
+
}
|
|
259
|
+
if (headerLabels[norm(btns[i].textContent)]) return true;
|
|
260
|
+
}
|
|
261
|
+
if (!sawSortControl) {
|
|
262
|
+
// no sort control found at all (different Cline build): fall back to the visible header only
|
|
263
|
+
for (var j = 0; j < btns.length; j++) {
|
|
264
|
+
if (headerLabels[norm(btns[j].textContent)]) return true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Native grouping mode is not persisted by Cline, so keep it on. We stop interfering the moment
|
|
271
|
+
// the user touches the sort control themselves - their choice wins for the rest of the session,
|
|
272
|
+
// but a mode flip caused by anything else (React state churn, another overlay, a workspace switch
|
|
273
|
+
// that rebuilds the window) is corrected on the next tick instead of leaving the feature off.
|
|
274
|
+
var userTouchedSort = false;
|
|
275
|
+
function sortButton() {
|
|
276
|
+
var btns = document.querySelectorAll("button");
|
|
277
|
+
for (var i = 0; i < btns.length; i++) {
|
|
278
|
+
var a = btns[i].getAttribute("aria-label") || "";
|
|
279
|
+
if (/会话排序|Sort sessions/i.test(a)) return btns[i];
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
document.addEventListener("click", function (ev) {
|
|
284
|
+
var t = ev.target;
|
|
285
|
+
if (!t || !t.closest) return;
|
|
286
|
+
var b = t.closest("button");
|
|
287
|
+
if (b && /会话排序|Sort sessions/i.test(b.getAttribute("aria-label") || "")) userTouchedSort = true;
|
|
288
|
+
}, true);
|
|
289
|
+
|
|
290
|
+
var lastAutoClick = 0;
|
|
291
|
+
function ensureProjectMode() {
|
|
292
|
+
if (userTouchedSort || CFG.groupMode === false) return;
|
|
293
|
+
if (projectMode()) return;
|
|
294
|
+
// cooldown so a detection mismatch cannot turn into a click loop every tick
|
|
295
|
+
if (Date.now() - lastAutoClick < 6000) return;
|
|
296
|
+
var btn = sortButton();
|
|
297
|
+
if (btn) { lastAutoClick = Date.now(); btn.click(); setTimeout(render, 300); }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
var sig = "";
|
|
301
|
+
function render() {
|
|
302
|
+
ensureProjectMode();
|
|
303
|
+
var box = listRoot();
|
|
304
|
+
if (!box || !projectMode()) { clearRows(); sig = ""; return; }
|
|
305
|
+
var reg = registry();
|
|
306
|
+
var currentPath = strip(reg.last).toLowerCase();
|
|
307
|
+
var missing = L.plan(reg.workspaces, nativeLabels(box), {
|
|
308
|
+
maxRows: MAX_ROWS, installDir: INSTALL, hide: HIDE
|
|
309
|
+
});
|
|
310
|
+
var s = missing.map(function (e) { return e.path + "=" + e.label; }).join("|") + "::" + currentPath + "::" + flashMsg;
|
|
311
|
+
// Self-heal: compare what we intended against what is actually in the DOM. If anything rewrote
|
|
312
|
+
// or dropped our labels (React reconciliation, another overlay, a partial render), rebuild.
|
|
313
|
+
var dom = [];
|
|
314
|
+
var ours = box.querySelectorAll(":scope > [data-ckit-feat]");
|
|
315
|
+
for (var k = 0; k < ours.length; k++) {
|
|
316
|
+
var sp = ours[k].querySelector("span");
|
|
317
|
+
dom.push((ours[k].dataset.wsPath || "") + "=" + (sp ? sp.textContent : ""));
|
|
318
|
+
}
|
|
319
|
+
var want = missing.map(function (e) { return e.path + "=" + e.label; }).join("|");
|
|
320
|
+
if (s === sig && dom.join("|") === want) { report(box, ours.length, missing.length, reg); return; }
|
|
321
|
+
sig = s;
|
|
322
|
+
clearRows();
|
|
323
|
+
if (missing.length) {
|
|
324
|
+
var frag = document.createDocumentFragment();
|
|
325
|
+
for (var j = 0; j < missing.length; j++) {
|
|
326
|
+
frag.appendChild(buildRow(missing[j].path, missing[j].label, strip(missing[j].path).toLowerCase() === currentPath));
|
|
327
|
+
}
|
|
328
|
+
box.appendChild(frag);
|
|
329
|
+
}
|
|
330
|
+
report(box, box.querySelectorAll(":scope > [data-ckit-feat]").length, missing.length, reg);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Counters for `ckit doctor`: what the feature believes right now, readable from outside the
|
|
334
|
+
// closure through window.__clineKitFeatureState.
|
|
335
|
+
function report(box, rows, intended, reg) {
|
|
336
|
+
var native = 0;
|
|
337
|
+
var all = box.querySelectorAll('button[aria-expanded]');
|
|
338
|
+
for (var i = 0; i < all.length; i++) {
|
|
339
|
+
if (/h-8 w-full/.test(all[i].className) && !all[i].closest("[data-ckit-feat]")) native++;
|
|
340
|
+
}
|
|
341
|
+
st[ID + "_stats"] = {
|
|
342
|
+
version: VER, build: BUILD, registered: reg.workspaces.length,
|
|
343
|
+
nativeGroups: native, intended: intended, rows: rows, mode: "project"
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function clearRows() {
|
|
348
|
+
var box = listRoot();
|
|
349
|
+
if (!box) return;
|
|
350
|
+
var olds = box.querySelectorAll(":scope > [data-ckit-feat]");
|
|
351
|
+
for (var i = 0; i < olds.length; i++) olds[i].remove();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
var queued = false;
|
|
355
|
+
function schedule() {
|
|
356
|
+
if (queued) return;
|
|
357
|
+
queued = true;
|
|
358
|
+
(window.requestAnimationFrame || setTimeout)(function () { queued = false; render(); }, 16);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!document.body) return;
|
|
362
|
+
render();
|
|
363
|
+
st[ID + "_observer"] = new MutationObserver(function (muts) {
|
|
364
|
+
for (var i = 0; i < muts.length; i++) {
|
|
365
|
+
var n = muts[i].target;
|
|
366
|
+
if (n && n.nodeType === 3) n = n.parentElement;
|
|
367
|
+
if (n && n.closest && n.closest("[data-ckit-feat]")) continue;
|
|
368
|
+
}
|
|
369
|
+
schedule();
|
|
370
|
+
});
|
|
371
|
+
st[ID + "_observer"].observe(document.documentElement, {
|
|
372
|
+
childList: true, subtree: true, attributes: true, attributeFilter: ["class", "aria-expanded"]
|
|
373
|
+
});
|
|
374
|
+
st[ID + "_timer"] = setInterval(render, 1500);
|
|
375
|
+
try { console.log("[cline-kit:" + ID + "] loaded v" + VER); } catch (e) { }
|
|
376
|
+
})();
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Pure helpers for the sidebar-groups feature: no DOM, no browser globals.
|
|
2
|
+
// Loaded twice from the same file - CommonJS for scripts/selftest.js, and prepended to the
|
|
3
|
+
// browser script by src/features/index.js, where it lands on window.__ckitSidebarLogic.
|
|
4
|
+
(function (root, factory) {
|
|
5
|
+
if (typeof module === "object" && module.exports) module.exports = factory();
|
|
6
|
+
else if (root) root.__ckitSidebarLogic = factory();
|
|
7
|
+
})(typeof window !== "undefined" ? window : this, function () {
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
var SEP = /[\\/]/;
|
|
11
|
+
|
|
12
|
+
// Collapse every run of whitespace (space, tab, newline) to one space. React renders the same
|
|
13
|
+
// label with newlines between children, so without this nothing ever matches.
|
|
14
|
+
function norm(s) { return (s == null ? "" : String(s)).replace(/\s+/g, " ").trim(); }
|
|
15
|
+
|
|
16
|
+
function strip(p) { return String(p == null ? "" : p).replace(/[\\/]+$/, ""); }
|
|
17
|
+
|
|
18
|
+
function lastSep(s) {
|
|
19
|
+
var i = -1;
|
|
20
|
+
for (var k = 0; k < s.length; k++) if (SEP.test(s[k])) i = k;
|
|
21
|
+
return i;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function base(p) {
|
|
25
|
+
var s = strip(p);
|
|
26
|
+
var i = lastSep(s);
|
|
27
|
+
return i >= 0 ? s.slice(i + 1) : s;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function dirname(p) {
|
|
31
|
+
var s = strip(p);
|
|
32
|
+
var i = lastSep(s);
|
|
33
|
+
return i >= 0 ? s.slice(0, i) : "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// The folder holding this project, used to tell two same-named projects apart.
|
|
37
|
+
function parent(p) { return base(dirname(p)); }
|
|
38
|
+
|
|
39
|
+
function samePath(a, b) { return strip(a).toLowerCase() === strip(b).toLowerCase(); }
|
|
40
|
+
|
|
41
|
+
// "p is at or under root". Case-insensitive for drive-letter paths, and separator-aware so
|
|
42
|
+
// "D:\Cl" is not treated as a parent of "D:\Cline".
|
|
43
|
+
function isUnder(p, rootPath) {
|
|
44
|
+
var a = strip(p), b = strip(rootPath);
|
|
45
|
+
if (!a || !b) return false;
|
|
46
|
+
if (/^[A-Za-z]:[\\/]/.test(a) || /^[A-Za-z]:[\\/]/.test(b)) { a = a.toLowerCase(); b = b.toLowerCase(); }
|
|
47
|
+
if (a === b) return true;
|
|
48
|
+
return a.indexOf(b + "\\") === 0 || a.indexOf(b + "/") === 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Cline stores its workspace registry under a versioned key; follow the highest one present so a
|
|
52
|
+
// v3 bump does not silently read an empty list.
|
|
53
|
+
var REGISTRY_KEY_RE = /^cline\.code\.workspace-selection\.v(\d+)$/;
|
|
54
|
+
function registryKey(keys) {
|
|
55
|
+
var best = null, bestVer = -1;
|
|
56
|
+
for (var i = 0; i < (keys || []).length; i++) {
|
|
57
|
+
var m = REGISTRY_KEY_RE.exec(keys[i]);
|
|
58
|
+
if (m) { var v = Number(m[1]); if (v > bestVer) { bestVer = v; best = keys[i]; } }
|
|
59
|
+
}
|
|
60
|
+
return best;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseRegistry(raw) {
|
|
64
|
+
try {
|
|
65
|
+
var obj = JSON.parse(raw || "{}");
|
|
66
|
+
var env = ((obj.environments || {}).local) || {};
|
|
67
|
+
var ws = Array.isArray(env.workspaces)
|
|
68
|
+
? env.workspaces.filter(function (x) { return typeof x === "string" && x.trim(); })
|
|
69
|
+
: [];
|
|
70
|
+
return { workspaces: ws, last: typeof env.lastWorkspace === "string" ? env.lastWorkspace : "" };
|
|
71
|
+
} catch (e) { return { workspaces: [], last: "" }; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// A registered path is a container rather than a project when another registered path sits
|
|
75
|
+
// inside it, when it is the app's own install directory, or when the user hid it.
|
|
76
|
+
function isContainer(p, all, opts) {
|
|
77
|
+
opts = opts || {};
|
|
78
|
+
var clean = strip(p);
|
|
79
|
+
for (var i = 0; i < (all || []).length; i++) {
|
|
80
|
+
if (!samePath(all[i], clean) && isUnder(all[i], clean)) return true;
|
|
81
|
+
}
|
|
82
|
+
var install = strip(opts.installDir || "");
|
|
83
|
+
if (install && isUnder(clean, install)) return true;
|
|
84
|
+
var hide = opts.hide || [];
|
|
85
|
+
for (var k = 0; k < hide.length; k++) {
|
|
86
|
+
if (hide[k] && isUnder(clean, hide[k])) return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Unique names show as-is; a collision adds the parent folder, "LLM (workspace)"; and if that
|
|
92
|
+
// still collides, a counter. Labels are unique by construction, so the sidebar never shows two
|
|
93
|
+
// indistinguishable rows - the tooltip carries the full path either way.
|
|
94
|
+
function labelize(paths) {
|
|
95
|
+
var counts = {};
|
|
96
|
+
(paths || []).forEach(function (p) { var b = base(p) || p; counts[b] = (counts[b] || 0) + 1; });
|
|
97
|
+
var used = {};
|
|
98
|
+
return (paths || []).map(function (p) {
|
|
99
|
+
var b = base(p) || p;
|
|
100
|
+
var label = counts[b] > 1 ? b + " (" + (parent(p) || "/") + ")" : b;
|
|
101
|
+
var n = 2;
|
|
102
|
+
while (used[label]) label = b + " (" + n++ + ")";
|
|
103
|
+
used[label] = true;
|
|
104
|
+
return { path: p, label: label };
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Which labelled projects still need a row: skip ones the native UI already shows (matched on the
|
|
109
|
+
// displayed label, so a same-named-but-different project still appears), containers, and repeats.
|
|
110
|
+
function pickMissing(entries, nativeLabels, opts) {
|
|
111
|
+
opts = opts || {};
|
|
112
|
+
var max = typeof opts.maxRows === "number" ? opts.maxRows : 80;
|
|
113
|
+
var have = {};
|
|
114
|
+
(nativeLabels || []).forEach(function (x) { have[x] = true; });
|
|
115
|
+
var out = [], seenLabel = {}, seenPath = {};
|
|
116
|
+
var list = entries || [];
|
|
117
|
+
for (var i = 0; i < list.length && out.length < max; i++) {
|
|
118
|
+
var e = list[i];
|
|
119
|
+
if (!e || !e.path || !e.label) continue;
|
|
120
|
+
if (have[e.label] || seenLabel[e.label] || seenPath[e.path]) continue;
|
|
121
|
+
if (isContainer(e.path, list.map(function (x) { return x.path; }), opts)) continue;
|
|
122
|
+
seenLabel[e.label] = true;
|
|
123
|
+
seenPath[e.path] = true;
|
|
124
|
+
out.push(e);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Convenience for callers that start from raw paths: containers are removed before labelling,
|
|
130
|
+
// so a container never steals a nice label from a real project.
|
|
131
|
+
function plan(workspaces, nativeLabels, opts) {
|
|
132
|
+
opts = opts || {};
|
|
133
|
+
var kept = (workspaces || []).filter(function (p) {
|
|
134
|
+
return norm(p) && !isContainer(p, workspaces, opts);
|
|
135
|
+
});
|
|
136
|
+
var seen = {};
|
|
137
|
+
kept = kept.filter(function (p) { if (seen[p]) return false; seen[p] = true; return true; });
|
|
138
|
+
return pickMissing(labelize(kept), nativeLabels, opts);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
norm, strip, base, parent, dirname, samePath, isUnder,
|
|
143
|
+
registryKey, parseRegistry, isContainer, labelize, pickMissing, plan
|
|
144
|
+
};
|
|
145
|
+
});
|
package/src/injector.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Resident keep-alive: keeps the overlay installed in every Cline webview page.
|
|
3
|
+
// Re-reads the dictionary each cycle, so `ckit update` takes effect without a restart.
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { execFileSync } = require("child_process");
|
|
7
|
+
const cfg = require("./config");
|
|
8
|
+
const cdp = require("./cdp");
|
|
9
|
+
const payload = require("./payload");
|
|
10
|
+
|
|
11
|
+
const INTERVAL_MS = 4000;
|
|
12
|
+
const registered = new Map(); // targetId -> { scriptId, dictVersion }
|
|
13
|
+
|
|
14
|
+
// Surface injection failures instead of swallowing them; a silent no-op is the worst outcome.
|
|
15
|
+
function log(msg) {
|
|
16
|
+
try {
|
|
17
|
+
cfg.ensureDirs();
|
|
18
|
+
fs.appendFileSync(path.join(cfg.logDir(), "injector.log"),
|
|
19
|
+
new Date().toISOString() + " " + msg + "\n");
|
|
20
|
+
} catch (e) { /* never fail the loop over logging */ }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function clineRunning() {
|
|
24
|
+
if (process.platform !== "win32") return true;
|
|
25
|
+
try {
|
|
26
|
+
const out = execFileSync("tasklist.exe", ["/FI", "IMAGENAME eq cline-app.exe", "/NH"], { encoding: "utf8" });
|
|
27
|
+
return /cline-app\.exe/i.test(out);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return true; // never exit on a transient query failure while a port still answers
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function installOnce(port, source, version) {
|
|
34
|
+
const results = await cdp.eachPage(port, async (api, target) => {
|
|
35
|
+
await api.rpc("Runtime.enable");
|
|
36
|
+
await api.rpc("Page.enable");
|
|
37
|
+
const prev = registered.get(target.id);
|
|
38
|
+
if (!prev || prev.dictVersion !== version) {
|
|
39
|
+
if (prev) {
|
|
40
|
+
try { await api.rpc("Page.removeScriptToEvaluateOnNewDocument", { identifier: prev.scriptId }); } catch (e) { }
|
|
41
|
+
}
|
|
42
|
+
const r = await api.rpc("Page.addScriptToEvaluateOnNewDocument", { source });
|
|
43
|
+
registered.set(target.id, { scriptId: r.identifier, dictVersion: version });
|
|
44
|
+
}
|
|
45
|
+
const r = await api.rpc("Runtime.evaluate", { expression: source });
|
|
46
|
+
if (r && r.exceptionDetails) {
|
|
47
|
+
const ex = r.exceptionDetails.exception || {};
|
|
48
|
+
log("evaluate error on " + target.id + ": " + (ex.description || ex.value || r.exceptionDetails.text));
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
});
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The injector is a long-lived process, so editing src/*.js does not affect a running one.
|
|
56
|
+
// It records the payload version it actually installed; ckit start compares that with the
|
|
57
|
+
// current version and restarts a stale injector (otherwise `git pull` appears to do nothing).
|
|
58
|
+
function activeVersionFile() { return path.join(cfg.cacheDir(), "active-version"); }
|
|
59
|
+
|
|
60
|
+
function readActiveVersion() {
|
|
61
|
+
try { return fs.readFileSync(activeVersionFile(), "utf8").trim(); } catch (e) { return ""; }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeActiveVersion(v) {
|
|
65
|
+
if (readActiveVersion() === v) return;
|
|
66
|
+
try { cfg.ensureDirs(); fs.writeFileSync(activeVersionFile(), v); } catch (e) { /* best effort */ }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function main() {
|
|
70
|
+
const boot = cfg.read();
|
|
71
|
+
const port = boot.port;
|
|
72
|
+
if (!port) {
|
|
73
|
+
console.error("[cline-kit] injector: no port in config; start via `ckit start`");
|
|
74
|
+
process.exit(2);
|
|
75
|
+
}
|
|
76
|
+
let gone = 0;
|
|
77
|
+
for (;;) {
|
|
78
|
+
try {
|
|
79
|
+
// re-read every cycle so feature toggles, dictionary updates and path changes apply live
|
|
80
|
+
const conf = cfg.read();
|
|
81
|
+
const composed = payload.compose(conf);
|
|
82
|
+
await installOnce(conf.port || port, composed.source, composed.version);
|
|
83
|
+
writeActiveVersion(composed.version);
|
|
84
|
+
gone = 0;
|
|
85
|
+
} catch (e) {
|
|
86
|
+
if (!clineRunning() && ++gone > 3) {
|
|
87
|
+
console.log("[cline-kit] cline is gone; injector exiting");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
await new Promise((r) => setTimeout(r, INTERVAL_MS));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (require.main === module) main().catch((e) => { console.error(e); process.exit(1); });
|
|
96
|
+
module.exports = { installOnce, readActiveVersion, activeVersionFile };
|