@songsid/agend 2.2.0-alpha.8 → 2.2.0-alpha.9
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/auth-api.js +1 -1
- package/dist/auth-api.js.map +1 -1
- package/dist/fleet-manager.d.ts +4 -3
- package/dist/fleet-manager.js +6 -4
- package/dist/fleet-manager.js.map +1 -1
- package/dist/ui/panel-chat.js +3 -13
- package/dist/ui/panel-settings.js +54 -5
- package/dist/ui/settings-strings.js +2 -0
- package/dist/ui/shared/app-i18n.js +2 -4
- package/dist/ui/shared/app-route.js +1 -1
- package/dist/ui/shared/app-shell.js +22 -15
- package/dist/ui/shared/app-swipe.js +89 -0
- package/dist/ui/shared/app.css +16 -1
- package/dist/ui/shared/app.js +9 -1
- package/dist/web-shell-routes.d.ts +1 -1
- package/dist/web-shell-routes.js +2 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/package.json +1 -1
package/dist/ui/panel-chat.js
CHANGED
|
@@ -10,7 +10,7 @@ import { html, useEffect, useLayoutEffect, useRef, useState } from "/assets/app-
|
|
|
10
10
|
import { t } from "/assets/app-i18n.js";
|
|
11
11
|
import { appStore, useStore } from "/assets/app-store.js";
|
|
12
12
|
import { useLease } from "/assets/app-ctx.js";
|
|
13
|
-
import { PanelHeader, onPanelKey, setTitle,
|
|
13
|
+
import { PanelHeader, onPanelKey, setTitle, statusClass, statusLabel, openDrawer, requestNewInstance } from "/assets/app-shell.js";
|
|
14
14
|
import { detailsPath } from "/assets/app-route.js";
|
|
15
15
|
import { InstanceSwitch } from "/assets/instance-switch.js";
|
|
16
16
|
import { HeaderTools } from "/assets/header-tools.js";
|
|
@@ -27,7 +27,7 @@ import { createChatStore } from "./chat-store.js";
|
|
|
27
27
|
import { createThread, msgKey } from "./chat-thread.js";
|
|
28
28
|
import { PreviewPanel, openPanel, panelStore, shownKey } from "./preview-panel.js";
|
|
29
29
|
import { needsArgument, paletteFor, parseCommandLine } from "./chat-commands.js";
|
|
30
|
-
import { installTour, refreshTourSpot
|
|
30
|
+
import { installTour, refreshTourSpot } from "./chat-tour.js";
|
|
31
31
|
import { FirstRunCard } from "./first-run.js";
|
|
32
32
|
import { LightboxHost, openLightbox } from "./image-lightbox.js";
|
|
33
33
|
|
|
@@ -77,21 +77,11 @@ export function boot({ stream, boot: bootData, deps = {} }) {
|
|
|
77
77
|
if (auth && typeof auth.sessionHandle === "function") auth.sessionHandle().then((h) => P().setBannerScope(h), () => P().setBannerScope(null));
|
|
78
78
|
else P().setBannerScope(null);
|
|
79
79
|
}
|
|
80
|
-
|
|
81
|
-
addFooterItem("tour", TourButton);
|
|
80
|
+
// #1604: the preview opt-in and Tour live in Settings → This device now (and on each preview card's menu).
|
|
82
81
|
installTour();
|
|
83
82
|
return store;
|
|
84
83
|
}
|
|
85
84
|
|
|
86
|
-
function PreviewOptIn() {
|
|
87
|
-
const [on, setOn] = useState(P() ? P().optedIn() : false);
|
|
88
|
-
useEffect(() => (P() ? P().onChange(() => setOn(P().optedIn())) : undefined), []);
|
|
89
|
-
return html`<label class="side-row toggle"><input type="checkbox" checked=${on} onChange=${(e) => { const want = e.target.checked; setPreviewOptIn(want).then(setOn); }} />
|
|
90
|
-
<span>${t("chat.previews")}</span></label>`;
|
|
91
|
-
}
|
|
92
|
-
function TourButton() {
|
|
93
|
-
return html`<button type="button" id="tourBtn" class="side-row" title=${t("chat.tourBtnTitle")} onClick=${startTour}><${Icon} name="info" /><span>${t("chat.tour")}</span></button>`;
|
|
94
|
-
}
|
|
95
85
|
|
|
96
86
|
// ── Code wrap: this browser's choice (agend_code_wrap). On a phone, code wraps whatever it is (app.css).
|
|
97
87
|
const stored = (k) => { try { return localStorage.getItem(k); } catch { return null; } };
|
|
@@ -12,9 +12,9 @@ import { html, useEffect, useMemo, useRef, useState } from "/assets/app-html.js"
|
|
|
12
12
|
import { t } from "/assets/app-i18n.js";
|
|
13
13
|
import { appStore, useStore } from "/assets/app-store.js";
|
|
14
14
|
import { useLease } from "/assets/app-ctx.js";
|
|
15
|
-
import { PanelHeader, PendingChanges, setTitle } from "/assets/app-shell.js";
|
|
16
|
-
import { navigate, setLeaveGuard } from "/assets/app-nav.js";
|
|
17
|
-
import { settingsPath, SETTINGS_SECTIONS } from "/assets/app-route.js";
|
|
15
|
+
import { DevicePrefs, PanelHeader, PendingChanges, setTitle } from "/assets/app-shell.js";
|
|
16
|
+
import { navigate, navStore, setLeaveGuard } from "/assets/app-nav.js";
|
|
17
|
+
import { chatPath, settingsPath, SETTINGS_SECTIONS } from "/assets/app-route.js";
|
|
18
18
|
import { Dialog } from "/assets/ui-dialog.js";
|
|
19
19
|
import { Empty, ErrorState, Skeleton } from "/assets/ui-states.js";
|
|
20
20
|
import { Icon } from "/assets/ui-icons.js";
|
|
@@ -37,9 +37,13 @@ import { SetupWizard } from "./settings-wizard.js";
|
|
|
37
37
|
import { CreateInstanceDialog } from "./panel-fleet.js";
|
|
38
38
|
import { FirstRunCard, takeWizardRequest } from "./first-run.js";
|
|
39
39
|
import { takeAgentRequest } from "./settings-request.js";
|
|
40
|
+
// #1604: This device — the preview opt-in (one path: the app's confirm, then preview.js) and the tour, from the chat.
|
|
41
|
+
import { setPreviewOptIn } from "./panel-chat.js";
|
|
42
|
+
import { startTour } from "./chat-tour.js";
|
|
43
|
+
import "./chat-strings.js";
|
|
40
44
|
|
|
41
45
|
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
42
|
-
const ICONS = { agents: "bot", bots: "plug", classic: "room", general: "sliders", advanced: "code" };
|
|
46
|
+
const ICONS = { device: "user", agents: "bot", bots: "plug", classic: "room", general: "sliders", advanced: "code" };
|
|
43
47
|
const RELEASES = "https://github.com/songsid/AgEnD/releases";
|
|
44
48
|
|
|
45
49
|
/** GET under the panel's lease → { ok, status, body }. */
|
|
@@ -218,7 +222,9 @@ export function SettingsPanel({ route, navKey }) {
|
|
|
218
222
|
const insts = (ctx.fleet && ctx.fleet.instances) || {};
|
|
219
223
|
if (own(insts, want.name)) setDialog({ kind: "agent", name: want.name, inst: insts[want.name], key: navKey });
|
|
220
224
|
}, [!!ctx, navKey]);
|
|
221
|
-
|
|
225
|
+
// #1604: This device needs no configuration: shown at once, whatever the configuration read does.
|
|
226
|
+
const sectionBody = section === "device" ? html`<${DeviceSection} />`
|
|
227
|
+
: !data ? html`<${Skeleton} lines=${6} />`
|
|
222
228
|
: data.error ? html`<${ErrorState} message=${tn("configLoadFailed")} onRetry=${reload} />`
|
|
223
229
|
: section === "agents" ? html`<${Agents} ctx=${ctx} search=${search} openDialog=${(d) => setDialog({ ...d, key: navKey })} />`
|
|
224
230
|
: section === "bots" ? html`<${Bots} ctx=${ctx} search=${search} openDialog=${(d) => setDialog({ ...d, key: navKey })} />`
|
|
@@ -250,6 +256,49 @@ export function SettingsPanel({ route, navKey }) {
|
|
|
250
256
|
</div>`;
|
|
251
257
|
}
|
|
252
258
|
|
|
259
|
+
/**
|
|
260
|
+
* #1604: what this browser chooses for itself — moved here from the sidebar footer, each with the storage it always had:
|
|
261
|
+
* theme (theme.js), language (app-i18n.js), HTML previews on this device (preview.js, through the app's confirm), and
|
|
262
|
+
* the chat tour (replayed on the chat it points at).
|
|
263
|
+
*/
|
|
264
|
+
export function DeviceSection() {
|
|
265
|
+
const P = () => globalThis.AgendPreview;
|
|
266
|
+
const [on, setOn] = useState(P() ? P().optedIn() : false);
|
|
267
|
+
useEffect(() => (P() ? P().onChange(() => setOn(P().optedIn())) : undefined), []);
|
|
268
|
+
const replay = () => {
|
|
269
|
+
let last = null;
|
|
270
|
+
try { last = localStorage.getItem("agend_last_instance"); } catch { /* none */ }
|
|
271
|
+
navigate(last ? chatPath(last) : "/ui");
|
|
272
|
+
// This replay belongs to the navigation it just made — that one, by its sequence number, not by URL (A → B → A, or
|
|
273
|
+
// the same path again, is a new navigation). It did not happen (a leave guard said no) or it is not the chat:
|
|
274
|
+
// nothing. Left meanwhile (another page, another navigation): nothing (#1609 review).
|
|
275
|
+
const nav = navStore.get();
|
|
276
|
+
if (!nav.route || nav.route.panel !== "chat") return;
|
|
277
|
+
const mine = nav.seq;
|
|
278
|
+
const current = () => navStore.get().seq === mine;
|
|
279
|
+
// The tour points at the chat's own controls: start it once the CHAT has drawn — its Send button, not #main (the
|
|
280
|
+
// shell's landmark is on every page, Settings included) — waiting at most ~3 s (#1608 review); a chat that has not
|
|
281
|
+
// drawn by then (its module failed, an unknown instance) gets no tour. The focus goes to the composer first: the
|
|
282
|
+
// tour returns it there when it ends, not to this button, which is gone.
|
|
283
|
+
let tries = 0;
|
|
284
|
+
const go = () => {
|
|
285
|
+
if (!current()) return;
|
|
286
|
+
if (!document.getElementById("sendBtn")) { if (++tries <= 30) setTimeout(go, 100); return; }
|
|
287
|
+
const here = document.getElementById("msgIn") || document.getElementById("main");
|
|
288
|
+
if (here && typeof here.focus === "function") here.focus();
|
|
289
|
+
if (current()) startTour();
|
|
290
|
+
};
|
|
291
|
+
setTimeout(go, 0);
|
|
292
|
+
};
|
|
293
|
+
return html`<section class="card s-device"><h3>${tn("sec_device")}</h3>
|
|
294
|
+
<p class="sub">${tn("deviceLead")}</p>
|
|
295
|
+
<${DevicePrefs} labels=${true} />
|
|
296
|
+
${P() ? html`<label class="check"><input type="checkbox" checked=${on} onChange=${(e) => { const want = e.target.checked; setPreviewOptIn(want).then(setOn); }} />
|
|
297
|
+
<span>${t("chat.previews")}</span></label>` : null}
|
|
298
|
+
<div class="s-device-row"><button type="button" id="tourBtn" class="btn" title=${t("chat.tourBtnTitle")} onClick=${replay}><${Icon} name="info" size=${16} />${t("chat.tour")}</button></div>
|
|
299
|
+
</section>`;
|
|
300
|
+
}
|
|
301
|
+
|
|
253
302
|
/** The operation in hand still owns Apply: its writes and job, or a restart of AgEnD from the press to its end. */
|
|
254
303
|
const opBusy = (op) => !!op && (["writing", "posting", "watching"].includes(op.phase) || op.restart === "busy");
|
|
255
304
|
function dropSecrets(staged) { for (const c of staged.values()) if (c.connectionSecret) c.connectionSecret.secret = ""; }
|
|
@@ -217,6 +217,7 @@ register("settings", {
|
|
|
217
217
|
concurrencyWarning: "May exceed CPU capacity on smaller hosts",
|
|
218
218
|
staggerRange: "Must be between 0 and 30000",
|
|
219
219
|
staggerWarning: "Very low values can cause startup spikes",
|
|
220
|
+
sec_device: "This device", deviceLead: "Choices for this browser only — other devices keep their own.",
|
|
220
221
|
sec_agents: "Agents",
|
|
221
222
|
sec_bots: "Connections",
|
|
222
223
|
sec_classic: "ClassicBot",
|
|
@@ -561,6 +562,7 @@ register("settings", {
|
|
|
561
562
|
concurrencyWarning: "在較小的主機上可能超出 CPU 負荷",
|
|
562
563
|
staggerRange: "必須介於 0 到 30000",
|
|
563
564
|
staggerWarning: "數值太低可能造成啟動尖峰",
|
|
565
|
+
sec_device: "這台裝置", deviceLead: "只套用在這個瀏覽器,其他裝置保留各自的設定。",
|
|
564
566
|
sec_agents: "Agent",
|
|
565
567
|
sec_bots: "連線",
|
|
566
568
|
sec_classic: "ClassicBot",
|
|
@@ -45,12 +45,11 @@ register("app", {
|
|
|
45
45
|
needsYou: "needs you", approxNote: "Read from the terminal, so approximate", chat: "Chat", details: "Details", instanceViews: "Views of {0}",
|
|
46
46
|
textSize: "Text size: {0}", textSize_s: "S", textSize_m: "M", textSize_l: "L", textSize_fit: "Fit", usage: "Usage",
|
|
47
47
|
statusRunning: "running", statusStopped: "stopped", statusCrashed: "crashed", statusWorking: "working", statusStuck: "looks stuck", statusIdle: "idle",
|
|
48
|
-
connPolling: "Live updates paused — refreshing every 5 s", connDown: "Disconnected — trying again…", connConnecting: "Connecting…",
|
|
49
48
|
connReconnecting: "Can't reach AgEnD — it may be restarting. Reconnecting…", connReconnectingIn: "Can't reach AgEnD — it may be restarting. Trying again in {0} s.",
|
|
50
49
|
hydrating: "Loading what is open now…", hydrateFailed: "Could not load what is open now (prompts, ticks).",
|
|
51
50
|
skip: "Skip to the conversation", loadFailed: "This could not be loaded.", retry: "Try again", loading: "Loading…",
|
|
52
51
|
noInstances: "No instances yet", noInstancesHint: "Create one to start chatting.",
|
|
53
|
-
session: "Session", signIn: "Sign in", signedIn: "Signed in", signedOut: "Not signed in", thisDevice: "This device",
|
|
52
|
+
session: "Session", signIn: "Sign in", signedIn: "Signed in", signedOut: "Not signed in", thisDevice: "This device", devicePrefs: "Theme and language",
|
|
54
53
|
lastActive: "active {0}", justNow: "just now", minAgo: "{0} min ago", hrAgo: "{0} h ago", signOut: "Sign out",
|
|
55
54
|
signOutAll: "Sign out everywhere", devices: "Signed-in devices", expires: "Ends by {0} at the latest, or after {1} unused",
|
|
56
55
|
failed: "That did not work — try again.", close: "Close", more: "More actions", menu: "Menu",
|
|
@@ -75,12 +74,11 @@ register("app", {
|
|
|
75
74
|
needsYou: "等你回應", approxNote: "從終端畫面判讀,僅供參考", chat: "聊天", details: "詳細資訊", instanceViews: "{0} 的檢視",
|
|
76
75
|
textSize: "字級:{0}", textSize_s: "小", textSize_m: "中", textSize_l: "大", textSize_fit: "剛好填滿", usage: "用量",
|
|
77
76
|
statusRunning: "執行中", statusStopped: "已停止", statusCrashed: "已當掉", statusWorking: "工作中", statusStuck: "似乎卡住", statusIdle: "閒置",
|
|
78
|
-
connPolling: "即時更新暫停中——每 5 秒重新整理", connDown: "已斷線——正在重試…", connConnecting: "連線中…",
|
|
79
77
|
connReconnecting: "連不上 AgEnD——可能正在重新啟動。正在重新連線…", connReconnectingIn: "連不上 AgEnD——可能正在重新啟動。{0} 秒後再試。",
|
|
80
78
|
hydrating: "正在載入目前的狀態…", hydrateFailed: "無法載入目前的狀態(提示、送達標記)。",
|
|
81
79
|
skip: "跳到對話", loadFailed: "無法載入。", retry: "再試一次", loading: "載入中…",
|
|
82
80
|
noInstances: "還沒有 instance", noInstancesHint: "建立一個就能開始聊天。",
|
|
83
|
-
session: "登入狀態", signIn: "登入", signedIn: "已登入", signedOut: "尚未登入", thisDevice: "這台裝置",
|
|
81
|
+
session: "登入狀態", signIn: "登入", signedIn: "已登入", signedOut: "尚未登入", thisDevice: "這台裝置", devicePrefs: "主題與語言",
|
|
84
82
|
lastActive: "{0}活動", justNow: "剛剛", minAgo: "{0} 分鐘前", hrAgo: "{0} 小時前", signOut: "登出",
|
|
85
83
|
signOutAll: "全部登出", devices: "已登入的裝置", expires: "最晚 {0} 結束;閒置 {1} 也會結束",
|
|
86
84
|
failed: "沒有成功,請再試一次。", close: "關閉", more: "更多動作", menu: "選單",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// a test runs both over one list of cases). Pure: no DOM, no history.
|
|
3
3
|
|
|
4
4
|
export const FLEET_TABS = ["tasks", "schedules", "teams", "org", "cache", "config"];
|
|
5
|
-
export const SETTINGS_SECTIONS = ["agents", "bots", "classic", "general", "advanced"];
|
|
5
|
+
export const SETTINGS_SECTIONS = ["device", "agents", "bots", "classic", "general", "advanced"]; // #1604: device first
|
|
6
6
|
const MAX_INSTANCE_NAME = 128;
|
|
7
7
|
const PATH_SEPARATOR_OR_CONTROL = /[/\\\u0000-\u001f\u007f]/;
|
|
8
8
|
|
|
@@ -14,7 +14,7 @@ import { ErrorState, Skeleton } from "./ui-states.js";
|
|
|
14
14
|
import { InstanceNav, RosterNav } from "./instance-nav.js";
|
|
15
15
|
|
|
16
16
|
/** Shell state shared with panels: the phone drawer, the collapsed sidebar, extensions a panel module adds. */
|
|
17
|
-
export const shellStore = createStore({ drawer: false, collapsed: readCollapsed(),
|
|
17
|
+
export const shellStore = createStore({ drawer: false, collapsed: readCollapsed(), keyboard: false, dialog: null, side: null });
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* The mounted panel's own sidebar section, in place of the instance list (View's roster: its groups, filter and order,
|
|
@@ -49,10 +49,6 @@ export function toggleSidebar() {
|
|
|
49
49
|
shellStore.set({ collapsed });
|
|
50
50
|
try { localStorage.setItem("agend_sidebar", collapsed ? "hidden" : "shown"); } catch { /* this page only */ }
|
|
51
51
|
}
|
|
52
|
-
/** A panel module adds a control to the sidebar footer (the chat adds the preview opt-in and the tour). */
|
|
53
|
-
export function addFooterItem(key, Component) {
|
|
54
|
-
shellStore.set(s => ({ ...s, footer: [...s.footer.filter(x => x.key !== key), { key, Component }] }));
|
|
55
|
-
}
|
|
56
52
|
|
|
57
53
|
// ── Keys ── One document listener for the page. Esc closes what is on top first (the drawer); then the panel's own
|
|
58
54
|
// handlers run — only for the mounted panel, and only when the focus is inside it or on nothing (the body).
|
|
@@ -125,8 +121,8 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
|
|
|
125
121
|
<nav class="side-nav" aria-label=${t("app.menu")}>${navLink("view", "/view", "view", t("app.view"))}</nav>
|
|
126
122
|
${shell.side ? html`<${shell.side.Component} />` : html`<${ViewOnlyNav} route=${route} />`}
|
|
127
123
|
<div class="side-foot">
|
|
128
|
-
<${Prefs} />
|
|
129
124
|
<a class="side-row" href=${signInHref()}><${Icon} name="user" /><span>${t("app.signIn")}</span></a>
|
|
125
|
+
<${PrefsPopover} />
|
|
130
126
|
</div>
|
|
131
127
|
</aside>`;
|
|
132
128
|
}
|
|
@@ -145,8 +141,6 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
|
|
|
145
141
|
exec=${app.exec} awaiting=${app.awaiting} onPick=${closeDrawer} />`}
|
|
146
142
|
<div class="side-foot">
|
|
147
143
|
${navLink("settings", settingsPath(), "settings", t("app.settings"))}
|
|
148
|
-
${shell.footer.map(({ key, Component }) => html`<${Component} key=${key} />`)}
|
|
149
|
-
<${Prefs} />
|
|
150
144
|
<${SessionMenu} />
|
|
151
145
|
</div>
|
|
152
146
|
</aside>`;
|
|
@@ -157,19 +151,31 @@ function ViewOnlyNav({ route }) {
|
|
|
157
151
|
return html`<${RosterNav} route=${route} viewOnly=${true} onPick=${closeDrawer} />`;
|
|
158
152
|
}
|
|
159
153
|
|
|
160
|
-
|
|
154
|
+
/**
|
|
155
|
+
* This browser's theme and language (#1604: Settings → This device; for the anonymous View reader, the footer's
|
|
156
|
+
* popover). The same stores as ever: AgendTheme (theme.js) and setLang (app-i18n.js) — only where they are shown moved.
|
|
157
|
+
*/
|
|
158
|
+
export function DevicePrefs({ labels = false } = {}) {
|
|
161
159
|
const [theme, setTheme] = useState(typeof AgendTheme !== "undefined" ? AgendTheme.get() : "system");
|
|
162
160
|
const [l, setL] = useState(lang());
|
|
163
|
-
return html`<div class
|
|
164
|
-
<label class="pref"><${Icon} name="sun" size=${16} /><span class
|
|
161
|
+
return html`<div class=${`prefs${labels ? " labelled" : ""}`}>
|
|
162
|
+
<label class="pref"><${Icon} name="sun" size=${16} /><span class=${labels ? "pref-label" : "sr-only"}>${t("app.theme")}</span>
|
|
165
163
|
<select value=${theme} aria-label=${t("app.theme")} onChange=${(e) => { setTheme(e.target.value); if (typeof AgendTheme !== "undefined") AgendTheme.set(e.target.value); }}>
|
|
166
164
|
<option value="system">${t("app.themeSystem")}</option><option value="light">${t("app.themeLight")}</option><option value="dark">${t("app.themeDark")}</option></select></label>
|
|
167
|
-
<label class="pref"><${Icon} name="globe" size=${16} /><span class
|
|
165
|
+
<label class="pref"><${Icon} name="globe" size=${16} /><span class=${labels ? "pref-label" : "sr-only"}>${t("app.language")}</span>
|
|
168
166
|
<select value=${l} aria-label=${t("app.language")} onChange=${(e) => { setL(e.target.value); setLang(e.target.value); }}>
|
|
169
167
|
<option value="en">English</option><option value="zh-TW">中文</option></select></label>
|
|
170
168
|
</div>`;
|
|
171
169
|
}
|
|
172
170
|
|
|
171
|
+
/** #1604: the anonymous View reader has no Settings: theme and language from one compact control beside Sign in. */
|
|
172
|
+
function PrefsPopover() {
|
|
173
|
+
return html`<details class="prefs-pop">
|
|
174
|
+
<summary class="icon-btn" aria-label=${t("app.devicePrefs")} title=${t("app.devicePrefs")}><${Icon} name="sun" /></summary>
|
|
175
|
+
<div class="pop" role="group" aria-label=${t("app.devicePrefs")}><${DevicePrefs} labels=${true} /></div>
|
|
176
|
+
</details>`;
|
|
177
|
+
}
|
|
178
|
+
|
|
173
179
|
/**
|
|
174
180
|
* The header every panel shows: ☰ (phone, or a collapsed sidebar), its title and status, then its actions. `nav`
|
|
175
181
|
* (#1523 N2): an instance page's view switch — beside the title on a wide screen, a row of tabs under it on a phone.
|
|
@@ -228,9 +234,10 @@ function ConnectionLine() {
|
|
|
228
234
|
${hydration === "failed" && retryHydration ? html` <button type="button" class="btn btn-sm" onClick=${() => retryHydration()}>${t("app.retry")}</button>` : null}</div>`;
|
|
229
235
|
}
|
|
230
236
|
if (connection === "reconnecting") return html`<${Reconnecting} />`;
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
237
|
+
// #1603: polling is a working transport (the public link polls by design; a buffering proxy makes a page poll), not
|
|
238
|
+
// a fault — messages still arrive, at most ~5 s late, and there is nothing to do: no strip. Live, none, and ended
|
|
239
|
+
// (agend-auth's banner says that) draw nothing either; only reconnecting and a failed catch-up are shown.
|
|
240
|
+
return null;
|
|
234
241
|
}
|
|
235
242
|
/** #1580: the fleet is not answering (a restart): said, with when the next try is — never a silent stale page. */
|
|
236
243
|
function Reconnecting() {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// #1605: on a phone (whenever the sidebar is a drawer), a swipe right in the main area opens the drawer and a swipe left
|
|
2
|
+
// on the drawer or its scrim closes it — through the same open/close as ☰, so focus, the scrim and Esc are unchanged.
|
|
3
|
+
//
|
|
4
|
+
// Never in the way of anything that owns a sideways drag or a touch:
|
|
5
|
+
// - the left screen edge (the first EDGE px) belongs to the browser: on iOS Safari a swipe from there is "back";
|
|
6
|
+
// - a gesture that starts in something that scrolls sideways (a code block, a wide table or CSV card — found as the
|
|
7
|
+
// nearest horizontally scrollable ancestor, not a fixed list), in the terminal view (xterm), in the composer or any
|
|
8
|
+
// input, or while a dialog, the image lightbox or the preview panel is open, is left alone;
|
|
9
|
+
// - vertical scrolling is never taken: only a mostly-horizontal swipe counts (dx ≥ MIN_DX and |dy| < |dx|/2), and the
|
|
10
|
+
// listeners are passive — nothing is ever prevented, the browser scrolls as it always does.
|
|
11
|
+
|
|
12
|
+
export const SWIPE = { EDGE: 24, MIN_DX: 60 };
|
|
13
|
+
|
|
14
|
+
/** Elements a sideways gesture belongs to (their own scrolling, selection, typing or drag). */
|
|
15
|
+
function owns(el) {
|
|
16
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
17
|
+
if (tag === "input" || tag === "textarea" || tag === "select" || tag === "pre" || tag === "table" || tag === "iframe") return true;
|
|
18
|
+
if (el.isContentEditable || (el.getAttribute && el.getAttribute("contenteditable") === "true")) return true;
|
|
19
|
+
const cl = el.classList;
|
|
20
|
+
return !!cl && (cl.contains("xterm") || cl.contains("composer") || cl.contains("composer-box") || cl.contains("pv-panel")
|
|
21
|
+
|| cl.contains("lb") || cl.contains("csv-card") || cl.contains("codeblock"));
|
|
22
|
+
}
|
|
23
|
+
/** Scrolls sideways (and has room to): the gesture is the content's. */
|
|
24
|
+
function scrollsSideways(el, win) {
|
|
25
|
+
if (!(el.scrollWidth > el.clientWidth + 1)) return false;
|
|
26
|
+
const cs = win && typeof win.getComputedStyle === "function" ? win.getComputedStyle(el) : null;
|
|
27
|
+
const ox = cs ? cs.overflowX : "";
|
|
28
|
+
return ox === "auto" || ox === "scroll";
|
|
29
|
+
}
|
|
30
|
+
function blocked(target, win) {
|
|
31
|
+
for (let el = target; el && el.nodeType === 1; el = el.parentNode) {
|
|
32
|
+
if (owns(el) || scrollsSideways(el, win)) return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
function inside(target, test) {
|
|
37
|
+
for (let el = target; el && el.nodeType === 1; el = el.parentNode) if (test(el)) return true;
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Install once per page. `isDrawer()`: the sidebar is a drawer now (phone width); `isOpen()`; `open()` / `close()`: the
|
|
43
|
+
* same paths ☰ and the scrim use. Returns the uninstall function.
|
|
44
|
+
*/
|
|
45
|
+
export function installSwipe({ doc = document, win = window, isDrawer, isOpen, open, close }) {
|
|
46
|
+
let start = null; // { x, y, opening }
|
|
47
|
+
const modalUp = () => !!(doc.querySelector && (doc.querySelector("dialog[open]") || doc.querySelector(".pv-panel")))
|
|
48
|
+
|| !!(doc.documentElement && doc.documentElement.classList && doc.documentElement.classList.contains("lb-open"));
|
|
49
|
+
function onStart(e) {
|
|
50
|
+
start = null;
|
|
51
|
+
if (!e.touches || e.touches.length !== 1 || !isDrawer()) return;
|
|
52
|
+
const t = e.touches[0], target = e.target;
|
|
53
|
+
if (modalUp() || blocked(target, win)) return;
|
|
54
|
+
if (isOpen()) {
|
|
55
|
+
// Closing: a swipe left that starts on the drawer or its scrim.
|
|
56
|
+
if (inside(target, (el) => el.id === "sidebar" || (el.classList && el.classList.contains("scrim")))) start = { x: t.clientX, y: t.clientY, opening: false };
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Opening: in the main area, and never from the edge the browser owns.
|
|
60
|
+
if (t.clientX < SWIPE.EDGE) return;
|
|
61
|
+
if (!inside(target, (el) => el.id === "main")) return;
|
|
62
|
+
start = { x: t.clientX, y: t.clientY, opening: true };
|
|
63
|
+
}
|
|
64
|
+
function onEnd(e) {
|
|
65
|
+
const s = start;
|
|
66
|
+
start = null;
|
|
67
|
+
if (!s || !e.changedTouches || !e.changedTouches.length || !isDrawer()) return;
|
|
68
|
+
// A dialog, the lightbox or the preview panel that came up during the gesture still owns it (#1609 review).
|
|
69
|
+
if (modalUp()) return;
|
|
70
|
+
const t = e.changedTouches[0];
|
|
71
|
+
const dx = t.clientX - s.x, dy = t.clientY - s.y;
|
|
72
|
+
if (Math.abs(dx) < SWIPE.MIN_DX || Math.abs(dy) >= Math.abs(dx) / 2) return; // short, or mostly vertical: a scroll
|
|
73
|
+
if (s.opening && dx > 0 && !isOpen()) open();
|
|
74
|
+
else if (!s.opening && dx < 0 && isOpen()) close();
|
|
75
|
+
}
|
|
76
|
+
function onCancel() { start = null; }
|
|
77
|
+
function onMove(e) { if (start && e.touches && e.touches.length !== 1) start = null; } // a second finger: a pinch
|
|
78
|
+
const opts = { passive: true };
|
|
79
|
+
doc.addEventListener("touchstart", onStart, opts);
|
|
80
|
+
doc.addEventListener("touchmove", onMove, opts);
|
|
81
|
+
doc.addEventListener("touchend", onEnd, opts);
|
|
82
|
+
doc.addEventListener("touchcancel", onCancel, opts);
|
|
83
|
+
return () => {
|
|
84
|
+
doc.removeEventListener("touchstart", onStart, opts);
|
|
85
|
+
doc.removeEventListener("touchmove", onMove, opts);
|
|
86
|
+
doc.removeEventListener("touchend", onEnd, opts);
|
|
87
|
+
doc.removeEventListener("touchcancel", onCancel, opts);
|
|
88
|
+
};
|
|
89
|
+
}
|
package/dist/ui/shared/app.css
CHANGED
|
@@ -108,7 +108,22 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
|
|
|
108
108
|
flex-shrink: 0; font-size: 11px; line-height: 16px; font-weight: var(--fw-semibold); padding: 0 var(--s-2); border-radius: var(--r-full);
|
|
109
109
|
background: var(--warn-bg); color: var(--warn); border: 1px solid var(--warn-line); white-space: nowrap;
|
|
110
110
|
}
|
|
111
|
-
|
|
111
|
+
/* #1604: one row — Settings and Session (the anonymous View reader: Sign in and the theme/language popover). Their
|
|
112
|
+
pop-ups open over the whole footer's width. */
|
|
113
|
+
.side-foot { display: flex; align-items: center; gap: var(--s-1); padding: var(--s-2); position: relative; }
|
|
114
|
+
.side-foot > .side-row, .side-foot > .session { flex: 1; min-width: 0; }
|
|
115
|
+
.side-foot > .session { position: static; }
|
|
116
|
+
.side-foot .side-row span:not(.dot):not(.meta) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
117
|
+
.side-foot .session .meta { display: none; }
|
|
118
|
+
.prefs-pop > summary { list-style: none; cursor: pointer; }
|
|
119
|
+
.prefs-pop > summary::-webkit-details-marker { display: none; }
|
|
120
|
+
.prefs.labelled { flex-direction: column; gap: var(--s-2); padding: 0; }
|
|
121
|
+
.prefs.labelled .pref { flex: none; gap: var(--s-2); color: var(--text); }
|
|
122
|
+
.prefs.labelled .pref-label { min-width: 7em; font-size: var(--fs-sm); }
|
|
123
|
+
.prefs.labelled .pref select { flex: 0 1 14em; height: 34px; border: 1px solid var(--border); background: var(--bg); color: var(--text); font-size: var(--fs-sm); padding: 0 var(--s-2); }
|
|
124
|
+
.s-device { display: flex; flex-direction: column; gap: var(--s-3); }
|
|
125
|
+
.s-device .sub { margin: 0; color: var(--text-3); font-size: var(--fs-sm); }
|
|
126
|
+
.s-device-row { display: flex; gap: var(--s-2); }
|
|
112
127
|
.prefs { display: flex; gap: var(--s-2); padding: var(--s-1) var(--s-3); }
|
|
113
128
|
.pref { display: flex; align-items: center; gap: var(--s-1); color: var(--text-2); flex: 1; min-width: 0; }
|
|
114
129
|
.pref select { flex: 1; min-width: 0; height: 30px; border: 0; border-radius: var(--r-sm); background: transparent; color: var(--text-2); font-size: var(--fs-xs); cursor: pointer; }
|
package/dist/ui/shared/app.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// reached only by the dynamic imports below, in "full" mode. A test walks the static imports over real HTTP and asserts
|
|
7
7
|
// none of them names /ui/.
|
|
8
8
|
import { html, render } from "./app-html.js";
|
|
9
|
-
import { Shell, handleKey, showDialog, closeDrawer, retryable, setNewInstanceOpener } from "./app-shell.js";
|
|
9
|
+
import { Shell, handleKey, showDialog, closeDrawer, openDrawer, shellStore, retryable, setNewInstanceOpener } from "./app-shell.js";
|
|
10
|
+
import { installSwipe } from "./app-swipe.js";
|
|
10
11
|
import { startRouter, navStore, navigate } from "./app-nav.js";
|
|
11
12
|
import { createStream } from "./app-stream.js";
|
|
12
13
|
import { appStore, applyStatus, applyActivity } from "./app-store.js";
|
|
@@ -97,5 +98,12 @@ startRouter(window);
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
document.addEventListener("keydown", handleKey);
|
|
101
|
+
// #1605: on a phone (the sidebar is a drawer), swipe right in the main area to open it, left to close it — the same
|
|
102
|
+
// open/close as ☰ and the scrim. Nothing that owns a sideways drag (code, tables, the terminal, inputs, dialogs) and
|
|
103
|
+
// never the left edge (iOS: "back").
|
|
104
|
+
installSwipe({
|
|
105
|
+
isDrawer: () => typeof matchMedia === "function" && matchMedia("(max-width: 899px)").matches,
|
|
106
|
+
isOpen: () => shellStore.get().drawer, open: openDrawer, close: closeDrawer,
|
|
107
|
+
});
|
|
100
108
|
|
|
101
109
|
render(html`<${Shell} panels=${panels} onNewInstance=${onNewInstance} viewOnly=${mode === "view-only"} />`, document.getElementById("app"));
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
export declare const FLEET_TABS: readonly ["tasks", "schedules", "teams", "org", "cache", "config"];
|
|
14
14
|
export type FleetTab = typeof FLEET_TABS[number];
|
|
15
|
-
export declare const SETTINGS_SECTIONS: readonly ["agents", "bots", "classic", "general", "advanced"];
|
|
15
|
+
export declare const SETTINGS_SECTIONS: readonly ["device", "agents", "bots", "classic", "general", "advanced"];
|
|
16
16
|
export type SettingsSection = typeof SETTINGS_SECTIONS[number];
|
|
17
17
|
export type ShellRoute = {
|
|
18
18
|
panel: "chat";
|
package/dist/web-shell-routes.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* Settings panel (step 3; settings-api.ts serves them, signed in only).
|
|
12
12
|
*/
|
|
13
13
|
export const FLEET_TABS = ["tasks", "schedules", "teams", "org", "cache", "config"];
|
|
14
|
-
|
|
14
|
+
// #1604: "device" — this browser's own choices (theme, language, HTML previews, the tour); no configuration behind it.
|
|
15
|
+
export const SETTINGS_SECTIONS = ["device", "agents", "bots", "classic", "general", "advanced"];
|
|
15
16
|
const PATH_SEPARATOR_OR_CONTROL = /[/\\\u0000-\u001f\u007f]/;
|
|
16
17
|
/** Longest instance name a path may carry; real names are far shorter. */
|
|
17
18
|
export const MAX_INSTANCE_NAME = 128;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-shell-routes.js","sourceRoot":"","sources":["../src/web-shell-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAU,CAAC;AAE7F,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,CAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"web-shell-routes.js","sourceRoot":"","sources":["../src/web-shell-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAU,CAAC;AAE7F,uHAAuH;AACvH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,CAAU,CAAC;AAezG,MAAM,yBAAyB,GAAG,0BAA0B,CAAC;AAC7D,0EAA0E;AAC1E,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAErC;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,IAAI,iBAAiB,IAAI,CAAC,yBAAyB,CAAC,IAAI,CAAC,IAAI,CAAC;WAC9F,IAAI,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AAC5C,CAAC;AAED,2EAA2E;AAC3E,SAAS,aAAa,CAAC,OAAe;IACpC,IAAI,CAAC;QAAC,OAAO,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,IAAI,CAAC;IAAC,CAAC;AACpE,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,UAAU,CAAC,MAAc,EAAE,IAAY;IACrD,IAAI,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IACvD,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC;IACvF,IAAI,IAAI,KAAK,WAAW;QAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC;IAC5F,8GAA8G;IAC9G,wCAAwC;IACxC,IAAI,IAAI,KAAK,WAAW;QAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC;IAC9E,8GAA8G;IAC9G,MAAM,OAAO,GAAG,+BAA+B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3D,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,IAAI,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC,CAAE,CAAC,CAAC;QACxC,OAAO,IAAI,KAAK,IAAI,IAAI,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IAC5I,CAAC;IACD,MAAM,KAAK,GAAG,wBAAwB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClD,IAAI,KAAK;QAAE,OAAQ,UAAgC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC,CAAa,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACzJ,MAAM,IAAI,GAAG,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChD,IAAI,IAAI,EAAE,CAAC;QACT,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC,CAAE,CAAC,CAAC;QACrC,OAAO,IAAI,KAAK,IAAI,IAAI,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IACzI,CAAC;IACD,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC;IACzF,MAAM,IAAI,GAAG,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5C,IAAI,IAAI,EAAE,CAAC;QACT,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC,CAAE,CAAC,CAAC;QACrC,OAAO,IAAI,KAAK,IAAI,IAAI,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IACzI,CAAC;IACD,IAAI,IAAI,KAAK,WAAW;QAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,CAAC;IACpG,MAAM,QAAQ,GAAG,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpD,IAAI,QAAQ,EAAE,CAAC;QACb,OAAQ,iBAAuC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAE,CAAC;YACpE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAoB,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACtG,CAAC;IACD,sGAAsG;IACtG,OAAO,IAAI,CAAC;AACd,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,MAAM,CAAC,GAAG,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAClC,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,IAAI,KAAK,OAAO,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,KAAK,UAAU,CAAC;AAC1E,CAAC;AAED,0GAA0G;AAC1G,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,CAAC,GAAG,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAClC,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC;AACvG,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,OAAO,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,CAAC;AAC1C,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,QAAQ,CAAC,QAAgB;IACvC,OAAO,YAAY,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAC;AACpD,CAAC;AACD,MAAM,UAAU,QAAQ,CAAC,QAAgB;IACvC,OAAO,SAAS,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAC;AACjD,CAAC;AACD,MAAM,UAAU,WAAW,CAAC,QAAgB;IAC1C,OAAO,mBAAmB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAC;AAC3D,CAAC"}
|