@rightkit/system-ui 0.0.0-stage → 0.1.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 (70) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE-APACHE +201 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +36 -2
  5. package/dist/activityLease.d.ts +1 -0
  6. package/dist/activityLease.js +20 -0
  7. package/dist/api.d.ts +1011 -0
  8. package/dist/api.js +296 -0
  9. package/dist/chart.d.ts +14 -0
  10. package/dist/chart.js +94 -0
  11. package/dist/components/Chart.d.ts +40 -0
  12. package/dist/components/Chart.js +109 -0
  13. package/dist/env.d.ts +60 -0
  14. package/dist/index.d.ts +26 -0
  15. package/dist/index.js +28 -0
  16. package/dist/kept.d.ts +3 -0
  17. package/dist/kept.js +21 -0
  18. package/dist/lib/chatKind.d.ts +4 -0
  19. package/dist/lib/chatKind.js +15 -0
  20. package/dist/lib/legal.d.ts +26 -0
  21. package/dist/lib/legal.js +39 -0
  22. package/dist/lib/onboardingSettings.d.ts +2 -0
  23. package/dist/lib/onboardingSettings.js +17 -0
  24. package/dist/lib/qr.d.ts +9 -0
  25. package/dist/lib/qr.js +286 -0
  26. package/dist/lib/rk.d.ts +6 -0
  27. package/dist/lib/rk.js +30 -0
  28. package/dist/metrics.d.ts +24 -0
  29. package/dist/metrics.js +76 -0
  30. package/dist/onboarding/flow.d.ts +31 -0
  31. package/dist/onboarding/flow.js +81 -0
  32. package/dist/onboarding/gate.d.ts +8 -0
  33. package/dist/onboarding/gate.js +52 -0
  34. package/dist/styles.css +8 -0
  35. package/dist/views/Activity.d.ts +3 -0
  36. package/dist/views/Activity.js +111 -0
  37. package/dist/views/Apps.d.ts +1 -0
  38. package/dist/views/Apps.js +478 -0
  39. package/dist/views/Chat.d.ts +1 -0
  40. package/dist/views/Chat.js +255 -0
  41. package/dist/views/ChromeSnapshots.d.ts +6 -0
  42. package/dist/views/ChromeSnapshots.js +19 -0
  43. package/dist/views/Cleanup.d.ts +4 -0
  44. package/dist/views/Cleanup.js +291 -0
  45. package/dist/views/DriveHealth.d.ts +14 -0
  46. package/dist/views/DriveHealth.js +81 -0
  47. package/dist/views/Duplicates.d.ts +2 -0
  48. package/dist/views/Duplicates.js +99 -0
  49. package/dist/views/LauncherSettings.d.ts +63 -0
  50. package/dist/views/LauncherSettings.js +242 -0
  51. package/dist/views/NearbySettings.d.ts +117 -0
  52. package/dist/views/NearbySettings.js +237 -0
  53. package/dist/views/Storage.d.ts +9 -0
  54. package/dist/views/Storage.js +916 -0
  55. package/dist/views/activity.css +94 -0
  56. package/dist/views/apps.css +98 -0
  57. package/dist/views/chat.css +61 -0
  58. package/dist/views/cleanup.css +49 -0
  59. package/dist/views/health.css +17 -0
  60. package/dist/views/membrane.css +23 -0
  61. package/dist/views/monitor.css +99 -0
  62. package/dist/views/settings.css +176 -0
  63. package/dist/views/settingsKit.d.ts +14 -0
  64. package/dist/views/settingsKit.js +43 -0
  65. package/dist/views/storage.css +121 -0
  66. package/dist/views/storageEstimate.d.ts +6 -0
  67. package/dist/views/storageEstimate.js +7 -0
  68. package/dist/visible.d.ts +3 -0
  69. package/dist/visible.js +42 -0
  70. package/package.json +110 -3
