palmier 0.8.1 → 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/CLAUDE.md +13 -0
- package/README.md +16 -14
- package/dist/agents/agent.d.ts +0 -4
- package/dist/agents/claude.js +1 -1
- package/dist/agents/codex.js +2 -2
- package/dist/agents/cursor.js +1 -1
- package/dist/agents/deepagents.js +1 -1
- package/dist/agents/gemini.js +3 -2
- package/dist/agents/goose.js +1 -1
- package/dist/agents/hermes.js +1 -1
- package/dist/agents/kiro.js +1 -1
- package/dist/agents/opencode.js +1 -1
- package/dist/agents/qoder.js +1 -1
- package/dist/agents/shared-prompt.d.ts +0 -3
- package/dist/agents/shared-prompt.js +0 -3
- package/dist/commands/info.d.ts +0 -3
- package/dist/commands/info.js +0 -5
- package/dist/commands/init.d.ts +0 -3
- package/dist/commands/init.js +2 -11
- package/dist/commands/pair.d.ts +1 -4
- package/dist/commands/pair.js +3 -12
- package/dist/commands/restart.d.ts +0 -3
- package/dist/commands/restart.js +0 -3
- package/dist/commands/run.d.ts +1 -14
- package/dist/commands/run.js +18 -61
- package/dist/commands/serve.d.ts +0 -3
- package/dist/commands/serve.js +29 -27
- package/dist/config.d.ts +0 -8
- package/dist/config.js +0 -8
- package/dist/device-capabilities.d.ts +1 -1
- package/dist/event-queues.d.ts +6 -21
- package/dist/event-queues.js +6 -21
- package/dist/events.d.ts +0 -6
- package/dist/events.js +1 -9
- package/dist/index.js +0 -1
- package/dist/mcp-handler.js +1 -2
- package/dist/mcp-tools.d.ts +0 -3
- package/dist/mcp-tools.js +12 -16
- package/dist/nats-client.d.ts +0 -3
- package/dist/nats-client.js +1 -4
- package/dist/pending-requests.d.ts +4 -18
- package/dist/pending-requests.js +4 -18
- package/dist/platform/index.d.ts +1 -4
- package/dist/platform/index.js +8 -7
- package/dist/platform/linux.d.ts +3 -9
- package/dist/platform/linux.js +9 -20
- package/dist/platform/macos.d.ts +32 -0
- package/dist/platform/macos.js +287 -0
- package/dist/platform/platform.d.ts +1 -4
- package/dist/platform/windows.d.ts +2 -5
- package/dist/platform/windows.js +19 -39
- package/dist/pwa/assets/index-499vYQvR.js +120 -0
- package/dist/pwa/assets/{index-CQxcuDhM.css → index-UaZFu6XL.css} +1 -1
- package/dist/pwa/assets/{web-DOyOiwsW.js → web-Bp48ONY3.js} +1 -1
- package/dist/pwa/assets/{web-D7Kq3Nvk.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.d.ts +0 -6
- package/dist/rpc-handler.js +14 -47
- package/dist/spawn-command.d.ts +10 -25
- package/dist/spawn-command.js +7 -15
- package/dist/task.d.ts +6 -64
- package/dist/task.js +7 -70
- package/dist/transports/http-transport.d.ts +0 -4
- package/dist/transports/http-transport.js +7 -28
- package/dist/transports/nats-transport.d.ts +0 -4
- package/dist/transports/nats-transport.js +3 -9
- package/dist/types.d.ts +3 -7
- package/dist/update-checker.d.ts +1 -4
- package/dist/update-checker.js +2 -5
- package/package.json +1 -1
- package/palmier-server/pwa/src/App.css +325 -22
- 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 +20 -207
- package/palmier-server/pwa/src/components/RunDetailView.tsx +3 -3
- package/palmier-server/pwa/src/components/SessionComposer.tsx +11 -2
- package/palmier-server/pwa/src/components/SessionsView.tsx +60 -32
- package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +160 -0
- package/palmier-server/pwa/src/components/TaskCard.tsx +1 -1
- package/palmier-server/pwa/src/components/TaskForm.tsx +207 -5
- package/palmier-server/pwa/src/components/TasksView.tsx +3 -1
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/native/Device.ts +18 -2
- package/palmier-server/pwa/src/pages/Dashboard.tsx +13 -6
- package/palmier-server/pwa/src/pages/PairHost.tsx +3 -1
- package/palmier-server/pwa/src/pages/PairSetup.tsx +70 -0
- package/palmier-server/server/src/index.ts +7 -7
- package/palmier-server/server/src/routes/device.ts +4 -4
- package/palmier-server/spec.md +38 -7
- package/src/agents/agent.ts +0 -4
- package/src/agents/claude.ts +1 -1
- package/src/agents/codex.ts +2 -2
- package/src/agents/cursor.ts +1 -1
- package/src/agents/deepagents.ts +1 -1
- package/src/agents/gemini.ts +3 -2
- package/src/agents/goose.ts +1 -1
- package/src/agents/hermes.ts +1 -1
- package/src/agents/kiro.ts +1 -1
- package/src/agents/opencode.ts +1 -1
- package/src/agents/qoder.ts +1 -1
- package/src/agents/shared-prompt.ts +0 -3
- package/src/commands/info.ts +0 -5
- package/src/commands/init.ts +2 -11
- package/src/commands/pair.ts +3 -12
- package/src/commands/restart.ts +0 -3
- package/src/commands/run.ts +18 -65
- package/src/commands/serve.ts +28 -27
- package/src/config.ts +0 -8
- package/src/device-capabilities.ts +3 -2
- package/src/event-queues.ts +6 -21
- package/src/events.ts +1 -9
- package/src/index.ts +0 -1
- package/src/mcp-handler.ts +1 -2
- package/src/mcp-tools.ts +12 -18
- package/src/nats-client.ts +1 -4
- package/src/pending-requests.ts +4 -18
- package/src/platform/index.ts +5 -7
- package/src/platform/linux.ts +9 -20
- package/src/platform/macos.ts +310 -0
- package/src/platform/platform.ts +1 -4
- package/src/platform/windows.ts +19 -40
- package/src/rpc-handler.ts +14 -47
- package/src/spawn-command.ts +11 -27
- package/src/task.ts +7 -70
- package/src/transports/http-transport.ts +7 -39
- package/src/transports/nats-transport.ts +3 -9
- package/src/types.ts +3 -10
- package/src/update-checker.ts +2 -5
- package/test/macos-plist.test.ts +112 -0
- package/test/task-parsing.test.ts +2 -3
- package/test/windows-xml.test.ts +11 -12
- package/dist/pwa/assets/index-DQfOEB03.js +0 -120
|
@@ -1793,25 +1793,16 @@ body {
|
|
|
1793
1793
|
z-index: 101;
|
|
1794
1794
|
display: flex;
|
|
1795
1795
|
flex-direction: column;
|
|
1796
|
+
overflow-y: auto;
|
|
1797
|
+
overscroll-behavior: contain;
|
|
1796
1798
|
animation: drawerSlideIn 0.25s ease;
|
|
1797
1799
|
}
|
|
1798
1800
|
|
|
1799
|
-
.drawer-header {
|
|
1800
|
-
display: flex;
|
|
1801
|
-
align-items: center;
|
|
1802
|
-
justify-content: space-between;
|
|
1803
|
-
padding: var(--space-md);
|
|
1804
|
-
border-bottom: 1px solid var(--color-border);
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
.drawer-title {
|
|
1808
|
-
font-size: 1.125rem;
|
|
1809
|
-
font-weight: 800;
|
|
1810
|
-
color: var(--color-primary);
|
|
1811
|
-
letter-spacing: -0.04em;
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
1801
|
.drawer-close-btn {
|
|
1802
|
+
position: absolute;
|
|
1803
|
+
top: 8px;
|
|
1804
|
+
right: 8px;
|
|
1805
|
+
z-index: 1;
|
|
1815
1806
|
display: flex;
|
|
1816
1807
|
align-items: center;
|
|
1817
1808
|
justify-content: center;
|
|
@@ -1847,6 +1838,18 @@ body {
|
|
|
1847
1838
|
margin-bottom: var(--space-sm);
|
|
1848
1839
|
}
|
|
1849
1840
|
|
|
1841
|
+
.drawer-toggle-group {
|
|
1842
|
+
display: flex;
|
|
1843
|
+
flex-direction: column;
|
|
1844
|
+
gap: var(--space-sm);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
.drawer-toggle-group-divided {
|
|
1848
|
+
border-top: 1px solid var(--color-border);
|
|
1849
|
+
padding-top: var(--space-sm);
|
|
1850
|
+
margin-top: var(--space-xs);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1850
1853
|
.drawer-toggle {
|
|
1851
1854
|
display: flex;
|
|
1852
1855
|
align-items: center;
|
|
@@ -1961,17 +1964,42 @@ body {
|
|
|
1961
1964
|
|
|
1962
1965
|
/* ===== Tab bar ===== */
|
|
1963
1966
|
|
|
1964
|
-
.
|
|
1965
|
-
display: flex;
|
|
1966
|
-
align-items: center;
|
|
1967
|
-
background: color-mix(in srgb, var(--color-surface) 92%, transparent);
|
|
1968
|
-
border-bottom: 1px solid var(--color-border);
|
|
1967
|
+
.app-header {
|
|
1969
1968
|
position: sticky;
|
|
1970
1969
|
top: 0;
|
|
1971
1970
|
z-index: 10;
|
|
1971
|
+
background: color-mix(in srgb, var(--color-surface) 92%, transparent);
|
|
1972
|
+
border-bottom: 1px solid var(--color-border);
|
|
1972
1973
|
backdrop-filter: blur(8px);
|
|
1973
1974
|
}
|
|
1974
1975
|
|
|
1976
|
+
.app-title-bar {
|
|
1977
|
+
position: relative;
|
|
1978
|
+
display: flex;
|
|
1979
|
+
align-items: center;
|
|
1980
|
+
justify-content: center;
|
|
1981
|
+
padding: 8px 0;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
.app-title-bar .hamburger-btn {
|
|
1985
|
+
position: absolute;
|
|
1986
|
+
left: 4px;
|
|
1987
|
+
top: 50%;
|
|
1988
|
+
transform: translateY(-50%);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
.app-title {
|
|
1992
|
+
margin: 0;
|
|
1993
|
+
font-size: 1rem;
|
|
1994
|
+
font-weight: 600;
|
|
1995
|
+
color: var(--color-text);
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
.tab-bar {
|
|
1999
|
+
display: flex;
|
|
2000
|
+
align-items: center;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
1975
2003
|
.tab-btn {
|
|
1976
2004
|
flex: 1;
|
|
1977
2005
|
display: flex;
|
|
@@ -2606,13 +2634,14 @@ body {
|
|
|
2606
2634
|
position: sticky;
|
|
2607
2635
|
top: 0;
|
|
2608
2636
|
height: 100dvh;
|
|
2609
|
-
width:
|
|
2610
|
-
min-width:
|
|
2637
|
+
width: 320px;
|
|
2638
|
+
min-width: 320px;
|
|
2611
2639
|
background: var(--color-surface);
|
|
2612
2640
|
border-right: 1px solid var(--color-border);
|
|
2613
2641
|
display: flex;
|
|
2614
2642
|
flex-direction: column;
|
|
2615
2643
|
overflow-y: auto;
|
|
2644
|
+
overscroll-behavior: contain;
|
|
2616
2645
|
animation: none;
|
|
2617
2646
|
}
|
|
2618
2647
|
|
|
@@ -2630,3 +2659,277 @@ body {
|
|
|
2630
2659
|
right: max(var(--space-lg), calc((100vw - 1080px) / 2));
|
|
2631
2660
|
}
|
|
2632
2661
|
}
|
|
2662
|
+
|
|
2663
|
+
/* ===== Swipe-to-delete row ===== */
|
|
2664
|
+
|
|
2665
|
+
.swipe-row {
|
|
2666
|
+
position: relative;
|
|
2667
|
+
overflow: hidden;
|
|
2668
|
+
touch-action: pan-y; /* let vertical scroll through; capture horizontal ourselves */
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
.swipe-row-action {
|
|
2672
|
+
position: absolute;
|
|
2673
|
+
top: 0;
|
|
2674
|
+
right: 0;
|
|
2675
|
+
bottom: 0;
|
|
2676
|
+
display: flex;
|
|
2677
|
+
align-items: center;
|
|
2678
|
+
justify-content: center;
|
|
2679
|
+
background: var(--color-error, #dc2626);
|
|
2680
|
+
color: #fff;
|
|
2681
|
+
border: none;
|
|
2682
|
+
font-size: 0.9375rem;
|
|
2683
|
+
font-weight: 600;
|
|
2684
|
+
cursor: pointer;
|
|
2685
|
+
padding: 0;
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
.swipe-row-action:focus-visible {
|
|
2689
|
+
outline: 2px solid var(--color-accent, #2E5CE5);
|
|
2690
|
+
outline-offset: -4px;
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
.swipe-row-content {
|
|
2694
|
+
position: relative;
|
|
2695
|
+
background: var(--color-surface);
|
|
2696
|
+
transition: transform 0.2s cubic-bezier(0.22, 1, 0.36, 1);
|
|
2697
|
+
will-change: transform;
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
/* During an active drag, skip the transition so the row tracks the finger 1:1.
|
|
2701
|
+
On release the class is removed and the snap animates. */
|
|
2702
|
+
.swipe-row-content-dragging {
|
|
2703
|
+
transition: none;
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
.app-filter-help {
|
|
2707
|
+
margin-top: var(--space-xs);
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
.app-filter-trigger {
|
|
2711
|
+
display: inline-block;
|
|
2712
|
+
padding: 0;
|
|
2713
|
+
font-size: 0.875rem;
|
|
2714
|
+
font-weight: 500;
|
|
2715
|
+
text-align: left;
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
.app-filter-selected {
|
|
2719
|
+
display: inline-flex;
|
|
2720
|
+
align-items: center;
|
|
2721
|
+
gap: var(--space-sm);
|
|
2722
|
+
padding: 6px 10px;
|
|
2723
|
+
border: 1px solid var(--color-border);
|
|
2724
|
+
border-radius: var(--radius-md);
|
|
2725
|
+
background: var(--color-surface);
|
|
2726
|
+
max-width: 100%;
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
.app-filter-selected-name {
|
|
2730
|
+
font-size: 0.9375rem;
|
|
2731
|
+
font-weight: 500;
|
|
2732
|
+
white-space: nowrap;
|
|
2733
|
+
overflow: hidden;
|
|
2734
|
+
text-overflow: ellipsis;
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
.app-filter-selected-pkg {
|
|
2738
|
+
font-size: 0.75rem;
|
|
2739
|
+
color: var(--color-muted);
|
|
2740
|
+
white-space: nowrap;
|
|
2741
|
+
overflow: hidden;
|
|
2742
|
+
text-overflow: ellipsis;
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
.app-filter-selected-clear {
|
|
2746
|
+
display: flex;
|
|
2747
|
+
align-items: center;
|
|
2748
|
+
justify-content: center;
|
|
2749
|
+
width: 22px;
|
|
2750
|
+
height: 22px;
|
|
2751
|
+
padding: 0;
|
|
2752
|
+
margin-left: auto;
|
|
2753
|
+
border: none;
|
|
2754
|
+
background: none;
|
|
2755
|
+
color: var(--color-muted);
|
|
2756
|
+
font-size: 0.875rem;
|
|
2757
|
+
cursor: pointer;
|
|
2758
|
+
border-radius: var(--radius-sm);
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
.app-filter-selected-clear:hover {
|
|
2762
|
+
background: var(--color-border-subtle);
|
|
2763
|
+
color: var(--color-text);
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
.app-filter-overlay {
|
|
2767
|
+
position: fixed;
|
|
2768
|
+
inset: 0;
|
|
2769
|
+
background: var(--color-overlay);
|
|
2770
|
+
display: flex;
|
|
2771
|
+
align-items: center;
|
|
2772
|
+
justify-content: center;
|
|
2773
|
+
z-index: 1100;
|
|
2774
|
+
backdrop-filter: blur(4px);
|
|
2775
|
+
padding: var(--space-md);
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
.app-filter-dialog {
|
|
2779
|
+
background: var(--color-surface);
|
|
2780
|
+
width: 100%;
|
|
2781
|
+
max-width: 480px;
|
|
2782
|
+
max-height: 80dvh;
|
|
2783
|
+
border-radius: var(--radius-lg);
|
|
2784
|
+
box-shadow: var(--shadow-xl);
|
|
2785
|
+
display: flex;
|
|
2786
|
+
flex-direction: column;
|
|
2787
|
+
overflow: hidden;
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
.app-filter-header {
|
|
2791
|
+
display: flex;
|
|
2792
|
+
align-items: center;
|
|
2793
|
+
justify-content: space-between;
|
|
2794
|
+
padding: var(--space-md) var(--space-lg);
|
|
2795
|
+
border-bottom: 1px solid var(--color-border);
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
.app-filter-header h2 {
|
|
2799
|
+
margin: 0;
|
|
2800
|
+
font-size: 1rem;
|
|
2801
|
+
font-weight: 600;
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
.app-filter-close {
|
|
2805
|
+
display: flex;
|
|
2806
|
+
align-items: center;
|
|
2807
|
+
justify-content: center;
|
|
2808
|
+
width: 32px;
|
|
2809
|
+
height: 32px;
|
|
2810
|
+
padding: 0;
|
|
2811
|
+
border: none;
|
|
2812
|
+
background: none;
|
|
2813
|
+
color: var(--color-muted);
|
|
2814
|
+
cursor: pointer;
|
|
2815
|
+
border-radius: var(--radius-sm);
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
.app-filter-close:hover {
|
|
2819
|
+
background: var(--color-border-subtle);
|
|
2820
|
+
color: var(--color-text);
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
.app-filter-search {
|
|
2824
|
+
margin: var(--space-md) var(--space-lg) 0;
|
|
2825
|
+
width: calc(100% - 2 * var(--space-lg));
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
.app-filter-list {
|
|
2829
|
+
flex: 1;
|
|
2830
|
+
overflow-y: auto;
|
|
2831
|
+
list-style: none;
|
|
2832
|
+
margin: 0;
|
|
2833
|
+
padding: var(--space-xs) 0 var(--space-md);
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
.app-filter-row {
|
|
2837
|
+
display: flex;
|
|
2838
|
+
align-items: center;
|
|
2839
|
+
gap: var(--space-md);
|
|
2840
|
+
padding: 10px var(--space-lg);
|
|
2841
|
+
cursor: pointer;
|
|
2842
|
+
user-select: none;
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
.app-filter-row:hover {
|
|
2846
|
+
background: var(--color-hover, rgba(0, 0, 0, 0.04));
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
.app-filter-row-labels {
|
|
2850
|
+
display: flex;
|
|
2851
|
+
flex-direction: column;
|
|
2852
|
+
min-width: 0;
|
|
2853
|
+
flex: 1;
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
.app-filter-row-name {
|
|
2857
|
+
font-size: 0.9375rem;
|
|
2858
|
+
font-weight: 500;
|
|
2859
|
+
white-space: nowrap;
|
|
2860
|
+
overflow: hidden;
|
|
2861
|
+
text-overflow: ellipsis;
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
.app-filter-row-pkg {
|
|
2865
|
+
font-size: 0.75rem;
|
|
2866
|
+
color: var(--color-muted);
|
|
2867
|
+
white-space: nowrap;
|
|
2868
|
+
overflow: hidden;
|
|
2869
|
+
text-overflow: ellipsis;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
.app-filter-empty {
|
|
2873
|
+
padding: var(--space-lg);
|
|
2874
|
+
text-align: center;
|
|
2875
|
+
color: var(--color-muted);
|
|
2876
|
+
font-size: 0.875rem;
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
.app-filter-skeleton {
|
|
2880
|
+
cursor: default;
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
.app-filter-skeleton-bar {
|
|
2884
|
+
flex: 1;
|
|
2885
|
+
height: 14px;
|
|
2886
|
+
border-radius: 4px;
|
|
2887
|
+
background: linear-gradient(90deg, var(--color-border-subtle) 25%, var(--color-border) 50%, var(--color-border-subtle) 75%);
|
|
2888
|
+
background-size: 200% 100%;
|
|
2889
|
+
animation: appFilterShimmer 1.2s ease-in-out infinite;
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
@keyframes appFilterShimmer {
|
|
2893
|
+
0% { background-position: 200% 0; }
|
|
2894
|
+
100% { background-position: -200% 0; }
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
.pair-setup {
|
|
2898
|
+
min-height: 100dvh;
|
|
2899
|
+
display: flex;
|
|
2900
|
+
justify-content: center;
|
|
2901
|
+
padding: var(--space-lg);
|
|
2902
|
+
background: var(--color-bg);
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
.pair-setup-inner {
|
|
2906
|
+
width: 100%;
|
|
2907
|
+
max-width: 480px;
|
|
2908
|
+
display: flex;
|
|
2909
|
+
flex-direction: column;
|
|
2910
|
+
gap: var(--space-lg);
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
.pair-setup-title {
|
|
2914
|
+
margin: 0;
|
|
2915
|
+
font-size: 1.25rem;
|
|
2916
|
+
font-weight: 600;
|
|
2917
|
+
line-height: 1.35;
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
.pair-setup-description {
|
|
2921
|
+
margin: 0;
|
|
2922
|
+
color: var(--color-muted);
|
|
2923
|
+
font-size: 0.875rem;
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
.pair-setup-loading {
|
|
2927
|
+
padding: var(--space-lg);
|
|
2928
|
+
text-align: center;
|
|
2929
|
+
color: var(--color-muted);
|
|
2930
|
+
font-size: 0.875rem;
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
.pair-setup-actions {
|
|
2934
|
+
margin-top: var(--space-md);
|
|
2935
|
+
}
|
|
@@ -5,6 +5,7 @@ import { HostConnectionProvider } from "./contexts/HostConnectionContext";
|
|
|
5
5
|
import { Device } from "./native/Device";
|
|
6
6
|
import Dashboard from "./pages/Dashboard";
|
|
7
7
|
import PairHost from "./pages/PairHost";
|
|
8
|
+
import PairSetup from "./pages/PairSetup";
|
|
8
9
|
|
|
9
10
|
/** Routes FCM notification taps (fired by DevicePlugin) into the client-side router. */
|
|
10
11
|
function DeepLinkRouter() {
|
|
@@ -29,6 +30,7 @@ export default function App() {
|
|
|
29
30
|
<Route path="/runs/:taskId" element={<Dashboard />} />
|
|
30
31
|
<Route path="/runs/:taskId/:runId" element={<Dashboard />} />
|
|
31
32
|
<Route path="/pair" element={<PairHost />} />
|
|
33
|
+
<Route path="/pair/setup" element={<PairSetup />} />
|
|
32
34
|
</Routes>
|
|
33
35
|
</HostConnectionProvider>
|
|
34
36
|
</HostStoreProvider>
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import { Capacitor } from "@capacitor/core";
|
|
4
|
+
import { App as CapacitorApp } from "@capacitor/app";
|
|
5
|
+
import { Device, type PermissionType } from "../native/Device";
|
|
6
|
+
|
|
7
|
+
const isNative = Capacitor.isNativePlatform();
|
|
8
|
+
|
|
9
|
+
type CapabilityGroup = "Messaging" | "Data" | "Device";
|
|
10
|
+
|
|
11
|
+
const CAPABILITY_GROUPS: CapabilityGroup[] = ["Device", "Data", "Messaging"];
|
|
12
|
+
|
|
13
|
+
interface CapabilityDefinition {
|
|
14
|
+
capability: string;
|
|
15
|
+
label: string;
|
|
16
|
+
group: CapabilityGroup;
|
|
17
|
+
permission?: PermissionType;
|
|
18
|
+
needsFullScreenIntent?: boolean;
|
|
19
|
+
enableMethod?: string;
|
|
20
|
+
disableMethod?: string;
|
|
21
|
+
enableParams?(fcmToken: string): Record<string, unknown>;
|
|
22
|
+
disableParams?(): Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const CAPABILITIES: CapabilityDefinition[] = [
|
|
26
|
+
{ capability: "sms-read", label: "Read SMS", group: "Messaging", permission: "smsRead" },
|
|
27
|
+
{ capability: "sms-send", label: "Send SMS", group: "Messaging", permission: "smsSend" },
|
|
28
|
+
{ capability: "send-email", label: "Send Email", group: "Messaging", permission: "postNotifications" },
|
|
29
|
+
{ capability: "notifications", label: "Read Notifications", group: "Data", permission: "notificationListener" },
|
|
30
|
+
{ capability: "contacts", label: "Manage Contacts", group: "Data", permission: "contacts" },
|
|
31
|
+
{ capability: "calendar", label: "Manage Calendar", group: "Data", permission: "calendar" },
|
|
32
|
+
{
|
|
33
|
+
capability: "location",
|
|
34
|
+
label: "Get Location",
|
|
35
|
+
group: "Device",
|
|
36
|
+
permission: "location",
|
|
37
|
+
enableMethod: "device.location.enable",
|
|
38
|
+
disableMethod: "device.location.disable",
|
|
39
|
+
enableParams: (fcmToken) => ({ fcmToken }),
|
|
40
|
+
disableParams: () => ({}),
|
|
41
|
+
},
|
|
42
|
+
{ capability: "battery", label: "Read Battery Status", group: "Device" },
|
|
43
|
+
{ capability: "dnd", label: "Set Ringer Mode", group: "Device", permission: "dnd" },
|
|
44
|
+
{ capability: "alarm", label: "Trigger Alarms", group: "Device", needsFullScreenIntent: true },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
interface CapabilityTogglesProps {
|
|
48
|
+
capabilityTokens?: Record<string, string | null>;
|
|
49
|
+
activeClientToken?: string | null;
|
|
50
|
+
request<T = unknown>(method: string, params?: unknown): Promise<T>;
|
|
51
|
+
onCapabilityTokensChange(tokens: Record<string, string | null>): void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default function CapabilityToggles({ capabilityTokens, activeClientToken, request, onCapabilityTokensChange }: CapabilityTogglesProps) {
|
|
55
|
+
const [togglingCapability, setTogglingCapability] = useState<string | null>(null);
|
|
56
|
+
const [confirmingSwitchCapability, setConfirmingSwitchCapability] = useState<CapabilityDefinition | null>(null);
|
|
57
|
+
const [confirmingDisableCapability, setConfirmingDisableCapability] = useState<CapabilityDefinition | null>(null);
|
|
58
|
+
|
|
59
|
+
// Null while loading; empty set means the native plugin doesn't advertise the
|
|
60
|
+
// list (old APK / web) — fall back to the per-call {supported} flag.
|
|
61
|
+
const [supportedPerms, setSupportedPerms] = useState<Set<PermissionType> | null>(null);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!isNative || !Device) {
|
|
65
|
+
setSupportedPerms(new Set());
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
Device.getSupportedPermissions()
|
|
69
|
+
.then(({ types }) => setSupportedPerms(new Set(types)))
|
|
70
|
+
.catch(() => setSupportedPerms(new Set()));
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
function isCapabilityEnabled(capability: string): boolean {
|
|
74
|
+
return !!(activeClientToken && capabilityTokens?.[capability] === activeClientToken);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isCapabilityVisible(definition: CapabilityDefinition): boolean {
|
|
78
|
+
if (!supportedPerms) return false;
|
|
79
|
+
if (supportedPerms.size === 0) return true;
|
|
80
|
+
if (definition.permission && !supportedPerms.has(definition.permission)) return false;
|
|
81
|
+
if (definition.needsFullScreenIntent && !supportedPerms.has("fullScreenIntent")) return false;
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function setCapabilityEnabled(capability: string, enabled: boolean) {
|
|
86
|
+
const updated: Record<string, string | null> = {};
|
|
87
|
+
for (const [k, v] of Object.entries(capabilityTokens ?? {})) updated[k] = v ?? null;
|
|
88
|
+
updated[capability] = enabled ? (activeClientToken ?? null) : null;
|
|
89
|
+
onCapabilityTokensChange(updated);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// If the OS location permission is revoked while the app is backgrounded,
|
|
93
|
+
// disable the capability on the host so agents don't keep pinging for fixes.
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!isNative || !Device) return;
|
|
96
|
+
const locationEnabled = isCapabilityEnabled("location");
|
|
97
|
+
if (!locationEnabled) return;
|
|
98
|
+
|
|
99
|
+
function syncPermissionState() {
|
|
100
|
+
Device!.checkPermission({ type: "location" }).then(({ granted }) => {
|
|
101
|
+
if (!granted) {
|
|
102
|
+
request("device.location.disable").then(() => {
|
|
103
|
+
setCapabilityEnabled("location", false);
|
|
104
|
+
}).catch(() => {});
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
syncPermissionState();
|
|
110
|
+
const listener = CapacitorApp.addListener("resume", syncPermissionState);
|
|
111
|
+
return () => { listener.then((h) => h.remove()); };
|
|
112
|
+
}, [capabilityTokens, activeClientToken]);
|
|
113
|
+
|
|
114
|
+
// Mirror the server-derived enabled set into native as a local kill-switch.
|
|
115
|
+
// Toggle paths write through immediately; this catches host-initiated changes
|
|
116
|
+
// (e.g. a capability revoked on another device) on the next render.
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (!isNative || !Device) return;
|
|
119
|
+
const enabled = CAPABILITIES
|
|
120
|
+
.map((definition) => definition.capability)
|
|
121
|
+
.filter((capability) => capabilityTokens?.[capability] === activeClientToken);
|
|
122
|
+
Device.setEnabledCapabilities({ capabilities: enabled }).catch(() => {});
|
|
123
|
+
}, [capabilityTokens, activeClientToken]);
|
|
124
|
+
|
|
125
|
+
async function toggleCapability(definition: CapabilityDefinition, bypassConfirmation = false) {
|
|
126
|
+
const enabled = isCapabilityEnabled(definition.capability);
|
|
127
|
+
|
|
128
|
+
if (enabled && !bypassConfirmation) {
|
|
129
|
+
setConfirmingDisableCapability(definition);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const ownedByOther = !enabled && !!capabilityTokens?.[definition.capability]
|
|
134
|
+
&& capabilityTokens[definition.capability] !== activeClientToken;
|
|
135
|
+
if (ownedByOther && !bypassConfirmation) {
|
|
136
|
+
setConfirmingSwitchCapability(definition);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
setTogglingCapability(definition.capability);
|
|
141
|
+
try {
|
|
142
|
+
if (enabled) {
|
|
143
|
+
const method = definition.disableMethod ?? "device.capability.disable";
|
|
144
|
+
const params = definition.disableParams?.() ?? { capability: definition.capability };
|
|
145
|
+
await request(method, params);
|
|
146
|
+
setCapabilityEnabled(definition.capability, false);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (Device && definition.capability === "send-email") {
|
|
151
|
+
try {
|
|
152
|
+
const result = await Device.hasEmailClient();
|
|
153
|
+
if (result.supported && !result.available) {
|
|
154
|
+
alert("No email app is installed on this device. Install one (e.g. Gmail) before enabling Sending Email.");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
} catch { /* older APK: fall through */ }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (Device && definition.permission) {
|
|
161
|
+
const check = await Device.checkPermission({ type: definition.permission });
|
|
162
|
+
if (!check.supported) {
|
|
163
|
+
console.warn(`Native build does not support permission '${definition.permission}'`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (!check.granted) {
|
|
167
|
+
const result = await Device.requestPermission({ type: definition.permission });
|
|
168
|
+
if (!result.granted) return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (Device && definition.needsFullScreenIntent) {
|
|
172
|
+
const check = await Device.checkPermission({ type: "fullScreenIntent" });
|
|
173
|
+
if (!check.supported) {
|
|
174
|
+
console.warn("Native build does not support fullScreenIntent");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (!check.granted) {
|
|
178
|
+
const result = await Device.requestPermission({ type: "fullScreenIntent" });
|
|
179
|
+
if (!result.granted) return;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!Device) return;
|
|
184
|
+
const { token: fcmToken } = await Device.getFcmToken();
|
|
185
|
+
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
186
|
+
|
|
187
|
+
// Whitelist the capability natively before enabling on the host, so an FCM
|
|
188
|
+
// from the host can't arrive in the gap before our useEffect syncs.
|
|
189
|
+
const enabledNow = CAPABILITIES
|
|
190
|
+
.map((c) => c.capability)
|
|
191
|
+
.filter((name) => name === definition.capability || capabilityTokens?.[name] === activeClientToken);
|
|
192
|
+
await Device.setEnabledCapabilities({ capabilities: enabledNow });
|
|
193
|
+
|
|
194
|
+
const method = definition.enableMethod ?? "device.capability.enable";
|
|
195
|
+
const params = definition.enableParams?.(fcmToken) ?? { capability: definition.capability, fcmToken };
|
|
196
|
+
await request(method, params);
|
|
197
|
+
setCapabilityEnabled(definition.capability, true);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
console.error(`Failed to toggle ${definition.capability}:`, err);
|
|
200
|
+
} finally {
|
|
201
|
+
setTogglingCapability(null);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const visibleGroups = CAPABILITY_GROUPS
|
|
206
|
+
.map((group) => ({ group, items: CAPABILITIES.filter((d) => d.group === group && isCapabilityVisible(d)) }))
|
|
207
|
+
.filter((g) => g.items.length > 0);
|
|
208
|
+
|
|
209
|
+
if (!isNative || visibleGroups.length === 0) return null;
|
|
210
|
+
|
|
211
|
+
const switchModal = confirmingSwitchCapability && createPortal(
|
|
212
|
+
<div className="confirm-modal-overlay" onClick={() => setConfirmingSwitchCapability(null)}>
|
|
213
|
+
<div className="confirm-modal" onClick={(e) => e.stopPropagation()}>
|
|
214
|
+
<h2 className="confirm-modal-title">Switch {confirmingSwitchCapability.label} to this device?</h2>
|
|
215
|
+
<p className="confirm-modal-message">
|
|
216
|
+
{confirmingSwitchCapability.label} is currently enabled on another device. Switching will make this device the one the host uses for {confirmingSwitchCapability.label}, and it will be disabled on the other device.
|
|
217
|
+
</p>
|
|
218
|
+
<div className="confirm-modal-actions">
|
|
219
|
+
<button className="btn btn-secondary" onClick={() => setConfirmingSwitchCapability(null)}>Cancel</button>
|
|
220
|
+
<button
|
|
221
|
+
className="btn btn-primary"
|
|
222
|
+
onClick={() => {
|
|
223
|
+
const d = confirmingSwitchCapability;
|
|
224
|
+
setConfirmingSwitchCapability(null);
|
|
225
|
+
toggleCapability(d, true);
|
|
226
|
+
}}
|
|
227
|
+
>
|
|
228
|
+
Switch
|
|
229
|
+
</button>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</div>,
|
|
233
|
+
document.body,
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const disableModal = confirmingDisableCapability && createPortal(
|
|
237
|
+
<div className="confirm-modal-overlay" onClick={() => setConfirmingDisableCapability(null)}>
|
|
238
|
+
<div className="confirm-modal" onClick={(e) => e.stopPropagation()}>
|
|
239
|
+
<h2 className="confirm-modal-title">Disable {confirmingDisableCapability.label}?</h2>
|
|
240
|
+
<p className="confirm-modal-message">
|
|
241
|
+
Agents running on the host will no longer be able to use {confirmingDisableCapability.label} until it is re-enabled on a device.
|
|
242
|
+
</p>
|
|
243
|
+
<div className="confirm-modal-actions">
|
|
244
|
+
<button className="btn btn-secondary" onClick={() => setConfirmingDisableCapability(null)}>Cancel</button>
|
|
245
|
+
<button
|
|
246
|
+
className="btn btn-danger"
|
|
247
|
+
onClick={() => {
|
|
248
|
+
const d = confirmingDisableCapability;
|
|
249
|
+
setConfirmingDisableCapability(null);
|
|
250
|
+
toggleCapability(d, true);
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
Disable
|
|
254
|
+
</button>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
</div>,
|
|
258
|
+
document.body,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<>
|
|
263
|
+
{visibleGroups.map(({ group, items }, index) => (
|
|
264
|
+
<div key={group} className={index > 0 ? "drawer-toggle-group drawer-toggle-group-divided" : "drawer-toggle-group"}>
|
|
265
|
+
{items.map((definition) => {
|
|
266
|
+
const enabled = isCapabilityEnabled(definition.capability);
|
|
267
|
+
return (
|
|
268
|
+
<label key={definition.capability} className="drawer-toggle">
|
|
269
|
+
<span className="drawer-toggle-label">{definition.label}</span>
|
|
270
|
+
<button
|
|
271
|
+
className={`toggle-switch ${enabled ? "toggle-switch-on" : ""}`}
|
|
272
|
+
onClick={() => toggleCapability(definition)}
|
|
273
|
+
disabled={togglingCapability === definition.capability}
|
|
274
|
+
role="switch"
|
|
275
|
+
aria-checked={enabled}
|
|
276
|
+
>
|
|
277
|
+
<span className="toggle-switch-thumb" />
|
|
278
|
+
</button>
|
|
279
|
+
</label>
|
|
280
|
+
);
|
|
281
|
+
})}
|
|
282
|
+
</div>
|
|
283
|
+
))}
|
|
284
|
+
{switchModal}
|
|
285
|
+
{disableModal}
|
|
286
|
+
</>
|
|
287
|
+
);
|
|
288
|
+
}
|