@oharness/server 0.10.0 → 0.11.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 +152 -8
- package/dist/i18n.js.map +1 -1
- package/dist/icons.js +1 -0
- 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 +40 -16
- package/dist/page/client/base.js.map +1 -1
- package/dist/page/client/browser.js +1 -1
- package/dist/page/client/composer.js +111 -7
- 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 +183 -22
- package/dist/page/style.js.map +1 -1
- package/dist/page.js +4 -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 +465 -42
- 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 +28 -2
- package/dist/terminal.js +45 -3
- 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/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
|
@@ -275,8 +275,8 @@ async function showRoutines() {
|
|
|
275
275
|
|
|
276
276
|
const remove = el("button", "ghost", t("routines.remove"));
|
|
277
277
|
remove.type = "button";
|
|
278
|
-
remove.addEventListener("click", () => {
|
|
279
|
-
if (!
|
|
278
|
+
remove.addEventListener("click", async () => {
|
|
279
|
+
if (!(await askConfirm({ body: t("routines.removeConfirm", { name: routine.name }), ok: t("routines.remove"), danger: true }))) return;
|
|
280
280
|
remove.disabled = true;
|
|
281
281
|
void api("/routine/remove", { name: routine.name }).then((result) => {
|
|
282
282
|
pushNotice(result);
|
|
@@ -315,7 +315,10 @@ async function showRoutines() {
|
|
|
315
315
|
* because a switch you have to open a row to reach is a switch that has been
|
|
316
316
|
* hidden rather than tidied.
|
|
317
317
|
*/
|
|
318
|
-
async function showSettings() {
|
|
318
|
+
async function showSettings(opts) {
|
|
319
|
+
// A row whose controls live inside it is reopened after a change redraws
|
|
320
|
+
// the page; called from a click handler, opts is the event, and opens none.
|
|
321
|
+
const opened = opts && typeof opts.open === "string" ? opts.open : undefined;
|
|
319
322
|
const settings = await getJson("/settings");
|
|
320
323
|
panelBody.replaceChildren();
|
|
321
324
|
if (settings.error) return void panelBody.appendChild(panelError(settings.error));
|
|
@@ -439,6 +442,7 @@ async function showSettings() {
|
|
|
439
442
|
change.addEventListener("click", openProjectPicker);
|
|
440
443
|
field(t("settings.cwd"), settings.cwd || "—", t("settings.cwdHint"), change);
|
|
441
444
|
permissionsField(settings);
|
|
445
|
+
claudeAgentField(settings.claudeAgent || { on: false, available: false });
|
|
442
446
|
|
|
443
447
|
// A door, not a report: the numbers live on a page of their own, and the row
|
|
444
448
|
// here carries no total of its own because working one out means walking
|
|
@@ -588,6 +592,34 @@ async function showSettings() {
|
|
|
588
592
|
* and a "Show trail" you had to find after that was one click too many for
|
|
589
593
|
* the only thing anybody opens this row to see.
|
|
590
594
|
*/
|
|
595
|
+
/**
|
|
596
|
+
* Claude Agent: the user's own Claude Code, on their own Claude account, as
|
|
597
|
+
* a subagent and as @claude.
|
|
598
|
+
*
|
|
599
|
+
* Off until switched on here, because it spends someone's Claude plan and
|
|
600
|
+
* that is theirs to decide — the hint says so in so many words before the
|
|
601
|
+
* switch is flipped, not after. With no Claude Code on this machine the
|
|
602
|
+
* switch is shown but shut, and the row says where to get it.
|
|
603
|
+
*/
|
|
604
|
+
function claudeAgentField(status) {
|
|
605
|
+
const value = !status.available
|
|
606
|
+
? t("settings.claudeAgentMissingValue")
|
|
607
|
+
: t(status.on ? "settings.claudeAgentOnValue" : "settings.claudeAgentOffValue");
|
|
608
|
+
const body = field(
|
|
609
|
+
t("settings.claudeAgent"),
|
|
610
|
+
value,
|
|
611
|
+
t("settings.claudeAgentHint"),
|
|
612
|
+
toggle(status.on, t("settings.claudeAgent"), (next) => {
|
|
613
|
+
void api("/settings/claude-agent", { enabled: next }).then((result) => {
|
|
614
|
+
if (result && result.error) addNotice(result.error, "err");
|
|
615
|
+
void showSettings();
|
|
616
|
+
});
|
|
617
|
+
}, !status.available && !status.on),
|
|
618
|
+
);
|
|
619
|
+
if (!status.available) body.appendChild(el("div", "muted small", t("settings.claudeAgentMissing")));
|
|
620
|
+
else if (status.unavailable) body.appendChild(el("div", "muted small", status.unavailable));
|
|
621
|
+
}
|
|
622
|
+
|
|
591
623
|
function auditField(audit) {
|
|
592
624
|
const mine = audit.source !== "project";
|
|
593
625
|
|
|
@@ -792,7 +824,9 @@ async function showSettings() {
|
|
|
792
824
|
// a switch a browser tab cannot honour would be one that lies.
|
|
793
825
|
const keepAwake = await getJson("/keep-awake");
|
|
794
826
|
if (keepAwake && !keepAwake.error && keepAwake.available) {
|
|
795
|
-
|
|
827
|
+
// Set once, if ever: under Advanced, folded away with the other rows
|
|
828
|
+
// nobody comes to Settings to change.
|
|
829
|
+
target = groups.advanced;
|
|
796
830
|
field(
|
|
797
831
|
t("settings.keepAwake"),
|
|
798
832
|
t(keepAwake.enabled ? "settings.keepAwakeOn" : "settings.keepAwakeOff"),
|
|
@@ -890,44 +924,9 @@ async function showSettings() {
|
|
|
890
924
|
}
|
|
891
925
|
body.appendChild(phones);
|
|
892
926
|
}
|
|
893
|
-
// Computer use: only in the desktop app, which ships the helper.
|
|
894
|
-
//
|
|
895
|
-
// it is the person's own additions to the apps it refuses.
|
|
927
|
+
// Computer use: only in the desktop app, which ships the helper. Read here,
|
|
928
|
+
// drawn after the browser row, its neighbour in kind.
|
|
896
929
|
const computer = await getJson("/computer-use/settings");
|
|
897
|
-
if (computer && !computer.error && computer.available) {
|
|
898
|
-
target = groups.workspace;
|
|
899
|
-
const takeover = computer.takeover !== "never";
|
|
900
|
-
const body = field(
|
|
901
|
-
t("settings.computerTakeover"),
|
|
902
|
-
t(takeover ? "settings.computerTakeoverAsk" : "settings.computerTakeoverNever"),
|
|
903
|
-
t("settings.computerTakeoverHint"),
|
|
904
|
-
toggle(takeover, t("settings.computerTakeover"), (next) => {
|
|
905
|
-
void api("/computer-use/settings", { takeover: next ? "ask" : "never" }).then(showSettings);
|
|
906
|
-
}),
|
|
907
|
-
);
|
|
908
|
-
const blocked = el("div", "setting-list");
|
|
909
|
-
blocked.appendChild(el("div", "muted small", t("settings.computerBlocked")));
|
|
910
|
-
const box = el("textarea", "boxed");
|
|
911
|
-
box.value = (computer.blockedApps || []).join("\n");
|
|
912
|
-
box.rows = Math.max(2, (computer.blockedApps || []).length + 1);
|
|
913
|
-
box.spellcheck = false;
|
|
914
|
-
box.style.width = "100%";
|
|
915
|
-
box.placeholder = t("settings.computerBlockedPlaceholder");
|
|
916
|
-
blocked.appendChild(box);
|
|
917
|
-
const save = el("button", "ghost", t("perms.save"));
|
|
918
|
-
save.type = "button";
|
|
919
|
-
save.addEventListener("click", () => {
|
|
920
|
-
save.disabled = true;
|
|
921
|
-
const apps = box.value.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
922
|
-
void api("/computer-use/settings", { blockedApps: apps }).then((result) => {
|
|
923
|
-
save.disabled = false;
|
|
924
|
-
if (result && result.error) return void addNotice(result.error, "err");
|
|
925
|
-
addNotice(t("perms.saved"));
|
|
926
|
-
});
|
|
927
|
-
});
|
|
928
|
-
blocked.appendChild(save);
|
|
929
|
-
body.appendChild(blocked);
|
|
930
|
-
}
|
|
931
930
|
// Which browsers the agent may use. Three choices, so a segmented control
|
|
932
931
|
// rather than a switch; absent when the browser tools are off altogether,
|
|
933
932
|
// since then there is nothing for it to move.
|
|
@@ -961,6 +960,105 @@ async function showSettings() {
|
|
|
961
960
|
body.appendChild(status);
|
|
962
961
|
body.appendChild(extensionHelp(ext));
|
|
963
962
|
}
|
|
963
|
+
// Computer use, as one row: the row says how it gets switched on, and opens
|
|
964
|
+
// onto the three things a person owns about it — whether the agent may
|
|
965
|
+
// switch it on itself, whether it may ever take the real mouse and
|
|
966
|
+
// keyboard, and their own additions to the apps it refuses.
|
|
967
|
+
if (computer && !computer.error && computer.available) {
|
|
968
|
+
target = groups.workspace;
|
|
969
|
+
const enableModes = ["ask", "auto", "off"];
|
|
970
|
+
const enable = enableModes.includes(computer.enable) ? computer.enable : "ask";
|
|
971
|
+
const body = field(t("settings.computer"), t("settings.computerEnable." + enable), "");
|
|
972
|
+
const sub = (label, control, hint) => {
|
|
973
|
+
const part = el("div", "setting-part");
|
|
974
|
+
const line = el("div", "setting-part-line");
|
|
975
|
+
line.appendChild(el("div", "setting-part-name", label));
|
|
976
|
+
line.appendChild(control);
|
|
977
|
+
part.appendChild(line);
|
|
978
|
+
if (hint) part.appendChild(el("div", "muted small", hint));
|
|
979
|
+
body.appendChild(part);
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
const enableSeg = el("div", "seg setting-seg");
|
|
983
|
+
enableSeg.setAttribute("role", "radiogroup");
|
|
984
|
+
enableSeg.setAttribute("aria-label", t("settings.computerEnable"));
|
|
985
|
+
for (const mode of enableModes) {
|
|
986
|
+
const chosen = mode === enable;
|
|
987
|
+
const button = el("button", "seg-item" + (chosen ? " on" : ""), t("settings.computerEnable." + mode));
|
|
988
|
+
button.type = "button";
|
|
989
|
+
button.setAttribute("role", "radio");
|
|
990
|
+
button.setAttribute("aria-checked", chosen ? "true" : "false");
|
|
991
|
+
button.addEventListener("click", () => {
|
|
992
|
+
if (chosen) return;
|
|
993
|
+
enableSeg.querySelectorAll("button").forEach((b) => (b.disabled = true));
|
|
994
|
+
void api("/computer-use/settings", { enable: mode }).then(() => showSettings({ open: "computer" }));
|
|
995
|
+
});
|
|
996
|
+
enableSeg.appendChild(button);
|
|
997
|
+
}
|
|
998
|
+
sub(t("settings.computerEnable"), enableSeg, t("settings.computerEnableHint." + enable));
|
|
999
|
+
|
|
1000
|
+
const takeover = computer.takeover !== "never";
|
|
1001
|
+
sub(
|
|
1002
|
+
t("settings.computerTakeover"),
|
|
1003
|
+
toggle(takeover, t("settings.computerTakeover"), (next) => {
|
|
1004
|
+
void api("/computer-use/settings", { takeover: next ? "ask" : "never" }).then(() => showSettings({ open: "computer" }));
|
|
1005
|
+
}),
|
|
1006
|
+
t("settings.computerTakeoverHint"),
|
|
1007
|
+
);
|
|
1008
|
+
|
|
1009
|
+
const blocked = el("div", "setting-list setting-part");
|
|
1010
|
+
blocked.appendChild(el("div", "setting-part-name", t("settings.computerBlockedTitle")));
|
|
1011
|
+
blocked.appendChild(el("div", "muted small", t("settings.computerBlocked")));
|
|
1012
|
+
const box = el("textarea", "boxed");
|
|
1013
|
+
box.value = (computer.blockedApps || []).join("\n");
|
|
1014
|
+
box.rows = Math.max(2, (computer.blockedApps || []).length + 1);
|
|
1015
|
+
box.spellcheck = false;
|
|
1016
|
+
box.style.width = "100%";
|
|
1017
|
+
box.placeholder = t("settings.computerBlockedPlaceholder");
|
|
1018
|
+
blocked.appendChild(box);
|
|
1019
|
+
const save = el("button", "ghost", t("perms.save"));
|
|
1020
|
+
save.type = "button";
|
|
1021
|
+
save.addEventListener("click", () => {
|
|
1022
|
+
save.disabled = true;
|
|
1023
|
+
const apps = box.value.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
1024
|
+
void api("/computer-use/settings", { blockedApps: apps }).then((result) => {
|
|
1025
|
+
save.disabled = false;
|
|
1026
|
+
if (result && result.error) return void addNotice(result.error, "err");
|
|
1027
|
+
addNotice(t("perms.saved"));
|
|
1028
|
+
});
|
|
1029
|
+
});
|
|
1030
|
+
blocked.appendChild(save);
|
|
1031
|
+
body.appendChild(blocked);
|
|
1032
|
+
body.parentElement.dataset.row = "computer";
|
|
1033
|
+
if (opened === "computer") body.parentElement.open = true;
|
|
1034
|
+
}
|
|
1035
|
+
// One switch for both: the pictures of what a turn left in the browser and
|
|
1036
|
+
// on the computer, under its reply. Shown wherever either can run.
|
|
1037
|
+
const shots = await getJson("/turn-shots");
|
|
1038
|
+
if (shots && !shots.error && (browserModes.includes(settings.browserMode) || (computer && computer.available))) {
|
|
1039
|
+
target = groups.advanced;
|
|
1040
|
+
field(
|
|
1041
|
+
t("settings.turnShots"),
|
|
1042
|
+
t(shots.enabled ? "settings.keepAwakeOn" : "settings.keepAwakeOff"),
|
|
1043
|
+
t("settings.turnShotsHint"),
|
|
1044
|
+
toggle(shots.enabled, t("settings.turnShots"), (next) => {
|
|
1045
|
+
void api("/turn-shots", { enabled: next }).then(showSettings);
|
|
1046
|
+
}),
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
// The grey next message in the empty composer, written after each turn.
|
|
1050
|
+
const suggestions = await getJson("/suggestions");
|
|
1051
|
+
if (suggestions && !suggestions.error) {
|
|
1052
|
+
target = groups.workspace;
|
|
1053
|
+
field(
|
|
1054
|
+
t("settings.suggestions"),
|
|
1055
|
+
t(suggestions.enabled ? "settings.keepAwakeOn" : "settings.keepAwakeOff"),
|
|
1056
|
+
t("settings.suggestionsHint"),
|
|
1057
|
+
toggle(suggestions.enabled, t("settings.suggestions"), (next) => {
|
|
1058
|
+
void api("/suggestions", { enabled: next }).then(showSettings);
|
|
1059
|
+
}),
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
964
1062
|
// The desktop app's version, with its release notes beside it. A row that
|
|
965
1063
|
// is always there, because the menu bar is not where people look and the
|
|
966
1064
|
// post-update hint is gone after one click.
|
|
@@ -988,12 +1086,46 @@ async function showSettings() {
|
|
|
988
1086
|
];
|
|
989
1087
|
for (const [title, parts] of sections) {
|
|
990
1088
|
if (!parts.some((part) => part.childNodes.length)) continue;
|
|
1089
|
+
if (title === "settings.section.advanced") {
|
|
1090
|
+
panel.appendChild(advancedSection(parts));
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
991
1093
|
panel.appendChild(el("div", "section-title", t(title)));
|
|
992
1094
|
for (const part of parts) panel.appendChild(part);
|
|
993
1095
|
}
|
|
994
1096
|
panelBody.appendChild(panel);
|
|
995
1097
|
}
|
|
996
1098
|
|
|
1099
|
+
/**
|
|
1100
|
+
* Advanced: the rows nobody opens Settings to change, folded under their
|
|
1101
|
+
* heading so the ones people do are the page. Shut until opened, and then
|
|
1102
|
+
* open on every visit in this browser until shut again — a person who went
|
|
1103
|
+
* looking once is likely to go looking again.
|
|
1104
|
+
*/
|
|
1105
|
+
const ADVANCED_OPEN_KEY = "oh.settings.advancedOpen";
|
|
1106
|
+
|
|
1107
|
+
function advancedSection(parts) {
|
|
1108
|
+
const section = el("details", "settings-advanced");
|
|
1109
|
+
const head = el("summary", "section-title");
|
|
1110
|
+
head.appendChild(el("span", "chev"));
|
|
1111
|
+
head.appendChild(document.createTextNode(t("settings.section.advanced")));
|
|
1112
|
+
section.appendChild(head);
|
|
1113
|
+
for (const part of parts) section.appendChild(part);
|
|
1114
|
+
try {
|
|
1115
|
+
section.open = localStorage.getItem(ADVANCED_OPEN_KEY) === "1";
|
|
1116
|
+
} catch {
|
|
1117
|
+
// Storage refused: shut, as on a first visit.
|
|
1118
|
+
}
|
|
1119
|
+
section.addEventListener("toggle", () => {
|
|
1120
|
+
try {
|
|
1121
|
+
localStorage.setItem(ADVANCED_OPEN_KEY, section.open ? "1" : "0");
|
|
1122
|
+
} catch {
|
|
1123
|
+
// Not remembered, which costs one click next time.
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
return section;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
997
1129
|
/**
|
|
998
1130
|
* Where the money went, and on which model.
|
|
999
1131
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routines.js","sourceRoot":"","sources":["../../../src/page/client/routines.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"routines.js","sourceRoot":"","sources":["../../../src/page/client/routines.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kFA4xCgD,CAAC"}
|
|
@@ -8,6 +8,8 @@ export const SHELL = String.raw `
|
|
|
8
8
|
|
|
9
9
|
const projectBtn = document.getElementById("project");
|
|
10
10
|
let projects = [];
|
|
11
|
+
/** The server's home directory, which the picker writes as ~. */
|
|
12
|
+
let projectHome = "";
|
|
11
13
|
/** Session directories that are worktrees, each naming the project it came from. */
|
|
12
14
|
let worktrees = [];
|
|
13
15
|
const worktreeAt = (cwd) => worktrees.find((one) => one.path === cwd);
|
|
@@ -54,6 +56,7 @@ function repaintPanel() {
|
|
|
54
56
|
async function loadProjects() {
|
|
55
57
|
const body = await getJson("/projects");
|
|
56
58
|
projects = body.projects || [];
|
|
59
|
+
projectHome = body.home || "";
|
|
57
60
|
worktrees = body.worktrees || [];
|
|
58
61
|
projectOrder = body.order || [];
|
|
59
62
|
paintProject();
|
|
@@ -64,9 +67,9 @@ async function loadProjects() {
|
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
async function renameProjectUi(cwd, name) {
|
|
67
|
-
const next =
|
|
68
|
-
if (!next || next
|
|
69
|
-
const body = await api("/project", { path: cwd, rename: next
|
|
70
|
+
const next = await askText({ title: t("projects.rename"), body: t("projects.renamePrompt"), value: name, ok: t("dialog.save") });
|
|
71
|
+
if (!next || next === name) return;
|
|
72
|
+
const body = await api("/project", { path: cwd, rename: next });
|
|
70
73
|
if (body && body.error) return void addNotice(body.error, "err");
|
|
71
74
|
addNotice(t("projects.renamed", { name: body.project.name }));
|
|
72
75
|
// Paths key tabs, local open/closed state and drag order. Reloading after
|
|
@@ -81,73 +84,124 @@ function exportProjectUi(cwd) {
|
|
|
81
84
|
link.click();
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
/** A project's directory as people say it: under home, from ~. */
|
|
88
|
+
function projectDir(path) {
|
|
89
|
+
const home = projectHome.replace(/[\\/]+$/, "");
|
|
90
|
+
if (home && (path === home || path.startsWith(home + "/") || path.startsWith(home + "\\"))) return "~" + path.slice(home.length);
|
|
91
|
+
return path;
|
|
92
|
+
}
|
|
93
|
+
|
|
84
94
|
function openProjectPicker() {
|
|
85
95
|
const dialog = document.getElementById("project-picker");
|
|
86
96
|
const field = document.getElementById("proj-search");
|
|
87
97
|
const listEl = document.getElementById("proj-list");
|
|
88
98
|
field.placeholder = t("projects.search");
|
|
89
99
|
|
|
90
|
-
|
|
100
|
+
// The row menu lives in <body>, which a modal dialog makes inert: while the
|
|
101
|
+
// picker is open the menu is moved inside it, and put back when it closes.
|
|
102
|
+
const menuHome = rowMenu.parentNode;
|
|
103
|
+
dialog.appendChild(rowMenu);
|
|
104
|
+
dialog.addEventListener("close", () => {
|
|
105
|
+
closeRowMenu();
|
|
106
|
+
menuHome.appendChild(rowMenu);
|
|
107
|
+
}, { once: true });
|
|
108
|
+
|
|
109
|
+
/** The rows that open a project, in order, and which one Enter opens. */
|
|
110
|
+
let rows = [];
|
|
111
|
+
let active = 0;
|
|
112
|
+
const light = (index, scroll) => {
|
|
113
|
+
if (!rows.length) return;
|
|
114
|
+
active = (index + rows.length) % rows.length;
|
|
115
|
+
rows.forEach((row, i) => row.parentNode.classList.toggle("active", i === active));
|
|
116
|
+
if (scroll) rows[active].scrollIntoView({ block: "nearest" });
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const menuFor = (project, more, line) => {
|
|
120
|
+
const items = [
|
|
121
|
+
menuItem(t("projects.rename"), {
|
|
122
|
+
key: "r",
|
|
123
|
+
run: () => {
|
|
124
|
+
dialog.close();
|
|
125
|
+
void renameProjectUi(project.path, project.name);
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
menuItem(t("projects.export"), {
|
|
129
|
+
key: "e",
|
|
130
|
+
run: () => {
|
|
131
|
+
closeRowMenu();
|
|
132
|
+
exportProjectUi(project.path);
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
];
|
|
136
|
+
// Not offered for the project this window is standing in: the next
|
|
137
|
+
// switch calls addProject and puts it straight back, so the control
|
|
138
|
+
// would appear to do nothing. Nothing recorded is lost either way —
|
|
139
|
+
// this forgets a path, and the sessions stay where they are.
|
|
140
|
+
if (!project.current) {
|
|
141
|
+
items.push(
|
|
142
|
+
menuSep(),
|
|
143
|
+
menuItem(t("projects.forget"), {
|
|
144
|
+
danger: true,
|
|
145
|
+
run: () => {
|
|
146
|
+
closeRowMenu();
|
|
147
|
+
void api("/project", { path: project.path, remove: true }).then(async (body) => {
|
|
148
|
+
if (body && body.error) return void addNotice(body.error, "err");
|
|
149
|
+
await loadProjects();
|
|
150
|
+
paint();
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
openRowMenu(more, line, items);
|
|
157
|
+
};
|
|
158
|
+
|
|
91
159
|
const paint = () => {
|
|
92
160
|
const query = field.value.trim().toLowerCase();
|
|
93
161
|
listEl.replaceChildren();
|
|
94
|
-
|
|
162
|
+
rows = [];
|
|
163
|
+
let here = 0;
|
|
95
164
|
for (const project of projects) {
|
|
96
165
|
if (query && !(project.name + " " + project.path).toLowerCase().includes(query)) continue;
|
|
166
|
+
const line = el("div", "proj-row" + (project.current ? " current" : ""));
|
|
97
167
|
const row = el("button", "proj-item");
|
|
98
168
|
row.type = "button";
|
|
169
|
+
row.title = project.path;
|
|
99
170
|
row.appendChild(el("span", "avatar", project.name.slice(0, 1).toUpperCase()));
|
|
100
|
-
|
|
171
|
+
const text = el("span", "proj-text");
|
|
172
|
+
text.appendChild(el("span", "path", project.name));
|
|
173
|
+
text.appendChild(el("span", "proj-dir", projectDir(project.path)));
|
|
174
|
+
row.appendChild(text);
|
|
101
175
|
if (project.missing) row.appendChild(el("span", "tag", t("projects.missing")));
|
|
102
|
-
|
|
176
|
+
const tick = el("span", "proj-check");
|
|
177
|
+
if (project.current) tick.appendChild(svgIcon("check"));
|
|
178
|
+
row.appendChild(tick);
|
|
103
179
|
row.addEventListener("click", () => {
|
|
104
180
|
dialog.close();
|
|
105
181
|
if (!project.current) void chooseProject(project.path);
|
|
106
182
|
});
|
|
107
|
-
|
|
108
|
-
|
|
183
|
+
const index = rows.length;
|
|
184
|
+
row.addEventListener("mousemove", () => { if (active !== index) light(index); });
|
|
109
185
|
line.appendChild(row);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
void renameProjectUi(project.path, project.name);
|
|
118
|
-
});
|
|
119
|
-
line.appendChild(rename);
|
|
120
|
-
const archive = el("button", "proj-forget", "↓");
|
|
121
|
-
archive.type = "button";
|
|
122
|
-
archive.title = t("projects.export");
|
|
123
|
-
archive.setAttribute("aria-label", t("projects.export") + ": " + project.name);
|
|
124
|
-
archive.addEventListener("click", (e) => {
|
|
186
|
+
|
|
187
|
+
const more = el("button", "proj-more");
|
|
188
|
+
more.type = "button";
|
|
189
|
+
more.title = t("session.more");
|
|
190
|
+
more.setAttribute("aria-label", t("session.more") + ": " + project.name);
|
|
191
|
+
more.appendChild(svgIcon("more"));
|
|
192
|
+
more.addEventListener("click", (e) => {
|
|
125
193
|
e.stopPropagation();
|
|
126
|
-
|
|
194
|
+
if (menuRow === line) return closeRowMenu();
|
|
195
|
+
menuFor(project, more, line);
|
|
127
196
|
});
|
|
128
|
-
line.appendChild(
|
|
129
|
-
// Not offered for the project this window is standing in: the next
|
|
130
|
-
// switch calls addProject and puts it straight back, so the control
|
|
131
|
-
// would appear to do nothing. Nothing recorded is lost either way —
|
|
132
|
-
// this forgets a path, and the sessions stay where they are.
|
|
133
|
-
if (!project.current) {
|
|
134
|
-
const forget = el("button", "proj-forget", "✕");
|
|
135
|
-
forget.type = "button";
|
|
136
|
-
forget.title = t("projects.forget");
|
|
137
|
-
forget.setAttribute("aria-label", t("projects.forget") + ": " + project.name);
|
|
138
|
-
forget.addEventListener("click", (e) => {
|
|
139
|
-
e.stopPropagation();
|
|
140
|
-
void api("/project", { path: project.path, remove: true }).then(async (body) => {
|
|
141
|
-
if (body && body.error) return void addNotice(body.error, "err");
|
|
142
|
-
await loadProjects();
|
|
143
|
-
paint();
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
line.appendChild(forget);
|
|
147
|
-
}
|
|
197
|
+
line.appendChild(more);
|
|
148
198
|
listEl.appendChild(line);
|
|
149
|
-
|
|
199
|
+
if (project.current && !query) here = rows.length;
|
|
200
|
+
rows.push(row);
|
|
150
201
|
}
|
|
202
|
+
if (!rows.length) listEl.appendChild(el("div", "proj-empty", t("projects.none")));
|
|
203
|
+
// Searching starts from the first match; an empty query from where you are.
|
|
204
|
+
light(here, true);
|
|
151
205
|
};
|
|
152
206
|
|
|
153
207
|
const add = document.getElementById("proj-add");
|
|
@@ -158,20 +212,38 @@ function openProjectPicker() {
|
|
|
158
212
|
};
|
|
159
213
|
|
|
160
214
|
field.value = "";
|
|
161
|
-
field.oninput =
|
|
162
|
-
|
|
163
|
-
|
|
215
|
+
field.oninput = () => {
|
|
216
|
+
closeRowMenu();
|
|
217
|
+
paint();
|
|
218
|
+
};
|
|
219
|
+
// A list with a search box above it promises the keys of one: arrows move
|
|
220
|
+
// through what matched, Enter opens it, Escape shuts a menu before the picker.
|
|
164
221
|
field.onkeydown = (e) => {
|
|
165
|
-
if (e.
|
|
166
|
-
e.
|
|
167
|
-
|
|
222
|
+
if (e.isComposing) return;
|
|
223
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
224
|
+
e.preventDefault();
|
|
225
|
+
closeRowMenu();
|
|
226
|
+
light(active + (e.key === "ArrowDown" ? 1 : -1), true);
|
|
227
|
+
} else if (e.key === "Enter" && rows[active]) {
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
rows[active].click();
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
dialog.onkeydown = (e) => {
|
|
233
|
+
if (e.key === "Escape" && !rowMenu.hidden) {
|
|
234
|
+
e.preventDefault();
|
|
235
|
+
e.stopPropagation();
|
|
236
|
+
closeRowMenu();
|
|
237
|
+
field.focus();
|
|
238
|
+
}
|
|
168
239
|
};
|
|
169
240
|
paint();
|
|
170
241
|
dialog.showModal();
|
|
171
242
|
field.focus();
|
|
243
|
+
// Again now that it has a size: before showModal there is nothing to scroll.
|
|
244
|
+
light(active, true);
|
|
172
245
|
}
|
|
173
246
|
|
|
174
|
-
|
|
175
247
|
/**
|
|
176
248
|
* A directory browser, for hosts with no folder dialog.
|
|
177
249
|
*
|
|
@@ -753,6 +825,80 @@ if (IS_DESKTOP && window.oharness.updates) {
|
|
|
753
825
|
// Asked once as well: a release found before this page loaded (or before a
|
|
754
826
|
// reopened window) was announced to nobody.
|
|
755
827
|
window.oharness.updates.state().then(paintUpdateHint, () => {});
|
|
828
|
+
if (window.oharness.updates.onPrompt) window.oharness.updates.onPrompt(showUpdatePrompt);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* The update questions, asked in the app's own dialog rather than the OS's.
|
|
833
|
+
* The main process decides when to ask and what a button does; the page only
|
|
834
|
+
* words the question and hands back which button it was — 0 is the one that
|
|
835
|
+
* acts, and Escape answers -1, the same as Later.
|
|
836
|
+
*/
|
|
837
|
+
function showUpdatePrompt(prompt) {
|
|
838
|
+
const kind = prompt && prompt.kind;
|
|
839
|
+
const vars = { version: prompt.version || "", current: prompt.current || "" };
|
|
840
|
+
const dialog = el("dialog", "upd-dialog");
|
|
841
|
+
if (prompt.important) dialog.classList.add("important");
|
|
842
|
+
const head = el("div", "upd-head");
|
|
843
|
+
head.appendChild(el("span", "upd-mark", kind === "error" ? "!" : "↑"));
|
|
844
|
+
const title =
|
|
845
|
+
kind === "download"
|
|
846
|
+
? t(prompt.important ? "update.prompt.importantTitle" : "update.prompt.availableTitle", vars)
|
|
847
|
+
: kind === "restart"
|
|
848
|
+
? t("update.prompt.readyTitle", vars)
|
|
849
|
+
: kind === "current"
|
|
850
|
+
? t("update.prompt.currentTitle")
|
|
851
|
+
: kind === "error"
|
|
852
|
+
? t("update.prompt.errorTitle")
|
|
853
|
+
: t("update.prompt.devTitle");
|
|
854
|
+
head.appendChild(el("div", "upd-title", title));
|
|
855
|
+
dialog.appendChild(head);
|
|
856
|
+
|
|
857
|
+
if (kind === "download" && prompt.notes) dialog.appendChild(el("div", "upd-notes", prompt.notes));
|
|
858
|
+
const body =
|
|
859
|
+
kind === "download"
|
|
860
|
+
? t("update.prompt.availableBody", vars)
|
|
861
|
+
: kind === "restart"
|
|
862
|
+
? t("update.prompt.readyBody")
|
|
863
|
+
: kind === "current"
|
|
864
|
+
? t("update.prompt.currentBody", vars)
|
|
865
|
+
: kind === "error"
|
|
866
|
+
? prompt.detail || ""
|
|
867
|
+
: t("update.prompt.devBody");
|
|
868
|
+
if (body) dialog.appendChild(el("div", kind === "error" ? "upd-body mono" : "upd-body", body));
|
|
869
|
+
|
|
870
|
+
const actions = el("div", "dlg-actions");
|
|
871
|
+
const finish = (response) => {
|
|
872
|
+
window.oharness.updates.answer(prompt.id, response);
|
|
873
|
+
if (dialog.open) dialog.close();
|
|
874
|
+
dialog.remove();
|
|
875
|
+
};
|
|
876
|
+
let main;
|
|
877
|
+
let focus;
|
|
878
|
+
if (kind === "download" || kind === "restart") {
|
|
879
|
+
const later = el("button", "quiet", t("update.prompt.later"));
|
|
880
|
+
main = el("button", "approve", t(kind === "download" ? "update.prompt.download" : "update.prompt.restart"));
|
|
881
|
+
later.type = main.type = "button";
|
|
882
|
+
later.onclick = () => finish(1);
|
|
883
|
+
main.onclick = () => finish(0);
|
|
884
|
+
actions.append(later, main);
|
|
885
|
+
// Restarting ends whatever is running, so Enter must not do it by accident.
|
|
886
|
+
focus = kind === "restart" ? later : main;
|
|
887
|
+
} else {
|
|
888
|
+
main = el("button", "approve", t("update.prompt.ok"));
|
|
889
|
+
main.type = "button";
|
|
890
|
+
main.onclick = () => finish(0);
|
|
891
|
+
actions.append(main);
|
|
892
|
+
focus = main;
|
|
893
|
+
}
|
|
894
|
+
dialog.appendChild(actions);
|
|
895
|
+
dialog.addEventListener("cancel", (e) => {
|
|
896
|
+
e.preventDefault();
|
|
897
|
+
finish(-1);
|
|
898
|
+
});
|
|
899
|
+
document.body.appendChild(dialog);
|
|
900
|
+
dialog.showModal();
|
|
901
|
+
focus.focus();
|
|
756
902
|
}
|
|
757
903
|
|
|
758
904
|
/**
|
|
@@ -785,7 +931,7 @@ async function takeDeepLink() {
|
|
|
785
931
|
}
|
|
786
932
|
if (typeof text !== "string" || !text) return;
|
|
787
933
|
const draft = input.value.trim();
|
|
788
|
-
if (draft && draft !== text && !
|
|
934
|
+
if (draft && draft !== text && !(await askConfirm({ body: t("deepLink.replaceDraft"), ok: t("dialog.replace") }))) return;
|
|
789
935
|
await switchTo({ action: "new" });
|
|
790
936
|
input.value = text;
|
|
791
937
|
// Called rather than an input event, which would also open the command
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../../../src/page/client/shell.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../../../src/page/client/shell.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MA2/BzB,CAAC"}
|
|
@@ -460,6 +460,10 @@ function projectHead(cwd, name, count, open) {
|
|
|
460
460
|
closeRowMenu();
|
|
461
461
|
void openTerminalIn(cwd, true);
|
|
462
462
|
} }),
|
|
463
|
+
menuItem(t("projects.claudeCode"), { key: "c", run: () => {
|
|
464
|
+
closeRowMenu();
|
|
465
|
+
void openClaudeCode(cwd);
|
|
466
|
+
} }),
|
|
463
467
|
menuItem(t(IS_MAC ? "projects.revealMac" : IS_WIN ? "projects.revealWin" : "projects.reveal"), { key: "f", run: () => {
|
|
464
468
|
closeRowMenu();
|
|
465
469
|
void api("/reveal", { path: cwd }).then((body) => {
|
|
@@ -968,11 +972,11 @@ async function showWorktreeFinish(cwd, anchor, head) {
|
|
|
968
972
|
async function finishWorktreeUi(wt, action, name, clean) {
|
|
969
973
|
const body = { path: wt.path, action };
|
|
970
974
|
if (action === "commit") {
|
|
971
|
-
const message =
|
|
975
|
+
const message = await askText({ title: t("worktree.commitPrompt"), value: wt.task || "", ok: t("dialog.ok") });
|
|
972
976
|
if (!message) return;
|
|
973
977
|
body.message = message;
|
|
974
978
|
}
|
|
975
|
-
if (action === "discard" && !clean && !
|
|
979
|
+
if (action === "discard" && !clean && !(await askConfirm({ body: t("worktree.discardConfirm", { name }), ok: t("dialog.discard"), danger: true }))) return;
|
|
976
980
|
const answer = await api("/worktree/finish", body, 5 * 60_000);
|
|
977
981
|
if (!answer || answer.error) return void addNotice((answer && answer.error) || t("worktree.failed"), "notice err");
|
|
978
982
|
addNotice(answer.result.message);
|
|
@@ -1487,6 +1491,7 @@ function renderHistory(entries, into) {
|
|
|
1487
1491
|
if (card) assistantTurn().appendChild(card);
|
|
1488
1492
|
}
|
|
1489
1493
|
if (entry.shots) turnShots(assistantTurn(), entry.shots);
|
|
1494
|
+
if (entry.agent) agentReply(assistantTurn(), entry.agent);
|
|
1490
1495
|
if (P.turn) turnActions(P.turn, entry.keepMessages);
|
|
1491
1496
|
}
|
|
1492
1497
|
for (const card of connectionCards.values()) {
|