@timqi/pier 0.0.17 → 0.0.20

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 (95) hide show
  1. package/dist/agent/config-sync.js +123 -0
  2. package/dist/agent/config.js +95 -3
  3. package/dist/agent/events.js +26 -5
  4. package/dist/agent/listing.js +12 -13
  5. package/dist/agent/pi.js +75 -27
  6. package/dist/boards/boards.js +7 -6
  7. package/dist/channels/attach.js +18 -4
  8. package/dist/channels/config.js +7 -2
  9. package/dist/channels/dedup.js +9 -1
  10. package/dist/channels/lark.js +2 -6
  11. package/dist/channels/receipts.js +21 -6
  12. package/dist/channels/slack-tool.js +86 -25
  13. package/dist/channels/slack.js +2 -10
  14. package/dist/channels/telegram.js +1 -0
  15. package/dist/config-sync-fetch.js +97 -0
  16. package/dist/config-sync-task.js +59 -0
  17. package/dist/config-sync.js +179 -0
  18. package/dist/core/hub.js +23 -6
  19. package/dist/core/inbox.js +1 -1
  20. package/dist/core/reply.js +76 -2
  21. package/dist/core/router.js +162 -37
  22. package/dist/db.js +50 -0
  23. package/dist/main.js +32 -1
  24. package/dist/secrets.js +18 -6
  25. package/dist/tasks/agent.js +25 -23
  26. package/dist/tasks/callbacks.js +1 -1
  27. package/dist/tasks/definitions.js +28 -17
  28. package/dist/tasks/execution.js +11 -3
  29. package/dist/tasks/messages.js +58 -16
  30. package/dist/tasks/routes.js +58 -10
  31. package/dist/tasks/runs.js +0 -5
  32. package/dist/tasks/service.js +14 -4
  33. package/dist/tasks/store.js +87 -10
  34. package/dist/tasks/tool.js +8 -4
  35. package/dist/tools.js +32 -2
  36. package/dist/web/auth.js +17 -23
  37. package/dist/web/config-sync.js +91 -0
  38. package/dist/web/config.js +61 -0
  39. package/dist/web/explorer.js +20 -70
  40. package/dist/web/fs.js +159 -0
  41. package/dist/web/instance.js +11 -0
  42. package/dist/web/public/assets/activity-DYo7QS7k.js +5 -0
  43. package/dist/web/public/assets/activity-DYo7QS7k.js.br +0 -0
  44. package/dist/web/public/assets/activity-DYo7QS7k.js.gz +0 -0
  45. package/dist/web/public/assets/boards-CwohTbtv.js +1 -0
  46. package/dist/web/public/assets/boards-CwohTbtv.js.br +0 -0
  47. package/dist/web/public/assets/boards-CwohTbtv.js.gz +0 -0
  48. package/dist/web/public/assets/code-p-Tq1bMK.js +1 -0
  49. package/dist/web/public/assets/explorer-50no_DrD.js +5 -0
  50. package/dist/web/public/assets/explorer-50no_DrD.js.br +0 -0
  51. package/dist/web/public/assets/explorer-50no_DrD.js.gz +0 -0
  52. package/dist/web/public/assets/{ghostty-web-CV7F9_iW.js → ghostty-web-Bvhh3ZFk.js} +1 -1
  53. package/dist/web/public/assets/ghostty-web-Bvhh3ZFk.js.br +0 -0
  54. package/dist/web/public/assets/ghostty-web-Bvhh3ZFk.js.gz +0 -0
  55. package/dist/web/public/assets/hljs-tWqyD59G.js +3 -0
  56. package/dist/web/public/assets/hljs-tWqyD59G.js.br +0 -0
  57. package/dist/web/public/assets/hljs-tWqyD59G.js.gz +0 -0
  58. package/dist/web/public/assets/{index-CtR-Ob4T.css → index-DuZ2vERd.css} +1 -1
  59. package/dist/web/public/assets/index-DuZ2vERd.css.br +0 -0
  60. package/dist/web/public/assets/index-DuZ2vERd.css.gz +0 -0
  61. package/dist/web/public/assets/index-WrsNstrb.js +79 -0
  62. package/dist/web/public/assets/index-WrsNstrb.js.br +0 -0
  63. package/dist/web/public/assets/index-WrsNstrb.js.gz +0 -0
  64. package/dist/web/public/assets/runs-BAXZF-6r.js +1 -0
  65. package/dist/web/public/assets/runs-BAXZF-6r.js.br +0 -0
  66. package/dist/web/public/assets/runs-BAXZF-6r.js.gz +0 -0
  67. package/dist/web/public/assets/settings-BmNXgRne.js +6 -0
  68. package/dist/web/public/assets/settings-BmNXgRne.js.br +0 -0
  69. package/dist/web/public/assets/settings-BmNXgRne.js.gz +0 -0
  70. package/dist/web/public/assets/task-runs-CpgHxFLf.js +3 -0
  71. package/dist/web/public/assets/task-runs-CpgHxFLf.js.br +0 -0
  72. package/dist/web/public/assets/task-runs-CpgHxFLf.js.gz +0 -0
  73. package/dist/web/public/assets/tasks-DybtogPF.js +4 -0
  74. package/dist/web/public/assets/tasks-DybtogPF.js.br +0 -0
  75. package/dist/web/public/assets/tasks-DybtogPF.js.gz +0 -0
  76. package/dist/web/public/assets/terminal-m2_WYu5W.js +2 -0
  77. package/dist/web/public/assets/terminal-m2_WYu5W.js.br +0 -0
  78. package/dist/web/public/assets/terminal-m2_WYu5W.js.gz +0 -0
  79. package/dist/web/public/icon.svg.br +0 -0
  80. package/dist/web/public/icon.svg.gz +0 -0
  81. package/dist/web/public/index.html +13 -2
  82. package/dist/web/public/index.html.br +0 -0
  83. package/dist/web/public/index.html.gz +0 -0
  84. package/dist/web/public/manifest.webmanifest.br +0 -0
  85. package/dist/web/public/manifest.webmanifest.gz +0 -0
  86. package/dist/web/public/sw.js.br +0 -0
  87. package/dist/web/public/sw.js.gz +0 -0
  88. package/dist/web/route.js +17 -0
  89. package/dist/web/server.js +152 -66
  90. package/dist/web/terminal.js +2 -7
  91. package/package.json +2 -2
  92. package/skills/pier-slack/SKILL.md +4 -16
  93. package/skills/pier-tasks/SKILL.md +14 -9
  94. package/dist/web/files.js +0 -183
  95. package/dist/web/public/assets/index-DAxKtidH.js +0 -93
