@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,237 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment, useEffect, useRef, useState } from "react";
3
+ import { invoke } from "@tauri-apps/api/core";
4
+ import { listen } from "@tauri-apps/api/event";
5
+ import { Pencil, X } from "lucide-react";
6
+ import { Button, IconButton, SegmentedControl, Toggle, Input } from "@rightkit/react-shell/react";
7
+ import { isWindows } from "../api.js";
8
+ import { SettingRow, SettingsGroup } from "@rightkit/settings-ui/core";
9
+ import { NeedsLine } from "./settingsKit.js";
10
+ import { clearVisibleInterval, visibleInterval } from "../visible.js";
11
+ import { CHAT_KIND_ORDER, chatKindName } from "../lib/chatKind.js";
12
+ const TARGET_NAMES = { claude: "Claude", codex: "Codex" };
13
+ /** Polls the sharing service; also refreshes when the service says something changed. */
14
+ export function useShareState() {
15
+ const [state, setState] = useState(null);
16
+ useEffect(() => {
17
+ let alive = true;
18
+ const load = () => {
19
+ invoke("share_state")
20
+ .then((value) => { if (alive)
21
+ setState(value); })
22
+ .catch(() => { if (alive)
23
+ setState(null); });
24
+ };
25
+ load();
26
+ const timer = visibleInterval(load, 3000);
27
+ const unlisten = listen("share-devices", load);
28
+ return () => {
29
+ alive = false;
30
+ clearVisibleInterval(timer);
31
+ void unlisten.then((f) => f());
32
+ };
33
+ }, []);
34
+ return state;
35
+ }
36
+ /** The "Chat" block: chats talking to chats on nearby computers. */
37
+ function AgentChat({ share, s, set }) {
38
+ const chat = share?.chat;
39
+ const enabled = chat?.enabled !== false;
40
+ const [message, setMessage] = useState(null);
41
+ const [report, setReport] = useState(null);
42
+ const [busy, setBusy] = useState(false);
43
+ const toggle = (on) => {
44
+ invoke("chat_set_enabled", { on }).catch((e) => setMessage(String(e)));
45
+ };
46
+ const install = () => {
47
+ setBusy(true);
48
+ setMessage(null);
49
+ invoke("chat_install_skill")
50
+ .then((r) => setReport(r))
51
+ .catch((e) => { setReport(null); setMessage(String(e)); })
52
+ .finally(() => setBusy(false));
53
+ };
54
+ let status = "";
55
+ if (!enabled)
56
+ status = "Off.";
57
+ else {
58
+ const local = chat?.localChats ?? 0;
59
+ const links = (chat?.links ?? []).map((l) => {
60
+ if (l.error) {
61
+ const since = l.lastErrorMs ? `offline ${ageText(l.lastErrorMs)}` : "offline";
62
+ return `${l.device}: ${since}: ${l.error}`;
63
+ }
64
+ const ok = l.lastOkMs ? `, ok ${ageText(l.lastOkMs)}` : "";
65
+ return `${l.chats ?? 0} on ${l.device}${ok}`;
66
+ });
67
+ status = [`${local} ${local === 1 ? "chat" : "chats"} here.`, ...links.map((t) => `${t}.`)].join(" ");
68
+ }
69
+ const activity = chat?.activity;
70
+ const outcome = activity?.sentMs
71
+ ? `Last message: ${activity.lastOutcome ?? "delivered"} · ${ageText(activity.sentMs)} ago`
72
+ : null;
73
+ return (_jsxs(_Fragment, { children: [_jsx(SettingRow, { className: "ck-set", label: "Chat", description: "Lets Claude, Codex and Muse chats on this computer message chats on linked computers over ssh. Link one with: pulse chat link <device> <ssh-host>", children: _jsx(Toggle, { checked: enabled, onChange: toggle, label: "Chat" }) }), enabled && (_jsx(SettingRow, { className: "ck-set", label: "Light the Send ring on Chat activity", description: "The Send ring pulses for a few seconds when a Chat message is sent or arrives. On by default.", children: _jsx(Toggle, { checked: s.chatRingLight !== false, onChange: (on) => set("chatRingLight", on), label: "Light the Send ring on Chat activity" }) })), enabled && (_jsx(SettingRow, { className: "ck-set", label: "Install Pulse Chat into the agent apps it finds", description: "Finds Claude Code, Codex and Muse on this computer and installs Pulse's Chat integration where it is missing, with nothing to do on your side. Muse asks once on a notch card; a Not now is not asked again until this is turned off and on. On by default.", children: _jsx(Toggle, { checked: s.chatPluginAutoInstall !== false, onChange: (on) => set("chatPluginAutoInstall", on), label: "Install Pulse Chat into the agent apps it finds" }) })), enabled && (_jsx(SettingRow, { className: "ck-set", label: "Deliver chat messages to Claude through the Pulse plugin", description: "The message arrives as added context labelled as coming from another agent, after the chat's next tool call, at the end of its turn, or with the user's next prompt. An idle chat reads it only on its next turn. Requires the Pulse plugin installed in Claude Code. Auto (the default): on once a Pulse Claude plugin hook has run on this computer within 7 days. On or Off overrides it.", children: _jsx(SegmentedControl, { label: "Deliver chat messages to Claude through the Pulse plugin", value: chat?.claudeHookDeliveryMode ?? "auto", options: [
74
+ { value: "auto", label: "Auto" },
75
+ { value: "on", label: "On" },
76
+ { value: "off", label: "Off" },
77
+ ], onChange: (v) => {
78
+ invoke("chat_set_claude_hook_delivery", { mode: v }).catch((e) => setMessage(String(e)));
79
+ } }) })), enabled && (_jsx(SettingRow, { className: "ck-set", label: "List terminal chats active in the last 6 hours", description: "Lists interactive terminal chats (Claude Code, Codex and Muse in a terminal) alongside the app chats, but only while they have been active in the last 6 hours, so old terminal sessions do not clutter the list. Off, only app chats are listed. App chats are always listed. On by default.", children: _jsx(Toggle, { checked: chat?.terminalChats !== false, onChange: (on) => {
80
+ invoke("chat_set_terminal_chats", { on }).catch((e) => setMessage(String(e)));
81
+ }, label: "List terminal chats active in the last 6 hours" }) })), enabled && (_jsx(SettingRow, { className: "ck-set", label: "Claude profiles in other home folders", description: "Lists and messages Claude chats in other Claude profiles (~/ClaudeProfiles), labelled by profile. On by default.", children: _jsx(Toggle, { checked: chat?.chatClaudeProfiles !== false, onChange: (on) => {
82
+ invoke("chat_set_claude_profiles", { on }).catch((e) => setMessage(String(e)));
83
+ }, label: "Claude profiles in other home folders" }) })), enabled && (_jsx(SettingRow, { className: "ck-set", label: "Muse chats", description: "Lists, reads and messages Muse Code chats, and lets Pulse's Muse plugin register them and hand them messages. Off, no Muse chat is listed or reached and the plugin's hooks do nothing. Install the plugin with pulse chat install --muse. On by default.", children: _jsx(Toggle, { checked: chat?.chatMuse !== false, onChange: (on) => {
84
+ invoke("chat_set_muse_chats", { on }).catch((e) => setMessage(String(e)));
85
+ }, label: "Muse chats" }) })), enabled && (_jsx(SettingRow, { className: "ck-set", label: "Stamp my words into messages", description: "Each message a chat sends carries your last three typed turns, read by Pulse from that chat's transcript, so the receiving chat can check what you asked. Up to 3 KB, only to your own linked computers. On by default.", children: _jsx(Toggle, { checked: chat?.stampUserWords !== false, onChange: (on) => {
86
+ invoke("chat_set_stamp_user_words", { on }).catch((e) => setMessage(String(e)));
87
+ }, label: "Stamp my words into messages" }) })), enabled && (_jsx(SettingRow, { className: "ck-set", label: "Start a turn in an idle Codex chat when a message arrives", description: "On, an idle Codex chat is started on the message without anyone pressing Steer, using Codex's own command line, with that chat's normal permissions, and it uses the account's Codex allowance. Only a chat that is positively idle is started, and never one the Codex app may hold: then the message waits for the chat's next prompt or tool call. Off, the message waits for the owner's next prompt. On by default. The app may not show the new turn until the chat is reopened.", children: _jsx(Toggle, { checked: chat?.codexWakeIdle !== false, onChange: (on) => {
88
+ invoke("chat_set_codex_wake_idle", { on }).catch((e) => setMessage(String(e)));
89
+ }, label: "Start a turn in an idle Codex chat when a message arrives" }) })), enabled && (_jsx(SettingRow, { className: "ck-set", label: "Start a turn in an idle Claude chat when a message arrives", description: "On, a message for an idle Claude Desktop chat opens that chat in Claude Desktop so it starts a turn, and it uses the account's Claude allowance. Only chats in Claude Desktop's default profile can be opened; a chat in another profile (Claude B) waits for its next turn. Off, the message waits in the chat's inbox. On by default.", children: _jsx(Toggle, { checked: chat?.claudeWakeIdle !== false, onChange: (on) => {
90
+ invoke("chat_set_claude_wake_idle", { on }).catch((e) => setMessage(String(e)));
91
+ }, label: "Start a turn in an idle Claude chat when a message arrives" }) })), enabled && (_jsx(SettingRow, { className: "ck-set", label: "Start a turn in an idle Muse chat when a message arrives", description: "On, Pulse would start a turn in an idle Muse chat on the message. This Muse version has no supported way to do that, so for now the message waits in the chat's inbox for its next prompt. On by default; it takes effect when Muse offers a supported entry.", children: _jsx(Toggle, { checked: chat?.museWakeIdle !== false, onChange: (on) => {
92
+ invoke("chat_set_muse_wake_idle", { on }).catch((e) => setMessage(String(e)));
93
+ }, label: "Start a turn in an idle Muse chat when a message arrives" }) })), enabled && (_jsx(SettingRow, { className: "ck-set", label: "Hidden delivery into Codex", description: "Wake an idle Codex chat with one short line; the message itself goes in as hidden context, like Claude's collapsed message row.", children: _jsx(Toggle, { checked: chat?.codexHiddenDelivery !== false, onChange: (on) => {
94
+ invoke("chat_set_codex_hidden_delivery", { on }).catch((e) => setMessage(String(e)));
95
+ }, label: "Hidden delivery into Codex" }) })), enabled && (_jsx(SettingRow, { className: "ck-set", label: "Show receipts of messages you sent in chat hooks", description: "On, a chat whose hook runs is told, in a short line, when a message it sent was seen by the receiving chat, picked up, released, refused or expired. At most one such notice a minute, batched. Off, the receipts are still kept (pulse chat receipts).", children: _jsx(Toggle, { checked: chat?.chatReceiptNotices !== false, onChange: (on) => {
96
+ invoke("chat_set_receipt_notices", { on }).catch((e) => setMessage(String(e)));
97
+ }, label: "Show receipts of messages you sent in chat hooks" }) })), enabled && (_jsx(SettingRow, { className: "ck-set", label: "Pulse skill", description: "Teaches Claude, Codex and Muse chats how to message chats on other computers. Restart a chat to see it.", children: _jsx(Button, { size: "sm", variant: "secondary", disabled: busy, onClick: install, children: "Install the Pulse skill for Claude and Codex" }) })), report && report.changes.map((c) => (_jsxs("div", { className: "ck-sub ck-foot", children: [TARGET_NAMES[c.target] ?? c.target, ": ", c.action, ". ", c.path] }, c.target))), message && _jsx("div", { className: "error", role: "alert", children: message }), _jsx("div", { className: chat?.lastError ? "error" : "ck-sub ck-foot", role: "status", children: chat?.lastError ?? status }), enabled && outcome && _jsx("div", { className: "ck-sub ck-foot", role: "status", children: outcome }), enabled && (chat?.chats?.length ?? 0) > 0 && _jsx(ChatList, { chats: chat.chats })] }));
98
+ }
99
+ /** "42s", "5min", "3h" or "2d" since `ms`. */
100
+ function ageText(ms) {
101
+ const secs = Math.max(0, Math.floor((Date.now() - ms) / 1000));
102
+ if (secs < 60)
103
+ return `${secs}s`;
104
+ if (secs < 3600)
105
+ return `${Math.floor(secs / 60)}min`;
106
+ if (secs < 86400)
107
+ return `${Math.floor(secs / 3600)}h`;
108
+ return `${Math.floor(secs / 86400)}d`;
109
+ }
110
+ function ChatRow({ c, kind }) {
111
+ const cwd = c.cwd ?? "";
112
+ const folder = cwd.split(/[\\/]/).filter(Boolean).pop() ?? "";
113
+ return (_jsxs("li", { className: "ck-chats-row", children: [_jsx("span", { className: `ck-chats-kind ck-chats-kind-${kind}`, children: chatKindName(kind) }), _jsx("span", { className: "ck-chats-name", title: c.name, children: c.name }), _jsx("span", { className: "ck-chats-folder", title: cwd || undefined, children: folder }), _jsxs("span", { className: "ck-chats-when", children: [c.updatedMs ? _jsxs("span", { className: "ck-chats-age", children: [ageText(c.updatedMs), " ago"] }) : null, _jsxs("span", { className: `ck-chats-status ck-chats-status-${c.status}`, children: [c.status, c.liveness ? ` · ${c.liveness}` : ""] })] }), c.local && ((c.unread ?? 0) > 0 || (c.evicted ?? 0) > 0) && (_jsxs("span", { className: "ck-sub ck-chats-inbox", children: ["Inbox: ", c.unread ?? 0, " unread", (c.evicted ?? 0) > 0 ? `, ${c.evicted} evicted` : ""] }))] }));
114
+ }
115
+ /** Every chat the host app can message, grouped by computer and then by kind: Claude chats live; Codex and Muse threads active when written to in the last ten minutes. */
116
+ function ChatList({ chats }) {
117
+ const devices = Array.from(new Set(chats.map((c) => c.device)));
118
+ const ordered = [...devices.filter((d) => chats.some((c) => c.device === d && c.local)), ...devices.filter((d) => !chats.some((c) => c.device === d && c.local))];
119
+ // Known kinds in their fixed order, then any other kind the listing reports, in first-seen order.
120
+ const kinds = [...CHAT_KIND_ORDER, ...Array.from(new Set(chats.map((c) => c.kind))).filter((k) => !CHAT_KIND_ORDER.includes(k))];
121
+ return (_jsx("div", { className: "ck-chats", "aria-label": "Chats Pulse can message", children: ordered.map((device) => {
122
+ const here = chats.filter((c) => c.device === device);
123
+ const counts = kinds.map((kind) => ({ kind, rows: here.filter((c) => c.kind === kind) }));
124
+ // Claude and Codex always show their count, as before; other kinds show once a chat of that kind is here.
125
+ const headed = counts.filter((g) => g.rows.length > 0 || CHAT_KIND_ORDER.slice(0, 2).includes(g.kind));
126
+ return (_jsxs("section", { className: "ck-chats-device", children: [_jsxs("h3", { className: "ck-chats-head", children: [device, " ", _jsx("span", { className: "ck-sub", children: headed.map((g) => `${g.rows.length} ${chatKindName(g.kind)}`).join(" · ") })] }), _jsx("ul", { className: "ck-chats-list", children: counts.map(({ kind, rows }) => {
127
+ // Claude chats are all listed; other kinds show eight and say how many more there are.
128
+ const shown = kind === "claude" ? rows : rows.slice(0, 8);
129
+ return (_jsxs(Fragment, { children: [shown.map((c, i) => _jsx(ChatRow, { c: c, kind: kind }, `${kind}-${i}-${c.name}`)), rows.length > shown.length && _jsxs("li", { className: "ck-sub ck-chats-more", children: ["+", rows.length - shown.length, " more ", chatKindName(kind), " threads (`pulse chat peers` lists all)"] })] }, kind));
130
+ }) })] }, device));
131
+ }) }));
132
+ }
133
+ const KIND_NAMES = {
134
+ mobile: "Phone", desktop: "Computer", web: "Browser", headless: "Terminal", server: "Server",
135
+ };
136
+ const NOT_VERIFIED_NOTE = "Not verified: this app can't prove its identity. Allow asks first until it answers a check at its address.";
137
+ function proofText(d) {
138
+ if (d.proof === "certificate")
139
+ return { text: "verified (proved its key)", note: "This device proved it holds its key when it connected." };
140
+ if (d.proof === "callback")
141
+ return { text: "answered at its address (weaker: files only, kept apart, never the clipboard)", note: "This device answered a check at its network address. Anyone on the network can relay that check while the real device is online, so it only skips the question; files are kept apart and nothing reaches the clipboard." };
142
+ return { text: "not verified", note: NOT_VERIFIED_NOTE };
143
+ }
144
+ function DeviceRow({ d, run }) {
145
+ const [editing, setEditing] = useState(false);
146
+ const [draft, setDraft] = useState(d.label);
147
+ const cancelled = useRef(false);
148
+ useEffect(() => { if (!editing)
149
+ setDraft(d.label); }, [d.label, editing]);
150
+ const name = d.label || d.alias || "Unnamed device";
151
+ const proof = proofText(d);
152
+ const commit = () => {
153
+ if (cancelled.current) {
154
+ cancelled.current = false;
155
+ return;
156
+ }
157
+ const next = draft.trim();
158
+ if (next !== d.label)
159
+ run("share_trust_label", { fingerprint: d.fingerprint, label: next });
160
+ setEditing(false);
161
+ };
162
+ const what = [d.kind ? KIND_NAMES[d.kind] ?? "Device" : null, d.model].filter(Boolean).join(" · ");
163
+ const when = d.present ? "nearby now" : `last seen ${ageText(d.lastSeenMs)} ago`;
164
+ const refused = d.refused > 0 ? `refused ${d.refused} ${d.refused === 1 ? "time" : "times"}` : null;
165
+ return (_jsxs("div", { className: "ck-device", "data-device": d.fingerprint, children: [_jsxs("div", { className: "ck-text", children: [_jsxs("div", { className: "ck-account-name", children: [editing ? (_jsx(Input, { className: "ck-input ck-claude-edit", autoFocus: true, value: draft, placeholder: d.alias || "Name this device", onChange: (e) => setDraft(e.target.value), onFocus: (e) => e.target.select(), onBlur: commit, onKeyDown: (e) => {
166
+ if (e.key === "Enter")
167
+ e.target.blur();
168
+ if (e.key === "Escape") {
169
+ cancelled.current = true;
170
+ setDraft(d.label);
171
+ setEditing(false);
172
+ }
173
+ }, "aria-label": `Name for ${name}`, title: "Enter saves, Escape cancels, empty restores the device's own name" })) : (_jsxs(Button, { variant: "ghost", className: "ck-claude-name", onClick: () => setEditing(true), "aria-label": `Rename ${name}`, title: "Rename this device", children: [_jsx("span", { className: "ck-claude-name-text", children: name }), _jsx(Pencil, { size: 12, strokeWidth: 1.75, "aria-hidden": "true" })] })), _jsx("span", { className: `ck-status ck-status-${d.proof === "certificate" ? "granted" : "unknown"}`, title: proof.note, children: proof.text })] }), _jsx("div", { className: "ck-sub", children: [what, when, refused].filter(Boolean).join(" · ") }), !d.verified && d.state === "allow" && _jsx("div", { className: "ck-sub", children: NOT_VERIFIED_NOTE })] }), _jsx("div", { className: "ck-ctl", children: _jsxs("span", { className: "ck-ctls", children: [_jsx(SegmentedControl, { label: `Trust for ${name}`, value: d.state, options: [{ value: "allow", label: "Allow" }, { value: "ask", label: "Ask" }, { value: "deny", label: "Deny" }], onChange: (v) => run("share_trust_set", { fingerprint: d.fingerprint, state: v }) }), !d.present && (_jsx(IconButton, { className: "ck-forget", label: `Forget ${name}`, onClick: () => run("share_trust_forget", { fingerprint: d.fingerprint }), title: "Removes this device from the list; it returns when it is next seen.", children: _jsx(X, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }) }))] }) })] }));
174
+ }
175
+ /** Every device seen on the network or in a request, with Allow / Ask / Deny. */
176
+ function DevicesGroup({ share }) {
177
+ const trust = share.trust;
178
+ const [message, setMessage] = useState(null);
179
+ const run = (command, args) => {
180
+ setMessage(null);
181
+ invoke(command, args).catch((e) => setMessage(String(e)));
182
+ };
183
+ if (!trust)
184
+ return null;
185
+ const devices = [...trust.devices].sort((a, b) => Number(b.present) - Number(a.present) || b.lastSeenMs - a.lastSeenMs);
186
+ return (_jsxs(SettingsGroup, { hideWhenEmpty: true, headingLevel: 2, className: "ck-sgroup", rowsClassName: "ck-card", title: "Devices", children: [_jsx(SettingRow, { className: "ck-set", label: "New devices", description: "What a device gets the first time it is seen. Allow sends and receives with no question; Ask asks every time; Deny refuses.", children: _jsx(SegmentedControl, { label: "New devices", value: trust.defaultState, options: [{ value: "ask", label: "Ask" }, { value: "deny", label: "Deny" }], onChange: (v) => run("share_trust_default", { state: v }) }) }), devices.length === 0 && _jsx("div", { className: "ck-sub ck-foot", children: "No devices seen yet." }), devices.map((d) => _jsx(DeviceRow, { d: d, run: run }, d.fingerprint)), message && _jsx("div", { className: "error", role: "alert", children: message })] }));
187
+ }
188
+ /** General > Nearby sharing: send and receive files with LocalSend and other the host app Macs. */
189
+ export function NearbyGroup({ s, set }) {
190
+ const share = useShareState();
191
+ const enabled = s.nearbyEnabled !== false;
192
+ const stored = typeof s.nearbyAlias === "string" ? s.nearbyAlias : "";
193
+ const [alias, setAlias] = useState(stored);
194
+ useEffect(() => setAlias(stored), [stored]);
195
+ const folder = typeof s.nearbySaveFolder === "string" ? s.nearbySaveFolder : "";
196
+ const commitAlias = () => {
197
+ if (alias.trim() !== stored.trim())
198
+ set("nearbyAlias", alias.trim());
199
+ };
200
+ const chooseFolder = () => {
201
+ invoke("file_choose_folder")
202
+ .then((picked) => { if (picked)
203
+ set("nearbySaveFolder", picked); })
204
+ .catch(() => undefined);
205
+ };
206
+ let status = "";
207
+ if (!enabled)
208
+ status = "Off.";
209
+ else if (!share)
210
+ status = "Starting…";
211
+ else if (share.error)
212
+ status = share.error;
213
+ else if (share.running) {
214
+ const count = share.devices.length;
215
+ status = `Visible as "${share.alias ?? ""}". ${count === 0 ? "No devices nearby." : `${count} nearby.`}`;
216
+ }
217
+ return (_jsxs(_Fragment, { children: [_jsxs(SettingsGroup, { hideWhenEmpty: true, headingLevel: 2, className: "ck-sgroup", rowsClassName: "ck-card", title: "Nearby sharing", children: [_jsx(SettingRow, { className: "ck-set", label: "Send and receive files nearby", description: `Works with the LocalSend app on iPhone, Android, ${isWindows ? "Mac" : "Windows"} and Linux. Pulse can be found on your network while this is on.`, children: _jsx(Toggle, { checked: enabled, onChange: (v) => set("nearbyEnabled", v), label: "Nearby sharing" }) }), enabled && (_jsxs(_Fragment, { children: [_jsx(SettingRow, { className: "ck-set", label: "Device name", description: isWindows ? "How other devices list this PC. Empty uses the PC's name." : "How other devices list this Mac. Empty uses the Mac's name.", children: _jsx(Input, { className: "ck-input ck-input-name", value: alias, placeholder: share?.alias ?? (isWindows ? "This PC" : "This Mac"), onChange: (e) => setAlias(e.target.value), onBlur: commitAlias, onKeyDown: (e) => { if (e.key === "Enter")
218
+ e.target.blur(); }, "aria-label": "Device name" }) }), _jsx(SettingRow, { className: "ck-set", label: "Save received files to", description: folder || "Downloads", children: _jsxs("span", { className: "ck-ctls", children: [_jsx(Button, { size: "sm", variant: "secondary", onClick: chooseFolder, children: "Choose\u2026" }), folder && _jsx(Button, { size: "sm", variant: "ghost", onClick: () => set("nearbySaveFolder", ""), children: "Use Downloads" })] }) }), _jsx(AgentChat, { share: share, s: s, set: set })] })), _jsx("div", { className: share?.error ? "error" : "ck-sub ck-foot", role: share?.error ? "alert" : "status", children: status }), enabled && share?.warnings.map((w) => _jsx("div", { className: "ck-sub ck-foot", children: w }, w))] }), enabled && share?.running && _jsx(DevicesGroup, { share: share })] }));
219
+ }
220
+ /**
221
+ * The Permissions row for macOS Local Network access (Windows Firewall on Windows). Neither
222
+ * system can be asked for its state, so it is read from whether multicast sends are refused.
223
+ * With nearby sharing off nothing sends, so there is nothing to check: the row reads Off,
224
+ * as the Mac's does.
225
+ */
226
+ export function LocalNetworkRow({ enabled, nearby = true, needs }) {
227
+ const share = useShareState();
228
+ if (!enabled)
229
+ return null;
230
+ const blocked = nearby && share?.localNetwork === "blocked";
231
+ const granted = nearby && share?.localNetwork === "granted";
232
+ const status = !nearby ? "off" : blocked ? "needsApproval" : granted ? "granted" : "unknown";
233
+ const text = !nearby ? "Off" : blocked ? "Needs approval" : granted ? "Granted" : "Checking…";
234
+ return (_jsxs("div", { className: "ck-permrow", children: [_jsxs("div", { className: "ck-set", children: [_jsxs("div", { className: "ck-text", children: [_jsx("strong", { children: isWindows ? "Windows Firewall" : "Local network" }), _jsx("div", { className: "ck-sub", children: isWindows
235
+ ? "Lets Pulse find and talk to nearby devices for sharing files. Windows Firewall must allow Pulse on private networks."
236
+ : "Lets Pulse find and talk to nearby devices for sharing files. macOS asks the first time a device connects." })] }), _jsx("div", { className: "ck-ctl", children: _jsxs("span", { className: "ck-ctls", children: [_jsx("span", { className: `ck-status ck-status-${status}`, children: text }), _jsx(Button, { size: "sm", variant: "secondary", disabled: granted, onClick: () => invoke("open_local_network_settings"), children: isWindows ? "Open Firewall" : "Open Settings" })] }) })] }), _jsx(NeedsLine, { needs: needs })] }));
237
+ }
@@ -0,0 +1,9 @@
1
+ /** `autoScan` is Settings > General > Background > "Scan when Storage or Cleanup opens" (on by default). */
2
+ export declare function Storage({ autoScan }: {
3
+ autoScan?: boolean;
4
+ }): import("react").JSX.Element;
5
+ export declare function Bar({ fraction, height, color }: {
6
+ fraction: number;
7
+ height?: number;
8
+ color?: string;
9
+ }): import("react").JSX.Element;