dsh-skill-hub 0.3.4 → 0.3.5
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/lib/client.js +136 -330
- package/lib/client.js.map +1 -1
- package/package.json +29 -28
- package/src/client/index.tsx +2 -10
package/lib/client.js
CHANGED
|
@@ -846,333 +846,6 @@ window.__ModuleLoader__.load({
|
|
|
846
846
|
};
|
|
847
847
|
}
|
|
848
848
|
//#endregion
|
|
849
|
-
//#region src/client/grouping.ts
|
|
850
|
-
/**
|
|
851
|
-
* Derive a group switch view from its member names and the set of currently
|
|
852
|
-
* enabled skill names (catalog.skills). Members not in the enabled set count
|
|
853
|
-
* as disabled (catalog.disabled or absent read-only rows).
|
|
854
|
-
*/
|
|
855
|
-
function groupSwitchView(members, enabledNames) {
|
|
856
|
-
const enabled = [];
|
|
857
|
-
const disabled = [];
|
|
858
|
-
for (const name of members) if (enabledNames.has(name)) enabled.push(name);
|
|
859
|
-
else disabled.push(name);
|
|
860
|
-
return {
|
|
861
|
-
state: disabled.length === 0 ? "on" : enabled.length === 0 ? "off" : "mixed",
|
|
862
|
-
enabled,
|
|
863
|
-
disabled
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
/** Names of every group a skill belongs to (tags + collections). */
|
|
867
|
-
function groupNamesOf(name, tags, collections) {
|
|
868
|
-
const names = [];
|
|
869
|
-
for (const tag of tags) if (tag.skillNames.includes(name)) names.push(tag.name);
|
|
870
|
-
for (const collection of collections) if (collection.skillNames.includes(name)) names.push(collection.name);
|
|
871
|
-
return names;
|
|
872
|
-
}
|
|
873
|
-
/**
|
|
874
|
-
* Members of a group that are currently enabled AND also belong to at least
|
|
875
|
-
* one other group — the set a "close" action must ask about.
|
|
876
|
-
*/
|
|
877
|
-
function conflictsOnClose(members, enabledNames, otherGroups) {
|
|
878
|
-
return members.filter((name) => {
|
|
879
|
-
if (!enabledNames.has(name)) return false;
|
|
880
|
-
return otherGroups.some((group) => group.members.includes(name));
|
|
881
|
-
});
|
|
882
|
-
}
|
|
883
|
-
/** Origin-repo filter value: skills with no source record (private skills). */
|
|
884
|
-
const PRIVATE_SOURCE = "private";
|
|
885
|
-
/**
|
|
886
|
-
* Apply the origin filter ('all' or a specific origin repo; skills without a
|
|
887
|
-
* source record count as PRIVATE_SOURCE). The origins map is the store's
|
|
888
|
-
* skillName → repo derivation, so filtering follows the tracked source
|
|
889
|
-
* records instead of the filesystem root a skill happens to live under.
|
|
890
|
-
* 项目级技能(有 workspace 归属)永远不算「个人」。
|
|
891
|
-
*/
|
|
892
|
-
function filterBySource(skills, source, origins) {
|
|
893
|
-
if (source === "all") return [...skills];
|
|
894
|
-
return skills.filter((skill) => {
|
|
895
|
-
if (isProjectSource(skill.source)) return false;
|
|
896
|
-
return (origins[skill.name] ?? "private") === source;
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
/**
|
|
900
|
-
* Sort a skill list in place-safe copy order: name ascending, added
|
|
901
|
-
* descending (newest first, unknown addedAt last), or uses descending
|
|
902
|
-
* (most-called first). Unknown values always trail.
|
|
903
|
-
*/
|
|
904
|
-
function sortSkills(skills, key, getUses) {
|
|
905
|
-
const list = [...skills];
|
|
906
|
-
if (key === "name") list.sort((a, b) => a.name.localeCompare(b.name));
|
|
907
|
-
else if (key === "added") list.sort((a, b) => (b.addedAt ?? -Infinity) - (a.addedAt ?? -Infinity));
|
|
908
|
-
else if (key === "uses") list.sort((a, b) => (getUses?.(b.name) ?? 0) - (getUses?.(a.name) ?? 0));
|
|
909
|
-
return list;
|
|
910
|
-
}
|
|
911
|
-
/** Format an epoch-ms timestamp as a short relative time bucket. */
|
|
912
|
-
function formatRelativeTime(ms, now = Date.now()) {
|
|
913
|
-
const diff = Math.max(0, now - ms);
|
|
914
|
-
const minutes = Math.floor(diff / 6e4);
|
|
915
|
-
if (minutes < 1) return { key: "time.justNow" };
|
|
916
|
-
if (minutes < 60) return {
|
|
917
|
-
key: "time.minutesAgo",
|
|
918
|
-
value: minutes
|
|
919
|
-
};
|
|
920
|
-
const hours = Math.floor(minutes / 60);
|
|
921
|
-
if (hours < 24) return {
|
|
922
|
-
key: "time.hoursAgo",
|
|
923
|
-
value: hours
|
|
924
|
-
};
|
|
925
|
-
const days = Math.floor(hours / 24);
|
|
926
|
-
if (days < 7) return {
|
|
927
|
-
key: "time.daysAgo",
|
|
928
|
-
value: days
|
|
929
|
-
};
|
|
930
|
-
return {
|
|
931
|
-
key: "time.weeksAgo",
|
|
932
|
-
value: Math.floor(days / 7)
|
|
933
|
-
};
|
|
934
|
-
}
|
|
935
|
-
//#endregion
|
|
936
|
-
//#region src/client/helpers.ts
|
|
937
|
-
/**
|
|
938
|
-
* Shared panel helpers: the active-dictionary pick (document-language
|
|
939
|
-
* based, family precedent) plus a small error-message extractor.
|
|
940
|
-
*/
|
|
941
|
-
/** Active dictionary, picked by the document language at call time. */
|
|
942
|
-
function dictionary() {
|
|
943
|
-
return (typeof document !== "undefined" ? document.documentElement.lang : "zh").toLowerCase().startsWith("en") ? { ...en } : { ...zh };
|
|
944
|
-
}
|
|
945
|
-
/** Translate a key with optional {name} template params (current language). */
|
|
946
|
-
function tt(key, values) {
|
|
947
|
-
return t(dictionary(), key, values);
|
|
948
|
-
}
|
|
949
|
-
/** Human-readable error text from an unknown thrown value. */
|
|
950
|
-
function errorMessage(error) {
|
|
951
|
-
if (error instanceof Error) return error.message;
|
|
952
|
-
return String(error);
|
|
953
|
-
}
|
|
954
|
-
//#endregion
|
|
955
|
-
//#region src/client/panel/format.ts
|
|
956
|
-
/** Model-invocable dot color default. Single source for the TS side; the
|
|
957
|
-
* panel's CSS mirrors it via --hub-model (panel.module.css). */
|
|
958
|
-
const DEFAULT_DOT_MODEL_COLOR = "#2f81f7";
|
|
959
|
-
/** User-invocable dot color default. Single source for the TS side; the
|
|
960
|
-
* panel's CSS mirrors it via --hub-user (panel.module.css). */
|
|
961
|
-
const DEFAULT_DOT_USER_COLOR = "#3fb950";
|
|
962
|
-
/** Dot inline style from the user-chosen color (undefined keeps the CSS default). */
|
|
963
|
-
function dotStyle(color) {
|
|
964
|
-
if (color === void 0) return void 0;
|
|
965
|
-
return {
|
|
966
|
-
background: color,
|
|
967
|
-
borderColor: color
|
|
968
|
-
};
|
|
969
|
-
}
|
|
970
|
-
/** Localized relative-time text for a Unix-ms timestamp. */
|
|
971
|
-
function relativeTimeText(ms) {
|
|
972
|
-
const rt = formatRelativeTime(ms);
|
|
973
|
-
return tt(rt.key, rt.value !== void 0 ? { value: rt.value } : void 0);
|
|
974
|
-
}
|
|
975
|
-
/** Localized absolute time for the detail meta (e.g. "2026/8/16 11:00:00"). */
|
|
976
|
-
function formatDateTime(ms) {
|
|
977
|
-
return new Date(ms).toLocaleString();
|
|
978
|
-
}
|
|
979
|
-
/** Short form of a commit SHA for display. */
|
|
980
|
-
function shortSha(sha) {
|
|
981
|
-
return sha.length > 7 ? sha.slice(0, 7) : sha;
|
|
982
|
-
}
|
|
983
|
-
//#endregion
|
|
984
|
-
//#region src/client/slash-dots.tsx
|
|
985
|
-
/** The core plugin's skill source identity on the '/' trigger. */
|
|
986
|
-
const SKILL_SOURCE = {
|
|
987
|
-
trigger: "/",
|
|
988
|
-
name: "skill"
|
|
989
|
-
};
|
|
990
|
-
/** How long one catalog-derived modelInvocable map stays hot before refresh. */
|
|
991
|
-
const MODEL_TTL_MS = 6e4;
|
|
992
|
-
/**
|
|
993
|
-
* Find the core `/skill` source through the runtime registry, or undefined.
|
|
994
|
-
* The lookup never throws: a missing/reshaped registry just means no dots. Exported
|
|
995
|
-
* for unit tests; the apply path only calls it indirectly through setupSkillSlashDots.
|
|
996
|
-
* @param service - the ctx.inputTriggers service face.
|
|
997
|
-
* @returns the registered skill source, or undefined.
|
|
998
|
-
*/
|
|
999
|
-
function findSkillSource(service) {
|
|
1000
|
-
const live = service.live;
|
|
1001
|
-
if (live?.sources === void 0) return void 0;
|
|
1002
|
-
return live.sources.find((source) => source.trigger === SKILL_SOURCE.trigger && source.name === SKILL_SOURCE.name);
|
|
1003
|
-
}
|
|
1004
|
-
/** One catalog-derived name → modelInvocable snapshot with a load timestamp. */
|
|
1005
|
-
let modelCache;
|
|
1006
|
-
/**
|
|
1007
|
-
* Clear the modelInvocable cache. Called on connection/reset so a fresh
|
|
1008
|
-
* catalog wins after reconnect; exported for deterministic unit tests.
|
|
1009
|
-
*/
|
|
1010
|
-
function resetModelCache() {
|
|
1011
|
-
modelCache = void 0;
|
|
1012
|
-
}
|
|
1013
|
-
/**
|
|
1014
|
-
* Resolve name → modelInvocable from the hub's catalog, cached for
|
|
1015
|
-
* MODEL_TTL_MS. A failed load caches the failure briefly so a downed route
|
|
1016
|
-
* doesn't hammer the host on every keystroke; the returned map is empty then
|
|
1017
|
-
* (callers fall back to the model dot for unknown names).
|
|
1018
|
-
* @param api - the hub browser API.
|
|
1019
|
-
* @returns name → whether the model may call the skill.
|
|
1020
|
-
*/
|
|
1021
|
-
async function modelInvocableMap(api) {
|
|
1022
|
-
const now = Date.now();
|
|
1023
|
-
const cached = modelCache;
|
|
1024
|
-
if (cached !== void 0 && now - cached.at < MODEL_TTL_MS) return "failed" in cached ? /* @__PURE__ */ new Map() : cached.map;
|
|
1025
|
-
try {
|
|
1026
|
-
const catalog = await api.catalog();
|
|
1027
|
-
const map = new Map(catalog.skills.map((skill) => [skill.name, skill.invocation.modelInvocable]));
|
|
1028
|
-
modelCache = {
|
|
1029
|
-
at: now,
|
|
1030
|
-
map
|
|
1031
|
-
};
|
|
1032
|
-
return map;
|
|
1033
|
-
} catch (error) {
|
|
1034
|
-
console.error("[dsh-skill-hub] slash dot color lookup failed:", error);
|
|
1035
|
-
modelCache = {
|
|
1036
|
-
at: now,
|
|
1037
|
-
failed: true
|
|
1038
|
-
};
|
|
1039
|
-
return /* @__PURE__ */ new Map();
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
/**
|
|
1043
|
-
* One menu-row dot element. Inline span so it needs no CSS module; the
|
|
1044
|
-
* candidate menu centers it inside its 16×16 leading icon slot.
|
|
1045
|
-
* @param color - the dot's background color.
|
|
1046
|
-
* @returns a React node (memory-only, never crosses the Host boundary).
|
|
1047
|
-
*/
|
|
1048
|
-
function dotIcon(color) {
|
|
1049
|
-
return (0, react.createElement)("span", {
|
|
1050
|
-
"aria-hidden": true,
|
|
1051
|
-
style: {
|
|
1052
|
-
display: "inline-block",
|
|
1053
|
-
width: 6,
|
|
1054
|
-
height: 6,
|
|
1055
|
-
borderRadius: 3,
|
|
1056
|
-
background: color,
|
|
1057
|
-
flex: "none"
|
|
1058
|
-
}
|
|
1059
|
-
});
|
|
1060
|
-
}
|
|
1061
|
-
/**
|
|
1062
|
-
* Wrap the core skill source so every menu row carries the invocation dot.
|
|
1063
|
-
* Exported for unit tests; production wiring goes through setupSkillSlashDots.
|
|
1064
|
-
* @param source - the registered `/skill` source.
|
|
1065
|
-
* @param api - hub browser API for the modelInvocable lookup.
|
|
1066
|
-
* @param scope - hub settings scope for the dot colors.
|
|
1067
|
-
* @returns a disposer restoring the original candidates.
|
|
1068
|
-
*/
|
|
1069
|
-
/**
|
|
1070
|
-
* DOM 兜底:在 alpha.2 新版 MenuView(icon 仅枚举)下通过直接操作
|
|
1071
|
-
* 已渲染的 `[role="option"]` 列表注入彩色点,绕过 `icon` 限制。
|
|
1072
|
-
* 旧版仍走 `icon` 注入,此处仅为新版。
|
|
1073
|
-
*/
|
|
1074
|
-
function injectDotsViaDOM(modelByName, modelColor, userColor) {
|
|
1075
|
-
if (typeof document === "undefined" || typeof requestAnimationFrame === "undefined") return;
|
|
1076
|
-
const run = () => {
|
|
1077
|
-
const listbox = document.querySelector("[class*=\"_3e4SsG_menu\"]")?.querySelector("[role=\"listbox\"]") ?? document.querySelector("[role=\"listbox\"]");
|
|
1078
|
-
if (listbox === null) return;
|
|
1079
|
-
const options = listbox.querySelectorAll("[role=\"option\"]");
|
|
1080
|
-
for (const opt of options) {
|
|
1081
|
-
if (opt.querySelector("[data-skill-dot]") !== null) continue;
|
|
1082
|
-
const nameEl = opt.querySelector("[class*=\"itemName\"]");
|
|
1083
|
-
if (nameEl === null) continue;
|
|
1084
|
-
const name = (nameEl.textContent ?? "").trim();
|
|
1085
|
-
if (name.length === 0) continue;
|
|
1086
|
-
if (!modelByName.has(name)) continue;
|
|
1087
|
-
const color = modelByName.get(name) ?? true ? modelColor : userColor;
|
|
1088
|
-
const wrapper = document.createElement("span");
|
|
1089
|
-
wrapper.setAttribute("data-skill-dot", "");
|
|
1090
|
-
wrapper.setAttribute("aria-hidden", "true");
|
|
1091
|
-
wrapper.style.width = "16px";
|
|
1092
|
-
wrapper.style.height = "16px";
|
|
1093
|
-
wrapper.style.display = "inline-flex";
|
|
1094
|
-
wrapper.style.justifyContent = "center";
|
|
1095
|
-
wrapper.style.alignItems = "center";
|
|
1096
|
-
wrapper.style.flex = "none";
|
|
1097
|
-
const dot = document.createElement("span");
|
|
1098
|
-
dot.style.display = "inline-block";
|
|
1099
|
-
dot.style.width = "6px";
|
|
1100
|
-
dot.style.height = "6px";
|
|
1101
|
-
dot.style.borderRadius = "3px";
|
|
1102
|
-
dot.style.background = color;
|
|
1103
|
-
dot.style.flex = "none";
|
|
1104
|
-
wrapper.appendChild(dot);
|
|
1105
|
-
nameEl.parentElement?.insertBefore(wrapper, nameEl);
|
|
1106
|
-
}
|
|
1107
|
-
};
|
|
1108
|
-
requestAnimationFrame(() => setTimeout(run, 0));
|
|
1109
|
-
}
|
|
1110
|
-
function wrapSkillSource(source, api, scope) {
|
|
1111
|
-
const original = source.candidates;
|
|
1112
|
-
source.candidates = async (session, req) => {
|
|
1113
|
-
const items = await original(session, req);
|
|
1114
|
-
if (req.signal.aborted) return items;
|
|
1115
|
-
const isNewHost = req !== null && typeof req === "object" && "drilled" in req;
|
|
1116
|
-
const modelByName = await modelInvocableMap(api);
|
|
1117
|
-
if (req.signal.aborted) return items;
|
|
1118
|
-
const snapshot = scope.getSnapshot();
|
|
1119
|
-
const modelColor = snapshot.value?.dotModelColor ?? "#2f81f7";
|
|
1120
|
-
const userColor = snapshot.value?.dotUserColor ?? "#3fb950";
|
|
1121
|
-
if (isNewHost) {
|
|
1122
|
-
injectDotsViaDOM(modelByName, modelColor, userColor);
|
|
1123
|
-
return items;
|
|
1124
|
-
}
|
|
1125
|
-
return items.map((item) => ({
|
|
1126
|
-
...item,
|
|
1127
|
-
icon: dotIcon(modelByName.get(item.name) ?? true ? modelColor : userColor)
|
|
1128
|
-
}));
|
|
1129
|
-
};
|
|
1130
|
-
return () => {
|
|
1131
|
-
source.candidates = original;
|
|
1132
|
-
};
|
|
1133
|
-
}
|
|
1134
|
-
/**
|
|
1135
|
-
* Mount the slash-menu dots on the registered `/skill` source. Idempotent and
|
|
1136
|
-
* defensive: if the core source isn't registered yet (or the registry shape
|
|
1137
|
-
* changes), it retries briefly and then gives up silently — the chat keeps
|
|
1138
|
-
* working, it simply shows no dots. The returned disposer restores the
|
|
1139
|
-
* original candidates and clears the model cache.
|
|
1140
|
-
* @param ctx - the client root context (inputTriggers + events).
|
|
1141
|
-
* @param api - hub browser API.
|
|
1142
|
-
* @param scope - hub settings scope for dot colors.
|
|
1143
|
-
* @returns a cleanup function for `ctx.effect`.
|
|
1144
|
-
*/
|
|
1145
|
-
function setupSkillSlashDots(ctx, api, scope) {
|
|
1146
|
-
const inputTriggers = ctx.get("inputTriggers");
|
|
1147
|
-
if (inputTriggers === void 0) return () => {};
|
|
1148
|
-
let disposed = false;
|
|
1149
|
-
let restore;
|
|
1150
|
-
let attempts = 0;
|
|
1151
|
-
const attempt = () => {
|
|
1152
|
-
if (disposed) return;
|
|
1153
|
-
const source = findSkillSource(inputTriggers);
|
|
1154
|
-
if (source === void 0) {
|
|
1155
|
-
if (attempts < 10) {
|
|
1156
|
-
attempts += 1;
|
|
1157
|
-
setTimeout(attempt, 100);
|
|
1158
|
-
}
|
|
1159
|
-
return;
|
|
1160
|
-
}
|
|
1161
|
-
restore = wrapSkillSource(source, api, scope);
|
|
1162
|
-
};
|
|
1163
|
-
attempt();
|
|
1164
|
-
const clearCache = () => {
|
|
1165
|
-
resetModelCache();
|
|
1166
|
-
};
|
|
1167
|
-
const offReset = ctx.on("connection/reset", clearCache);
|
|
1168
|
-
return () => {
|
|
1169
|
-
disposed = true;
|
|
1170
|
-
offReset();
|
|
1171
|
-
restore?.();
|
|
1172
|
-
restore = void 0;
|
|
1173
|
-
};
|
|
1174
|
-
}
|
|
1175
|
-
//#endregion
|
|
1176
849
|
//#region src/client/icons.tsx
|
|
1177
850
|
/** ic_ds_chevron_down_outline_14 */
|
|
1178
851
|
function IconChevronDownOutline14({ size = 14, className }) {
|
|
@@ -2440,6 +2113,141 @@ window.__ModuleLoader__.load({
|
|
|
2440
2113
|
}
|
|
2441
2114
|
};
|
|
2442
2115
|
//#endregion
|
|
2116
|
+
//#region src/client/grouping.ts
|
|
2117
|
+
/**
|
|
2118
|
+
* Derive a group switch view from its member names and the set of currently
|
|
2119
|
+
* enabled skill names (catalog.skills). Members not in the enabled set count
|
|
2120
|
+
* as disabled (catalog.disabled or absent read-only rows).
|
|
2121
|
+
*/
|
|
2122
|
+
function groupSwitchView(members, enabledNames) {
|
|
2123
|
+
const enabled = [];
|
|
2124
|
+
const disabled = [];
|
|
2125
|
+
for (const name of members) if (enabledNames.has(name)) enabled.push(name);
|
|
2126
|
+
else disabled.push(name);
|
|
2127
|
+
return {
|
|
2128
|
+
state: disabled.length === 0 ? "on" : enabled.length === 0 ? "off" : "mixed",
|
|
2129
|
+
enabled,
|
|
2130
|
+
disabled
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
/** Names of every group a skill belongs to (tags + collections). */
|
|
2134
|
+
function groupNamesOf(name, tags, collections) {
|
|
2135
|
+
const names = [];
|
|
2136
|
+
for (const tag of tags) if (tag.skillNames.includes(name)) names.push(tag.name);
|
|
2137
|
+
for (const collection of collections) if (collection.skillNames.includes(name)) names.push(collection.name);
|
|
2138
|
+
return names;
|
|
2139
|
+
}
|
|
2140
|
+
/**
|
|
2141
|
+
* Members of a group that are currently enabled AND also belong to at least
|
|
2142
|
+
* one other group — the set a "close" action must ask about.
|
|
2143
|
+
*/
|
|
2144
|
+
function conflictsOnClose(members, enabledNames, otherGroups) {
|
|
2145
|
+
return members.filter((name) => {
|
|
2146
|
+
if (!enabledNames.has(name)) return false;
|
|
2147
|
+
return otherGroups.some((group) => group.members.includes(name));
|
|
2148
|
+
});
|
|
2149
|
+
}
|
|
2150
|
+
/** Origin-repo filter value: skills with no source record (private skills). */
|
|
2151
|
+
const PRIVATE_SOURCE = "private";
|
|
2152
|
+
/**
|
|
2153
|
+
* Apply the origin filter ('all' or a specific origin repo; skills without a
|
|
2154
|
+
* source record count as PRIVATE_SOURCE). The origins map is the store's
|
|
2155
|
+
* skillName → repo derivation, so filtering follows the tracked source
|
|
2156
|
+
* records instead of the filesystem root a skill happens to live under.
|
|
2157
|
+
* 项目级技能(有 workspace 归属)永远不算「个人」。
|
|
2158
|
+
*/
|
|
2159
|
+
function filterBySource(skills, source, origins) {
|
|
2160
|
+
if (source === "all") return [...skills];
|
|
2161
|
+
return skills.filter((skill) => {
|
|
2162
|
+
if (isProjectSource(skill.source)) return false;
|
|
2163
|
+
return (origins[skill.name] ?? "private") === source;
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
/**
|
|
2167
|
+
* Sort a skill list in place-safe copy order: name ascending, added
|
|
2168
|
+
* descending (newest first, unknown addedAt last), or uses descending
|
|
2169
|
+
* (most-called first). Unknown values always trail.
|
|
2170
|
+
*/
|
|
2171
|
+
function sortSkills(skills, key, getUses) {
|
|
2172
|
+
const list = [...skills];
|
|
2173
|
+
if (key === "name") list.sort((a, b) => a.name.localeCompare(b.name));
|
|
2174
|
+
else if (key === "added") list.sort((a, b) => (b.addedAt ?? -Infinity) - (a.addedAt ?? -Infinity));
|
|
2175
|
+
else if (key === "uses") list.sort((a, b) => (getUses?.(b.name) ?? 0) - (getUses?.(a.name) ?? 0));
|
|
2176
|
+
return list;
|
|
2177
|
+
}
|
|
2178
|
+
/** Format an epoch-ms timestamp as a short relative time bucket. */
|
|
2179
|
+
function formatRelativeTime(ms, now = Date.now()) {
|
|
2180
|
+
const diff = Math.max(0, now - ms);
|
|
2181
|
+
const minutes = Math.floor(diff / 6e4);
|
|
2182
|
+
if (minutes < 1) return { key: "time.justNow" };
|
|
2183
|
+
if (minutes < 60) return {
|
|
2184
|
+
key: "time.minutesAgo",
|
|
2185
|
+
value: minutes
|
|
2186
|
+
};
|
|
2187
|
+
const hours = Math.floor(minutes / 60);
|
|
2188
|
+
if (hours < 24) return {
|
|
2189
|
+
key: "time.hoursAgo",
|
|
2190
|
+
value: hours
|
|
2191
|
+
};
|
|
2192
|
+
const days = Math.floor(hours / 24);
|
|
2193
|
+
if (days < 7) return {
|
|
2194
|
+
key: "time.daysAgo",
|
|
2195
|
+
value: days
|
|
2196
|
+
};
|
|
2197
|
+
return {
|
|
2198
|
+
key: "time.weeksAgo",
|
|
2199
|
+
value: Math.floor(days / 7)
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
//#endregion
|
|
2203
|
+
//#region src/client/helpers.ts
|
|
2204
|
+
/**
|
|
2205
|
+
* Shared panel helpers: the active-dictionary pick (document-language
|
|
2206
|
+
* based, family precedent) plus a small error-message extractor.
|
|
2207
|
+
*/
|
|
2208
|
+
/** Active dictionary, picked by the document language at call time. */
|
|
2209
|
+
function dictionary() {
|
|
2210
|
+
return (typeof document !== "undefined" ? document.documentElement.lang : "zh").toLowerCase().startsWith("en") ? { ...en } : { ...zh };
|
|
2211
|
+
}
|
|
2212
|
+
/** Translate a key with optional {name} template params (current language). */
|
|
2213
|
+
function tt(key, values) {
|
|
2214
|
+
return t(dictionary(), key, values);
|
|
2215
|
+
}
|
|
2216
|
+
/** Human-readable error text from an unknown thrown value. */
|
|
2217
|
+
function errorMessage(error) {
|
|
2218
|
+
if (error instanceof Error) return error.message;
|
|
2219
|
+
return String(error);
|
|
2220
|
+
}
|
|
2221
|
+
//#endregion
|
|
2222
|
+
//#region src/client/panel/format.ts
|
|
2223
|
+
/** Model-invocable dot color default. Single source for the TS side; the
|
|
2224
|
+
* panel's CSS mirrors it via --hub-model (panel.module.css). */
|
|
2225
|
+
const DEFAULT_DOT_MODEL_COLOR = "#2f81f7";
|
|
2226
|
+
/** User-invocable dot color default. Single source for the TS side; the
|
|
2227
|
+
* panel's CSS mirrors it via --hub-user (panel.module.css). */
|
|
2228
|
+
const DEFAULT_DOT_USER_COLOR = "#3fb950";
|
|
2229
|
+
/** Dot inline style from the user-chosen color (undefined keeps the CSS default). */
|
|
2230
|
+
function dotStyle(color) {
|
|
2231
|
+
if (color === void 0) return void 0;
|
|
2232
|
+
return {
|
|
2233
|
+
background: color,
|
|
2234
|
+
borderColor: color
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
/** Localized relative-time text for a Unix-ms timestamp. */
|
|
2238
|
+
function relativeTimeText(ms) {
|
|
2239
|
+
const rt = formatRelativeTime(ms);
|
|
2240
|
+
return tt(rt.key, rt.value !== void 0 ? { value: rt.value } : void 0);
|
|
2241
|
+
}
|
|
2242
|
+
/** Localized absolute time for the detail meta (e.g. "2026/8/16 11:00:00"). */
|
|
2243
|
+
function formatDateTime(ms) {
|
|
2244
|
+
return new Date(ms).toLocaleString();
|
|
2245
|
+
}
|
|
2246
|
+
/** Short form of a commit SHA for display. */
|
|
2247
|
+
function shortSha(sha) {
|
|
2248
|
+
return sha.length > 7 ? sha.slice(0, 7) : sha;
|
|
2249
|
+
}
|
|
2250
|
+
//#endregion
|
|
2443
2251
|
//#region src/client/SkillHubSettingsCard.tsx
|
|
2444
2252
|
/**
|
|
2445
2253
|
* The dsh-skill-hub plugin settings card: bridges the hub's settings
|
|
@@ -6525,8 +6333,7 @@ window.__ModuleLoader__.load({
|
|
|
6525
6333
|
"locale",
|
|
6526
6334
|
"connection",
|
|
6527
6335
|
"remote",
|
|
6528
|
-
"settingsScope"
|
|
6529
|
-
"inputTriggers"
|
|
6336
|
+
"settingsScope"
|
|
6530
6337
|
];
|
|
6531
6338
|
/**
|
|
6532
6339
|
* Mount the settings card and the skill hub section.
|
|
@@ -6540,7 +6347,6 @@ window.__ModuleLoader__.load({
|
|
|
6540
6347
|
const t = ctx.locale.bind(NS);
|
|
6541
6348
|
const api = new SkillHubApi();
|
|
6542
6349
|
const settingsCard = new SkillHubSettingsCardController(ctx.settingsScope.bind({ namespace: NS }));
|
|
6543
|
-
ctx.effect(() => setupSkillSlashDots(ctx, api, ctx.settingsScope.bind({ namespace: NS })), "dsh-skill-hub: slash dots");
|
|
6544
6350
|
ctx.effect(() => ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
|
|
6545
6351
|
name: "settings.plugin.item",
|
|
6546
6352
|
key: NS,
|