@oharness/server 0.10.0 → 0.12.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/dist/analytics.d.ts +32 -29
- package/dist/analytics.js +225 -278
- package/dist/analytics.js.map +1 -1
- package/dist/i18n.js +242 -12
- package/dist/i18n.js.map +1 -1
- package/dist/icons.d.ts +3 -0
- package/dist/icons.js +69 -3
- package/dist/icons.js.map +1 -1
- package/dist/model-order.js +1 -0
- package/dist/model-order.js.map +1 -1
- package/dist/page/client/account.js +140 -4
- package/dist/page/client/account.js.map +1 -1
- package/dist/page/client/base.js +42 -16
- package/dist/page/client/base.js.map +1 -1
- package/dist/page/client/browser.js +1 -1
- package/dist/page/client/composer.js +641 -47
- package/dist/page/client/composer.js.map +1 -1
- package/dist/page/client/dialogs.js +75 -0
- package/dist/page/client/dialogs.js.map +1 -1
- package/dist/page/client/events.js +25 -2
- package/dist/page/client/events.js.map +1 -1
- package/dist/page/client/index.js +4 -0
- package/dist/page/client/index.js.map +1 -1
- package/dist/page/client/panels.js +4 -4
- package/dist/page/client/ripple.d.ts +13 -0
- package/dist/page/client/ripple.js +247 -0
- package/dist/page/client/ripple.js.map +1 -0
- package/dist/page/client/routines.js +173 -41
- package/dist/page/client/routines.js.map +1 -1
- package/dist/page/client/shell.js +200 -54
- package/dist/page/client/shell.js.map +1 -1
- package/dist/page/client/state.js +7 -2
- package/dist/page/client/state.js.map +1 -1
- package/dist/page/client/terminal.js +34 -7
- package/dist/page/client/terminal.js.map +1 -1
- package/dist/page/client/theme.d.ts +17 -0
- package/dist/page/client/theme.js +365 -0
- package/dist/page/client/theme.js.map +1 -0
- package/dist/page/client/transcript.js +63 -7
- package/dist/page/client/transcript.js.map +1 -1
- package/dist/page/style.js +273 -22
- package/dist/page/style.js.map +1 -1
- package/dist/page.js +5 -5
- package/dist/page.js.map +1 -1
- package/dist/prefs.d.ts +17 -0
- package/dist/prefs.js +5 -0
- package/dist/prefs.js.map +1 -1
- package/dist/server.js +710 -45
- package/dist/server.js.map +1 -1
- package/dist/suggest.d.ts +42 -0
- package/dist/suggest.js +215 -0
- package/dist/suggest.js.map +1 -0
- package/dist/terminal.d.ts +35 -2
- package/dist/terminal.js +60 -4
- package/dist/terminal.js.map +1 -1
- package/dist/theme-assets.d.ts +39 -0
- package/dist/theme-assets.js +109 -0
- package/dist/theme-assets.js.map +1 -0
- package/dist/theme-store.d.ts +50 -0
- package/dist/theme-store.js +124 -0
- package/dist/theme-store.js.map +1 -0
- package/dist/theme-tools.d.ts +25 -0
- package/dist/theme-tools.js +243 -0
- package/dist/theme-tools.js.map +1 -0
- package/dist/tool-icons.d.ts +12 -0
- package/dist/tool-icons.js +16 -0
- package/dist/tool-icons.js.map +1 -0
- package/dist/turn-shots.d.ts +18 -2
- package/dist/turn-shots.js +10 -1
- package/dist/turn-shots.js.map +1 -1
- package/dist/user-theme.d.ts +143 -0
- package/dist/user-theme.js +510 -0
- package/dist/user-theme.js.map +1 -0
- package/package.json +2 -2
|
@@ -50,13 +50,11 @@ const inputPlaceholder = () =>
|
|
|
50
50
|
|
|
51
51
|
function setGoalMode(on) {
|
|
52
52
|
goalMode = on;
|
|
53
|
-
input.placeholder = inputPlaceholder();
|
|
54
53
|
renderAttachments();
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
function setWorktreeMode(on) {
|
|
58
57
|
worktreeMode = on;
|
|
59
|
-
input.placeholder = inputPlaceholder();
|
|
60
58
|
renderAttachments();
|
|
61
59
|
}
|
|
62
60
|
|
|
@@ -251,6 +249,9 @@ function renderQueued() {
|
|
|
251
249
|
|
|
252
250
|
function renderAttachments() {
|
|
253
251
|
attachEl.replaceChildren();
|
|
252
|
+
// What is staged decides whether the guess still fits, and the goal and
|
|
253
|
+
// worktree modes repaint through here with their own placeholder.
|
|
254
|
+
paintSuggestion();
|
|
254
255
|
|
|
255
256
|
if (goalMode) {
|
|
256
257
|
const chip = el("div", "att goal");
|
|
@@ -553,13 +554,21 @@ let fileList = { entries: [] };
|
|
|
553
554
|
/** Whether the context endpoint has answered once, for the mention list. */
|
|
554
555
|
let offerLoaded = false;
|
|
555
556
|
/** Connectors and plugins, refreshed each time the menu opens. */
|
|
556
|
-
let offer = { connectors: [], plugins: [], skills: [] };
|
|
557
|
+
let offer = { connectors: [], plugins: [], skills: [], agents: [] };
|
|
557
558
|
/** The connectors on offer rather than the ones already here. */
|
|
558
559
|
let market = [];
|
|
560
|
+
/** Servers found in Claude Code's and Codex's config, offered beside them. */
|
|
561
|
+
let foreignMarket = [];
|
|
559
562
|
/** The connector whose sign-in is open in a browser tab, if any. */
|
|
560
563
|
let authorizing = null;
|
|
561
564
|
/** The connector being added right now, so its row can say so. */
|
|
562
565
|
let adding = null;
|
|
566
|
+
/** The plugins on offer, from our own list; fetched when that screen opens. */
|
|
567
|
+
let pluginMarket = [];
|
|
568
|
+
/** The plugin whose install was asked for once and is waiting to be confirmed. */
|
|
569
|
+
let confirming = null;
|
|
570
|
+
/** The plugin downloading right now. */
|
|
571
|
+
let installing = null;
|
|
563
572
|
let attachQuery = "";
|
|
564
573
|
let attachCursor = 0;
|
|
565
574
|
/** Guards against a slow answer to an old keystroke landing after a new one. */
|
|
@@ -602,7 +611,7 @@ function attachRows() {
|
|
|
602
611
|
if (attachView === "skills" || attachView === "plugins" || attachView === "connectors") {
|
|
603
612
|
const kind = attachView;
|
|
604
613
|
const items =
|
|
605
|
-
kind === "skills" ?
|
|
614
|
+
kind === "skills" ? usableSkills()
|
|
606
615
|
: kind === "plugins" ? offer.plugins || []
|
|
607
616
|
: offer.connectors || [];
|
|
608
617
|
const row = (entry) => ({
|
|
@@ -640,11 +649,11 @@ function attachRows() {
|
|
|
640
649
|
...(!items.length ? [{ type: "hint", label: t(empty[kind]) }] : []),
|
|
641
650
|
...(hidden > 0 ? [{ type: "hint", label: t("add.andMore", { n: hidden }) }] : []),
|
|
642
651
|
// Browse is only a different question where there is somewhere to browse:
|
|
643
|
-
// connectors have a registry of ones not installed
|
|
644
|
-
// are whatever is on this machine already. "More" only when the
|
|
645
|
-
// is holding some back; with everything in view it is the catalogue.
|
|
646
|
-
...(kind === "connectors"
|
|
647
|
-
? [{ type: "market", kind, icon: "search", label: t(hidden > 0 ? "add.browse" : "add.browseAll") }]
|
|
652
|
+
// connectors have a registry of ones not installed and plugins a market,
|
|
653
|
+
// skills are whatever is on this machine already. "More" only when the
|
|
654
|
+
// menu is holding some back; with everything in view it is the catalogue.
|
|
655
|
+
...(kind === "connectors" || kind === "plugins"
|
|
656
|
+
? [{ type: "market", kind, icon: "search", label: t(hidden > 0 ? "add.browse" : "add.browseAll." + kind) }]
|
|
648
657
|
: []),
|
|
649
658
|
{ type: "manage", kind, icon: "settings", label: t("add.manage") },
|
|
650
659
|
];
|
|
@@ -679,7 +688,7 @@ function attachRows() {
|
|
|
679
688
|
* under the name is the one a person would think of. Someone after the
|
|
680
689
|
* video tools types "video", and only the description contains it.
|
|
681
690
|
*/
|
|
682
|
-
const skills = (
|
|
691
|
+
const skills = usableSkills()
|
|
683
692
|
.filter((skill) =>
|
|
684
693
|
!query ||
|
|
685
694
|
skill.name.toLowerCase().includes(query) ||
|
|
@@ -730,6 +739,25 @@ function attachRows() {
|
|
|
730
739
|
note: plugin.description || "",
|
|
731
740
|
ref: { kind: "plugin", value: plugin.name, label: plugin.name },
|
|
732
741
|
}));
|
|
742
|
+
/**
|
|
743
|
+
* External agents first: naming one at the start of a message hands the
|
|
744
|
+
* whole message to it — Claude Code on the user's own account — so it is
|
|
745
|
+
* the one mention that changes who answers, and worth seeing before the
|
|
746
|
+
* rest. Offered only while the @ opens the message, the one place it
|
|
747
|
+
* routes; mid-sentence it would only be a word.
|
|
748
|
+
*/
|
|
749
|
+
const atStart = mentionAt >= 0 && !input.value.slice(0, mentionAt).trim();
|
|
750
|
+
const agents = atStart
|
|
751
|
+
? (offer.agents || [])
|
|
752
|
+
.filter((agent) => !query || agent.name.toLowerCase().includes(query))
|
|
753
|
+
.map((agent) => ({
|
|
754
|
+
type: "agent",
|
|
755
|
+
icon: "command",
|
|
756
|
+
label: "@" + agent.name,
|
|
757
|
+
note: t("mention.agentNote"),
|
|
758
|
+
ref: { kind: "agent", value: agent.name, label: agent.name },
|
|
759
|
+
}))
|
|
760
|
+
: [];
|
|
733
761
|
const files = (fileList.entries || []).map((entry) => ({
|
|
734
762
|
type: entry.dir ? "folder" : "file",
|
|
735
763
|
icon: entry.dir ? "folder" : "file",
|
|
@@ -746,6 +774,7 @@ function attachRows() {
|
|
|
746
774
|
* label on a list of one kind, which is noise.
|
|
747
775
|
*/
|
|
748
776
|
const sections = [
|
|
777
|
+
[t("mention.agents"), agents],
|
|
749
778
|
[t("add.skills"), skills],
|
|
750
779
|
[t("add.plugins"), plugins],
|
|
751
780
|
[t("mention.files"), files],
|
|
@@ -758,7 +787,7 @@ function attachRows() {
|
|
|
758
787
|
|
|
759
788
|
const connectors = (offer.connectors || []).length;
|
|
760
789
|
const plugins = (offer.plugins || []).length;
|
|
761
|
-
const skills = (
|
|
790
|
+
const skills = usableSkills().length;
|
|
762
791
|
return [
|
|
763
792
|
// What goes with the message: from this machine, first. Two rows for
|
|
764
793
|
// files and folders, because the dialogs behind them are two: only macOS
|
|
@@ -767,7 +796,7 @@ function attachRows() {
|
|
|
767
796
|
// which one opens before it opens is also how someone reaching for a
|
|
768
797
|
// screenshot stops reading at the first row.
|
|
769
798
|
{ type: "group", label: t("add.sectionAttach") },
|
|
770
|
-
{ type: "pick", kind: "files", icon: "
|
|
799
|
+
{ type: "pick", kind: "files", icon: "attach", label: t("add.attachFiles") },
|
|
771
800
|
{ type: "pick", kind: "folder", icon: "folder", label: t("add.attachFolders") },
|
|
772
801
|
{ type: "group", label: t("add.sectionMode") },
|
|
773
802
|
// The permission mode of the same name, from the place people look for a
|
|
@@ -813,6 +842,21 @@ function attachRows() {
|
|
|
813
842
|
sub: skills ? String(skills) : "",
|
|
814
843
|
into: true,
|
|
815
844
|
},
|
|
845
|
+
// What this person already set up in Claude Code or Codex and has not
|
|
846
|
+
// brought over yet. At the top level, one click from the +: a section
|
|
847
|
+
// two screens in is a section nobody goes looking for. Gone once there is
|
|
848
|
+
// nothing left to add.
|
|
849
|
+
...(foreignWaiting() > 0
|
|
850
|
+
? [{
|
|
851
|
+
type: "market",
|
|
852
|
+
kind: "imports",
|
|
853
|
+
icon: "import",
|
|
854
|
+
// Each tool's own mark: the row is about where these come from.
|
|
855
|
+
marks: ["claude", "codex"],
|
|
856
|
+
label: t("add.importFrom"),
|
|
857
|
+
sub: String(foreignWaiting()),
|
|
858
|
+
}]
|
|
859
|
+
: []),
|
|
816
860
|
];
|
|
817
861
|
}
|
|
818
862
|
|
|
@@ -821,7 +865,45 @@ function notChoice(row) {
|
|
|
821
865
|
return row.type === "group" || row.type === "sep";
|
|
822
866
|
}
|
|
823
867
|
|
|
868
|
+
/**
|
|
869
|
+
* Repaint the menu, gliding from the height it had to the one it needs.
|
|
870
|
+
*
|
|
871
|
+
* Every repaint goes through here — a new screen, a list that arrived from
|
|
872
|
+
* the server after the menu opened, a filter narrowing it — because any one
|
|
873
|
+
* of them that changed the height in a single frame was the jolt. A glide
|
|
874
|
+
* already under way is interrupted from wherever it got to, not restarted
|
|
875
|
+
* from where it began.
|
|
876
|
+
*/
|
|
877
|
+
let attachResize = null;
|
|
824
878
|
function paintAttachMenu() {
|
|
879
|
+
const visible = !attachMenu.hidden;
|
|
880
|
+
const before = visible ? attachMenu.offsetHeight : 0;
|
|
881
|
+
if (attachResize) {
|
|
882
|
+
attachResize.cancel();
|
|
883
|
+
attachResize = null;
|
|
884
|
+
}
|
|
885
|
+
paintAttachRows();
|
|
886
|
+
if (!visible || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
887
|
+
const after = attachMenu.offsetHeight;
|
|
888
|
+
if (Math.abs(after - before) < 2) return;
|
|
889
|
+
attachMenu.classList.add("resizing");
|
|
890
|
+
const glide = attachMenu.animate([{ height: before + "px" }, { height: after + "px" }], {
|
|
891
|
+
duration: 200,
|
|
892
|
+
easing: "cubic-bezier(.2,.8,.2,1)",
|
|
893
|
+
});
|
|
894
|
+
attachResize = glide;
|
|
895
|
+
const done = () => {
|
|
896
|
+
if (attachResize !== glide) return;
|
|
897
|
+
attachResize = null;
|
|
898
|
+
attachMenu.classList.remove("resizing");
|
|
899
|
+
};
|
|
900
|
+
glide.onfinish = done;
|
|
901
|
+
glide.oncancel = () => {
|
|
902
|
+
if (attachResize === glide || !attachResize) attachMenu.classList.remove("resizing");
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function paintAttachRows() {
|
|
825
907
|
const rows = attachRows();
|
|
826
908
|
if (attachCursor >= rows.length) attachCursor = Math.max(0, rows.length - 1);
|
|
827
909
|
// The list opens with the cursor at 0, and 0 is a heading whenever the list
|
|
@@ -849,7 +931,13 @@ function paintAttachMenu() {
|
|
|
849
931
|
"menu-item" + ((row.ref && referenced(row.ref)) || row.on ? " on" : "") + (i === attachCursor ? " cursor" : ""),
|
|
850
932
|
);
|
|
851
933
|
item.setAttribute("role", "button");
|
|
852
|
-
|
|
934
|
+
if (row.marks) {
|
|
935
|
+
const pair = el("span", "mark-pair");
|
|
936
|
+
for (const source of row.marks) pair.appendChild(vendorMark(source));
|
|
937
|
+
item.appendChild(pair);
|
|
938
|
+
} else {
|
|
939
|
+
item.appendChild(row.mark ? connectorMark(row.mark, row.label) : svgIcon(row.icon));
|
|
940
|
+
}
|
|
853
941
|
if (row.note) {
|
|
854
942
|
item.classList.add("stacked");
|
|
855
943
|
const stack = el("span", "stack");
|
|
@@ -1028,15 +1116,27 @@ async function withPreview(file) {
|
|
|
1028
1116
|
}
|
|
1029
1117
|
|
|
1030
1118
|
function showAttachView(view) {
|
|
1119
|
+
const from = attachView;
|
|
1120
|
+
const before = attachMenu.hidden ? 0 : attachMenu.offsetHeight;
|
|
1031
1121
|
attachView = view;
|
|
1032
1122
|
// No row is under the keyboard until a key moves there. Opening onto a
|
|
1033
1123
|
// highlighted first row, with the pointer highlighting a second, made the
|
|
1034
1124
|
// menu look like it had two selections and no rhythm.
|
|
1035
1125
|
attachCursor = -1;
|
|
1036
1126
|
paintAttachMenu();
|
|
1127
|
+
// Into a list slides in from the right, back out from the left, so the
|
|
1128
|
+
// menu reads as a stack of screens rather than one that changed its mind.
|
|
1129
|
+
if (before && from !== view) slideAttachBody(view === "root" ? "slide-back" : "slide-in");
|
|
1037
1130
|
attachMenu.focus();
|
|
1038
1131
|
}
|
|
1039
1132
|
|
|
1133
|
+
function slideAttachBody(name) {
|
|
1134
|
+
attachBody.classList.remove("slide-in", "slide-back");
|
|
1135
|
+
void attachBody.offsetWidth; // restart the animation
|
|
1136
|
+
attachBody.classList.add(name);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
|
|
1040
1140
|
// --- browsing connectors and plugins ---------------------------------------
|
|
1041
1141
|
/**
|
|
1042
1142
|
* What is on offer, in a dialog rather than in the menu.
|
|
@@ -1055,37 +1155,100 @@ let marketKind = "connectors";
|
|
|
1055
1155
|
const marketSearch = document.getElementById("mk-search");
|
|
1056
1156
|
marketSearch.addEventListener("input", () => paintMarket());
|
|
1057
1157
|
|
|
1158
|
+
/**
|
|
1159
|
+
* Which list the import tab sits beside. Bringing things in from Claude Code
|
|
1160
|
+
* and Codex is its own tab, not a section of the catalogue: those rows are
|
|
1161
|
+
* this person's own setup, and a shelf of them between "in this session" and
|
|
1162
|
+
* seventy vendors read as one more vendor list.
|
|
1163
|
+
*/
|
|
1164
|
+
let marketHome = "connectors";
|
|
1165
|
+
const marketTabs = document.getElementById("mk-tabs");
|
|
1166
|
+
|
|
1167
|
+
function paintMarketTabs() {
|
|
1168
|
+
const tabbed = marketKind === "connectors" || marketKind === "plugins" || marketKind === "imports";
|
|
1169
|
+
marketTabs.hidden = !tabbed;
|
|
1170
|
+
if (!tabbed) return;
|
|
1171
|
+
marketTabs.replaceChildren();
|
|
1172
|
+
const tab = (kind, build) => {
|
|
1173
|
+
const button = el("button", "mk-tab" + (marketKind === kind ? " on" : ""));
|
|
1174
|
+
button.type = "button";
|
|
1175
|
+
button.setAttribute("role", "tab");
|
|
1176
|
+
button.setAttribute("aria-selected", String(marketKind === kind));
|
|
1177
|
+
build(button);
|
|
1178
|
+
button.addEventListener("click", () => {
|
|
1179
|
+
if (marketKind !== kind) void openMarket(kind);
|
|
1180
|
+
});
|
|
1181
|
+
marketTabs.appendChild(button);
|
|
1182
|
+
};
|
|
1183
|
+
tab(marketHome, (button) => button.appendChild(document.createTextNode(t(marketHome === "plugins" ? "add.plugins" : "add.connectors"))));
|
|
1184
|
+
tab("imports", (button) => {
|
|
1185
|
+
button.appendChild(vendorMark("claude"));
|
|
1186
|
+
button.appendChild(vendorMark("codex"));
|
|
1187
|
+
button.appendChild(document.createTextNode(t("market.tab.import")));
|
|
1188
|
+
const waiting = foreignWaiting();
|
|
1189
|
+
if (waiting) button.appendChild(el("span", "mk-tab-count", String(waiting)));
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1058
1193
|
async function openMarket(kind) {
|
|
1194
|
+
if (kind === "connectors" || kind === "plugins") marketHome = kind;
|
|
1059
1195
|
marketKind = kind;
|
|
1060
1196
|
// Cleared on open: a query left over from the last screen would hide most of
|
|
1061
1197
|
// this one and look like an empty catalogue.
|
|
1062
1198
|
marketSearch.value = "";
|
|
1063
1199
|
marketSearch.placeholder = t("menu.search");
|
|
1064
|
-
const titles = { plugins: "add.plugins", skills: "add.skills", connectors: "add.connectors" };
|
|
1065
|
-
const notes = { plugins: "market.pluginsNote", skills: "market.skillsNote", connectors: "market.connectorsNote" };
|
|
1066
|
-
|
|
1200
|
+
const titles = { plugins: "add.plugins", skills: "add.skills", connectors: "add.connectors", imports: "add.importFrom" };
|
|
1201
|
+
const notes = { plugins: "market.pluginsNote", skills: "market.skillsNote", connectors: "market.connectorsNote", imports: "market.importsNote" };
|
|
1202
|
+
// The import tab belongs to the list it sits beside, so the dialog keeps
|
|
1203
|
+
// that list's name and only the sentence under it changes.
|
|
1204
|
+
document.getElementById("mk-title").textContent = t(titles[kind === "imports" ? marketHome : kind] || titles.connectors);
|
|
1205
|
+
paintMarketTabs();
|
|
1067
1206
|
document.getElementById("mk-note").textContent = t(notes[kind] || notes.connectors);
|
|
1068
|
-
|
|
1069
|
-
|
|
1207
|
+
// A tab switch keeps the old list up until the new one is ready: a
|
|
1208
|
+
// "Loading…" line in between collapses the dialog for a frame.
|
|
1209
|
+
const switching = marketDialog.open;
|
|
1210
|
+
if (!switching) marketList.replaceChildren(el("div", "mk-desc", t("market.loading")));
|
|
1211
|
+
marketDialog.classList.remove("closing");
|
|
1212
|
+
// Rows play their entrance once per opening, not on every repaint.
|
|
1213
|
+
marketDialog.classList.add("fresh");
|
|
1214
|
+
// Already open when one list links to another — the plugins screen
|
|
1215
|
+
// pointing at the import screen — and showModal on an open dialog throws.
|
|
1216
|
+
if (!marketDialog.open) marketDialog.showModal();
|
|
1070
1217
|
|
|
1071
1218
|
const context = await getJson("/context");
|
|
1072
1219
|
if (!context.error) offer = context;
|
|
1073
|
-
if (kind === "connectors") {
|
|
1220
|
+
if (kind === "connectors" || kind === "imports" || kind === "plugins") {
|
|
1074
1221
|
const body = await getJson("/registry");
|
|
1075
1222
|
market = body.error ? [] : body.connectors || [];
|
|
1223
|
+
foreignMarket = body.error ? [] : body.imports || [];
|
|
1224
|
+
}
|
|
1225
|
+
if (kind === "plugins") {
|
|
1226
|
+
confirming = null;
|
|
1227
|
+
const body = await getJson("/plugin-market");
|
|
1228
|
+
pluginMarket = body.error ? [] : body.plugins || [];
|
|
1076
1229
|
}
|
|
1230
|
+
paintMarketTabs();
|
|
1077
1231
|
paintMarket();
|
|
1232
|
+
if (switching) {
|
|
1233
|
+
marketList.classList.remove("swap");
|
|
1234
|
+
void marketList.offsetWidth;
|
|
1235
|
+
marketList.classList.add("swap");
|
|
1236
|
+
marketList.scrollTop = 0;
|
|
1237
|
+
}
|
|
1238
|
+
setTimeout(() => marketDialog.classList.remove("fresh"), 400);
|
|
1078
1239
|
}
|
|
1079
1240
|
|
|
1080
1241
|
function paintMarket() {
|
|
1081
1242
|
marketList.replaceChildren();
|
|
1082
1243
|
const query = (marketSearch.value || "").trim().toLowerCase();
|
|
1244
|
+
if (marketKind === "imports") return paintImports(query);
|
|
1083
1245
|
const hit = (entry) =>
|
|
1084
1246
|
!query ||
|
|
1085
1247
|
(entry.name || "").toLowerCase().includes(query) ||
|
|
1086
1248
|
(entry.title || "").toLowerCase().includes(query) ||
|
|
1087
1249
|
(entry.description || "").toLowerCase().includes(query) ||
|
|
1088
|
-
(entry.label || "").toLowerCase().includes(query)
|
|
1250
|
+
(entry.label || "").toLowerCase().includes(query) ||
|
|
1251
|
+
(entry.publisher || "").toLowerCase().includes(query);
|
|
1089
1252
|
const installed = (
|
|
1090
1253
|
marketKind === "plugins" ? offer.plugins || []
|
|
1091
1254
|
: marketKind === "skills" ? offer.skills || []
|
|
@@ -1099,10 +1262,28 @@ function paintMarket() {
|
|
|
1099
1262
|
marketList.appendChild(grid);
|
|
1100
1263
|
}
|
|
1101
1264
|
|
|
1102
|
-
|
|
1103
|
-
//
|
|
1104
|
-
|
|
1105
|
-
|
|
1265
|
+
|
|
1266
|
+
// Skills have nowhere to come from yet. Saying so beats an "Available"
|
|
1267
|
+
// heading over an empty space that reads as a failed load.
|
|
1268
|
+
const available =
|
|
1269
|
+
marketKind === "connectors" ? market.filter((one) => !one.installed && hit(one))
|
|
1270
|
+
: marketKind === "plugins" ? pluginMarket.filter((one) => !one.installed && hit(one))
|
|
1271
|
+
: [];
|
|
1272
|
+
if ((marketKind === "plugins" || marketKind === "connectors") && available.length) {
|
|
1273
|
+
// Seventy rows under one heading is a wall; under seven it is a shelf.
|
|
1274
|
+
// Connectors share the plugins' shelves, so a vendor sits on the same one
|
|
1275
|
+
// in both dialogs, plus one of their own for the servers run locally.
|
|
1276
|
+
const shelves = marketKind === "connectors" ? ["local", ...PLUGIN_CATEGORIES] : PLUGIN_CATEGORIES;
|
|
1277
|
+
const paint = marketKind === "connectors" ? offerRow : pluginOfferRow;
|
|
1278
|
+
for (const category of shelves) {
|
|
1279
|
+
const rows = available.filter((one) => (shelves.includes(one.category) ? one.category : "other") === category);
|
|
1280
|
+
if (!rows.length) continue;
|
|
1281
|
+
marketList.appendChild(el("div", "mk-group", t("market.cat." + category)));
|
|
1282
|
+
const grid = el("div", "mk-grid");
|
|
1283
|
+
for (const entry of rows) grid.appendChild(paint(entry));
|
|
1284
|
+
marketList.appendChild(grid);
|
|
1285
|
+
}
|
|
1286
|
+
} else if (available.length) {
|
|
1106
1287
|
marketList.appendChild(el("div", "mk-group", t("market.available")));
|
|
1107
1288
|
const grid = el("div", "mk-grid");
|
|
1108
1289
|
for (const entry of available) grid.appendChild(offerRow(entry));
|
|
@@ -1120,14 +1301,16 @@ function paintMarket() {
|
|
|
1120
1301
|
* browser's own placeholder, which is uglier than the fallback and arrives at
|
|
1121
1302
|
* a different moment in every browser.
|
|
1122
1303
|
*/
|
|
1123
|
-
function connectorMark(id, label) {
|
|
1304
|
+
function connectorMark(id, label, route = "/connector-icon", fallback) {
|
|
1124
1305
|
const letter = (label || id || "?").trim().charAt(0).toUpperCase();
|
|
1125
1306
|
// A stable hue per connector, so the tile is the same colour every time
|
|
1126
1307
|
// rather than a different one each render.
|
|
1127
1308
|
let hash = 0;
|
|
1128
1309
|
for (let i = 0; i < id.length; i++) hash = (hash * 31 + id.charCodeAt(i)) % 360;
|
|
1129
|
-
|
|
1130
|
-
|
|
1310
|
+
// A caller with a better placeholder than a letter — a plugin with no site
|
|
1311
|
+
// keeps the plugin glyph it always had — passes it in.
|
|
1312
|
+
const mono = fallback || el("span", "mk-mono", letter);
|
|
1313
|
+
if (!fallback) mono.style.background = "hsl(" + hash + " 42% 42%)";
|
|
1131
1314
|
|
|
1132
1315
|
const img = new Image();
|
|
1133
1316
|
img.className = "mk-logo";
|
|
@@ -1136,7 +1319,7 @@ function connectorMark(id, label) {
|
|
|
1136
1319
|
if (isDarkFlatGlyph(img)) img.classList.add("invert-dark");
|
|
1137
1320
|
mono.replaceWith(img);
|
|
1138
1321
|
});
|
|
1139
|
-
img.src = q("
|
|
1322
|
+
img.src = q(route + "?id=" + encodeURIComponent(id));
|
|
1140
1323
|
return mono;
|
|
1141
1324
|
}
|
|
1142
1325
|
|
|
@@ -1197,7 +1380,11 @@ function installedRow(entry) {
|
|
|
1197
1380
|
const row = el("div", "mk-item");
|
|
1198
1381
|
const isPlugin = marketKind === "plugins";
|
|
1199
1382
|
const isSkill = marketKind === "skills";
|
|
1200
|
-
row.appendChild(
|
|
1383
|
+
row.appendChild(
|
|
1384
|
+
isPlugin ? connectorMark(entry.name, entry.title || entry.name, "/plugin-icon", svgIcon("plugin"))
|
|
1385
|
+
: isSkill ? svgIcon("skill")
|
|
1386
|
+
: connectorMark(entry.name, entry.name),
|
|
1387
|
+
);
|
|
1201
1388
|
|
|
1202
1389
|
const naming = el("div", "mk-naming");
|
|
1203
1390
|
// A plugin may carry a display title over its id; the id is still what a
|
|
@@ -1215,11 +1402,18 @@ function installedRow(entry) {
|
|
|
1215
1402
|
// A runtime plugin says so in its source; a skill carries its own flag. Reading
|
|
1216
1403
|
// only one of them is how the two that ship here — memory and the media
|
|
1217
1404
|
// plugin — ended up as the only rows in the panel with no provenance at all.
|
|
1405
|
+
// Claude Code's and Codex's are named for the tool that put them there:
|
|
1406
|
+
// that is where one gets fixed, and where removing it would take it from.
|
|
1218
1407
|
const tag =
|
|
1219
1408
|
entry.builtin || entry.source === "builtin" ? "built-in"
|
|
1220
1409
|
: entry.source === "runtime" ? "runtime"
|
|
1221
1410
|
: "";
|
|
1222
1411
|
if (tag) heading.appendChild(el("span", "mk-tag", tag));
|
|
1412
|
+
if (entry.source === "claude" || entry.source === "codex") heading.appendChild(vendorChip(entry.source));
|
|
1413
|
+
if (isSkill && entry.disabled) {
|
|
1414
|
+
row.classList.add("off");
|
|
1415
|
+
heading.appendChild(el("span", "mk-tag", t("market.off")));
|
|
1416
|
+
}
|
|
1223
1417
|
naming.appendChild(heading);
|
|
1224
1418
|
// A skill has nothing to connect to and nothing to be offline from, so the
|
|
1225
1419
|
// slot that carries a connector's state carries what the skill is for —
|
|
@@ -1233,7 +1427,11 @@ function installedRow(entry) {
|
|
|
1233
1427
|
: entry.needsAuth
|
|
1234
1428
|
? t("add.needsAuth")
|
|
1235
1429
|
: entry.error || t("add.offline");
|
|
1236
|
-
if (detail)
|
|
1430
|
+
if (detail) {
|
|
1431
|
+
const line = el("div", "mk-desc", detail);
|
|
1432
|
+
line.title = detail;
|
|
1433
|
+
naming.appendChild(line);
|
|
1434
|
+
}
|
|
1237
1435
|
row.appendChild(naming);
|
|
1238
1436
|
|
|
1239
1437
|
const acts = el("div", "mk-acts");
|
|
@@ -1241,22 +1439,42 @@ function installedRow(entry) {
|
|
|
1241
1439
|
// and repeating it on every row distinguishes nothing. What is worth saying
|
|
1242
1440
|
// per row is the part that would otherwise surprise someone: this plugin
|
|
1243
1441
|
// declares servers, and they are not running.
|
|
1244
|
-
const
|
|
1442
|
+
const here = new Set((offer.connectors || []).map((one) => one.name));
|
|
1443
|
+
const idle = isPlugin && (entry.source === "claude" || entry.source === "codex")
|
|
1444
|
+
? (entry.servers || []).filter((name) => !here.has(name)).length
|
|
1445
|
+
: 0;
|
|
1245
1446
|
// Two keys rather than a plural engine: this is the only place in the page
|
|
1246
1447
|
// where the count is usually one, and "1 servers" is worth the extra string.
|
|
1448
|
+
// A button, because the next question is always "then how do I start it".
|
|
1247
1449
|
if (idle) {
|
|
1248
1450
|
const said = idle === 1 ? t("market.serverIdle") : t("market.serversIdle", { n: idle });
|
|
1249
|
-
|
|
1451
|
+
const go = el("button", "mk-btn", said);
|
|
1452
|
+
go.type = "button";
|
|
1453
|
+
go.title = t("add.importFrom");
|
|
1454
|
+
go.addEventListener("click", () => void openMarket("imports"));
|
|
1455
|
+
acts.appendChild(go);
|
|
1250
1456
|
}
|
|
1251
1457
|
const ref = { kind: isPlugin ? "plugin" : isSkill ? "skill" : "connector", value: entry.name, label: entry.name };
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
attach.
|
|
1258
|
-
|
|
1259
|
-
|
|
1458
|
+
// A skill that is off cannot be loaded, so it cannot be attached either.
|
|
1459
|
+
if (!(isSkill && entry.disabled)) {
|
|
1460
|
+
const attach = el("button", "mk-icon-btn", referenced(ref) ? "✓" : "+");
|
|
1461
|
+
attach.type = "button";
|
|
1462
|
+
attach.title = t("market.attach");
|
|
1463
|
+
attach.addEventListener("click", () => {
|
|
1464
|
+
toggleRef(ref);
|
|
1465
|
+
attach.textContent = referenced(ref) ? "✓" : "+";
|
|
1466
|
+
});
|
|
1467
|
+
acts.appendChild(attach);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// Off rather than removed: most of these are files another tool put on
|
|
1471
|
+
// disk, and deleting one to hide it here would take it from that tool too.
|
|
1472
|
+
if (isSkill) {
|
|
1473
|
+
const toggle = el("button", "mk-btn", t(entry.disabled ? "market.enable" : "market.disable"));
|
|
1474
|
+
toggle.type = "button";
|
|
1475
|
+
toggle.addEventListener("click", () => void setSkill(entry.name, Boolean(entry.disabled)));
|
|
1476
|
+
acts.appendChild(toggle);
|
|
1477
|
+
}
|
|
1260
1478
|
|
|
1261
1479
|
// A connector that asked to be signed in to gets the one button that moves
|
|
1262
1480
|
// it forward, in front of the one that removes it.
|
|
@@ -1272,8 +1490,17 @@ function installedRow(entry) {
|
|
|
1272
1490
|
acts.appendChild(go);
|
|
1273
1491
|
}
|
|
1274
1492
|
|
|
1275
|
-
//
|
|
1276
|
-
//
|
|
1493
|
+
// A plugin this menu installed can be taken away by it. Any other plugin is
|
|
1494
|
+
// a directory somebody put on disk, and a skill is a file in one; neither is
|
|
1495
|
+
// undone from a composer menu.
|
|
1496
|
+
if (isPlugin && entry.market) {
|
|
1497
|
+
const drop = el("button", "mk-icon-btn drop", "×");
|
|
1498
|
+
drop.type = "button";
|
|
1499
|
+
drop.title = t("add.remove");
|
|
1500
|
+
drop.disabled = Boolean(installing);
|
|
1501
|
+
drop.addEventListener("click", () => void installPlugin(entry.market, true));
|
|
1502
|
+
acts.appendChild(drop);
|
|
1503
|
+
}
|
|
1277
1504
|
if (!isPlugin && !isSkill) {
|
|
1278
1505
|
const drop = el("button", "mk-icon-btn drop", "×");
|
|
1279
1506
|
drop.type = "button";
|
|
@@ -1309,6 +1536,250 @@ function offerRow(entry) {
|
|
|
1309
1536
|
return row;
|
|
1310
1537
|
}
|
|
1311
1538
|
|
|
1539
|
+
/** Claude Code's or Codex's app icon; see tool-icons.ts. */
|
|
1540
|
+
function vendorMark(source, size) {
|
|
1541
|
+
const mark = el("span", "vendor-mark");
|
|
1542
|
+
mark.setAttribute("aria-hidden", "true");
|
|
1543
|
+
if (size) mark.style.width = mark.style.height = size + "px";
|
|
1544
|
+
const img = document.createElement("img");
|
|
1545
|
+
img.alt = "";
|
|
1546
|
+
img.decoding = "async";
|
|
1547
|
+
img.src = q("/tool-icon?id=" + (source === "codex" ? "codex" : "claude"));
|
|
1548
|
+
mark.appendChild(img);
|
|
1549
|
+
return mark;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
/** "Claude Code" or "Codex" with its mark, where a row says whose it is. */
|
|
1553
|
+
function vendorChip(source) {
|
|
1554
|
+
const chip = el("span", "vendor-chip");
|
|
1555
|
+
chip.appendChild(vendorMark(source));
|
|
1556
|
+
chip.appendChild(document.createTextNode(source === "codex" ? "Codex" : "Claude Code"));
|
|
1557
|
+
return chip;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
/**
|
|
1561
|
+
* One row per server name. The same server set up in both tools — or twice
|
|
1562
|
+
* in one — is one thing to add, so it is listed once, under both tools'
|
|
1563
|
+
* marks, and the declaration that would work is the one the button adds.
|
|
1564
|
+
*/
|
|
1565
|
+
function mergeForeign(list) {
|
|
1566
|
+
const usable = (one) => !one.reason && !(one.missingEnv || []).length;
|
|
1567
|
+
const byName = new Map();
|
|
1568
|
+
for (const one of list) {
|
|
1569
|
+
const key = one.scope === "plugin" ? "plugin:" + one.plugin + ":" + one.name : one.name;
|
|
1570
|
+
const group = byName.get(key) || [];
|
|
1571
|
+
group.push(one);
|
|
1572
|
+
byName.set(key, group);
|
|
1573
|
+
}
|
|
1574
|
+
return [...byName.values()].map((group) => {
|
|
1575
|
+
const best = group.find(usable) || group[0];
|
|
1576
|
+
return { ...best, sources: [...new Set(group.map((one) => one.source))] };
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/**
|
|
1581
|
+
* Everything worth bringing over from Claude Code and Codex, on one screen.
|
|
1582
|
+
*
|
|
1583
|
+
* Skills, agents and plugins are read where they lie and need nothing done,
|
|
1584
|
+
* so the only rows are servers: those in the two tools' own config files
|
|
1585
|
+
* first, then those inside their plugins, under the plugin that brought each.
|
|
1586
|
+
*/
|
|
1587
|
+
function paintImports(query) {
|
|
1588
|
+
const hit = (one) =>
|
|
1589
|
+
!query ||
|
|
1590
|
+
one.name.toLowerCase().includes(query) ||
|
|
1591
|
+
(one.pluginTitle || one.plugin || "").toLowerCase().includes(query) ||
|
|
1592
|
+
(one.target || "").toLowerCase().includes(query);
|
|
1593
|
+
const here = new Set((offer.connectors || []).map((one) => one.name));
|
|
1594
|
+
const waiting = mergeForeign(foreignMarket.filter((one) => !one.installed && !here.has(one.name) && hit(one)));
|
|
1595
|
+
|
|
1596
|
+
const files = waiting.filter((one) => one.scope !== "plugin");
|
|
1597
|
+
if (files.length) {
|
|
1598
|
+
marketList.appendChild(el("div", "mk-group", t("market.fromConfig")));
|
|
1599
|
+
const grid = el("div", "mk-grid");
|
|
1600
|
+
for (const entry of files) grid.appendChild(foreignRow(entry));
|
|
1601
|
+
marketList.appendChild(grid);
|
|
1602
|
+
}
|
|
1603
|
+
const bundled = waiting.filter((one) => one.scope === "plugin");
|
|
1604
|
+
if (bundled.length) {
|
|
1605
|
+
marketList.appendChild(el("div", "mk-group", t("market.fromPlugins")));
|
|
1606
|
+
const grid = el("div", "mk-grid");
|
|
1607
|
+
for (const entry of bundled) grid.appendChild(foreignRow(entry));
|
|
1608
|
+
marketList.appendChild(grid);
|
|
1609
|
+
}
|
|
1610
|
+
if (!files.length && !bundled.length) {
|
|
1611
|
+
marketList.appendChild(el("div", "mk-desc mk-empty", t(query ? "market.empty" : "market.allImported")));
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
/**
|
|
1616
|
+
* One server from Claude Code's or Codex's config.
|
|
1617
|
+
*
|
|
1618
|
+
* Offered, never started on its own: it is a process, and often a key, the
|
|
1619
|
+
* person set up for another program. The row says where it was found and
|
|
1620
|
+
* what it reaches, and — when the file has a key written out in it — that
|
|
1621
|
+
* adding it copies that key, because that is the one consequence of the
|
|
1622
|
+
* click a reader would not guess.
|
|
1623
|
+
*/
|
|
1624
|
+
function foreignRow(entry) {
|
|
1625
|
+
const row = el("div", "mk-item");
|
|
1626
|
+
row.appendChild(connectorMark(entry.name, entry.name));
|
|
1627
|
+
|
|
1628
|
+
const naming = el("div", "mk-naming");
|
|
1629
|
+
const heading = el("div", "mk-name", entry.name);
|
|
1630
|
+
for (const source of entry.sources || [entry.source]) heading.appendChild(vendorChip(source));
|
|
1631
|
+
naming.appendChild(heading);
|
|
1632
|
+
const missing = entry.missingEnv || [];
|
|
1633
|
+
const problem = entry.reason
|
|
1634
|
+
? t("market.reason." + entry.reason)
|
|
1635
|
+
: missing.length
|
|
1636
|
+
? t("market.needsEnv", { names: missing.join(", ") })
|
|
1637
|
+
: "";
|
|
1638
|
+
const where = [
|
|
1639
|
+
entry.scope === "plugin" ? t("market.scope.inPlugin", { name: entry.pluginTitle || entry.plugin }) : t("market.scope." + entry.scope),
|
|
1640
|
+
entry.target,
|
|
1641
|
+
].filter(Boolean).join(" · ");
|
|
1642
|
+
naming.appendChild(el("div", problem ? "mk-desc warn" : "mk-desc", problem || where));
|
|
1643
|
+
if (!problem && entry.inlineSecret) naming.appendChild(el("div", "mk-desc", t("market.inlineSecret")));
|
|
1644
|
+
row.title = entry.file;
|
|
1645
|
+
row.appendChild(naming);
|
|
1646
|
+
|
|
1647
|
+
const acts = el("div", "mk-acts");
|
|
1648
|
+
const key = foreignKey(entry);
|
|
1649
|
+
// A row that cannot be added says why in its text; the button steps back
|
|
1650
|
+
// rather than sitting there as a bright block that does nothing.
|
|
1651
|
+
const add = el("button", problem ? "mk-btn" : "mk-btn go", t(key === adding ? "market.importing" : "market.import"));
|
|
1652
|
+
add.type = "button";
|
|
1653
|
+
add.disabled = Boolean(adding) || Boolean(problem);
|
|
1654
|
+
add.addEventListener("click", () => void importConnector(entry));
|
|
1655
|
+
acts.appendChild(add);
|
|
1656
|
+
row.appendChild(acts);
|
|
1657
|
+
return row;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
/** Add one found in Claude Code's or Codex's config, by where it was found. */
|
|
1661
|
+
function foreignKey(entry) {
|
|
1662
|
+
return [entry.source, entry.scope, entry.plugin || "", entry.name].join(":");
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
async function importConnector(entry) {
|
|
1666
|
+
if (adding) return;
|
|
1667
|
+
adding = foreignKey(entry);
|
|
1668
|
+
paintMarket();
|
|
1669
|
+
const body = await api(
|
|
1670
|
+
"/connector",
|
|
1671
|
+
{ import: { source: entry.source, scope: entry.scope, name: entry.name, ...(entry.plugin ? { plugin: entry.plugin } : {}) } },
|
|
1672
|
+
3 * 60_000,
|
|
1673
|
+
);
|
|
1674
|
+
adding = null;
|
|
1675
|
+
if (body.error) {
|
|
1676
|
+
addNotice(body.error, "err");
|
|
1677
|
+
return void paintMarket();
|
|
1678
|
+
}
|
|
1679
|
+
if (body.connectors) offer = { connectors: body.connectors, plugins: body.plugins || [], skills: body.skills || [], agents: body.agents || [] };
|
|
1680
|
+
// By name: the same server is often in both tools, and once one is here
|
|
1681
|
+
// the other would be a second copy of it.
|
|
1682
|
+
for (const one of foreignMarket) if (one.name === entry.name) one.installed = true;
|
|
1683
|
+
for (const one of market) if (one.id === entry.name) one.installed = true;
|
|
1684
|
+
paintMarketTabs();
|
|
1685
|
+
paintMarket();
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
/** Switch a skill off or back on. */
|
|
1689
|
+
async function setSkill(name, enabled) {
|
|
1690
|
+
const body = await api("/skill", { name: name, enabled: enabled });
|
|
1691
|
+
if (body.error) return void addNotice(body.error, "err");
|
|
1692
|
+
if (body.skills) offer = { connectors: body.connectors || [], plugins: body.plugins || [], skills: body.skills, agents: body.agents || [] };
|
|
1693
|
+
paintMarket();
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
/** The shelves the plugin list is cut into, in the order they are shown. */
|
|
1697
|
+
const PLUGIN_CATEGORIES = ["build", "data", "monitor", "dev", "web", "design", "work", "other"];
|
|
1698
|
+
|
|
1699
|
+
/**
|
|
1700
|
+
* One plugin on offer.
|
|
1701
|
+
*
|
|
1702
|
+
* Installing downloads somebody else's repository and, for a plugin with
|
|
1703
|
+
* servers, connects to them — so the first click says exactly that, and only
|
|
1704
|
+
* the second one does it. A dialog would say the same thing further from the
|
|
1705
|
+
* row it is about.
|
|
1706
|
+
*/
|
|
1707
|
+
function pluginOfferRow(entry) {
|
|
1708
|
+
const row = el("div", "mk-item");
|
|
1709
|
+
row.appendChild(connectorMark(entry.id, entry.label, "/plugin-icon", svgIcon("plugin")));
|
|
1710
|
+
|
|
1711
|
+
const naming = el("div", "mk-naming");
|
|
1712
|
+
const heading = el("div", "mk-name", entry.label);
|
|
1713
|
+
if (entry.publisher && entry.publisher !== entry.label) heading.appendChild(el("span", "mk-tag", entry.publisher));
|
|
1714
|
+
naming.appendChild(heading);
|
|
1715
|
+
const asking = confirming === entry.id;
|
|
1716
|
+
naming.appendChild(el("div", asking ? "mk-desc warn" : "mk-desc", asking ? consent(entry) : entry.description));
|
|
1717
|
+
if (!asking) naming.appendChild(el("div", "mk-desc", provides(entry.provides || {})));
|
|
1718
|
+
row.appendChild(naming);
|
|
1719
|
+
|
|
1720
|
+
const acts = el("div", "mk-acts");
|
|
1721
|
+
const label = installing === entry.id ? "market.installing" : asking ? "market.confirm" : "market.install";
|
|
1722
|
+
const add = el("button", "mk-btn go", t(label));
|
|
1723
|
+
add.type = "button";
|
|
1724
|
+
add.disabled = Boolean(installing);
|
|
1725
|
+
add.addEventListener("click", () => {
|
|
1726
|
+
if (!asking) {
|
|
1727
|
+
confirming = entry.id;
|
|
1728
|
+
return void paintMarket();
|
|
1729
|
+
}
|
|
1730
|
+
void installPlugin(entry.id, false);
|
|
1731
|
+
});
|
|
1732
|
+
acts.appendChild(add);
|
|
1733
|
+
if (asking && !installing) {
|
|
1734
|
+
const cancel = el("button", "mk-btn", t("market.cancel"));
|
|
1735
|
+
cancel.type = "button";
|
|
1736
|
+
cancel.addEventListener("click", () => {
|
|
1737
|
+
confirming = null;
|
|
1738
|
+
paintMarket();
|
|
1739
|
+
});
|
|
1740
|
+
acts.appendChild(cancel);
|
|
1741
|
+
}
|
|
1742
|
+
row.appendChild(acts);
|
|
1743
|
+
return row;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
/** "37 skills · 3 agents · 1 connector", from what the list says the plugin holds. */
|
|
1747
|
+
function provides(what) {
|
|
1748
|
+
const parts = [];
|
|
1749
|
+
if (what.skills) parts.push(t(what.skills === 1 ? "market.oneSkill" : "market.skills", { n: what.skills }));
|
|
1750
|
+
if (what.agents) parts.push(t(what.agents === 1 ? "market.oneAgent" : "market.agents", { n: what.agents }));
|
|
1751
|
+
const servers = (what.servers || []).length;
|
|
1752
|
+
if (servers) parts.push(t(servers === 1 ? "market.oneServer" : "market.servers", { n: servers }));
|
|
1753
|
+
return parts.join(" · ");
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
/** What the second click agrees to, said before it is made. */
|
|
1757
|
+
function consent(entry) {
|
|
1758
|
+
const servers = (entry.provides && entry.provides.servers) || [];
|
|
1759
|
+
return t(servers.length ? "market.consentServers" : "market.consent", {
|
|
1760
|
+
publisher: entry.publisher || entry.label,
|
|
1761
|
+
servers: servers.join(", "),
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
/** Install a plugin from the list, or remove one it installed. */
|
|
1766
|
+
async function installPlugin(id, remove) {
|
|
1767
|
+
if (installing) return;
|
|
1768
|
+
installing = remove ? null : id;
|
|
1769
|
+
confirming = null;
|
|
1770
|
+
paintMarket();
|
|
1771
|
+
const body = await api("/plugin-market", { id: id, remove: remove === true }, 5 * 60_000);
|
|
1772
|
+
installing = null;
|
|
1773
|
+
if (body.error) {
|
|
1774
|
+
addNotice(body.error, "err");
|
|
1775
|
+
return void paintMarket();
|
|
1776
|
+
}
|
|
1777
|
+
if (body.plugins) offer = { connectors: body.connectors || [], plugins: body.plugins, skills: body.skills || [], agents: body.agents || [] };
|
|
1778
|
+
for (const entry of pluginMarket) if (entry.id === id) entry.installed = remove !== true;
|
|
1779
|
+
if (!remove) addNotice(t("market.installedNotice", { name: (pluginMarket.find((one) => one.id === id) || { label: id }).label }));
|
|
1780
|
+
paintMarket();
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1312
1783
|
/**
|
|
1313
1784
|
* Add a connector, or take one away.
|
|
1314
1785
|
*
|
|
@@ -1326,7 +1797,7 @@ async function installConnector(id, remove) {
|
|
|
1326
1797
|
addNotice(body.error, "err");
|
|
1327
1798
|
return void paintMarket();
|
|
1328
1799
|
}
|
|
1329
|
-
if (body.connectors) offer = { connectors: body.connectors, plugins: body.plugins || [], skills: body.skills || [] };
|
|
1800
|
+
if (body.connectors) offer = { connectors: body.connectors, plugins: body.plugins || [], skills: body.skills || [], agents: body.agents || [] };
|
|
1330
1801
|
for (const entry of market) if (entry.id === id) entry.installed = remove !== true;
|
|
1331
1802
|
paintMarket();
|
|
1332
1803
|
}
|
|
@@ -1357,7 +1828,23 @@ async function authorizeConnector(id) {
|
|
|
1357
1828
|
|
|
1358
1829
|
document.getElementById("mk-close").appendChild(svgIcon("close"));
|
|
1359
1830
|
document.getElementById("mk-close").setAttribute("aria-label", t("market.close"));
|
|
1360
|
-
document.getElementById("mk-close").addEventListener("click", () =>
|
|
1831
|
+
document.getElementById("mk-close").addEventListener("click", () => closeMarket());
|
|
1832
|
+
// Escape closes it too, and should leave the same way the button does.
|
|
1833
|
+
marketDialog.addEventListener("cancel", (e) => {
|
|
1834
|
+
e.preventDefault();
|
|
1835
|
+
closeMarket();
|
|
1836
|
+
});
|
|
1837
|
+
|
|
1838
|
+
/** Fade the dialog out before it goes, rather than vanishing mid-frame. */
|
|
1839
|
+
function closeMarket() {
|
|
1840
|
+
if (!marketDialog.open || marketDialog.classList.contains("closing")) return;
|
|
1841
|
+
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return void marketDialog.close();
|
|
1842
|
+
marketDialog.classList.add("closing");
|
|
1843
|
+
setTimeout(() => {
|
|
1844
|
+
marketDialog.classList.remove("closing");
|
|
1845
|
+
marketDialog.close();
|
|
1846
|
+
}, 140);
|
|
1847
|
+
}
|
|
1361
1848
|
|
|
1362
1849
|
async function loadMentionFiles() {
|
|
1363
1850
|
const seq = ++attachSeq;
|
|
@@ -1416,6 +1903,22 @@ function openAttachMenu() {
|
|
|
1416
1903
|
offer = body;
|
|
1417
1904
|
paintAttachMenu();
|
|
1418
1905
|
});
|
|
1906
|
+
// For the import row's count; the dialog fetches its own. Kept between
|
|
1907
|
+
// opens, so after the first the row is there from the first frame.
|
|
1908
|
+
void getJson("/registry").then((body) => {
|
|
1909
|
+
if (body.error) return;
|
|
1910
|
+
foreignMarket = body.imports || [];
|
|
1911
|
+
if (!attachMenu.hidden && attachView === "root") paintAttachMenu();
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
/**
|
|
1916
|
+
* How many servers from Claude Code and Codex could be added now: not here
|
|
1917
|
+
* already, usable, and counted once when both tools declare the same one.
|
|
1918
|
+
*/
|
|
1919
|
+
function foreignWaiting() {
|
|
1920
|
+
const here = new Set((offer.connectors || []).map((one) => one.name));
|
|
1921
|
+
return new Set(foreignMarket.filter((one) => !one.installed && !one.reason && !here.has(one.name)).map((one) => one.name)).size;
|
|
1419
1922
|
}
|
|
1420
1923
|
|
|
1421
1924
|
/**
|
|
@@ -1475,10 +1978,18 @@ function moveAttachCursor(e) {
|
|
|
1475
1978
|
|
|
1476
1979
|
const inputMirror = document.getElementById("input-mirror");
|
|
1477
1980
|
|
|
1981
|
+
/**
|
|
1982
|
+
* The skills the model can load. The offer also carries the ones switched
|
|
1983
|
+
* off, for the list that switches them back on; nothing else should see them.
|
|
1984
|
+
*/
|
|
1985
|
+
function usableSkills() {
|
|
1986
|
+
return (offer.skills || []).filter((skill) => !skill.disabled);
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1478
1989
|
/** Every name an @ can stand for. */
|
|
1479
1990
|
function mentionNames() {
|
|
1480
1991
|
return new Map([
|
|
1481
|
-
...(
|
|
1992
|
+
...usableSkills().map((skill) => [skill.name, "skill"]),
|
|
1482
1993
|
...(offer.plugins || []).map((plugin) => [plugin.name, "plugin"]),
|
|
1483
1994
|
]);
|
|
1484
1995
|
}
|
|
@@ -1549,7 +2060,10 @@ function paintMarks() {
|
|
|
1549
2060
|
ensureOffer();
|
|
1550
2061
|
const names = mentionNames();
|
|
1551
2062
|
inputMirror.replaceChildren();
|
|
1552
|
-
if (!names.size)
|
|
2063
|
+
if (!names.size) {
|
|
2064
|
+
inputMirror.textContent = input.value;
|
|
2065
|
+
return void paintGhost();
|
|
2066
|
+
}
|
|
1553
2067
|
|
|
1554
2068
|
const text = input.value;
|
|
1555
2069
|
let at = 0;
|
|
@@ -1570,6 +2084,7 @@ function paintMarks() {
|
|
|
1570
2084
|
}
|
|
1571
2085
|
inputMirror.appendChild(document.createTextNode(text.slice(at)));
|
|
1572
2086
|
if (inputMirror.querySelector("mark")) inputMirror.style.setProperty("--at-w", atWidth() + "px");
|
|
2087
|
+
paintGhost();
|
|
1573
2088
|
}
|
|
1574
2089
|
|
|
1575
2090
|
// --- the caret ------------------------------------------------------------
|
|
@@ -1632,7 +2147,10 @@ for (const event of ["input", "focus", "blur", "scroll"]) {
|
|
|
1632
2147
|
input.addEventListener(event, placeCaret);
|
|
1633
2148
|
}
|
|
1634
2149
|
document.addEventListener("selectionchange", () => {
|
|
1635
|
-
if (document.activeElement
|
|
2150
|
+
if (document.activeElement !== input) return;
|
|
2151
|
+
placeCaret();
|
|
2152
|
+
// The rest of a guess is only offered from the end of the text.
|
|
2153
|
+
paintGhost();
|
|
1636
2154
|
});
|
|
1637
2155
|
window.addEventListener("resize", placeCaret);
|
|
1638
2156
|
|
|
@@ -2029,6 +2547,82 @@ input.addEventListener("keydown", (e) => {
|
|
|
2029
2547
|
input.addEventListener("input", () => {
|
|
2030
2548
|
autosize();
|
|
2031
2549
|
paintMirror();
|
|
2550
|
+
paintSuggestion();
|
|
2551
|
+
});
|
|
2552
|
+
|
|
2553
|
+
// --- the next-message guess -----------------------------------------------
|
|
2554
|
+
|
|
2555
|
+
/**
|
|
2556
|
+
* The server's guess at what this conversation's user types next, drawn as
|
|
2557
|
+
* the empty box's placeholder and taken whole with Tab.
|
|
2558
|
+
*
|
|
2559
|
+
* Held per pane, so a guess written for a background tab waits there rather
|
|
2560
|
+
* than showing on the one on screen. Shown only where it can be acted on: an
|
|
2561
|
+
* idle conversation, nothing typed, and no goal or worktree mode, whose own
|
|
2562
|
+
* placeholder says what the next message will be sent as.
|
|
2563
|
+
*/
|
|
2564
|
+
function suggestionNow() {
|
|
2565
|
+
const here = panes.get(activeId);
|
|
2566
|
+
if (!here || here.busy || staged.length || refs.length || goalMode || worktreeMode) return "";
|
|
2567
|
+
return here.suggestion || "";
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
/** The whole guess, while the box is empty. */
|
|
2571
|
+
function suggestionHere() {
|
|
2572
|
+
return input.value ? "" : suggestionNow();
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
/**
|
|
2576
|
+
* What is left of the guess once some of it has been typed: the text so far
|
|
2577
|
+
* has to be its start, letter for letter, and the caret at the end with
|
|
2578
|
+
* nothing selected — anywhere else, Tab finishing a sentence the caret is not
|
|
2579
|
+
* at would put words somewhere the user is not looking.
|
|
2580
|
+
*/
|
|
2581
|
+
function ghostTail() {
|
|
2582
|
+
const typed = input.value;
|
|
2583
|
+
const guess = suggestionNow();
|
|
2584
|
+
if (!typed || !guess || guess.length <= typed.length || !guess.startsWith(typed)) return "";
|
|
2585
|
+
if (input.selectionStart !== typed.length || input.selectionEnd !== typed.length) return "";
|
|
2586
|
+
return guess.slice(typed.length);
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
/**
|
|
2590
|
+
* Draw the rest of the guess after the text, on the mirror under the field.
|
|
2591
|
+
* The mirror lays out exactly as the field does, so the tail starts where the
|
|
2592
|
+
* last typed glyph ends.
|
|
2593
|
+
*/
|
|
2594
|
+
function paintGhost() {
|
|
2595
|
+
if (!inputMirror) return;
|
|
2596
|
+
const old = inputMirror.querySelector(".ghost");
|
|
2597
|
+
if (old) old.remove();
|
|
2598
|
+
const tail = ghostTail();
|
|
2599
|
+
if (tail) inputMirror.appendChild(el("span", "ghost", tail));
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
function paintSuggestion() {
|
|
2603
|
+
const guess = suggestionHere();
|
|
2604
|
+
input.placeholder = guess || inputPlaceholder();
|
|
2605
|
+
input.classList.toggle("suggesting", !!guess);
|
|
2606
|
+
paintGhost();
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
input.addEventListener("keydown", (e) => {
|
|
2610
|
+
if ((e.key !== "Tab" && e.key !== "Escape") || e.shiftKey || e.altKey || e.metaKey || e.ctrlKey || e.isComposing) return;
|
|
2611
|
+
const tail = ghostTail();
|
|
2612
|
+
const guess = tail ? input.value + tail : suggestionHere();
|
|
2613
|
+
if (!guess) return;
|
|
2614
|
+
e.preventDefault();
|
|
2615
|
+
e.stopImmediatePropagation();
|
|
2616
|
+
if (e.key === "Escape") {
|
|
2617
|
+
panes.get(activeId).suggestion = null;
|
|
2618
|
+
paintGhost();
|
|
2619
|
+
return void paintSuggestion();
|
|
2620
|
+
}
|
|
2621
|
+
input.value = guess;
|
|
2622
|
+
input.setSelectionRange(guess.length, guess.length);
|
|
2623
|
+
autosize();
|
|
2624
|
+
paintMirror();
|
|
2625
|
+
paintSuggestion();
|
|
2032
2626
|
});
|
|
2033
2627
|
|
|
2034
2628
|
// --- the field's height ---------------------------------------------------
|