palmier 0.9.2 → 0.9.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/README.md +21 -20
- package/dist/platform/linux.js +14 -0
- package/dist/pwa/assets/index-BsB1tIsn.css +1 -0
- package/dist/pwa/assets/index-CknFGshO.js +120 -0
- package/dist/pwa/assets/{web-C2AU9S9n.js → web-DdzXb-jW.js} +1 -1
- package/dist/pwa/assets/{web-CfD_ah7K.js → web-Dl9aC-Qr.js} +1 -1
- package/dist/pwa/assets/{web-DugGj1t8.js → web-a9jK1xeo.js} +1 -1
- package/dist/pwa/index.html +3 -3
- package/dist/pwa/manifest.webmanifest +1 -1
- package/dist/pwa/service-worker.js +1 -1
- package/package.json +1 -1
- package/palmier-server/pwa/index.html +1 -1
- package/palmier-server/pwa/src/App.css +42 -81
- package/palmier-server/pwa/src/components/CapabilityToggles.tsx +51 -9
- package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +5 -1
- package/palmier-server/pwa/src/components/HostMenu.tsx +10 -10
- package/palmier-server/pwa/src/components/{PlanDialog.tsx → PermissionsDialog.tsx} +6 -6
- package/palmier-server/pwa/src/components/RunDetailView.tsx +2 -0
- package/palmier-server/pwa/src/components/SessionsView.tsx +1 -0
- package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +13 -3
- package/palmier-server/pwa/src/components/TaskForm.tsx +2 -2
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +23 -8
- package/palmier-server/pwa/src/pages/PairHost.tsx +11 -4
- package/palmier-server/pwa/src/pages/PairSetup.tsx +8 -6
- package/palmier-server/pwa/vite.config.ts +1 -1
- package/palmier-server/spec.md +2 -2
- package/src/platform/linux.ts +9 -0
- package/dist/pwa/assets/index-BLCVzS_l.js +0 -120
- package/dist/pwa/assets/index-Cjjw24Ok.css +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{W as p}from"./index-
|
|
1
|
+
import{W as p}from"./index-CknFGshO.js";class f extends p{constructor(){super(...arguments),this.group="CapacitorStorage"}async configure({group:e}){typeof e=="string"&&(this.group=e)}async get(e){return{value:this.impl.getItem(this.applyPrefix(e.key))}}async set(e){this.impl.setItem(this.applyPrefix(e.key),e.value)}async remove(e){this.impl.removeItem(this.applyPrefix(e.key))}async keys(){return{keys:this.rawKeys().map(t=>t.substring(this.prefix.length))}}async clear(){for(const e of this.rawKeys())this.impl.removeItem(e)}async migrate(){var e;const t=[],s=[],n="_cap_",o=Object.keys(this.impl).filter(i=>i.indexOf(n)===0);for(const i of o){const r=i.substring(n.length),a=(e=this.impl.getItem(i))!==null&&e!==void 0?e:"",{value:l}=await this.get({key:r});typeof l=="string"?s.push(r):(await this.set({key:r,value:a}),t.push(r))}return{migrated:t,existing:s}}async removeOld(){const e="_cap_",t=Object.keys(this.impl).filter(s=>s.indexOf(e)===0);for(const s of t)this.impl.removeItem(s)}get impl(){return window.localStorage}get prefix(){return this.group==="NativeStorage"?"":`${this.group}.`}rawKeys(){return Object.keys(this.impl).filter(e=>e.indexOf(this.prefix)===0)}applyPrefix(e){return this.prefix+e}}export{f as PreferencesWeb};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{W as i}from"./index-
|
|
1
|
+
import{W as i}from"./index-CknFGshO.js";function o(){const t=window.navigator.connection||window.navigator.mozConnection||window.navigator.webkitConnection;let n="unknown";const e=t?t.type||t.effectiveType:null;if(e&&typeof e=="string")switch(e){case"bluetooth":case"cellular":n="cellular";break;case"none":n="none";break;case"ethernet":case"wifi":case"wimax":n="wifi";break;case"other":case"unknown":n="unknown";break;case"slow-2g":case"2g":case"3g":n="cellular";break;case"4g":n="wifi";break}return n}class s extends i{constructor(){super(),this.handleOnline=()=>{const e={connected:!0,connectionType:o()};this.notifyListeners("networkStatusChange",e)},this.handleOffline=()=>{const n={connected:!1,connectionType:"none"};this.notifyListeners("networkStatusChange",n)},typeof window<"u"&&(window.addEventListener("online",this.handleOnline),window.addEventListener("offline",this.handleOffline))}async getStatus(){if(!window.navigator)throw this.unavailable("Browser does not support the Network Information API");const n=window.navigator.onLine,e=o();return{connected:n,connectionType:n?e:"none"}}}const r=new s;export{r as Network,s as NetworkWeb};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{W as t}from"./index-
|
|
1
|
+
import{W as t}from"./index-CknFGshO.js";class s extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{s as AppWeb};
|
package/dist/pwa/index.html
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
8
8
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
|
9
9
|
<title>Palmier</title>
|
|
10
|
-
<meta name="description" content="
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
10
|
+
<meta name="description" content="Bridge your AI agents and your phone. Agents on your machine get your phone as a tool — SMS, calendar, GPS, alarms, approvals — and your phone as the remote to run them from anywhere." />
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-CknFGshO.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BsB1tIsn.css">
|
|
13
13
|
<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"Palmier","short_name":"Palmier","description":"
|
|
1
|
+
{"name":"Palmier","short_name":"Palmier","description":"Bridge your AI agents and your phone. Agents on your machine get your phone as a tool — SMS, calendar, GPS, alarms, approvals — and your phone as the remote to run them from anywhere.","start_url":"/","display":"standalone","background_color":"#ffffff","theme_color":"#2E5CE5","lang":"en","scope":"/","icons":[{"src":"pwa-192x192.png","sizes":"192x192","type":"image/png"},{"src":"pwa-512x512.png","sizes":"512x512","type":"image/png"}]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
try{self["workbox:core:7.3.0"]&&_()}catch{}const N=(n,...e)=>{let t=n;return e.length>0&&(t+=` :: ${JSON.stringify(e)}`),t},E=N;class h extends Error{constructor(e,t){const s=E(e,t);super(s),this.name=e,this.details=t}}const d={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:typeof registration<"u"?registration.scope:""},U=n=>[d.prefix,n,d.suffix].filter(e=>e&&e.length>0).join("-"),O=n=>{for(const e of Object.keys(d))n(e)},L={updateDetails:n=>{O(e=>{typeof n[e]=="string"&&(d[e]=n[e])})},getGoogleAnalyticsName:n=>n||U(d.googleAnalytics),getPrecacheName:n=>n||U(d.precache),getPrefix:()=>d.prefix,getRuntimeName:n=>n||U(d.runtime),getSuffix:()=>d.suffix};function v(n,e){const t=e();return n.waitUntil(t),t}try{self["workbox:precaching:7.3.0"]&&_()}catch{}const A="__WB_REVISION__";function M(n){if(!n)throw new h("add-to-cache-list-unexpected-type",{entry:n});if(typeof n=="string"){const i=new URL(n,location.href);return{cacheKey:i.href,url:i.href}}const{revision:e,url:t}=n;if(!t)throw new h("add-to-cache-list-unexpected-type",{entry:n});if(!e){const i=new URL(t,location.href);return{cacheKey:i.href,url:i.href}}const s=new URL(t,location.href),a=new URL(t,location.href);return s.searchParams.set(A,e),{cacheKey:s.href,url:a.href}}class W{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:s})=>{if(e.type==="install"&&t&&t.originalRequest&&t.originalRequest instanceof Request){const a=t.originalRequest.url;s?this.notUpdatedURLs.push(a):this.updatedURLs.push(a)}return s}}}class q{constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:t,params:s})=>{const a=(s==null?void 0:s.cacheKey)||this._precacheController.getCacheKeyForURL(t.url);return a?new Request(a,{headers:t.headers}):t},this._precacheController=e}}let w;function S(){if(w===void 0){const n=new Response("");if("body"in n)try{new Response(n.body),w=!0}catch{w=!1}w=!1}return w}async function j(n,e){let t=null;if(n.url&&(t=new URL(n.url).origin),t!==self.location.origin)throw new h("cross-origin-copy-response",{origin:t});const s=n.clone(),i={headers:new Headers(s.headers),status:s.status,statusText:s.statusText},r=S()?s.body:await s.blob();return new Response(r,i)}const D=n=>new URL(String(n),location.href).href.replace(new RegExp(`^${location.origin}`),"");function x(n,e){const t=new URL(n);for(const s of e)t.searchParams.delete(s);return t.href}async function H(n,e,t,s){const a=x(e.url,t);if(e.url===a)return n.match(e,s);const i=Object.assign(Object.assign({},s),{ignoreSearch:!0}),r=await n.keys(e,i);for(const c of r){const o=x(c.url,t);if(a===o)return n.match(c,s)}}class F{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}}const $=new Set;async function B(){for(const n of $)await n()}function V(n){return new Promise(e=>setTimeout(e,n))}try{self["workbox:strategies:7.3.0"]&&_()}catch{}function C(n){return typeof n=="string"?new Request(n):n}class G{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new F,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(const s of this._plugins)this._pluginStateMap.set(s,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){const{event:t}=this;let s=C(e);if(s.mode==="navigate"&&t instanceof FetchEvent&&t.preloadResponse){const r=await t.preloadResponse;if(r)return r}const a=this.hasCallback("fetchDidFail")?s.clone():null;try{for(const r of this.iterateCallbacks("requestWillFetch"))s=await r({request:s.clone(),event:t})}catch(r){if(r instanceof Error)throw new h("plugin-error-request-will-fetch",{thrownErrorMessage:r.message})}const i=s.clone();try{let r;r=await fetch(s,s.mode==="navigate"?void 0:this._strategy.fetchOptions);for(const c of this.iterateCallbacks("fetchDidSucceed"))r=await c({event:t,request:i,response:r});return r}catch(r){throw a&&await this.runCallbacks("fetchDidFail",{error:r,event:t,originalRequest:a.clone(),request:i.clone()}),r}}async fetchAndCachePut(e){const t=await this.fetch(e),s=t.clone();return this.waitUntil(this.cachePut(e,s)),t}async cacheMatch(e){const t=C(e);let s;const{cacheName:a,matchOptions:i}=this._strategy,r=await this.getCacheKey(t,"read"),c=Object.assign(Object.assign({},i),{cacheName:a});s=await caches.match(r,c);for(const o of this.iterateCallbacks("cachedResponseWillBeUsed"))s=await o({cacheName:a,matchOptions:i,cachedResponse:s,request:r,event:this.event})||void 0;return s}async cachePut(e,t){const s=C(e);await V(0);const a=await this.getCacheKey(s,"write");if(!t)throw new h("cache-put-with-no-response",{url:D(a.url)});const i=await this._ensureResponseSafeToCache(t);if(!i)return!1;const{cacheName:r,matchOptions:c}=this._strategy,o=await self.caches.open(r),l=this.hasCallback("cacheDidUpdate"),u=l?await H(o,a.clone(),["__WB_REVISION__"],c):null;try{await o.put(a,l?i.clone():i)}catch(f){if(f instanceof Error)throw f.name==="QuotaExceededError"&&await B(),f}for(const f of this.iterateCallbacks("cacheDidUpdate"))await f({cacheName:r,oldResponse:u,newResponse:i.clone(),request:a,event:this.event});return!0}async getCacheKey(e,t){const s=`${e.url} | ${t}`;if(!this._cacheKeys[s]){let a=e;for(const i of this.iterateCallbacks("cacheKeyWillBeUsed"))a=C(await i({mode:t,request:a,event:this.event,params:this.params}));this._cacheKeys[s]=a}return this._cacheKeys[s]}hasCallback(e){for(const t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(const s of this.iterateCallbacks(e))await s(t)}*iterateCallbacks(e){for(const t of this._strategy.plugins)if(typeof t[e]=="function"){const s=this._pluginStateMap.get(t);yield i=>{const r=Object.assign(Object.assign({},i),{state:s});return t[e](r)}}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){for(;this._extendLifetimePromises.length;){const e=this._extendLifetimePromises.splice(0),s=(await Promise.allSettled(e)).find(a=>a.status==="rejected");if(s)throw s.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,s=!1;for(const a of this.iterateCallbacks("cacheWillUpdate"))if(t=await a({request:this.request,response:t,event:this.event})||void 0,s=!0,!t)break;return s||t&&t.status!==200&&(t=void 0),t}}class J{constructor(e={}){this.cacheName=L.getRuntimeName(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){const[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});const t=e.event,s=typeof e.request=="string"?new Request(e.request):e.request,a="params"in e?e.params:void 0,i=new G(this,{event:t,request:s,params:a}),r=this._getResponse(i,s,t),c=this._awaitComplete(r,i,s,t);return[r,c]}async _getResponse(e,t,s){await e.runCallbacks("handlerWillStart",{event:s,request:t});let a;try{if(a=await this._handle(t,e),!a||a.type==="error")throw new h("no-response",{url:t.url})}catch(i){if(i instanceof Error){for(const r of e.iterateCallbacks("handlerDidError"))if(a=await r({error:i,event:s,request:t}),a)break}if(!a)throw i}for(const i of e.iterateCallbacks("handlerWillRespond"))a=await i({event:s,request:t,response:a});return a}async _awaitComplete(e,t,s,a){let i,r;try{i=await e}catch{}try{await t.runCallbacks("handlerDidRespond",{event:a,request:s,response:i}),await t.doneWaiting()}catch(c){c instanceof Error&&(r=c)}if(await t.runCallbacks("handlerDidComplete",{event:a,request:s,response:i,error:r}),t.destroy(),r)throw r}}class p extends J{constructor(e={}){e.cacheName=L.getPrecacheName(e.cacheName),super(e),this._fallbackToNetwork=e.fallbackToNetwork!==!1,this.plugins.push(p.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){const s=await t.cacheMatch(e);return s||(t.event&&t.event.type==="install"?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,t){let s;const a=t.params||{};if(this._fallbackToNetwork){const i=a.integrity,r=e.integrity,c=!r||r===i;s=await t.fetch(new Request(e,{integrity:e.mode!=="no-cors"?r||i:void 0})),i&&c&&e.mode!=="no-cors"&&(this._useDefaultCacheabilityPluginIfNeeded(),await t.cachePut(e,s.clone()))}else throw new h("missing-precache-entry",{cacheName:this.cacheName,url:e.url});return s}async _handleInstall(e,t){this._useDefaultCacheabilityPluginIfNeeded();const s=await t.fetch(e);if(!await t.cachePut(e,s.clone()))throw new h("bad-precaching-response",{url:e.url,status:s.status});return s}_useDefaultCacheabilityPluginIfNeeded(){let e=null,t=0;for(const[s,a]of this.plugins.entries())a!==p.copyRedirectedCacheableResponsesPlugin&&(a===p.defaultPrecacheCacheabilityPlugin&&(e=s),a.cacheWillUpdate&&t++);t===0?this.plugins.push(p.defaultPrecacheCacheabilityPlugin):t>1&&e!==null&&this.plugins.splice(e,1)}}p.defaultPrecacheCacheabilityPlugin={async cacheWillUpdate({response:n}){return!n||n.status>=400?null:n}};p.copyRedirectedCacheableResponsesPlugin={async cacheWillUpdate({response:n}){return n.redirected?await j(n):n}};class Q{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:s=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new p({cacheName:L.getPrecacheName(e),plugins:[...t,new q({precacheController:this})],fallbackToNetwork:s}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this._strategy}precache(e){this.addToCacheList(e),this._installAndActiveListenersAdded||(self.addEventListener("install",this.install),self.addEventListener("activate",this.activate),this._installAndActiveListenersAdded=!0)}addToCacheList(e){const t=[];for(const s of e){typeof s=="string"?t.push(s):s&&s.revision===void 0&&t.push(s.url);const{cacheKey:a,url:i}=M(s),r=typeof s!="string"&&s.revision?"reload":"default";if(this._urlsToCacheKeys.has(i)&&this._urlsToCacheKeys.get(i)!==a)throw new h("add-to-cache-list-conflicting-entries",{firstEntry:this._urlsToCacheKeys.get(i),secondEntry:a});if(typeof s!="string"&&s.integrity){if(this._cacheKeysToIntegrities.has(a)&&this._cacheKeysToIntegrities.get(a)!==s.integrity)throw new h("add-to-cache-list-conflicting-integrities",{url:i});this._cacheKeysToIntegrities.set(a,s.integrity)}if(this._urlsToCacheKeys.set(i,a),this._urlsToCacheModes.set(i,r),t.length>0){const c=`Workbox is precaching URLs without revision info: ${t.join(", ")}
|
|
2
|
-
This is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(c)}}}install(e){return v(e,async()=>{const t=new W;this.strategy.plugins.push(t);for(const[i,r]of this._urlsToCacheKeys){const c=this._cacheKeysToIntegrities.get(r),o=this._urlsToCacheModes.get(i),l=new Request(i,{integrity:c,cache:o,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:r},request:l,event:e}))}const{updatedURLs:s,notUpdatedURLs:a}=t;return{updatedURLs:s,notUpdatedURLs:a}})}activate(e){return v(e,async()=>{const t=await self.caches.open(this.strategy.cacheName),s=await t.keys(),a=new Set(this._urlsToCacheKeys.values()),i=[];for(const r of s)a.has(r.url)||(await t.delete(r),i.push(r.url));return{deletedURLs:i}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){const t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){const t=e instanceof Request?e.url:e,s=this.getCacheKeyForURL(t);if(s)return(await self.caches.open(this.strategy.cacheName)).match(s)}createHandlerBoundToURL(e){const t=this.getCacheKeyForURL(e);if(!t)throw new h("non-precached-url",{url:e});return s=>(s.request=new Request(e),s.params=Object.assign({cacheKey:t},s.params),this.strategy.handle(s))}}let k;const T=()=>(k||(k=new Q),k);try{self["workbox:routing:7.3.0"]&&_()}catch{}const I="GET",b=n=>n&&typeof n=="object"?n:{handle:n};class R{constructor(e,t,s=I){this.handler=b(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=b(e)}}class z extends R{constructor(e,t,s){const a=({url:i})=>{const r=e.exec(i.href);if(r&&!(i.origin!==location.origin&&r.index!==0))return r.slice(1)};super(a,t,s)}}class X{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener("fetch",(e=>{const{request:t}=e,s=this.handleRequest({request:t,event:e});s&&e.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(e=>{if(e.data&&e.data.type==="CACHE_URLS"){const{payload:t}=e.data,s=Promise.all(t.urlsToCache.map(a=>{typeof a=="string"&&(a=[a]);const i=new Request(...a);return this.handleRequest({request:i,event:e})}));e.waitUntil(s),e.ports&&e.ports[0]&&s.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){const s=new URL(e.url,location.href);if(!s.protocol.startsWith("http"))return;const a=s.origin===location.origin,{params:i,route:r}=this.findMatchingRoute({event:t,request:e,sameOrigin:a,url:s});let c=r&&r.handler;const o=e.method;if(!c&&this._defaultHandlerMap.has(o)&&(c=this._defaultHandlerMap.get(o)),!c)return;let l;try{l=c.handle({url:s,request:e,event:t,params:i})}catch(f){l=Promise.reject(f)}const u=r&&r.catchHandler;return l instanceof Promise&&(this._catchHandler||u)&&(l=l.catch(async f=>{if(u)try{return await u.handle({url:s,request:e,event:t,params:i})}catch(g){g instanceof Error&&(f=g)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw f})),l}findMatchingRoute({url:e,sameOrigin:t,request:s,event:a}){const i=this._routes.get(s.method)||[];for(const r of i){let c;const o=r.match({url:e,sameOrigin:t,request:s,event:a});if(o)return c=o,(Array.isArray(c)&&c.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o=="boolean")&&(c=void 0),{route:r,params:c}}return{}}setDefaultHandler(e,t=I){this._defaultHandlerMap.set(t,b(e))}setCatchHandler(e){this._catchHandler=b(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new h("unregister-route-but-not-found-with-method",{method:e.method});const t=this._routes.get(e.method).indexOf(e);if(t>-1)this._routes.get(e.method).splice(t,1);else throw new h("unregister-route-route-not-registered")}}let m;const Y=()=>(m||(m=new X,m.addFetchListener(),m.addCacheListener()),m);function Z(n,e,t){let s;if(typeof n=="string"){const i=new URL(n,location.href),r=({url:c})=>c.href===i.href;s=new R(r,e,t)}else if(n instanceof RegExp)s=new z(n,e,t);else if(typeof n=="function")s=new R(n,e,t);else if(n instanceof R)s=n;else throw new h("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});return Y().registerRoute(s),s}function ee(n,e=[]){for(const t of[...n.searchParams.keys()])e.some(s=>s.test(t))&&n.searchParams.delete(t);return n}function*te(n,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:t="index.html",cleanURLs:s=!0,urlManipulation:a}={}){const i=new URL(n,location.href);i.hash="",yield i.href;const r=ee(i,e);if(yield r.href,t&&r.pathname.endsWith("/")){const c=new URL(r.href);c.pathname+=t,yield c.href}if(s){const c=new URL(r.href);c.pathname+=".html",yield c.href}if(a){const c=a({url:i});for(const o of c)yield o.href}}class se extends R{constructor(e,t){const s=({request:a})=>{const i=e.getURLsToCacheKeys();for(const r of te(a.url,t)){const c=i.get(r);if(c){const o=e.getIntegrityForCacheKey(c);return{cacheKey:c,integrity:o}}}};super(s,e.strategy)}}function ne(n){const e=T(),t=new se(e,n);Z(t)}function ae(n){T().precache(n)}function ie(n,e){ae(n),ne(e)}ie([{"revision":"38013143dc2183340ede8bc1c5124507","url":"registerSW.js"},{"revision":"
|
|
2
|
+
This is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(c)}}}install(e){return v(e,async()=>{const t=new W;this.strategy.plugins.push(t);for(const[i,r]of this._urlsToCacheKeys){const c=this._cacheKeysToIntegrities.get(r),o=this._urlsToCacheModes.get(i),l=new Request(i,{integrity:c,cache:o,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:r},request:l,event:e}))}const{updatedURLs:s,notUpdatedURLs:a}=t;return{updatedURLs:s,notUpdatedURLs:a}})}activate(e){return v(e,async()=>{const t=await self.caches.open(this.strategy.cacheName),s=await t.keys(),a=new Set(this._urlsToCacheKeys.values()),i=[];for(const r of s)a.has(r.url)||(await t.delete(r),i.push(r.url));return{deletedURLs:i}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){const t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){const t=e instanceof Request?e.url:e,s=this.getCacheKeyForURL(t);if(s)return(await self.caches.open(this.strategy.cacheName)).match(s)}createHandlerBoundToURL(e){const t=this.getCacheKeyForURL(e);if(!t)throw new h("non-precached-url",{url:e});return s=>(s.request=new Request(e),s.params=Object.assign({cacheKey:t},s.params),this.strategy.handle(s))}}let k;const T=()=>(k||(k=new Q),k);try{self["workbox:routing:7.3.0"]&&_()}catch{}const I="GET",b=n=>n&&typeof n=="object"?n:{handle:n};class R{constructor(e,t,s=I){this.handler=b(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=b(e)}}class z extends R{constructor(e,t,s){const a=({url:i})=>{const r=e.exec(i.href);if(r&&!(i.origin!==location.origin&&r.index!==0))return r.slice(1)};super(a,t,s)}}class X{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener("fetch",(e=>{const{request:t}=e,s=this.handleRequest({request:t,event:e});s&&e.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(e=>{if(e.data&&e.data.type==="CACHE_URLS"){const{payload:t}=e.data,s=Promise.all(t.urlsToCache.map(a=>{typeof a=="string"&&(a=[a]);const i=new Request(...a);return this.handleRequest({request:i,event:e})}));e.waitUntil(s),e.ports&&e.ports[0]&&s.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){const s=new URL(e.url,location.href);if(!s.protocol.startsWith("http"))return;const a=s.origin===location.origin,{params:i,route:r}=this.findMatchingRoute({event:t,request:e,sameOrigin:a,url:s});let c=r&&r.handler;const o=e.method;if(!c&&this._defaultHandlerMap.has(o)&&(c=this._defaultHandlerMap.get(o)),!c)return;let l;try{l=c.handle({url:s,request:e,event:t,params:i})}catch(f){l=Promise.reject(f)}const u=r&&r.catchHandler;return l instanceof Promise&&(this._catchHandler||u)&&(l=l.catch(async f=>{if(u)try{return await u.handle({url:s,request:e,event:t,params:i})}catch(g){g instanceof Error&&(f=g)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw f})),l}findMatchingRoute({url:e,sameOrigin:t,request:s,event:a}){const i=this._routes.get(s.method)||[];for(const r of i){let c;const o=r.match({url:e,sameOrigin:t,request:s,event:a});if(o)return c=o,(Array.isArray(c)&&c.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o=="boolean")&&(c=void 0),{route:r,params:c}}return{}}setDefaultHandler(e,t=I){this._defaultHandlerMap.set(t,b(e))}setCatchHandler(e){this._catchHandler=b(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new h("unregister-route-but-not-found-with-method",{method:e.method});const t=this._routes.get(e.method).indexOf(e);if(t>-1)this._routes.get(e.method).splice(t,1);else throw new h("unregister-route-route-not-registered")}}let m;const Y=()=>(m||(m=new X,m.addFetchListener(),m.addCacheListener()),m);function Z(n,e,t){let s;if(typeof n=="string"){const i=new URL(n,location.href),r=({url:c})=>c.href===i.href;s=new R(r,e,t)}else if(n instanceof RegExp)s=new z(n,e,t);else if(typeof n=="function")s=new R(n,e,t);else if(n instanceof R)s=n;else throw new h("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});return Y().registerRoute(s),s}function ee(n,e=[]){for(const t of[...n.searchParams.keys()])e.some(s=>s.test(t))&&n.searchParams.delete(t);return n}function*te(n,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:t="index.html",cleanURLs:s=!0,urlManipulation:a}={}){const i=new URL(n,location.href);i.hash="",yield i.href;const r=ee(i,e);if(yield r.href,t&&r.pathname.endsWith("/")){const c=new URL(r.href);c.pathname+=t,yield c.href}if(s){const c=new URL(r.href);c.pathname+=".html",yield c.href}if(a){const c=a({url:i});for(const o of c)yield o.href}}class se extends R{constructor(e,t){const s=({request:a})=>{const i=e.getURLsToCacheKeys();for(const r of te(a.url,t)){const c=i.get(r);if(c){const o=e.getIntegrityForCacheKey(c);return{cacheKey:c,integrity:o}}}};super(s,e.strategy)}}function ne(n){const e=T(),t=new se(e,n);Z(t)}function ae(n){T().precache(n)}function ie(n,e){ae(n),ne(e)}ie([{"revision":"38013143dc2183340ede8bc1c5124507","url":"registerSW.js"},{"revision":"24edcc91783323299757366e29c6a55e","url":"index.html"},{"revision":null,"url":"assets/web-a9jK1xeo.js"},{"revision":null,"url":"assets/web-Dl9aC-Qr.js"},{"revision":null,"url":"assets/web-DdzXb-jW.js"},{"revision":null,"url":"assets/index-CknFGshO.js"},{"revision":null,"url":"assets/index-BsB1tIsn.css"},{"revision":"fcc457fce855ad0df7178e0786c0d4ef","url":"apple-touch-icon.png"},{"revision":"276650c30bc4effc7d649ec66519aab6","url":"favicon.ico"},{"revision":"2e46512b835c05e17787059909305f22","url":"pwa-192x192.png"},{"revision":"ec5652b5834b4711337743e80e506a41","url":"pwa-512x512.png"},{"revision":"1c5f5d4fb1f74fd2c4cb52075f2643f6","url":"manifest.webmanifest"}]);const re="/api/push/respond";self.addEventListener("message",n=>{});self.addEventListener("push",n=>{var r;if(!n.data)return;let e;try{e=n.data.json()}catch{e={title:"Palmier",body:n.data.text()}}const t=e.type??((r=e.data)==null?void 0:r.type);if(t==="confirm-dismiss"||t==="permission-dismiss"||t==="input-dismiss"){const c=e.data??e,o=c.host_id,l=c.session_id,u=c.task_id;n.waitUntil(self.registration.getNotifications().then(f=>{var g,P,K;for(const y of f)if(((g=y.data)==null?void 0:g.host_id)===o){if(l&&((P=y.data)==null?void 0:P.session_id)===l){y.close();continue}u&&((K=y.data)==null?void 0:K.task_id)===u&&y.close()}}));return}const s=e.title??"Palmier";let a=e.body??"";!a&&t==="confirm"&&(a="A task requires confirmation to run."),!a&&t==="permission"&&(a="A task needs additional permissions to continue."),!a&&t==="input"&&(a="A task needs your input to continue.");const i={body:a,icon:"/pwa-192x192.png",badge:"/pwa-192x192.png",data:e.data??e,vibrate:[100,50,100]};t==="confirm"&&(i.actions=[{action:"confirm",title:"Confirm"},{action:"abort",title:"Abort"}]),n.waitUntil(self.registration.showNotification(s,i))});self.addEventListener("notificationclick",n=>{const e=n.notification;e.close();const t=e.data??{},s=n.action;if(s&&t.type==="confirm"&&t.session_id&&t.host_id){const a=s==="confirm"?"confirmed":"aborted";n.waitUntil(fetch(re,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({session_id:t.session_id,host_id:t.host_id,response:a})}).catch(i=>{console.error("Failed to send push response:",i)}))}else{const a=t.host_id,i=t.task_id,r=t.run_id,c=a?`/hosts/${encodeURIComponent(a)}`:"",o=c&&i&&r?`${c}/runs/${encodeURIComponent(i)}/${encodeURIComponent(r)}`:c&&i?`${c}/runs/${encodeURIComponent(i)}/latest`:c||"/";n.waitUntil(self.clients.matchAll({type:"window",includeUncontrolled:!0}).then(l=>{for(const u of l)if(u.url.includes(self.location.origin)&&"focus"in u)return u.navigate(o),u.focus();return self.clients.openWindow(o)}))}});self.addEventListener("install",()=>{self.skipWaiting()});self.addEventListener("activate",n=>{n.waitUntil(self.clients.claim())});
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
8
8
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
|
9
9
|
<title>Palmier</title>
|
|
10
|
-
<meta name="description" content="
|
|
10
|
+
<meta name="description" content="Bridge your AI agents and your phone. Agents on your machine get your phone as a tool — SMS, calendar, GPS, alarms, approvals — and your phone as the remote to run them from anywhere." />
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
|
@@ -453,6 +453,14 @@ body {
|
|
|
453
453
|
-webkit-overflow-scrolling: touch;
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
+
.pair-platform-label {
|
|
457
|
+
display: block;
|
|
458
|
+
margin-top: 8px;
|
|
459
|
+
font-size: 0.75rem;
|
|
460
|
+
font-weight: 600;
|
|
461
|
+
color: var(--color-muted);
|
|
462
|
+
}
|
|
463
|
+
|
|
456
464
|
.pair-instruction-divider {
|
|
457
465
|
height: 1px;
|
|
458
466
|
background: var(--color-border);
|
|
@@ -600,6 +608,18 @@ body {
|
|
|
600
608
|
animation: spin 0.7s linear infinite;
|
|
601
609
|
}
|
|
602
610
|
|
|
611
|
+
.spinner-lg {
|
|
612
|
+
width: 40px;
|
|
613
|
+
height: 40px;
|
|
614
|
+
border-width: 3px;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
.capability-toggles-loading {
|
|
618
|
+
display: flex;
|
|
619
|
+
justify-content: center;
|
|
620
|
+
padding: var(--space-lg);
|
|
621
|
+
}
|
|
622
|
+
|
|
603
623
|
@keyframes spin {
|
|
604
624
|
to {
|
|
605
625
|
transform: rotate(360deg);
|
|
@@ -1038,7 +1058,7 @@ body {
|
|
|
1038
1058
|
flex-wrap: wrap;
|
|
1039
1059
|
}
|
|
1040
1060
|
|
|
1041
|
-
.
|
|
1061
|
+
.permissions-dialog {
|
|
1042
1062
|
display: flex;
|
|
1043
1063
|
flex-direction: column;
|
|
1044
1064
|
gap: var(--space-md);
|
|
@@ -1047,22 +1067,18 @@ body {
|
|
|
1047
1067
|
min-height: 0;
|
|
1048
1068
|
}
|
|
1049
1069
|
|
|
1050
|
-
.
|
|
1070
|
+
.permissions-dialog h2 {
|
|
1051
1071
|
margin: 0;
|
|
1052
1072
|
font-size: 1.125rem;
|
|
1053
1073
|
}
|
|
1054
1074
|
|
|
1055
|
-
.
|
|
1075
|
+
.permissions-dialog-scroll {
|
|
1056
1076
|
flex: 1;
|
|
1057
1077
|
min-height: 0;
|
|
1058
1078
|
overflow-y: auto;
|
|
1059
1079
|
}
|
|
1060
1080
|
|
|
1061
|
-
.
|
|
1062
|
-
max-height: none;
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
.plan-empty {
|
|
1081
|
+
.permissions-empty {
|
|
1066
1082
|
color: var(--color-text-secondary);
|
|
1067
1083
|
font-size: 0.875rem;
|
|
1068
1084
|
font-style: italic;
|
|
@@ -1101,7 +1117,7 @@ body {
|
|
|
1101
1117
|
font-size: 0.9em;
|
|
1102
1118
|
}
|
|
1103
1119
|
|
|
1104
|
-
.
|
|
1120
|
+
.permissions-dialog-actions {
|
|
1105
1121
|
display: flex;
|
|
1106
1122
|
gap: var(--space-sm);
|
|
1107
1123
|
align-items: center;
|
|
@@ -1109,7 +1125,7 @@ body {
|
|
|
1109
1125
|
}
|
|
1110
1126
|
|
|
1111
1127
|
@media (min-width: 600px) {
|
|
1112
|
-
.
|
|
1128
|
+
.permissions-dialog {
|
|
1113
1129
|
max-width: none;
|
|
1114
1130
|
}
|
|
1115
1131
|
}
|
|
@@ -1135,73 +1151,6 @@ body {
|
|
|
1135
1151
|
50% { opacity: 1; }
|
|
1136
1152
|
}
|
|
1137
1153
|
|
|
1138
|
-
.plan-preview {
|
|
1139
|
-
font-size: 0.8125rem;
|
|
1140
|
-
line-height: 1.6;
|
|
1141
|
-
color: var(--color-text-secondary);
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
.plan-preview h1,
|
|
1145
|
-
.plan-preview h2,
|
|
1146
|
-
.plan-preview h3,
|
|
1147
|
-
.plan-preview h4 {
|
|
1148
|
-
color: var(--color-text);
|
|
1149
|
-
margin: var(--space-sm) 0 var(--space-xs) 0;
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
.plan-preview h1 { font-size: 1.1rem; }
|
|
1153
|
-
.plan-preview h2 { font-size: 1rem; }
|
|
1154
|
-
.plan-preview h3 { font-size: 0.9375rem; }
|
|
1155
|
-
|
|
1156
|
-
.plan-preview p {
|
|
1157
|
-
margin: var(--space-xs) 0;
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
.plan-preview ul,
|
|
1161
|
-
.plan-preview ol {
|
|
1162
|
-
padding-left: var(--space-md);
|
|
1163
|
-
margin: var(--space-xs) 0;
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
.plan-preview code {
|
|
1167
|
-
background: var(--color-border);
|
|
1168
|
-
border-radius: 3px;
|
|
1169
|
-
padding: 1px 4px;
|
|
1170
|
-
font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
|
|
1171
|
-
font-size: 0.75rem;
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
.plan-preview pre {
|
|
1175
|
-
background: var(--color-border);
|
|
1176
|
-
border-radius: var(--radius-sm);
|
|
1177
|
-
padding: var(--space-sm);
|
|
1178
|
-
overflow-x: auto;
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
.plan-preview pre code {
|
|
1182
|
-
background: none;
|
|
1183
|
-
padding: 0;
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
.plan-preview table {
|
|
1187
|
-
width: 100%;
|
|
1188
|
-
border-collapse: collapse;
|
|
1189
|
-
margin: var(--space-xs) 0;
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
.plan-preview th,
|
|
1193
|
-
.plan-preview td {
|
|
1194
|
-
border: 1px solid var(--color-border);
|
|
1195
|
-
padding: var(--space-xs) var(--space-sm);
|
|
1196
|
-
text-align: left;
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
.plan-preview th {
|
|
1200
|
-
background: var(--color-border);
|
|
1201
|
-
color: var(--color-text);
|
|
1202
|
-
font-weight: 600;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
1154
|
.granted-permissions-row {
|
|
1206
1155
|
flex-basis: 100%;
|
|
1207
1156
|
}
|
|
@@ -2776,17 +2725,24 @@ body {
|
|
|
2776
2725
|
right: 0;
|
|
2777
2726
|
bottom: 0;
|
|
2778
2727
|
display: flex;
|
|
2728
|
+
flex-direction: column;
|
|
2779
2729
|
align-items: center;
|
|
2780
2730
|
justify-content: center;
|
|
2731
|
+
gap: 4px;
|
|
2781
2732
|
background: var(--color-error, #dc2626);
|
|
2782
2733
|
color: #fff;
|
|
2783
2734
|
border: none;
|
|
2784
|
-
font-size: 0.
|
|
2735
|
+
font-size: 0.75rem;
|
|
2785
2736
|
font-weight: 600;
|
|
2786
2737
|
cursor: pointer;
|
|
2787
2738
|
padding: 0;
|
|
2788
2739
|
}
|
|
2789
2740
|
|
|
2741
|
+
.swipe-row-action-label {
|
|
2742
|
+
font-size: 0.75rem;
|
|
2743
|
+
line-height: 1;
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2790
2746
|
.swipe-row-action:focus-visible {
|
|
2791
2747
|
outline: 2px solid var(--color-accent, #2E5CE5);
|
|
2792
2748
|
outline-offset: -4px;
|
|
@@ -3026,10 +2982,15 @@ body {
|
|
|
3026
2982
|
}
|
|
3027
2983
|
|
|
3028
2984
|
.pair-setup-loading {
|
|
3029
|
-
padding: var(--space-lg);
|
|
3030
|
-
|
|
2985
|
+
padding: var(--space-xl, 32px) var(--space-lg);
|
|
2986
|
+
min-height: 200px;
|
|
2987
|
+
display: flex;
|
|
2988
|
+
flex-direction: column;
|
|
2989
|
+
align-items: center;
|
|
2990
|
+
justify-content: center;
|
|
2991
|
+
gap: var(--space-md);
|
|
3031
2992
|
color: var(--color-muted);
|
|
3032
|
-
font-size: 0.
|
|
2993
|
+
font-size: 0.95rem;
|
|
3033
2994
|
}
|
|
3034
2995
|
|
|
3035
2996
|
.pair-setup-actions {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
2
3
|
import { Capacitor } from "@capacitor/core";
|
|
3
4
|
import { App as CapacitorApp } from "@capacitor/app";
|
|
4
5
|
import { Device, type CapabilityStatus } from "../native/Device";
|
|
@@ -39,11 +40,15 @@ export async function loadEnabledCapabilities(): Promise<Set<string>> {
|
|
|
39
40
|
|
|
40
41
|
interface CapabilityTogglesProps {
|
|
41
42
|
onChange?(enabled: Set<string>): void;
|
|
43
|
+
/** When true, disabling a currently-on capability prompts a confirmation
|
|
44
|
+
* mentioning that all linked hosts will lose it. */
|
|
45
|
+
confirmDisable?: boolean;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
export default function CapabilityToggles({ onChange }: CapabilityTogglesProps) {
|
|
48
|
+
export default function CapabilityToggles({ onChange, confirmDisable = false }: CapabilityTogglesProps) {
|
|
45
49
|
const [statuses, setStatuses] = useState<Map<string, CapabilityStatus>>(new Map());
|
|
46
50
|
const [busyCapability, setBusyCapability] = useState<string | null>(null);
|
|
51
|
+
const [confirmingDisable, setConfirmingDisable] = useState<CapabilityDefinition | null>(null);
|
|
47
52
|
|
|
48
53
|
const refresh = useCallback(async () => {
|
|
49
54
|
if (!isNative || !Device) return;
|
|
@@ -66,16 +71,11 @@ export default function CapabilityToggles({ onChange }: CapabilityTogglesProps)
|
|
|
66
71
|
return () => { listener.then((h) => h.remove()); };
|
|
67
72
|
}, [refresh]);
|
|
68
73
|
|
|
69
|
-
async function
|
|
74
|
+
async function applyToggle(definition: CapabilityDefinition, enabled: boolean) {
|
|
70
75
|
if (!Device) return;
|
|
71
|
-
const status = statuses.get(definition.capability);
|
|
72
|
-
if (!status) return;
|
|
73
76
|
setBusyCapability(definition.capability);
|
|
74
77
|
try {
|
|
75
|
-
const result = await Device.setCapabilityEnabled({
|
|
76
|
-
capability: definition.capability,
|
|
77
|
-
enabled: !status.enabled,
|
|
78
|
-
});
|
|
78
|
+
const result = await Device.setCapabilityEnabled({ capability: definition.capability, enabled });
|
|
79
79
|
if (!result.enabled && result.reason === "no-email-client") {
|
|
80
80
|
alert("No email app is installed on this device. Install one (e.g. Gmail) before enabling Send Email.");
|
|
81
81
|
}
|
|
@@ -87,6 +87,25 @@ export default function CapabilityToggles({ onChange }: CapabilityTogglesProps)
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
function toggleCapability(definition: CapabilityDefinition) {
|
|
91
|
+
const status = statuses.get(definition.capability);
|
|
92
|
+
if (!status) return;
|
|
93
|
+
if (status.enabled && confirmDisable) {
|
|
94
|
+
setConfirmingDisable(definition);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
applyToggle(definition, !status.enabled);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function confirmAndDisable() {
|
|
101
|
+
const definition = confirmingDisable;
|
|
102
|
+
if (!definition) return;
|
|
103
|
+
setConfirmingDisable(null);
|
|
104
|
+
applyToggle(definition, false);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!isNative) return null;
|
|
108
|
+
|
|
90
109
|
const visibleGroups = CAPABILITY_GROUPS
|
|
91
110
|
.map((group) => ({
|
|
92
111
|
group,
|
|
@@ -97,7 +116,29 @@ export default function CapabilityToggles({ onChange }: CapabilityTogglesProps)
|
|
|
97
116
|
}))
|
|
98
117
|
.filter((g) => g.items.length > 0);
|
|
99
118
|
|
|
100
|
-
if (
|
|
119
|
+
if (visibleGroups.length === 0) {
|
|
120
|
+
return (
|
|
121
|
+
<div className="capability-toggles-loading">
|
|
122
|
+
<span className="spinner" />
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const disableModal = confirmingDisable && createPortal(
|
|
128
|
+
<div className="confirm-modal-overlay" onClick={() => setConfirmingDisable(null)}>
|
|
129
|
+
<div className="confirm-modal" onClick={(e) => e.stopPropagation()}>
|
|
130
|
+
<h2 className="confirm-modal-title">Disable {confirmingDisable.label}?</h2>
|
|
131
|
+
<p className="confirm-modal-message">
|
|
132
|
+
All hosts linked to this device will no longer be able to use {confirmingDisable.label}.
|
|
133
|
+
</p>
|
|
134
|
+
<div className="confirm-modal-actions">
|
|
135
|
+
<button className="btn btn-secondary" onClick={() => setConfirmingDisable(null)}>Cancel</button>
|
|
136
|
+
<button className="btn btn-danger" onClick={confirmAndDisable}>Disable</button>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>,
|
|
140
|
+
document.body,
|
|
141
|
+
);
|
|
101
142
|
|
|
102
143
|
return (
|
|
103
144
|
<>
|
|
@@ -123,6 +164,7 @@ export default function CapabilityToggles({ onChange }: CapabilityTogglesProps)
|
|
|
123
164
|
})}
|
|
124
165
|
</div>
|
|
125
166
|
))}
|
|
167
|
+
{disableModal}
|
|
126
168
|
</>
|
|
127
169
|
);
|
|
128
170
|
}
|
|
@@ -57,7 +57,11 @@ export default function ConnectionStatusIcon() {
|
|
|
57
57
|
if (!containerRef.current?.contains(e.target as Node)) setPopoverOpen(false);
|
|
58
58
|
}
|
|
59
59
|
document.addEventListener("pointerdown", onPointerDown);
|
|
60
|
-
|
|
60
|
+
const timer = window.setTimeout(() => setPopoverOpen(false), 3000);
|
|
61
|
+
return () => {
|
|
62
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
};
|
|
61
65
|
}, [popoverOpen]);
|
|
62
66
|
|
|
63
67
|
if (mode === "direct") return null;
|
|
@@ -245,7 +245,7 @@ export default function HostMenu({ daemonVersion, linkedClientToken, request, on
|
|
|
245
245
|
className="btn btn-primary btn-full"
|
|
246
246
|
onClick={() => { if (!confirmLeaveDraft()) return; navigate("/pair"); if (!isDesktop) close(); }}
|
|
247
247
|
>
|
|
248
|
-
|
|
248
|
+
Add New Host
|
|
249
249
|
</button>
|
|
250
250
|
</div>
|
|
251
251
|
</>)}
|
|
@@ -256,11 +256,11 @@ export default function HostMenu({ daemonVersion, linkedClientToken, request, on
|
|
|
256
256
|
<div className="drawer-section">
|
|
257
257
|
<h3 className="drawer-section-label">Device Capabilities</h3>
|
|
258
258
|
{isLinkedDevice ? (
|
|
259
|
-
<CapabilityToggles onChange={onEnabledCapabilitiesChange} />
|
|
259
|
+
<CapabilityToggles onChange={onEnabledCapabilitiesChange} confirmDisable />
|
|
260
260
|
) : (
|
|
261
261
|
<>
|
|
262
262
|
<p className="drawer-section-hint">
|
|
263
|
-
This device isn't the linked device for
|
|
263
|
+
This device isn't the linked device for the host, so it can't provide capabilities (SMS, contacts, location, etc.).
|
|
264
264
|
</p>
|
|
265
265
|
<button
|
|
266
266
|
className="btn btn-secondary btn-full"
|
|
@@ -273,7 +273,7 @@ export default function HostMenu({ daemonVersion, linkedClientToken, request, on
|
|
|
273
273
|
}}
|
|
274
274
|
disabled={linkingBusy}
|
|
275
275
|
>
|
|
276
|
-
{linkingBusy ? "Linking…" : "Link this device"}
|
|
276
|
+
{linkingBusy ? "Linking…" : "Link the host to this device"}
|
|
277
277
|
</button>
|
|
278
278
|
</>
|
|
279
279
|
)}
|
|
@@ -300,11 +300,11 @@ export default function HostMenu({ daemonVersion, linkedClientToken, request, on
|
|
|
300
300
|
const deleteModal = confirmingDeleteId && createPortal(
|
|
301
301
|
<div className="confirm-modal-overlay" onClick={() => setConfirmingDeleteId(null)}>
|
|
302
302
|
<div className="confirm-modal" onClick={(e) => e.stopPropagation()}>
|
|
303
|
-
<h2 className="confirm-modal-title">
|
|
303
|
+
<h2 className="confirm-modal-title">Remove host?</h2>
|
|
304
304
|
<p className="confirm-modal-message">
|
|
305
|
-
"{pairedHosts.find((h) => h.hostId === confirmingDeleteId)?.name || confirmingDeleteId.slice(0, 8)}" will be
|
|
305
|
+
"{pairedHosts.find((h) => h.hostId === confirmingDeleteId)?.name || confirmingDeleteId.slice(0, 8)}" will be {deletingIsLinked ? "removed and unlinked" : "removed"}.
|
|
306
306
|
{deletingIsLinked && (
|
|
307
|
-
<> This device is currently linked to the host —
|
|
307
|
+
<> This device is currently linked to the host — unlinking will revoke its access to all device capabilities (SMS, contacts, location, etc.) until another device is linked.</>
|
|
308
308
|
)}
|
|
309
309
|
</p>
|
|
310
310
|
<div className="confirm-modal-actions">
|
|
@@ -318,7 +318,7 @@ export default function HostMenu({ daemonVersion, linkedClientToken, request, on
|
|
|
318
318
|
className="btn btn-danger"
|
|
319
319
|
onClick={() => handleDelete(confirmingDeleteId)}
|
|
320
320
|
>
|
|
321
|
-
|
|
321
|
+
Remove
|
|
322
322
|
</button>
|
|
323
323
|
</div>
|
|
324
324
|
</div>
|
|
@@ -329,9 +329,9 @@ export default function HostMenu({ daemonVersion, linkedClientToken, request, on
|
|
|
329
329
|
const linkModal = confirmingLink && createPortal(
|
|
330
330
|
<div className="confirm-modal-overlay" onClick={() => setConfirmingLink(false)}>
|
|
331
331
|
<div className="confirm-modal" onClick={(e) => e.stopPropagation()}>
|
|
332
|
-
<h2 className="confirm-modal-title">Link this device?</h2>
|
|
332
|
+
<h2 className="confirm-modal-title">Link the host to this device?</h2>
|
|
333
333
|
<p className="confirm-modal-message">
|
|
334
|
-
Only one device can be linked
|
|
334
|
+
Another device is already linked to this host. Only one device can be linked to the host — switching will disable those capabilities on the currently linked device.
|
|
335
335
|
</p>
|
|
336
336
|
<div className="confirm-modal-actions">
|
|
337
337
|
<button
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { RequiredPermission } from "../types";
|
|
2
2
|
|
|
3
|
-
interface
|
|
3
|
+
interface PermissionsDialogProps {
|
|
4
4
|
permissions?: RequiredPermission[];
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
export default function
|
|
7
|
+
export default function PermissionsDialog({ permissions }: PermissionsDialogProps) {
|
|
8
8
|
return (
|
|
9
|
-
<div className="
|
|
9
|
+
<div className="permissions-dialog">
|
|
10
10
|
<h2>Granted Permissions</h2>
|
|
11
|
-
<div className="
|
|
11
|
+
<div className="permissions-dialog-scroll">
|
|
12
12
|
{permissions && permissions.length > 0 ? (
|
|
13
13
|
<div className="permissions-section">
|
|
14
14
|
<ul className="permissions-list">
|
|
@@ -21,10 +21,10 @@ export default function PlanDialog({ permissions }: PlanDialogProps) {
|
|
|
21
21
|
</ul>
|
|
22
22
|
</div>
|
|
23
23
|
) : (
|
|
24
|
-
<p className="
|
|
24
|
+
<p className="permissions-empty">No permissions have been granted for this task.</p>
|
|
25
25
|
)}
|
|
26
26
|
</div>
|
|
27
|
-
<div className="
|
|
27
|
+
<div className="permissions-dialog-actions">
|
|
28
28
|
<button className="btn btn-secondary" onClick={() => history.back()}>
|
|
29
29
|
Back
|
|
30
30
|
</button>
|
|
@@ -5,6 +5,7 @@ import remarkGfm from "remark-gfm";
|
|
|
5
5
|
import remarkBreaks from "remark-breaks";
|
|
6
6
|
import { getAgentLabel } from "../agentLabels";
|
|
7
7
|
import { formatTime } from "../formatTime";
|
|
8
|
+
import { useBackClose } from "../hooks/useBackClose";
|
|
8
9
|
import type { ConversationMessage } from "../types";
|
|
9
10
|
|
|
10
11
|
interface RunDetailViewProps {
|
|
@@ -26,6 +27,7 @@ export default function RunDetailView({ connected, hostId, request, subscribeEve
|
|
|
26
27
|
const isFollowupRunning = runState === "followup";
|
|
27
28
|
const isAgentGenerating = runState === "started" || runState === "followup";
|
|
28
29
|
const [reportDialog, setReportDialog] = useState<{ file: string; content?: string; data_url?: string } | null>(null);
|
|
30
|
+
useBackClose(reportDialog !== null, () => setReportDialog(null));
|
|
29
31
|
const [aborting, setAborting] = useState(false);
|
|
30
32
|
const [followupText, setFollowupText] = useState("");
|
|
31
33
|
const [sendingFollowup, setSendingFollowup] = useState(false);
|
|
@@ -278,6 +278,7 @@ export default function SessionsView({ connected, hostId, request, subscribeEven
|
|
|
278
278
|
revealedId={revealedKey}
|
|
279
279
|
setRevealedId={setRevealedKey}
|
|
280
280
|
onDelete={() => deleteEntry(entry)}
|
|
281
|
+
confirmMessage="Delete this session? Its run history will be removed from the host."
|
|
281
282
|
onClick={() => !entry.error && handleCardClick(entry.task_id, entry.run_id)}
|
|
282
283
|
>
|
|
283
284
|
<div className="sessions-card">
|
|
@@ -11,9 +11,11 @@ interface SwipeToDeleteRowProps {
|
|
|
11
11
|
children: ReactNode;
|
|
12
12
|
/** Label for the action button (default "Delete"). */
|
|
13
13
|
actionLabel?: string;
|
|
14
|
+
/** Message shown in the native confirm() dialog (default "Delete this item? This can't be undone."). */
|
|
15
|
+
confirmMessage?: string;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
const REVEAL_WIDTH =
|
|
18
|
+
const REVEAL_WIDTH = 72; // px width of the action button
|
|
17
19
|
const OPEN_THRESHOLD = REVEAL_WIDTH / 2;
|
|
18
20
|
const AXIS_LOCK_THRESHOLD = 6; // px of horizontal travel before we claim the gesture
|
|
19
21
|
|
|
@@ -33,6 +35,7 @@ export default function SwipeToDeleteRow({
|
|
|
33
35
|
onClick,
|
|
34
36
|
children,
|
|
35
37
|
actionLabel = "Delete",
|
|
38
|
+
confirmMessage = "Delete this item? This can't be undone.",
|
|
36
39
|
}: SwipeToDeleteRowProps) {
|
|
37
40
|
const revealed = revealedId === id;
|
|
38
41
|
const [dragOffset, setDragOffset] = useState(0);
|
|
@@ -137,11 +140,18 @@ export default function SwipeToDeleteRow({
|
|
|
137
140
|
type="button"
|
|
138
141
|
className="swipe-row-action"
|
|
139
142
|
style={{ width: REVEAL_WIDTH }}
|
|
140
|
-
onClick={(e) => { e.stopPropagation(); onDelete(); }}
|
|
143
|
+
onClick={(e) => { e.stopPropagation(); if (window.confirm(confirmMessage)) onDelete(); }}
|
|
141
144
|
tabIndex={revealed ? 0 : -1}
|
|
142
145
|
aria-hidden={!revealed}
|
|
146
|
+
aria-label={actionLabel}
|
|
143
147
|
>
|
|
144
|
-
|
|
148
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
149
|
+
<polyline points="3 6 5 6 21 6" />
|
|
150
|
+
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
|
151
|
+
<path d="M10 11v6M14 11v6" />
|
|
152
|
+
<path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2" />
|
|
153
|
+
</svg>
|
|
154
|
+
<span className="swipe-row-action-label">{actionLabel}</span>
|
|
145
155
|
</button>
|
|
146
156
|
<div
|
|
147
157
|
className={`swipe-row-content ${dragging ? "swipe-row-content-dragging" : ""}`}
|
|
@@ -2,7 +2,7 @@ import { useEffect, useState, useCallback, useRef } from "react";
|
|
|
2
2
|
import { Capacitor } from "@capacitor/core";
|
|
3
3
|
import { useHostConnection } from "../contexts/HostConnectionContext";
|
|
4
4
|
import { useHostStore } from "../contexts/HostStoreContext";
|
|
5
|
-
import
|
|
5
|
+
import PermissionsDialog from "./PermissionsDialog";
|
|
6
6
|
import { useBackClose } from "../hooks/useBackClose";
|
|
7
7
|
import { Device } from "../native/Device";
|
|
8
8
|
import type { AgentInfo, Task } from "../types";
|
|
@@ -362,7 +362,7 @@ export default function TaskForm({ initial, agents, hostPlatform, isNotification
|
|
|
362
362
|
<div className="task-form-overlay">
|
|
363
363
|
<div className="task-form">
|
|
364
364
|
{planDialogOpen ? (
|
|
365
|
-
<
|
|
365
|
+
<PermissionsDialog permissions={initial?.permissions} />
|
|
366
366
|
) : (<>
|
|
367
367
|
<div className="task-form-header">
|
|
368
368
|
<h2>{initial ? "Edit Task" : "New Task"}</h2>
|