@oharness/server 0.9.2 → 0.10.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.
Files changed (61) hide show
  1. package/dist/i18n.js +356 -62
  2. package/dist/i18n.js.map +1 -1
  3. package/dist/main.js +4 -0
  4. package/dist/main.js.map +1 -1
  5. package/dist/page/client/account.js +430 -23
  6. package/dist/page/client/account.js.map +1 -1
  7. package/dist/page/client/base.js +20 -1
  8. package/dist/page/client/base.js.map +1 -1
  9. package/dist/page/client/composer.js +709 -58
  10. package/dist/page/client/composer.js.map +1 -1
  11. package/dist/page/client/computer.d.ts +27 -0
  12. package/dist/page/client/computer.js +358 -0
  13. package/dist/page/client/computer.js.map +1 -0
  14. package/dist/page/client/dialogs.js +93 -6
  15. package/dist/page/client/dialogs.js.map +1 -1
  16. package/dist/page/client/events.js +94 -7
  17. package/dist/page/client/events.js.map +1 -1
  18. package/dist/page/client/files.js +86 -1
  19. package/dist/page/client/files.js.map +1 -1
  20. package/dist/page/client/index.js +4 -0
  21. package/dist/page/client/index.js.map +1 -1
  22. package/dist/page/client/panels.js +12 -6
  23. package/dist/page/client/panels.js.map +1 -1
  24. package/dist/page/client/routines.js +296 -41
  25. package/dist/page/client/routines.js.map +1 -1
  26. package/dist/page/client/shell.js +229 -51
  27. package/dist/page/client/shell.js.map +1 -1
  28. package/dist/page/client/state.js +273 -19
  29. package/dist/page/client/state.js.map +1 -1
  30. package/dist/page/client/tasks.d.ts +8 -0
  31. package/dist/page/client/tasks.js +293 -0
  32. package/dist/page/client/tasks.js.map +1 -0
  33. package/dist/page/client/transcript.js +174 -6
  34. package/dist/page/client/transcript.js.map +1 -1
  35. package/dist/page/client/usage.js +1 -11
  36. package/dist/page/client/usage.js.map +1 -1
  37. package/dist/page/style.js +706 -104
  38. package/dist/page/style.js.map +1 -1
  39. package/dist/page.js +161 -54
  40. package/dist/page.js.map +1 -1
  41. package/dist/prefs.d.ts +14 -0
  42. package/dist/prefs.js +8 -1
  43. package/dist/prefs.js.map +1 -1
  44. package/dist/remote-crypto.d.ts +78 -0
  45. package/dist/remote-crypto.js +216 -0
  46. package/dist/remote-crypto.js.map +1 -0
  47. package/dist/remote.d.ts +103 -0
  48. package/dist/remote.js +498 -0
  49. package/dist/remote.js.map +1 -0
  50. package/dist/server.d.ts +70 -0
  51. package/dist/server.js +1291 -49
  52. package/dist/server.js.map +1 -1
  53. package/dist/starters.d.ts +92 -0
  54. package/dist/starters.js +213 -0
  55. package/dist/starters.js.map +1 -0
  56. package/dist/theme.js +5 -0
  57. package/dist/theme.js.map +1 -1
  58. package/dist/turn-shots.d.ts +27 -0
  59. package/dist/turn-shots.js +71 -0
  60. package/dist/turn-shots.js.map +1 -0
  61. package/package.json +2 -2
@@ -22,55 +22,180 @@ async function newRoutine() {
22
22
  input.setSelectionRange(input.value.length, input.value.length);
23
23
  }
24
24
 