@@ -0,0 +1,81 @@
1
+ import { invoke } from "@tauri-apps/api/core";
2
+ import { onboardingSettings } from "../lib/onboardingSettings.js";
3
+ // The window's own progress, kept in the hub's `hub-onboarding.json` document beside the shared component's record
4
+ // (`onboarding.state`: step, completed, a copy of the legal receipt). One versioned object holds the draft choices, the access
5
+ // choices and the commit status, saved after every change, so closing the window or restarting loses nothing.
6
+ export const FLOW_KEY = "pulseFlow";
7
+ export const FLOW_VERSION = 1;
8
+ export const freshFlow = (first) => ({ version: FLOW_VERSION, first, draft: {}, deferred: [], commit: { status: "idle", failed: {} } });
9
+ export async function loadFlow() {
10
+ try {
11
+ const snapshot = await onboardingSettings.load();
12
+ const raw = snapshot.values[FLOW_KEY];
13
+ if (!raw || raw.version !== FLOW_VERSION || typeof raw.first !== "boolean")
14
+ return null;
15
+ const commit = raw.commit && typeof raw.commit === "object" ? raw.commit : undefined;
16
+ return {
17
+ version: FLOW_VERSION,
18
+ first: raw.first,
19
+ draft: raw.draft && typeof raw.draft === "object" ? raw.draft : {},
20
+ deferred: Array.isArray(raw.deferred) ? raw.deferred.filter((id) => typeof id === "string") : [],
21
+ // A run interrupted mid-write is unfinished, not done.
22
+ commit: { status: commit?.status === "done" ? "done" : commit?.status === "partial" ? "partial" : "idle", failed: commit?.failed ?? {} },
23
+ };
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ export async function saveFlow(flow) {
30
+ await onboardingSettings.patch({ [FLOW_KEY]: flow });
31
+ }
32
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
33
+ export async function readSettings() {
34
+ try {
35
+ return ((await invoke("notch_state")).settings ?? {});
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ /** Waits for the notch to publish `key` at `value`: the proof for an answer that was only "pending". */
42
+ async function published(key, value) {
43
+ for (let attempt = 0; attempt < 8; attempt++) {
44
+ const settings = await readSettings();
45
+ if (settings && settings[key] === value)
46
+ return true;
47
+ await sleep(600);
48
+ }
49
+ return false;
50
+ }
51
+ /** One confirmed write: the notch's typed answer, or, when it has not answered yet, its published state. */
52
+ export async function writeSetting(key, value) {
53
+ let answer;
54
+ try {
55
+ answer = await invoke("notch_command", { command: { command: "set", key, value } });
56
+ }
57
+ catch (error) {
58
+ return String(error);
59
+ }
60
+ if (answer?.status === "rejected" || answer?.status === "not_saved")
61
+ return answer.reason ?? answer.status;
62
+ if (answer?.status === "committed" || answer?.status === "unchanged" || answer?.status === "applied")
63
+ return null;
64
+ return (await published(key, value)) ? null : "the notch did not confirm the change";
65
+ }
66
+ /**
67
+ * Writes each `[key, value]` that the notch does not already hold, one at a time, and returns the keys whose write was
68
+ * not confirmed. Nothing is assumed: the next call reads the published state again, so a retry repeats only what failed.
69
+ */
70
+ export async function writeAll(plan) {
71
+ const failed = {};
72
+ const current = (await readSettings()) ?? {};
73
+ for (const [key, value] of plan) {
74
+ if (current[key] === value)
75
+ continue;
76
+ const reason = await writeSetting(key, value);
77
+ if (reason)
78
+ failed[key] = reason;
79
+ }
80
+ return failed;
81
+ }
@@ -0,0 +1,8 @@
1
+ /** Settings > General > "Show onboarding again" fires this on `window` (Settings.tsx). */
2
+ export declare const SHOW_ONBOARDING = "pulse-show-onboarding";
3
+ /**
4
+ * The hub window's part of first run. It never renders onboarding: the first-run window is its own window
5
+ * (`onboarding.rs`). The native side opens it instead of the hub while the notch says setup is unfinished; this hook
6
+ * covers the two cases the native side cannot see, a new legal agreement and "Show onboarding again".
7
+ */
8
+ export declare function useFirstRunGate(section: string): void;
@@ -0,0 +1,52 @@
1
+ import { useEffect } from "react";
2
+ import { invoke } from "@tauri-apps/api/core";
3
+ import { onboardingStateKey } from "@rightkit/onboarding";
4
+ import { legalReceipt } from "../lib/legal.js";
5
+ import { onboardingSettings } from "../lib/onboardingSettings.js";
6
+ import { FLOW_KEY, freshFlow, writeSetting } from "./flow.js";
7
+ /** Settings > General > "Show onboarding again" fires this on `window` (Settings.tsx). */
8
+ export const SHOW_ONBOARDING = "pulse-show-onboarding";
9
+ async function savedState() {
10
+ const snapshot = await onboardingSettings.load();
11
+ return snapshot.values[onboardingStateKey()];
12
+ }
13
+ /**
14
+ * Whether the native receipt holds the current agreement (version and document hash). The first read also carries over
15
+ * one the web view recorded earlier, including one saved under the pre-port digest of the same documents (#475).
16
+ */
17
+ async function legalAccepted() {
18
+ return (await legalReceipt.read()) !== null;
19
+ }
20
+ /**
21
+ * Re-run: puts the shared record back to its first app step (the agreement, once accepted, is kept), starts a flow that
22
+ * keeps the current values, marks setup unfinished in the notch so an interrupted re-run resumes, and opens the window.
23
+ */
24
+ async function rerun(section) {
25
+ const saved = await savedState();
26
+ await onboardingSettings.patch({
27
+ ...(saved ? { [onboardingStateKey()]: { ...saved, currentStep: 1, completed: [], skipped: false, skippedSteps: [] } } : {}),
28
+ [FLOW_KEY]: freshFlow(false),
29
+ });
30
+ // Unconfirmed, the re-run still opens: the window itself decides again when it finishes.
31
+ await writeSetting("onboardingDone", false);
32
+ await invoke("onboarding_open", { section });
33
+ }
34
+ /**
35
+ * The hub window's part of first run. It never renders onboarding: the first-run window is its own window
36
+ * (`onboarding.rs`). The native side opens it instead of the hub while the notch says setup is unfinished; this hook
37
+ * covers the two cases the native side cannot see, a new legal agreement and "Show onboarding again".
38
+ */
39
+ export function useFirstRunGate(section) {
40
+ useEffect(() => {
41
+ void legalAccepted()
42
+ .then((ok) => (ok ? undefined : invoke("onboarding_open", { section })))
43
+ .catch(() => { });
44
+ // Once, when the hub window opens.
45
+ // eslint-disable-next-line react-hooks/exhaustive-deps
46
+ }, []);
47
+ useEffect(() => {
48
+ const again = () => void rerun(section).catch(() => { });
49
+ window.addEventListener(SHOW_ONBOARDING, again);
50
+ return () => window.removeEventListener(SHOW_ONBOARDING, again);
51
+ }, [section]);
52
+ }
@@ -0,0 +1,8 @@
1
+ /* Every stylesheet this package's components use. Hosts import "<package>/styles.css" once (Node cannot import CSS from JS). */
2
+ @import "./views/monitor.css";
3
+ @import "./views/activity.css";
4
+ @import "./views/apps.css";
5
+ @import "./views/chat.css";
6
+ @import "./views/cleanup.css";
7
+ @import "./views/health.css";
8
+ @import "./views/storage.css";
@@ -0,0 +1,3 @@
1
+ export declare function Activity({ onNavigate }: {
2
+ onNavigate: (section: string) => void;
3
+ }): import("react").JSX.Element;
@@ -0,0 +1,111 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useState } from "react";
3
+ import { Button, Select } from "@rightkit/react-shell/react";
4
+ import { api, bytes, isWindows } from "../api.js";
5
+ import { clearVisibleInterval, visibleInterval } from "../visible.js";
6
+ import { useActivityLease } from "../activityLease.js";
7
+ /** Rows per page, both for the newest page and for each "Load older". */
8
+ const PAGE = 50;
9
+ /** Refresh while the section is open. Never faster than 30 seconds. */
10
+ const REFRESH_MS = 30_000;
11
+ const SECTION_TITLES = {
12
+ cleanup: "Cleanup",
13
+ apps: "Apps",
14
+ storage: "Storage",
15
+ chat: "Chat",
16
+ };
17
+ const KIND_ORDER = ["Cleanup", "Undo", "Uninstall", "App update", "Drive health", "Scan", "Send", "Chat", "Remote", "File index", "Hub"];
18
+ const startOfDay = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate());
19
+ /** "Today", "Yesterday", or the date. */
20
+ function dayLabel(at, now) {
21
+ const day = startOfDay(new Date(at * 1000));
22
+ const today = startOfDay(now);
23
+ const yesterday = new Date(today);
24
+ yesterday.setDate(today.getDate() - 1);
25
+ if (day.getTime() === today.getTime())
26
+ return "Today";
27
+ if (day.getTime() === yesterday.getTime())
28
+ return "Yesterday";
29
+ return day.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric", year: "numeric" });
30
+ }
31
+ const clock = (at) => new Date(at * 1000).toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
32
+ const rowKey = (row) => `${row.at}|${row.kind}|${row.summary}`;
33
+ const TRASH = isWindows ? "Recycle Bin" : "Trash";
34
+ const utc = (at) => `${new Date(at * 1000).toISOString().slice(0, 16).replace("T", " ")} UTC`;
35
+ const TOTAL_TITLES = { week: "This week", month: "This month" };
36
+ export function Activity({ onNavigate }) {
37
+ const [rows, setRows] = useState(null);
38
+ const [unreadable, setUnreadable] = useState([]);
39
+ const [totals, setTotals] = useState([]);
40
+ const [more, setMore] = useState(false);
41
+ const [filter, setFilter] = useState("All");
42
+ const [busy, setBusy] = useState(false);
43
+ const [error, setError] = useState(null);
44
+ /** Replace the newest page, keeping older rows the user has already loaded. */
45
+ const refresh = useCallback(async () => {
46
+ setBusy(true);
47
+ try {
48
+ const page = await api.activityTimeline(PAGE, null);
49
+ const oldest = page.rows.length ? page.rows[page.rows.length - 1].at : null;
50
+ setRows((previous) => {
51
+ const older = oldest === null ? [] : (previous ?? []).filter((row) => row.at < oldest);
52
+ return [...page.rows, ...older];
53
+ });
54
+ setUnreadable(page.unreadable);
55
+ setTotals(page.totals ?? []);
56
+ setMore((previous) => previous || page.rows.length >= PAGE);
57
+ setError(null);
58
+ }
59
+ catch (e) {
60
+ setError(String(e));
61
+ }
62
+ finally {
63
+ setBusy(false);
64
+ }
65
+ }, []);
66
+ useEffect(() => {
67
+ void refresh();
68
+ const id = visibleInterval(() => void refresh(), REFRESH_MS);
69
+ return () => clearVisibleInterval(id);
70
+ }, [refresh]);
71
+ // Live-activity lease (#448): the notch's session monitors poll every 1 s while this page is visible.
72
+ useActivityLease();
73
+ const loadOlder = async () => {
74
+ if (!rows || rows.length === 0)
75
+ return;
76
+ setBusy(true);
77
+ try {
78
+ // `before` is exclusive; one second past the oldest row shown brings back its
79
+ // whole second, and rows already shown are dropped below.
80
+ const last = rows[rows.length - 1];
81
+ const page = await api.activityTimeline(PAGE, last.at + 1);
82
+ setRows((previous) => {
83
+ const seen = new Set((previous ?? []).map(rowKey));
84
+ return [...(previous ?? []), ...page.rows.filter((row) => !seen.has(rowKey(row)))];
85
+ });
86
+ setUnreadable(page.unreadable);
87
+ setMore(page.rows.length >= PAGE);
88
+ setError(null);
89
+ }
90
+ catch (e) {
91
+ setError(String(e));
92
+ }
93
+ finally {
94
+ setBusy(false);
95
+ }
96
+ };
97
+ const kinds = useMemo(() => KIND_ORDER.filter((kind) => (rows ?? []).some((row) => row.kind === kind)), [rows]);
98
+ const current = kinds.includes(filter) ? filter : "All";
99
+ const visible = useMemo(() => (rows ?? []).filter((row) => current === "All" || row.kind === current), [rows, current]);
100
+ const now = new Date();
101
+ const groups = [];
102
+ for (const row of visible) {
103
+ const label = dayLabel(row.at, now);
104
+ const last = groups[groups.length - 1];
105
+ if (last && last.label === label)
106
+ last.rows.push(row);
107
+ else
108
+ groups.push({ label, rows: [row] });
109
+ }
110
+ return (_jsxs("div", { className: "ck-activity", children: [_jsxs("div", { className: "ck-activity-head", children: [_jsx("p", { className: "muted", children: "What Pulse did and saw, newest first. Read only: nothing is recorded here." }), _jsx(Select, { className: "ck-activity-select", label: "Filter by kind", value: current, disabled: rows === null, onChange: setFilter, options: [{ value: "All", label: "All" }, ...kinds.map((kind) => ({ value: kind, label: kind }))] })] }), error && _jsxs("p", { className: "ck-activity-error", role: "alert", children: ["Activity could not be loaded: ", error] }), rows === null && !error && _jsx("p", { className: "muted", children: "Loading activity\u2026" }), rows !== null && rows.length === 0 && (_jsxs("div", { className: "ck-activity-empty", children: [_jsx("strong", { children: "Nothing recorded yet" }), _jsx("p", { className: "muted", children: "Cleanup runs and undos, uninstalls, drive alerts, storage scans, file transfers and chat messages will appear here as they happen." })] })), rows !== null && rows.length > 0 && visible.length === 0 && (_jsxs("p", { className: "muted", children: ["No ", current.toLowerCase(), " records in the rows loaded so far."] })), totals.length > 0 && current === "All" && (_jsxs("section", { className: "ck-activity-day", "aria-label": "Totals", children: [_jsx("h2", { children: "Totals" }), _jsx("ul", { className: "ck-activity-list", children: totals.map((t) => (_jsxs("li", { className: "ck-activity-row", children: [_jsx("span", { className: "ck-activity-kind", children: TOTAL_TITLES[t.period] }), _jsxs("span", { className: "ck-activity-summary", children: [t.events, " ", t.events === 1 ? "record" : "records", ", ", t.transfers, " ", t.transfers === 1 ? "transfer" : "transfers", ",", " ", bytes(t.moved_bytes), " moved to the ", TRASH, " (not freed). From ", utc(t.from), " to ", utc(t.to), "."] })] }, t.period))) }), _jsx("p", { className: "muted ck-activity-note", children: "Totals cover the newest records each source keeps, not every record ever made." })] })), groups.map((group) => (_jsxs("section", { className: "ck-activity-day", "aria-label": group.label, children: [_jsx("h2", { children: group.label }), _jsx("ul", { className: "ck-activity-list", children: group.rows.map((row) => (_jsxs("li", { className: "ck-activity-row", children: [_jsx("time", { className: "ck-activity-time", dateTime: new Date(row.at * 1000).toISOString(), children: clock(row.at) }), _jsx("span", { className: "ck-activity-kind", children: row.kind }), _jsx("span", { className: "ck-activity-summary", children: row.summary }), row.open && (_jsxs(Button, { size: "sm", variant: "secondary", onClick: () => onNavigate(row.open), children: ["Open ", SECTION_TITLES[row.open] ?? row.open] }))] }, rowKey(row)))) })] }, group.label))), more && (_jsx("div", { className: "ck-activity-more", children: _jsx(Button, { size: "sm", variant: "secondary", onClick: () => void loadOlder(), disabled: busy, children: "Load older" }) })), unreadable.length > 0 && (_jsxs("p", { className: "muted ck-activity-note", children: ["Could not read: ", unreadable.join(", "), "."] }))] }));
111
+ }
@@ -0,0 +1 @@
1
+ export declare function Apps(): import("react").JSX.Element;