@@ -1,28 +1,12 @@
1
- // Files view backend: read-only directory listing, file bytes and git
2
- // ref/diff queries for the Console's Files view. `root` is any directory the
3
- // process can read sessions work in worktrees and siblings of their cwd, and
4
- // an owner who is already past the Console password can reach those paths
5
- // anyway. `path` is still confined to the `root` it was asked under, so a
6
- // listing can never widen itself, and nothing here writes.
1
+ // What git knows about a project directory, for the Console's Files view: the
2
+ // refs, commits and worktrees its pickers offer, and the diffs it tones into a
3
+ // file. Every route here runs git and nothing else reading the directory and
4
+ // the files themselves is web/fs.ts, which also owns the scoping both share.
7
5
  import { execFile } from "node:child_process";
8
- import { readdir, readFile, realpath, stat } from "node:fs/promises";
9
- import { basename, extname, isAbsolute, resolve, sep } from "node:path";
10
6
  import { promisify } from "node:util";
11
- import { guarded } from "./files.js";
7
+ import { scoped } from "./fs.js";
8
+ import { guarded } from "./route.js";
12
9
  const run = promisify(execFile);
13
- // Inline-renderable binary types; text is sniffed, everything else downloads.
14
- const BINARY_TYPES = {
15
- ".png": "image/png",
16
- ".jpg": "image/jpeg",
17
- ".jpeg": "image/jpeg",
18
- ".gif": "image/gif",
19
- ".webp": "image/webp",
20
- ".avif": "image/avif",
21
- ".bmp": "image/bmp",
22
- ".svg": "image/svg+xml",
23
- ".pdf": "application/pdf",
24
- };
25
- const MAX_FILE_BYTES = 32 * 1024 * 1024;
26
10
  const MAX_DIFF_BYTES = 2 * 1024 * 1024;
27
11
  /** A ref never starts with `-`: execFile blocks the shell, this blocks the
28
12
  * argument parser (`--output=…` is a write). Git validates the rest. */
@@ -30,53 +14,11 @@ const REF_RE = /^[^-\s][^\s]*$/;
30
14
  /** git in `root`, output capped — a diff is display data, not an archive. */
31
15
  const git = async (root, ...args) => (await run("git", ["-C", root, ...args], { maxBuffer: MAX_DIFF_BYTES })).stdout;
