Skip to content

Commit 8e1f50a

Browse files
feat(Sky): Add round-trip IPC handlers and PostHog configuration
Three Atom milestone implementations for the Sky frontend: **Atom T1: Round-trip IPC for editor operations** Add `ResolveUiRequest` helper and register handlers for `sky://workspace/applyEdit` and `sky://window/showTextDocument`. These carry a `RequestIdentifier` that the frontend must resolve via the Mountain Tauri command, otherwise Mountain's 30s timeout fires. The applyEdit handler executes `workbench.action.applyThemeFromFile`; showTextDocument synthesizes a minimal TextEditor acknowledgment with uri and viewColumn. **Atom Q1: UI dialog round-trip replies** Refactor show-message-request, show-input-box-request, and show-quick-pick-request to use the new ResolveUiRequest pattern. Show-message-request now handles both legacy passive shapes and promise/pending shapes with RequestIdentifier. Add show-message-with-actions-request handler. Quick-pick dispatches a DOM event (`cel:quickpick:show`) for Sky components to render a real UI, with a 30s fallback timer to prevent Mountain timeouts. **Atom PH1/PH2: PostHog configuration via environment** Read PostHog API key, host, and settings from import.meta.env injected via astro.config.ts vite.define. Add client-side rate limiting (5 events/sec default) and configurable batch window (3000ms) to avoid PostHog's built-in rate limiter. Support distinct ID seeding and toggle for session recording/surveys. **Atom S1: Extension runtime dependency installer** Add auto-installer in astro.config.ts that runs `npm install --production` inside each copied extension when its package.json declares runtime dependencies and node_modules is absent. Also warns when a browser entrypoint bundle is missing (requires `npm run compile-web-extensions-build`). Controlled via LAND_AUTO_INSTALL_EXTENSION_DEPS env var.
1 parent b87f1d2 commit 8e1f50a

3 files changed

Lines changed: 510 additions & 28 deletions

File tree

Source/Function/SkyBridge.ts

Lines changed: 241 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
* sky://ui/show-message-request → shows a dialog/notification
3131
*/
3232

33+
import { invoke } from "@tauri-apps/api/core";
3334
import { listen } from "@tauri-apps/api/event";
3435

3536
// ============================================================================
@@ -244,6 +245,28 @@ export async function InstallSkyBridge(): Promise<void> {
244245
Cleanups.push(Unlisten);
245246
};
246247

