@timqi/pier 0.0.25 → 0.0.27

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.
Files changed (60) hide show
  1. package/dist/agent/pi.js +16 -16
  2. package/dist/channels/attach.js +6 -3
  3. package/dist/channels/lark-outbound.js +6 -1
  4. package/dist/channels/lark.js +16 -4
  5. package/dist/channels/slack-api.js +8 -0
  6. package/dist/channels/slack-outbound.js +14 -5
  7. package/dist/channels/slack-tool.js +83 -4
  8. package/dist/channels/slack.js +156 -8
  9. package/dist/channels/telegram.js +15 -4
  10. package/dist/core/inbound-file.js +69 -3
  11. package/dist/core/reply.js +17 -5
  12. package/dist/core/router.js +34 -1
  13. package/dist/web/fs.js +6 -22
  14. package/dist/web/public/assets/{activity-xRauUtIM.js → activity-B44nbhXy.js} +1 -1
  15. package/dist/web/public/assets/activity-B44nbhXy.js.br +0 -0
  16. package/dist/web/public/assets/activity-B44nbhXy.js.gz +0 -0
  17. package/dist/web/public/assets/{boards-CxXk1JfX.js → boards-CeiKKBaY.js} +1 -1
  18. package/dist/web/public/assets/boards-CeiKKBaY.js.br +0 -0
  19. package/dist/web/public/assets/boards-CeiKKBaY.js.gz +0 -0
  20. package/dist/web/public/assets/{code-pUOkepAa.js → code-I0lxXuvE.js} +1 -1
  21. package/dist/web/public/assets/{explorer-C1KiLceA.js → explorer-BOeDogsY.js} +1 -1
  22. package/dist/web/public/assets/explorer-BOeDogsY.js.br +0 -0
  23. package/dist/web/public/assets/explorer-BOeDogsY.js.gz +0 -0
  24. package/dist/web/public/assets/{index-D9euDszY.js → index-BmKvoulH.js} +14 -13
  25. package/dist/web/public/assets/index-BmKvoulH.js.br +0 -0
  26. package/dist/web/public/assets/index-BmKvoulH.js.gz +0 -0
  27. package/dist/web/public/assets/{runs-BuE-mags.js → runs-C4OlQT7W.js} +1 -1
  28. package/dist/web/public/assets/runs-C4OlQT7W.js.br +0 -0
  29. package/dist/web/public/assets/runs-C4OlQT7W.js.gz +0 -0
  30. package/dist/web/public/assets/{settings-5KvCAMBF.js → settings-Cn8i_QiP.js} +1 -1
  31. package/dist/web/public/assets/settings-Cn8i_QiP.js.br +0 -0
  32. package/dist/web/public/assets/settings-Cn8i_QiP.js.gz +0 -0
  33. package/dist/web/public/assets/{task-runs-DIlw1pXt.js → task-runs-BW06ZEVz.js} +1 -1
  34. package/dist/web/public/assets/task-runs-BW06ZEVz.js.br +0 -0
  35. package/dist/web/public/assets/task-runs-BW06ZEVz.js.gz +0 -0
  36. package/dist/web/public/assets/{tasks-CcAxbMR8.js → tasks-DZzYlKsd.js} +1 -1
  37. package/dist/web/public/assets/tasks-DZzYlKsd.js.br +0 -0
  38. package/dist/web/public/assets/tasks-DZzYlKsd.js.gz +0 -0
  39. package/dist/web/public/index.html +1 -1
  40. package/dist/web/public/index.html.br +0 -0
  41. package/dist/web/public/index.html.gz +0 -0
  42. package/dist/web/server.js +11 -11
  43. package/package.json +1 -1
  44. package/skills/pier-slack/SKILL.md +61 -133
  45. package/dist/web/public/assets/activity-xRauUtIM.js.br +0 -0
  46. package/dist/web/public/assets/activity-xRauUtIM.js.gz +0 -0
  47. package/dist/web/public/assets/boards-CxXk1JfX.js.br +0 -0
  48. package/dist/web/public/assets/boards-CxXk1JfX.js.gz +0 -0
  49. package/dist/web/public/assets/explorer-C1KiLceA.js.br +0 -0
  50. package/dist/web/public/assets/explorer-C1KiLceA.js.gz +0 -0
  51. package/dist/web/public/assets/index-D9euDszY.js.br +0 -0
  52. package/dist/web/public/assets/index-D9euDszY.js.gz +0 -0
  53. package/dist/web/public/assets/runs-BuE-mags.js.br +0 -0
  54. package/dist/web/public/assets/runs-BuE-mags.js.gz +0 -0
  55. package/dist/web/public/assets/settings-5KvCAMBF.js.br +0 -0
  56. package/dist/web/public/assets/settings-5KvCAMBF.js.gz +0 -0
  57. package/dist/web/public/assets/task-runs-DIlw1pXt.js.br +0 -0
  58. package/dist/web/public/assets/task-runs-DIlw1pXt.js.gz +0 -0
  59. package/dist/web/public/assets/tasks-CcAxbMR8.js.br +0 -0
  60. package/dist/web/public/assets/tasks-CcAxbMR8.js.gz +0 -0
@@ -1,6 +1,7 @@
1
- // The inbound-file convention, browser-safe: what a user's attachment may be
1
+ // The file-link convention, browser-safe: what a user's attachment may be
2
2
  // called, the marker line that tells the agent about it, the parser that
3
- // splits it back out of a message, and the size cap both ends enforce.
3
+ // splits it back out of a message, the size cap both ends enforce, and where a
4
+ // `file://` link does not count as one at all (inside code).
4
5
  // Producers are node code (channels/, web/server.ts) but the web composer
5
6
  // builds markers and the web chat parses them in the browser, so the grammar
6
7
  // lives in a module with no node imports that either side can load. The