32
16
  export function registerExplorerRoutes(app) {
33
- /** The scope check every route shares: `root` must be an absolute directory,
34
- * and `path` must resolve inside it (realpath both ends — neither `..` nor a
35
- * symlink steps outside). Returns the real target. */
36
- const resolveScoped = async (root, path = "") => {
37
- if (!root || !isAbsolute(root))
38
- throw new Error("root must be an absolute directory");
39
- const real = await realpath(root);
40
- if (!(await stat(real)).isDirectory())
41
- throw new Error("root must be an absolute directory");
42
- const target = await realpath(resolve(real, path));
43
- if (target !== real && !target.startsWith(real + sep))
44
- throw new Error("path escapes root");
45
- return target;
46
- };
47
- // Directory listing, names only. `.git` is plumbing, not content.
48
- guarded(app, "GET", "/api/explorer/ls", 404, async (c) => {
49
- c.header("cache-control", "no-store");
50
- const dir = await resolveScoped(c.req.query("root"), c.req.query("path"));
51
- const entries = (await readdir(dir, { withFileTypes: true }))
52
- .filter((e) => e.name !== ".git" && (e.isDirectory() || e.isFile()))
53
- .map((e) => ({ name: e.name, dir: e.isDirectory() }))
54
- .sort((a, b) => Number(b.dir) - Number(a.dir) || a.name.localeCompare(b.name));
55
- return c.json({ entries });
56
- });
57
- // File bytes, read-only. Known binary types render inline; anything else is
58
- // sniffed — a NUL in the head means bytes we can't vouch for, so it
59
- // downloads instead of rendering (that is how a file starts executing).
60
- guarded(app, "GET", "/api/explorer/file", 404, async (c) => {
61
- const file = await resolveScoped(c.req.query("root"), c.req.query("path"));
62
- if (!(await stat(file)).isFile())
63
- throw new Error("not a file");
64
- const bytes = await readFile(file);
65
- if (bytes.byteLength > MAX_FILE_BYTES)
66
- return c.json({ error: "file too large" }, 413);
67
- const binary = BINARY_TYPES[extname(file).toLowerCase()];
68
- const text = !binary && !bytes.subarray(0, 8192).includes(0);
69
- return c.body(bytes, 200, {
70
- "content-type": binary ?? (text ? "text/plain; charset=utf-8" : "application/octet-stream"),
71
- "content-disposition": `${binary || text ? "inline" : "attachment"}; filename*=UTF-8''${encodeURIComponent(basename(file))}`,
72
- "cache-control": "no-store",
73
- });
74
- });
75
17
  // Git refs for the diff pickers: current branch, branches+tags, recent
76
18
  // commits. Not a repo → { branch: null }, which the UI renders as "no git".
77
19
  guarded(app, "GET", "/api/explorer/git", 404, async (c) => {
78
20
  c.header("cache-control", "no-store");
79
- const root = await resolveScoped(c.req.query("root"));
21
+ const root = await scoped(c.req.query("root"));
80
22
  let branch;
81
23
  try {
82
24
  branch = (await git(root, "rev-parse", "--abbrev-ref", "HEAD")).trim();
@@ -85,11 +27,20 @@ export function registerExplorerRoutes(app) {
85
27
  // not a repo, or no commits yet
86
28
  return c.json({ branch: null, refs: [], commits: [], worktrees: [] });
87
29
  }
30
+ // Three reads of the same repository, none of which is an argument to
31
+ // another: one wait, not three. The rev-parse above stays alone — it is
32
+ // the guard that decides whether these three are asked at all.
33
+ const [worktreeList, refList, commitLog] = await Promise.all([
34
+ git(root, "worktree", "list", "--porcelain"),
35
+ git(root, "for-each-ref", "--format=%(refname:short)\t%(subject)", "refs/heads", "refs/tags"),
36
+ // Unit/record separators, because a body is multi-line by nature.
37
+ git(root, "log", "-20", "--format=%h\u001f%at\u001f%an\u001f%ae\u001f%s\u001f%b\u001e"),
38
+ ]);
88
39
  // Every checkout of this repository, from git rather than from the
89
40
  // sessions that happen to live in one: a worktree created ten seconds ago
90
41
  // has no session in it yet, and that is exactly when its files are worth
91
42
  // opening. Detached heads have no `branch` line, so the path stands alone.
92
- const worktrees = (await git(root, "worktree", "list", "--porcelain"))
43
+ const worktrees = worktreeList
93
44
  .split("\n\n")
94
45
  .map((block) => {
95
46
  const path = /^worktree (.+)$/m.exec(block)?.[1];
@@ -97,15 +48,14 @@ export function registerExplorerRoutes(app) {
97
48
  return path ? { path, ...(on ? { branch: on } : {}) } : null;
98
49
  })
99
50
  .filter((w) => w !== null);
100
- const refs = (await git(root, "for-each-ref", "--format=%(refname:short)\t%(subject)", "refs/heads", "refs/tags"))
51
+ const refs = refList
101
52
  .split("\n")
102
53
  .filter(Boolean)
103
54
  .map((line) => {
104
55
  const tab = line.indexOf("\t");
105
56
  return { name: line.slice(0, tab), subject: line.slice(tab + 1) };
106
57
  });
107
- // Unit/record separators, because a body is multi-line by nature.
108
- const commits = (await git(root, "log", "-20", "--format=%h\u001f%at\u001f%an\u001f%ae\u001f%s\u001f%b\u001e"))
58
+ const commits = commitLog
109
59
  .split("\u001e")
110
60
  .map((r) => r.trimStart())
111
61
  .filter(Boolean)
@@ -120,7 +70,7 @@ export function registerExplorerRoutes(app) {
120
70
  // means the working tree.
121
71
  guarded(app, "GET", "/api/explorer/diff", 404, async (c) => {
122
72
  c.header("cache-control", "no-store");
123
- const root = await resolveScoped(c.req.query("root"));
73
+ const root = await scoped(c.req.query("root"));
124
74
  const base = c.req.query("base") ?? "";
125
75
  const head = c.req.query("head") ?? "";
126
76
  if (!REF_RE.test(base) || (head !== "" && !REF_RE.test(head)))
package/dist/web/fs.js ADDED
@@ -0,0 +1,159 @@
1
+ // Every path the Console reaches on disk resolves here: the browse/read/mkdir
2
+ // routes, the session attachments server.ts serves, and the pty's cwd all ask
3
+ // the same question, and asked it four different ways until this file existed.
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
8
+ // 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
+ import { mkdir, open, readdir, realpath, stat } from "node:fs/promises";
12
+ import { homedir } from "node:os";
13
+ import { basename, dirname, extname, isAbsolute, resolve, sep } from "node:path";
14
+ import { Readable } from "node:stream";
15
+ import { logger } from "../log.js";
16
+ import { guarded } from "./route.js";
17
+ const log = logger("web");
18
+ /** What a browser is asked to hold at once — the same ceiling for a preview
19
+ * and for an attachment, because it is the reader's patience, not the route's. */
20
+ export const MAX_FILE_BYTES = 32 * 1024 * 1024;
21
+ /** Types a browser may render as themselves. Everything else is sniffed for
22
+ * text, and bytes we can't vouch for download instead of rendering — that is
23
+ * how a file starts executing. */
24
+ const RENDERABLE = {
25
+ ".png": "image/png",
26
+ ".jpg": "image/jpeg",
27
+ ".jpeg": "image/jpeg",
28
+ ".gif": "image/gif",
29
+ ".webp": "image/webp",
30
+ ".avif": "image/avif",
31
+ ".bmp": "image/bmp",
32
+ ".svg": "image/svg+xml",
33
+ ".pdf": "application/pdf",
34
+ };
35
+ /** The containment check, and the only way a path becomes a path this reads:
36
+ * `root` must be an absolute directory, and the returned target is `path`
37
+ * resolved inside it. Throws — the route wrapper turns that into the answer. */
38
+ export async function scoped(root, path = "") {
39
+ if (!root || !isAbsolute(root))
40
+ throw new Error("not a directory this can read");
41
+ const real = await realpath(root);
42
+ if (!(await stat(real)).isDirectory())
43
+ throw new Error("not a directory this can read");
44
+ const target = await realpath(resolve(real, path));
45
+ if (target !== real && !target.startsWith(real + sep))
46
+ throw new Error("path escapes root");
47
+ return target;
48
+ }
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
+ /** How bytes leave: the type they may be served as, and whether a browser may
67
+ * render them in place. `download` forces the attachment disposition — the
68
+ * chat's Download button, on a file it would otherwise show inline. */
69
+ export function fileHeaders(file, bytes, download = false) {
70
+ const ext = extname(file).toLowerCase();
71
+ const known = RENDERABLE[ext];
72
+ const text = !known && !bytes.subarray(0, 8192).includes(0);
73
+ // An <img> renders its source whatever the disposition says, but a tab
74
+ // navigated straight at an SVG runs the script inside it — same origin, past
75
+ // the password. So an SVG keeps its type and loses only its own tab.
76
+ const inline = !download && (text || (known !== undefined && ext !== ".svg"));
77
+ return {
78
+ "content-type": known ?? (text ? "text/plain; charset=utf-8" : "application/octet-stream"),
79
+ "content-disposition": `${inline ? "inline" : "attachment"}; filename*=UTF-8''${encodeURIComponent(basename(file))}`,
80
+ };
81
+ }
82
+ export function registerFsRoutes(app) {
83
+ // One listing behind both trees: the Files view walks a project (`root`,
84
+ // which confines it and is the floor `parent` stops at), the cwd pickers
85
+ // walk from anywhere (no root, starting at home). Names and which are
86
+ // directories — never contents, and never `.git`, which is plumbing. What to
87
+ // leave out of the answer is the caller's business: one tree shows folders
88
+ // only, the other shows files too.
89
+ guarded(app, "GET", "/api/fs/ls", 404, async (c) => {
90
+ c.header("cache-control", "no-store");
91
+ const asked = c.req.query("path");
92
+ const root = c.req.query("root");
93
+ const top = root ? await scoped(root) : null;
94
+ const dir = top ? await scoped(top, asked) : await scoped(asked || homedir());
95
+ const up = dirname(dir);
96
+ const entries = (await readdir(dir, { withFileTypes: true }))
97
+ .filter((e) => e.name !== ".git" && (e.isDirectory() || e.isFile()))
98
+ .map((e) => ({ name: e.name, dir: e.isDirectory() }))
99
+ .sort((a, b) => Number(b.dir) - Number(a.dir) || a.name.localeCompare(b.name));
100
+ return c.json({ path: dir, parent: dir === top || up === dir ? null : up, entries });
101
+ });
102
+ // File bytes, read-only: the Files view's previews and whole-file reads.
103
+ guarded(app, "GET", "/api/fs/file", 404, async (c) => {
104
+ const file = await scoped(c.req.query("root"), c.req.query("path"));
105
+ const handle = await open(file);
106
+ let streaming = false;
107
+ try {
108
+ const info = await handle.stat();
109
+ if (!info.isFile())
110
+ throw new Error("not a file");
111
+ if (info.size > MAX_FILE_BYTES)
112
+ return c.json({ error: "file too large" }, 413);
113
+ // Size and mtime, not a digest: the view re-reads a file every time it is
114
+ // clicked again, and a validator the stat above already knows costs
115
+ // nothing to offer. `no-cache` keeps the answer conditional, so an edited
116
+ // file is never shown from a browser cache.
117
+ const tag = `"${info.size.toString(16)}-${info.mtime.getTime().toString(16)}"`;
118
+ const validators = {
119
+ etag: tag,
120
+ "last-modified": info.mtime.toUTCString(),
121
+ "cache-control": "private, no-cache",
122
+ };
123
+ if (c.req.header("if-none-match") === tag)
124
+ return c.body(null, 304, validators);
125
+ // Sniff and stream the same open file, so a replacement cannot bypass
126
+ // the size check or make the headers describe different bytes.
127
+ const head = Buffer.alloc(Math.min(8192, info.size));
128
+ await handle.read(head, 0, head.length, 0);
129
+ const bytes = handle.createReadStream({ start: 0 });
130
+ streaming = true;
131
+ // Past the headers a failure can only truncate the body, so this is the
132
+ // one place it can still be said at all (§5b).
133
+ bytes.on("error", (err) => log.warn(`serving ${file} stopped mid-stream`, err));
134
+ return c.body(Readable.toWeb(bytes), 200, {
135
+ ...fileHeaders(file, head),
136
+ ...validators,
137
+ });
138
+ }
139
+ finally {
140
+ if (!streaming)
141
+ await handle.close();
142
+ }
143
+ });
144
+ // Create a folder while picking one — a new project usually needs a new
145
+ // directory. A name, never a path: traversal is rejected, not normalized.
146
+ guarded(app, "POST", "/api/fs/mkdir", 400, async (c) => {
147
+ const body = await c.req.json().catch(() => null);
148
+ const parent = typeof body?.path === "string" ? body.path : "";
149
+ const name = typeof body?.name === "string" ? body.name.trim() : "";
150
+ if (!isAbsolute(parent))
151
+ return c.json({ error: "absolute path required" }, 400);
152
+ if (!name || name.length > 64 || /[/\\]|^\.\.?$/.test(name)) {
153
+ return c.json({ error: "invalid folder name" }, 400);
154
+ }
155
+ const path = resolve(parent, name);
156
+ await mkdir(path); // no recursive: the parent must already exist
157
+ return c.json({ path });
158
+ });
159
+ }
@@ -302,6 +302,17 @@ export function registerInstanceRoutes(app, deps) {
302
302
  onUnlocked?.();
303
303
  return c.json(secretsStatus());
304
304
  });
305
+ // Why vt cannot hand the key over is vt's answer, not Pier's: config
306
+ // sources, routing, whether an agent is listening. Read-only and safe while
307
+ // locked — without it "locked" is one error string and no way to repair it.
308
+ app.get("/api/secrets/doctor", async (c) => {
309
+ try {
310
+ return c.json({ report: await secrets.doctor() });
311
+ }
312
+ catch (err) {
313
+ return c.json({ error: String(err) }, 500);
314
+ }
315
+ });
305
316
  app.post("/api/secrets/rotate", async (c) => {
306
317
  const body = (await c.req.json().catch(() => ({})));
307
318
  if (body.mode !== undefined && body.mode !== "vt" && body.mode !== "file") {
@@ -0,0 +1,5 @@
1
+ import{F as e,I as t,K as n,M as r,R as i,T as a,V as o,z as s}from"./index-WrsNstrb.js";var c=(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},l=t=>t===null?`-`:e(Date.now()-t),u={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`}}},d=()=>u[document.documentElement.dataset.theme===`dark`?`dark`:`light`];function f(e,u,f){let p=`sessions`,m=`active`,h={sessions:[],runs:[],messages:[]},g=``,_=s(async()=>{let r=m,i=await o(`/api/activity?scope=${r}`,`Failed to load activity`);if(!i.ok){g=``,e.replaceChildren(t(`p`,`p-4 text-[13px] text-red-600`,i.error));return}if(r!==m)return;let a=JSON.stringify(i.value);if(`${r}:${a}`===g)return;g=`${r}:${a}`;let s=i.value;h={...s,sessions:s.sessions.map(e=>({...e,title:n(e.title)}))},v()});function v(){let n=t(`header`,`flex h-10 flex-none items-center gap-2 border-b border-neutral-200 px-4 max-md:hidden`,t(`span`,`font-medium`,`Activity`)),r=t(`div`,`tabstrip`);r.append(a(`Sessions`,p===`sessions`,()=>{p=`sessions`,v()}),a(`Relationships`,p===`dependencies`,()=>{p=`dependencies`,v()}));let i=t(`div`,`flex flex-none gap-1 border-b border-neutral-200 px-4 py-2`),o=e=>()=>{m=e,v(),_()};i.append(a(`Current`,m===`active`,o(`active`)),a(`Last 24h`,m===`recent`,o(`recent`)));let s=t(`div`,`min-h-0 flex-1 overflow-auto`);p===`sessions`?y(s):b(s),e.replaceChildren(n,r,i,s)}function y(e){let n=document.createElement(`table`);n.className=`w-full table-fixed text-left text-[12.5px]`,n.innerHTML=`<thead class="bg-neutral-50 text-[10.5px] uppercase text-neutral-400"><tr>
2
+ <th class="w-[50%] px-4 py-2 font-semibold md:w-[34%]">Session</th>
3
+ <th class="hidden w-[38%] px-2 py-2 font-semibold md:table-cell">Project</th>
4
+ <th class="px-2 py-2 font-semibold md:w-[14%]">State</th>
5
+ <th class="px-2 py-2 font-semibold">In state</th></tr></thead>`;let r=document.createElement(`tbody`);for(let e of h.sessions){let n=document.createElement(`tr`);n.className=`cursor-pointer border-b border-neutral-100 hover:bg-neutral-50`,n.onclick=()=>u(e.id);let a=t(`button`,`block w-full cursor-pointer truncate text-left font-medium`,e.title??i(e.cwd));a.setAttribute(`type`,`button`),a.title=`${e.id}\n${e.cwd}`,a.onclick=t=>{t.stopPropagation(),u(e.id)},n.append(t(`td`,`px-4 py-2.5`,a,t(`div`,`truncate font-mono text-[11px] text-neutral-400 md:hidden`,e.cwd||`-`)),t(`td`,`hidden truncate px-2 py-2.5 font-mono text-[11.5px] md:table-cell`,e.cwd||`-`),t(`td`,`px-2 py-2.5`,t(`span`,`inline-flex items-center gap-1.5 whitespace-nowrap`,t(`span`,`inline-block h-2 w-2 flex-none rounded-full ${e.state===`streaming`?`animate-pulse bg-green-500`:`bg-neutral-300`}`),e.state)),t(`td`,`px-2 py-2.5 text-neutral-500`,l(e.stateSince))),r.append(n)}n.append(r),e.append(n),h.sessions.length||e.append(t(`p`,`p-4 text-[13px] text-neutral-400`,m===`active`?`No active sessions.`:`No sessions in the last 24 hours.`))}function b(e){let n=new Map,r=[],i=(e,t,r,i)=>{n.has(e)||n.set(e,{id:e,label:t,kind:r,state:i})};for(let e of h.sessions)i(e.id,e.title??e.id.slice(0,10),`session`,e.state);for(let e of h.runs){let t=e.invokedBySessionId??`scheduler`,n=e.targetSessionId??(e.context.definition.action.type===`bash`?`process:${e.id}`:`task:${e.id}`);i(t,t===`scheduler`?`Scheduler`:t.slice(0,10),t===`scheduler`?`scheduler`:`session`),i(n,e.targetSessionId?e.targetSessionId.slice(0,10):e.context.definition.action.type===`bash`?`Bash process`:`Task`,e.targetSessionId?`session`:e.context.definition.action.type===`bash`?`process`:`task`),r.push({from:t,to:n,run:e,kind:`invocation`}),e.callbackSessionId&&(i(e.callbackSessionId,e.callbackSessionId.slice(0,10),`session`),r.push({from:n,to:e.callbackSessionId,run:e,kind:`callback`}))}for(let e of h.messages){if(!e.fromSessionId||!e.toSessionId||e.fromSessionId===e.toSessionId)continue;let t=h.runs.find(t=>t.id===e.runId);if(t){for(let t of[e.fromSessionId,e.toSessionId])i(t,t===`console`?`Console`:t.slice(0,10),t===`console`?`console`:`session`);r.push({from:e.fromSessionId,to:e.toSessionId,run:t,kind:`message`})}}if(!n.size){e.append(t(`p`,`p-4 text-[13px] text-neutral-400`,m===`active`?`No active dependencies.`:`No dependencies in the last 24 hours.`));return}let a=[...n.values()],o=new Set(r.map(e=>e.to)),s=new Map,l=a.filter(e=>!o.has(e.id)).map(e=>e.id);l.length||l.push(a[0].id);for(let e of l)s.set(e,0);for(;l.length;){let e=l.shift();for(let t of r)t.from!==e||s.has(t.to)||(s.set(t.to,s.get(e)+1),l.push(t.to))}for(let e of a)s.has(e.id)||s.set(e.id,0);let p=new Map;for(let e of a){let t=s.get(e.id);p.set(t,[...p.get(t)??[],e])}let g=Math.max(...p.keys())+1,_=Math.max(...[...p.values()].map(e=>e.length)),v=r.some(e=>e.from!==e.to&&s.get(e.from)===s.get(e.to))?40:0,y=56+g*180+(g-1)*110+v,b=56+_*46+(_-1)*30,x=new Map;for(let[e,t]of p)t.forEach((t,n)=>x.set(t.id,{x:28+e*290+90,y:28+n*76+23}));let S=c(`svg`,{width:String(y),height:String(b),viewBox:`0 0 ${y} ${b}`,class:`block`}),C=c(`defs`),w=d();for(let[e,t]of[[`activity-arrow`,w.edge],[`activity-arrow-cb`,w.callback],[`activity-arrow-msg`,w.message]]){let n=c(`marker`,{id:e,viewBox:`0 0 10 10`,refX:`9`,refY:`5`,markerWidth:`6`,markerHeight:`6`,orient:`auto-start-reverse`});n.append(c(`path`,{d:`M 0 0 L 10 5 L 0 10 z`,fill:t})),C.append(n)}S.append(C);let T=new Set;for(let e of r){let t=`${e.from}→${e.to}:${e.kind}`;if(T.has(t))continue;T.add(t);let n=x.get(e.from),r=x.get(e.to),i=c(`path`,{d:n.x===r.x?`M ${n.x+90} ${n.y} C ${n.x+90+46} ${n.y}, ${r.x+90+46} ${r.y}, ${r.x+90} ${r.y}`:(()=>{let e=r.x>n.x,t=n.x+(e?90:-90),i=r.x+(e?-90:90),a=Math.max(36,Math.abs(i-t)*.45)*(e?1:-1);return`M ${t} ${n.y} C ${t+a} ${n.y}, ${i-a} ${r.y}, ${i} ${r.y}`})(),fill:`none`,stroke:e.kind===`callback`?w.callback:e.kind===`message`?w.message:w.edge,"stroke-width":`1.5`,"marker-end":e.kind===`callback`?`url(#activity-arrow-cb)`:e.kind===`message`?`url(#activity-arrow-msg)`:`url(#activity-arrow)`});e.kind===`callback`&&i.setAttribute(`stroke-dasharray`,`6 5`),e.kind===`message`&&i.setAttribute(`stroke-dasharray`,`2 5`),i.classList.add(`cursor-pointer`),i.onclick=()=>f(e.run.id),S.append(i)}let E=/[\u1100-\u11FF\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6]/,D=(e,t)=>{let n=0;for(let r=0;r<e.length;r++)if(n+=E.test(e[r])?2:1,n>t)return`${e.slice(0,r)}…`;return e};for(let e of a){let t=x.get(e.id),n=e.kind===`session`?e.state===`streaming`?w.streaming:w.idle:w[e.kind],r=c(`g`,{transform:`translate(${t.x},${t.y})`});e.kind===`session`&&r.classList.add(`cursor-pointer`),r.onclick=()=>{e.kind===`session`&&u(e.id)};let i=c(`rect`,{x:`-90`,y:`-23`,width:`180`,height:`46`,rx:`8`,fill:n.fill,stroke:n.stroke,"stroke-width":e.kind===`session`&&e.state===`streaming`?`1.5`:`1`});n.dash&&i.setAttribute(`stroke-dasharray`,n.dash),r.append(i);let a=-78;if(e.kind===`session`){let t=c(`circle`,{cx:`-75`,cy:`0`,r:`3.5`,fill:n.stroke});if(e.state===`streaming`){let e=c(`animate`,{attributeName:`opacity`,values:`1;0.3;1`,dur:`1.6s`,repeatCount:`indefinite`});t.append(e)}r.append(t),a=-64}let o=c(`text`,{x:String(a),"text-anchor":`start`,"dominant-baseline":`middle`,"font-size":`11.5`,"font-weight":e.kind===`session`?`500`:`400`,fill:n.text});o.textContent=D(e.label,e.kind===`session`?22:25);let s=c(`title`);s.textContent=e.label===e.id?e.id:`${e.label}\n${e.id}`,r.append(o,s),S.append(r)}let O=e=>t(`span`,`inline-block h-2 w-2 rounded-full ${e}`);e.append(t(`div`,`w-max p-4`,S),t(`div`,`flex flex-wrap gap-x-5 gap-y-1 border-t border-neutral-200 px-4 py-2 text-[11px] text-neutral-500`,t(`span`,``,`Solid: task invocation`),t(`span`,`text-cyan-700`,`Dashed: callback`),t(`span`,`text-amber-700`,`Dotted: supervisor/control`),t(`span`,`inline-flex items-center gap-1.5`,O(`bg-emerald-500`),`streaming`),t(`span`,`inline-flex items-center gap-1.5`,O(`bg-neutral-300`),`idle`)))}let x=r(e,e=>{e===`dependencies`?p=`dependencies`:e===`sessions`&&(p=`sessions`),_()},()=>{e.replaceChildren(),h={sessions:[],runs:[],messages:[]},g=``});return window.addEventListener(`pier:theme`,()=>{x.visible&&v()}),Object.assign(x,{refresh(){x.visible&&_()}})}export{f as createActivityView};
@@ -0,0 +1 @@
1
+ import{B as e,G as t,I as n,L as r,M as i,N as a,O as o,V as s,W as c,h as l}from"./index-WrsNstrb.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 _(n,r){let i=await t(`/api/boards/${n.slug}`,{public:r},`PATCH`);if(i.ok)return n.token=(await i.json().catch(()=>({}))).token??``,x();m=await e(i,`Could not change ${n.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`),i=document.createElement(`a`);i.className=`truncate font-medium text-neutral-800 hover:text-indigo-700`,i.href=u(e),i.target=`_blank`,i.rel=`noreferrer`,i.textContent=e.title;let s=n(`div`,`flex items-center gap-2`,i,n(`span`,`flex-none font-mono text-[11.5px] text-neutral-400`,e.slug)),c=o(``,``,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`,r(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 s(`/api/boards`,`Could not load boards`);if(!e.ok)return m=e.error,x();p=e.value,x(),m=``}let C=i(d,()=>void S());return window.addEventListener(`focus`,()=>{C.visible&&S()}),C}export{d as createBoardsView};
@@ -0,0 +1 @@
1
+ import{I as e,u as t}from"./index-WrsNstrb.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};
@@ -0,0 +1,5 @@
1
+ import{H as e,I as t,L as n,M as r,P as i,a,i as o,j as s,l as c,n as l,s as u}from"./index-WrsNstrb.js";import{n as d,t as f}from"./code-p-Tq1bMK.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
+
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,r,i){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===r?`✓`:``),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?n(e.at):``}`)),_(a,()=>m(e)),a.onclick=()=>i(e.value),a}function ee(e,n,r,i,s){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)))),a=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,o(),i(c,l)}))),f=t(`div`,`flex-none border-t border-neutral-200`,v({value:s,label:`↺ ${s} ↔ Working tree`,subject:`back to the default: the main line's tip vs the files on disk`},c===s&&l===``?s:``,()=>{c=s,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`)),a,...d===`compare`?[f]:[])}h(),a(e,m)}var te=/\.(png|jpe?g|gif|webp|avif|bmp|svg)$/i,y={A:`text-emerald-600`,M:`text-amber-600`,D:`text-red-600`},ne=30,b=`pier.filesPrefs`,x=50,S=()=>{try{let e=JSON.parse(localStorage.getItem(b)??`{}`);return e&&typeof e==`object`?e:{}}catch{return{}}},re=e=>{let t=e?S()[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=S();delete n[e],n[e]=t;let r=Object.keys(n);for(let e of r.slice(0,Math.max(0,r.length-x)))delete n[e];try{localStorage.setItem(b,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(n,a,o,p,h){let g=``,v=``,b={branch:null,refs:[],commits:[],worktrees:[]},x=`HEAD`,S=``,w=new Map,T=!0,E=!1,D=[],O=new Set,k=null,A=null,ae=t(`header`,`flex h-10 flex-none items-center gap-2 border-b border-neutral-200 bg-white px-3`),j=t(`div`,`flex-none border-b border-neutral-200`),M=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`,j,M),N=t(`div`,`h-full min-w-0 overflow-auto`),P=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`,N,P);n.append(ae,t(`div`,`flex min-h-0 flex-1 max-md:flex-col`,oe,se));let F=(e,t)=>`/api/explorer/${e}?${new URLSearchParams({root:g,...t})}`,I=(e,t)=>`/api/fs/${e}?${new URLSearchParams({root:g,...t})}`,L=(e,n=`text-neutral-400`)=>t(`p`,`px-4 py-3 text-[12.5px] ${n}`,e),R=e=>{let t=0;for(let n of w.keys())n.startsWith(`${e}/`)&&t++;return t};function z(e,t){let n=new Set(t.map(e=>e.name)),r=e?`${e}/`:``;for(let[e,i]of w){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 T&&b.branch&&(i=t.filter(e=>e.dir?R(`${r}${e.name}`)>0:w.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 B(t,n){try{let{entries:r}=await e(I(`ls`,{path:n}),`could not list this folder`);t.replaceChildren(...z(n,r)),t.childElementCount||t.append(L(T?`No changes.`:`Empty.`))}catch(e){let r=z(n,[]);r.length?t.replaceChildren(...r):t.replaceChildren(L(String(e),`text-red-600`))}}function ce(e,n){let r=R(e),a=[t(`span`,`truncate`,n)];r&&a.push(t(`span`,`ml-auto flex-none rounded bg-amber-100/80 px-1 font-mono text-[10px] text-amber-700`,String(r)));let{el:o,summary:s}=i(``,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(L(`…`)),B(c,e))};return o.ontoggle=()=>{O[o.open?`add`:`delete`](e),o.open&&u()},(T&&b.branch&&!E||O.has(e))&&(o.open=!0,u()),o}function V(e){A?.classList.remove(`bg-indigo-50`,`text-indigo-700`),A=e,e.classList.add(`bg-indigo-50`,`text-indigo-700`)}function le(e,n){let r=w.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 ${y[r]??`text-neutral-500`}`,r)),i.title=e,e===k&&V(i),i.onclick=()=>{V(i),k=e,Y(e)},i}let H=()=>void B(M,``),U=(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 W(e,n,r){let i=w.get(e),a=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));i&&(a.append(t(`span`,`flex-none font-semibold ${y[i.status]??``}`,i.status)),a.append(...U(i.add,i.del)));let o=t(`div`,`ml-auto flex flex-none items-center gap-2`);if(r&&o.append(r),n){let t=document.createElement(`a`);t.className=`flex-none text-neutral-400 hover:text-indigo-700 hover:underline`,t.href=I(`file`,{path:e}),t.download=s(e),t.textContent=`Download`,o.append(t)}return a.append(o),a}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 G=_e();function K(){P.classList.add(`hidden`),P.replaceChildren(),G.set([],[])}let q=0,J=e=>e===q;function Y(e){let t=++q;return w.has(e)?ve(e,t):de(e,t)}async function de(e,n){if(!J(n))return;K();let r=I(`file`,{path:e}),i=t(`div`,`min-w-0`);if(N.classList.remove(`flex`,`flex-col`),N.replaceChildren(W(e,!0),i),te.test(e)){let e=t(`img`,`max-w-full p-4`);e.src=r,e.onerror=()=>i.replaceChildren(L(`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,N.replaceChildren(W(e,!0),t),N.classList.add(`flex`,`flex-col`);return}i.append(L(`…`));let a=await fetch(r);if(!J(n))return;if(!a.ok){let{error:e}=await a.json().catch(()=>({}));i.replaceChildren(L(e??`HTTP ${a.status}`,`text-red-600`));return}if(!(a.headers.get(`content-type`)??``).startsWith(`text/plain`)){i.replaceChildren(L(`Binary file — use Download.`));return}let o=(await a.text()).split(`
4
+ `);if(!J(n))return;o.at(-1)===``&&o.pop();let s=await c(e);J(n)&&i.replaceChildren(f(d(o),s))}function fe(e){let t=[],n=0,r=0,i=!1,a=e.split(`
5
+ `);a.at(-1)===``&&a.pop();for(let e of a){let a=/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(e);if(a){i&&t.push({nums:[``,``],text:`⋯`,tone:``}),[n,r]=[Number(a[1]),Number(a[2])],i=!0;continue}!i||e.startsWith(`\\`)||(e.startsWith(`+`)?t.push({nums:[``,r++],text:e.slice(1),tone:`add`}):e.startsWith(`-`)?t.push({nums:[n++,``],text:e.slice(1),tone:`del`}):t.push({nums:[n++,r++],text:e.slice(1),tone:``}))}return t}function pe(e){let t=[];for(let n=0;n<e.length;n++){let r=e[n].tone;if(!r)continue;let i=t.at(-1);i&&i.end===n-1?(i.end=n,(r===`add`&&i.tone===`del`||r===`del`&&i.tone===`add`)&&(i.tone=`mixed`)):t.push({start:n,end:n,tone:r})}return t}let me={add:`bg-emerald-400`,del:`bg-red-400`,mixed:`bg-amber-400`};function he(e,n,r){P.replaceChildren(...e.map(e=>{let i=t(`button`,`pointer-events-auto absolute w-full cursor-pointer ${me[e.tone]}`);return i.style.top=`${e.start/n*100}%`,i.style.height=`${Math.max(.4,(e.end-e.start+1)/n*100)}%`,i.title=`line ${ge(r,e.start)}`,i.onclick=()=>r[e.start].scrollIntoView({block:`center`}),i})),P.classList.remove(`hidden`)}let ge=(e,t)=>e[t]?.querySelector(`span`)?.textContent?.trim()||String(t+1);function _e(){let e=[],r=[],i=-1,a=t(`span`,`flex-none text-[10.5px] text-neutral-400`),o=t=>{e.length&&(i=(i+t+e.length)%e.length,r[e[i].start].scrollIntoView({block:`center`}),a.textContent=`${i+1}/${e.length}`)},s=()=>e.length>0&&!n.classList.contains(`hidden`),c=(e,n,r,i)=>{let a=t(`button`,`icon-btn flex-none`,e);return a.onclick=()=>o(n),u(a,i,r,()=>o(n),s),a};return{el:t(`div`,`flex flex-none items-center gap-1`,a,c(`↑`,-1,`Previous change`,[`p`,`k`]),c(`↓`,1,`Next change`,[`n`,`j`])),set(t,n){e=t,r=n,i=-1,a.textContent=`${t.length} change${t.length===1?``:`s`}`}}}async function ve(t,n){K(),N.classList.remove(`flex`,`flex-col`);let r=w.get(t)?.status!==`D`;N.replaceChildren(W(t,r),L(`…`));try{let{diff:i}=await e(F(`diff`,{base:x,head:S,file:t,context:`99999`}),`could not read this diff`);if(!J(n))return;if(!i||/^Binary files /m.test(i))return de(t,n);let a=fe(i);ue(a);let o=await c(t);if(!J(n))return;let s=f(a,o),l=[...s.children],u=pe(a);G.set(u,l),N.replaceChildren(W(t,r,G.el),s),he(u,a.length,l)}catch(e){J(n)&&N.replaceChildren(W(t,!1),L(String(e),`text-red-600`))}}let X=()=>S&&x===`${S}~1`?S:``,ye=()=>b.commits.find(e=>e.hash===X())?.subject??``,be=()=>X()?`${X()} — ${ye()||`commit`}`:`${x} ↔ ${S||`working tree`}`,xe=(e,n,r,i)=>{let a=t(`button`,`flex h-6 w-6 flex-none cursor-pointer items-center justify-center rounded-md ${r?`bg-indigo-50 text-indigo-700`:`text-neutral-400 hover:bg-neutral-100 hover:text-neutral-700`}`);return a.innerHTML=e,a.title=n,a.onclick=i,a};function Z(){let e=xe(`<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" class="h-3.5 w-3.5"><path d="M2.5 3h11l-4.25 5v4.5l-2.5 1.25V8L2.5 3z" stroke-linejoin="round"/></svg>`,`Show only files that differ`,T,()=>{T=!T,Z(),H()}),n=xe(`<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" class="h-3.5 w-3.5"><path d="m4.5 2.5 3.5 3 3.5-3M4.5 13.5l3.5-3 3.5 3" stroke-linecap="round" stroke-linejoin="round"/></svg>`,E?`Restore folders`:`Collapse all folders`,E,()=>{if(E=!E,E)D=[...O],O.clear();else for(let e of D)O.add(e);Z(),H()});if(!b.branch){j.replaceChildren(t(`div`,`flex items-center justify-end gap-1 p-1.5`,n));return}let r=C(be());if(X()){let e=b.commits.find(e=>e.hash===X());_(r,()=>e?m(e):``)}else r.title=`Choose what to diff`;r.onclick=async()=>{await Te(),ee(r,b,{base:x,head:S},(e,t)=>{x=e,S=t,Q()},Ee())};let[i,a]=[0,0];for(let e of w.values())i+=e.add,a+=e.del;j.replaceChildren(t(`div`,`flex flex-col gap-1 p-2`,r,t(`div`,`flex items-center gap-1.5 pl-1`,t(`span`,`text-[11px] text-neutral-400`,`${w.size} changed file${w.size===1?``:`s`}`),...U(i,a),t(`span`,`ml-auto`),e,n)))}let Se=()=>{v&&g&&ie(v,{cwd:g,base:x,head:S})},Ce=e=>{let t=e.replace(/~\d+$/,``);return!t||t===`HEAD`||b.refs.some(e=>e.name===t)||b.commits.some(e=>e.hash===t)};async function we(){if(w=new Map,!b.branch)return;let{files:t}=await e(F(`diff`,{base:x,head:S}),`could not read what changed`);for(let e of t)w.set(e.path,{status:e.status,add:e.add,del:e.del});if(w.size<=ne)for(let e of w.keys()){let t=e.split(`/`);for(let e=1;e<t.length;e++)O.add(t.slice(0,e).join(`/`))}}async function Q(){Se();try{await we()}catch(e){Z(),M.replaceChildren(L(String(e),`text-red-600`));return}Z(),H(),k&&Y(k)}function $(){let e=C(g||`Choose a folder…`,`neutral`);e.className+=` max-w-72`,e.title=`Switch folder`,e.onclick=()=>l(e,b.worktrees.map(e=>({path:e.path,...e.branch?{hint:e.branch}:{}})),g||void 0,o);let n=C(`Terminal`,`neutral`);n.title=`Open a shell in this folder`,n.onclick=()=>p(g);let r=t(`button`,`icon-btn`,`✕`);r.type=`button`,r.title=`Close Files`,r.setAttribute(`aria-label`,`Close Files`),r.onclick=h;let i=g?[n,r]:[r];i[0].classList.add(`ml-auto`),ae.replaceChildren(e,t(`span`,`truncate font-mono text-[11.5px] text-neutral-400`,b.branch?`⎇ ${b.branch}`:`no git`),...i)}async function Te(){if(g)try{b=await e(F(`git`,{}),`could not read this repository`),$()}catch(e){N.replaceChildren(L(String(e),`text-red-600`))}}let Ee=()=>b.refs.some(e=>e.name===`main`)?`main`:b.refs.some(e=>e.name===`master`)?`master`:`HEAD`;async function De(){if($(),k=null,A=null,O.clear(),K(),N.classList.remove(`flex`,`flex-col`),N.replaceChildren(L(`Select a file.`)),!g){j.replaceChildren(),M.replaceChildren(),N.replaceChildren(L(`No folder yet — pick one from the chip above.`));return}try{b=await e(F(`git`,{}),`could not read this repository`)}catch(e){b={branch:null,refs:[],commits:[],worktrees:[]},N.replaceChildren(L(String(e),`text-red-600`))}let t=re(v),n=t&&t.cwd===g&&Ce(t.base)&&Ce(t.head)?t:null;x=n?.base??Ee(),S=n?.head??``,$(),await Q()}return r(n,e=>{let t=a(),n=t?.id??``,r=e?.startsWith(`/`)?e:(n===v?g:``)||re(n)?.cwd||t?.cwd||``;if(r===g&&n===v&&M.childElementCount){$(),Te().then(Q);return}v=n,g=r,De()},()=>{K(),N.classList.remove(`flex`,`flex-col`),N.replaceChildren(L(`Select a file.`))})}export{w as createExplorerView};