25
- /** One run as a line: when, how it went, and what it cost. */
26
- function runSummary(run) {
27
- const when = new Date(run.startedAt).toLocaleString();
28
- if (run.skipped) return t("routines.skipped", { when });
29
- const parts = [t(run.ok ? "routines.lastOk" : "routines.lastFailed", { when })];
30
- if (run.catchUp) parts.push(t("routines.catchUp"));
31
- parts.push(t("routines.turns", { n: run.turns }));
32
- if (run.costUsd) parts.push("$" + run.costUsd.toFixed(4));
33
- return parts.join(" · ");
25
+ /** A span of time as someone reads it: 42s, 3m 05s, 1h 12m. */
26
+ function runDuration(ms) {
27
+ const sec = Math.max(0, Math.round(ms / 1000));
28
+ if (sec < 60) return sec + "s";
29
+ const min = Math.floor(sec / 60);
30
+ if (min < 60) return min + "m " + String(sec % 60).padStart(2, "0") + "s";
31
+ return Math.floor(min / 60) + "h " + (min % 60) + "m";
34
32
  }
35
33
 
34
+ /** The first line worth reading out of what a run said, or why it stopped. */
35
+ function runExcerpt(run) {
36
+ const text = run.error || run.output || "";
37
+ const line = text.split("\n").map((one) => one.replace(/^[#>*\-\s]+/, "").trim()).find(Boolean) || "";
38
+ return line.length > 160 ? line.slice(0, 159) + "…" : line;
39
+ }
40
+
41
+ /**
42
+ * What the page remembers across repaints: which histories are folded away
43
+ * and which runs are open. The panel is rebuilt whenever a run starts or
44
+ * ends, and losing what someone had opened each time would make it unusable
45
+ * while a routine is going.
46
+ */
47
+ const foldedHistories = new Set();
48
+ const openRuns = new Set();
49
+ const runKey = (routine, run) => routine.name + "\0" + run.startedAt;
50
+
36
51
  /**
37
- * A routine's past runs, fetched when opened.
52
+ * One run in the history: a status, when, how long, what it cost, and one
53
+ * line under it — what it is doing, while it runs; the start of its answer or
54
+ * its error, after. Clicking opens it in place to the whole of that text,
55
+ * with a way into its conversation.
56
+ */
57
+ function routineRunRow(run, routine) {
58
+ const state = run.running ? "running" : run.skipped ? "skipped" : run.ok ? "ok" : "failed";
59
+ const key = runKey(routine, run);
60
+ const row = el("div", "routine-run " + state);
61
+ row.dataset.routine = routine.name;
62
+ const top = el("div", "routine-run-top");
63
+ top.appendChild(el("span", "run-status " + state, t("routines.status." + state)));
64
+ top.appendChild(el("span", null, new Date(run.startedAt).toLocaleString()));
65
+ const facts = el("span", "muted routine-run-facts");
66
+ if (run.running) {
67
+ const tick = () => {
68
+ if (!facts.isConnected && facts.dataset.painted) return clearInterval(timer);
69
+ facts.dataset.painted = "1";
70
+ const bits = [t("routines.elapsed", { d: runDuration(Date.now() - run.startedAt) })];
71
+ const turns = Number(row.dataset.turns || run.turns || 0);
72
+ if (turns) bits.push(t("routines.turns", { n: turns }));
73
+ facts.textContent = bits.join(" · ");
74
+ };
75
+ const timer = setInterval(tick, 1000);
76
+ tick();
77
+ } else if (!run.skipped) {
78
+ const bits = [runDuration(run.finishedAt - run.startedAt), t("routines.turns", { n: run.turns })];
79
+ if (run.costUsd) bits.push("$" + run.costUsd.toFixed(4));
80
+ if (run.catchUp) bits.push(t("routines.catchUp"));
81
+ facts.textContent = bits.join(" · ");
82
+ } else if (run.reason === "still-running") {
83
+ facts.textContent = t("routines.stillRunning");
84
+ }
85
+ top.appendChild(facts);
86
+ row.appendChild(top);
87
+
88
+ const full = run.running ? run.activity || "" : run.error || run.output || "";
89
+ const line = el("div", "routine-run-text" + (run.error ? " err" : " muted"),
90
+ run.running ? full || t("routines.starting") : runExcerpt(run));
91
+ row.appendChild(line);
92
+ if (!line.textContent) line.remove();
93
+
94
+ const detail = el("div", "routine-run-detail");
95
+ if (full && !run.running) {
96
+ // The answer as the conversation shows it; an error stays plain text.
97
+ const body = el("div", "routine-run-full" + (run.error ? " err" : " md"));
98
+ if (run.error) body.textContent = full;
99
+ else body.appendChild(renderMarkdown(full));
100
+ detail.appendChild(body);
101
+ }
102
+ if (run.sessionId) {
103
+ const open = el("button", "ghost small", t("routines.openRun"));
104
+ open.type = "button";
105
+ open.addEventListener("click", (event) => {
106
+ event.stopPropagation();
107
+ void openSession(run.sessionId, routine.cwd);
108
+ });
109
+ detail.appendChild(open);
110
+ }
111
+ if (!detail.childNodes.length) return row;
112
+
113
+ row.appendChild(detail);
114
+ row.tabIndex = 0;
115
+ row.setAttribute("role", "button");
116
+ const setOpen = (on) => {
117
+ row.classList.toggle("open", on);
118
+ row.setAttribute("aria-expanded", on ? "true" : "false");
119
+ if (on) openRuns.add(key); else openRuns.delete(key);
120
+ };
121
+ setOpen(openRuns.has(key));
122
+ row.addEventListener("click", (event) => {
123
+ // Selecting text in the full answer is reading it, not closing it.
124
+ if (String(getSelection() || "")) return;
125
+ if (event.target.closest(".routine-run-full") && row.classList.contains("open")) return;
126
+ setOpen(!row.classList.contains("open"));
127
+ });
128
+ row.addEventListener("keydown", (event) => {
129
+ if (event.target !== row || (event.key !== "Enter" && event.key !== " ")) return;
130
+ event.preventDefault();
131
+ setOpen(!row.classList.contains("open"));
132
+ });
133
+ return row;
134
+ }
135
+
136
+ /** How many runs a card shows before "Show all". */
137
+ const RUNS_SHOWN = 5;
138
+
139
+ /**
140
+ * A routine's runs, newest first, the one going now on top.
38
141
  *
39
- * Skipped times are listed with the runs, with why: a routine that "didn't
40
- * run" is the question this answers, and the answer is usually that the
41
- * previous run was still going. A run opens its session — the whole of what
42
- * the agent did, not the last paragraph of it.
142
+ * Open unless folded: whether it ran, and what came of it, is what anyone
143
+ * opening this page wants to know. Repainted whenever a run starts or ends,
144
+ * and patched in place as a running one moves on, so a run started here is
145
+ * on the page at once and turns into its result when it finishes.
43
146
  */
44
147
  function routineHistory(routine) {
45
- const box = el("details", "routine-history");
46
- box.appendChild(el("summary", "muted small", t("routines.history")));
148
+ const box = el("div", "routine-history");
149
+ const head = el("button", "routine-history-head muted small");
150
+ head.type = "button";
151
+ const label = el("span", null, t("routines.history"));
152
+ head.append(el("span", "chev", "▾"), label);
153
+ box.appendChild(head);
47
154
  const list = el("div", "routine-runs");
48
155
  box.appendChild(list);
49
- box.addEventListener("toggle", () => {
50
- if (!box.open || list.dataset.loaded) return;
51
- list.dataset.loaded = "1";
52
- list.appendChild(el("div", "muted small", t("routines.loading")));
53
- void getJson("/routine/history?name=" + encodeURIComponent(routine.name)).then((body) => {
156
+ const fold = (on) => {
157
+ box.classList.toggle("folded", on);
158
+ head.setAttribute("aria-expanded", on ? "false" : "true");
159
+ if (on) foldedHistories.add(routine.name); else foldedHistories.delete(routine.name);
160
+ };
161
+ fold(foldedHistories.has(routine.name));
162
+ head.addEventListener("click", () => fold(!box.classList.contains("folded")));
163
+
164
+ list.appendChild(el("div", "muted small", t("routines.loading")));
165
+ void getJson("/routine/history?name=" + encodeURIComponent(routine.name)).then((body) => {
166
+ list.replaceChildren();
167
+ const runs = (body && body.runs) || [];
168
+ if (runs.length) label.textContent = t("routines.historyCount", { n: runs.filter((run) => !run.running).length });
169
+ if (!runs.length) return void list.appendChild(el("div", "muted small", t("routines.noRuns")));
170
+ const paint = (all) => {
54
171
  list.replaceChildren();
55
- const runs = (body && body.runs) || [];
56
- if (!runs.length) return void list.appendChild(el("div", "muted small", t("routines.noRuns")));
57
- for (const run of runs) {
58
- const line = el(run.sessionId ? "button" : "div", "routine-run small" + (run.ok || run.skipped ? "" : " failed"));
59
- line.appendChild(el("span", null, runSummary(run)));
60
- if (run.skipped && run.reason === "still-running") {
61
- line.appendChild(el("span", "muted", t("routines.stillRunning")));
62
- }
63
- if (run.sessionId) {
64
- line.type = "button";
65
- line.addEventListener("click", () => void openSession(run.sessionId, routine.cwd));
66
- }
67
- list.appendChild(line);
172
+ for (const run of all ? runs : runs.slice(0, RUNS_SHOWN)) list.appendChild(routineRunRow(run, routine));
173
+ if (runs.length > RUNS_SHOWN) {
174
+ const more = el("button", "ghost small routine-more", all ? t("routines.showFewer") : t("routines.showAll", { n: runs.length }));
175
+ more.type = "button";
176
+ more.addEventListener("click", () => paint(!all));
177
+ list.appendChild(more);
68
178
  }
69
- });
179
+ };
180
+ paint(false);
70
181
  });
71
182
  return box;
72
183
  }
73
184
 
185
+ /** A running routine moved on: say so in its row, without a repaint. */
186
+ function paintRoutineActivity(event) {
187
+ for (const row of document.querySelectorAll(".routine-run.running")) {
188
+ if (row.dataset.routine !== event.name) continue;
189
+ row.dataset.turns = String(event.turns || 0);
190
+ let line = row.querySelector(".routine-run-text");
191
+ if (!line) {
192
+ line = el("div", "routine-run-text muted");
193
+ row.querySelector(".routine-run-top").after(line);
194
+ }
195
+ if (event.activity) line.textContent = event.activity;
196
+ }
197
+ }
198
+
74
199
  /**
75
200
  * Scheduled runs, read from the scheduler rather than kept here.
76
201
  *
@@ -103,6 +228,10 @@ async function showRoutines() {
103
228
  const head = el("div", "row-head");
104
229
  head.appendChild(el("span", "row-title", routine.name));
105
230
  head.appendChild(el("span", "mono row-cron", routine.cron));
231
+ // What it runs on, and what it needs connected: the two things a run
232
+ // fails on without anyone there to fix them.
233
+ if (routine.model) head.appendChild(el("span", "muted small routine-where", routine.model.split("/").pop()));
234
+ if (routine.apps && routine.apps.length) head.appendChild(el("span", "muted small routine-where", routine.apps.join(", ")));
106
235
  if (!routine.enabled) head.appendChild(el("span", "tag", t("routines.paused")));
107
236
  // Where it runs. A routine is made in one project and runs there, from
108
237
  // whichever project is open when its time comes, so the card says which.
@@ -112,8 +241,9 @@ async function showRoutines() {
112
241
  head.appendChild(where);
113
242
  }
114
243
 
115
- const run = el("button", "ghost", t("routines.run"));
244
+ const run = el("button", "ghost", t(routine.running ? "routines.running" : "routines.run"));
116
245
  run.type = "button";
246
+ run.disabled = Boolean(routine.running);
117
247
  run.addEventListener("click", () => {
118
248
  run.disabled = true;
119
249
  run.textContent = t("routines.running");
@@ -161,11 +291,9 @@ async function showRoutines() {
161
291
  if (routine.enabled && routine.nextRunAt) {
162
292
  bits.push(t("routines.next", { when: new Date(routine.nextRunAt).toLocaleString() }));
163
293
  }
164
- if (routine.lastRun) bits.push(runSummary(routine.lastRun));
294
+ // The last run is the first line of the history below, so it is not
295
+ // repeated here.
165
296
  if (bits.length) row.appendChild(el("div", "muted small", bits.join(" · ")));
166
- if (routine.lastRun && routine.lastRun.error) {
167
- row.appendChild(el("div", "err small", routine.lastRun.error));
168
- }
169
297
  row.appendChild(routineHistory(routine));
170
298
  panel.appendChild(row);
171
299
  }
@@ -197,7 +325,7 @@ async function showSettings() {
197
325
 
198
326
  // Rows are grouped by what they are about. Each group is filled as the page
199
327
  // is built and drawn at the end under its heading, and only when it has rows.
200
- const groups = { workspace: el("div"), sync: el("div"), memory: el("div"), activity: el("div"), advanced: el("div"), about: el("div") };
328
+ const groups = { workspace: el("div"), remote: el("div"), sync: el("div"), memory: el("div"), activity: el("div"), advanced: el("div"), about: el("div") };
201
329
  let target = groups.workspace;
202
330
 
203
331
  // A click on a button inside a summary toggles the disclosure as well as
@@ -674,6 +802,132 @@ async function showSettings() {
674
802
  }),
675
803
  );
676
804
  }
805
+ // Remote control: a phone on this account driving this machine through the
806
+ // relay. Offered only where the server was given a relay to use, and off
807
+ // until somebody at the machine turns it on.
808
+ const remote = await getJson("/remote");
809
+ if (remote && !remote.error && remote.available) {
810
+ target = groups.remote;
811
+ const stateKey = (view) =>
812
+ view.state === "online" ? "online" : view.state === "connecting" ? "connecting" : view.state === "signed-out" ? "signedOut" : "off";
813
+ const body = field(
814
+ t("settings.remote"),
815
+ t("settings.remote." + stateKey(remote)),
816
+ t("settings.remoteHint"),
817
+ toggle(remote.on, t("settings.remote"), (next) => {
818
+ void api("/remote", { enabled: next }).then((result) => {
819
+ if (result && result.error) addNotice(result.error, "err");
820
+ void showSettings();
821
+ });
822
+ }),
823
+ );
824
+ if (remote.keepsAwake) body.appendChild(el("div", "muted small", t("settings.remoteAwake")));
825
+
826
+ // Where it stands, said in a sentence: off, waiting on an account,
827
+ // reaching for the relay, or there.
828
+ const status = el("div", "setting-status small");
829
+ const dot = el("span", "status-dot");
830
+ const said = el("span");
831
+ status.append(dot, said);
832
+ body.appendChild(status);
833
+ let signIn = null;
834
+ if (typeof openConnect === "function") {
835
+ signIn = el("button", "ghost", t("settings.remoteSignIn"));
836
+ signIn.type = "button";
837
+ signIn.addEventListener("click", () => openConnect("official"));
838
+ body.appendChild(signIn);
839
+ }
840
+ const row = body.parentNode;
841
+ const shownValue = row.querySelector(".setting-value");
842
+ const paint = (view) => {
843
+ const key = stateKey(view);
844
+ dot.className = "status-dot" + (key === "online" ? " ok" : "");
845
+ said.textContent = t("settings.remoteStatus." + key);
846
+ if (shownValue) shownValue.textContent = t("settings.remote." + key);
847
+ if (signIn) signIn.hidden = key !== "signedOut";
848
+ };
849
+ paint(remote);
850
+ // Connecting and signing in finish on their own time, so the row looks
851
+ // again while it is on screen and not yet online.
852
+ if (remote.on) {
853
+ const timer = setInterval(() => {
854
+ if (!document.body.contains(row)) return clearInterval(timer);
855
+ void getJson("/remote").then((view) => {
856
+ if (!view || view.error || !document.body.contains(row)) return;
857
+ paint(view);
858
+ if (!view.on || view.state === "online") clearInterval(timer);
859
+ });
860
+ }, 3000);
861
+ }
862
+
863
+ // The phones allowed in so far. Forgetting one ends its connection now
864
+ // and sends it back to the Allow card next time.
865
+ const phones = el("div", "setting-list");
866
+ phones.appendChild(el("div", "muted small", t("settings.remotePhones")));
867
+ const devices = remote.devices || [];
868
+ if (!devices.length) phones.appendChild(el("div", "small", t("settings.remoteNoPhones")));
869
+ for (const device of devices) {
870
+ const line = el("div", "remote-phone small");
871
+ line.appendChild(el("span", "remote-phone-name", device.name || device.id));
872
+ if (device.pairedAt) {
873
+ line.appendChild(el("span", "muted", t("settings.remotePairedOn", { date: new Date(device.pairedAt).toLocaleDateString() })));
874
+ }
875
+ const forget = el("button", "ghost", t("settings.remoteForget"));
876
+ forget.type = "button";
877
+ forget.addEventListener("click", () => {
878
+ forget.disabled = true;
879
+ void api("/remote/revoke", { id: device.id }).then((result) => {
880
+ if (result && result.error) {
881
+ forget.disabled = false;
882
+ return void addNotice(result.error, "err");
883
+ }
884
+ addNotice(t("settings.remoteForgotten", { name: device.name || device.id }));
885
+ void showSettings();
886
+ });
887
+ });
888
+ line.appendChild(forget);
889
+ phones.appendChild(line);
890
+ }
891
+ body.appendChild(phones);
892
+ }
893
+ // Computer use: only in the desktop app, which ships the helper. The switch
894
+ // is whether it may ever take the real mouse and keyboard; the list below
895
+ // it is the person's own additions to the apps it refuses.
896
+ 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
+ }
677
931
  // Which browsers the agent may use. Three choices, so a segmented control
678
932
  // rather than a switch; absent when the browser tools are off altogether,
679
933
  // since then there is nothing for it to move.
@@ -726,6 +980,7 @@ async function showSettings() {
726
980
  }
727
981
  const sections = [
728
982
  ["settings.section.workspace", [groups.workspace]],
983
+ ["settings.section.remote", [groups.remote]],
729
984
  ["settings.section.sync", [groups.sync, groups.memory]],
730
985
  ["settings.section.activity", [groups.activity]],
731
986
  ["settings.section.advanced", [groups.advanced]],
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kFAy5BgD,CAAC"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kFAwpCgD,CAAC"}