palmier 0.8.1 → 0.8.3
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 +11 -11
- 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/app-registry.d.ts +10 -0
- package/dist/app-registry.js +44 -0
- 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 +1 -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 +33 -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 +1 -4
- package/dist/platform/linux.d.ts +3 -9
- package/dist/platform/linux.js +9 -20
- 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-CQxcuDhM.css → index-B0F9mtid.css} +1 -1
- package/dist/pwa/assets/index-SYs3mcdJ.js +120 -0
- package/dist/pwa/assets/{web-D7Kq3Nvk.js → web-C6lkQj9J.js} +1 -1
- package/dist/pwa/assets/{web-DOyOiwsW.js → web-Z1623me-.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 +18 -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 +6 -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 +165 -20
- package/palmier-server/pwa/src/components/HostMenu.tsx +159 -49
- package/palmier-server/pwa/src/components/RunDetailView.tsx +3 -3
- package/palmier-server/pwa/src/components/SessionsView.tsx +57 -31
- package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +160 -0
- package/palmier-server/pwa/src/components/TaskForm.tsx +152 -2
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/native/Device.ts +20 -2
- package/palmier-server/pwa/src/pages/Dashboard.tsx +11 -6
- 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/app-registry.ts +52 -0
- package/src/commands/info.ts +0 -5
- package/src/commands/init.ts +2 -11
- package/src/commands/pair.ts +1 -12
- package/src/commands/restart.ts +0 -3
- package/src/commands/run.ts +18 -65
- package/src/commands/serve.ts +31 -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 +1 -4
- package/src/platform/linux.ts +9 -20
- package/src/platform/platform.ts +1 -4
- package/src/platform/windows.ts +19 -40
- package/src/rpc-handler.ts +19 -47
- package/src/spawn-command.ts +11 -27
- package/src/task.ts +7 -70
- package/src/transports/http-transport.ts +6 -39
- package/src/transports/nats-transport.ts +3 -9
- package/src/types.ts +3 -10
- package/src/update-checker.ts +2 -5
- 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;
|
|
@@ -2613,6 +2641,7 @@ body {
|
|
|
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,119 @@ 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-combobox {
|
|
2711
|
+
position: relative;
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
.app-combobox-list {
|
|
2715
|
+
position: absolute;
|
|
2716
|
+
top: calc(100% + 2px);
|
|
2717
|
+
left: 0;
|
|
2718
|
+
right: 0;
|
|
2719
|
+
max-height: 280px;
|
|
2720
|
+
overflow-y: auto;
|
|
2721
|
+
overscroll-behavior: contain;
|
|
2722
|
+
background: var(--color-surface);
|
|
2723
|
+
border: 1px solid var(--color-border);
|
|
2724
|
+
border-radius: var(--radius-md);
|
|
2725
|
+
box-shadow: var(--shadow-md);
|
|
2726
|
+
list-style: none;
|
|
2727
|
+
margin: 0;
|
|
2728
|
+
padding: var(--space-xs) 0;
|
|
2729
|
+
z-index: 20;
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
.app-combobox-row {
|
|
2733
|
+
display: flex;
|
|
2734
|
+
align-items: center;
|
|
2735
|
+
gap: var(--space-md);
|
|
2736
|
+
padding: 8px var(--space-md);
|
|
2737
|
+
cursor: pointer;
|
|
2738
|
+
user-select: none;
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
.app-combobox-row:hover {
|
|
2742
|
+
background: var(--color-hover, rgba(0, 0, 0, 0.04));
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
.app-combobox-icon {
|
|
2746
|
+
width: 28px;
|
|
2747
|
+
height: 28px;
|
|
2748
|
+
border-radius: 6px;
|
|
2749
|
+
flex-shrink: 0;
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
.app-combobox-icon-placeholder {
|
|
2753
|
+
background: var(--color-border);
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
.app-combobox-labels {
|
|
2757
|
+
display: flex;
|
|
2758
|
+
flex-direction: column;
|
|
2759
|
+
min-width: 0;
|
|
2760
|
+
flex: 1;
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
.app-combobox-name {
|
|
2764
|
+
font-size: 0.9375rem;
|
|
2765
|
+
font-weight: 500;
|
|
2766
|
+
white-space: nowrap;
|
|
2767
|
+
overflow: hidden;
|
|
2768
|
+
text-overflow: ellipsis;
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
.app-combobox-pkg {
|
|
2772
|
+
font-size: 0.75rem;
|
|
2773
|
+
color: var(--color-muted);
|
|
2774
|
+
white-space: nowrap;
|
|
2775
|
+
overflow: hidden;
|
|
2776
|
+
text-overflow: ellipsis;
|
|
2777
|
+
}
|
|
@@ -12,14 +12,20 @@ import { confirmLeaveDraft } from "../draftGuard";
|
|
|
12
12
|
const isLanMode = !!(window as any).__PALMIER_SERVE__;
|
|
13
13
|
const isNative = Capacitor.isNativePlatform();
|
|
14
14
|
|
|
15
|
+
type CapabilityGroup = "Messaging" | "Data" | "Device";
|
|
16
|
+
|
|
17
|
+
const CAPABILITY_GROUPS: CapabilityGroup[] = ["Device", "Data", "Messaging"];
|
|
18
|
+
|
|
15
19
|
interface CapabilityDefinition {
|
|
16
20
|
/** Server-side capability name used in device.capability.{enable,disable} RPCs. */
|
|
17
21
|
capability: string;
|
|
18
22
|
/** Label shown in the drawer toggle. */
|
|
19
23
|
label: string;
|
|
24
|
+
/** Drawer section this capability is grouped under. */
|
|
25
|
+
group: CapabilityGroup;
|
|
20
26
|
/** Runtime or settings permission to request before enabling. */
|
|
21
27
|
permission?: PermissionType;
|
|
22
|
-
/** True for capabilities that display full-screen
|
|
28
|
+
/** True for capabilities that display a full-screen UI (alarm). */
|
|
23
29
|
needsFullScreenIntent?: boolean;
|
|
24
30
|
/** Override RPC methods; location uses device.location.{enable,disable} instead. */
|
|
25
31
|
enableMethod?: string;
|
|
@@ -29,23 +35,25 @@ interface CapabilityDefinition {
|
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
const CAPABILITIES: CapabilityDefinition[] = [
|
|
38
|
+
{ capability: "sms-read", label: "Read SMS", group: "Messaging", permission: "smsRead" },
|
|
39
|
+
{ capability: "sms-send", label: "Send SMS", group: "Messaging", permission: "smsSend" },
|
|
40
|
+
{ capability: "send-email", label: "Send Email", group: "Messaging", permission: "postNotifications" },
|
|
41
|
+
{ capability: "notifications", label: "Read Notifications", group: "Data", permission: "notificationListener" },
|
|
42
|
+
{ capability: "contacts", label: "Manage Contacts", group: "Data", permission: "contacts" },
|
|
43
|
+
{ capability: "calendar", label: "Manage Calendar", group: "Data", permission: "calendar" },
|
|
32
44
|
{
|
|
33
45
|
capability: "location",
|
|
34
|
-
label: "Location
|
|
46
|
+
label: "Get Location",
|
|
47
|
+
group: "Device",
|
|
35
48
|
permission: "location",
|
|
36
49
|
enableMethod: "device.location.enable",
|
|
37
50
|
disableMethod: "device.location.disable",
|
|
38
51
|
enableParams: (fcmToken) => ({ fcmToken }),
|
|
39
52
|
disableParams: () => ({}),
|
|
40
53
|
},
|
|
41
|
-
{ capability: "
|
|
42
|
-
{ capability: "
|
|
43
|
-
{ capability: "
|
|
44
|
-
{ capability: "calendar", label: "Calendar Access", permission: "calendar" },
|
|
45
|
-
{ capability: "dnd", label: "Do Not Disturb Control", permission: "dnd" },
|
|
46
|
-
{ capability: "alert", label: "Alert Access", needsFullScreenIntent: true },
|
|
47
|
-
{ capability: "battery", label: "Battery Access" },
|
|
48
|
-
{ capability: "send-email", label: "Email Drafting", needsFullScreenIntent: true },
|
|
54
|
+
{ capability: "battery", label: "Read Battery Status", group: "Device" },
|
|
55
|
+
{ capability: "dnd", label: "Set Ringer Mode", group: "Device", permission: "dnd" },
|
|
56
|
+
{ capability: "alarm", label: "Trigger Alarms", group: "Device", needsFullScreenIntent: true },
|
|
49
57
|
];
|
|
50
58
|
|
|
51
59
|
interface HostMenuProps {
|
|
@@ -67,6 +75,8 @@ export default function HostMenu({ daemonVersion, capabilityTokens, activeClient
|
|
|
67
75
|
const [renameValue, setRenameValue] = useState("");
|
|
68
76
|
const [confirmingDeleteId, setConfirmingDeleteId] = useState<string | null>(null);
|
|
69
77
|
const [togglingCapability, setTogglingCapability] = useState<string | null>(null);
|
|
78
|
+
const [confirmingSwitchCapability, setConfirmingSwitchCapability] = useState<CapabilityDefinition | null>(null);
|
|
79
|
+
const [confirmingDisableCapability, setConfirmingDisableCapability] = useState<CapabilityDefinition | null>(null);
|
|
70
80
|
/**
|
|
71
81
|
* Permission types the installed APK understands. Null while loading; an empty
|
|
72
82
|
* set means the native plugin doesn't expose a discovery method (pre-Device
|
|
@@ -145,9 +155,22 @@ export default function HostMenu({ daemonVersion, capabilityTokens, activeClient
|
|
|
145
155
|
Device.setEnabledCapabilities({ capabilities: enabledCapabilities }).catch(() => {});
|
|
146
156
|
}, [capabilityTokens, activeClientToken]);
|
|
147
157
|
|
|
148
|
-
async function toggleCapability(definition: CapabilityDefinition) {
|
|
158
|
+
async function toggleCapability(definition: CapabilityDefinition, bypassConfirmation = false) {
|
|
149
159
|
if (!request) return;
|
|
150
160
|
const enabled = isCapabilityEnabled(definition.capability);
|
|
161
|
+
|
|
162
|
+
if (enabled && !bypassConfirmation) {
|
|
163
|
+
setConfirmingDisableCapability(definition);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const ownedByOther = !enabled && !!capabilityTokens?.[definition.capability]
|
|
168
|
+
&& capabilityTokens[definition.capability] !== activeClientToken;
|
|
169
|
+
if (ownedByOther && !bypassConfirmation) {
|
|
170
|
+
setConfirmingSwitchCapability(definition);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
151
174
|
setTogglingCapability(definition.capability);
|
|
152
175
|
try {
|
|
153
176
|
if (enabled) {
|
|
@@ -158,6 +181,21 @@ export default function HostMenu({ daemonVersion, capabilityTokens, activeClient
|
|
|
158
181
|
return;
|
|
159
182
|
}
|
|
160
183
|
|
|
184
|
+
if (Device && definition.capability === "send-email") {
|
|
185
|
+
// Verify a mailto: handler exists before enabling — silent PackageManager
|
|
186
|
+
// lookup. If the APK predates this method, allow enabling and let the
|
|
187
|
+
// user discover the missing-app failure at first use.
|
|
188
|
+
try {
|
|
189
|
+
const result = await Device.hasEmailClient();
|
|
190
|
+
if (result.supported && !result.available) {
|
|
191
|
+
alert("No email app is installed on this device. Install one (e.g. Gmail) before enabling Sending Email.");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
// Older APK without this method — fall through.
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
161
199
|
if (Device && definition.permission) {
|
|
162
200
|
const check = await Device.checkPermission({ type: definition.permission });
|
|
163
201
|
if (!check.supported) {
|
|
@@ -261,20 +299,17 @@ export default function HostMenu({ daemonVersion, capabilityTokens, activeClient
|
|
|
261
299
|
|
|
262
300
|
const drawerContent = (
|
|
263
301
|
<>
|
|
264
|
-
|
|
265
|
-
<
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
</button>
|
|
276
|
-
)}
|
|
277
|
-
</div>
|
|
302
|
+
{!isDesktop && (
|
|
303
|
+
<button
|
|
304
|
+
className="drawer-close-btn"
|
|
305
|
+
onClick={close}
|
|
306
|
+
aria-label="Close menu"
|
|
307
|
+
>
|
|
308
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
309
|
+
<path d="M4 4L12 12M12 4L4 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
310
|
+
</svg>
|
|
311
|
+
</button>
|
|
312
|
+
)}
|
|
278
313
|
|
|
279
314
|
{!isLanMode && pairedHosts.length > 0 && (
|
|
280
315
|
<div className="drawer-section">
|
|
@@ -382,30 +417,41 @@ export default function HostMenu({ daemonVersion, capabilityTokens, activeClient
|
|
|
382
417
|
</div>
|
|
383
418
|
</>)}
|
|
384
419
|
|
|
385
|
-
{isNative && (
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
420
|
+
{isNative && (() => {
|
|
421
|
+
const visibleGroups = CAPABILITY_GROUPS
|
|
422
|
+
.map((group) => ({ group, items: CAPABILITIES.filter((definition) => definition.group === group && isCapabilityVisible(definition)) }))
|
|
423
|
+
.filter((g) => g.items.length > 0);
|
|
424
|
+
if (visibleGroups.length === 0) return null;
|
|
425
|
+
return (
|
|
426
|
+
<>
|
|
427
|
+
<div className="drawer-divider" />
|
|
428
|
+
<div className="drawer-section">
|
|
429
|
+
<h3 className="drawer-section-label">Host capabilities on this device</h3>
|
|
430
|
+
{visibleGroups.map(({ group, items }, index) => (
|
|
431
|
+
<div key={group} className={index > 0 ? "drawer-toggle-group drawer-toggle-group-divided" : "drawer-toggle-group"}>
|
|
432
|
+
{items.map((definition) => {
|
|
433
|
+
const enabled = isCapabilityEnabled(definition.capability);
|
|
434
|
+
return (
|
|
435
|
+
<label key={definition.capability} className="drawer-toggle">
|
|
436
|
+
<span className="drawer-toggle-label">{definition.label}</span>
|
|
437
|
+
<button
|
|
438
|
+
className={`toggle-switch ${enabled ? "toggle-switch-on" : ""}`}
|
|
439
|
+
onClick={() => toggleCapability(definition)}
|
|
440
|
+
disabled={togglingCapability === definition.capability}
|
|
441
|
+
role="switch"
|
|
442
|
+
aria-checked={enabled}
|
|
443
|
+
>
|
|
444
|
+
<span className="toggle-switch-thumb" />
|
|
445
|
+
</button>
|
|
446
|
+
</label>
|
|
447
|
+
);
|
|
448
|
+
})}
|
|
449
|
+
</div>
|
|
450
|
+
))}
|
|
451
|
+
</div>
|
|
452
|
+
</>
|
|
453
|
+
);
|
|
454
|
+
})()}
|
|
409
455
|
|
|
410
456
|
<div className="drawer-footer">
|
|
411
457
|
{daemonVersion && (
|
|
@@ -451,6 +497,66 @@ export default function HostMenu({ daemonVersion, capabilityTokens, activeClient
|
|
|
451
497
|
document.body
|
|
452
498
|
);
|
|
453
499
|
|
|
500
|
+
const disableCapabilityModal = confirmingDisableCapability && createPortal(
|
|
501
|
+
<div className="confirm-modal-overlay" onClick={() => setConfirmingDisableCapability(null)}>
|
|
502
|
+
<div className="confirm-modal" onClick={(e) => e.stopPropagation()}>
|
|
503
|
+
<h2 className="confirm-modal-title">Disable {confirmingDisableCapability.label}?</h2>
|
|
504
|
+
<p className="confirm-modal-message">
|
|
505
|
+
Agents running on the host will no longer be able to use {confirmingDisableCapability.label} until it is re-enabled on a device.
|
|
506
|
+
</p>
|
|
507
|
+
<div className="confirm-modal-actions">
|
|
508
|
+
<button
|
|
509
|
+
className="btn btn-secondary"
|
|
510
|
+
onClick={() => setConfirmingDisableCapability(null)}
|
|
511
|
+
>
|
|
512
|
+
Cancel
|
|
513
|
+
</button>
|
|
514
|
+
<button
|
|
515
|
+
className="btn btn-danger"
|
|
516
|
+
onClick={() => {
|
|
517
|
+
const definition = confirmingDisableCapability;
|
|
518
|
+
setConfirmingDisableCapability(null);
|
|
519
|
+
toggleCapability(definition, true);
|
|
520
|
+
}}
|
|
521
|
+
>
|
|
522
|
+
Disable
|
|
523
|
+
</button>
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
</div>,
|
|
527
|
+
document.body
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
const switchCapabilityModal = confirmingSwitchCapability && createPortal(
|
|
531
|
+
<div className="confirm-modal-overlay" onClick={() => setConfirmingSwitchCapability(null)}>
|
|
532
|
+
<div className="confirm-modal" onClick={(e) => e.stopPropagation()}>
|
|
533
|
+
<h2 className="confirm-modal-title">Switch {confirmingSwitchCapability.label} to this device?</h2>
|
|
534
|
+
<p className="confirm-modal-message">
|
|
535
|
+
{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.
|
|
536
|
+
</p>
|
|
537
|
+
<div className="confirm-modal-actions">
|
|
538
|
+
<button
|
|
539
|
+
className="btn btn-secondary"
|
|
540
|
+
onClick={() => setConfirmingSwitchCapability(null)}
|
|
541
|
+
>
|
|
542
|
+
Cancel
|
|
543
|
+
</button>
|
|
544
|
+
<button
|
|
545
|
+
className="btn btn-primary"
|
|
546
|
+
onClick={() => {
|
|
547
|
+
const definition = confirmingSwitchCapability;
|
|
548
|
+
setConfirmingSwitchCapability(null);
|
|
549
|
+
toggleCapability(definition, true);
|
|
550
|
+
}}
|
|
551
|
+
>
|
|
552
|
+
Switch
|
|
553
|
+
</button>
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
</div>,
|
|
557
|
+
document.body
|
|
558
|
+
);
|
|
559
|
+
|
|
454
560
|
// Desktop: persistent inline sidebar
|
|
455
561
|
if (isDesktop) {
|
|
456
562
|
return (
|
|
@@ -459,6 +565,8 @@ export default function HostMenu({ daemonVersion, capabilityTokens, activeClient
|
|
|
459
565
|
{drawerContent}
|
|
460
566
|
</div>
|
|
461
567
|
{deleteModal}
|
|
568
|
+
{switchCapabilityModal}
|
|
569
|
+
{disableCapabilityModal}
|
|
462
570
|
</>
|
|
463
571
|
);
|
|
464
572
|
}
|
|
@@ -494,6 +602,8 @@ export default function HostMenu({ daemonVersion, capabilityTokens, activeClient
|
|
|
494
602
|
)}
|
|
495
603
|
|
|
496
604
|
{deleteModal}
|
|
605
|
+
{switchCapabilityModal}
|
|
606
|
+
{disableCapabilityModal}
|
|
497
607
|
</>
|
|
498
608
|
);
|
|
499
609
|
}
|
|
@@ -131,7 +131,8 @@ export default function RunDetailView({ connected, hostId, request, subscribeEve
|
|
|
131
131
|
}, [messages]);
|
|
132
132
|
|
|
133
133
|
// On first load of a run, scroll the window to the bottom so the follow-up
|
|
134
|
-
// input is visible
|
|
134
|
+
// input is visible. Deliberately not focusing the input — on mobile that
|
|
135
|
+
// would pop the soft keyboard as soon as the run opens.
|
|
135
136
|
useEffect(() => {
|
|
136
137
|
if (loading || isLatestEmpty || !resolvedRunId) return;
|
|
137
138
|
if (initialFocusForRunId.current === resolvedRunId) return;
|
|
@@ -139,9 +140,8 @@ export default function RunDetailView({ connected, hostId, request, subscribeEve
|
|
|
139
140
|
requestAnimationFrame(() => {
|
|
140
141
|
if (threadRef.current) threadRef.current.scrollTop = threadRef.current.scrollHeight;
|
|
141
142
|
window.scrollTo({ top: document.documentElement.scrollHeight, behavior: "auto" });
|
|
142
|
-
if (!isAgentGenerating) followupInputRef.current?.focus();
|
|
143
143
|
});
|
|
144
|
-
}, [loading, isLatestEmpty, resolvedRunId
|
|
144
|
+
}, [loading, isLatestEmpty, resolvedRunId]);
|
|
145
145
|
|
|
146
146
|
function typeLabel(type?: string): string | undefined {
|
|
147
147
|
if (type === "input") return "User Input";
|
|
@@ -4,6 +4,7 @@ import { formatTime } from "../formatTime";
|
|
|
4
4
|
import { confirmLeaveDraft } from "../draftGuard";
|
|
5
5
|
import SessionComposer from "./SessionComposer";
|
|
6
6
|
import PullToRefreshIndicator from "./PullToRefreshIndicator";
|
|
7
|
+
import SwipeToDeleteRow from "./SwipeToDeleteRow";
|
|
7
8
|
import { usePullToRefresh } from "../hooks/usePullToRefresh";
|
|
8
9
|
import type { AgentInfo, HistoryEntry } from "../types";
|
|
9
10
|
|
|
@@ -24,8 +25,25 @@ export default function SessionsView({ connected, hostId, request, subscribeEven
|
|
|
24
25
|
const [total, setTotal] = useState(0);
|
|
25
26
|
const [loading, setLoading] = useState(false);
|
|
26
27
|
const [loadingMore, setLoadingMore] = useState(false);
|
|
28
|
+
/** Key of the row currently showing its delete action, or null. iOS pattern — at most one at a time. */
|
|
29
|
+
const [revealedKey, setRevealedKey] = useState<string | null>(null);
|
|
27
30
|
const navigate = useNavigate();
|
|
28
31
|
|
|
32
|
+
async function deleteEntry(entry: HistoryEntry) {
|
|
33
|
+
const key = `${entry.task_id}:${entry.run_id}`;
|
|
34
|
+
// Optimistic: drop from the list immediately, restore if the RPC fails.
|
|
35
|
+
setEntries((prev) => prev.filter((e) => `${e.task_id}:${e.run_id}` !== key));
|
|
36
|
+
setTotal((t) => Math.max(0, t - 1));
|
|
37
|
+
setRevealedKey(null);
|
|
38
|
+
try {
|
|
39
|
+
await request("taskrun.delete", { task_id: entry.task_id, run_id: entry.run_id });
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error("Failed to delete run:", err);
|
|
42
|
+
setEntries((prev) => [entry, ...prev]);
|
|
43
|
+
setTotal((t) => t + 1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
29
47
|
const sentinelRef = useRef<HTMLDivElement>(null);
|
|
30
48
|
|
|
31
49
|
// Build RPC params with optional task_id filter
|
|
@@ -247,38 +265,46 @@ export default function SessionsView({ connected, hostId, request, subscribeEven
|
|
|
247
265
|
{composer}
|
|
248
266
|
{filterChip}
|
|
249
267
|
<div className="task-list">
|
|
250
|
-
{entries.map((entry, i) =>
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
<
|
|
265
|
-
{
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
268
|
+
{entries.map((entry, i) => {
|
|
269
|
+
const key = `${entry.task_id}:${entry.run_id}`;
|
|
270
|
+
return (
|
|
271
|
+
<SwipeToDeleteRow
|
|
272
|
+
key={`${key}-${i}`}
|
|
273
|
+
id={key}
|
|
274
|
+
revealedId={revealedKey}
|
|
275
|
+
setRevealedId={setRevealedKey}
|
|
276
|
+
onDelete={() => deleteEntry(entry)}
|
|
277
|
+
onClick={() => !entry.error && handleCardClick(entry.task_id, entry.run_id)}
|
|
278
|
+
>
|
|
279
|
+
<div className="sessions-card">
|
|
280
|
+
<div className="sessions-card-body">
|
|
281
|
+
<h3 className="sessions-card-name">{entry.task_name || entry.task_id}</h3>
|
|
282
|
+
<div className="sessions-card-meta">
|
|
283
|
+
{entry.running_state === "started" ? (
|
|
284
|
+
<span className="status-spinner" aria-label="Running">
|
|
285
|
+
<span />
|
|
286
|
+
</span>
|
|
287
|
+
) : (
|
|
288
|
+
<span style={{ color: stateColor(entry.running_state) }}>
|
|
289
|
+
{stateLabel[entry.running_state ?? ""] ?? entry.running_state}
|
|
290
|
+
</span>
|
|
291
|
+
)}
|
|
292
|
+
{entry.end_time && <span>{formatTime(entry.end_time)}</span>}
|
|
293
|
+
{entry.start_time && entry.end_time && (
|
|
294
|
+
<span style={{ color: "var(--color-muted)" }}>
|
|
295
|
+
{formatDuration(entry.start_time, entry.end_time)}
|
|
296
|
+
</span>
|
|
297
|
+
)}
|
|
298
|
+
{entry.error && (
|
|
299
|
+
<span style={{ color: "var(--color-muted)" }}>{entry.error}</span>
|
|
300
|
+
)}
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
<span className="sessions-card-chevron">›</span>
|
|
277
304
|
</div>
|
|
278
|
-
</
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
))}
|
|
305
|
+
</SwipeToDeleteRow>
|
|
306
|
+
);
|
|
307
|
+
})}
|
|
282
308
|
|
|
283
309
|
{/* Sentinel for infinite scroll */}
|
|
284
310
|
<div ref={sentinelRef} style={{ height: 1 }} />
|