@liyang8246/dsh-codetime 0.0.0-stage → 0.1.8
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/cordis.patch.yml +3 -0
- package/icon.svg +6 -0
- package/lib/client.js +245 -0
- package/lib/index.js +1667 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +40 -4
- package/README.md +0 -3
package/cordis.patch.yml
ADDED
package/icon.svg
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="CodeTime">
|
|
2
|
+
<rect width="64" height="64" rx="14" fill="#101B2C"/>
|
|
3
|
+
<circle cx="32" cy="32" r="17" fill="none" stroke="#5B8DF7" stroke-width="3.2"/>
|
|
4
|
+
<path d="M32 21.5V32l7.4 4.4" fill="none" stroke="#7ED6C4" stroke-width="3.4" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
|
+
<circle cx="32" cy="32" r="2.7" fill="#7ED6C4"/>
|
|
6
|
+
</svg>
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
// src/client/api.ts
|
|
3
|
+
/** Narrow an unknown response body to the settings payload. */
|
|
4
|
+
function isStatus(value) {
|
|
5
|
+
if (typeof value !== "object" || value === null) return false;
|
|
6
|
+
const candidate = value;
|
|
7
|
+
return typeof candidate.configured === "boolean" && typeof candidate.autoUpload === "boolean" && typeof candidate.pending === "number" && typeof candidate.sessions === "number";
|
|
8
|
+
}
|
|
9
|
+
/** Read or mutate the plugin's settings state through its host route. */
|
|
10
|
+
async function call(request, signal) {
|
|
11
|
+
const response = await fetch(new URL("api/codetime", document.baseURI), {
|
|
12
|
+
method: "POST",
|
|
13
|
+
credentials: "same-origin",
|
|
14
|
+
redirect: "error",
|
|
15
|
+
headers: { "Content-Type": "application/json" },
|
|
16
|
+
body: JSON.stringify(request),
|
|
17
|
+
signal
|
|
18
|
+
});
|
|
19
|
+
const value = await response.json();
|
|
20
|
+
signal?.throwIfAborted();
|
|
21
|
+
if (!response.ok) throw new Error(errorText(value) ?? `HTTP ${response.status}`);
|
|
22
|
+
if (!isStatus(value)) throw new Error("unexpected response");
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
/** Extract the route's error message when present. */
|
|
26
|
+
function errorText(value) {
|
|
27
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
28
|
+
const error = value.error;
|
|
29
|
+
return typeof error === "string" ? error : void 0;
|
|
30
|
+
}
|
|
31
|
+
// src/client/strings.ts
|
|
32
|
+
/** Settings copy for the default locale. */
|
|
33
|
+
const en = {
|
|
34
|
+
title: "CodeTime",
|
|
35
|
+
subtitle: "Upload Harness coding activity to codetime.dev.",
|
|
36
|
+
connected: "Connected",
|
|
37
|
+
notConnected: "No token yet",
|
|
38
|
+
tokenLabel: "Upload token",
|
|
39
|
+
tokenPlaceholder: "Paste the token from codetime.dev/dashboard/settings",
|
|
40
|
+
tokenHint: "Create an upload token on the codetime.dev dashboard, then paste it here. The Harness sends turns, tool calls, models and token usage for this machine only.",
|
|
41
|
+
autoUpload: "Upload automatically",
|
|
42
|
+
save: "Save",
|
|
43
|
+
syncNow: "Upload now",
|
|
44
|
+
disconnect: "Disconnect",
|
|
45
|
+
saved: "Token saved",
|
|
46
|
+
cleared: "Token removed",
|
|
47
|
+
loading: "Loading…",
|
|
48
|
+
failed: "CodeTime request failed",
|
|
49
|
+
pending: "to upload",
|
|
50
|
+
sessions: "sessions",
|
|
51
|
+
uploaded: "Uploaded",
|
|
52
|
+
lastUpload: "Last upload",
|
|
53
|
+
never: "never",
|
|
54
|
+
machine: "Machine"
|
|
55
|
+
};
|
|
56
|
+
/** Settings copy for Chinese. */
|
|
57
|
+
const zh = {
|
|
58
|
+
title: "CodeTime",
|
|
59
|
+
subtitle: "把 Harness 的 AI 编码数据上报到 codetime.dev。",
|
|
60
|
+
connected: "已连接",
|
|
61
|
+
notConnected: "未填写 token",
|
|
62
|
+
tokenLabel: "上报 Token",
|
|
63
|
+
tokenPlaceholder: "粘贴 codetime.dev/dashboard/settings 里的 token",
|
|
64
|
+
tokenHint: "在 codetime.dev 后台创建一个上报 token 后粘贴到这里。只会把本机的轮次、工具调用、模型与 token 用量发出去。",
|
|
65
|
+
autoUpload: "自动上报",
|
|
66
|
+
save: "保存",
|
|
67
|
+
syncNow: "立即上报",
|
|
68
|
+
disconnect: "断开连接",
|
|
69
|
+
saved: "Token 已保存",
|
|
70
|
+
cleared: "Token 已清除",
|
|
71
|
+
loading: "加载中…",
|
|
72
|
+
failed: "编码统计请求失败",
|
|
73
|
+
pending: "待上报",
|
|
74
|
+
sessions: "个会话",
|
|
75
|
+
uploaded: "已上报",
|
|
76
|
+
lastUpload: "上次上报",
|
|
77
|
+
never: "从未",
|
|
78
|
+
machine: "机器"
|
|
79
|
+
};
|
|
80
|
+
const css = `
|
|
81
|
+
.ct-settings{display:flex;flex-direction:column;gap:14px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5}
|
|
82
|
+
.ct-settings h2,.ct-settings h3,.ct-settings p{margin:0}
|
|
83
|
+
.ct-settings h2{font-size:16px;font-weight:600}
|
|
84
|
+
.ct-settings .ct-sub{color:var(--dsw-alias-label-secondary);font-size:12px}
|
|
85
|
+
.ct-settings .ct-card{padding:16px;border-radius:12px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);display:flex;flex-direction:column;gap:12px}
|
|
86
|
+
.ct-settings .ct-state{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-weight:600}
|
|
87
|
+
.ct-settings .ct-badge{font-weight:400;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;line-height:1.8;letter-spacing:.4px;padding:1px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);white-space:nowrap}
|
|
88
|
+
.ct-settings .ct-muted{color:var(--dsw-alias-label-secondary);font-size:11px;overflow-wrap:anywhere}
|
|
89
|
+
.ct-settings .ct-form{display:flex;flex-direction:column;gap:10px}
|
|
90
|
+
.ct-settings label{display:flex;flex-direction:column;gap:4px;font-size:12px}
|
|
91
|
+
.ct-settings .ct-inline{display:flex;align-items:flex-end;gap:8px}
|
|
92
|
+
.ct-settings .ct-grow{flex:1 1 auto;min-width:0}
|
|
93
|
+
.ct-settings .ct-spacer{margin-left:auto}
|
|
94
|
+
.ct-settings input[type=password],.ct-settings input[type=text]{font:inherit;box-sizing:border-box;min-height:32px;padding:5px 10px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);min-width:0;width:100%}
|
|
95
|
+
.ct-settings input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
96
|
+
.ct-settings .ct-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:2px}
|
|
97
|
+
.ct-settings button{font:inherit;min-height:30px;padding:5px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);cursor:pointer}
|
|
98
|
+
.ct-settings button:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2)}
|
|
99
|
+
.ct-settings button:disabled{opacity:.5;cursor:default}
|
|
100
|
+
.ct-settings button[data-primary=true]{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary)}
|
|
101
|
+
.ct-settings button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
102
|
+
.ct-settings .ct-switch{display:flex;flex-direction:row;align-items:center;gap:8px;cursor:pointer;font-size:12px}
|
|
103
|
+
.ct-settings .ct-switch input{appearance:none;-webkit-appearance:none;position:relative;width:36px;height:20px;min-height:0;padding:0;border:0;border-radius:10px;background:var(--dsw-alias-bg-layer-2);cursor:pointer;transition:background .15s;flex:none}
|
|
104
|
+
.ct-settings .ct-switch input::after{content:'';position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-label-secondary);transition:transform .15s,background .15s}
|
|
105
|
+
.ct-settings .ct-switch input:checked{background:var(--dsw-alias-label-primary)}
|
|
106
|
+
.ct-settings .ct-switch input:checked::after{transform:translateX(16px);background:var(--dsw-alias-bg-base)}
|
|
107
|
+
.ct-settings .ct-switch input:disabled{opacity:.5;cursor:default}
|
|
108
|
+
.ct-settings .ct-ok{color:var(--dsw-alias-state-success-primary);font-size:12px}
|
|
109
|
+
.ct-settings .ct-error{color:var(--dsw-alias-state-error-primary);font-size:12px;overflow-wrap:anywhere}
|
|
110
|
+
.ct-settings .ct-stats{display:flex;gap:14px;flex-wrap:wrap;font-size:11px;color:var(--dsw-alias-label-secondary)}
|
|
111
|
+
`;
|
|
112
|
+
// src/client/settings.tsx
|
|
113
|
+
/**
|
|
114
|
+
* Build the settings section component.
|
|
115
|
+
* @param React - React from the browser module table.
|
|
116
|
+
* @param ctx - Client context carrying the locale service.
|
|
117
|
+
* @param t - locale-bound translator for this plugin's namespace.
|
|
118
|
+
* @returns the slot component for `settings.section`.
|
|
119
|
+
*/
|
|
120
|
+
function createSettings(React, ctx, t) {
|
|
121
|
+
return function CodeTimeSettings() {
|
|
122
|
+
React.useSyncExternalStore((fn) => ctx.locale.subscribe(fn), () => ctx.locale.getSnapshot());
|
|
123
|
+
const [status, setStatus] = React.useState(null);
|
|
124
|
+
const [token, setToken] = React.useState("");
|
|
125
|
+
const [busy, setBusy] = React.useState(false);
|
|
126
|
+
const [notice, setNotice] = React.useState("");
|
|
127
|
+
const [error, setError] = React.useState("");
|
|
128
|
+
const mounted = React.useRef(true);
|
|
129
|
+
const apply = React.useCallback((value) => {
|
|
130
|
+
if (!mounted.current) return;
|
|
131
|
+
setStatus(value);
|
|
132
|
+
}, []);
|
|
133
|
+
const run = React.useCallback(async (action, payload = {}, message) => {
|
|
134
|
+
setBusy(true);
|
|
135
|
+
setError("");
|
|
136
|
+
if (message) setNotice("");
|
|
137
|
+
try {
|
|
138
|
+
const value = await call({
|
|
139
|
+
action,
|
|
140
|
+
...payload
|
|
141
|
+
});
|
|
142
|
+
apply(value);
|
|
143
|
+
if (message) setNotice(message);
|
|
144
|
+
if (action === "save" || action === "clear") setToken("");
|
|
145
|
+
} catch (failure) {
|
|
146
|
+
if (mounted.current) setError(failure instanceof Error ? failure.message : t("failed"));
|
|
147
|
+
} finally {
|
|
148
|
+
if (mounted.current) setBusy(false);
|
|
149
|
+
}
|
|
150
|
+
}, [apply, t]);
|
|
151
|
+
React.useEffect(() => {
|
|
152
|
+
mounted.current = true;
|
|
153
|
+
run("status");
|
|
154
|
+
return () => {
|
|
155
|
+
mounted.current = false;
|
|
156
|
+
};
|
|
157
|
+
}, [run]);
|
|
158
|
+
React.useEffect(() => {
|
|
159
|
+
if (!status || !status.uploading && status.pending === 0) return;
|
|
160
|
+
const timer = setTimeout(() => {
|
|
161
|
+
call({ action: "status" }).then(apply).catch(() => {});
|
|
162
|
+
}, 4e3);
|
|
163
|
+
return () => clearTimeout(timer);
|
|
164
|
+
}, [status, apply]);
|
|
165
|
+
const configured = status?.configured ?? false;
|
|
166
|
+
const autoUpload = status?.autoUpload ?? true;
|
|
167
|
+
return /* @__PURE__ */ React.createElement("section", {
|
|
168
|
+
className: "ct-settings",
|
|
169
|
+
"aria-label": t("title"),
|
|
170
|
+
"aria-busy": busy
|
|
171
|
+
}, /* @__PURE__ */ React.createElement("style", null, css), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", null, t("title")), /* @__PURE__ */ React.createElement("p", { className: "ct-sub" }, t("subtitle"))), /* @__PURE__ */ React.createElement("div", { className: "ct-card" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "ct-state" }, configured ? t("connected") : t("notConnected"), configured && status?.maskedToken && /* @__PURE__ */ React.createElement("span", { className: "ct-badge" }, status.maskedToken)), !configured && /* @__PURE__ */ React.createElement("p", { className: "ct-muted" }, t("tokenHint"))), /* @__PURE__ */ React.createElement("form", {
|
|
172
|
+
className: "ct-form",
|
|
173
|
+
onSubmit: (event) => {
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
run("save", { token }, t("saved"));
|
|
176
|
+
}
|
|
177
|
+
}, /* @__PURE__ */ React.createElement("div", { className: "ct-inline" }, /* @__PURE__ */ React.createElement("label", { className: "ct-grow" }, t("tokenLabel"), /* @__PURE__ */ React.createElement("input", {
|
|
178
|
+
type: "password",
|
|
179
|
+
value: token,
|
|
180
|
+
placeholder: t("tokenPlaceholder"),
|
|
181
|
+
autoComplete: "off",
|
|
182
|
+
spellCheck: false,
|
|
183
|
+
onChange: (event) => setToken(event.currentTarget.value)
|
|
184
|
+
})), /* @__PURE__ */ React.createElement("button", {
|
|
185
|
+
type: "submit",
|
|
186
|
+
"data-primary": "true",
|
|
187
|
+
disabled: busy || !token.trim()
|
|
188
|
+
}, t("save"))), /* @__PURE__ */ React.createElement("div", { className: "ct-actions" }, /* @__PURE__ */ React.createElement("button", {
|
|
189
|
+
type: "button",
|
|
190
|
+
disabled: busy || !configured,
|
|
191
|
+
onClick: () => {
|
|
192
|
+
run("sync", {}, t("uploaded"));
|
|
193
|
+
}
|
|
194
|
+
}, t("syncNow")), /* @__PURE__ */ React.createElement("button", {
|
|
195
|
+
type: "button",
|
|
196
|
+
disabled: busy || !configured,
|
|
197
|
+
onClick: () => {
|
|
198
|
+
run("clear", {}, t("cleared"));
|
|
199
|
+
}
|
|
200
|
+
}, t("disconnect")), /* @__PURE__ */ React.createElement("label", { className: "ct-switch ct-spacer" }, /* @__PURE__ */ React.createElement("input", {
|
|
201
|
+
type: "checkbox",
|
|
202
|
+
role: "switch",
|
|
203
|
+
"aria-checked": autoUpload,
|
|
204
|
+
disabled: busy,
|
|
205
|
+
checked: autoUpload,
|
|
206
|
+
onChange: (event) => {
|
|
207
|
+
run("save", { autoUpload: event.currentTarget.checked });
|
|
208
|
+
}
|
|
209
|
+
}), t("autoUpload")))), notice && /* @__PURE__ */ React.createElement("p", {
|
|
210
|
+
className: "ct-ok",
|
|
211
|
+
role: "status"
|
|
212
|
+
}, notice), error && /* @__PURE__ */ React.createElement("p", {
|
|
213
|
+
className: "ct-error",
|
|
214
|
+
role: "alert"
|
|
215
|
+
}, error), status?.lastError && !error && /* @__PURE__ */ React.createElement("p", {
|
|
216
|
+
className: "ct-error",
|
|
217
|
+
role: "alert"
|
|
218
|
+
}, status.lastError), /* @__PURE__ */ React.createElement("div", { className: "ct-stats" }, /* @__PURE__ */ React.createElement("span", null, `${status?.pending ?? 0} ${t("pending")}`), /* @__PURE__ */ React.createElement("span", null, `${status?.sessions ?? 0} ${t("sessions")}`), /* @__PURE__ */ React.createElement("span", null, `${t("lastUpload")}: ${status?.lastUploadAt ? new Date(status.lastUploadAt).toLocaleString() : t("never")}`), status?.machineId && /* @__PURE__ */ React.createElement("span", null, `${t("machine")}: ${status.machineId.slice(0, 8)}`))));
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
// src/client.tsx
|
|
222
|
+
const loader = window.__ModuleLoader__;
|
|
223
|
+
if (!loader || typeof loader.load !== "function") throw new Error("Harness module loader is unavailable");
|
|
224
|
+
loader.load({
|
|
225
|
+
id: "@liyang8246/dsh-codetime",
|
|
226
|
+
factory(require) {
|
|
227
|
+
const React = require("react");
|
|
228
|
+
return {
|
|
229
|
+
inject: ["slots", "locale"],
|
|
230
|
+
apply(ctx) {
|
|
231
|
+
ctx.effect(() => ctx.locale.register("codetime", "en", en));
|
|
232
|
+
ctx.effect(() => ctx.locale.register("codetime", "zh", zh));
|
|
233
|
+
const t = ctx.locale.bind("codetime");
|
|
234
|
+
const Settings = createSettings(React, ctx, t);
|
|
235
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
236
|
+
name: "settings.section",
|
|
237
|
+
id: "codetime",
|
|
238
|
+
order: 14,
|
|
239
|
+
label: () => t("title")
|
|
240
|
+
}, Settings));
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
})();
|