palmier 0.9.4 → 0.9.6
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 +4 -2
- package/dist/commands/init.js +2 -2
- package/dist/pwa/apple-touch-icon.png +0 -0
- package/dist/pwa/assets/index-D1bIhEbd.css +1 -0
- package/dist/pwa/assets/index-MLEFUP3r.js +120 -0
- package/dist/pwa/assets/{web-Eg0A6HEi.js → web-B1sKCc7e.js} +1 -1
- package/dist/pwa/assets/{web-Dcldtodb.js → web-B4xEa6WO.js} +1 -1
- package/dist/pwa/assets/{web-DdVpqhvX.js → web-ETD-8ZHd.js} +1 -1
- package/dist/pwa/favicon.ico +0 -0
- package/dist/pwa/index.html +3 -3
- package/dist/pwa/manifest.webmanifest +1 -1
- package/dist/pwa/pwa-192x192.png +0 -0
- package/dist/pwa/pwa-512x512.png +0 -0
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.js +15 -1
- package/dist/task.d.ts +1 -0
- package/dist/task.js +14 -0
- package/package.json +1 -1
- package/palmier-server/pwa/index.html +1 -1
- package/palmier-server/pwa/logo/logo_20260421.png +0 -0
- package/palmier-server/pwa/public/apple-touch-icon.png +0 -0
- package/palmier-server/pwa/public/favicon.ico +0 -0
- package/palmier-server/pwa/public/pwa-192x192.png +0 -0
- package/palmier-server/pwa/public/pwa-512x512.png +0 -0
- package/palmier-server/pwa/src/App.css +14 -0
- package/palmier-server/pwa/src/components/CapabilityToggles.tsx +1 -1
- package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +5 -6
- package/palmier-server/pwa/src/components/HostMenu.tsx +28 -1
- package/palmier-server/pwa/src/components/RunDetailView.tsx +3 -2
- package/palmier-server/pwa/src/components/SessionsView.tsx +2 -1
- package/palmier-server/pwa/src/components/TaskCard.tsx +14 -6
- package/palmier-server/pwa/src/components/TaskForm.tsx +12 -6
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +12 -0
- package/palmier-server/pwa/src/formatTime.ts +38 -4
- package/palmier-server/pwa/src/pages/Dashboard.tsx +4 -2
- package/palmier-server/pwa/src/types.ts +2 -0
- package/palmier-server/pwa/vite.config.ts +1 -1
- package/src/commands/init.ts +2 -2
- package/src/rpc-handler.ts +12 -1
- package/src/task.ts +13 -0
- package/dist/pwa/assets/index-BsB1tIsn.css +0 -1
- package/dist/pwa/assets/index-DX5qJgHZ.js +0 -120
- package/palmier-server/pwa/logo/logo-prompt.md +0 -28
- package/palmier-server/pwa/logo/logo_20260330.png +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
import{W as t}from"./index-
|
|
1
|
+
import{W as t}from"./index-MLEFUP3r.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};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{W as p}from"./index-
|
|
1
|
+
import{W as p}from"./index-MLEFUP3r.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-MLEFUP3r.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};
|
package/dist/pwa/favicon.ico
CHANGED
|
Binary file
|
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="Bridge your AI agents and your phone. Your AI agents use your phone as a tool — GPS, email, calendar, contacts — and you
|
|
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. Your AI agents use your phone as a tool — GPS, email, calendar, contacts — and you use your phone as an agent remote." />
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-MLEFUP3r.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-D1bIhEbd.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":"Bridge your AI agents and your phone. Your AI agents use your phone as a tool — GPS, email, calendar, contacts — and you
|
|
1
|
+
{"name":"Palmier","short_name":"Palmier","description":"Bridge your AI agents and your phone. Your AI agents use your phone as a tool — GPS, email, calendar, contacts — and you use your phone as an agent remote.","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"}]}
|
package/dist/pwa/pwa-192x192.png
CHANGED
|
Binary file
|
package/dist/pwa/pwa-512x512.png
CHANGED
|
Binary file
|
|
@@ -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":"c60052b7fa76e3b8737fdd47056f224e","url":"index.html"},{"revision":null,"url":"assets/web-ETD-8ZHd.js"},{"revision":null,"url":"assets/web-B4xEa6WO.js"},{"revision":null,"url":"assets/web-B1sKCc7e.js"},{"revision":null,"url":"assets/index-MLEFUP3r.js"},{"revision":null,"url":"assets/index-D1bIhEbd.css"},{"revision":"52153002d5b9308ba313d435ee6dedcd","url":"apple-touch-icon.png"},{"revision":"438da4214ab645a9c18a515c92cd9dfc","url":"favicon.ico"},{"revision":"7499c65c226796ebfc9674049a757362","url":"pwa-192x192.png"},{"revision":"e0cfcedd3448d33a3e75880e29be3d77","url":"pwa-512x512.png"},{"revision":"50cd09d8b1eba396c10f48577e4e8ae3","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/dist/rpc-handler.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
|
-
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, appendHistory, createRunDir, appendRunMessage, getRunDir, writeFollowupStatus, readFollowupStatus, deleteFollowupStatus } from "./task.js";
|
|
4
|
+
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, isTaskInList, appendHistory, createRunDir, appendRunMessage, getRunDir, writeFollowupStatus, readFollowupStatus, deleteFollowupStatus } from "./task.js";
|
|
5
5
|
import { resolvePending, getPending, listPending } from "./pending-requests.js";
|
|
6
6
|
import { getPlatform } from "./platform/index.js";
|
|
7
7
|
import { spawnCommand } from "./spawn-command.js";
|
|
@@ -127,6 +127,7 @@ export function createRpcHandler(config, nc) {
|
|
|
127
127
|
agents: config.agents ?? [],
|
|
128
128
|
version: currentVersion,
|
|
129
129
|
host_platform: process.platform,
|
|
130
|
+
host_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
130
131
|
linked_client_token: getLinkedDevice()?.clientToken ?? null,
|
|
131
132
|
pending_prompts: listPending(),
|
|
132
133
|
lan_url: buildLanUrl(config.httpPort ?? 7256, config.defaultInterface),
|
|
@@ -555,10 +556,23 @@ export function createRpcHandler(config, nc) {
|
|
|
555
556
|
if (!params.task_id || !params.run_id) {
|
|
556
557
|
return { error: "task_id and run_id are required" };
|
|
557
558
|
}
|
|
559
|
+
const deleteTaskDir = getTaskDir(config.projectRoot, params.task_id);
|
|
558
560
|
const deleted = deleteHistoryEntry(config.projectRoot, params.task_id, params.run_id);
|
|
559
561
|
if (!deleted) {
|
|
560
562
|
return { error: "History entry not found" };
|
|
561
563
|
}
|
|
564
|
+
const { total: remainingRuns } = readHistory(config.projectRoot, { task_id: params.task_id, limit: 1 });
|
|
565
|
+
if (remainingRuns === 0 && !isTaskInList(config.projectRoot, params.task_id)) {
|
|
566
|
+
try {
|
|
567
|
+
getPlatform().removeTaskTimer(params.task_id);
|
|
568
|
+
}
|
|
569
|
+
catch { /* best-effort */ }
|
|
570
|
+
clearTaskQueue(params.task_id);
|
|
571
|
+
try {
|
|
572
|
+
fs.rmSync(deleteTaskDir, { recursive: true, force: true });
|
|
573
|
+
}
|
|
574
|
+
catch { /* best-effort */ }
|
|
575
|
+
}
|
|
562
576
|
return { ok: true, task_id: params.task_id, run_id: params.run_id };
|
|
563
577
|
}
|
|
564
578
|
case "host.update": {
|
package/dist/task.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export declare function parseTaskFile(taskDir: string): ParsedTask;
|
|
|
3
3
|
export declare function parseTaskContent(content: string): ParsedTask;
|
|
4
4
|
export declare function writeTaskFile(taskDir: string, task: ParsedTask): void;
|
|
5
5
|
export declare function appendTaskList(projectRoot: string, taskId: string): void;
|
|
6
|
+
export declare function isTaskInList(projectRoot: string, taskId: string): boolean;
|
|
6
7
|
export declare function removeFromTaskList(projectRoot: string, taskId: string): boolean;
|
|
7
8
|
export declare function listTasks(projectRoot: string): ParsedTask[];
|
|
8
9
|
export declare function getTaskDir(projectRoot: string, taskId: string): string;
|
package/dist/task.js
CHANGED
|
@@ -35,6 +35,20 @@ export function appendTaskList(projectRoot, taskId) {
|
|
|
35
35
|
const listPath = path.join(projectRoot, "tasks.jsonl");
|
|
36
36
|
fs.appendFileSync(listPath, JSON.stringify({ task_id: taskId }) + "\n", "utf-8");
|
|
37
37
|
}
|
|
38
|
+
export function isTaskInList(projectRoot, taskId) {
|
|
39
|
+
const listPath = path.join(projectRoot, "tasks.jsonl");
|
|
40
|
+
if (!fs.existsSync(listPath))
|
|
41
|
+
return false;
|
|
42
|
+
const lines = fs.readFileSync(listPath, "utf-8").split("\n").filter(Boolean);
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
try {
|
|
45
|
+
if (JSON.parse(line).task_id === taskId)
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch { /* skip malformed */ }
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
38
52
|
export function removeFromTaskList(projectRoot, taskId) {
|
|
39
53
|
const listPath = path.join(projectRoot, "tasks.jsonl");
|
|
40
54
|
if (!fs.existsSync(listPath))
|
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="Bridge your AI agents and your phone. Your AI agents use your phone as a tool — GPS, email, calendar, contacts — and you
|
|
10
|
+
<meta name="description" content="Bridge your AI agents and your phone. Your AI agents use your phone as a tool — GPS, email, calendar, contacts — and you use your phone as an agent remote." />
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1174,6 +1174,14 @@ body {
|
|
|
1174
1174
|
margin: 0;
|
|
1175
1175
|
}
|
|
1176
1176
|
|
|
1177
|
+
.schedule-section-hint {
|
|
1178
|
+
font-weight: 400;
|
|
1179
|
+
text-transform: none;
|
|
1180
|
+
letter-spacing: 0;
|
|
1181
|
+
color: var(--color-text-secondary);
|
|
1182
|
+
margin-left: 6px;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1177
1185
|
.schedule-reactive {
|
|
1178
1186
|
display: flex;
|
|
1179
1187
|
flex-direction: column;
|
|
@@ -1895,6 +1903,12 @@ body {
|
|
|
1895
1903
|
color: var(--color-muted);
|
|
1896
1904
|
}
|
|
1897
1905
|
|
|
1906
|
+
.drawer-host-time {
|
|
1907
|
+
font-size: 0.75rem;
|
|
1908
|
+
color: var(--color-muted);
|
|
1909
|
+
margin-bottom: var(--space-xs);
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1898
1912
|
.drawer-legal {
|
|
1899
1913
|
font-size: 0.75rem;
|
|
1900
1914
|
color: var(--color-muted);
|
|
@@ -19,7 +19,7 @@ interface CapabilityDefinition {
|
|
|
19
19
|
const CAPABILITIES: CapabilityDefinition[] = [
|
|
20
20
|
{ capability: "sms-read", label: "Read SMS", group: "Messaging" },
|
|
21
21
|
{ capability: "sms-send", label: "Send SMS", group: "Messaging" },
|
|
22
|
-
{ capability: "send-email", label: "
|
|
22
|
+
{ capability: "send-email", label: "Prompt Email to Send", group: "Messaging" },
|
|
23
23
|
{ capability: "notifications", label: "Notifications from Other Apps", group: "Data" },
|
|
24
24
|
{ capability: "contacts", label: "Manage Contacts", group: "Data" },
|
|
25
25
|
{ capability: "calendar", label: "Manage Calendar", group: "Data" },
|
|
@@ -15,13 +15,12 @@ const SVG_PROPS = {
|
|
|
15
15
|
strokeLinejoin: "round" as const,
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
function
|
|
18
|
+
function LinkedDevicesIcon() {
|
|
19
19
|
return (
|
|
20
20
|
<svg {...SVG_PROPS} aria-hidden="true">
|
|
21
|
-
<
|
|
22
|
-
<
|
|
23
|
-
<
|
|
24
|
-
<line x1="12" y1="20" x2="12.01" y2="20" />
|
|
21
|
+
<rect x="3" y="6" width="5" height="12" rx="1" />
|
|
22
|
+
<rect x="16" y="6" width="5" height="12" rx="1" />
|
|
23
|
+
<line x1="8" y1="12" x2="16" y2="12" />
|
|
25
24
|
</svg>
|
|
26
25
|
);
|
|
27
26
|
}
|
|
@@ -71,7 +70,7 @@ export default function ConnectionStatusIcon() {
|
|
|
71
70
|
let modifier: string;
|
|
72
71
|
switch (mode) {
|
|
73
72
|
case "lan":
|
|
74
|
-
icon = <
|
|
73
|
+
icon = <LinkedDevicesIcon />;
|
|
75
74
|
label = "Connected via LAN";
|
|
76
75
|
modifier = "lan";
|
|
77
76
|
break;
|
|
@@ -34,6 +34,28 @@ export default function HostMenu({ daemonVersion, linkedClientToken, request, on
|
|
|
34
34
|
const isDesktop = useMediaQuery("(min-width: 768px)");
|
|
35
35
|
const [linkingBusy, setLinkingBusy] = useState(false);
|
|
36
36
|
|
|
37
|
+
const [now, setNow] = useState(() => Date.now());
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const tick = () => setNow(Date.now());
|
|
40
|
+
const msUntilNextMinute = 60_000 - (Date.now() % 60_000);
|
|
41
|
+
const first = setTimeout(() => {
|
|
42
|
+
tick();
|
|
43
|
+
const iv = setInterval(tick, 60_000);
|
|
44
|
+
(first as unknown as { _iv: ReturnType<typeof setInterval> })._iv = iv;
|
|
45
|
+
}, msUntilNextMinute);
|
|
46
|
+
return () => {
|
|
47
|
+
const iv = (first as unknown as { _iv?: ReturnType<typeof setInterval> })._iv;
|
|
48
|
+
if (iv) clearInterval(iv);
|
|
49
|
+
clearTimeout(first);
|
|
50
|
+
};
|
|
51
|
+
}, []);
|
|
52
|
+
const hostClock = activeHost.timezone
|
|
53
|
+
? new Date(now).toLocaleString(undefined, {
|
|
54
|
+
month: "short", day: "numeric", hour: "numeric", minute: "2-digit",
|
|
55
|
+
timeZone: activeHost.timezone,
|
|
56
|
+
})
|
|
57
|
+
: "";
|
|
58
|
+
|
|
37
59
|
async function makeThisLinkedDevice() {
|
|
38
60
|
if (!Device || !request || !activeClientToken) return;
|
|
39
61
|
setLinkingBusy(true);
|
|
@@ -282,9 +304,14 @@ export default function HostMenu({ daemonVersion, linkedClientToken, request, on
|
|
|
282
304
|
)}
|
|
283
305
|
|
|
284
306
|
<div className="drawer-footer">
|
|
307
|
+
{activeHost.timezone && (
|
|
308
|
+
<div className="drawer-host-time">
|
|
309
|
+
Host time: {hostClock} · {activeHost.timezone}
|
|
310
|
+
</div>
|
|
311
|
+
)}
|
|
285
312
|
{daemonVersion && (
|
|
286
313
|
<div className="drawer-version">
|
|
287
|
-
|
|
314
|
+
Host version: v{daemonVersion}
|
|
288
315
|
</div>
|
|
289
316
|
)}
|
|
290
317
|
<div className="drawer-legal">
|
|
@@ -4,7 +4,7 @@ import Markdown from "react-markdown";
|
|
|
4
4
|
import remarkGfm from "remark-gfm";
|
|
5
5
|
import remarkBreaks from "remark-breaks";
|
|
6
6
|
import { getAgentLabel } from "../agentLabels";
|
|
7
|
-
import {
|
|
7
|
+
import { useFormatTime } from "../formatTime";
|
|
8
8
|
import { useBackClose } from "../hooks/useBackClose";
|
|
9
9
|
import type { ConversationMessage } from "../types";
|
|
10
10
|
|
|
@@ -19,6 +19,7 @@ interface RunDetailViewProps {
|
|
|
19
19
|
|
|
20
20
|
export default function RunDetailView({ connected, hostId, request, subscribeEvents, taskId, runId }: RunDetailViewProps) {
|
|
21
21
|
const navigate = useNavigate();
|
|
22
|
+
const formatTime = useFormatTime();
|
|
22
23
|
const [loading, setLoading] = useState(true);
|
|
23
24
|
const [messages, setMessages] = useState<ConversationMessage[]>([]);
|
|
24
25
|
const [runState, setRunState] = useState<string | undefined>();
|
|
@@ -300,7 +301,7 @@ export default function RunDetailView({ connected, hostId, request, subscribeEve
|
|
|
300
301
|
ref={followupInputRef}
|
|
301
302
|
className="chat-input"
|
|
302
303
|
type="text"
|
|
303
|
-
placeholder="Follow
|
|
304
|
+
placeholder="Follow-up message"
|
|
304
305
|
value={followupText}
|
|
305
306
|
onChange={(e) => setFollowupText(e.target.value)}
|
|
306
307
|
disabled={sendingFollowup}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useRef } from "react";
|
|
2
2
|
import { useNavigate } from "react-router-dom";
|
|
3
|
-
import {
|
|
3
|
+
import { useFormatTime } from "../formatTime";
|
|
4
4
|
import { confirmLeaveDraft } from "../draftGuard";
|
|
5
5
|
import SessionComposer from "./SessionComposer";
|
|
6
6
|
import PullToRefreshIndicator from "./PullToRefreshIndicator";
|
|
@@ -22,6 +22,7 @@ interface SessionsViewProps {
|
|
|
22
22
|
const PAGE_SIZE = 10;
|
|
23
23
|
|
|
24
24
|
export default function SessionsView({ connected, hostId, request, subscribeEvents, agents, hostPlatform, filterTaskId, onClearFilter }: SessionsViewProps) {
|
|
25
|
+
const formatTime = useFormatTime();
|
|
25
26
|
const [entries, setEntries] = useState<HistoryEntry[]>([]);
|
|
26
27
|
const [total, setTotal] = useState(0);
|
|
27
28
|
const [loading, setLoading] = useState(false);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState, useRef, useEffect } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
import { useHostConnection } from "../contexts/HostConnectionContext";
|
|
4
|
-
import {
|
|
4
|
+
import { useFormatTime } from "../formatTime";
|
|
5
5
|
import { getAgentLabel } from "../agentLabels";
|
|
6
6
|
import type { Task, TaskStatus } from "../types";
|
|
7
7
|
|
|
@@ -16,7 +16,9 @@ interface TaskCardProps {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export default function TaskCard({ task, lastEvent, onEdit, onDelete, onViewRun }: TaskCardProps) {
|
|
19
|
-
const { request } = useHostConnection();
|
|
19
|
+
const { request, activeHost } = useHostConnection();
|
|
20
|
+
const formatTime = useFormatTime();
|
|
21
|
+
const timeZone = activeHost.timezone;
|
|
20
22
|
const [aborting, setAborting] = useState(false);
|
|
21
23
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
22
24
|
const [sheetOpen, setSheetOpen] = useState(false);
|
|
@@ -105,7 +107,9 @@ export default function TaskCard({ task, lastEvent, onEdit, onDelete, onViewRun
|
|
|
105
107
|
function classifyValue(scheduleType: "crons" | "specific_times", value: string): { kind: string; detail: string } {
|
|
106
108
|
if (scheduleType === "specific_times") {
|
|
107
109
|
const d = new Date(value);
|
|
108
|
-
const label = isNaN(d.getTime())
|
|
110
|
+
const label = isNaN(d.getTime())
|
|
111
|
+
? value
|
|
112
|
+
: `${d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric", timeZone })} at ${d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit", timeZone })}`;
|
|
109
113
|
return { kind: "specific_times", detail: label };
|
|
110
114
|
}
|
|
111
115
|
const parts = value.split(" ");
|
|
@@ -113,9 +117,13 @@ export default function TaskCard({ task, lastEvent, onEdit, onDelete, onViewRun
|
|
|
113
117
|
const [min, hour, dom, , dow] = parts;
|
|
114
118
|
const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
115
119
|
if (hour === "*") return { kind: "hourly", detail: "" };
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
120
|
+
// Cron HH:MM is in the host's wall clock. Format directly — no Date
|
|
121
|
+
// conversion — to avoid implicit shifting through the viewer's local zone.
|
|
122
|
+
const h = Number(hour);
|
|
123
|
+
const m = Number(min);
|
|
124
|
+
const hour12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
|
|
125
|
+
const ampm = h >= 12 ? "PM" : "AM";
|
|
126
|
+
const time = `${hour12}:${String(m).padStart(2, "0")} ${ampm}`;
|
|
119
127
|
if (dow !== "*") return { kind: "weekly", detail: `${DAYS[Number(dow)] ?? dow} at ${time}` };
|
|
120
128
|
if (dom !== "*") return { kind: "monthly", detail: `day ${dom} at ${time}` };
|
|
121
129
|
return { kind: "daily", detail: time };
|
|
@@ -2,6 +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 { hostNowParts } from "../formatTime";
|
|
5
6
|
import PermissionsDialog from "./PermissionsDialog";
|
|
6
7
|
import { useBackClose } from "../hooks/useBackClose";
|
|
7
8
|
import { Device } from "../native/Device";
|
|
@@ -234,8 +235,13 @@ export default function TaskForm({ initial, agents, hostPlatform, isNotification
|
|
|
234
235
|
|| (scheduleMode === "on_new_notification" && notificationApp.trim() !== initialNotificationApp.trim())
|
|
235
236
|
|| (scheduleMode === "on_new_sms" && smsSender.trim() !== initialSmsSender.trim());
|
|
236
237
|
|
|
238
|
+
const hostNow = hostNowParts(activeHost.timezone);
|
|
237
239
|
const hasInvalidTrigger = modeIsScheduled && triggerRows.some((r) =>
|
|
238
|
-
r.schedule === "specific_times" && (
|
|
240
|
+
r.schedule === "specific_times" && (
|
|
241
|
+
!r.onceDate
|
|
242
|
+
|| r.onceDate < hostNow.date
|
|
243
|
+
|| (r.onceDate === hostNow.date && (r.onceTime ?? "") <= hostNow.time)
|
|
244
|
+
)
|
|
239
245
|
);
|
|
240
246
|
const canSave = isDirty
|
|
241
247
|
&& !!userPrompt.trim()
|
|
@@ -425,7 +431,9 @@ export default function TaskForm({ initial, agents, hostPlatform, isNotification
|
|
|
425
431
|
|
|
426
432
|
<div className="toggles-group">
|
|
427
433
|
<div className="schedule-section">
|
|
428
|
-
<h3 className="schedule-section-title">
|
|
434
|
+
<h3 className="schedule-section-title">
|
|
435
|
+
Schedule <span className="schedule-section-hint">based on host time</span>
|
|
436
|
+
</h3>
|
|
429
437
|
<select
|
|
430
438
|
className="form-select"
|
|
431
439
|
value={scheduleMode}
|
|
@@ -594,16 +602,14 @@ export default function TaskForm({ initial, agents, hostPlatform, isNotification
|
|
|
594
602
|
className="form-input"
|
|
595
603
|
type="date"
|
|
596
604
|
value={row.onceDate}
|
|
597
|
-
min={
|
|
605
|
+
min={hostNow.date}
|
|
598
606
|
onChange={(e) => updateRow(i, { onceDate: e.target.value })}
|
|
599
607
|
/>
|
|
600
608
|
<input
|
|
601
609
|
className="form-input"
|
|
602
610
|
type="time"
|
|
603
611
|
value={row.onceTime}
|
|
604
|
-
min={row.onceDate ===
|
|
605
|
-
? new Date().toTimeString().slice(0, 5)
|
|
606
|
-
: undefined}
|
|
612
|
+
min={row.onceDate === hostNow.date ? hostNow.time : undefined}
|
|
607
613
|
onChange={(e) => updateRow(i, { onceTime: e.target.value })}
|
|
608
614
|
/>
|
|
609
615
|
</div>
|
|
@@ -15,6 +15,7 @@ interface HostStoreContextValue {
|
|
|
15
15
|
renamePairedHost(hostId: string, name: string): void;
|
|
16
16
|
setHostLanUrl(hostId: string, lanUrl: string | undefined): void;
|
|
17
17
|
setHostLastAgent(hostId: string, agent: string): void;
|
|
18
|
+
setHostTimezone(hostId: string, timezone: string | undefined): void;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
const HostStoreContext = createContext<HostStoreContextValue | null>(null);
|
|
@@ -91,6 +92,16 @@ export function HostStoreProvider({ children }: { children: ReactNode }) {
|
|
|
91
92
|
);
|
|
92
93
|
}, []);
|
|
93
94
|
|
|
95
|
+
const setHostTimezone = useCallback((hostId: string, timezone: string | undefined) => {
|
|
96
|
+
setPairedHosts((prev) =>
|
|
97
|
+
prev.map((h) => {
|
|
98
|
+
if (h.hostId !== hostId) return h;
|
|
99
|
+
if (h.timezone === timezone) return h;
|
|
100
|
+
return { ...h, timezone };
|
|
101
|
+
})
|
|
102
|
+
);
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
94
105
|
return (
|
|
95
106
|
<HostStoreContext.Provider value={{
|
|
96
107
|
pairedHosts,
|
|
@@ -99,6 +110,7 @@ export function HostStoreProvider({ children }: { children: ReactNode }) {
|
|
|
99
110
|
renamePairedHost,
|
|
100
111
|
setHostLanUrl,
|
|
101
112
|
setHostLastAgent,
|
|
113
|
+
setHostTimezone,
|
|
102
114
|
}}>
|
|
103
115
|
{children}
|
|
104
116
|
</HostStoreContext.Provider>
|
|
@@ -1,10 +1,44 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useHostConnection } from "./contexts/HostConnectionContext";
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Format a timestamp for display. Shows time only if today, otherwise includes the date.
|
|
6
|
+
* If `timeZone` is provided, renders the wall-clock time in that IANA zone.
|
|
3
7
|
*/
|
|
4
|
-
export function formatTime(ms: number): string {
|
|
8
|
+
export function formatTime(ms: number, timeZone?: string): string {
|
|
5
9
|
const d = new Date(ms);
|
|
6
10
|
const now = new Date();
|
|
7
|
-
const time = d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
const time = d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit", timeZone });
|
|
12
|
+
// toDateString() would compare in the viewer's local zone, masking day
|
|
13
|
+
// boundaries in the host's zone. Format both dates in the target zone and
|
|
14
|
+
// compare the strings instead.
|
|
15
|
+
const dayOpts: Intl.DateTimeFormatOptions = { year: "numeric", month: "2-digit", day: "2-digit", timeZone };
|
|
16
|
+
const sameDay = d.toLocaleDateString(undefined, dayOpts) === now.toLocaleDateString(undefined, dayOpts);
|
|
17
|
+
if (sameDay) return time;
|
|
18
|
+
return `${d.toLocaleDateString(undefined, { month: "short", day: "numeric", timeZone })} ${time}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Returns a `formatTime` bound to the active host's timezone. */
|
|
22
|
+
export function useFormatTime(): (ms: number) => string {
|
|
23
|
+
const { activeHost } = useHostConnection();
|
|
24
|
+
const tz = activeHost.timezone;
|
|
25
|
+
return useCallback((ms: number) => formatTime(ms, tz), [tz]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Current wall-clock in the host timezone, as ISO-shaped date + HH:MM strings.
|
|
30
|
+
* Used for "not in the past" validation on scheduled triggers so the floor is
|
|
31
|
+
* the host's now, not the viewer's now.
|
|
32
|
+
*/
|
|
33
|
+
export function hostNowParts(timeZone?: string): { date: string; time: string } {
|
|
34
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
35
|
+
timeZone,
|
|
36
|
+
year: "numeric", month: "2-digit", day: "2-digit",
|
|
37
|
+
hour: "2-digit", minute: "2-digit", hour12: false,
|
|
38
|
+
}).formatToParts(new Date());
|
|
39
|
+
const pick = (t: string) => parts.find((p) => p.type === t)?.value ?? "";
|
|
40
|
+
return {
|
|
41
|
+
date: `${pick("year")}-${pick("month")}-${pick("day")}`,
|
|
42
|
+
time: `${pick("hour")}:${pick("minute")}`,
|
|
43
|
+
};
|
|
10
44
|
}
|
|
@@ -46,7 +46,7 @@ interface PermissionPrompt { permissions: RequiredPermission[]; sessionName?: st
|
|
|
46
46
|
interface InputPrompt { questions: string[]; description?: string; sessionName?: string }
|
|
47
47
|
|
|
48
48
|
export default function Dashboard() {
|
|
49
|
-
const { removePairedHost, setHostLanUrl } = useHostStore();
|
|
49
|
+
const { removePairedHost, setHostLanUrl, setHostTimezone } = useHostStore();
|
|
50
50
|
const { connected, request, subscribeEvents, unauthorized, activeHost } = useHostConnection();
|
|
51
51
|
const hostId = activeHost.hostId;
|
|
52
52
|
const activeClientToken = activeHost.clientToken || null;
|
|
@@ -100,6 +100,7 @@ export default function Dashboard() {
|
|
|
100
100
|
agents?: AgentInfo[];
|
|
101
101
|
version?: string | null;
|
|
102
102
|
host_platform?: string;
|
|
103
|
+
host_timezone?: string;
|
|
103
104
|
linked_client_token?: string | null;
|
|
104
105
|
pending_prompts?: PendingPrompt[];
|
|
105
106
|
lan_url?: string | null;
|
|
@@ -113,6 +114,7 @@ export default function Dashboard() {
|
|
|
113
114
|
setDaemonVersion(version);
|
|
114
115
|
setUpdateRequired(!!version && isOlderThan(version, MIN_HOST_VERSION));
|
|
115
116
|
setHostLanUrl(hostId, result.lan_url ?? undefined);
|
|
117
|
+
setHostTimezone(hostId, result.host_timezone);
|
|
116
118
|
|
|
117
119
|
// Seed modal state from already-pending prompts.
|
|
118
120
|
const confirms = new Map<string, ConfirmPrompt>();
|
|
@@ -146,7 +148,7 @@ export default function Dashboard() {
|
|
|
146
148
|
setInputValues(inputVals);
|
|
147
149
|
})
|
|
148
150
|
.catch(() => { /* silent — update-required prompt guards the broken case */ });
|
|
149
|
-
}, [connected, hostId, request, setHostLanUrl]);
|
|
151
|
+
}, [connected, hostId, request, setHostLanUrl, setHostTimezone]);
|
|
150
152
|
|
|
151
153
|
// Always-on event subscription for modal lifecycle. Independent of which tab
|
|
152
154
|
// is active. Task-card status updates happen inside TasksView while mounted.
|
|
@@ -70,4 +70,6 @@ export interface PairedHost {
|
|
|
70
70
|
lanUrl?: string;
|
|
71
71
|
/** Last-used agent key for this host. Seeds the agent picker on the session composer and task form. */
|
|
72
72
|
lastAgent?: string;
|
|
73
|
+
/** IANA timezone from the host, refreshed on each `host.info`. Drives all time rendering. */
|
|
74
|
+
timezone?: string;
|
|
73
75
|
}
|
|
@@ -24,7 +24,7 @@ export default defineConfig({
|
|
|
24
24
|
manifest: {
|
|
25
25
|
name: "Palmier",
|
|
26
26
|
short_name: "Palmier",
|
|
27
|
-
description: "Bridge your AI agents and your phone. Your AI agents use your phone as a tool — GPS, email, calendar, contacts — and you
|
|
27
|
+
description: "Bridge your AI agents and your phone. Your AI agents use your phone as a tool — GPS, email, calendar, contacts — and you use your phone as an agent remote.",
|
|
28
28
|
start_url: "/",
|
|
29
29
|
display: "standalone",
|
|
30
30
|
background_color: "#ffffff",
|