palmier 0.8.3 → 0.8.4
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/README.md +6 -4
- package/dist/commands/pair.js +2 -0
- package/dist/commands/serve.js +0 -4
- package/dist/platform/index.js +7 -3
- package/dist/platform/macos.d.ts +32 -0
- package/dist/platform/macos.js +287 -0
- package/dist/pwa/assets/index-499vYQvR.js +120 -0
- package/dist/pwa/assets/{index-B0F9mtid.css → index-UaZFu6XL.css} +1 -1
- package/dist/pwa/assets/{web-Z1623me-.js → web-Bp48ONY3.js} +1 -1
- package/dist/pwa/assets/{web-C6lkQj9J.js → web-CyJutAy4.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.js +0 -4
- package/dist/transports/http-transport.js +1 -0
- package/package.json +1 -1
- package/palmier-server/pwa/src/App.css +191 -33
- package/palmier-server/pwa/src/App.tsx +2 -0
- package/palmier-server/pwa/src/components/CapabilityToggles.tsx +288 -0
- package/palmier-server/pwa/src/components/HostMenu.tsx +15 -312
- package/palmier-server/pwa/src/components/SessionComposer.tsx +11 -2
- package/palmier-server/pwa/src/components/SessionsView.tsx +3 -1
- package/palmier-server/pwa/src/components/TaskCard.tsx +1 -1
- package/palmier-server/pwa/src/components/TaskForm.tsx +126 -74
- package/palmier-server/pwa/src/components/TasksView.tsx +3 -1
- package/palmier-server/pwa/src/native/Device.ts +0 -2
- package/palmier-server/pwa/src/pages/Dashboard.tsx +2 -0
- package/palmier-server/pwa/src/pages/PairHost.tsx +3 -1
- package/palmier-server/pwa/src/pages/PairSetup.tsx +70 -0
- package/src/commands/pair.ts +2 -0
- package/src/commands/serve.ts +0 -3
- package/src/platform/index.ts +4 -3
- package/src/platform/macos.ts +310 -0
- package/src/rpc-handler.ts +0 -5
- package/src/transports/http-transport.ts +1 -0
- package/test/macos-plist.test.ts +112 -0
- package/dist/app-registry.d.ts +0 -10
- package/dist/app-registry.js +0 -44
- package/dist/pwa/assets/index-SYs3mcdJ.js +0 -120
- package/src/app-registry.ts +0 -52
|
@@ -5,6 +5,7 @@ import type { AgentInfo } from "../types";
|
|
|
5
5
|
|
|
6
6
|
interface SessionComposerProps {
|
|
7
7
|
agents: AgentInfo[];
|
|
8
|
+
hostPlatform?: string;
|
|
8
9
|
onStarted(taskId: string, runId?: string): void;
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -15,7 +16,7 @@ function pickDefaultAgent(agents: AgentInfo[]): string {
|
|
|
15
16
|
return agents[0]?.key ?? "";
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
export default function SessionComposer({ agents, onStarted }: SessionComposerProps) {
|
|
19
|
+
export default function SessionComposer({ agents, hostPlatform, onStarted }: SessionComposerProps) {
|
|
19
20
|
const { request } = useHostConnection();
|
|
20
21
|
const [prompt, setPrompt] = useState("");
|
|
21
22
|
const [agent, setAgent] = useState(() => pickDefaultAgent(agents));
|
|
@@ -62,7 +63,15 @@ export default function SessionComposer({ agents, onStarted }: SessionComposerPr
|
|
|
62
63
|
try {
|
|
63
64
|
const result = await request<{ task_id?: string; run_id?: string; error?: string }>(
|
|
64
65
|
"task.run_oneoff",
|
|
65
|
-
{
|
|
66
|
+
{
|
|
67
|
+
user_prompt: prompt,
|
|
68
|
+
agent,
|
|
69
|
+
yolo_mode: yoloMode,
|
|
70
|
+
// Direct runs on Windows need a visible session so interactive tools
|
|
71
|
+
// (browsers, GUI apps) can attach; background task-scheduler runs
|
|
72
|
+
// would otherwise land in session 0 with no display.
|
|
73
|
+
...(hostPlatform === "win32" ? { foreground_mode: true } : {}),
|
|
74
|
+
},
|
|
66
75
|
);
|
|
67
76
|
if (result.error) {
|
|
68
77
|
setError(result.error);
|
|
@@ -14,13 +14,14 @@ interface SessionsViewProps {
|
|
|
14
14
|
request<T = unknown>(method: string, params?: unknown, opts?: { timeout?: number }): Promise<T>;
|
|
15
15
|
subscribeEvents(hostId: string, callback: (msg: { subject: string; data: Uint8Array }) => void): () => void;
|
|
16
16
|
agents: AgentInfo[];
|
|
17
|
+
hostPlatform?: string;
|
|
17
18
|
filterTaskId?: string | null;
|
|
18
19
|
onClearFilter?: () => void;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
const PAGE_SIZE = 10;
|
|
22
23
|
|
|
23
|
-
export default function SessionsView({ connected, hostId, request, subscribeEvents, agents, filterTaskId, onClearFilter }: SessionsViewProps) {
|
|
24
|
+
export default function SessionsView({ connected, hostId, request, subscribeEvents, agents, hostPlatform, filterTaskId, onClearFilter }: SessionsViewProps) {
|
|
24
25
|
const [entries, setEntries] = useState<HistoryEntry[]>([]);
|
|
25
26
|
const [total, setTotal] = useState(0);
|
|
26
27
|
const [loading, setLoading] = useState(false);
|
|
@@ -175,6 +176,7 @@ export default function SessionsView({ connected, hostId, request, subscribeEven
|
|
|
175
176
|
const composer = !filterTaskId && (
|
|
176
177
|
<SessionComposer
|
|
177
178
|
agents={agents}
|
|
179
|
+
hostPlatform={hostPlatform}
|
|
178
180
|
onStarted={(taskId, runId) => {
|
|
179
181
|
if (runId) navigate(`/runs/${encodeURIComponent(taskId)}/${encodeURIComponent(runId)}`);
|
|
180
182
|
else navigate(`/runs/${encodeURIComponent(taskId)}`);
|
|
@@ -133,7 +133,7 @@ export default function TaskCard({ task, lastEvent, onEdit, onDelete, onViewRun
|
|
|
133
133
|
values: string[] | undefined,
|
|
134
134
|
): string {
|
|
135
135
|
if (!scheduleType) return "";
|
|
136
|
-
if (scheduleType === "on_new_notification") return "On new
|
|
136
|
+
if (scheduleType === "on_new_notification") return "On new notification";
|
|
137
137
|
if (scheduleType === "on_new_sms") return "On new SMS";
|
|
138
138
|
if (!values || values.length === 0) return "";
|
|
139
139
|
if (values.length === 1) return formatSingleValue(scheduleType, values[0]);
|
|
@@ -96,12 +96,13 @@ interface TaskFormProps {
|
|
|
96
96
|
initial?: Task;
|
|
97
97
|
agents: AgentInfo[];
|
|
98
98
|
hostPlatform?: string;
|
|
99
|
+
isNotificationListener: boolean;
|
|
99
100
|
onSaved(task: Task): void;
|
|
100
101
|
onRun(taskId: string, runId?: string): void;
|
|
101
102
|
onCancel(): void;
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun, onCancel }: TaskFormProps) {
|
|
105
|
+
export default function TaskForm({ initial, agents, hostPlatform, isNotificationListener, onSaved, onRun, onCancel }: TaskFormProps) {
|
|
105
106
|
const { request } = useHostConnection();
|
|
106
107
|
|
|
107
108
|
const defaultAgent = () => {
|
|
@@ -154,46 +155,35 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
|
|
|
154
155
|
return "";
|
|
155
156
|
})();
|
|
156
157
|
const [notificationApp, setNotificationApp] = useState<string>(initialNotificationApp);
|
|
157
|
-
const [knownApps, setKnownApps] = useState<Array<{ packageName: string; appName: string
|
|
158
|
-
const [
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
const [knownApps, setKnownApps] = useState<Array<{ packageName: string; appName: string }>>([]);
|
|
159
|
+
const [knownAppsLoading, setKnownAppsLoading] = useState(false);
|
|
160
|
+
const [appFilterOpen, setAppFilterOpen] = useState(false);
|
|
161
|
+
const [appSearch, setAppSearch] = useState("");
|
|
162
|
+
const closeAppFilter = useCallback(() => setAppFilterOpen(false), []);
|
|
163
|
+
useBackClose(appFilterOpen, closeAppFilter);
|
|
164
|
+
|
|
165
|
+
// Only the notification-listening device can enumerate installed apps. On any
|
|
166
|
+
// other client we leave the list empty and fall back to a plain packageName
|
|
167
|
+
// input — the app registry we used to maintain on the host was inconsistent
|
|
168
|
+
// across devices, so we no longer cache it.
|
|
163
169
|
useEffect(() => {
|
|
164
170
|
if (scheduleMode !== "on_new_notification") return;
|
|
171
|
+
if (!isNotificationListener || !Capacitor.isNativePlatform() || !Device) return;
|
|
165
172
|
let cancelled = false;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
for (const a of apps) {
|
|
176
|
-
if (a.packageName === PALMIER_PACKAGE) continue;
|
|
177
|
-
merged.set(a.packageName, { packageName: a.packageName, appName: a.appName, icon: a.icon });
|
|
178
|
-
}
|
|
179
|
-
flush();
|
|
180
|
-
})
|
|
181
|
-
.catch(() => {});
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
request<{ apps?: Array<{ packageName: string; appName: string }> }>("device.notifications.apps")
|
|
185
|
-
.then((res) => {
|
|
186
|
-
if (cancelled || !res.apps) return;
|
|
187
|
-
for (const a of res.apps) {
|
|
188
|
-
if (a.packageName === PALMIER_PACKAGE) continue;
|
|
189
|
-
if (!merged.has(a.packageName)) merged.set(a.packageName, a);
|
|
190
|
-
}
|
|
191
|
-
flush();
|
|
173
|
+
setKnownAppsLoading(true);
|
|
174
|
+
Device.getInstalledApps()
|
|
175
|
+
.then(({ apps }) => {
|
|
176
|
+
if (cancelled) return;
|
|
177
|
+
const PALMIER_PACKAGE = "com.palmier.app";
|
|
178
|
+
const list = apps
|
|
179
|
+
.filter((a) => a.packageName !== PALMIER_PACKAGE)
|
|
180
|
+
.sort((a, b) => (a.appName || a.packageName).localeCompare(b.appName || b.packageName));
|
|
181
|
+
setKnownApps(list);
|
|
192
182
|
})
|
|
193
|
-
.catch(() => {})
|
|
194
|
-
|
|
183
|
+
.catch(() => {})
|
|
184
|
+
.finally(() => { if (!cancelled) setKnownAppsLoading(false); });
|
|
195
185
|
return () => { cancelled = true; };
|
|
196
|
-
}, [scheduleMode]);
|
|
186
|
+
}, [scheduleMode, isNotificationListener]);
|
|
197
187
|
|
|
198
188
|
// Sender filter for on_new_sms tasks. Empty string = any sender; non-empty =
|
|
199
189
|
// whitelist a single sender (stored as a single-entry schedule_values array
|
|
@@ -444,7 +434,7 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
|
|
|
444
434
|
<option value="daily">Daily</option>
|
|
445
435
|
<option value="weekly">Weekly</option>
|
|
446
436
|
<option value="monthly">Monthly</option>
|
|
447
|
-
<option value="on_new_notification">On New
|
|
437
|
+
<option value="on_new_notification">On New Notification</option>
|
|
448
438
|
<option value="on_new_sms">On New SMS</option>
|
|
449
439
|
<option value="command">Command-triggered</option>
|
|
450
440
|
</select>
|
|
@@ -453,57 +443,53 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
|
|
|
453
443
|
<div className="schedule-reactive">
|
|
454
444
|
<p className="command-help-text">
|
|
455
445
|
{scheduleMode === "on_new_notification"
|
|
456
|
-
? "Runs each time a new notification arrives on the paired Android device."
|
|
446
|
+
? "Runs each time a new push notification arrives on the paired Android device."
|
|
457
447
|
: "Runs each time a new SMS arrives on the paired Android device."}
|
|
458
448
|
{" "}The triggering payload is spliced into your task prompt — reference it as “the new {scheduleMode === "on_new_notification" ? "notification" : "SMS"}”.
|
|
459
449
|
</p>
|
|
460
450
|
{scheduleMode === "on_new_notification" && (() => {
|
|
461
|
-
const
|
|
462
|
-
const
|
|
463
|
-
? knownApps.filter((a) => a.packageName.toLowerCase().includes(q) || a.appName.toLowerCase().includes(q))
|
|
464
|
-
: knownApps;
|
|
451
|
+
const selected = knownApps.find((a) => a.packageName === notificationApp);
|
|
452
|
+
const selectedLabel = selected?.appName || notificationApp;
|
|
465
453
|
return (
|
|
466
454
|
<>
|
|
467
|
-
|
|
455
|
+
{isNotificationListener ? (
|
|
456
|
+
notificationApp.trim() ? (
|
|
457
|
+
<div className="app-filter-selected">
|
|
458
|
+
<span className="app-filter-selected-name">{selectedLabel}</span>
|
|
459
|
+
{selected?.appName && <span className="app-filter-selected-pkg">{selected.packageName}</span>}
|
|
460
|
+
<button
|
|
461
|
+
type="button"
|
|
462
|
+
className="app-filter-selected-clear"
|
|
463
|
+
onClick={() => setNotificationApp("")}
|
|
464
|
+
aria-label="Clear app filter"
|
|
465
|
+
disabled={saving}
|
|
466
|
+
>
|
|
467
|
+
✕
|
|
468
|
+
</button>
|
|
469
|
+
</div>
|
|
470
|
+
) : (
|
|
471
|
+
<button
|
|
472
|
+
type="button"
|
|
473
|
+
className="btn btn-link app-filter-trigger"
|
|
474
|
+
onClick={() => { setAppSearch(""); setAppFilterOpen(true); }}
|
|
475
|
+
disabled={saving}
|
|
476
|
+
>
|
|
477
|
+
Select app
|
|
478
|
+
</button>
|
|
479
|
+
)
|
|
480
|
+
) : (
|
|
468
481
|
<input
|
|
469
482
|
className="form-input"
|
|
470
483
|
type="text"
|
|
471
484
|
value={notificationApp}
|
|
472
|
-
onChange={(e) =>
|
|
473
|
-
|
|
474
|
-
onBlur={() => setAppDropdownOpen(false)}
|
|
475
|
-
placeholder={Capacitor.isNativePlatform() ? "App (optional)" : "App (optional, e.g. com.google.android.gm)"}
|
|
485
|
+
onChange={(e) => setNotificationApp(e.target.value)}
|
|
486
|
+
placeholder="App (optional), e.g. com.google.android.gm"
|
|
476
487
|
disabled={saving}
|
|
477
488
|
/>
|
|
478
|
-
|
|
479
|
-
<ul className="app-combobox-list">
|
|
480
|
-
{filtered.map((a) => (
|
|
481
|
-
<li
|
|
482
|
-
key={a.packageName}
|
|
483
|
-
className="app-combobox-row"
|
|
484
|
-
// onMouseDown instead of onClick so the input's blur doesn't close
|
|
485
|
-
// the dropdown before the selection registers.
|
|
486
|
-
onMouseDown={(e) => {
|
|
487
|
-
e.preventDefault();
|
|
488
|
-
setNotificationApp(a.packageName);
|
|
489
|
-
setAppDropdownOpen(false);
|
|
490
|
-
}}
|
|
491
|
-
>
|
|
492
|
-
{a.icon
|
|
493
|
-
? <img src={a.icon} alt="" className="app-combobox-icon" />
|
|
494
|
-
: <div className="app-combobox-icon app-combobox-icon-placeholder" />}
|
|
495
|
-
<div className="app-combobox-labels">
|
|
496
|
-
<div className="app-combobox-name">{a.appName || a.packageName}</div>
|
|
497
|
-
{a.appName && <div className="app-combobox-pkg">{a.packageName}</div>}
|
|
498
|
-
</div>
|
|
499
|
-
</li>
|
|
500
|
-
))}
|
|
501
|
-
</ul>
|
|
502
|
-
)}
|
|
503
|
-
</div>
|
|
489
|
+
)}
|
|
504
490
|
<p className="command-help-text app-filter-help">
|
|
505
491
|
{notificationApp.trim()
|
|
506
|
-
? `Only notifications from ${
|
|
492
|
+
? `Only notifications from ${selectedLabel} will trigger this task.`
|
|
507
493
|
: "Every notification from your device triggers this task."}
|
|
508
494
|
</p>
|
|
509
495
|
</>
|
|
@@ -699,6 +685,72 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
|
|
|
699
685
|
|
|
700
686
|
</>)}
|
|
701
687
|
</div>
|
|
688
|
+
{appFilterOpen && (() => {
|
|
689
|
+
const q = appSearch.trim().toLowerCase();
|
|
690
|
+
const filtered = q
|
|
691
|
+
? knownApps.filter((a) => a.packageName.toLowerCase().includes(q) || a.appName.toLowerCase().includes(q))
|
|
692
|
+
: knownApps;
|
|
693
|
+
return (
|
|
694
|
+
<div className="app-filter-overlay" onClick={closeAppFilter}>
|
|
695
|
+
<div className="app-filter-dialog" onClick={(e) => e.stopPropagation()}>
|
|
696
|
+
<div className="app-filter-header">
|
|
697
|
+
<h2>Select app</h2>
|
|
698
|
+
<button className="app-filter-close" onClick={closeAppFilter} aria-label="Close">
|
|
699
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
700
|
+
<path d="M4 4L12 12M12 4L4 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
701
|
+
</svg>
|
|
702
|
+
</button>
|
|
703
|
+
</div>
|
|
704
|
+
<input
|
|
705
|
+
className="form-input app-filter-search"
|
|
706
|
+
type="text"
|
|
707
|
+
value={appSearch}
|
|
708
|
+
onChange={(e) => setAppSearch(e.target.value)}
|
|
709
|
+
onKeyDown={(e) => {
|
|
710
|
+
if (e.key === "Enter" && appSearch.trim()) {
|
|
711
|
+
setNotificationApp(appSearch.trim());
|
|
712
|
+
closeAppFilter();
|
|
713
|
+
}
|
|
714
|
+
}}
|
|
715
|
+
placeholder="Search or type a package name"
|
|
716
|
+
autoFocus
|
|
717
|
+
/>
|
|
718
|
+
<ul className="app-filter-list">
|
|
719
|
+
{knownAppsLoading && knownApps.length === 0
|
|
720
|
+
? Array.from({ length: 6 }).map((_, i) => (
|
|
721
|
+
<li key={`sk-${i}`} className="app-filter-row app-filter-skeleton">
|
|
722
|
+
<div className="app-filter-skeleton-bar" />
|
|
723
|
+
</li>
|
|
724
|
+
))
|
|
725
|
+
: filtered.length === 0 && !appSearch.trim()
|
|
726
|
+
? <li className="app-filter-empty">No apps</li>
|
|
727
|
+
: filtered.map((a) => (
|
|
728
|
+
<li
|
|
729
|
+
key={a.packageName}
|
|
730
|
+
className="app-filter-row"
|
|
731
|
+
onClick={() => { setNotificationApp(a.packageName); closeAppFilter(); }}
|
|
732
|
+
>
|
|
733
|
+
<div className="app-filter-row-labels">
|
|
734
|
+
<div className="app-filter-row-name">{a.appName || a.packageName}</div>
|
|
735
|
+
{a.appName && <div className="app-filter-row-pkg">{a.packageName}</div>}
|
|
736
|
+
</div>
|
|
737
|
+
</li>
|
|
738
|
+
))}
|
|
739
|
+
{appSearch.trim() && (
|
|
740
|
+
<li
|
|
741
|
+
className="app-filter-row"
|
|
742
|
+
onClick={() => { setNotificationApp(appSearch.trim()); closeAppFilter(); }}
|
|
743
|
+
>
|
|
744
|
+
<div className="app-filter-row-labels">
|
|
745
|
+
<div className="app-filter-row-name">{appSearch.trim()}</div>
|
|
746
|
+
</div>
|
|
747
|
+
</li>
|
|
748
|
+
)}
|
|
749
|
+
</ul>
|
|
750
|
+
</div>
|
|
751
|
+
</div>
|
|
752
|
+
);
|
|
753
|
+
})()}
|
|
702
754
|
</div>
|
|
703
755
|
);
|
|
704
756
|
}
|
|
@@ -12,10 +12,11 @@ interface TasksViewProps {
|
|
|
12
12
|
subscribeEvents(hostId: string, callback: (msg: { subject: string; data: Uint8Array }) => void): () => void;
|
|
13
13
|
agents: AgentInfo[];
|
|
14
14
|
hostPlatform?: string;
|
|
15
|
+
isNotificationListener: boolean;
|
|
15
16
|
onViewRun(taskId: string, runId?: string): void;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
export default function TasksView({ connected, hostId, request, subscribeEvents, agents, hostPlatform, onViewRun }: TasksViewProps) {
|
|
19
|
+
export default function TasksView({ connected, hostId, request, subscribeEvents, agents, hostPlatform, isNotificationListener, onViewRun }: TasksViewProps) {
|
|
19
20
|
const [tasks, setTasks] = useState<Task[]>([]);
|
|
20
21
|
const [loadingTasks, setLoadingTasks] = useState(false);
|
|
21
22
|
const [taskError, setTaskError] = useState<string | null>(null);
|
|
@@ -167,6 +168,7 @@ export default function TasksView({ connected, hostId, request, subscribeEvents,
|
|
|
167
168
|
initial={editingTask}
|
|
168
169
|
agents={agents}
|
|
169
170
|
hostPlatform={hostPlatform}
|
|
171
|
+
isNotificationListener={isNotificationListener}
|
|
170
172
|
onSaved={handleTaskSaved}
|
|
171
173
|
onRun={onViewRun}
|
|
172
174
|
onCancel={closeForm}
|
|
@@ -348,6 +348,7 @@ export default function Dashboard() {
|
|
|
348
348
|
subscribeEvents={subscribeEvents}
|
|
349
349
|
agents={agents}
|
|
350
350
|
hostPlatform={hostPlatform}
|
|
351
|
+
isNotificationListener={!!activeClientToken && capabilityTokens["notifications"] === activeClientToken}
|
|
351
352
|
onViewRun={handleViewRun}
|
|
352
353
|
/>
|
|
353
354
|
)}
|
|
@@ -367,6 +368,7 @@ export default function Dashboard() {
|
|
|
367
368
|
request={request}
|
|
368
369
|
subscribeEvents={subscribeEvents}
|
|
369
370
|
agents={agents}
|
|
371
|
+
hostPlatform={hostPlatform}
|
|
370
372
|
filterTaskId={runsFilterTaskId}
|
|
371
373
|
onClearFilter={() => { if (confirmLeaveDraft()) navigate("/"); }}
|
|
372
374
|
/>
|
|
@@ -12,6 +12,7 @@ interface PairResponse {
|
|
|
12
12
|
hostId: string;
|
|
13
13
|
clientToken: string;
|
|
14
14
|
directUrl?: string;
|
|
15
|
+
hostName?: string;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
/** LAN mode: PWA is served by palmier serve (marker injected into HTML). */
|
|
@@ -82,6 +83,7 @@ export default function PairHost() {
|
|
|
82
83
|
hostId: response.hostId,
|
|
83
84
|
clientToken: response.clientToken,
|
|
84
85
|
directUrl: isLanMode ? window.location.origin : undefined,
|
|
86
|
+
...(response.hostName ? { name: response.hostName } : {}),
|
|
85
87
|
};
|
|
86
88
|
|
|
87
89
|
addPairedHost(host);
|
|
@@ -106,7 +108,7 @@ export default function PairHost() {
|
|
|
106
108
|
}
|
|
107
109
|
}
|
|
108
110
|
|
|
109
|
-
navigate("/");
|
|
111
|
+
navigate(Capacitor.isNativePlatform() ? "/pair/setup" : "/");
|
|
110
112
|
} catch (err) {
|
|
111
113
|
const message = err instanceof Error ? err.message : String(err);
|
|
112
114
|
if (message.includes("timeout") || message.includes("TIMEOUT") || message.includes("503") || message.toLowerCase().includes("no responders")) {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useNavigate } from "react-router-dom";
|
|
3
|
+
import { useHostConnection } from "../contexts/HostConnectionContext";
|
|
4
|
+
import { useHostStore } from "../contexts/HostStoreContext";
|
|
5
|
+
import CapabilityToggles from "../components/CapabilityToggles";
|
|
6
|
+
|
|
7
|
+
interface HostInfoResponse {
|
|
8
|
+
capability_tokens?: Record<string, string | null>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function PairSetup() {
|
|
12
|
+
const navigate = useNavigate();
|
|
13
|
+
const { connected, request } = useHostConnection();
|
|
14
|
+
const { getActiveHost } = useHostStore();
|
|
15
|
+
const activeHost = getActiveHost();
|
|
16
|
+
const activeClientToken = activeHost?.clientToken ?? null;
|
|
17
|
+
|
|
18
|
+
const [capabilityTokens, setCapabilityTokens] = useState<Record<string, string | null>>({});
|
|
19
|
+
const [loaded, setLoaded] = useState(false);
|
|
20
|
+
|
|
21
|
+
// If the user lands here without an active host (direct URL, refresh), bounce
|
|
22
|
+
// back to the dashboard — setup only makes sense right after pairing.
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!activeHost) navigate("/", { replace: true });
|
|
25
|
+
}, [activeHost, navigate]);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!connected || !activeHost) return;
|
|
29
|
+
let cancelled = false;
|
|
30
|
+
request<HostInfoResponse>("host.info")
|
|
31
|
+
.then((res) => {
|
|
32
|
+
if (cancelled) return;
|
|
33
|
+
setCapabilityTokens(res.capability_tokens ?? {});
|
|
34
|
+
setLoaded(true);
|
|
35
|
+
})
|
|
36
|
+
.catch(() => { if (!cancelled) setLoaded(true); });
|
|
37
|
+
return () => { cancelled = true; };
|
|
38
|
+
}, [connected, activeHost, request]);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="pair-setup">
|
|
42
|
+
<div className="pair-setup-inner">
|
|
43
|
+
<h1 className="pair-setup-title">What capabilities of this device do you want your host computer to have?</h1>
|
|
44
|
+
<p className="pair-setup-description">
|
|
45
|
+
You can change these later from the menu.
|
|
46
|
+
</p>
|
|
47
|
+
|
|
48
|
+
{!loaded ? (
|
|
49
|
+
<div className="pair-setup-loading">Connecting to host…</div>
|
|
50
|
+
) : (
|
|
51
|
+
<CapabilityToggles
|
|
52
|
+
capabilityTokens={capabilityTokens}
|
|
53
|
+
activeClientToken={activeClientToken}
|
|
54
|
+
request={request}
|
|
55
|
+
onCapabilityTokensChange={setCapabilityTokens}
|
|
56
|
+
/>
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
<div className="pair-setup-actions">
|
|
60
|
+
<button
|
|
61
|
+
className="btn btn-primary btn-full"
|
|
62
|
+
onClick={() => navigate("/", { replace: true })}
|
|
63
|
+
>
|
|
64
|
+
Finish
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
package/src/commands/pair.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as http from "node:http";
|
|
2
|
+
import * as os from "node:os";
|
|
2
3
|
import { StringCodec } from "nats";
|
|
3
4
|
import { loadConfig } from "../config.js";
|
|
4
5
|
import { connectNats } from "../nats-client.js";
|
|
@@ -21,6 +22,7 @@ function buildPairResponse(config: HostConfig, label?: string) {
|
|
|
21
22
|
return {
|
|
22
23
|
hostId: config.hostId,
|
|
23
24
|
clientToken: client.token,
|
|
25
|
+
hostName: os.hostname(),
|
|
24
26
|
};
|
|
25
27
|
}
|
|
26
28
|
|
package/src/commands/serve.ts
CHANGED
|
@@ -16,7 +16,6 @@ import { StringCodec, type NatsConnection } from "nats";
|
|
|
16
16
|
import { addNotification } from "../notification-store.js";
|
|
17
17
|
import { addSmsMessage } from "../sms-store.js";
|
|
18
18
|
import { enqueueEvent } from "../event-queues.js";
|
|
19
|
-
import { recordApp } from "../app-registry.js";
|
|
20
19
|
|
|
21
20
|
const POLL_INTERVAL_MS = 30_000;
|
|
22
21
|
const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
|
|
@@ -159,9 +158,7 @@ export async function serveCommand(): Promise<void> {
|
|
|
159
158
|
let parsed: unknown;
|
|
160
159
|
try {
|
|
161
160
|
parsed = JSON.parse(raw);
|
|
162
|
-
const data = parsed as { packageName?: string; appName?: string };
|
|
163
161
|
addNotification({ ...(parsed as object), receivedAt: Date.now() } as Parameters<typeof addNotification>[0]);
|
|
164
|
-
if (data.packageName && data.appName) recordApp(data.packageName, data.appName);
|
|
165
162
|
} catch (err) {
|
|
166
163
|
console.error("[nats] Failed to parse device notification:", err);
|
|
167
164
|
}
|
package/src/platform/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PlatformService } from "./platform.js";
|
|
2
2
|
import { LinuxPlatform } from "./linux.js";
|
|
3
3
|
import { WindowsPlatform } from "./windows.js";
|
|
4
|
+
import { MacOsPlatform } from "./macos.js";
|
|
4
5
|
|
|
5
6
|
/** Windows needs an explicit shell for execSync to resolve .cmd shims. */
|
|
6
7
|
export const SHELL: string | undefined = process.platform === "win32" ? "cmd.exe" : undefined;
|
|
@@ -9,9 +10,9 @@ let _instance: PlatformService | undefined;
|
|
|
9
10
|
|
|
10
11
|
export function getPlatform(): PlatformService {
|
|
11
12
|
if (!_instance) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
if (process.platform === "win32") _instance = new WindowsPlatform();
|
|
14
|
+
else if (process.platform === "darwin") _instance = new MacOsPlatform();
|
|
15
|
+
else _instance = new LinuxPlatform();
|
|
15
16
|
}
|
|
16
17
|
return _instance;
|
|
17
18
|
}
|