@timqi/pier 0.2.0 → 0.3.0
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/dist/agent/listing.js +14 -10
- package/dist/agent/pi.js +2 -2
- package/dist/agent/roles.js +4 -3
- package/dist/channels/conversations.js +4 -4
- package/dist/channels/lark-outbound.js +5 -0
- package/dist/channels/lark.js +25 -5
- package/dist/channels/runtime.js +47 -2
- package/dist/channels/slack-outbound.js +7 -0
- package/dist/channels/slack-thread.js +65 -0
- package/dist/channels/slack.js +27 -72
- package/dist/cli.js +7 -2
- package/dist/core/chain.js +23 -4
- package/dist/core/identity.js +2 -2
- package/dist/core/inbound-file.js +9 -3
- package/dist/core/reply.js +17 -3
- package/dist/core/search.js +65 -0
- package/dist/core/types.js +2 -0
- package/dist/db.js +8 -0
- package/dist/drain.js +1 -1
- package/dist/main.js +21 -5
- package/dist/settings.js +1 -1
- package/dist/socket.js +1 -0
- package/dist/tasks/execution.js +9 -3
- package/dist/tasks/open-items.js +53 -35
- package/dist/tasks/service.js +22 -1
- package/dist/tasks/store.js +5 -2
- package/dist/tasks/types.js +0 -2
- package/dist/tools.js +10 -3
- package/dist/web/public/assets/{code-BZwvPani.js → code-CWT27KH1.js} +1 -1
- package/dist/web/public/assets/code-CWT27KH1.js.br +0 -0
- package/dist/web/public/assets/code-CWT27KH1.js.gz +0 -0
- package/dist/web/public/assets/{explorer-DDoVzMLQ.js → explorer-C4DhXFCm.js} +4 -4
- package/dist/web/public/assets/explorer-C4DhXFCm.js.br +0 -0
- package/dist/web/public/assets/explorer-C4DhXFCm.js.gz +0 -0
- package/dist/web/public/assets/highlight-CDSk2hRJ.js +72 -0
- package/dist/web/public/assets/highlight-CDSk2hRJ.js.br +0 -0
- package/dist/web/public/assets/highlight-CDSk2hRJ.js.gz +0 -0
- package/dist/web/public/assets/index-D0ZYdweB.js +14 -0
- package/dist/web/public/assets/index-D0ZYdweB.js.br +0 -0
- package/dist/web/public/assets/index-D0ZYdweB.js.gz +0 -0
- package/dist/web/public/assets/index-JN0Ic8K5.css +2 -0
- package/dist/web/public/assets/index-JN0Ic8K5.css.br +0 -0
- package/dist/web/public/assets/index-JN0Ic8K5.css.gz +0 -0
- package/dist/web/public/assets/settings-DSTeuIyc.js +9 -0
- package/dist/web/public/assets/settings-DSTeuIyc.js.br +0 -0
- package/dist/web/public/assets/settings-DSTeuIyc.js.gz +0 -0
- package/dist/web/public/index.html +3 -4
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/push.js +2 -2
- package/dist/web/server.js +2 -4
- package/dist/web/session-state.js +1 -2
- package/package.json +1 -1
- package/skills/pier-help/SKILL.md +6 -1
- package/skills/pier-search/SKILL.md +27 -0
- package/skills/pier-tasks/SKILL.md +10 -0
- package/dist/web/public/assets/code-BZwvPani.js.br +0 -0
- package/dist/web/public/assets/code-BZwvPani.js.gz +0 -0
- package/dist/web/public/assets/explorer-DDoVzMLQ.js.br +0 -0
- package/dist/web/public/assets/explorer-DDoVzMLQ.js.gz +0 -0
- package/dist/web/public/assets/highlight-5Bl8zZ8q.js +0 -72
- package/dist/web/public/assets/highlight-5Bl8zZ8q.js.br +0 -0
- package/dist/web/public/assets/highlight-5Bl8zZ8q.js.gz +0 -0
- package/dist/web/public/assets/index-BpFzCwlo.js +0 -7
- package/dist/web/public/assets/index-BpFzCwlo.js.br +0 -0
- package/dist/web/public/assets/index-BpFzCwlo.js.gz +0 -0
- package/dist/web/public/assets/index-wAGxFFPj.css +0 -2
- package/dist/web/public/assets/index-wAGxFFPj.css.br +0 -0
- package/dist/web/public/assets/index-wAGxFFPj.css.gz +0 -0
- package/dist/web/public/assets/report-zjE8Bt8S.js +0 -8
- package/dist/web/public/assets/report-zjE8Bt8S.js.br +0 -0
- package/dist/web/public/assets/report-zjE8Bt8S.js.gz +0 -0
- package/dist/web/public/assets/settings-Cv7rbeqf.js +0 -9
- package/dist/web/public/assets/settings-Cv7rbeqf.js.br +0 -0
- package/dist/web/public/assets/settings-Cv7rbeqf.js.gz +0 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import{A as e,At as t,B as n,C as r,D as i,Dt as a,E as o,Et as s,F as c,G as l,H as u,I as d,J as f,K as p,L as m,M as h,N as g,O as _,Ot as v,P as y,Q as b,R as x,S,T as C,U as w,Z as T,_ as E,_t as D,a as O,at as k,b as A,c as ee,d as te,f as j,g as M,h as N,it as P,j as F,jt as I,k as L,m as R,mt as z,n as B,nt as V,ot as H,p as ne,tt as U,v as W,vt as G,w as re,wt as K,x as q,y as J,yt as Y,z as ie}from"./highlight-CDSk2hRJ.js";import{i as ae,n as X,o as oe,r as se,t as Z}from"./index-D0ZYdweB.js";import{n as ce,t as le}from"./code-CWT27KH1.js";var ue=[[`path`,{d:`m18 15-6-6-6 6`}]],de=[[`path`,{d:`M15 3h6v6`}],[`path`,{d:`M10 14 21 3`}],[`path`,{d:`M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6`}]],fe=[[`path`,{d:`m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3`}],[`path`,{d:`M12 9v4`}],[`path`,{d:`M12 17h.01`}]],Q=e=>e.public&&e.token?`/p/${e.slug}-${e.token}/`:`/boards/${e.slug}/`;function $(){let e=f(`div`,`flex flex-col gap-2`),t=f(`span`,`text-[11.5px]`,``);async function n(e,n){let r=await H(`/api/boards/${e.slug}`,{public:n},`PATCH`);r.ok?y(t,`saved`,`${e.slug} is ${n?`public`:`private`}.`):y(t,`failed`,await U(r,`Could not change ${e.slug}`)),await o()}async function i(e){let n=await k(`/api/boards/${e.slug}`,`DELETE`,`Could not delete ${e.slug}`);n?y(t,`failed`,n):y(t,`saved`,`Deleted ${e.slug} — the folder is kept as ${e.slug}.deleted-<time>.`),await o()}function a(e){let t=f(`a`,`min-w-0 truncate text-[12.5px] font-medium text-neutral-700 hover:text-indigo-700`,e.title);t.href=Q(e),t.target=`_blank`,t.rel=`noreferrer`;let r=[e.slug,`updated ${T(Date.parse(e.updatedAt))}`];e.description&&r.push(e.description);let a=f(`div`,`flex min-w-0 flex-col gap-1`,t,f(`span`,`truncate text-[11.5px] text-neutral-400`,r.join(` · `))),o=x(``,``,e.public,t=>void n(e,t));o.title=`Public: anyone holding the /p/ link can read it, no password`,o.append(f(`span`,`text-[11.5px] text-neutral-500`,`Public`));let s=p(F(`hover:text-indigo-600`),()=>`${location.origin}${Q(e)}`);s.title=`Copy the board's link`;let c=q(`Delete`,F());return c.title=`Renames the folder on disk; nothing is erased`,c.onclick=()=>void i(e),f(`div`,`group flex items-center justify-between gap-3 rounded-lg border border-neutral-200 px-3 py-2`,a,f(`div`,`flex flex-none items-center gap-3`,o,s,c))}async function o(){let t=await V(`/api/boards`,`Could not load boards`);if(!t.ok)return void e.replaceChildren(C(t.error));e.replaceChildren(...t.value.length?t.value.map(a):[C(`No boards yet. Ask an agent to build one — it writes a folder under ~/.pier/boards.`)])}return{el:f(`div`,`mx-auto flex w-full min-w-0 max-w-3xl flex-col`,r(`Boards`,`Static pages agents wrote, freshest first. A private board opens on a link good for 8 hours; a public one on /p/<slug>-<token>/ for anyone holding it.`,e,t)),show:()=>void o()}}var pe={display_information:{name:`Pier`,description:`Coding agent sessions in Slack threads`,background_color:`#262626`},features:{bot_user:{display_name:`Pier`,always_online:!1},app_home:{messages_tab_enabled:!0,messages_tab_read_only_enabled:!1}},oauth_config:{scopes:{bot:[`chat:write`,`reactions:write`,`channels:history`,`channels:read`,`groups:history`,`groups:read`,`im:history`,`im:write`,`mpim:history`,`mpim:read`,`mpim:write`,`users:read`,`files:read`,`files:write`]}},settings:{event_subscriptions:{bot_events:[`message.channels`,`message.groups`,`message.im`,`message.mpim`]},interactivity:{is_enabled:!0},socket_mode_enabled:!0,token_rotation_enabled:!1}},me=()=>`https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(JSON.stringify(pe))}`;function he(){let e=S(`Create Slack app`,!0);e.append(K(de)),e.classList.add(`inline-flex`,`items-center`,`gap-1.5`),e.classList.add(`mt-1`,`w-fit`),e.onclick=()=>window.open(me(),`_blank`,`noreferrer`);let t=p(`btn w-fit text-[12.5px]`,()=>JSON.stringify(pe,null,2));return i(`Creating a Slack app`,[ie(`Opens Slack with every scope, event and Socket Mode already filled in. Pick your workspace → Create.`,e),"`Install to Workspace` → allow. Then `OAuth & Permissions` → copy the bot token (`xoxb-…`) into the field above.","`Basic Information` → `App-Level Tokens` → `Generate` with scope `connections:write`. Paste that `xapp-…` token below. The manifest cannot mint this one for you.","In Slack, invite the bot where you want it: `/invite @Pier`. It answers in threads only, so it never adds noise to the channel.",ie(`Prefer to paste it yourself? Create the app from a manifest and use this:`,t)])}var ge=()=>i(`How Slack threads work here`,[`Pier never posts in a channel's main flow. A message in the channel is answered in *its own thread*; a message in a thread is answered in that thread.`,`Each thread is its own Pi session, so one channel hosts many parallel sessions.`,`The same applies in a DM: every new message you send there starts its own thread and its own session. Reply *inside* a thread to continue that conversation.`,"Inside a thread Pier already owns, no `@mention` is needed — continuing that thread is addressing it.","Commands start with `%`: `%settings`, `%stop`, `%bind <code>`, since Slack's client swallows an unregistered `/command` before it ever reaches an app. `%s <question>`, as the first message of a thread, drafts a session — directory, model, reasoning — and Start runs the question in it."]),_e=()=>i(`Creating a Feishu app`,["On [open.feishu.cn](https://open.feishu.cn/app) create a 企业自建应用 (custom app). `Credentials & Basic Info` shows the App ID (`cli_…`) and App Secret — paste both here.","`Add Features` → enable **Bot** (机器人).","`Permissions` (权限管理), add: `im:message` (read messages), `im:message:send_as_bot` (send), `im:resource` (attachments, both directions), `im:message.reactions:create` + `:read` + `:delete` (the 👀 receipt), `im:chat:readonly` (chat names for this page), `contact:user.base:readonly` (speaker names).","`Events & Callbacks` → set the mode to **长连接 (long connection)** — no public URL needed. Under Events subscribe `im.message.receive_v1` (接收消息); Callbacks gain `card.action.trigger` automatically once cards are sent, add it if listed.",`**Publish a version** (版本管理与发布 → 创建版本 → 申请发布). Nothing above takes effect until this is approved — the usual reason a configured bot stays silent.`,`Add the bot to a group (群设置 → 群机器人), or DM it directly.`]),ve=()=>i(`How Lark topics work here`,[`Pier never posts in a chat's main flow. A message in the chat is answered in *its own topic* (话题); a message inside a topic is answered there.`,`Each topic is its own Pi session, so one group hosts many parallel sessions.`,`The same applies in a DM: every new message starts its own topic and its own session. Reply *inside* a topic to continue that conversation.`,`Inside a topic Pier already owns, no @mention is needed — continuing that topic is addressing it.`,"Commands start with `/` or `%`: `/stop`, `/settings`, `/bind <code>`. `/s <question>`, outside any topic, drafts a session — directory, model, reasoning — and Start runs the question in it."]),ye=[[`slack`,`Slack`],[`lark`,`Lark`]],be={dm:`bg-sky-50 text-sky-700 ring-sky-200`,group:`bg-neutral-100 text-neutral-600 ring-neutral-200`},xe=`Discovered from inbound traffic — no platform reliably lists every chat a bot is in.`,Se=`pier.channelsPlatform`;function Ce(e){let t=localStorage.getItem(Se),n=ye.some(([e])=>e===t)?t:`slack`,i=null,a=[],s=f(`div`,`ml-auto flex items-center gap-1.5 text-[11.5px]`),c=f(`div`,`pagehead pagehead-hug sticky top-[-8px] z-30`,s),u=f(`div`,`px-4 pb-5 pt-4`);e.append(f(`div`,`min-h-0 flex-1 overflow-y-auto`,c,u));function p(){c.replaceChildren(h(ye.map(([e,t])=>[t,e]),n,e=>{m&&T(),n=e,localStorage.setItem(Se,e),E()}),s)}let m,g=!1,_=null;function v(e){if(s.replaceChildren(),e!==`clean`){if(s.className=`ml-auto flex items-center gap-1.5 text-[11.5px] ${J[e]}`,e===`saving`)s.append(K(oe,`spinner`),f(`span`,``,`Saving…`));else if(e===`saved`)s.append(K(I),f(`span`,``,`Saved`)),setTimeout(()=>{s.textContent?.includes(`Saved`)&&v(`clean`)},2e3);else{let e=q(`Retry`,`cursor-pointer underline`);e.onclick=T,s.append(K(fe),f(`span`,``,`Save failed`),e)}}}function y(){v(`saving`),clearTimeout(m),m=setTimeout(T,500)}let b=e=>t=>{e(t),y()};async function w(e){g=!0,v(`saving`);try{v((await H(`/api/channels/${e.platform}`,e.config,`PUT`)).ok?`saved`:`failed`)}catch{v(`failed`)}finally{g=!1;let e=_;_=null,e&&w(e)}}function T(){if(clearTimeout(m),m=void 0,!i)return;let e={platform:n,config:i};g?_=e:w(e)}async function E(){if(!a.length){let e=await V(`/api/models`,`Could not load the model catalog`);e.ok&&(a=e.value)}let e=await V(`/api/channels/${n}`,`Failed to load`);if(!e.ok){i=null,p(),u.replaceChildren(C(e.error));return}i=e.value,v(`clean`),M()}function D(e){let t=n===`lark`,i=d(e.token,t?`cli_…`:`xoxb-…`,b(t=>e.token=t),!0),a=d(e.appToken,t?``:`xapp-…`,b(t=>e.appToken=t),!0),s=O(e.cwd,`(pier process cwd)`,b(t=>e.cwd=t));return r(`Connection`,t?`Pier connects over Feishu's WebSocket long connection; no public URL or webhook needed.`:`Pier connects over Socket Mode; no public URL or webhook needed.`,x(`Enabled`,`Start the adapter when Pier boots.`,e.enabled,b(t=>{e.enabled=t,p()})),o(t?`App ID`:`Bot token`,i,{hint:`Stored locally, shown masked once saved.`,help:t?_e():he()}),t?o(`App Secret`,a,{hint:`From Credentials & Basic Info, beside the App ID.`}):o(`App-level token`,a,{hint:`Opens the Socket Mode connection. Needs connections:write.`}),o(`Default working directory`,s.el,{hint:`Where sessions this channel opens start.`}),f(`span`,`flex items-center gap-1.5 text-[13px] text-neutral-400`,f(`span`,``,t?`Every topic is its own session.`:`Every thread is its own session.`),t?ve():ge()))}function k(e){return r(`Defaults for new chats`,`Copied into a group the first time the bot sees it; changing them here never touches a group that already exists. DMs are always bound-users-only.`,x(`Require mention in groups`,`Ignore group messages that do not @mention or reply to the bot.`,e.requireMention,b(t=>e.requireMention=t)),x(`Require bound user`,`Only users bound with a code below can drive the agent.`,e.requireBind,b(t=>e.requireBind=t)),Z(`Model & reasoning`,e,a,b(t=>{e.model=t.model,e.thinking=t.thinking,M()})))}function ee(e){let t=f(`span`,`font-mono text-[15px] font-semibold tracking-widest`),i=f(`div`,`hidden items-center gap-2 rounded-lg bg-indigo-50 px-3 py-2 text-[12.5px] text-indigo-800`,t,f(`span`,`text-[11.5px] text-indigo-500`,`DM the bot /bind <code> · expires in 10 min`)),a=S(`Generate bind code`);a.onclick=async()=>{let e=await fetch(`/api/channels/${n}/bind-code`,{method:`POST`});if(!e.ok)return;let{code:r}=await e.json();t.textContent=r,i.classList.replace(`hidden`,`flex`)};let o=f(`div`,`flex flex-col`);e.users.length||o.append(C(`No bound users yet.`));for(let t of e.users){let e=q(`Remove`,F());e.onclick=async()=>{await fetch(`/api/channels/${n}/users/${encodeURIComponent(t.id)}`,{method:`DELETE`}),await E()},o.append(f(`div`,`group flex items-center gap-2.5 border-b border-neutral-100 py-2 last:border-0`,f(`span`,`flex h-6 w-6 flex-none items-center justify-center rounded-full bg-neutral-100 text-[11px] font-semibold text-neutral-500`,(t.name[0]??`?`).toUpperCase()),f(`span`,`min-w-0 flex-1 truncate text-[13px] text-neutral-700`,t.name),f(`span`,`flex-none font-mono text-[11.5px] text-neutral-400`,t.id),e))}return r(`Bound users`,`A code is single-use and expires; redeeming it in a DM binds that account.`,o,f(`div`,`flex flex-col gap-2`,a,i))}function te(e,t){let r=f(`div`,`group rounded-xl border px-3.5 py-3 transition-colors ${t.enabled?`border-neutral-200 bg-white`:`border-neutral-200 bg-neutral-50/60`}`),i=t.botId!==e.botId,s=q(`Remove`,F());s.onclick=async()=>{let e=t.name||t.id;window.confirm(`Remove ${e}? Threads in it lose their sessions; a chat the bot can still reach comes back on its next message.`)&&(await fetch(`/api/channels/${n}/chats/${encodeURIComponent(t.id)}`,{method:`DELETE`}),await E())};let c=f(`div`,`flex flex-wrap items-center gap-2`,f(`span`,`min-w-0 flex-1 basis-full truncate text-[13px] font-medium sm:basis-0 ${t.enabled?`text-neutral-800`:`text-neutral-400`}`,t.name||t.id),A(t.kind,be[t.kind]),...i?[A(t.botId?`other bot`:`no bot`,`bg-amber-50 text-amber-700 ring-amber-200`)]:[],f(`span`,`flex-none font-mono text-[11px] text-neutral-400`,t.id),s),l=x(``,``,t.enabled,b(e=>{t.enabled=e,M()}));l.classList.add(`flex-none`,`items-center`),c.append(l);let u=f(`div`,`mt-3 flex flex-wrap items-center gap-x-6 gap-y-2`);if(t.kind===`dm`?u.append(f(`span`,`text-[13px] text-neutral-400`,`Direct message · bound users only, mention not applicable`),x(`This DM is the conversation`,``,t.home===!0,b(n=>{for(let t of e.chats)delete t.home;n&&(t.home=!0),M()}))):u.append(x(`Require mention`,``,t.requireMention,b(e=>t.requireMention=e)),x(`Require bind`,``,t.requireBind,b(e=>t.requireBind=e))),i&&u.append(f(`span`,`w-full text-[12.5px] text-amber-700`,t.botId?`Last seen under bot ${t.botId}${e.botId?`, not the current ${e.botId}`:``} — remove it if nothing arrives here.`:`No message since Pier started recording bot identities — remove it if nothing arrives here.`)),t.home)return r.append(c,u,f(`p`,`mt-3 text-[13px] text-neutral-400`,`The conversation's model and reasoning are set from its ⋯ menu.`)),r;let d=O(t.cwd,``,b(e=>t.cwd=e)),p=f(`div`,`mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2`,o(`Working directory`,d.el),Z(`Model & reasoning`,t,a,b(e=>{t.model=e.model,t.thinking=e.thinking,M()})));return r.append(c,u,p),r}function j(e){let t=f(`div`,`flex flex-col gap-2.5`);return e.chats.length?t.append(...e.chats.map(t=>te(e,t))):t.append(C(`None yet. Chats appear here after the bot sees a message in them.`)),r(n===`slack`?`Channels`:`Chats`,e.botId?`${xe} Current bot: ${e.botId}.`:xe,t)}function M(){if(!i)return;p();let e=f(`div`,`mx-auto flex max-w-3xl flex-col gap-4`);e.append(D(i),k(i),ee(i),j(i)),u.replaceChildren(e)}return l(e,()=>void E(),()=>{m&&T()})}var we=`Managed by the configuration subscription: the source's copy is applied on every sync. To edit here, pause it under Agent → Configuration sync.`,Te=async()=>{let e=await V(`/api/config-sync`,``,{cache:`no-store`});return e.ok&&e.value.enabled};function Ee(){let e=f(`div`,`min-h-0 min-w-0 flex-1 overflow-y-auto p-4`),t=f(`div`,`mx-auto flex max-w-3xl min-w-0 flex-col gap-4`),n=f(`p`,`text-[12px] break-words`),r=f(`section`,`flex min-w-0 flex-col gap-3 border-b border-neutral-200 pb-4`),i=f(`section`,`flex min-w-0 flex-col gap-3`);t.append(f(`h2`,`text-[14px] font-semibold text-neutral-700`,`Configuration sync`),n,r,i),e.append(t);let a=null,s=!1,c=!1,l=0,u=``,m=!1,h=e=>e?new Date(e).toLocaleString():`Never`;async function g(){let e=l,t=await V(`/api/config-sync`,`Could not load configuration sync`,{cache:`no-store`});if(!(c||e!==l)){if(!t.ok){y(n,`failed`,t.error);return}a=t.value,m||(u=a.sourceUrl),b()}}async function _(e){if(!s){++l,s=!0,b(),y(n,`saving`,`Working...`);try{let t=await H(`/api/config-sync`,{action:e,...e===`subscribe`?{url:u}:{}});if(!t.ok)throw Error(await U(t,`Configuration sync failed`));let r=await t.json();a=r,m=!1,u=r.sourceUrl,y(n,`saved`,r.result??`Saved`)}catch(e){y(n,`failed`,e instanceof Error?e.message:`Configuration sync failed`),await g()}finally{s=!1,c||b()}}}function v(e,t){let n=S(e);return n.disabled=s,n.onclick=()=>{_(t)},n}function b(){if(!a||c)return;if(r.replaceChildren(f(`h3`,`text-[13px] font-medium text-neutral-700`,`Publish`)),a.publishedPath){let e=`${a.publicUrl||window.location.origin}${a.publishedPath}`,t=d(e,``,()=>{},!0);t.readOnly=!0,t.setAttribute(`aria-label`,`Sharing URL`),r.append(t,f(`div`,`flex flex-wrap items-center gap-2`,p(`btn text-[12px]`,()=>e),v(`Revoke link`,`revoke`)))}else r.append(f(`div`,``,v(`Generate link`,`publish`)));r.append(f(`p`,`text-[12px] text-neutral-500`,`Anyone with the link can read shared prompts, model definitions, the default model and its reasoning effort. API keys, headers and endpoints stay private.`));let e=d(u,`https://example.com/config-sync/...`,e=>{u=e,m=!0},!0);e.type=`url`,e.setAttribute(`aria-label`,`Configuration source URL`),e.disabled=s||a.enabled;let t=x(`Automatic sync`,``,a.enabled,e=>{_(e?`subscribe`:`pause`)});t.querySelector(`input`).disabled=s;let n=f(`div`,`flex flex-wrap items-center gap-3`,t);a.enabled&&n.append(v(`Sync now`,`sync`)),i.replaceChildren(f(`h3`,`text-[13px] font-medium text-neutral-700`,`Source`),o(`Source URL`,e),n);let l=f(`dl`,`grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1 text-[12px]`);for(let[e,t]of[[`Last checked`,h(a.lastChecked)],[`Last applied`,h(a.lastApplied)],[`Next check`,a.enabled?h(a.nextRunAt):`Paused`]])l.append(f(`dt`,`text-neutral-500`,e),f(`dd`,`min-w-0 break-words text-neutral-700`,t));i.append(l),a.error&&i.append(f(`p`,`text-[12px] break-words text-red-600`,a.error)),a.needsReload&&i.append(f(`p`,`text-[12px] text-amber-700`,`Reload pending; retry synchronization.`))}g();let C=setInterval(()=>{!s&&!m&&g()},15e3);return{el:e,dispose:()=>{c=!0,clearInterval(C)}}}var De=`text-[11.5px] text-neutral-400`,Oe=`bg-neutral-50 text-neutral-500 ring-neutral-200`,ke=`bg-amber-50 text-amber-700 ring-amber-100`,Ae=`Pi packages run with full system access: an extension executes arbitrary code, and a skill can instruct the model to run anything. Review the source before installing.`,je=e=>e===`pier`||e===`local`?e:e.replace(/^(npm|git):/,``).replace(/\.git$/,``).replace(/@[^@/]*$/,``).split(`/`).filter(Boolean).pop()??e,Me=e=>e.startsWith(`npm:`)?`npm`:/^(git:|(https?|ssh|git):\/\/)/.test(e)?`git`:`path`,Ne=e=>e.kind===`pier`||e.kind===`local`,Pe=e=>e.slice(0,e.lastIndexOf(`/`))||`/`;function Fe(e){let t=null,n=``,r=()=>{let t=e.cwd();return t?`?cwd=${encodeURIComponent(t)}`:``},i=t=>!Ne(t)&&t.scope===`global`&&e.cwd()===void 0;async function a(){let e=await V(`/api/packages${r()}`,`packages could not be loaded`,{cache:`no-store`});t=e.ok?e.value:null,n=e.ok?``:e.error}let s=(e,n)=>t?.packages.find(t=>t.source===e&&t.scope===n),c=(e,n,r)=>{for(let i of t?.packages.filter(t=>t.source===e)??[]){let e=i.resources.find(e=>e.kind===n&&e.path===r);if(e)return{pkg:i,resource:e}}};async function l(t,n,r){let i=e.cwd(),a=await P(`/api/packages/resource`,{source:t.source,kind:n.kind,path:n.path,enabled:r,...i?{cwd:i}:{}},`could not save`,`PUT`);return a.ok?(t.resources=t.resources.map(e=>e.kind===n.kind&&e.path===n.path?a.value:e),e.changed(),{state:`saved`,text:`Saved — sessions take it on their next message.`}):{state:`failed`,text:a.error}}let p=t=>{e.pane.replaceChildren(f(`p`,`px-4 py-3 text-[12.5px] leading-relaxed text-red-600`,t))},m=(...e)=>f(`div`,`flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4`,...e),h=(e,t,n=!0)=>[f(`dt`,`text-[12px] text-neutral-500`,e),f(`dd`,`text-[12px] leading-snug text-neutral-700 ${n?`font-mono break-all`:``}`,t)],g=(t,n)=>{let r=q(`Browse files`,`btn text-[12px]`);return r.onclick=()=>e.browse(t,n),r},_=(e,t,n,r,i)=>x(n,r,t.enabled,n=>void l(e,t,n).then(i));function v(t,n,r){let i=q(n.name,`font-mono text-[13px] text-neutral-700 hover:text-indigo-600 hover:underline`);i.onclick=()=>{let r={type:`resource`,source:t.source,kind:n.kind,path:n.path};e.select(r),T(r.source,r.kind,r.path)};let a=f(`div`,`flex min-w-0 flex-1 flex-col gap-0.5`,f(`span`,`flex flex-wrap items-center gap-2`,i,A(n.kind,Oe)),...n.state?[f(`span`,`text-[11.5px] leading-snug text-amber-700`,n.state)]:[]);return f(`div`,`flex max-w-2xl items-start gap-3 border-b border-neutral-100 py-2.5 last:border-0`,a,_(t,n,``,``,r))}function b(n,r,o){let c=e.claim(),l=s(n,r);if(!l)return p(`unknown package: ${n}`);let d=f(`span`,De,``);o&&y(d,o.state,o.text);let _=t?.busy===l.source;_&&!o&&y(d,`saving`,`${l.installedPath?`updating`:`installing`}…`);let x=t=>{e.live(c)&&b(n,r,t)},S=[...h(`Source`,l.source),...h(`Kind`,l.kind===`pier`?`built in — ships with Pier`:l.kind===`local`?`the agent directory's own files`:l.kind,!1),...l.version?h(`Version`,l.version):[],...l.installedPath===null?Ne(l)?[]:h(`Installed at`,`not installed`,!1):h(`Installed at`,f(`span`,`flex flex-wrap items-center gap-2`,f(`span`,`font-mono break-all`,l.installedPath),g(l.installedPath)),!1)],w=[];if(i(l)&&l.kind!==`path`){let i=t?.checkedAt?`checked ${u(Date.parse(t.checkedAt))}`:`not checked yet`,o=q(`Check for updates`,`btn text-[12px]`);o.disabled=_,o.onclick=()=>{o.disabled=!0,y(d,`saving`,`checking…`),P(`/api/packages/check`,{},`update check failed`).then(i=>{i.ok&&(t=i.value),e.changed();let a=s(n,r);x(i.ok?{state:`saved`,text:a?.updateAvailable?`Checked — an update is available.`:`Checked — up to date.`}:{state:`failed`,text:i.error})})},S.push(...h(`Updates`,f(`span`,`flex flex-wrap items-center gap-2`,f(`span`,l.updateAvailable?`text-emerald-700`:``,l.updateAvailable?`update available (${i})`:i),o),!1));let c=q(`Update`,`btn text-[12.5px]`);c.disabled=_,c.onclick=()=>{c.disabled=!0,t&&={...t,busy:l.source},e.changed(),x({state:`saved`,text:`updating…`}),P(`/api/packages/update`,{source:l.source},`update failed`).then(async t=>{await a(),e.changed();let n=t.ok?t.value.packages.find(e=>e.source===l.source):void 0;x(t.ok?{state:`saved`,text:`Updated${n?.version?` to ${n.version}`:``} — sessions take it on their next message.`}:{state:`failed`,text:t.error})})},w.push(c)}if(i(l)){let t=q(`Remove`,`btn text-[12.5px] text-neutral-600 hover:text-red-600`);t.disabled=_,t.onclick=()=>{let n=l.kind===`path`?`settings.json forgets it; the directory stays.`:`Its files are deleted and settings.json forgets it.`;window.confirm(`Remove ${l.source}? ${n}`)&&(t.disabled=!0,y(d,`saving`,`removing…`),P(`/api/packages/remove`,{source:l.source},`remove failed`).then(async t=>{if(!t.ok)return x({state:`failed`,text:t.error});await a(),e.select(null),e.live(c)&&e.pane.replaceChildren(f(`div`,`flex min-h-0 flex-1 items-center justify-center p-6`,C(`Removed ${l.source}.`)))}))},w.push(t)}e.pane.replaceChildren(e.paneBar(je(l.source),A(l.kind,Oe),...l.scope===`project`?[A(`project`,ke)]:[],...w.length?[f(`div`,`ml-auto flex flex-none items-center gap-2`,...w)]:[]),m(f(`dl`,`grid max-w-2xl grid-cols-[auto_1fr] gap-x-4 gap-y-1.5`,...S),l.resources.length?f(`div`,`flex max-w-2xl flex-col`,...l.resources.map(e=>v(l,e,x))):C(l.installedPath?`No extensions or skills.`:`Not installed — nothing to load.`),d))}let S=(t,n)=>t.kind===`pier`?n.kind===`skill`?`Pier's own skill. Off, no session is offered it.`:`Loaded from inside Pier — nothing installed, nothing to update. A session mid-turn keeps the tools it started with.`:`Written to ${e.cwd()?`this project's .pi/settings.json`:`settings.json`} as +path / −path for ${je(t.source)}; sessions pick it up on their next open.`;async function T(t,n,r,i){let a=e.claim(),o=c(t,n,r);if(!o)return p(`unknown ${n}: ${r}`);let{pkg:s,resource:l}=o,u=f(`span`,De,``);i&&y(u,i.state,i.text);let d=f(`div`,`min-h-0 flex-1 overflow-auto`),m=r.startsWith(`/`);if(e.pane.replaceChildren(e.paneBar(l.name,A(je(s.source),Oe),A(l.kind,Oe),f(`span`,`ml-auto flex flex-wrap items-center gap-2`,f(`span`,`font-mono text-[11px] text-neutral-400 break-all`,l.path),...m?[g(Pe(r),w(r))]:[])),f(`div`,`flex flex-none flex-col gap-2 border-b border-neutral-200 px-4 py-3`,_(s,l,`Enabled`,S(s,l),i=>{e.live(a)&&T(t,n,r,i)}),...l.state?[f(`p`,`text-[12px] text-amber-700`,l.state)]:[],u),d),r.startsWith(`<inline:`)){d.append(f(`p`,`px-4 py-3 text-[12.5px] text-neutral-400`,`Loaded from inside Pier — there is no file to show.`));return}let h=await E(r);if(!e.live(a))return;if(!h.ok)return d.append(f(`p`,`px-4 py-3 text-[12.5px] text-red-600`,h.error));let v=await B(r);e.live(a)&&d.append(le(ce(h.value),v))}async function E(e){let t=`/api/fs/file?root=${encodeURIComponent(Pe(e))}&path=${encodeURIComponent(w(e))}`,n;try{n=await fetch(t,{cache:`no-store`})}catch(t){return{ok:!1,error:`could not read ${e}: ${String(t)}`}}return n.ok?{ok:!0,value:await n.text()}:{ok:!1,error:await U(n,`could not read ${e}`)}}function D(n,r=``){let i=e.claim(),s=f(`span`,De,``);n&&y(s,n.state,n.text);let c=r,l=d(r,`npm:@scope/name · git:github.com/owner/repo · /path/to/package`,e=>c=e,!0),u=q(`Install`,`btn btn-primary text-[12.5px]`),p=()=>{let n=c.trim();if(!n)return y(s,`failed`,`A package source is required.`);u.disabled=!0,y(s,`saving`,`installing ${n}…`),t&&(t={...t,busy:n,packages:[...t.packages,{source:n,kind:Me(n),scope:`global`,version:null,installedPath:null,updateAvailable:!1,resources:[]}]},e.changed()),P(`/api/packages`,{source:n},`install failed`).then(async t=>{if(await a(),e.changed(),!e.live(i))return;if(!t.ok)return D({state:`failed`,text:t.error},n);let{package:r}=t.value;e.select({type:`package`,source:r.source,scope:r.scope}),b(r.source,r.scope,{state:`saved`,text:`Installed ${r.source} — sessions take it on their next message.`})})};u.onclick=p,l.onkeydown=e=>{e.key===`Enter`&&p()},e.pane.replaceChildren(e.paneBar(`Add package`),m(f(`div`,`flex max-w-2xl flex-col gap-3`,o(`Source`,l,{hint:"What `pi install` takes: an npm package, a git URL (both may pin @version), or a directory on this machine."}),f(`p`,`text-[12.5px] leading-relaxed text-amber-800`,Ae),f(`div`,`flex items-center gap-3`,u,s)))),l.focus()}return{get registry(){return t},get error(){return n},load:a,openPackage:b,openResource:T,openAdd:D}}async function Ie(e,t){let n;try{n=await H(`/api/settings`,e,`PUT`)}catch(e){return{outcome:{state:`failed`,text:`Could not save: ${String(e)}`}}}if(!n.ok)return{outcome:{state:`failed`,text:await U(n,`could not save`)}};let r;try{r=await n.json()}catch(e){return{outcome:{state:`failed`,text:`Saved, but the answer could not be read: ${String(e)}`}}}return r.toolsSync?.state===`refused`?{outcome:{state:`failed`,text:`Saved, but nothing will install it: ${r.toolsSync.reason}`},answer:r}:r.toolsSync?.state===`waiting`?{outcome:{state:`saved`,text:`Saved — a sync is already running; this change goes in the run right after it.`},answer:r}:{outcome:{state:`saved`,text:t},answer:r}}function Le(e,t){return e.enabled?{body:{tool:{name:e.name,on:!1}},saved:`Switched ${e.name} off — the next run uninstalls it. Remove it again once its binary is gone.`}:{body:{customTools:t.filter(t=>t.name!==e.name)},saved:`Removed ${e.name}.`}}function Re(e,n,r){let i=`global`,a=null,s=0,c=0,u=``,p=()=>u?`Global (${u})`:`Global`,m=null,h=[],g=``,_=null,b=null,S=()=>{b?.dispose(),b=null},T=()=>{S(),++c,b=Ee(),I.replaceChildren(b.el)},E=[],D=e=>e.custom===!0,O=f(`span`,`min-w-0 flex-1 truncate`),k=f(`button`,`${N} flex items-center gap-1.5`,O,K(t,`h-3.5 w-3.5 text-neutral-500`));k.setAttribute(`aria-haspopup`,`true`),k.onclick=()=>{if(k.getAttribute(`aria-expanded`)===`true`)return ee();te(k,[{label:p(),checked:i===`global`,onSelect:()=>be(`global`)},...n().map(e=>({label:e,hint:w(e),checked:i===e,onSelect:()=>be(e)}))],`Scope`)};let j=f(`div`,`${ne} flex-col items-stretch gap-1.5 px-3 py-2.5`);j.append(f(`span`,`field-label`,`Scope`),k);let P=f(`div`,`min-h-0 flex-1 overflow-y-auto py-1.5`),F=f(`nav`,`${M} w-64 flex-none text-[13px] leading-5 max-md:max-h-48 max-md:w-full`);F.append(j,P);let I=f(`div`,`${M} min-w-0 flex-1`),L=f(`div`,`flex min-h-0 flex-1 gap-3 px-4 pb-4 pt-1 max-md:flex-col`);L.append(F,I),e.append(L);let z=(e,...t)=>f(`div`,`${ne} flex-wrap gap-3 px-4 py-2.5`,f(`span`,`font-mono text-[12.5px] text-neutral-500`,e),...t),W=Fe({pane:I,paneBar:z,claim:()=>++c,live:e=>e===c,cwd:()=>i===`global`?void 0:i,changed:()=>$(m),browse:r,select:e=>{a=e,e?.type===`resource`&&Z.set(e.source,!0),$(m)}}),G=(e=``)=>`?scope=${encodeURIComponent(i)}${e}`;async function re(){if(i!==`global`){h=[],g=``;return}let e=await V(`/api/settings`,`could not be loaded`,{cache:`no-store`});if(!e.ok){h=[],g=e.error;return}J(e.value),g=``}function J(e){h=e.catalog,E=e.customTools,_=e.toolsTaskId}async function Y(e,t){let{outcome:n,answer:r}=await Ie(e,t);return r&&(J(r),$(m)),n}async function ie(){let e=++s;c++;let[t]=await Promise.all([V(`/api/config${G()}`,`failed to load config`,{cache:`no-store`}),re(),W.load()]);if(e!==s)return;if(!t.ok){$(null),me(t.error);return}let n=t.value;i===`global`&&n.dir&&(u=n.dir,xe()),m=n,$(n),a?a.type===`sync`&&T():pe()}function ae(e,t){return f(`div`,`flex items-center justify-between gap-2 px-3 pb-1 pt-3 text-[10.5px] font-semibold uppercase tracking-wide text-neutral-400`,f(`span`,``,e),...t?[t]:[])}let X=e=>f(`span`,`flex-none rounded bg-neutral-100 px-1 text-[10px] font-semibold uppercase tracking-wide text-neutral-500`,e),oe=()=>f(`span`,`flex-none rounded bg-emerald-50 px-1 text-[10px] font-semibold uppercase tracking-wide text-emerald-700`,`on`);function se(e,t,n,r,i={}){let a=f(`button`,`config-row flex w-full cursor-pointer items-center gap-1.5 py-1.5 pr-3 text-left transition-colors hover:bg-neutral-100 ${t?`bg-indigo-50 font-medium hover:bg-indigo-50`:``} ${n?`text-neutral-400`:``}`),o=f(`span`,`truncate`,e);if(i.note){let e=f(`span`,`truncate text-[11px] font-normal leading-4 text-amber-700`,i.note);e.title=i.note,a.append(f(`span`,`flex min-w-0 flex-1 flex-col`,o,e))}else a.append(o);return a.append(...i.tags??[]),a.style.paddingLeft=`${i.flush?0:20+(i.depth??0)*14}px`,a.title=e,a.onclick=r,a}let Z=new Map;function ue(e,t,n,r){let i=Z.get(e)??n.openByDefault,a=f(`button`,`flex w-5 flex-none cursor-pointer items-center justify-center self-stretch text-neutral-500 hover:bg-neutral-100`,K(v,`chev h-3 w-3`));i&&a.classList.add(`chev-open`),a.title=`${i?`Collapse`:`Expand`} ${t}`,a.setAttribute(`aria-label`,a.title),a.setAttribute(`aria-expanded`,String(i)),a.onclick=()=>{Z.set(e,!i),$(m)};let o=se(t,n.active,n.dim,n.onPick,{tags:n.tags,flush:!0}),s=[f(`div`,`flex items-stretch ${n.active?`bg-indigo-50`:``}`,a,o)];return i?[...s,...r()]:s}function de(e,t,n){let r={type:`package`,source:e.source,scope:e.scope},i=[];return W.registry?.busy===e.source?i.push(f(`span`,`flex-none text-[11px] text-neutral-400`,e.installedPath?`updating…`:`installing…`)):e.resources.some(e=>e.enabled)&&i.push(oe()),e.scope===`project`&&i.push(X(`project`)),ue(e.source,je(e.source),{active:t(r),dim:e.installedPath===null&&!Ne(e),tags:i,onPick:()=>n(r),openByDefault:Ne(e)},()=>fe(e,t,n))}function fe(e,t,n){let r=[],i=e.resources.map(e=>e.name),a=new Set(i.filter((e,t)=>i.indexOf(e)!==t));for(let[i,o]of[[`extensions`,`extension`],[`skills`,`skill`]]){let s=e.resources.filter(e=>e.kind===o);if(!s.length)continue;let c=f(`div`,`py-0.5 text-[10.5px] font-semibold uppercase tracking-wide text-neutral-400`,i);c.style.paddingLeft=`34px`,r.push(c);for(let i of s){let s={type:`resource`,source:e.source,kind:o,path:i.path};r.push(se(i.name,t(s),!i.enabled,()=>n(s),{depth:2,note:i.state,tags:a.has(i.name)?[X(o)]:[]}))}}return r}function Q(e,t){let n={type:`tools`};return ue(`tools`,`tools`,{active:e(n),dim:!1,tags:h.some(e=>e.enabled)?[oe()]:[],onPick:()=>t(n),openByDefault:!0},()=>g?[f(`p`,`py-1 pl-5 pr-3 text-[12.5px] text-red-600`,g)]:h.map(n=>{let r={type:`tool`,name:n.name};return se(n.name,e(r),!n.enabled,()=>t(r),{depth:1,note:n.binary.error,tags:D(n)?[X(`yours`)]:[]})}))}function $(e){if(!e){P.replaceChildren();return}let t=e=>JSON.stringify(e)===JSON.stringify(a),n=t=>{S(),a=t,$(e),t.type===`file`?he(t.name,t.readonly,t.managed):t.type===`tools`||t.type===`tool`?ve():t.type===`sync`?T():t.type===`package`?W.openPackage(t.source,t.scope):t.type===`resource`?W.openResource(t.source,t.kind,t.path):W.openAdd()},r=[];if(i===`global`){let e={type:`sync`};r.push(ae(`Instance`),se(`Configuration sync`,t(e),!1,()=>n(e)))}r.push(ae(`Files`));for(let i of e.files){let e={type:`file`,name:i.name,readonly:i.readonly,managed:i.managed};r.push(se(i.name,t(e),!i.exists,()=>n(e)))}let o={type:`add`},s=q(`Add package`,`normal-case tracking-normal hover:underline ${t(o)?`text-indigo-700`:`text-indigo-600`}`);s.onclick=()=>n(o),r.push(ae(`Packages`,i===`global`?s:void 0)),W.error&&r.push(f(`p`,`py-1 pl-5 pr-3 text-[12.5px] text-red-600`,W.error));for(let e of W.registry?.packages??[])r.push(...de(e,t,n));i===`global`&&r.push(...Q(t,n)),P.replaceChildren(...r)}function pe(){I.replaceChildren(f(`div`,`flex min-h-0 flex-1 items-center justify-center p-6`,C(`Select a file to edit, or a package, extension, skill or tool to switch.`)))}function me(e){I.replaceChildren(f(`p`,`px-4 py-3 text-[12.5px] leading-relaxed text-red-600`,e))}async function he(e,t,n){let r=++c,i=await V(`/api/config/files/${encodeURIComponent(e)}${G()}`,`failed to load ${e}`,{cache:`no-store`});if(!i.ok){r===c&&me(i.error);return}if(r!==c)return;let{content:a}=i.value,o=a,s=f(`span`,`text-[11.5px] text-neutral-400`,``),l=f(`button`,`btn btn-primary text-[12.5px]`,`Save`),u=document.createElement(`textarea`);u.className=`block min-h-0 flex-1 resize-none bg-white p-4 font-mono text-[13px] leading-relaxed focus:outline-none`,u.spellcheck=!1,u.value=a;let d=async()=>{if(l.disabled)return;let t=u.value;l.disabled=!0,y(s,`saving`,`saving…`);try{let n=await H(`/api/config/files/${encodeURIComponent(e)}${G()}`,{content:t,expected:o},`PUT`);if(!n.ok)return y(s,`failed`,await U(n,`save failed`));let r=await n.json();o=r.content,u.value===t?(u.value=r.content,y(s,`saved`,`saved — applies to new sessions`)):y(s,`idle`,`saved; newer changes are unsaved`)}catch(e){y(s,`failed`,`save failed: ${String(e)}`)}finally{l.disabled=!1}},p=f(`button`,`btn text-[12.5px]`,`Edit`),m=f(`button`,`btn text-[12.5px]`,`View`),h=(...e)=>f(`div`,`ml-auto flex flex-none items-center gap-2`,...e),g=()=>{I.replaceChildren(z(e,s,h(m,l)),u),u.focus()},_=async()=>{let i=await B(e);if(r!==c)return;let a=n?we:t?`Pier manages this file: the default model is set in Settings → Models; other keys are edited on disk, then pier reload.`:null;I.replaceChildren(a?z(e,f(`span`,`ml-auto text-[11px] uppercase tracking-wide text-neutral-400`,`read-only`)):z(e,s,h(p)),...a?[f(`p`,`border-b border-neutral-200 px-4 py-2 text-[12.5px] leading-relaxed text-neutral-500`,a)]:[],f(`div`,`min-h-0 flex-1 overflow-auto`,le(ce(u.value),i)))};p.onclick=g,m.onclick=()=>void _(),l.onclick=()=>void d(),u.onkeydown=e=>{(e.metaKey||e.ctrlKey)&&e.key===`s`&&(e.preventDefault(),d())},u.oninput=()=>{y(s,`idle`,`unsaved changes`)},await _()}let ge=()=>{let e=f(`a`,`text-indigo-600 hover:underline`,`ubix`);return e.href=`https://github.com/timqi/ubix`,e.target=`_blank`,e.rel=`noreferrer`,e};function _e(e){let{spec:t,error:n,installed:r,version:i,path:a}=e.binary;return n?`${t} — ${n}`:r?`${t} — ${i??`unknown version`} at ${a??`an unknown path`}`:`${t} — not installed`}function ve(e,t={name:``,toml:``}){c++;let n=a?.type===`tool`?a.name:null,r=f(`span`,`text-[11.5px] text-neutral-400`,``);e&&y(r,e.state,e.text);let i=h.map(e=>{let t=f(`div`,`flex min-w-0 flex-1 flex-col gap-0.5`,f(`span`,`flex items-center gap-2 text-[13px] text-neutral-700`,f(`span`,`font-mono`,e.name),...D(e)?[A(`yours`,`bg-neutral-50 text-neutral-500 ring-neutral-200`)]:[]),f(`span`,`text-[11.5px] leading-snug text-neutral-400`,e.summary||_e(e)),...e.summary?[f(`span`,`font-mono text-[11px] leading-snug ${e.binary.error?`text-red-600`:`text-neutral-400`}`,_e(e))]:[]),r=x(``,``,e.enabled,t=>void ye(e.name,t,e=>ve(e))),i=f(`button`,`btn text-[12px] text-neutral-500 hover:text-red-600`,`Remove`);return i.onclick=()=>{i.disabled=!0;let t=Le(e,E);Y(t.body,t.saved).then(e=>ve(e))},f(`div`,`flex max-w-2xl items-start gap-3 border-b border-neutral-100 py-2.5 last:border-0 ${e.name===n?`rounded bg-indigo-50 ring-8 ring-indigo-50`:``}`,t,...D(e)?[i]:[],r)}),s={...t},l=f(`span`,`font-mono text-[12px] text-neutral-500`,`[tools.<name>]`),u=d(s.name,`claude`,e=>{s.name=e,l.textContent=`[tools.${e.trim()||`<name>`}]`},!0),p=document.createElement(`textarea`);p.className=`${R} h-24 resize-y font-mono leading-snug`,p.spellcheck=!1,p.value=s.toml,p.placeholder=`spec = "github:owner/repo"
|
|
2
|
+
exe = "tool"`,p.oninput=()=>s.toml=p.value;let m=f(`button`,`btn btn-primary text-[12.5px]`,`Add`);m.onclick=()=>{let e={name:s.name.trim(),toml:s.toml.trim()};if(!e.name||!e.toml)return y(r,`failed`,`A custom tool needs a name and a block with a spec line.`);m.disabled=!0,Y({customTools:[...E,e],tool:{name:e.name,on:!0}},`Added ${e.name} — installing now.`).then(e=>ve(e,e.state===`failed`?s:void 0))},I.replaceChildren(z(`command-line tools`,f(`span`,`ml-auto text-[11px] uppercase tracking-wide text-neutral-400`,`installed by Pier`)),f(`div`,`flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4`,f(`p`,`max-w-2xl text-[12.5px] leading-relaxed text-neutral-500`,`Pier writes no downloader: `,ge(),` does the work — a declarative installer that resolves a spec like `,f(`span`,`font-mono text-[11.5px]`,`github:owner/repo`),` to the right release asset for this machine, verifies it, and upgrades in place. Pier bootstraps it, generates its config from the switches below, and keeps its files under Pier's own directory — your own ubix setup is untouched.`),g?f(`p`,`max-w-2xl text-[12.5px] text-red-600`,g):f(`div`,`flex max-w-2xl flex-col`,...i),f(`div`,`flex max-w-2xl flex-col gap-2 border-t border-neutral-200 pt-4`,f(`span`,`text-[12.5px] font-medium text-neutral-600`,`Add one of your own`),o(`Name`,u),o(`The block Pier writes under the header`,p,{hint:`A line that opens a section of its own is refused, because it could rewrite the rest of the file.`}),f(`div`,`flex items-center gap-3`,l,m)),f(`p`,`max-w-2xl text-[12px] leading-snug text-neutral-400`,_?`Installs and daily updates run as the update task; a failure shows on the tool's row.`:`Switching one on creates the daily task that installs it and keeps it current.`),r)),i[h.findIndex(e=>e.name===n)]?.scrollIntoView({block:`nearest`})}async function ye(e,t,n){n(await Y({tool:{name:e,on:t}},t?`Saved — installing now; watch the run.`:`Saved — uninstalling in the next run.`))}function be(e){ee(),e!==i&&(S(),i=e,a=null,xe(),ie())}function xe(){i!==`global`&&!n().includes(i)&&(i=`global`),O.textContent=i===`global`?p():w(i),k.title=i===`global`?u:i}return l(e,()=>{xe(),ie()},S)}var ze=`medium`,Be=e=>({model:e.defaultModel,thinking:e.defaultThinkingLevel}),Ve=`Off — the first message is the title`;function He(){let e=[],n=[],i=!1,s,c={model:null,thinking:null},l=!1,u=null,d=f(`span`,`text-[11.5px]`,``),p=f(`span`,`text-[11.5px]`,``),m=f(`div`,`flex items-center gap-3`),h=f(`span`,`text-[11.5px]`,``),_=f(`div`,`flex flex-col gap-1.5`),v=S(`Save menu`,!0),b=f(`div`,`flex min-w-0 flex-col gap-2`),x=f(`div`,`flex items-center gap-2`),w=f(`fieldset`,`contents`),T=f(`fieldset`,`contents`),E=[0,1].map(()=>f(`p`,`text-[12px] leading-snug text-amber-700`,we)),O=f(`div`,`mx-auto flex w-full max-w-3xl min-w-0 flex-col gap-6`);function k(){i=!0,y(d,`idle`,`unsaved changes`)}function A(t,n,r,i){let a=e.indexOf(t),o=a+n,s=q(``,`icon-btn max-md:h-11 max-md:w-11 disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent`);return s.append(K(r)),s.title=i,s.setAttribute(`aria-label`,`${i}: ${Y(t)}`),s.disabled=o<0||o>=e.length,s.onclick=()=>{e.splice(o,0,...e.splice(a,1)),u={at:t,step:n},k(),L()},s}function te(n){let r=g(G.map(e=>[`thinking: ${z(e)}`,e]),n.thinking);r.classList.replace(`w-full`,`w-44`),r.classList.add(`flex-none`),r.onchange=()=>{n.thinking=r.value,k()};let i=g([[`tier: none`,``],...D.map(e=>[`tier: ${e}`,e])],n.tier??``);i.classList.replace(`w-full`,`w-36`),i.classList.add(`flex-none`),i.onchange=()=>{i.value?n.tier=i.value:delete n.tier,k()};let a=S(`Remove`);a.classList.add(`flex-none`),a.onclick=()=>{e=e.filter(e=>e!==n),k(),L()};let o=f(`span`,`min-w-0 flex-1 truncate font-mono text-[12px] text-neutral-700`,Y(n));o.title=Y(n);let s=A(n,-1,ue,`Move up`),c=A(n,1,t,`Move down`);if(u?.at===n){let[e,t]=u.step===-1?[s,c]:[c,s];u=null,queueMicrotask(()=>(e.disabled?t:e).focus())}return f(`div`,`flex min-w-0 items-center gap-2 rounded-lg border border-neutral-200 bg-white px-3 py-2 shadow-2xs max-md:flex-wrap`,f(`span`,`flex flex-none items-center`,s,c),o,r,i,a)}function M(){let t=S(`Pin model`);t.prepend(K(a)),t.classList.add(`inline-flex`,`items-center`,`gap-1.5`,`flex-none`,`whitespace-nowrap`),t.disabled=n.length===0,t.onclick=()=>{let r=ze;j(t,X({models:n,current:null,thinkingLevel:r,thinkingLevels:[...G],onThinkingPick:e=>{r=e},onPick:t=>{ee();let n=r,i=e.findIndex(e=>Y(e)===Y(t)&&e.thinking===n);if(i>=0){y(d,`failed`,`${Y(t)} is already pinned at ${z(n)} (row ${String(i+1)})`);return}e.push({provider:t.provider,id:t.id,thinking:n}),k(),L()}}))},x.replaceChildren(t)}function P(){let e=s,t=e&&!n.some(t=>Y(t)===Y(e))?[e,...n]:n,r=q(e?e.id:Ve,`${N} flex min-w-0 flex-1 items-center ${e?`text-neutral-700`:`text-neutral-400`}`);e&&(r.title=Y(e));let i=e=>{ee(),(async()=>{y(p,`saving`,`saving…`);let t=await H(`/api/settings`,{titleModel:e&&{provider:e.provider,id:e.id}},`PUT`);if(!t.ok)return y(p,`failed`,await U(t,`Could not save`)),P();s=(await t.json()).titleModel,y(p,`saved`,s?`Saved — names the next new session after its first reply.`:`Off.`),P()})()};r.onclick=()=>{let n=X({models:t,current:e,thinkingLevel:`medium`,thinkingLevels:[],onThinkingPick:()=>{},onPick:i}),a=q(Ve,`w-full cursor-pointer px-3 py-1.5 text-left text-[12.5px] text-neutral-500 hover:bg-neutral-100`);a.onclick=()=>i(null);let o=f(`div`,`flex flex-col`);o.append(n,f(`div`,`border-t border-neutral-200`),a),j(r,o)},m.replaceChildren(r,p)}function F(){_.replaceChildren(Z(`Default model`,c,n,e=>void I(e)),h)}async function I(e){y(h,`saving`,`saving…`);let t=await H(`/api/config/defaults`,{defaultModel:e.model,defaultThinkingLevel:e.thinking},`PUT`);if(!t.ok)return y(h,`failed`,await U(t,`Could not save`)),F();c=Be(await t.json()),y(h,`saved`,c.model||c.thinking?`Saved — the next new session starts on it.`:`Pi default.`),F()}function L(){b.replaceChildren(...e.length?e.map(te):[C(`Nothing pinned — every picker shows the curated catalog as is.`)]),M(),F(),P(),w.disabled=l,T.disabled=l;for(let e of E)e.hidden=!l}async function R(){y(d,`saving`,`saving…`);let t=e.map(({provider:e,id:t,thinking:n,tier:r})=>({provider:e,id:t,thinking:n,...r?{tier:r}:{}})),n=await H(`/api/settings`,{modelMenu:t},`PUT`);if(!n.ok)return y(d,`failed`,await U(n,`Could not save`));e=(await n.json()).modelMenu,i=!1,y(d,`saved`,`Saved — agents see it on their next models call.`),L()}v.onclick=()=>void R();function B(){i||(async()=>{let[t,r,i,a]=await Promise.all([V(`/api/settings`,`Could not load the menu`),V(`/api/models`,`Could not load the model catalog`),V(`/api/config/defaults`,`Could not read the default model`),Te()]);if(!t.ok)return y(d,`failed`,t.error);l=a,e=t.value.modelMenu,s=t.value.titleModel,n=r.ok?r.value:[],i.ok?(c=Be(i.value),h.textContent=``):y(h,`failed`,i.error),d.textContent=``,L()})()}return w.append(E[0],o(`Pinned models`,b,{hint:"The tier is what a dispatcher names — `--model balanced` is the first balanced row, and the balanced rows below it are tried in turn when it is not in the catalog. The arrows set that order, the order pickers and `pier task --model ?` list."}),o(`Add`,x,{hint:`The list is the live catalog — only models that exist right now can be pinned. A model may be pinned again at another reasoning level, each row with its own tier.`}),f(`div`,`flex items-center gap-3`,v,d)),T.append(E[1],_),O.append(r(`Model menu`,`Which few models this deployment favors, each with a usual reasoning level and a tier. Pinned entries lead every model picker, and agents delegating work name a tier instead of guessing ids.`,w),r(`New sessions`,`The model and reasoning effort a session starts on when nothing names one — a channel, chat or task with its own launch choice overrides it. Pi default leaves the pick to Pi. Stored in settings.json; applies to sessions opened from now on.`,T),r(`Session titles`,`A session is titled by its first message unless a model names it: one short request after the first reply, on the model picked here — a small, cheap one is plenty. A failed request is reported in the session and the first message stays the title.`,o(`Title model`,m))),{el:O,load:B}}var Ue=e=>Uint8Array.from(atob(e.replace(/-/g,`+`).replace(/_/g,`/`)),e=>e.charCodeAt(0)),We=e=>btoa(String.fromCharCode(...new Uint8Array(e))).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/,``);function Ge(){let e=f(`div`,`flex flex-col gap-2`),t=f(`span`,`text-[11.5px]`,``),n=_();n.placeholder=`Label, e.g. MacBook Touch ID`,n.autocomplete=`off`,n.maxLength=80;let i=S(`Add a passkey`,!0),a=f(`div`,`flex flex-wrap items-center gap-3`,n,i,t),o=f(`p`,`hidden text-[12.5px] leading-snug text-neutral-600`),s=f(`p`,`hidden text-[12px] text-neutral-400`);function c(n){if(s.textContent=n.reason??``,s.classList.toggle(`hidden`,n.enabled),a.classList.toggle(`hidden`,!n.enabled),o.textContent=`Password sign-in is off while a passkey is registered; remove every passkey to turn it back on.`,o.classList.toggle(`hidden`,!n.passkeys.length),!n.passkeys.length){e.replaceChildren(C(`No passkey is registered.`));return}e.replaceChildren(...n.passkeys.map(e=>{let n=S(`Remove`);n.onclick=()=>{n.disabled=!0,(async()=>{let n=await k(`/api/passkeys/${encodeURIComponent(e.id)}`,`DELETE`,`Could not remove it`);n?y(t,`failed`,n):y(t,`saved`,`Removed.`),await d()})()};let r=e.lastUsedAt?`last used ${u(e.lastUsedAt)}`:`never used`,i=e.transports.length?` · ${e.transports.join(`, `)}`:``;return re(e.label,`added ${u(e.createdAt)} · ${r}${i}`,n)}))}async function l(){if(!window.PublicKeyCredential)return y(t,`failed`,`This browser has no passkey support.`);i.disabled=!0,y(t,`saving`,`asking this browser…`);try{let e=await P(`/api/passkeys/register/options`,{},`Could not start`);if(!e.ok)return y(t,`failed`,e.error);let r={...e.value,challenge:Ue(e.value.challenge),user:{...e.value.user,id:Ue(e.value.user.id)},excludeCredentials:e.value.excludeCredentials.map(e=>({...e,id:Ue(e.id),transports:e.transports}))},i=await navigator.credentials.create({publicKey:r});if(!i)return y(t,`failed`,`No passkey was created.`);let a=i.response,o=await H(`/api/passkeys/register/verify`,{id:i.id,type:i.type,label:n.value,response:{clientDataJSON:We(a.clientDataJSON),attestationObject:We(a.attestationObject),transports:a.getTransports?.()??[]}});if(!o.ok)return y(t,`failed`,await U(o,`Could not register it`));n.value=``,c(await o.json()),y(t,`saved`,`Added. The password is off until every passkey is removed.`)}catch(e){let n=e instanceof DOMException&&e.name===`NotAllowedError`;y(t,`failed`,n?`The prompt was dismissed or timed out.`:`Could not add it: ${String(e)}`)}finally{i.disabled=!1}}i.onclick=()=>void l();async function d(){let t=await V(`/api/passkeys`,`Could not load passkeys`);return t.ok?(c(t.value),t.value.passkeys.length>0):(e.replaceChildren(C(t.error)),!1)}return{el:r(`Passkeys`,`Sign in with Touch ID, Windows Hello, a phone or a security key instead of the password. Bound to the public URL's host.`,e,o,s,a),load:d}}var Ke=(e,t)=>{let n;try{n=new URL(e)}catch{return null}if(n.protocol!==`http:`&&n.protocol!==`https:`)return null;let r=f(`a`,`text-indigo-600 hover:underline`,t);return r.href=n.href,r.target=`_blank`,r.rel=`noreferrer`,r};function qe(e,t,n,r){let i=document.createElement(`dialog`);i.className=`m-auto w-[min(32rem,92vw)] rounded-lg border border-neutral-200 bg-white p-0 shadow-xl backdrop:bg-black/30`;let a=f(`div`,`flex flex-col gap-2`),o=f(`div`,`flex flex-col gap-2`),s=f(`p`,`text-[12.5px] text-neutral-400`,`Starting…`),c=S(`Cancel`),l=(e,t)=>{s.className=`text-[12.5px] ${J[e]}`,s.textContent=t};i.append(f(`div`,`border-b border-neutral-200 px-4 py-2.5 text-[14px] font-semibold`,t),f(`div`,`flex max-h-[65vh] flex-col gap-3 overflow-y-auto px-4 py-3`,a,o,s),f(`div`,`flex justify-end border-t border-neutral-200 px-4 py-2.5`,c)),document.body.append(i),i.showModal();let u=e.id,d=e.state,m,h,g=new Set,v=!1,y=!1,b=async e=>{if(!y){if(e&&d===`running`){y=!0,c.disabled=!0;try{let e=await H(`/api/providers/flows/${u}/cancel`,{});if(!e.ok&&e.status!==404){l(`failed`,await U(e,`Could not cancel authentication flow`));return}}catch(e){l(`failed`,`Could not cancel authentication flow: ${String(e)}`);return}finally{y=!1,c.disabled=!1}}clearTimeout(m),i.close()}};c.onclick=()=>void b(!0),i.oncancel=e=>{e.preventDefault(),b(!0)},i.onclose=()=>{v=!0,clearTimeout(m),i.remove()};let x=e=>{if(e.type===`auth_url`){let t=f(`div`,`flex flex-col gap-1 text-[12.5px] text-neutral-600`);return e.instructions&&t.append(f(`p`,``,e.instructions)),t.append(Ke(e.url,`Open authorization page`)??f(`span`,`text-red-600`,`Invalid authorization URL`)),t}if(e.type===`device_code`){let t=f(`div`,`flex flex-col gap-2 text-[12.5px] text-neutral-600`);return t.append(f(`div`,`flex items-center gap-2`,f(`code`,`font-mono text-[17px] font-semibold text-neutral-800`,e.userCode),p(`btn text-[11px]`,()=>e.userCode))),t.append(Ke(e.verificationUri,`Open verification page`)??f(`span`,`text-red-600`,`Invalid verification URL`)),t}let t=f(`div`,`text-[12.5px] text-neutral-600`,e.message);if(e.type===`info`)for(let n of e.links??[]){let e=Ke(n.url,n.label??n.url);e&&t.append(` `,e)}return t},C=(e,t)=>{h=t.id;let n;if(t.type===`select`){let e=document.createElement(`select`);e.append(...t.options.map(e=>new Option(e.description?`${e.label} — ${e.description}`:e.label,e.id))),n=e}else{let e=_(``,t.type===`secret`?`password`:`text`);e.placeholder=t.placeholder??``,e.autocomplete=`off`,n=e}n.className=`${n.tagName===`SELECT`?`select `:``}w-full rounded-lg border border-neutral-300 bg-white px-2.5 py-1.5 text-[12.5px] focus:border-indigo-400 focus:outline-none`;let r=S(`Continue`,!0);r.type=`submit`;let i=f(`form`,`flex flex-col gap-2`,f(`label`,`text-[12.5px] font-medium text-neutral-700`,t.message),n,f(`div`,`flex justify-end`,r));i.onsubmit=i=>{i.preventDefault(),r.disabled=!0,(async()=>{try{let r=await H(`/api/providers/flows/${e.id}/respond`,{promptId:t.id,value:n.value});if(v)return;if(!r.ok){l(`failed`,await U(r,`Could not submit response`));return}g.add(t.id),h=void 0,o.replaceChildren(),l(`saving`,`Waiting for provider…`)}catch(e){v||l(`failed`,`Could not submit response: ${String(e)}`)}finally{r.disabled=!1}})()},o.replaceChildren(i),n.focus()},w=e=>{d=e.state,a.replaceChildren(...e.events.map(x));let t=e.prompt&&!g.has(e.prompt.id)?e.prompt:void 0;if(t?.id!==h&&(t?C(e,t):(h=void 0,o.replaceChildren())),e.state===`running`){l(`saving`,t?`Waiting for your response.`:`Waiting for provider…`);return}clearTimeout(m),o.replaceChildren(),e.state===`succeeded`?l(`saved`,`${n} configured.`):l(`failed`,e.state===`cancelled`?`Cancelled.`:e.error??`Authentication failed.`),c.textContent=`Close`,c.onclick=()=>void b(!1),e.state===`succeeded`&&r()},T=async()=>{if(!(v||d!==`running`))try{let e=await fetch(`/api/providers/flows/${u}`,{cache:`no-store`});if(v)return;if(!e.ok){l(`failed`,await U(e,`Authentication status unavailable`)),e.status===404?d=`failed`:m=setTimeout(()=>void T(),1e3);return}w(await e.json()),d===`running`&&(m=setTimeout(()=>void T(),500))}catch(e){if(v)return;l(`failed`,`Authentication status unavailable: ${String(e)}`),m=setTimeout(()=>void T(),1e3)}};w(e),d===`running`&&(m=setTimeout(()=>void T(),500))}var Je=[`anthropic`,`openai`,`openai-codex`,`google`,`openrouter`,`deepseek`],Ye=[[`OpenAI Chat Completions`,`openai-completions`],[`OpenAI Responses`,`openai-responses`],[`Anthropic Messages`,`anthropic-messages`],[`Google Generative AI`,`google-generative-ai`]],Xe=`__custom__`;async function Ze(e){let t=S(`Add provider`,!0);t.classList.add(`ml-auto`);let n=f(`div`,`flex flex-none items-center gap-3 rounded-t-3xl ${W} px-4 py-2`,f(`h2`,`text-[13px] font-semibold text-neutral-700`,`Providers`),t),r=f(`div`,`min-h-0 flex-1 overflow-y-auto`);e.replaceChildren(n,r);let i=[],a=``,o=async()=>{r.replaceChildren(f(`p`,`px-4 py-3 text-[13px] text-neutral-400`,`Loading…`));try{let[e,n]=await Promise.all([V(`/api/providers`,`Could not load providers`,{cache:`no-store`}),V(`/api/models`,`the model catalog could not be read`,{cache:`no-store`})]);if(i=n.ok?n.value:[],a=n.ok?``:n.error,!e.ok){r.replaceChildren(C(e.error));return}let s=e.value;t.onclick=()=>$e(s,void 0,o),d(s)}catch(e){r.replaceChildren(C(`Could not load providers: ${String(e)}`))}},s=async e=>{if(window.confirm(`Remove the stored credential for ${e.name}?`))try{let t=await H(`/api/providers/${encodeURIComponent(e.id)}/logout`,{});if(!t.ok){window.alert(await U(t,`Could not remove credential`));return}await o()}catch(e){window.alert(`Could not remove credential: ${String(e)}`)}},c=e=>[...new Set([...(e.models??[]).map(e=>e.id),...i.filter(t=>t.provider===e.id).map(e=>e.id)])],l=(e,t)=>f(`div`,`flex flex-col gap-1`,f(`span`,`text-[10.5px] font-semibold uppercase tracking-wide text-neutral-400`,e),f(`pre`,`max-h-56 overflow-auto whitespace-pre-wrap break-all rounded-md bg-neutral-50 p-2 font-mono text-[11.5px] leading-relaxed text-neutral-600`,t)),u=async(e,t)=>{try{let n=await H(`/api/providers/${encodeURIComponent(e)}/check`,{model:t});return n.ok?await n.json():await U(n,`Could not test`)}catch(e){return`Could not test: ${String(e)}`}},d=e=>{let t=e.filter(e=>e.configured);if(!t.length){r.replaceChildren(C(`No providers configured.`));return}r.replaceChildren(...t.map(t=>{let n=f(`div`,`min-w-0 flex-1`,f(`div`,`truncate text-[13px] font-medium text-neutral-700`,t.name),f(`div`,`truncate font-mono text-[11px] text-neutral-400`,t.id)),r=t.stored===`api_key`?`API key`:t.stored===`oauth`?`OAuth`:t.source??`configured`,i=t.endpoint??`Default endpoint`,d=f(`div`,`flex min-w-32 flex-col items-start gap-1`,A(`configured`,`bg-emerald-50 text-emerald-700 ring-emerald-200`),f(`span`,`max-w-48 truncate text-[11px] text-neutral-500`,r),f(`span`,`max-w-48 truncate text-[11px] text-neutral-400`,i));d.title=i;let p=f(`span`,`text-[11.5px]`,``),m=f(`div`,`hidden flex-col gap-2 px-4 pb-3`,p),h=f(`div`,`flex flex-wrap justify-end gap-1.5`),_=c(t);if(!_.length)y(p,`idle`,a||`No models to test: none are configured here.`),m.classList.replace(`hidden`,`flex`);else{let e=g([[`Model to test…`,``],..._.map(e=>[e,e])],``);e.classList.add(`max-w-52`,`text-[12px]`);let n=S(`Test`);n.disabled=!0,e.onchange=()=>n.disabled=!e.value,n.onclick=()=>{let r=e.value;n.disabled=!0,m.replaceChildren(p),m.classList.replace(`hidden`,`flex`),y(p,`saving`,`asking ${r}…`),u(t.id,r).then(e=>{if(n.disabled=!1,typeof e==`string`)return y(p,`failed`,e);y(p,e.ok?`saved`:`failed`,`${e.model} ${e.ok?`answered`:`did not answer`} in ${e.ms} ms`),m.append(l(`sent`,e.request||`(the request was never made)`),l(e.ok?`answered`:`replied`,e.response))})},h.append(e,n)}if(t.builtin||t.api&&t.models){let n=S(`Edit`);n.onclick=()=>$e(e,t,o),h.append(n)}if(t.stored){let e=S(`Remove credential`);e.onclick=()=>void s(t),h.append(e)}return f(`div`,`border-b border-neutral-100`,f(`div`,`grid grid-cols-[minmax(8rem,1fr)_minmax(8rem,auto)_minmax(9rem,auto)] items-center gap-3 px-4 py-2.5 max-sm:grid-cols-[1fr_auto]`,n,d,h),m)}))};await o()}function Qe(e,t){let n=t.filter(e=>!e.configured&&e.methods.length&&(e.builtin||e.api!==void 0&&e.models!==void 0)),r=(t,n)=>{if(!n.length)return;let r=document.createElement(`optgroup`);r.label=t,r.append(...n.map(e=>new Option(e.name,e.id))),e.append(r)};r(`Common`,Je.flatMap(e=>n.filter(t=>t.id===e))),r(`More providers`,n.filter(e=>e.builtin&&!Je.includes(e.id)).sort((e,t)=>e.name.localeCompare(t.name))),r(`Custom providers`,n.filter(e=>!e.builtin).sort((e,t)=>e.name.localeCompare(t.name))),e.append(new Option(`Custom provider…`,Xe))}function $e(e,t,n){let r=document.createElement(`dialog`);r.className=`m-auto w-[min(32rem,92vw)] rounded-lg border border-neutral-200 bg-white p-0 shadow-xl backdrop:bg-black/30`;let i=document.createElement(`select`);i.className=`${R} select`,t||Qe(i,e);let a=_(),s=_(),c=_(),l=document.createElement(`select`);l.className=`${R} select`,l.append(...Ye.map(([e,t])=>new Option(e,t)));let u=m(``,4);u.placeholder=`model-id
|
|
3
|
+
another-model-id`;let d=document.createElement(`input`);d.type=`checkbox`;let p=g([[`Up to High`,`high`],[`Up to Extra high`,`xhigh`],[`Up to Max`,`max`]],`high`);p.classList.replace(`w-full`,`w-48`),d.onchange=()=>{p.disabled=!d.checked};let h=document.createElement(`select`);h.className=`${R} select`;let v=f(`div`,`flex flex-col gap-4`),y=f(`span`,`text-[11.5px] text-red-600`,``),b=S(`Cancel`),x=S(t?`Save`:`Add`,!0);x.type=`submit`;let C=()=>t??e.find(e=>e.id===i.value),w=()=>{let e=C(),n=e?!e.builtin:i.value===Xe;a.value=e?.id??``,a.disabled=t!==void 0,s.value=e?.name??``,c.value=e?.endpoint??``,l.value=e?.api??`openai-completions`,u.value=e?.models?.map(e=>e.id).join(`
|
|
4
|
+
`)??``;let r=e?.models?.map(e=>e.reasoning)??[];d.checked=r.length?r.every(Boolean):!e,d.indeterminate=r.some(Boolean)&&!r.every(Boolean);let m=new Set((e?.models??[]).filter(e=>e.reasoning).map(e=>e.effort??`high`)),g=m.size>1;p.replaceChildren(...g?[new Option(`Keep as configured`,``)]:[],new Option(`Up to High`,`high`),new Option(`Up to Extra high`,`xhigh`),new Option(`Up to Max`,`max`)),p.value=g?``:[...m][0]??`high`,p.disabled=!d.checked&&!d.indeterminate;let _=[];e?.configured&&_.push(new Option(`Keep current authentication`,``));let y=e?.methods??[{type:`api_key`,name:`API key`}];for(let t of y.filter(t=>e?.builtin!==!1||t.type===`api_key`))_.push(new Option(t.name,t.type));h.replaceChildren(..._);let b=[];t||b.push(o(`Provider`,i)),n?b.push(o(`Provider ID`,a),o(`Display name`,s),o(`Endpoint`,c),o(`API format`,l),o(`Models`,u),o(`Model capabilities`,f(`div`,`flex flex-wrap items-center gap-3`,f(`label`,`flex items-center gap-2 text-[12.5px] text-neutral-600`,d,f(`span`,``,`Reasoning`)),p),{hint:`Extra high and Max reach the model only if it accepts them; every reasoning model has High.`})):(c.placeholder=`Provider default`,b.push(o(`Endpoint`,c))),b.push(o(`Authentication`,h)),v.replaceChildren(...b)};i.onchange=w,w();let T=f(`form`,`flex flex-col gap-4 px-4 py-3`,v,f(`div`,`flex items-center justify-end gap-2`,y,b,x)),E=!1;b.onclick=()=>{E||r.close()},r.oncancel=e=>{E&&e.preventDefault()},T.onsubmit=e=>{if(e.preventDefault(),E)return;y.textContent=``;let t=C(),o=(t?!t.builtin:i.value===Xe)?{kind:`custom`,id:a.value.trim(),...s.value.trim()?{name:s.value.trim()}:{},endpoint:c.value.trim(),api:l.value,models:[...new Set(u.value.split(`
|
|
5
|
+
`).map(e=>e.trim()).filter(Boolean))].map(e=>{let n=t?.models?.find(t=>t.id===e),r=d.indeterminate?n?.reasoning??d.checked:d.checked,i=p.value?p.value:n?.effort;return{id:e,reasoning:r,...r&&i&&i!==`high`?{effort:i}:{}}})}:{kind:`builtin`,id:t.id,...c.value.trim()?{endpoint:c.value.trim()}:{}},f=h.value||null;E=!0,x.disabled=!0,b.disabled=!0,(async()=>{try{let e=await H(`/api/providers/setup`,{setup:o,authType:f});if(!e.ok){y.textContent=await U(e,`Could not configure provider`);return}if(r.close(),e.status===202){let t=h.selectedOptions[0]?.textContent??`Authentication`;qe(await e.json(),o.id,t,()=>void n())}else await n()}catch(e){y.textContent=`Could not configure provider: ${String(e)}`}finally{E=!1,x.disabled=!1,b.disabled=!1}})()},r.append(f(`div`,`border-b border-neutral-200 px-4 py-2.5 text-[14px] font-semibold`,t?`Edit ${t.name}`:`Add provider`),T),r.onclose=()=>r.remove(),document.body.append(r),r.showModal(),(t?c:i).focus()}var et={queued:`bg-neutral-100 text-neutral-600 ring-neutral-200`,running:`bg-sky-50 text-sky-700 ring-sky-200`,succeeded:`bg-emerald-50 text-emerald-700 ring-emerald-200`,failed:`bg-red-50 text-red-700 ring-red-200`,cancelled:`bg-amber-50 text-amber-700 ring-amber-200`,interrupted:`bg-amber-50 text-amber-700 ring-amber-200`,skipped:`bg-neutral-100 text-neutral-600 ring-neutral-200`},tt=e=>e.state===`succeeded`&&e.matched===!1?A(`no match`,et.skipped):A(e.state,et[e.state],e.state===`running`?`animate-pulse bg-sky-500`:void 0),nt=e=>e.trigger.type===`manual`?`manual`:e.trigger.type===`cron`?`${e.trigger.expression} (${e.trigger.timezone})`:`watch every ${e.trigger.intervalSeconds}s`,rt=e=>`exit ${e.exitCode??`-`}\n${e.stdout}${e.stdoutTruncated?`
|
|
6
|
+
[stdout truncated]`:``}${e.stderr?`\nstderr:\n${e.stderr}`:``}${e.stderrTruncated?`
|
|
7
|
+
[stderr truncated]`:``}`;function it(e){let t=[e.error,e.skipReason,e.callbackError].filter(e=>!!e),n=e.result;return n?.type===`agent`||n?.type===`system`?t.push(n.text):n?.type===`bash`?t.push(rt(n)):n?.type===`task`&&t.push(`child run ${n.runId}`),e.probe&&t.push(`probe: ${rt(e.probe)}`),t.join(`
|
|
8
|
+
|
|
9
|
+
`)||`No output.`}var at=e=>{if(e.startedAt===null)return``;let t=Math.max(0,Math.round(((e.finishedAt??Date.now())-e.startedAt)/1e3));return t<60?`${t}s`:`${Math.floor(t/60)}m ${t%60}s`};function ot(e){let t=f(`details`,`rounded-lg border border-neutral-200`),n=f(`summary`,`flex cursor-pointer flex-wrap items-center gap-x-3 gap-y-1 px-3 py-2 text-[11.5px] text-neutral-500`,tt(e),f(`span`,``,b(e.queuedAt)),f(`span`,`text-neutral-400`,e.triggerSource),f(`span`,`ml-auto font-mono text-neutral-400`,at(e)));t.append(n);let r=f(`div`,`flex flex-col gap-2 border-t border-neutral-200 px-3 py-2`);return t.ontoggle=()=>{if(!(!t.open||r.childElementCount)){if(e.targetSessionId){let t=f(`a`,`text-[11.5px] text-indigo-600 hover:underline`,`Open the run's session`);t.href=`#/session/${encodeURIComponent(e.targetSessionId)}`,r.append(t)}r.append(f(`pre`,`max-h-80 overflow-auto whitespace-pre-wrap break-words font-mono text-[11.5px] leading-snug text-neutral-700`,it(e)))}},t.append(r),t}function st(){let e=f(`div`,`flex flex-col gap-2`),t=f(`span`,`text-[11.5px]`,``),n=new Set;async function i(e,n){let r=n?`resume`:`pause`,i=await k(`/api/tasks/${encodeURIComponent(e.id)}/${r}`,`POST`,`Could not ${r} ${e.name}`);i?y(t,`failed`,i):y(t,`saved`,`${e.name} ${n?`resumed`:`paused`}.`),await o()}function a(e){let t=f(`div`,`flex flex-col gap-1.5`),r=S(`Runs`),a=async()=>{t.replaceChildren(f(`span`,`text-[11.5px] text-neutral-400`,`loading…`));let n=await V(`/api/tasks/${encodeURIComponent(e.id)}/runs`,`Could not load runs`);if(!n.ok)return void t.replaceChildren(f(`span`,`text-[11.5px] text-red-600`,n.error));t.replaceChildren(...n.value.length?n.value.map(ot):[C(`No runs yet.`)])};r.onclick=()=>{if(n.delete(e.id))return void t.replaceChildren();n.add(e.id),a()},n.has(e.id)&&a();let o=[nt(e)];e.nextRunAt!==null&&o.push(`next ${b(e.nextRunAt)}`);let s=e.lastRun,c=f(`div`,`flex min-w-0 flex-col gap-1`,f(`span`,`flex min-w-0 flex-wrap items-center gap-2`,f(`span`,`truncate text-[12.5px] font-medium text-neutral-700`,e.name),...s?[tt(s),f(`span`,`text-[11px] text-neutral-400`,T(s.queuedAt))]:[]),f(`span`,`truncate font-mono text-[11.5px] text-neutral-400`,o.join(` · `)));e.description&&(c.title=e.description);let l=f(`div`,`flex flex-none items-center gap-3`);if(e.trigger.type!==`manual`){let t=x(``,``,e.enabled,t=>void i(e,t));t.title=e.enabled?`Enabled — click to pause`:`Paused — click to resume`,l.append(t)}return l.append(r),f(`div`,`flex flex-col gap-2 rounded-lg border border-neutral-200 px-3 py-2`,f(`div`,`flex items-center justify-between gap-3`,c,l),t)}async function o(){let t=await V(`/api/tasks`,`Could not load tasks`);if(!t.ok)return void e.replaceChildren(C(t.error));e.replaceChildren(...t.value.length?t.value.map(a):[C("No tasks yet. Ask an agent to schedule one — `pier task` saves it.")])}return{el:f(`div`,`mx-auto flex w-full min-w-0 max-w-3xl flex-col`,r(`Tasks`,"Saved tasks and their runs. A paused task keeps its definition and stops firing; defining or editing one is `pier task`'s job.",e,t)),show:()=>void o()}}var ct={auto:`bg-emerald-50 text-emerald-700 ring-emerald-200`,approve:`bg-amber-50 text-amber-700 ring-amber-200`},lt={auto:`auto = the agent may use this freely: any session, subagent or local process gets the value without asking.`,approve:`approve = every use pauses for a vt approval; the value never leaves vt's keeping.`},ut=/^[A-Z][A-Z0-9_]{0,63}$/;function dt(){let e=`auto`,t=f(`div`,`flex flex-col gap-2`),n=f(`span`,`text-[11.5px]`,``);function i(e){let t=S(`Remove`);t.onclick=()=>{window.confirm(`Remove ${e.name}? A command naming it fails until it is filed again.`)&&(t.disabled=!0,(async()=>{let r=await k(`/api/vault/${encodeURIComponent(e.name)}`,`DELETE`,`Could not remove it`);if(r)return t.disabled=!1,y(n,`failed`,r);y(n,`saved`,`Removed ${e.name}.`),await a()})())};let r=f(`div`,`flex min-w-0 flex-col gap-1`,f(`span`,`truncate font-mono text-[12.5px] text-neutral-700`,e.name),f(`span`,`flex items-center gap-1.5 text-[11.5px] text-neutral-400`,A(e.level,ct[e.level]),`updated ${u(e.updatedAt)}`));return f(`div`,`flex items-center justify-between gap-3 rounded-lg border border-neutral-200 px-3 py-2`,r,t)}async function a(){let e=await V(`/api/vault`,`Could not load the vault`);if(!e.ok)return void t.replaceChildren(C(e.error));t.replaceChildren(...e.value.length?e.value.map(i):[C(`No secrets yet. File one below; a skill names it by this name.`)])}let s=r(`Secrets`,"Named values `pier vault run` puts into one command's environment. The agent's context never holds them; a name it cannot find fails with a link back here. Channel tokens live here too (SLACK_TOKEN, LARK_APP_ID…): removing one empties that channel's credential.",t,n),c=_();c.classList.add(`font-mono`),c.placeholder=`SLACK_TOKEN`,c.autocomplete=`off`,c.spellcheck=!1;let l=_(``,`password`);l.placeholder=`paste the secret`,l.autocomplete=`new-password`;let d=f(`span`,`text-[11.5px] leading-snug text-neutral-400`,lt[e]),p=f(`div`,`flex flex-col gap-1.5`),m=f(`span`,`text-[11.5px]`,``),g=S(`File secret`,!0);function v(){d.textContent=lt[e],p.replaceChildren(h([[`auto`,`auto`],[`approve`,`approve`]],e,t=>{e=t,v()}),d)}async function b(){let t=c.value.trim();if(!ut.test(t))return y(m,`failed`,`Name must be A-Z, 0-9 and _, starting with a letter, like SLACK_TOKEN.`);if(!l.value)return y(m,`failed`,`Paste the secret first.`);g.disabled=!0,y(m,`saving`,e===`approve`?`filing… (vt creates the record; approve if it asks)`:`filing…`);let n=await H(`/api/vault/${encodeURIComponent(t)}`,{level:e,value:l.value},`PUT`);if(g.disabled=!1,!n.ok)return y(m,`failed`,await U(n,`Could not file it`));l.value=``,c.value=``,y(m,`saved`,`Filed ${t} (${e}). Rotate it by filing the same name again.`),await a()}g.onclick=()=>void b(),l.onkeydown=e=>{e.key===`Enter`&&b()};let x=r(`File a secret`,`Filing a name that exists replaces its value; commands already running keep the one they were given.`,o(`Name`,c,{hint:`An environment variable name. Skills name it by convention, e.g. SLACK_BOT_TOKEN=SLACK_TOKEN.`}),o(`Level`,p),o(`Value`,l,{hint:`Stored sealed (auto) or as a vt:// record (approve); shown to nobody afterwards.`}),f(`div`,`flex items-center gap-3`,g,m)),w=f(`div`,`mx-auto flex w-full min-w-0 max-w-3xl flex-col gap-6`,s,x);function T(e){v(),n.textContent=``,m.textContent=``,a();let t=new URLSearchParams(e??``).get(`name`);t&&ut.test(t)&&(c.value=t,l.focus(),x.scrollIntoView({block:`nearest`}))}return{el:w,show:T}}var ft=[[`models`,`Models`],[`files`,`Agent`],[`channels`,`Channels`],[`vault`,`Vault`],[`tasks`,`Tasks`],[`boards`,`Boards`],[`instance`,`Instance`],[`security`,`Security`]],pt=e=>ft.some(([t])=>t===e),mt=`pier.settingsTopic`;function ht(e){return[/\bEdg\//.test(e)?`Edge`:/\b(Firefox|Chrome|Safari)\/[\d.]+/.exec(e)?.[1],/\(([^;)]+)/.exec(e)?.[1]?.trim()].filter(Boolean).join(` · `)||e||`unknown client`}function gt(t,i,a,d,p){let m=localStorage.getItem(mt)??void 0,h=pt(m)?m:`models`,g=f(`header`,`pagehead`),v=n(`#version`),b=n(`#theme-toggle`);v.classList.add(`ml-auto`);let x=f(`button`,`icon-btn`,K(s));x.type=`button`,x.setAttribute(`aria-label`,`Close Settings`),x.onclick=p,ae(x,`Close Settings`,p,()=>!t.classList.contains(`hidden`));function w(){g.replaceChildren(L(`Settings`),f(`div`,`flex min-w-0 items-center gap-2 overflow-x-auto max-md:order-1 max-md:w-full`,...ft.map(([t,n])=>e(n,t===h,()=>a(t)))),v,b,x)}let T=_();T.placeholder=`https://pier.example.com`,T.autocomplete=`off`;let D=f(`span`,`text-[11.5px]`,``),O=S(`Save`,!0);async function k(){y(D,`saving`,`saving…`);let e=await H(`/api/settings`,{publicUrl:T.value},`PUT`);if(!e.ok)return y(D,`failed`,await U(e,`Could not save`));let{publicUrl:t}=await e.json();T.value=t,y(D,`saved`,t?`Saved — boards link as ${t}/p/<slug>-<token>/`:`Cleared.`)}O.onclick=()=>void k(),T.onkeydown=e=>{e.key===`Enter`&&k()};let ee=r(`Public URL`,`Where this Pier is reached from outside — the proxy's address, not the port it listens on.`,o(`Base URL`,T,{hint:`Agents read it to hand you a clickable board link instead of a path. Nothing here changes what Pier serves; leave it empty if Pier is only reachable locally.`}),f(`div`,`flex items-center gap-3`,O,D)),te=f(`div`,``),j=f(`span`,`text-[11.5px]`,``),M=e=>{e?document.documentElement.dataset.accent=e:delete document.documentElement.dataset.accent},N=``;function P(e){let[t=``]=Object.keys(e);te.replaceChildren(c(Object.entries(e),N||t,n=>{M(n===t?``:n),y(j,`saving`,`saving…`),(async()=>{let t=await H(`/api/settings`,{accent:n},`PUT`);if(!t.ok)return M(N),P(e),y(j,`failed`,await U(t,`Could not save`));N=(await t.json()).accent,M(N),y(j,`saved`,`Saved — the icon follows on the next load.`)})()}))}let F=r(`Accent`,`The workbench's colour and the installed app's icon — so two Piers on one machine are told apart at a glance.`,te,j),I=_(``,`password`),R=_(``,`password`),z=_(``,`password`);for(let e of[I,R,z])e.autocomplete=`off`;let B=f(`span`,`text-[11.5px]`,``),ne=S(`Change password`,!0);async function W(){if(R.value!==z.value)return y(B,`failed`,`The two new passwords differ.`);y(B,`saving`,`changing…`);let e=await H(`/api/password`,{current:I.value,next:R.value});if(!e.ok)return y(B,`failed`,await U(e,`Could not change the password`));for(let e of[I,R,z])e.value=``;y(B,`saved`,`Changed. Sign in again with the new password.`),location.href=`/login`}ne.onclick=()=>void W(),z.onkeydown=e=>{e.key===`Enter`&&W()};let G=r(`Password`,`One shared password guards every page and API route. Changing it signs out every signed-in browser, this one included.`,o(`Current password`,I),o(`New password`,R,{hint:`At least 10 characters.`}),o(`Repeat new password`,z),f(`div`,`flex items-center gap-3`,ne,B)),q=S(`Reload`,!0),J=f(`span`,`text-[11.5px]`,``);q.onclick=()=>{q.disabled=!0,y(J,`saving`,`reloading…`),(async()=>{let e=await H(`/api/reload`,{});if(q.disabled=!1,!e.ok)return y(J,`failed`,await U(e,`Could not reload`));let{recycled:t,busy:n}=await e.json(),r=t?`Recycled ${t} session(s).`:`No idle session needed it.`,i=n?` ${n} still mid-turn — they take it when the turn ends.`:``;y(J,n?`idle`:`saved`,r+i)})()};let Y=r(`Reload configuration`,`Re-reads channel configuration and lets go of idle sessions, so the next message opens them with the current agent files, skills and credentials. Saving here does this on its own — use it after something outside the Console changed a file.`,f(`div`,`flex items-center gap-3`,q,J)),ie=f(`div`,`flex flex-col gap-2`),X=f(`span`,`text-[11.5px]`,``);async function oe(e,t){y(X,`saving`,`signing out…`);try{let n=await H(e,{});if(n.ok)return!0;y(X,`failed`,await U(n,t))}catch(e){y(X,`failed`,`${t}: ${String(e)}`)}return!1}let Z=S(`Sign out`);Z.onclick=()=>{Z.disabled=!0,oe(`/logout`,`Could not sign out`).then(e=>{e?location.href=`/login`:Z.disabled=!1})};function ce(e){ie.replaceChildren(...e.map(e=>{let t=S(e.current?`This browser`:`Sign out`);return t.disabled=e.current,t.onclick=()=>{t.disabled=!0,oe(`/api/devices/${encodeURIComponent(e.id)}/signout`,`Could not sign it out`).then(e=>{if(!e)return void(t.disabled=!1);y(X,`saved`,`Signed out.`),le()})},re(ht(e.agent),`${e.ip} · seen ${u(e.seenAt)} · signed in ${u(e.createdAt)}`,t,{title:e.agent})}))}async function le(){let e=await V(`/api/devices`,`Could not load signed-in devices`);if(!e.ok){ie.replaceChildren(C(e.error));return}ce(e.value)}let ue=r(`Signed-in devices`,`One row per browser holding a session. Signing one out invalidates its cookie immediately; sessions expire on their own 7 days after last use.`,ie,f(`div`,`flex items-center gap-3`,Z,X)),de=se(),fe=f(`div`,`mx-auto flex max-w-2xl flex-col gap-6`,ee,F,de.el,Y),Q=e=>f(`div`,`hidden min-h-0 flex-1 overflow-y-auto`,f(`div`,`px-4 pb-5 pt-1`,e)),pe=He(),me=f(`div`,`mx-auto flex w-full min-w-0 max-w-3xl flex-col ${E}`),he=f(`section`,`hidden min-h-0 flex-1 flex-col`),ge=Ce(he),_e=dt(),ve=f(`section`,`hidden min-h-0 flex-1 flex-col`),ye=Re(ve,i,d);function be(){de.load(),(async()=>{let e=await V(`/api/settings`,`Could not load settings`);if(!e.ok)return y(D,`failed`,e.error);T.value=e.value.publicUrl,D.textContent=``,N=e.value.accent,P(e.value.accents),j.textContent=``})()}let xe=Ge(),Se=f(`div`,`mx-auto flex max-w-2xl flex-col gap-6`),we=e=>Se.replaceChildren(e,G,xe.el,ue);async function Te(){B.textContent=``,X.textContent=``,le(),xe.load().then(e=>G.classList.toggle(`hidden`,e));let e=await V(`/api/secrets`,`Could not load key status`);if(!e.ok){we(C(e.error));return}Ee(e.value)}function Ee(e,t){let n=e.state===`locked`,i=f(`span`,`text-[11.5px]`,``);t&&y(i,`saved`,t);let a=f(`div`,`flex items-center gap-2`,A(e.state,n?`bg-red-50 text-red-700 ring-red-200`:`bg-emerald-50 text-emerald-700 ring-emerald-200`),f(`span`,`text-[12.5px] text-neutral-600`,n?`Stored credentials cannot be read until the key is unlocked.`:e.mode===`vt`?`Key mode: vt:// — unlocking takes one vt approval per process start.`:`Key mode: file — the key sits raw in master.key; switch to vt:// to require an approval.`)),o=f(`a`,`text-indigo-600 hover:underline`,`vt`);o.href=`https://github.com/timqi/vt`,o.target=`_blank`,o.rel=`noreferrer`;let s=[a,f(`p`,`text-[12px] leading-snug text-neutral-500`,o,` is a small local KMS: it keeps the key wrapped and releases it only on an explicit approval — Touch ID on a Mac, a phone passkey on a headless host. Pier only stores the vt:// record, so master.key alone unlocks nothing.`)];if(n){s.push(f(`p`,`text-[12.5px] leading-snug text-red-600`,e.reason??`No reason reported.`));let t=S(`Unlock`,!0);t.onclick=()=>{y(i,`saving`,`unlocking… (vt mode waits on your approval)`),(async()=>{let e=await H(`/api/secrets/unlock`,{});if(!e.ok)return y(i,`failed`,await U(e,`Could not unlock`));Ee(await e.json(),`Unlocked — channels are starting.`)})()},s.push(f(`div`,`flex items-center gap-3`,t,i))}else{let t=async(e,t,n)=>{if(!window.confirm(t))return;y(i,`saving`,e===`vt`?`rotating… (vt asks for an approval)`:`rotating…`);let r=await H(`/api/secrets/rotate`,{mode:e});if(!r.ok)return y(i,`failed`,await U(r,`Could not rotate`));Ee(await r.json(),n)},n=e.mode??`file`,r=S(`Rotate key`,!0);r.onclick=()=>void t(n,`Generate a new master key? Stored credentials stay valid; the old key stops working.`,`Rotated.`);let a=n===`vt`?`file`:`vt`,o=S(a===`vt`?`Switch to vt://`:`Switch to file`);o.onclick=()=>void t(a,a===`vt`?`Move the master key into vt? Every process start will then wait on a vt approval.`:`Store the master key raw in master.key? Anyone who can read the file can read every credential.`,a===`vt`?`Switched to vt:// mode.`:`Switched to file mode.`),s.push(f(`div`,`flex items-center gap-3`,r,o,i))}if(n||e.mode===`vt`){let e=f(`pre`,`hidden overflow-x-auto rounded-lg bg-neutral-50 p-3 text-[11.5px] leading-snug text-neutral-700`),t=S(`vt doctor`);t.onclick=()=>{t.disabled=!0,e.classList.remove(`hidden`),e.textContent=`running vt doctor…`,(async()=>{let n=await V(`/api/secrets/doctor`,`Could not run vt doctor`);e.textContent=n.ok?n.value.report:n.error,t.disabled=!1})()},s.push(f(`div`,`flex items-center gap-3`,t),e)}we(r(`Master key`,`Seals every credential Pier stores (channel tokens, API keys). Rotating rewraps the key file; no stored data is rewritten.`,...s))}let De=Q(fe),Oe=Q(f(`div`,`flex flex-col gap-6`,me,pe.el)),ke=Q(Se),Ae=Q(_e.el),je=st(),Me=Q(je.el),Ne=$(),Pe=Q(Ne.el),Fe=[[`instance`,De,be],[`models`,Oe,()=>{Ze(me),pe.load()}],[`vault`,Ae,e=>_e.show(e)],[`tasks`,Me,je.show],[`boards`,Pe,Ne.show],[`security`,ke,()=>void Te()]];t.append(g,De,Oe,he,Ae,Me,Pe,ve,ke);function Ie(e,t){pt(e)&&(h=e),localStorage.setItem(mt,h),w();for(let[e,n,r]of Fe){let i=e===h;n.classList.toggle(`hidden`,!i),i&&r(t)}h===`channels`?ge.show():ge.hide(),h===`files`?ye.show():ye.hide()}return l(t,Ie)}export{gt as createSettingsView};
|
|
Binary file
|
|
Binary file
|
|
@@ -38,11 +38,10 @@
|
|
|
38
38
|
which style.css pays for with the safe-area inset. -->
|
|
39
39
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
40
40
|
<title>Pier</title>
|
|
41
|
-
<script type="module" crossorigin src="/app/assets/index-
|
|
42
|
-
<link rel="modulepreload" crossorigin href="/app/assets/highlight-
|
|
41
|
+
<script type="module" crossorigin src="/app/assets/index-D0ZYdweB.js"></script>
|
|
42
|
+
<link rel="modulepreload" crossorigin href="/app/assets/highlight-CDSk2hRJ.js">
|
|
43
43
|
<link rel="modulepreload" crossorigin href="/app/assets/hljs-BRz6a8Dq.js">
|
|
44
|
-
<link rel="
|
|
45
|
-
<link rel="stylesheet" crossorigin href="/app/assets/index-wAGxFFPj.css">
|
|
44
|
+
<link rel="stylesheet" crossorigin href="/app/assets/index-JN0Ic8K5.css">
|
|
46
45
|
</head>
|
|
47
46
|
<!-- The document never scrolls: this is a fixed-viewport workbench, and every
|
|
48
47
|
scrollable region is an inner pane. h-dvh, not h-screen, because 100vh can
|
|
Binary file
|
|
Binary file
|
package/dist/web/push.js
CHANGED
|
@@ -37,8 +37,8 @@ export class PushStore {
|
|
|
37
37
|
return { ...row, privateKey: this.#unsealed(row.privateKey) };
|
|
38
38
|
const keys = generateVapidKeys();
|
|
39
39
|
this.#db
|
|
40
|
-
.prepare("INSERT INTO push_identity(id, public_key, private_key
|
|
41
|
-
.run(keys.publicKey, this.#sealed(keys.privateKey)
|
|
40
|
+
.prepare("INSERT INTO push_identity(id, public_key, private_key) VALUES (1, ?, ?)")
|
|
41
|
+
.run(keys.publicKey, this.#sealed(keys.privateKey));
|
|
42
42
|
log.info("minted this instance's VAPID key pair");
|
|
43
43
|
return keys;
|
|
44
44
|
}
|
package/dist/web/server.js
CHANGED
|
@@ -358,10 +358,8 @@ export function createServer({ factory, router, hub, sessions: state, config, pa
|
|
|
358
358
|
};
|
|
359
359
|
// The chain, newest first; the client pages back through it with /history.
|
|
360
360
|
app.get("/api/continuous", (c) => c.json({ chain: continuous.members(), rotateAt: CHAIN_FULL_TOKENS }));
|
|
361
|
-
// The
|
|
362
|
-
app.get("/api/continuous/open", (c) => c.json(openItems?.() ?? { items: [], unlisted: []
|
|
363
|
-
// ⋯ → Status: `/status`'s answer, without posting it.
|
|
364
|
-
app.get("/api/continuous/status", (c) => c.json(continuous.status()));
|
|
361
|
+
// The status panel's rows.
|
|
362
|
+
app.get("/api/continuous/open", (c) => c.json(openItems?.() ?? { items: [], unlisted: [] }));
|
|
365
363
|
// The alias send: the head is resolved (and rotated) here, so a rotation
|
|
366
364
|
// between the client's snapshot and its send cannot land on an old head.
|
|
367
365
|
guarded(app, "POST", "/api/continuous/messages", 400, async (c) => {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// What the workbench decided about a session, and nothing a transcript already
|
|
2
|
-
// knows: a finished turn nobody looked at (`unread`).
|
|
3
|
-
// are columns nothing reads.
|
|
2
|
+
// knows: a finished turn nobody looked at (`unread`).
|
|
4
3
|
import { pierDb } from "../db.js";
|
|
5
4
|
export class SessionStateStore {
|
|
6
5
|
#db;
|
package/package.json
CHANGED
|
@@ -37,6 +37,8 @@ truth.
|
|
|
37
37
|
status chip opens the status panel — what is running over the open items;
|
|
38
38
|
the rest is in ⌘K. Its current session, the head, runs in `$PIER_HOME/home`,
|
|
39
39
|
which holds memory only: `MEMORY.md` and daily notes in `memory/`.
|
|
40
|
+
- Recall: `rg` over `memory/`; `pier search <words>` over every earlier
|
|
41
|
+
session's transcript.
|
|
40
42
|
- After an idle hour, or past 60K tokens, the next message starts a fresh
|
|
41
43
|
session, seeded with `$PIER_HOME/home`'s memory, the open items, its runs
|
|
42
44
|
and the last exchanges.
|
|
@@ -94,7 +96,10 @@ truth.
|
|
|
94
96
|
Every message there, top-level or in a thread, reaches Pier; its replies,
|
|
95
97
|
callbacks, seeds and command answers render in the DM's main flow.
|
|
96
98
|
`/settings` and `/s <text>` are prose there; while that adapter is live the
|
|
97
|
-
head sends no Web Push. Groups stay thread-per-session.
|
|
99
|
+
head sends no Web Push. Groups stay thread-per-session. A design lead that
|
|
100
|
+
waits for the user gets a thread of the home DM bound to its session, under
|
|
101
|
+
a `▷ <name> · design — waiting for you` root that the head edits to `✓ … design
|
|
102
|
+
final` or `⚠ … <error>`; that thread is the lead's, not Pier's conversation.
|
|
98
103
|
- Elsewhere `/settings` opens a panel. In a thread with no session yet it is a
|
|
99
104
|
draft: directory, model & reasoning (the operator's pinned models, one pick
|
|
100
105
|
sets both), and Start creates the session. `/s <text>` as a thread's first
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pier-search
|
|
3
|
+
description: Earlier sessions by what was said in them with `pier search` — every user message and reply Pier has, across all sessions. Read before looking for something said in an earlier session that memory does not hold.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Earlier sessions from the shell
|
|
7
|
+
|
|
8
|
+
For something said in an earlier session — a decision, an error, a name —
|
|
9
|
+
that `MEMORY.md` and the daily notes do not hold. `memory/` is `rg`'s, not
|
|
10
|
+
this; tool steps are never indexed, only messages and replies.
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
pier search the parser regression --limit 5
|
|
14
|
+
pier search 部署 --json
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- The words are the query: every one must appear in the same message, in
|
|
18
|
+
any order. `--limit` 1–50, default 20; at most one hit per session, best
|
|
19
|
+
first.
|
|
20
|
+
- One line per hit:
|
|
21
|
+
`<sessionId> · <title> · <role> · <YYYY-MM-DD HH:MM>: <snippet>`; the
|
|
22
|
+
title is cut to 30 characters with `…`, the snippet is the message around
|
|
23
|
+
the match with Pier's chat markup off (`<open>`, `<done>`, `<silent>`, the
|
|
24
|
+
next-step buttons); `--json` prints `{hits}` instead, the title whole. Nothing found is `no hits`, exit 0; a
|
|
25
|
+
refusal is one `search:` line, exit 1.
|
|
26
|
+
- A hit names a session: the web opens it at `/app/#/session/<id>`; link
|
|
27
|
+
that for the user rather than pasting the transcript.
|
|
@@ -96,6 +96,16 @@ every flag. Results reach Pier;
|
|
|
96
96
|
`--callback-session <id>` pins a session, `--callback-session none` silences.
|
|
97
97
|
`pier task list` shows definitions with `nextRun` and `lastRun`, never runs.
|
|
98
98
|
|
|
99
|
+
```sh
|
|
100
|
+
pier task save --name deploy-done --watch "test -f /repo/DONE" --every 60 --prompt "Summarize /repo/DONE."
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`--watch <script> --every <s>` (≥ 5, in `--cwd`) runs the script each interval:
|
|
104
|
+
exit 0 matched → the action runs; exit 1 → nothing, no callback; any other exit
|
|
105
|
+
fails the run. The action does not see the script's output. Without
|
|
106
|
+
`--repeat` the first match pauses the definition; with it, every match runs.
|
|
107
|
+
`--watch` and `--cron` are exclusive.
|
|
108
|
+
|
|
99
109
|
| Command | Does |
|
|
100
110
|
| --- | --- |
|
|
101
111
|
| `pier task pause --task-id <id>` | schedule off; still runs on demand |
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|