@@ -46,13 +47,78 @@ export const fileMarker = (path) => `[${path.split("/").pop() ?? "file"}](file:/
46
47
  * could not upload (channels/attach.ts) are the same fact to the reader.
47
48
  */
48
49
  export const lostMarker = (name, reason) => `[attachment lost: ${name} — ${reason}]`;
50
+ /** A fenced block's opening or closing line. */
51
+ const FENCE_RE = /^ {0,3}(`{3,}|~{3,})/;
52
+ /**
53
+ * The character ranges of `text` that are code: fenced blocks (fence lines
54
+ * included) and inline spans. Inline spans are paired within a line — one that
55
+ * wraps across a newline is legal markdown and not how anyone writes an
56
+ * example link, and per-line pairing keeps this a scan instead of a parser.
57
+ */
58
+ function codeRanges(text) {
59
+ const ranges = [];
60
+ let fence;
61
+ let at = 0;
62
+ for (const line of text.split("\n")) {
63
+ const run = FENCE_RE.exec(line)?.[1];
64
+ if (fence !== undefined) {
65
+ ranges.push([at, at + line.length]);
66
+ if (run && run[0] === fence[0] && run.length >= fence.length)
67
+ fence = undefined;
68
+ }
69
+ else if (run) {
70
+ fence = run;
71
+ ranges.push([at, at + line.length]);
72
+ }
73
+ else {
74
+ // A span closes on the next backtick run of the same length; runs in
75
+ // between are content, so an unpaired opener leaves the rest as prose.
76
+ const runs = [...line.matchAll(/`+/g)];
77
+ for (let i = 0; i < runs.length; i++) {
78
+ const open = runs[i];
79
+ let close = i + 1;
80
+ while (close < runs.length && runs[close][0].length !== open[0].length)
81
+ close++;
82
+ if (close === runs.length)
83
+ break;
84
+ ranges.push([at + open.index, at + runs[close].index + runs[close][0].length]);
85
+ i = close;
86
+ }
87
+ }
88
+ at += line.length + 1; // the newline split() removed
89
+ }
90
+ return ranges;
91
+ }
92
+ /**
93
+ * `text.replace(pattern, …)` for every match that is not inside code. An agent
94
+ * that documents this convention writes an example link in backticks, and a
95
+ * scanner that cannot tell an example from a link turned that example into a
96
+ * real attachment — a dead card in the chat, a lost-attachment line in Slack.
97
+ * Matches inside code are left byte-identical: the reader asked to see them.
98
+ */
99
+ export function replaceOutsideCode(text, pattern, replace) {
100
+ const skip = codeRanges(text);
101
+ let out = "";
102
+ let last = 0;
103
+ for (const match of text.matchAll(pattern)) {
104
+ const start = match.index;
105
+ const end = start + match[0].length;
106
+ if (skip.some(([from, to]) => start < to && end > from))
107
+ continue;
108
+ out += text.slice(last, start) + replace(match);
109
+ last = end;
110
+ }
111
+ return out + text.slice(last);
112
+ }
49
113
  // A whole line that is one `[name](file:///…)` link — what fileMarker emits.
50
114
  const MARKER_RE = /^\[[^\]\n]*\]\(\s*<?file:\/\/(\/[^)>\s]*)>?\s*\)$/;
51
115
  /**
52
116
  * Split a user message into its typed text and the attached files' paths.
53
117
  * Only the contiguous *trailing* block of marker lines is an attachment —
54
118
  * that is where every producer puts them — so a `file://` link the user
55
- * wrote mid-message stays message text.
119
+ * wrote mid-message stays message text. No code scan needed for the same
120
+ * reason: an example in a fence sits under its closing line, which is not a
121
+ * marker line, so the walk stops there before ever reaching it.
56
122
  */
57
123
  export function splitInboundFiles(raw) {
58
124
  const lines = raw.split("\n");
@@ -24,11 +24,11 @@ conventions:
24
24
  for anything destructive.
25
25
  - **Attachments** — link a file you produced by absolute \`file://\` URL:
26
26
  \`[report.md](file:///abs/path/report.md)\`. Images render as thumbnails,
27
- other files as a download card; only files inside the session's working
28
- directory or Pier's inbox are readable. The same convention runs inbound: a
29
- user message ending in \`[name](file:///…)\` lines is carrying files the
30
- sender attached, already saved to disk — read one only when it matters to
31
- the task; every read puts its content in your context for good.
27
+ other files as a download card, wherever on disk you wrote it. The same
28
+ convention runs inbound: a user message ending in \`[name](file:///…)\`
29
+ lines is carrying files the sender attached, already saved to disk — read
30
+ one only when it matters to the task; every read puts its content in your
31
+ context for good.
32
32
  - **Staying silent** — \`<silent>why</silent>\` is stripped, and if nothing else
33
33
  remains no message is sent. In a group chat you are handed every message,
34
34
  including humans talking to each other: stay silent rather than acknowledge
@@ -94,6 +94,18 @@ export function originLabel(origin) {
94
94
  };
95
95
  return `from a subagent \u00b7 ${kinds[origin.messageKind] ?? origin.messageKind}`;
96
96
  }
97
+ /**
98
+ * Is a turn coming, once this note is posted?
99
+ *
100
+ * A system input is context for the turn it triggers, so on IM the note is the
101
+ * only message that turn has to wear the 👀 — nobody typed one. An error note
102
+ * is the opposite: it reports a turn that already ended or never began, and a
103
+ * receipt marked on it would hang until the stale sweep.
104
+ *
105
+ * Beside `originLabel` for the same reason it is: three adapters ask this, and
106
+ * three copies of the answer are three chances to drift.
107
+ */
108
+ export const awaitsTurn = (origin) => origin.kind !== "error";
97
109
  /**
98
110
  * Quote and bracket characters that may be lifted out of a `**strong**` run.
99
111
  * Moving them changes nothing a reader can see — the punctuation is simply no
@@ -15,6 +15,38 @@ const IDLE_TTL_MS = 30 * 60_000;
15
15
  const SWEEP_MS = 5 * 60_000;
16
16
  /** An error goes into a chat window, so it is trimmed to something readable. */
17
17
  const truncate = (message) => message.length > 600 ? `${message.slice(0, 600)}…` : message;
18
+ /** What a chat window gets of a system input, and how much it may stand in
19
+ * for. A task callback carries up to 8000 characters of result text
20
+ * (tasks/callbacks.ts). */
21
+ const NOTE_CHARS = 200;
22
+ const NOTE_LINES = 4;
23
+ /**
24
+ * The head of a system input plus a count of what was left out.
25
+ *
26
+ * A note is *context* for the turn it precedes, not the message: pasted whole,
27
+ * a run result buries the conversation it was meant to explain — and on IM
28
+ * that is the one surface with no way to collapse it. The hub, the web
29
+ * timeline and the Pi transcript keep every character.
30
+ *
31
+ * Cut on a line boundary and then on a word, so the head reads as text rather
32
+ * than as a string that ran out.
33
+ */
34
+ function digest(text) {
35
+ const body = text.trimEnd();
36
+ let head = body.split("\n").slice(0, NOTE_LINES).join("\n");
37
+ if (head.length > NOTE_CHARS) {
38
+ const capped = head.slice(0, NOTE_CHARS);
39
+ // Only a boundary in the second half is worth taking: cutting further back
40
+ // than that loses more than the ragged edge was costing.
41
+ const boundary = Math.max(capped.lastIndexOf("\n"), capped.lastIndexOf(" "));
42
+ head = capped.slice(0, boundary > NOTE_CHARS / 2 ? boundary : NOTE_CHARS);
43
+ }
44
+ const rest = body.slice(head.length).trim();
45
+ if (!rest)
46
+ return body;
47
+ const dropped = rest.split("\n").length;
48
+ return `${head.trimEnd()}\n… +${String(dropped)} more line${dropped === 1 ? "" : "s"}`;
49
+ }
18
50
  function keyOf(key) {
19
51
  return `${key.channelId}:${key.conversationId}`;
20
52
  }
@@ -225,7 +257,8 @@ export class Router {
225
257
  // out before the turn it triggers, so the answer has a visible cause.
226
258
  if (payload.type === "system-input") {
227
259
  const channel = this.channels.get(key.channelId);
228
- channel?.notify(key.conversationId, { text: payload.text, origin: payload.origin })
260
+ // A digest, not the input: the hub emit above is what carries it whole.
261
+ channel?.notify(key.conversationId, { text: digest(payload.text), origin: payload.origin })
229
262
  .catch((err) => {
230
263
  log.error(`notify ${key.channelId} failed`, err);
231
264
  this.hub.emit(session.id, {
package/dist/web/fs.js CHANGED
@@ -2,12 +2,13 @@
2
2
  // routes and the session attachments server.ts serves all ask the same
3
3
  // question, and asked it several different ways until this file existed.
4
4
  //
5
- // A `root` is any directory the process can read — sessions work in worktrees
6
- // and siblings of their cwd, and an owner already past the Console password
7
- // can reach those paths anyway. What is confined is `path`: it resolves under
5
+ // The boundary is the Console password, not a path: an owner past it picks a
6
+ // session's cwd from anywhere under `$HOME`, so anything this process can read
7
+ // is already reachable — which is why the session attachments route (server.ts)
8
+ // takes any absolute file. What `root` confines is a *listing*, so a browse
9
+ // cannot widen itself past the tree the view asked for: `path` resolves under
8
10
  // the `root` it was asked with, realpath on both ends, so neither `..` nor a
9
- // symlink steps outside and a listing can never widen itself. Only mkdir
10
- // writes, and only a name.
11
+ // symlink steps outside. Only mkdir writes, and only a name.
11
12
  import { mkdir, open, readdir, realpath, stat } from "node:fs/promises";
12
13
  import { homedir } from "node:os";
13
14
  import { basename, dirname, extname, isAbsolute, resolve, sep } from "node:path";
@@ -46,23 +47,6 @@ export async function scoped(root, path = "") {
46
47
  throw new Error("path escapes root");
47
48
  return target;
48
49
  }
49
- /** The same containment against several roots, for the caller that has more
50
- * than one: a session's attachments come from its own cwd or from the inbox
51
- * its inbound files landed in. null rather than a throw, because to that
52
- * caller "outside every root", "not a file" and "gone" are one answer. */
53
- export async function scopedFile(roots, path) {
54
- for (const root of roots) {
55
- try {
56
- const file = await scoped(root, path);
57
- if ((await stat(file)).isFile())
58
- return file;
59
- }
60
- catch {
61
- /* outside this root, unreadable, or gone — try the next */
62
- }
63
- }
64
- return null;
65
- }
66
50
  /** How bytes leave: the type they may be served as, and whether a browser may
67
51
  * render them in place. `download` forces the attachment disposition — the
68
52
  * chat's Download button, on a file it would otherwise show inline. */
@@ -1,4 +1,4 @@
1
- import{B as e,C as t,H as n,I as r,K as i,L as a,N as o,k as s,m as c,v as l,z as u}from"./index-D9euDszY.js";var d=(e,t={})=>{let n=document.createElementNS(`http://www.w3.org/2000/svg`,e);for(let[e,r]of Object.entries(t))n.setAttribute(e,r);return n},f=e=>e===null?`-`:r(Date.now()-e),p={light:{edge:`#a3a3a3`,callback:`#0891b2`,message:`#d97706`,scheduler:{fill:`#f5f3ff`,stroke:`#a78bfa`,text:`#5b21b6`},console:{fill:`#eff6ff`,stroke:`#93c5fd`,text:`#1d4ed8`},task:{fill:`#fffbeb`,stroke:`#fbbf24`,text:`#92400e`},process:{fill:`#f5f5f5`,stroke:`#a3a3a3`,text:`#525252`,dash:`4 3`},streaming:{fill:`#ffffff`,stroke:`#10b981`,text:`#262626`},idle:{fill:`#fafafa`,stroke:`#d4d4d4`,text:`#737373`}},dark:{edge:`#7a7a7a`,callback:`#3fc3dd`,message:`#e0a13a`,scheduler:{fill:`#2b2440`,stroke:`#7d63c9`,text:`#c9b8f5`},console:{fill:`#1f2a3d`,stroke:`#4a7fbf`,text:`#a8c8f0`},task:{fill:`#3a2f1c`,stroke:`#b8862a`,text:`#f0d49a`},process:{fill:`#262626`,stroke:`#5c5c5c`,text:`#a3a3a3`,dash:`4 3`},streaming:{fill:`#22302a`,stroke:`#10b981`,text:`#e5e5e5`},idle:{fill:`#212121`,stroke:`#4a4a4a`,text:`#9a9a9a`}}},m=()=>p[document.documentElement.dataset.theme===`dark`?`dark`:`light`];function h(r,p,h){let g=`sessions`,_=`active`,v={sessions:[],runs:[],messages:[]},y=``,b=e(async()=>{let e=_,t=await n(`/api/activity?scope=${e}`,`Failed to load activity`);if(!t.ok){y=``,r.replaceChildren(a(`p`,`p-4 text-[13px] text-red-600`,t.error));return}if(e!==_)return;let o=JSON.stringify(t.value);if(`${e}:${o}`===y)return;y=`${e}:${o}`;let s=t.value;v={...s,sessions:s.sessions.map(e=>({...e,title:i(e.title)}))},x()});function x(){let e=s(t([[`Sessions`,`sessions`],[`Relationships`,`dependencies`]],g,e=>{g=e,x()}),a(`span`,`ml-auto text-[11.5px] text-neutral-400`,`${v.sessions.length} sessions · ${v.runs.length} runs`),t([[`Current`,`active`],[`Last 24h`,`recent`]],_,e=>{_=e,x(),b()})),n=a(`div`,`min-h-0 flex-1 overflow-auto`);g===`sessions`?S(n):w(n),r.replaceChildren(e,n)}function S(e){let t=document.createElement(`table`);t.className=`w-full table-fixed text-left text-[12.5px]`,t.innerHTML=`<thead class="sticky top-0 bg-neutral-50 text-[10.5px] uppercase tracking-wide text-neutral-400 shadow-[inset_0_-1px_0_var(--color-neutral-200)]"><tr>
1
+ import{B as e,C as t,H as n,I as r,K as i,L as a,N as o,k as s,m as c,v as l,z as u}from"./index-BmKvoulH.js";var d=(e,t={})=>{let n=document.createElementNS(`http://www.w3.org/2000/svg`,e);for(let[e,r]of Object.entries(t))n.setAttribute(e,r);return n},f=e=>e===null?`-`:r(Date.now()-e),p={light:{edge:`#a3a3a3`,callback:`#0891b2`,message:`#d97706`,scheduler:{fill:`#f5f3ff`,stroke:`#a78bfa`,text:`#5b21b6`},console:{fill:`#eff6ff`,stroke:`#93c5fd`,text:`#1d4ed8`},task:{fill:`#fffbeb`,stroke:`#fbbf24`,text:`#92400e`},process:{fill:`#f5f5f5`,stroke:`#a3a3a3`,text:`#525252`,dash:`4 3`},streaming:{fill:`#ffffff`,stroke:`#10b981`,text:`#262626`},idle:{fill:`#fafafa`,stroke:`#d4d4d4`,text:`#737373`}},dark:{edge:`#7a7a7a`,callback:`#3fc3dd`,message:`#e0a13a`,scheduler:{fill:`#2b2440`,stroke:`#7d63c9`,text:`#c9b8f5`},console:{fill:`#1f2a3d`,stroke:`#4a7fbf`,text:`#a8c8f0`},task:{fill:`#3a2f1c`,stroke:`#b8862a`,text:`#f0d49a`},process:{fill:`#262626`,stroke:`#5c5c5c`,text:`#a3a3a3`,dash:`4 3`},streaming:{fill:`#22302a`,stroke:`#10b981`,text:`#e5e5e5`},idle:{fill:`#212121`,stroke:`#4a4a4a`,text:`#9a9a9a`}}},m=()=>p[document.documentElement.dataset.theme===`dark`?`dark`:`light`];function h(r,p,h){let g=`sessions`,_=`active`,v={sessions:[],runs:[],messages:[]},y=``,b=e(async()=>{let e=_,t=await n(`/api/activity?scope=${e}`,`Failed to load activity`);if(!t.ok){y=``,r.replaceChildren(a(`p`,`p-4 text-[13px] text-red-600`,t.error));return}if(e!==_)return;let o=JSON.stringify(t.value);if(`${e}:${o}`===y)return;y=`${e}:${o}`;let s=t.value;v={...s,sessions:s.sessions.map(e=>({...e,title:i(e.title)}))},x()});function x(){let e=s(t([[`Sessions`,`sessions`],[`Relationships`,`dependencies`]],g,e=>{g=e,x()}),a(`span`,`ml-auto text-[11.5px] text-neutral-400`,`${v.sessions.length} sessions · ${v.runs.length} runs`),t([[`Current`,`active`],[`Last 24h`,`recent`]],_,e=>{_=e,x(),b()})),n=a(`div`,`min-h-0 flex-1 overflow-auto`);g===`sessions`?S(n):w(n),r.replaceChildren(e,n)}function S(e){let t=document.createElement(`table`);t.className=`w-full table-fixed text-left text-[12.5px]`,t.innerHTML=`<thead class="sticky top-0 bg-neutral-50 text-[10.5px] uppercase tracking-wide text-neutral-400 shadow-[inset_0_-1px_0_var(--color-neutral-200)]"><tr>
2
2
  <th class="w-[50%] px-4 py-2 font-semibold md:w-[34%]">Session</th>
3
3
  <th class="hidden w-[38%] px-2 py-2 font-semibold md:table-cell">Project</th>
4
4
  <th class="px-2 py-2 font-semibold md:w-[14%]">State</th>
@@ -1 +1 @@
1
- import{G as e,H as t,L as n,N as r,O as i,P as a,R as o,V as s,W as c,h as l}from"./index-D9euDszY.js";var u=e=>e.public&&e.token?`/p/${e.slug}-${e.token}/`:`/boards/${e.slug}/`;function d(d,f){let p=[],m=``,h=n(`header`,`sticky top-0 z-30 flex h-10 items-center gap-3 border-b border-neutral-200 bg-white px-4`,n(`span`,`font-medium max-md:hidden`,`Boards`)),g=n(`div`,`px-4 py-5`);d.append(n(`div`,`min-h-0 flex-1 overflow-y-auto`,h,g));async function _(t,n){let r=await e(`/api/boards/${t.slug}`,{public:n},`PATCH`);if(r.ok)return t.token=(await r.json().catch(()=>({}))).token??``,x();m=await s(r,`Could not change ${t.slug}`),await S()}async function v(e){m=await c(`/api/boards/${e}`,`DELETE`,`Could not delete ${e}`)??``,await S()}function y(e){let t=n(`div`,`group flex flex-col gap-1 border-b border-neutral-200/70 px-1 py-2.5 last:border-b-0`),r=document.createElement(`a`);r.className=`truncate font-medium text-neutral-800 hover:text-indigo-700`,r.href=u(e),r.target=`_blank`,r.rel=`noreferrer`,r.textContent=e.title;let s=n(`div`,`flex items-center gap-2`,r,n(`span`,`flex-none font-mono text-[11.5px] text-neutral-400`,e.slug)),c=i(``,``,e.public,t=>{e.public=t,_(e,t)});c.className=`ml-auto flex flex-none cursor-pointer items-center gap-1.5 text-[11.5px] text-neutral-500`,c.append(n(`span`,``,`Public`));let d=l(`Delete`,`flex-none cursor-pointer text-[11.5px] text-neutral-400 opacity-0 transition-opacity hover:text-red-600 group-hover:opacity-100`);d.title=`Renames the folder on disk; nothing is erased`,d.onclick=()=>void v(e.slug);let p=a(`flex-none cursor-pointer text-[11.5px] text-neutral-400 opacity-0 transition-opacity hover:text-indigo-600 group-hover:opacity-100`,()=>`${location.origin}${u(e)}`);p.title=`Copy the board's link`,s.append(c,p,d),t.append(s);let m=n(`div`,`flex flex-wrap items-center gap-x-3 gap-y-1 text-[11.5px] text-neutral-500`);e.description&&m.append(n(`span`,`min-w-0 truncate`,e.description)),m.append(n(`span`,`flex-none text-neutral-400`,o(Date.parse(e.updatedAt))));for(let t of e.sessions){let e=l(t.slice(0,8),`flex-none cursor-pointer font-mono text-indigo-600 hover:underline`);e.title=`Open session ${t}`,e.onclick=()=>f(t),m.append(e)}if(e.public&&e.token){let t=n(`span`,`flex-none font-mono text-neutral-400`,u(e));t.title=`Public URL — anyone holding this link can read the board, no password`,m.append(t)}return t.append(m),t}let b=[{title:`Public`,hint:`Readable at /p/<slug>-<token>/ by anyone holding the link.`,wanted:!0},{title:`Private`,hint:`Reachable from the Console only.`,wanted:!1}];function x(){let e=n(`div`,`mx-auto flex max-w-3xl flex-col`);p.length===0&&e.append(n(`p`,`rounded-xl border border-dashed border-neutral-300 px-4 py-6 text-center text-[12.5px] text-neutral-500`,`No boards yet. Ask an agent to build one — it writes a folder under ~/.pier/boards.`));for(let{title:t,hint:r,wanted:i}of b){let a=p.filter(e=>e.public===i);if(a.length===0)continue;let o=n(`section`,`mb-6`);o.append(n(`h2`,`text-[13px] font-semibold text-neutral-700`,t)),o.append(n(`p`,`mb-1 text-[11.5px] leading-snug text-neutral-500`,r));let s=n(`div`,`rounded-xl border border-neutral-200 bg-white px-3`);s.append(...a.map(y)),o.append(s),e.append(o)}g.replaceChildren(...m?[n(`p`,`mb-3 text-[13px] text-red-600`,m)]:[],e)}async function S(){let e=await t(`/api/boards`,`Could not load boards`);if(!e.ok)return m=e.error,x();p=e.value,x(),m=``}let C=r(d,()=>void S());return window.addEventListener(`focus`,()=>{C.visible&&S()}),C}export{d as createBoardsView};
1
+ import{G as e,H as t,L as n,N as r,O as i,P as a,R as o,V as s,W as c,h as l}from"./index-BmKvoulH.js";var u=e=>e.public&&e.token?`/p/${e.slug}-${e.token}/`:`/boards/${e.slug}/`;function d(d,f){let p=[],m=``,h=n(`header`,`sticky top-0 z-30 flex h-10 items-center gap-3 border-b border-neutral-200 bg-white px-4`,n(`span`,`font-medium max-md:hidden`,`Boards`)),g=n(`div`,`px-4 py-5`);d.append(n(`div`,`min-h-0 flex-1 overflow-y-auto`,h,g));async function _(t,n){let r=await e(`/api/boards/${t.slug}`,{public:n},`PATCH`);if(r.ok)return t.token=(await r.json().catch(()=>({}))).token??``,x();m=await s(r,`Could not change ${t.slug}`),await S()}async function v(e){m=await c(`/api/boards/${e}`,`DELETE`,`Could not delete ${e}`)??``,await S()}function y(e){let t=n(`div`,`group flex flex-col gap-1 border-b border-neutral-200/70 px-1 py-2.5 last:border-b-0`),r=document.createElement(`a`);r.className=`truncate font-medium text-neutral-800 hover:text-indigo-700`,r.href=u(e),r.target=`_blank`,r.rel=`noreferrer`,r.textContent=e.title;let s=n(`div`,`flex items-center gap-2`,r,n(`span`,`flex-none font-mono text-[11.5px] text-neutral-400`,e.slug)),c=i(``,``,e.public,t=>{e.public=t,_(e,t)});c.className=`ml-auto flex flex-none cursor-pointer items-center gap-1.5 text-[11.5px] text-neutral-500`,c.append(n(`span`,``,`Public`));let d=l(`Delete`,`flex-none cursor-pointer text-[11.5px] text-neutral-400 opacity-0 transition-opacity hover:text-red-600 group-hover:opacity-100`);d.title=`Renames the folder on disk; nothing is erased`,d.onclick=()=>void v(e.slug);let p=a(`flex-none cursor-pointer text-[11.5px] text-neutral-400 opacity-0 transition-opacity hover:text-indigo-600 group-hover:opacity-100`,()=>`${location.origin}${u(e)}`);p.title=`Copy the board's link`,s.append(c,p,d),t.append(s);let m=n(`div`,`flex flex-wrap items-center gap-x-3 gap-y-1 text-[11.5px] text-neutral-500`);e.description&&m.append(n(`span`,`min-w-0 truncate`,e.description)),m.append(n(`span`,`flex-none text-neutral-400`,o(Date.parse(e.updatedAt))));for(let t of e.sessions){let e=l(t.slice(0,8),`flex-none cursor-pointer font-mono text-indigo-600 hover:underline`);e.title=`Open session ${t}`,e.onclick=()=>f(t),m.append(e)}if(e.public&&e.token){let t=n(`span`,`flex-none font-mono text-neutral-400`,u(e));t.title=`Public URL — anyone holding this link can read the board, no password`,m.append(t)}return t.append(m),t}let b=[{title:`Public`,hint:`Readable at /p/<slug>-<token>/ by anyone holding the link.`,wanted:!0},{title:`Private`,hint:`Reachable from the Console only.`,wanted:!1}];function x(){let e=n(`div`,`mx-auto flex max-w-3xl flex-col`);p.length===0&&e.append(n(`p`,`rounded-xl border border-dashed border-neutral-300 px-4 py-6 text-center text-[12.5px] text-neutral-500`,`No boards yet. Ask an agent to build one — it writes a folder under ~/.pier/boards.`));for(let{title:t,hint:r,wanted:i}of b){let a=p.filter(e=>e.public===i);if(a.length===0)continue;let o=n(`section`,`mb-6`);o.append(n(`h2`,`text-[13px] font-semibold text-neutral-700`,t)),o.append(n(`p`,`mb-1 text-[11.5px] leading-snug text-neutral-500`,r));let s=n(`div`,`rounded-xl border border-neutral-200 bg-white px-3`);s.append(...a.map(y)),o.append(s),e.append(o)}g.replaceChildren(...m?[n(`p`,`mb-3 text-[13px] text-red-600`,m)]:[],e)}async function S(){let e=await t(`/api/boards`,`Could not load boards`);if(!e.ok)return m=e.error,x();p=e.value,x(),m=``}let C=r(d,()=>void S());return window.addEventListener(`focus`,()=>{C.visible&&S()}),C}export{d as createBoardsView};
@@ -1 +1 @@
1
- import{L as e,d as t}from"./index-D9euDszY.js";var n=2e4,r=e=>e.map((e,t)=>({nums:[t+1],text:e,tone:``}));function i(t,r){let i=t.length<=n?r:null,o=Math.max(2,...t.map(e=>e.nums.reduce((e,t)=>Math.max(e,String(t).length),0))),s=e(`div`,`py-2 font-mono text-[12px] leading-[1.5]`);for(let n of t){let t=e(`div`,`flex pl-2 pr-4 ${n.tone===`add`?`bg-emerald-50`:n.tone===`del`?`bg-red-50`:``}`);for(let r of n.nums){let n=e(`span`,`flex-none select-none pr-1.5 text-right text-neutral-300`,r===``?``:String(r));n.style.width=`calc(${o}ch + 0.375rem)`,t.append(n)}t.append(a(n,i)),s.append(t)}return s}function a(n,r){let i=`min-w-0 flex-1 whitespace-pre-wrap [overflow-wrap:anywhere]`;if(!n.mark||n.mark[0]>=n.mark[1]){let e=t(n.text,r);return e.className=i,e}let[a,o]=n.mark,s=t(n.text.slice(a,o),r);return s.className=`rounded-xs ${n.tone===`add`?`bg-emerald-200/80`:`bg-red-200/70`}`,e(`span`,i,t(n.text.slice(0,a),r),s,t(n.text.slice(o),r))}export{r as n,i as t};
1
+ import{L as e,d as t}from"./index-BmKvoulH.js";var n=2e4,r=e=>e.map((e,t)=>({nums:[t+1],text:e,tone:``}));function i(t,r){let i=t.length<=n?r:null,o=Math.max(2,...t.map(e=>e.nums.reduce((e,t)=>Math.max(e,String(t).length),0))),s=e(`div`,`py-2 font-mono text-[12px] leading-[1.5]`);for(let n of t){let t=e(`div`,`flex pl-2 pr-4 ${n.tone===`add`?`bg-emerald-50`:n.tone===`del`?`bg-red-50`:``}`);for(let r of n.nums){let n=e(`span`,`flex-none select-none pr-1.5 text-right text-neutral-300`,r===``?``:String(r));n.style.width=`calc(${o}ch + 0.375rem)`,t.append(n)}t.append(a(n,i)),s.append(t)}return s}function a(n,r){let i=`min-w-0 flex-1 whitespace-pre-wrap [overflow-wrap:anywhere]`;if(!n.mark||n.mark[0]>=n.mark[1]){let e=t(n.text,r);return e.className=i,e}let[a,o]=n.mark,s=t(n.text.slice(a,o),r);return s.className=`rounded-xs ${n.tone===`add`?`bg-emerald-200/80`:`bg-red-200/70`}`,e(`span`,i,t(n.text.slice(0,a),r),s,t(n.text.slice(o),r))}export{r as n,i as t};
@@ -1,4 +1,4 @@
1
- import{F as e,L as t,M as n,N as r,R as i,U as a,a as o,i as s,n as c,s as l,u}from"./index-D9euDszY.js";import{n as d,t as f}from"./code-pUOkepAa.js";var p=e=>{let t=new Date(e),n=e=>String(e).padStart(2,`0`);return`${t.getFullYear()}-${n(t.getMonth()+1)}-${n(t.getDate())} ${n(t.getHours())}:${n(t.getMinutes())}:${n(t.getSeconds())}`},m=e=>[e.subject,e.body,[[e.author,e.email?`<${e.email}>`:``].filter(Boolean).join(` `),e.at?p(e.at):``].filter(Boolean).join(` · `)].filter(Boolean).join(`
1
+ import{F as e,L as t,M as n,N as r,R as i,U as a,a as o,i as s,n as c,s as l,u}from"./index-BmKvoulH.js";import{n as d,t as f}from"./code-I0lxXuvE.js";var p=e=>{let t=new Date(e),n=e=>String(e).padStart(2,`0`);return`${t.getFullYear()}-${n(t.getMonth()+1)}-${n(t.getDate())} ${n(t.getHours())}:${n(t.getMinutes())}:${n(t.getSeconds())}`},m=e=>[e.subject,e.body,[[e.author,e.email?`<${e.email}>`:``].filter(Boolean).join(` `),e.at?p(e.at):``].filter(Boolean).join(` · `)].filter(Boolean).join(`
2
2
 
3
3
  `),h=null;function g(){h&&(h.remove(),h=null,document.removeEventListener(`pointerdown`,g,!0),window.removeEventListener(`scroll`,g,!0))}function _(e,n){e.onmouseenter=()=>{g();let r=n();if(!r)return;h=t(`div`,`pointer-events-none fixed z-50 max-w-96 whitespace-pre-wrap rounded-lg bg-neutral-800/95 px-3 py-2 font-sans text-[11.5px] leading-snug text-neutral-100 shadow-lg`),h.textContent=r,document.body.append(h),document.addEventListener(`pointerdown`,g,!0),window.addEventListener(`scroll`,g,!0);let i=e.getBoundingClientRect();h.style.left=`${Math.max(8,Math.min(i.left,window.innerWidth-h.offsetWidth-8))}px`,h.style.top=i.bottom+6+h.offsetHeight>window.innerHeight?`${i.top-h.offsetHeight-6}px`:`${i.bottom+6}px`},e.onmouseleave=g}function v(e,n,r){let a=t(`button`,`flex w-full cursor-pointer items-center gap-1.5 px-2 py-1 text-left hover:bg-neutral-100`,t(`span`,`w-3 flex-none text-indigo-600`,e.value===n?`✓`:``),t(`span`,`flex-none font-mono text-[11.5px] text-neutral-800`,e.label??e.value));return e.subject&&a.append(t(`span`,`min-w-0 truncate text-[11px] text-neutral-400`,e.subject)),e.author&&a.append(t(`span`,`ml-auto flex-none pl-1.5 text-[10.5px] text-neutral-400`,`${e.author} · ${e.at?i(e.at):``}`)),_(a,()=>m(e)),a.onclick=()=>r(e.value),a}function ee(e,n,r,i,a){let{base:c,head:l}=r,u=()=>l&&c===`${l}~1`?l:``,d=u()?`commit`:`compare`,f=n.commits[0],p=e=>[...e?[{value:``,label:`Working tree`,subject:`the files on disk, uncommitted changes included`}]:[],{...f,value:`HEAD`},...n.refs.map(e=>({value:e.name,subject:e.subject})),...n.commits.map(e=>({...e,value:e.hash}))],m=t(`div`,`flex w-[38rem] max-w-full flex-col`);function h(){let e=(e,n)=>{let r=t(`button`,`cursor-pointer rounded-full px-2.5 py-0.5 text-[11.5px] ${d===n?`bg-indigo-50 font-medium text-indigo-700`:`text-neutral-500 hover:bg-neutral-100`}`,e);return r.onclick=()=>{d=n,h()},r},r=(e,n,r,i)=>t(`div`,`flex min-w-0 flex-1 flex-col`,t(`div`,`flex-none px-2 pb-0.5 text-[10.5px] font-semibold uppercase tracking-wide text-neutral-400`,e),t(`div`,`min-h-0 flex-1 overflow-y-auto`,...n.map(e=>v(e,r,i)))),o=d===`compare`?t(`div`,`flex h-72 min-h-0 divide-x divide-neutral-200 pb-1`,r(`base`,p(!1),c,e=>{c=e,h(),i(c,l)}),r(`to`,p(!0),l,e=>{l=e,h(),i(c,l)})):t(`div`,`flex h-72 min-h-0 flex-col overflow-y-auto pb-1`,...n.commits.map(e=>v({value:e.hash,...e},u(),e=>{c=`${e}~1`,l=e,s(),i(c,l)}))),f=t(`div`,`flex-none border-t border-neutral-200`,v({value:a,label:`↺ ${a} ↔ Working tree`,subject:`back to the default: the main line's tip vs the files on disk`},c===a&&l===``?a:``,()=>{c=a,l=``,h(),i(c,l)}));m.replaceChildren(t(`div`,`flex flex-none items-center gap-1 border-b border-neutral-200 px-2 py-1.5`,e(`Compare two points`,`compare`),e(`One commit`,`commit`)),o,...d===`compare`?[f]:[])}h(),o(e,m)}var te=/\.(png|jpe?g|gif|webp|avif|bmp|svg)$/i,ne={A:`text-emerald-600`,M:`text-amber-600`,D:`text-red-600`},re=30,y=`pier.filesPrefs`,b=50,x=()=>{try{let e=JSON.parse(localStorage.getItem(y)??`{}`);return e&&typeof e==`object`?e:{}}catch{return{}}},S=e=>{let t=e?x()[e]:void 0;return typeof t?.cwd==`string`&&typeof t.base==`string`&&typeof t.head==`string`?{cwd:t.cwd,base:t.base,head:t.head}:null},ie=(e,t)=>{let n=x();delete n[e],n[e]=t;let r=Object.keys(n);for(let e of r.slice(0,Math.max(0,r.length-b)))delete n[e];try{localStorage.setItem(y,JSON.stringify(n))}catch(e){console.warn(`Files: could not remember this folder`,e)}},C=(e,n=`indigo`)=>{let r=t(`button`,`min-w-0 cursor-pointer truncate rounded-md px-2 py-0.5 text-left font-mono text-[11.5px] ${n===`indigo`?`bg-indigo-50 font-medium text-indigo-700 hover:bg-indigo-100`:`bg-neutral-100 text-neutral-600 hover:bg-neutral-200`}`,e);return r.type=`button`,r};function w(i,o,s,p){let h=``,g=``,v={branch:null,refs:[],commits:[],worktrees:[]},y=`HEAD`,b=``,x=new Map,w=!0,T=!1,ae=[],E=new Set,D=null,O=null,k=t(`header`,`flex h-10 flex-none items-center gap-2 border-b border-neutral-200 bg-white px-3`),A=t(`div`,`flex-none border-b border-neutral-200`),j=t(`div`,`min-h-0 flex-1 overflow-y-auto py-1`),oe=t(`aside`,`flex w-64 flex-none flex-col border-r border-neutral-200 text-[12.5px] max-md:h-2/5 max-md:w-full max-md:border-b max-md:border-r-0`,A,j),M=t(`div`,`h-full min-w-0 overflow-auto`),N=t(`div`,`pointer-events-none absolute inset-y-0 right-0 z-20 hidden w-2`),se=t(`section`,`relative min-h-0 min-w-0 flex-1`,M,N);i.append(k,t(`div`,`flex min-h-0 flex-1 max-md:flex-col`,oe,se));let P=(e,t)=>`/api/explorer/${e}?${new URLSearchParams({root:h,...t})}`,F=(e,t)=>`/api/fs/${e}?${new URLSearchParams({root:h,...t})}`,I=(e,n=`text-neutral-400`)=>t(`p`,`px-4 py-3 text-[12.5px] ${n}`,e),L=e=>{let t=0;for(let n of x.keys())n.startsWith(`${e}/`)&&t++;return t};function R(e,t){let n=new Set(t.map(e=>e.name)),r=e?`${e}/`:``;for(let[e,i]of x){if(i.status!==`D`||!e.startsWith(r))continue;let a=e.slice(r.length).split(`/`)[0];n.has(a)||(n.add(a),t.push({name:a,dir:e.slice(r.length).includes(`/`)}))}let i=t;return w&&v.branch&&(i=t.filter(e=>e.dir?L(`${r}${e.name}`)>0:x.has(`${r}${e.name}`))),i.sort((e,t)=>Number(t.dir)-Number(e.dir)||e.name.localeCompare(t.name)),i.map(e=>e.dir?ce(`${r}${e.name}`,e.name):le(`${r}${e.name}`,e.name))}async function z(e,t){try{let{entries:n}=await a(F(`ls`,{path:t}),`could not list this folder`);e.replaceChildren(...R(t,n)),e.childElementCount||e.append(I(w?`No changes.`:`Empty.`))}catch(n){let r=R(t,[]);r.length?e.replaceChildren(...r):e.replaceChildren(I(String(n),`text-red-600`))}}function ce(n,r){let i=L(n),a=[t(`span`,`truncate`,r)];i&&a.push(t(`span`,`ml-auto flex-none rounded bg-amber-100/80 px-1 font-mono text-[10px] text-amber-700`,String(i)));let{el:o,summary:s}=e(``,a);s.className+=` px-2 py-0.5 hover:bg-neutral-100`;let c=t(`div`,`pl-3`);o.append(c);let l=!1,u=()=>{l||(l=!0,c.append(I(`…`)),z(c,n))};return o.ontoggle=()=>{E[o.open?`add`:`delete`](n),o.open&&u()},(w&&v.branch&&!T||E.has(n))&&(o.open=!0,u()),o}function B(e){O?.classList.remove(`bg-indigo-50`,`text-indigo-700`),O=e,e.classList.add(`bg-indigo-50`,`text-indigo-700`)}function le(e,n){let r=x.get(e)?.status,i=t(`button`,`flex w-full cursor-pointer items-center gap-1.5 px-2 py-0.5 pl-6 text-left hover:bg-neutral-100`,t(`span`,`truncate`,n));return r&&i.append(t(`span`,`ml-auto flex-none font-mono text-[10.5px] font-semibold ${ne[r]??`text-neutral-500`}`,r)),i.title=e,e===D&&B(i),i.onclick=()=>{B(i),D=e,de(e)},i}let V=()=>void z(j,``),H=(e,n)=>[...e?[t(`span`,`flex-none font-mono text-[10.5px] font-semibold text-emerald-600`,`+${e}`)]:[],...n?[t(`span`,`flex-none font-mono text-[10.5px] font-semibold text-red-600`,`−${n}`)]:[]];function U(e,r,i){let a=x.get(e),o=t(`div`,`sticky top-0 z-10 flex items-center gap-2 border-b border-neutral-200 bg-white px-4 py-1.5 font-mono text-[11.5px] text-neutral-500`,t(`span`,`truncate`,e));a&&(o.append(t(`span`,`flex-none font-semibold ${ne[a.status]??``}`,a.status)),o.append(...H(a.add,a.del)));let s=t(`div`,`ml-auto flex flex-none items-center gap-2`);if(i&&s.append(i),r){let t=document.createElement(`a`);t.className=`flex-none text-neutral-400 hover:text-indigo-700 hover:underline`,t.href=F(`file`,{path:e}),t.download=n(e),t.textContent=`Download`,s.append(t)}return o.append(s),o}function ue(e){let t=0;for(;t<e.length;){if(e[t].tone!==`del`){t++;continue}let n=t;for(;t<e.length&&e[t].tone===`del`;)t++;let r=t;for(;t<e.length&&e[t].tone===`add`;)t++;let i=Math.min(r-n,t-r);for(let t=0;t<i;t++){let i=e[n+t],a=e[r+t],o=Math.min(i.text.length,a.text.length),s=0;for(;s<o&&i.text[s]===a.text[s];)s++;let c=0;for(;c<o-s&&i.text[i.text.length-1-c]===a.text[a.text.length-1-c];)c++;s+c!==0&&(i.mark=[s,i.text.length-c],a.mark=[s,a.text.length-c])}}}let W=_e();function G(){N.classList.add(`hidden`),N.replaceChildren(),W.set([],[])}let K=0,q=e=>e===K;function de(e){let t=++K;return x.has(e)?ve(e,t):J(e,t)}async function J(e,n){if(!q(n))return;G();let r=F(`file`,{path:e}),i=t(`div`,`min-w-0`);if(M.classList.remove(`flex`,`flex-col`),M.replaceChildren(U(e,!0),i),te.test(e)){let e=t(`img`,`max-w-full p-4`);e.src=r,e.onerror=()=>i.replaceChildren(I(`Could not load image.`,`text-red-600`)),i.append(e);return}if(e.toLowerCase().endsWith(`.pdf`)){let t=document.createElement(`iframe`);t.className=`h-full w-full`,t.src=r,M.replaceChildren(U(e,!0),t),M.classList.add(`flex`,`flex-col`);return}i.append(I(`…`));let a=await fetch(r);if(!q(n))return;if(!a.ok){let{error:e}=await a.json().catch(()=>({}));i.replaceChildren(I(e??`HTTP ${a.status}`,`text-red-600`));return}if(!(a.headers.get(`content-type`)??``).startsWith(`text/plain`)){i.replaceChildren(I(`Binary file — use Download.`));return}let o=(await a.text()).split(`
4
4
  `);if(!q(n))return;o.at(-1)===``&&o.pop();let s=await u(e);q(n)&&i.replaceChildren(f(d(o),s))}function fe(e){let t=[],n=0,r=0,i=!1,a=e.split(`