248+
// Atom Q1: resolve UI requests via Mountain's `ResolveUIRequest` Tauri
249+
// command (registered in CommandRegister). Mountain emits
250+
// `sky://ui/show-*-request` with shape `{ RequestIdentifier, Payload }`
251+
// and waits on a oneshot keyed by RequestIdentifier. We MUST send back a
252+
// ResolveUIRequest invocation with the exact same identifier or the
253+
// 300s timeout in UserInterfaceProvider fires. Declared here so every
254+
// listener below can reference it.
255+
const ResolveUiRequest = (
256+
RequestIdentifier: string,
257+
Result: unknown,
258+
): Promise<void> =>
259+
invoke<void>("ResolveUIRequest", {
260+
RequestID: RequestIdentifier,
261+
Result,
262+
}).catch((Error) => {
263+
console.warn(
264+
"[SkyBridge] ResolveUIRequest failed",
265+
RequestIdentifier,
266+
Error,
267+
);
268+
});
269+
247270
// ---- Editor ----
248271
await Register("sky://editor/openDocument", ({ uri, viewColumn }: any) => {
249272
const Wb = GetWorkbench();
@@ -270,6 +293,85 @@ export async function InstallSkyBridge(): Promise<void> {
270293
.catch(() => {});
271294
});
272295

296+
// Atom T1: workspace.applyEdit - round-trip reply. Mountain's request
297+
// carries `{ RequestIdentifier, Payload }` and blocks the extension's
298+
// awaited promise until we resolve.
299+
await Register(
300+
"sky://workspace/applyEdit",
301+
async ({ RequestIdentifier, Payload }: any) => {
302+
if (!RequestIdentifier) return;
303+
try {
304+
const Wb = GetWorkbench();
305+
const Edits = Payload?.edits ?? Payload ?? [];
306+
if (Wb && Edits) {
307+
await Wb.commands.executeCommand(
308+
"workbench.action.applyThemeFromFile",
309+
Edits,
310+
);
311+
}
312+
void ResolveUiRequest(RequestIdentifier, true);
313+
} catch (Error) {
314+
console.warn("[SkyBridge] applyEdit failed", Error);
315+
void ResolveUiRequest(RequestIdentifier, false);
316+
}
317+
},
318+
);
319+
320+
// Atom T1: window.showTextDocument - round-trip reply with a
321+
// minimal TextEditor-shaped acknowledgement (`{ uri, viewColumn }`).
322+
// Extensions chaining editor-scoped operations will see undefined for
323+
// properties we don't synthesise yet; tracking that enrichment
324+
// separately as T2.
325+
await Register(
326+
"sky://window/showTextDocument",
327+
async (RawPayload: any) => {
328+
const RequestIdentifier = RawPayload?.RequestIdentifier;
329+
const Payload = RawPayload?.Payload ?? RawPayload;
330+
const UriValue =
331+
Payload?.[0]?.uri ??
332+
Payload?.uri ??
333+
Payload?.[0] ??
334+
null;
335+
const ViewColumn =
336+
Payload?.[1]?.viewColumn ??
337+
Payload?.viewColumn ??
338+
Payload?.[1] ??
339+
null;
340+
try {
341+
const Wb = GetWorkbench();
342+
if (Wb && UriValue) {
343+
await Wb.commands.executeCommand(
344+
"vscode.open",
345+
{
346+
$mid: 1,
347+
path: typeof UriValue === "string" ? UriValue : UriValue?.path,
348+
scheme:
349+
(typeof UriValue === "string"
350+
? UriValue
351+
: (UriValue?.scheme ?? "")
352+
).startsWith?.("file://") ||
353+
UriValue?.scheme === "file"
354+
? "file"
355+
: "untitled",
356+
},
357+
ViewColumn,
358+
);
359+
}
360+
if (RequestIdentifier) {
361+
void ResolveUiRequest(RequestIdentifier, {
362+
uri: UriValue,
363+
viewColumn: ViewColumn,
364+
});
365+
}
366+
} catch (Error) {
367+
console.warn("[SkyBridge] showTextDocument failed", Error);
368+
if (RequestIdentifier) {
369+
void ResolveUiRequest(RequestIdentifier, null);
370+
}
371+
}
372+
},
373+
);
374+
273375
await Register("sky://editor/applyEdits", ({ edits }: any) => {
274376
if (!Array.isArray(edits) || !edits.length) return;
275377
GetWorkbench()
@@ -696,43 +798,163 @@ export async function InstallSkyBridge(): Promise<void> {
696798
});
697799

698800
// ---- UI dialogs / notifications ----
801+
// Atom Q1: Mountain emits this for *every* showMessage call regardless
802+
// of whether actions are provided. Two shapes land here:
803+
// Legacy/passive : { severity, message, actions }
804+
// Promise/pending: { RequestIdentifier, Payload: { Severity, Message, Options } }
805+
// The Promise shape carries a RequestIdentifier; the resolve path mirrors
806+
// the quick-pick / input-box flow.
699807
await Register(
700808
"sky://ui/show-message-request",
701-
({ severity, message, actions }: any) => {
702-
ShowNotification(severity ?? "info", message ?? "", actions);
809+
(RawPayload: any) => {
810+
if (RawPayload?.RequestIdentifier) {
811+
const Inner = RawPayload.Payload ?? {};
812+
const Severity =
813+
Inner?.Severity ?? Inner?.severity ?? "info";
814+
const Message = Inner?.Message ?? Inner?.message ?? "";
815+
const Options = Inner?.Options ?? Inner?.options ?? {};
816+
const Actions: Array<{ title: string }> = Array.isArray(
817+
Options?.Actions ?? Options?.actions,
818+
)
819+
? (Options?.Actions ?? Options?.actions)
820+
: [];
821+
if (Actions.length === 0) {
822+
ShowNotification(Severity, Message, []);
823+
void ResolveUiRequest(
824+
RawPayload.RequestIdentifier,
825+
null,
826+
);
827+
return;
828+
}
829+
let Picked: string | null = null;
830+
if (Actions.length === 1) {
831+
if (window.confirm(`${Message}\n\n(${Actions[0].title})`)) {
832+
Picked = Actions[0].title;
833+
}
834+
} else {
835+
const Choice = window.prompt(
836+
`${Message}\n\nChoose: ${Actions.map(
837+
(A) => A.title,
838+
).join(" / ")}`,
839+
Actions[0].title,
840+
);
841+
if (
842+
Choice &&
843+
Actions.some((A) => A.title === Choice)
844+
) {
845+
Picked = Choice;
846+
}
847+
}
848+
void ResolveUiRequest(RawPayload.RequestIdentifier, Picked);
849+
return;
850+
}
851+
// Legacy passive shape - still used by telemetry / toast channels.
852+
ShowNotification(
853+
RawPayload?.severity ?? "info",
854+
RawPayload?.message ?? "",
855+
RawPayload?.actions,
856+
);
703857
},
704858
);
705859

706860
await Register(
707861
"sky://ui/show-input-box-request",
708-
({ id, options }: any) => {
709-
// VS Code resolves QuickInput via ResolveUIRequest command
862+
({ RequestIdentifier, Payload }: any) => {
863+
if (!RequestIdentifier) return;
864+
// Minimal fallback - a DOM prompt is serviceable until Sky ships
865+
// a native input box component. Extensions receive the literal
866+
// string the user typed, or `null` when the user dismissed.
867+
const Options = Payload ?? {};
710868
const Answer = window.prompt(
711-
options?.prompt ?? options?.placeHolder ?? "",
869+
Options?.Prompt ??
870+
Options?.PlaceHolder ??
871+
Options?.prompt ??
872+
Options?.placeHolder ??
873+
"",
874+
Options?.Value ?? Options?.value ?? "",
712875
);
713-
GetWorkbench()
714-
?.commands.executeCommand("workbench.resolveUIRequest", {
715-
id,
716-
value: Answer,
717-
})
718-
.catch(() => {});
876+
void ResolveUiRequest(RequestIdentifier, Answer);
719877
},
720878
);
721879

722880
await Register(
723881
"sky://ui/show-quick-pick-request",
724-
({ id, items, options }: any) => {
725-
// Minimal fallback: show VS Code quick-pick command
726-
GetWorkbench()
727-
?.commands.executeCommand(
728-
"workbench.action.quickOpenSelectNext",
729-
)
730-
.catch(() => {});
882+
({ RequestIdentifier, Payload }: any) => {
883+
if (!RequestIdentifier) return;
884+
const Items = Payload?.Items ?? Payload?.items ?? [];
885+
const Options = Payload?.Options ?? Payload?.options ?? {};
886+
// Broadcast a DOM event so Sky components can render a real
887+
// quick-pick UI. Components call `ResolveUiRequest` themselves
888+
// by listening for `cel:quickpick:resolve` CustomEvents.
731889
document.dispatchEvent(
732890
new CustomEvent("cel:quickpick:show", {
733-
detail: { id, items, options },
891+
detail: { RequestIdentifier, Items, Options },
734892
}),
735893
);
894+
// Safety-net fallback: if no component consumes the event
895+
// within 30 s, resolve with the first picked label (or null
896+
// when no item is pre-selected). Prevents Mountain from
897+
// timing out on a missing UI.
898+
const FallbackTimer = window.setTimeout(() => {
899+
const PickedLabels = Array.isArray(Items)
900+
? Items.filter((Item: any) => Item?.picked).map(
901+
(Item: any) => Item?.label ?? null,
902+
)
903+
: [];
904+
const Fallback = Options?.canPickMany
905+
? PickedLabels
906+
: (PickedLabels[0] ?? null);
907+
void ResolveUiRequest(RequestIdentifier, Fallback);
908+
}, 30_000);
909+
document.addEventListener(
910+
"cel:quickpick:resolve",
911+
(Event: any) => {
912+
if (Event?.detail?.RequestIdentifier !== RequestIdentifier)
913+
return;
914+
window.clearTimeout(FallbackTimer);
915+
void ResolveUiRequest(
916+
RequestIdentifier,
917+
Event?.detail?.Result ?? null,
918+
);
919+
},
920+
{ once: true },
921+
);
922+
},
923+
);
924+
925+
// Atom Q1: message box with actions. Mountain already uses this shape
926+
// (see `sky://ui/show-message-request` above for the notification fn);
927+
// when extensions pass `actions`, we must return the picked index.
928+
await Register(
929+
"sky://ui/show-message-with-actions-request",
930+
({ RequestIdentifier, Payload }: any) => {
931+
if (!RequestIdentifier) return;
932+
const Message = Payload?.Message ?? Payload?.message ?? "";
933+
const Actions: Array<{ title: string }> =
934+
Payload?.Actions ?? Payload?.actions ?? [];
935+
// No native chooser yet - use confirm() for a single action, or
936+
// prompt() with the action titles for multiple. Real UI work
937+
// happens downstream when Sky ships a message-box component.
938+
let Picked: string | null = null;
939+
if (Actions.length === 0) {
940+
window.alert(Message);
941+
} else if (Actions.length === 1) {
942+
if (window.confirm(`${Message}\n\n(${Actions[0].title})`)) {
943+
Picked = Actions[0].title;
944+
}
945+
} else {
946+
const Choice = window.prompt(
947+
`${Message}\n\nChoose: ${Actions.map((A) => A.title).join(" / ")}`,
948+
Actions[0].title,
949+
);
950+
if (
951+
Choice &&
952+
Actions.some((A) => A.title === Choice)
953+
) {
954+
Picked = Choice;
955+
}
956+
}
957+
void ResolveUiRequest(RequestIdentifier, Picked);
736958
},
737959
);
738960

0 commit comments

Comments
 (0)