@timqi/pier 0.1.0 → 0.1.1

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 (158) hide show
  1. package/README.md +58 -125
  2. package/dist/agent/config.js +6 -15
  3. package/dist/agent/credentials.js +11 -23
  4. package/dist/agent/events.js +42 -64
  5. package/dist/agent/listing.js +39 -92
  6. package/dist/agent/pi.js +103 -252
  7. package/dist/boards/boards.js +19 -29
  8. package/dist/channels/attach.js +14 -42
  9. package/dist/channels/chains.js +33 -37
  10. package/dist/channels/chunk.js +8 -28
  11. package/dist/channels/commands.js +3 -14
  12. package/dist/channels/config.js +33 -52
  13. package/dist/channels/control.js +4 -13
  14. package/dist/channels/conversations.js +8 -25
  15. package/dist/channels/dedup.js +8 -17
  16. package/dist/channels/gatekeeper.js +13 -23
  17. package/dist/channels/lark-api.js +23 -63
  18. package/dist/channels/lark-outbound.js +12 -44
  19. package/dist/channels/lark-panel.js +7 -23
  20. package/dist/channels/lark-render.js +18 -62
  21. package/dist/channels/lark.js +52 -141
  22. package/dist/channels/lines.js +13 -15
  23. package/dist/channels/panel.js +16 -36
  24. package/dist/channels/receipts.js +29 -52
  25. package/dist/channels/routes.js +3 -9
  26. package/dist/channels/runtime.js +12 -23
  27. package/dist/channels/slack-api.js +34 -86
  28. package/dist/channels/slack-directory.js +7 -23
  29. package/dist/channels/slack-outbound.js +12 -56
  30. package/dist/channels/slack-panel.js +4 -13
  31. package/dist/channels/slack-render.js +23 -91
  32. package/dist/channels/slack-tool.js +48 -171
  33. package/dist/channels/slack.js +73 -239
  34. package/dist/channels/telegram-api.js +8 -20
  35. package/dist/channels/telegram-panel.js +5 -21
  36. package/dist/channels/telegram-render.js +13 -40
  37. package/dist/channels/telegram.js +54 -146
  38. package/dist/channels/types.js +5 -16
  39. package/dist/cli.js +17 -41
  40. package/dist/config-sync.js +87 -4
  41. package/dist/core/hub.js +7 -20
  42. package/dist/core/identity.js +20 -59
  43. package/dist/core/inbound-file.js +15 -49
  44. package/dist/core/inbox.js +12 -34
  45. package/dist/core/queue.js +3 -5
  46. package/dist/core/reply.js +41 -142
  47. package/dist/core/router.js +209 -264
  48. package/dist/core/types.js +4 -0
  49. package/dist/db.js +88 -272
  50. package/dist/drain.js +57 -50
  51. package/dist/extensions/index.js +3 -11
  52. package/dist/extensions/web/anthropic.js +3 -9
  53. package/dist/extensions/web/artifacts.js +2 -5
  54. package/dist/extensions/web/content.js +4 -12
  55. package/dist/extensions/web/http.js +2 -6
  56. package/dist/extensions/web/language.js +8 -18
  57. package/dist/extensions/web/openai.js +1 -1
  58. package/dist/extensions/web/provider.js +5 -18
  59. package/dist/extensions/web/tools.js +19 -63
  60. package/dist/lock.js +98 -0
  61. package/dist/log.js +9 -26
  62. package/dist/main.js +84 -183
  63. package/dist/paths.js +10 -26
  64. package/dist/secrets.js +18 -45
  65. package/dist/service.js +31 -73
  66. package/dist/settings.js +19 -63
  67. package/dist/tasks/agent.js +24 -45
  68. package/dist/tasks/callbacks.js +8 -16
  69. package/dist/tasks/command.js +2 -6
  70. package/dist/tasks/definitions.js +39 -62
  71. package/dist/tasks/execution.js +41 -39
  72. package/dist/tasks/groups.js +8 -11
  73. package/dist/tasks/messages.js +88 -155
  74. package/dist/tasks/outbox.js +33 -54
  75. package/dist/tasks/routes.js +4 -7
  76. package/dist/tasks/runs.js +4 -9
  77. package/dist/tasks/service.js +29 -42
  78. package/dist/tasks/store.js +36 -27
  79. package/dist/tasks/tool.js +59 -60
  80. package/dist/tools-task.js +20 -60
  81. package/dist/tools.js +98 -325
  82. package/dist/update.js +20 -43
  83. package/dist/web/auth.js +118 -179
  84. package/dist/web/config-sync.js +2 -2
  85. package/dist/web/config.js +3 -7
  86. package/dist/web/explorer.js +10 -21
  87. package/dist/web/fs.js +20 -42
  88. package/dist/web/instance.js +35 -82
  89. package/dist/web/providers.js +5 -11
  90. package/dist/web/public/assets/{activity-Bl3vZukb.js → activity-B89_hH7q.js} +1 -1
  91. package/dist/web/public/assets/activity-B89_hH7q.js.br +0 -0
  92. package/dist/web/public/assets/activity-B89_hH7q.js.gz +0 -0
  93. package/dist/web/public/assets/{boards-DYuf4Mlj.js → boards-BeKW0ZXK.js} +1 -1
  94. package/dist/web/public/assets/boards-BeKW0ZXK.js.br +0 -0
  95. package/dist/web/public/assets/boards-BeKW0ZXK.js.gz +0 -0
  96. package/dist/web/public/assets/explorer-DIuMlaV3.js +4 -0
  97. package/dist/web/public/assets/explorer-DIuMlaV3.js.br +0 -0
  98. package/dist/web/public/assets/explorer-DIuMlaV3.js.gz +0 -0
  99. package/dist/web/public/assets/index-DzXDXra_.js +85 -0
  100. package/dist/web/public/assets/index-DzXDXra_.js.br +0 -0
  101. package/dist/web/public/assets/index-DzXDXra_.js.gz +0 -0
  102. package/dist/web/public/assets/index-eqQLVS8Q.css +2 -0
  103. package/dist/web/public/assets/index-eqQLVS8Q.css.br +0 -0
  104. package/dist/web/public/assets/index-eqQLVS8Q.css.gz +0 -0
  105. package/dist/web/public/assets/{runs-BLJu7EXN.js → runs-Cwy0mN8i.js} +1 -1
  106. package/dist/web/public/assets/runs-Cwy0mN8i.js.br +0 -0
  107. package/dist/web/public/assets/runs-Cwy0mN8i.js.gz +0 -0
  108. package/dist/web/public/assets/{settings-BrdVh-Zi.js → settings-DzZLmujq.js} +1 -1
  109. package/dist/web/public/assets/settings-DzZLmujq.js.br +0 -0
  110. package/dist/web/public/assets/settings-DzZLmujq.js.gz +0 -0
  111. package/dist/web/public/assets/{task-runs-CeQS1rxa.js → task-runs-BCakxFk8.js} +1 -1
  112. package/dist/web/public/assets/task-runs-BCakxFk8.js.br +0 -0
  113. package/dist/web/public/assets/task-runs-BCakxFk8.js.gz +0 -0
  114. package/dist/web/public/assets/tasks-BlzEbk11.js +4 -0
  115. package/dist/web/public/assets/tasks-BlzEbk11.js.br +0 -0
  116. package/dist/web/public/assets/tasks-BlzEbk11.js.gz +0 -0
  117. package/dist/web/public/index.html +30 -16
  118. package/dist/web/public/index.html.br +0 -0
  119. package/dist/web/public/index.html.gz +0 -0
  120. package/dist/web/public/sw.js +14 -2
  121. package/dist/web/public/sw.js.br +0 -0
  122. package/dist/web/public/sw.js.gz +0 -0
  123. package/dist/web/push.js +55 -77
  124. package/dist/web/route.js +3 -7
  125. package/dist/web/server.js +109 -190
  126. package/dist/web/session-state.js +13 -53
  127. package/dist/web/types.js +2 -4
  128. package/dist/web/webpush.js +10 -25
  129. package/docs/deploy.md +115 -330
  130. package/package.json +1 -1
  131. package/skills/pier-boards/SKILL.md +81 -160
  132. package/skills/pier-help/SKILL.md +23 -20
  133. package/skills/pier-slack/SKILL.md +2 -2
  134. package/skills/pier-tasks/SKILL.md +23 -15
  135. package/dist/config-sync-fetch.js +0 -84
  136. package/dist/limits.js +0 -14
  137. package/dist/web/public/assets/activity-Bl3vZukb.js.br +0 -0
  138. package/dist/web/public/assets/activity-Bl3vZukb.js.gz +0 -0
  139. package/dist/web/public/assets/boards-DYuf4Mlj.js.br +0 -0
  140. package/dist/web/public/assets/boards-DYuf4Mlj.js.gz +0 -0
  141. package/dist/web/public/assets/explorer-qJH_9nTE.js +0 -4
  142. package/dist/web/public/assets/explorer-qJH_9nTE.js.br +0 -0
  143. package/dist/web/public/assets/explorer-qJH_9nTE.js.gz +0 -0
  144. package/dist/web/public/assets/index-Dqdb-Eqt.js +0 -85
  145. package/dist/web/public/assets/index-Dqdb-Eqt.js.br +0 -0
  146. package/dist/web/public/assets/index-Dqdb-Eqt.js.gz +0 -0
  147. package/dist/web/public/assets/index-DzmMzvi_.css +0 -2
  148. package/dist/web/public/assets/index-DzmMzvi_.css.br +0 -0
  149. package/dist/web/public/assets/index-DzmMzvi_.css.gz +0 -0
  150. package/dist/web/public/assets/runs-BLJu7EXN.js.br +0 -0
  151. package/dist/web/public/assets/runs-BLJu7EXN.js.gz +0 -0
  152. package/dist/web/public/assets/settings-BrdVh-Zi.js.br +0 -0
  153. package/dist/web/public/assets/settings-BrdVh-Zi.js.gz +0 -0
  154. package/dist/web/public/assets/task-runs-CeQS1rxa.js.br +0 -0
  155. package/dist/web/public/assets/task-runs-CeQS1rxa.js.gz +0 -0
  156. package/dist/web/public/assets/tasks-bcb3fYdK.js +0 -4
  157. package/dist/web/public/assets/tasks-bcb3fYdK.js.br +0 -0
  158. package/dist/web/public/assets/tasks-bcb3fYdK.js.gz +0 -0
package/dist/web/fs.js CHANGED
@@ -1,14 +1,8 @@
1
- // Every path the Console reaches on disk resolves here: the browse/read/mkdir
2
- // routes and the session attachments server.ts serves all ask the same
3
- // question, and asked it several different ways until this file existed.
4
- //
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
10
- // the `root` it was asked with, realpath on both ends, so neither `..` nor a
11
- // symlink steps outside. Only mkdir writes, and only a name.
1
+ // Every path the Console reaches on disk resolves here. The boundary is the
2
+ // Console password, not a path: an owner past it picks any cwd, so anything
3
+ // this process can read is already reachable. `root` confines a *listing*, with
4
+ // realpath on both ends so neither `..` nor a symlink steps outside. Only mkdir
5
+ // writes, and only a name.
12
6
  import { mkdir, open, readdir, realpath, stat } from "node:fs/promises";
13
7
  import { homedir } from "node:os";
14
8
  import { basename, dirname, extname, isAbsolute, resolve, sep } from "node:path";
@@ -16,12 +10,10 @@ import { Readable } from "node:stream";
16
10
  import { logger } from "../log.js";
17
11
  import { guarded } from "./route.js";
18
12
  const log = logger("web");
19
- /** What a browser is asked to hold at once — the same ceiling for a preview
20
- * and for an attachment, because it is the reader's patience, not the route's. */
13
+ /** The reader's patience, so one ceiling for a preview and an attachment. */
21
14
  export const MAX_FILE_BYTES = 32 * 1024 * 1024;
22
- /** Types a browser may render as themselves. Everything else is sniffed for
23
- * text, and bytes we can't vouch for download instead of rendering — that is
24
- * how a file starts executing. */
15
+ /** Everything else is sniffed for text; bytes we can't vouch for download
16
+ * instead of rendering, which is how a file starts executing. */
25
17
  const RENDERABLE = {
26
18
  ".png": "image/png",
27
19
  ".jpg": "image/jpeg",
@@ -33,9 +25,7 @@ const RENDERABLE = {
33
25
  ".svg": "image/svg+xml",
34
26
  ".pdf": "application/pdf",
35
27
  };
36
- /** The containment check, and the only way a path becomes a path this reads:
37
- * `root` must be an absolute directory, and the returned target is `path`
38
- * resolved inside it. Throws — the route wrapper turns that into the answer. */
28
+ /** The containment check, and the only way a path becomes one this reads. */
39
29
  export async function scoped(root, path = "") {
40
30
  if (!root || !isAbsolute(root))
41
31
  throw new Error("not a directory this can read");
@@ -47,16 +37,13 @@ export async function scoped(root, path = "") {
47
37
  throw new Error("path escapes root");
48
38
  return target;
49
39
  }
50
- /** How bytes leave: the type they may be served as, and whether a browser may
51
- * render them in place. `download` forces the attachment disposition — the
52
- * chat's Download button, on a file it would otherwise show inline. */
40
+ /** `download` forces the attachment disposition on a file otherwise shown inline. */
53
41
  export function fileHeaders(file, bytes, download = false) {
54
42
  const ext = extname(file).toLowerCase();
55
43
  const known = RENDERABLE[ext];
56
44
  const text = !known && !bytes.subarray(0, 8192).includes(0);
57
- // An <img> renders its source whatever the disposition says, but a tab
58
- // navigated straight at an SVG runs the script inside it — same origin, past
59
- // the password. So an SVG keeps its type and loses only its own tab.
45
+ // A tab navigated straight at an SVG runs the script inside it, same
46
+ // origin, past the password; an <img> ignores the disposition anyway.
60
47
  const inline = !download && (text || (known !== undefined && ext !== ".svg"));
61
48
  return {
62
49
  "content-type": known ?? (text ? "text/plain; charset=utf-8" : "application/octet-stream"),
@@ -64,12 +51,8 @@ export function fileHeaders(file, bytes, download = false) {
64
51
  };
65
52
  }
66
53
  export function registerFsRoutes(app) {
67
- // One listing behind both trees: the Files view walks a project (`root`,
68
- // which confines it and is the floor `parent` stops at), the cwd pickers
69
- // walk from anywhere (no root, starting at home). Names and which are
70
- // directories — never contents, and never `.git`, which is plumbing. What to
71
- // leave out of the answer is the caller's business: one tree shows folders
72
- // only, the other shows files too.
54
+ // The Files view walks a project (`root` confines it); the cwd pickers walk
55
+ // from anywhere. Never contents, and never `.git`.
73
56
  guarded(app, "GET", "/api/fs/ls", 404, async (c) => {
74
57
  c.header("cache-control", "no-store");
75
58
  const asked = c.req.query("path");
@@ -83,7 +66,6 @@ export function registerFsRoutes(app) {
83
66
  .sort((a, b) => Number(b.dir) - Number(a.dir) || a.name.localeCompare(b.name));
84
67
  return c.json({ path: dir, parent: dir === top || up === dir ? null : up, entries });
85
68
  });
86
- // File bytes, read-only: the Files view's previews and whole-file reads.
87
69
  guarded(app, "GET", "/api/fs/file", 404, async (c) => {
88
70
  const file = await scoped(c.req.query("root"), c.req.query("path"));
89
71
  const handle = await open(file);
@@ -94,10 +76,8 @@ export function registerFsRoutes(app) {
94
76
  throw new Error("not a file");
95
77
  if (info.size > MAX_FILE_BYTES)
96
78
  return c.json({ error: "file too large" }, 413);
97
- // Size and mtime, not a digest: the view re-reads a file every time it is
98
- // clicked again, and a validator the stat above already knows costs
99
- // nothing to offer. `no-cache` keeps the answer conditional, so an edited
100
- // file is never shown from a browser cache.
79
+ // `no-cache` keeps the answer conditional, so an edited file is never
80
+ // shown from a browser cache.
101
81
  const tag = `"${info.size.toString(16)}-${info.mtime.getTime().toString(16)}"`;
102
82
  const validators = {
103
83
  etag: tag,
@@ -106,14 +86,13 @@ export function registerFsRoutes(app) {
106
86
  };
107
87
  if (c.req.header("if-none-match") === tag)
108
88
  return c.body(null, 304, validators);
109
- // Sniff and stream the same open file, so a replacement cannot bypass
110
- // the size check or make the headers describe different bytes.
89
+ // The same open file, so a replacement cannot make the headers describe
90
+ // different bytes.
111
91
  const head = Buffer.alloc(Math.min(8192, info.size));
112
92
  await handle.read(head, 0, head.length, 0);
113
93
  const bytes = handle.createReadStream({ start: 0 });
114
94
  streaming = true;
115
- // Past the headers a failure can only truncate the body, so this is the
116
- // one place it can still be said at all (§5b).
95
+ // Past the headers a failure can only truncate the body (§5).
117
96
  bytes.on("error", (err) => log.warn(`serving ${file} stopped mid-stream`, err));
118
97
  return c.body(Readable.toWeb(bytes), 200, {
119
98
  ...fileHeaders(file, head),
@@ -125,8 +104,7 @@ export function registerFsRoutes(app) {
125
104
  await handle.close();
126
105
  }
127
106
  });
128
- // Create a folder while picking one — a new project usually needs a new
129
- // directory. A name, never a path: traversal is rejected, not normalized.
107
+ // A name, never a path: traversal is rejected, not normalized.
130
108
  guarded(app, "POST", "/api/fs/mkdir", 400, async (c) => {
131
109
  const body = await c.req.json().catch(() => null);
132
110
  const parent = typeof body?.path === "string" ? body.path : "";
@@ -1,11 +1,9 @@
1
- // Routes about the Pier instance itself — settings, update availability,
2
- // layer-1 secrets control, the browser's error reports. Nothing here touches
3
- // a session; server.ts stays the session/event surface.
1
+ // Routes about the Pier instance itself: settings, updates, secrets control,
2
+ // the browser's error reports. Nothing here touches a session.
4
3
  import { logger } from "../log.js";
5
4
  import { normalizeModelMenu, normalizeModelRef, normalizePublicUrl, } from "../settings.js";
6
- /** A rule that failed inside the transaction — its own class so the route can
7
- * tell it from a real fault. 400 when the request named something this Pier
8
- * does not have, 409 when it collided with the state as it stands. */
5
+ /** A rule that failed inside the transaction, distinct from a real fault: 400
6
+ * for a name this Pier does not have, 409 for a collision with current state. */
9
7
  class Refusal extends Error {
10
8
  status;
11
9
  constructor(message, status) {
@@ -13,21 +11,16 @@ class Refusal extends Error {
13
11
  this.status = status;
14
12
  }
15
13
  }
16
- /** One name added to or removed from a stored set, order preserved. */
17
14
  const withName = (current, { name, on }) => on ? [...new Set([...current, name])] : current.filter((each) => each !== name);
18
- /** Client reports per minute, for the whole server: a browser bug can fire in
19
- * a loop, and the journal is shared with everything else Pier says. */
15
+ /** A browser bug can fire in a loop, and the journal is shared. */
20
16
  const CLIENT_LOG_PER_MINUTE = 60;
21
17
  export function registerInstanceRoutes(app, deps) {
22
18
  const { settings, updates, updater = null, secrets, catalog, names = { extensions: [], tools: [] }, onUnlocked, onSettingsChanged, onToolsChanged, validateCustomTools, } = deps;
23
19
  const updateLog = logger("update");
24
- // How long POST /api/update may hold its response open. A busy Pier drains
25
- // first, which can take minutes, and a response held that long dies at every
26
- // proxy on the way (principle 7): past this cap the answer is "draining".
20
+ // A busy Pier drains first, which can take minutes, and a response held that
21
+ // long dies at every proxy: past this cap the answer is "draining".
27
22
  const APPLY_REPLY_CAP_MS = 10_000;
28
- // The browser's half of the log. A workbench that threw after the response
29
- // left the server is otherwise invisible here (ui/report.ts) — this is the
30
- // one route whose entire purpose is to make it visible.
23
+ // A workbench that threw after the response left is otherwise invisible here.
31
24
  const clientLog = logger("client");
32
25
  let reports = [];
33
26
  app.post("/api/client-log", async (c) => {
@@ -43,44 +36,33 @@ export function registerInstanceRoutes(app, deps) {
43
36
  const cap = (value, max) => typeof value === "string" ? value.slice(0, max) : "";
44
37
  const where = cap(body.view, 120);
45
38
  const stack = cap(body.stack, 2000);
46
- // One line, ua included: "only on iOS" is the answer half these questions
47
- // have, and the report is the only place it exists.
39
+ // ua included: "only on iOS" is the answer half these questions have.
48
40
  clientLog.warn(`${cap(body.message, 500)} [${where || "/"}] ${cap(c.req.header("user-agent"), 160)}` +
49
41
  (stack ? `\n${stack}` : ""));
50
42
  return c.body(null, 204);
51
43
  });
52
- // Instance settings. The password lives behind its own route (web/auth.ts):
53
- // it is a credential, and changing it takes the old one.
54
- // The catalog rides along: one round trip for the whole page, and the
55
- // switches cannot disagree with the setting they are drawn from. One shape
56
- // for both the read and the write, or the page reconciles two answers.
44
+ // The catalog rides along so the switches cannot disagree with the setting
45
+ // they are drawn from. One shape for read and write.
57
46
  const instanceSettings = async () => {
58
47
  const shown = await catalog?.();
59
48
  return {
60
49
  ...settings.get(),
61
50
  catalog: shown?.entries ?? [],
62
- /** Where the runs are: the install and every daily update is one task's
63
- * history, not a second status surface this route invented. */
51
+ /** The install and every daily update is one task's history. */
64
52
  toolsTaskId: shown?.toolsTaskId ?? null,
65
53
  };
66
54
  };
67
55
  app.get("/api/settings", async (c) => c.json(await instanceSettings()));
68
- // What the version badge reads: the two versions, whether this instance can
69
- // do anything about the gap, and whether it is allowed to do it unattended.
70
- // `statusNow` so a browser opened seconds after a restart is told the truth
71
- // rather than "no idea yet".
56
+ // `statusNow` so a browser opened seconds after a restart is told the truth.
72
57
  app.get("/api/update", async (c) => c.json({
73
58
  ...(await updates.statusNow()),
74
59
  canApply: updater !== null,
75
60
  autoUpdate: settings.get().autoUpdate,
76
- // Reported whether or not an update is pending: the repair is the same,
77
- // and finding out at the next restart is finding out too late.
61
+ // Whether or not an update is pending: the next restart is too late.
78
62
  problem: updater?.problem() ?? null,
79
63
  }));
80
- // Applying. Nothing is installed here: the work is handed to the service
81
- // manager's own oneshot unit, which backs the database up and installs while
82
- // Pier still runs, then stops it and starts the new version — an npm child of
83
- // this process would be killed by the very restart it is performing.
64
+ // Handed to the service manager's oneshot unit: an npm child of this
65
+ // process would be killed by the very restart it is performing.
84
66
  app.post("/api/update", async (c) => {
85
67
  if (!updater) {
86
68
  return c.json({ error: "no service manager owns this Pier — update it with: pier update" }, 409);
@@ -103,8 +85,7 @@ export function registerInstanceRoutes(app, deps) {
103
85
  new Promise((resolve) => setTimeout(resolve, APPLY_REPLY_CAP_MS, "draining").unref()),
104
86
  ]);
105
87
  if (started === "draining") {
106
- // The handover keeps running behind this response; if it fails later,
107
- // main.ts's takeWorkAgain reports it and reopens the gate (§5b).
88
+ // If the handover fails later, main.ts reports it and reopens the gate (§5).
108
89
  updateLog.info(`updating to ${latest} on the Console's request — waiting for running work to finish`);
109
90
  return c.json({ started: true, draining: true, latest }, 202);
110
91
  }
@@ -122,12 +103,8 @@ export function registerInstanceRoutes(app, deps) {
122
103
  updateLog.info(`updating to ${latest} on the Console's request — Pier stops and starts again`);
123
104
  return c.json({ started: true, latest });
124
105
  });
125
- // Partial on purpose: each surface sends only the setting it edits, and a
126
- // malformed field is rejected before anything is written.
127
- //
128
- // A switch sends a *delta* (`tool`/`extension`), not the list it computed:
129
- // two quick clicks each carry a list built from what the page knew a moment
130
- // ago, so the second silently drops the first.
106
+ // Partial: each surface sends only what it edits. A switch sends a delta,
107
+ // not a list: two quick clicks each carrying a list would drop one.
131
108
  app.put("/api/settings", async (c) => {
132
109
  const body = await c.req.json().catch(() => null);
133
110
  const fields = body
@@ -138,9 +115,8 @@ export function registerInstanceRoutes(app, deps) {
138
115
  error: "publicUrl, modelMenu, titleModel, autoUpdate, customTools, extension or tool required",
139
116
  }, 400);
140
117
  }
141
- // Everything is validated before anything is written, and everything is
142
- // written in one transaction: a request carrying a new custom tool *and*
143
- // the switch that turns it on must not leave one of the two stored.
118
+ // One transaction: a new custom tool and the switch that turns it on must
119
+ // not leave one of the two stored.
144
120
  const writes = [];
145
121
  const refuse = (error) => c.json({ error }, 400);
146
122
  if (body?.publicUrl !== undefined) {
@@ -170,9 +146,8 @@ export function registerInstanceRoutes(app, deps) {
170
146
  return refuse("autoUpdate must be a boolean");
171
147
  writes.push(() => settings.setAutoUpdate(autoUpdate));
172
148
  }
173
- /** The blocks this request declares, or null when it does not touch them.
174
- * Adding a tool is declaring it *and* switching it on, so a name declared
175
- * here is switchable in the same write. */
149
+ /** Null when the request does not touch them; a name declared here is
150
+ * switchable in the same write. */
176
151
  let declared = null;
177
152
  if (body?.customTools !== undefined) {
178
153
  const validated = validateCustomTools?.(body.customTools) ??
@@ -183,8 +158,7 @@ export function registerInstanceRoutes(app, deps) {
183
158
  const custom = validated.tools;
184
159
  writes.push(() => settings.setCustomTools(custom));
185
160
  }
186
- /** A single switch, applied to the set as it is *now* rather than to the
187
- * list the browser had. Returns the name, or the refusal. */
161
+ /** Applied to the set as it is now, not the list the browser had. */
188
162
  const delta = (raw) => {
189
163
  const given = typeof raw === "object" && raw !== null ? raw : null;
190
164
  const name = typeof given?.name === "string" ? given.name.trim() : "";
@@ -200,8 +174,6 @@ export function registerInstanceRoutes(app, deps) {
200
174
  extensionOne = one;
201
175
  writes.push(() => settings.setExtensions(withName(settings.get().extensions, one)));
202
176
  }
203
- // Whether the enabled set moved, for the install below. The delta itself
204
- // is resolved inside the transaction, against the set as it was stored.
205
177
  let toolsChanged = false;
206
178
  let toolOne = null;
207
179
  if (body?.tool !== undefined) {
@@ -212,10 +184,7 @@ export function registerInstanceRoutes(app, deps) {
212
184
  toolsChanged = true;
213
185
  writes.push(() => settings.setTools(withName(settings.get().tools, one)));
214
186
  }
215
- // What ubix says about the blocks this request would undeclare. Asked out
216
- // here because answering it spawns a subprocess, and a transaction may not
217
- // wait on one; the *sets* below are read inside the transaction, where
218
- // nothing can move them.
187
+ // Asked outside the transaction: it spawns a subprocess.
219
188
  const dropping = declared !== null &&
220
189
  settings.get().customTools.some((tool) => !declared?.some((kept) => kept.name === tool.name));
221
190
  const shown = dropping ? await catalog?.() : undefined;
@@ -223,20 +192,10 @@ export function registerInstanceRoutes(app, deps) {
223
192
  const entry = shown?.entries.find((row) => row.source === "binary" && row.name === name);
224
193
  return entry?.source === "binary" ? entry.binary : null;
225
194
  };
226
- /**
227
- * The rules that are about *sets*, applied inside the transaction against
228
- * the settings as they are at that instant. Outside it they are a
229
- * time-of-check: a request dropping a declaration and another switching
230
- * that tool on both passed, and left an enabled tool nothing declares.
231
- *
232
- * What may be switched on comes from `names`, which is code and cannot
233
- * change while this runs, plus what this request declares. What may be
234
- * undeclared is the second rule: a block is the only thing that can
235
- * uninstall its binary, so it may not go while the tool is on, installed,
236
- * broken, or while ubix's answer about it could not be read. Switching a
237
- * name *off* is never refused — that is the repair, and it is the first
238
- * half of removing one.
239
- */
195
+ /** Inside the transaction, or two requests both pass and leave an enabled
196
+ * tool nothing declares. A block is the only thing that can uninstall its
197
+ * binary, so it may not go while the tool is on, installed or broken;
198
+ * switching off is never refused, being the first half of removing one. */
240
199
  const check = () => {
241
200
  const current = settings.get();
242
201
  const after = (declared ?? current.customTools).map((tool) => tool.name);
@@ -269,24 +228,20 @@ export function registerInstanceRoutes(app, deps) {
269
228
  });
270
229
  }
271
230
  catch (err) {
272
- // Nothing was written: the transaction rolled back with it.
273
231
  if (err instanceof Refusal)
274
232
  return c.json({ error: err.message }, err.status);
275
233
  throw err;
276
234
  }
277
- // Stored first, then acted on: the switch shows what was written even when
278
- // the install cannot start — and then says why, here, rather than leaving
279
- // "saved" as the last thing anyone was told.
235
+ // Stored first: the switch shows what was written even when the install
236
+ // cannot start, and then says why.
280
237
  const note = toolsChanged ? await onToolsChanged?.() : null;
281
- // The URL and the extension set are both read when a session opens; the
282
- // model menu is read per picker call, so it needs no recycle.
238
+ // Read when a session opens; the model menu is read per picker call.
283
239
  if (body?.publicUrl !== undefined || body?.extension !== undefined)
284
240
  onSettingsChanged?.();
285
241
  return c.json({ ...(await instanceSettings()), ...(note ? { toolsSync: note } : {}) });
286
242
  });
287
- // Layer-1 key status and control (Console → Settings → Security). The GET
288
- // is what a locked instance shows; unlock is how it recovers without a
289
- // restart, and rotate is the only way to change how the KEK is protected.
243
+ // Layer-1 key status and control; unlock is how a locked instance recovers
244
+ // without a restart.
290
245
  const secretsStatus = () => ({
291
246
  state: secrets.state,
292
247
  mode: secrets.mode ?? null,
@@ -303,9 +258,7 @@ export function registerInstanceRoutes(app, deps) {
303
258
  onUnlocked?.();
304
259
  return c.json(secretsStatus());
305
260
  });
306
- // Why vt cannot hand the key over is vt's answer, not Pier's: config
307
- // sources, routing, whether an agent is listening. Read-only and safe while
308
- // locked — without it "locked" is one error string and no way to repair it.
261
+ // Safe while locked; without it "locked" is one error string and no way to repair.
309
262
  app.get("/api/secrets/doctor", async (c) => {
310
263
  try {
311
264
  return c.json({ report: await secrets.doctor() });
@@ -1,10 +1,8 @@
1
1
  // HTTP boundary for provider configuration.
2
2
  import { isProviderApi, MODEL_EFFORTS, validateProviderSetup } from "../core/types.js";
3
3
  import { ProviderFlows } from "./provider-flows.js";
4
- /** Unknown JSON into a trimmed, validated ProviderSetup. Shape lives here;
5
- * the rules (id charset, endpoint safety, model limits) are the shared
6
- * validateProviderSetup in core — web/ cannot import agent/ to reuse its
7
- * copy, and must not re-implement them. */
4
+ /** Shape here; the rules are core's validateProviderSetup, since web/ cannot
5
+ * import agent/ and must not re-implement them. */
8
6
  function setupFrom(raw) {
9
7
  if (typeof raw !== "object" || raw === null)
10
8
  return null;
@@ -45,9 +43,7 @@ function setupFrom(raw) {
45
43
  };
46
44
  }
47
45
  export function registerProviderRoutes(app, providers,
48
- /** A session picks its providers up when it opens, so one that is already
49
- * live cannot use what was just configured: server.ts recycles the idle
50
- * ones after every change of credentials or structure. */
46
+ /** A session picks its providers up when it opens; idle ones are recycled. */
51
47
  onProvidersChanged = () => { }) {
52
48
  const flows = new ProviderFlows(providers);
53
49
  const configure = async (setup) => {
@@ -135,10 +131,8 @@ onProvidersChanged = () => { }) {
135
131
  return c.json({ error: String(err) }, 404);
136
132
  }
137
133
  });
138
- // A probe, so nothing is written and nothing is recycled. 200 either way:
139
- // "the key is revoked" is a successful answer to "does this work", and only
140
- // a request that could not be made at all is a 400 — including one that
141
- // names no model, because nothing here picks one for you.
134
+ // 200 either way: "the key is revoked" is a successful answer to "does this
135
+ // work". Only a request that could not be made is a 400.
142
136
  app.post("/api/providers/:provider/check", async (c) => {
143
137
  const body = await c.req.json().catch(() => null);
144
138
  const model = typeof body?.model === "string" ? body.model.trim() : "";
@@ -1,4 +1,4 @@
1
- import{G as e,H as t,O as n,P as r,R as i,S as a,V as o,W as s,Z as c,q as l,v as u}from"./index-Dqdb-Eqt.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?`-`:o(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(o,p,h){let g=`sessions`,_=`active`,v={sessions:[],runs:[],messages:[]},y=``,b=e(async()=>{let e=_,n=await l(`/api/activity?scope=${e}`,`Failed to load activity`);if(!n.ok){y=``,o.replaceChildren(t(`p`,`p-4 text-[13px] text-red-600`,n.error));return}if(e!==_)return;let r=JSON.stringify(n.value);if(`${e}:${r}`===y)return;y=`${e}:${r}`;let i=n.value;v={...i,sessions:i.sessions.map(e=>({...e,title:c(e.title)}))},x()});function x(){let e=r(n([[`Sessions`,`sessions`],[`Relationships`,`dependencies`]],g,e=>{g=e,x()}),t(`span`,`ml-auto text-[11.5px] text-neutral-400`,`${v.sessions.length} sessions · ${v.runs.length} runs`),n([[`Current`,`active`],[`Last 24h`,`recent`]],_,e=>{_=e,x(),b()})),i=t(`div`,`automation-list min-h-0 flex-1 overflow-auto`);g===`sessions`?S(i):w(i),o.replaceChildren(e,i)}function S(e){let n=document.createElement(`table`);n.className=`w-full table-fixed text-left text-[12.5px]`,n.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{G as e,H as t,O as n,P as r,R as i,S as a,V as o,W as s,Z as c,q as l,v as u}from"./index-DzXDXra_.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?`-`:o(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(o,p,h){let g=`sessions`,_=`active`,v={sessions:[],runs:[],messages:[]},y=``,b=e(async()=>{let e=_,n=await l(`/api/activity?scope=${e}`,`Failed to load activity`);if(!n.ok){y=``,o.replaceChildren(t(`p`,`p-4 text-[13px] text-red-600`,n.error));return}if(e!==_)return;let r=JSON.stringify(n.value);if(`${e}:${r}`===y)return;y=`${e}:${r}`;let i=n.value;v={...i,sessions:i.sessions.map(e=>({...e,title:c(e.title)}))},x()});function x(){let e=r(n([[`Sessions`,`sessions`],[`Relationships`,`dependencies`]],g,e=>{g=e,x()}),t(`span`,`ml-auto text-[11.5px] text-neutral-400`,`${v.sessions.length} sessions · ${v.runs.length} runs`),n([[`Current`,`active`],[`Last 24h`,`recent`]],_,e=>{_=e,x(),b()})),i=t(`div`,`automation-list min-h-0 flex-1 overflow-auto`);g===`sessions`?S(i):w(i),o.replaceChildren(e,i)}function S(e){let n=document.createElement(`table`);n.className=`w-full table-fixed text-left text-[12.5px]`,n.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{E as e,H as t,K as n,N as r,R as i,S as a,U as o,X as s,Y as c,q as l,x as u,y as d,z as f}from"./index-Dqdb-Eqt.js";var p=e=>e.public&&e.token?`/p/${e.slug}-${e.token}/`:`/boards/${e.slug}/`;function m(m,h){let g=[],_=``,v=t(`header`,`pagehead sticky top-[-8px] z-30 max-md:hidden`,e(`Boards`)),y=t(`div`,`px-4 pb-5 pt-1`);m.append(t(`div`,`min-h-0 flex-1 overflow-y-auto`,v,y));async function b(e,t){let r=await s(`/api/boards/${e.slug}`,{public:t},`PATCH`);if(r.ok)return e.token=(await r.json().catch(()=>({}))).token??``,w();_=await n(r,`Could not change ${e.slug}`),await T()}async function x(e){_=await c(`/api/boards/${e}`,`DELETE`,`Could not delete ${e}`)??``,await T()}function S(e){let n=t(`div`,`group flex flex-col gap-1 py-2.5`),i=document.createElement(`a`);i.className=`min-w-0 flex-1 truncate text-[13px] font-medium text-neutral-800 hover:text-indigo-700 max-md:basis-full`,i.href=p(e),i.target=`_blank`,i.rel=`noreferrer`,i.textContent=e.title;let a=t(`div`,`flex flex-wrap items-center gap-x-2 gap-y-1`,i),s=r(``,``,e.public,t=>{e.public=t,b(e,t)});s.className=`flex flex-none cursor-pointer items-center gap-1.5 text-[11.5px] text-neutral-500`,s.append(t(`span`,``,`Public`));let c=d(`Delete`,`flex-none cursor-pointer text-[11.5px] text-neutral-400 opacity-0 transition-opacity hover:text-red-600 group-hover:opacity-100 pointer-coarse:opacity-100`);c.title=`Renames the folder on disk; nothing is erased`,c.onclick=()=>void x(e.slug);let l=f(`flex-none cursor-pointer text-[11.5px] text-neutral-400 opacity-0 transition-opacity hover:text-indigo-600 group-hover:opacity-100 pointer-coarse:opacity-100`,()=>`${location.origin}${p(e)}`);l.title=`Copy the board's link`,a.append(t(`div`,`ml-auto flex flex-none items-center gap-2`,s,l,c)),n.append(a);let u=t(`div`,`flex flex-wrap items-center gap-x-3 gap-y-1 text-[11.5px] text-neutral-500`);u.append(t(`span`,`min-w-0 break-all font-mono text-neutral-400`,e.slug)),e.description&&u.append(t(`span`,`min-w-0 truncate`,e.description)),u.append(t(`span`,`flex-none text-neutral-400`,o(Date.parse(e.updatedAt))));for(let t of e.sessions){let e=d(t.slice(0,8),`flex-none cursor-pointer font-mono text-indigo-600 hover:underline`);e.title=`Open session ${t}`,e.onclick=()=>h(t),u.append(e)}if(e.public&&e.token){let n=t(`span`,`min-w-0 break-all font-mono text-neutral-400`,p(e));n.title=`Public URL — anyone holding this link can read the board, no password`,u.append(n)}return n.append(u),n}let C=[{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 w(){let e=t(`div`,`mx-auto flex w-full max-w-3xl flex-col gap-4`);g.length===0&&e.append(a(`No boards yet. Ask an agent to build one — it writes a folder under ~/.pier/boards.`));for(let{title:n,hint:r,wanted:i}of C){let a=g.filter(e=>e.public===i);a.length!==0&&e.append(u(n,r,t(`div`,`flex flex-col divide-y divide-neutral-200/70`,...a.map(S))))}y.replaceChildren(..._?[t(`p`,`mb-3 text-[13px] text-red-600`,_)]:[],e)}async function T(){let e=await l(`/api/boards`,`Could not load boards`);if(!e.ok)return _=e.error,w();g=e.value,w(),_=``}let E=i(m,()=>void T());return window.addEventListener(`focus`,()=>{E.visible&&T()}),E}export{m as createBoardsView};
1
+ import{E as e,H as t,K as n,N as r,R as i,S as a,U as o,X as s,Y as c,q as l,x as u,y as d,z as f}from"./index-DzXDXra_.js";var p=e=>e.public&&e.token?`/p/${e.slug}-${e.token}/`:`/boards/${e.slug}/`;function m(m,h){let g=[],_=``,v=t(`header`,`pagehead sticky top-[-8px] z-30 max-md:hidden`,e(`Boards`)),y=t(`div`,`px-4 pb-5 pt-1`);m.append(t(`div`,`min-h-0 flex-1 overflow-y-auto`,v,y));async function b(e,t){let r=await s(`/api/boards/${e.slug}`,{public:t},`PATCH`);if(r.ok)return e.token=(await r.json().catch(()=>({}))).token??``,w();_=await n(r,`Could not change ${e.slug}`),await T()}async function x(e){_=await c(`/api/boards/${e}`,`DELETE`,`Could not delete ${e}`)??``,await T()}function S(e){let n=t(`div`,`group flex flex-col gap-1 py-2.5`),i=document.createElement(`a`);i.className=`min-w-0 flex-1 truncate text-[13px] font-medium text-neutral-800 hover:text-indigo-700 max-md:basis-full`,i.href=p(e),i.target=`_blank`,i.rel=`noreferrer`,i.textContent=e.title;let a=t(`div`,`flex flex-wrap items-center gap-x-2 gap-y-1`,i),s=r(``,``,e.public,t=>{e.public=t,b(e,t)});s.className=`flex flex-none cursor-pointer items-center gap-1.5 text-[11.5px] text-neutral-500`,s.append(t(`span`,``,`Public`));let c=d(`Delete`,`flex-none cursor-pointer text-[11.5px] text-neutral-400 opacity-0 transition-opacity hover:text-red-600 group-hover:opacity-100 pointer-coarse:opacity-100`);c.title=`Renames the folder on disk; nothing is erased`,c.onclick=()=>void x(e.slug);let l=f(`flex-none cursor-pointer text-[11.5px] text-neutral-400 opacity-0 transition-opacity hover:text-indigo-600 group-hover:opacity-100 pointer-coarse:opacity-100`,()=>`${location.origin}${p(e)}`);l.title=`Copy the board's link`,a.append(t(`div`,`ml-auto flex flex-none items-center gap-2`,s,l,c)),n.append(a);let u=t(`div`,`flex flex-wrap items-center gap-x-3 gap-y-1 text-[11.5px] text-neutral-500`);u.append(t(`span`,`min-w-0 break-all font-mono text-neutral-400`,e.slug)),e.description&&u.append(t(`span`,`min-w-0 truncate`,e.description)),u.append(t(`span`,`flex-none text-neutral-400`,o(Date.parse(e.updatedAt))));for(let t of e.sessions){let e=d(t.slice(0,8),`flex-none cursor-pointer font-mono text-indigo-600 hover:underline`);e.title=`Open session ${t}`,e.onclick=()=>h(t),u.append(e)}if(e.public&&e.token){let n=t(`span`,`min-w-0 break-all font-mono text-neutral-400`,p(e));n.title=`Public URL — anyone holding this link can read the board, no password`,u.append(n)}return n.append(u),n}let C=[{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 w(){let e=t(`div`,`mx-auto flex w-full max-w-3xl flex-col gap-4`);g.length===0&&e.append(a(`No boards yet. Ask an agent to build one — it writes a folder under ~/.pier/boards.`));for(let{title:n,hint:r,wanted:i}of C){let a=g.filter(e=>e.public===i);a.length!==0&&e.append(u(n,r,t(`div`,`flex flex-col divide-y divide-neutral-200/70`,...a.map(S))))}y.replaceChildren(..._?[t(`p`,`mb-3 text-[13px] text-red-600`,_)]:[],e)}async function T(){let e=await l(`/api/boards`,`Could not load boards`);if(!e.ok)return _=e.error,w();g=e.value,w(),_=``}let E=i(m,()=>void T());return window.addEventListener(`focus`,()=>{E.visible&&T()}),E}export{m as createBoardsView};
@@ -0,0 +1,4 @@
1
+ import{$ as e,B as t,H as n,J as r,L as i,R as a,U as o,a as s,at as c,c as l,d as u,et as d,f,i as p,it as m,o as h,p as g}from"./index-DzXDXra_.js";var ee=[[`path`,{d:`M12 5v14`}],[`path`,{d:`m19 12-7 7-7-7`}]],te=[[`path`,{d:`M12 22v-6`}],[`path`,{d:`M12 8V2`}],[`path`,{d:`M4 12H2`}],[`path`,{d:`M10 12H8`}],[`path`,{d:`M16 12h-2`}],[`path`,{d:`M22 12h-2`}],[`path`,{d:`m15 19-3-3-3 3`}],[`path`,{d:`m15 5-3 3-3-3`}]],ne=[[`path`,{d:`M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z`}]],re=[[`path`,{d:`M15 6a9 9 0 0 0-9 9V3`}],[`circle`,{cx:`18`,cy:`6`,r:`3`}],[`circle`,{cx:`6`,cy:`18`,r:`3`}]],_=[[`path`,{d:`M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8`}],[`path`,{d:`M3 3v5h5`}]],v=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())}`},ie=e=>[e.subject,e.body,[[e.author,e.email?`<${e.email}>`:``].filter(Boolean).join(` `),e.at?v(e.at):``].filter(Boolean).join(` · `)].filter(Boolean).join(`
2
+
3
+ `),y=null;function b(){y&&(y.remove(),y=null,document.removeEventListener(`pointerdown`,b,!0),window.removeEventListener(`scroll`,b,!0))}function x(e,t){e.onmouseenter=()=>{b();let r=t();if(!r)return;y=n(`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`),y.textContent=r,document.body.append(y),document.addEventListener(`pointerdown`,b,!0),window.addEventListener(`scroll`,b,!0);let i=e.getBoundingClientRect();y.style.left=`${Math.max(8,Math.min(i.left,window.innerWidth-y.offsetWidth-8))}px`,y.style.top=i.bottom+6+y.offsetHeight>window.innerHeight?`${i.top-y.offsetHeight-6}px`:`${i.bottom+6}px`},e.onmouseleave=b}function S(t,r,i){let a=n(`button`,`flex w-full cursor-pointer items-center gap-1.5 px-2 py-1 text-left hover:bg-neutral-100`,e(m,`h-3 w-3 text-indigo-600 ${t.value===r?``:`invisible`}`),n(`span`,`inline-flex flex-none items-center gap-1.5 font-mono text-[11.5px] text-neutral-800`,t.label??t.value));return t.subject&&a.append(n(`span`,`min-w-0 truncate text-[11px] text-neutral-400`,t.subject)),t.author&&a.append(n(`span`,`ml-auto flex-none pl-1.5 text-[10.5px] text-neutral-400`,`${t.author} · ${t.at?o(t.at):``}`)),x(a,()=>ie(t)),a.onclick=()=>i(t.value),a}function ae(t,r,i,a,o){let{base:c,head:l}=i,u=()=>l&&c===`${l}~1`?l:``,d=u()?`commit`:`compare`,f=r.commits[0],p=e=>[...e?[{value:``,label:`Working tree`,subject:`the files on disk, uncommitted changes included`}]:[],{...f,value:`HEAD`},...r.refs.map(e=>({value:e.name,subject:e.subject})),...r.commits.map(e=>({...e,value:e.hash}))],m=n(`div`,`flex w-[38rem] max-w-full flex-col`);function g(){let t=(e,t)=>{let r=n(`button`,`cursor-pointer rounded-full px-2.5 py-0.5 text-[11.5px] ${d===t?`bg-indigo-50 font-medium text-indigo-700`:`text-neutral-500 hover:bg-neutral-100`}`,e);return r.onclick=()=>{d=t,g()},r},i=(e,t,r,i)=>n(`div`,`flex min-w-0 flex-1 flex-col`,n(`div`,`flex-none px-2 pb-0.5 text-[10.5px] font-semibold uppercase tracking-wide text-neutral-400`,e),n(`div`,`min-h-0 flex-1 overflow-y-auto`,...t.map(e=>S(e,r,i)))),f=d===`compare`?n(`div`,`flex h-72 min-h-0 divide-x divide-neutral-200 pb-1`,i(`base`,p(!1),c,e=>{c=e,g(),a(c,l)}),i(`to`,p(!0),l,e=>{l=e,g(),a(c,l)})):n(`div`,`flex h-72 min-h-0 flex-col overflow-y-auto pb-1`,...r.commits.map(e=>S({value:e.hash,...e},u(),e=>{c=`${e}~1`,l=e,s(),a(c,l)}))),h=n(`div`,`flex-none border-t border-neutral-200`,S({value:o,label:`${o} ↔ Working tree`,subject:`back to the default: the main line's tip vs the files on disk`},c===o&&l===``?o:``,()=>{c=o,l=``,g(),a(c,l)}));h.querySelector(`button > span`).prepend(e(_)),m.replaceChildren(n(`div`,`flex flex-none items-center gap-1 border-b border-neutral-200 px-2 py-1.5`,t(`Compare two points`,`compare`),t(`One commit`,`commit`)),f,...d===`compare`?[h]:[])}g(),h(t,m)}var oe=/\.(png|jpe?g|gif|webp|avif|bmp|svg)$/i,se={A:`text-emerald-600`,M:`text-amber-600`,D:`text-red-600`},ce=30,C=`pier.filesPrefs`,w=50,T=()=>{try{let e=JSON.parse(localStorage.getItem(C)??`{}`);return e&&typeof e==`object`?e:{}}catch{return{}}},E=e=>{let t=e?T()[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},le=(e,t)=>{let n=T();delete n[e],n[e]=t;let r=Object.keys(n);for(let e of r.slice(0,Math.max(0,r.length-w)))delete n[e];try{localStorage.setItem(C,JSON.stringify(n))}catch(e){console.warn(`Files: could not remember this folder`,e)}},D=(e,t=`indigo`)=>{let r=n(`button`,`min-w-0 cursor-pointer truncate rounded-md px-2 py-0.5 text-left font-mono text-[11.5px] ${t===`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 O(o,s,m,h){let _=``,v=``,y={branch:null,refs:[],commits:[],worktrees:[]},b=`HEAD`,S=``,C=new Map,w=!0,T=!1,O=[],k=new Set,A=null,j=null,ue=n(`header`,`pagehead`),M=n(`div`,`flex-none border-b border-neutral-200`),N=n(`div`,`min-h-0 flex-1 overflow-y-auto py-1`),de=n(`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`,M,N),P=n(`div`,`h-full min-w-0 overflow-auto`),F=n(`div`,`pointer-events-none absolute inset-y-0 right-0 z-20 hidden w-2`),fe=n(`section`,`relative min-h-0 min-w-0 flex-1`,P,F);o.append(ue,n(`div`,`flex min-h-0 flex-1 max-md:flex-col`,de,fe));let I=(e,t)=>`/api/explorer/${e}?${new URLSearchParams({root:_,...t})}`,L=(e,t)=>`/api/fs/${e}?${new URLSearchParams({root:_,...t})}`,R=(e,t=`text-neutral-400`)=>n(`p`,`px-4 py-3 text-[12.5px] ${t}`,e),z=e=>{let t=0;for(let n of C.keys())n.startsWith(`${e}/`)&&t++;return t};function B(e,t){let n=new Set(t.map(e=>e.name)),r=e?`${e}/`:``;for(let[e,i]of C){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&&y.branch&&(i=t.filter(e=>e.dir?z(`${r}${e.name}`)>0:C.has(`${r}${e.name}`))),i.sort((e,t)=>Number(t.dir)-Number(e.dir)||e.name.localeCompare(t.name)),i.map(e=>e.dir?pe(`${r}${e.name}`,e.name):me(`${r}${e.name}`,e.name))}async function V(e,t){try{let{entries:n}=await r(L(`ls`,{path:t}),`could not list this folder`);e.replaceChildren(...B(t,n)),e.childElementCount||e.append(R(w?`No changes.`:`Empty.`))}catch(n){let r=B(t,[]);r.length?e.replaceChildren(...r):e.replaceChildren(R(String(n),`text-red-600`))}}function pe(e,r){let i=z(e),a=[n(`span`,`truncate`,r)];i&&a.push(n(`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}=t(``,a);s.className+=` px-2 py-0.5 hover:bg-neutral-100`;let c=n(`div`,`pl-3`);o.append(c);let l=!1,u=()=>{l||(l=!0,c.append(R(`…`)),V(c,e))};return o.ontoggle=()=>{k[o.open?`add`:`delete`](e),o.open&&u()},(w&&y.branch&&!T||k.has(e))&&(o.open=!0,u()),o}function H(e){j?.classList.remove(`bg-indigo-50`,`text-indigo-700`),j=e,e.classList.add(`bg-indigo-50`,`text-indigo-700`)}function me(e,t){let r=C.get(e)?.status,i=n(`button`,`flex w-full cursor-pointer items-center gap-1.5 px-2 py-0.5 pl-6 text-left hover:bg-neutral-100`,n(`span`,`truncate`,t));return r&&i.append(n(`span`,`ml-auto flex-none font-mono text-[10.5px] font-semibold ${se[r]??`text-neutral-500`}`,r)),i.title=e,e===A&&H(i),i.onclick=()=>{H(i),A=e,_e(e)},i}let U=()=>void V(N,``),W=(e,t)=>[...e?[n(`span`,`flex-none font-mono text-[10.5px] font-semibold text-emerald-600`,`+${e}`)]:[],...t?[n(`span`,`flex-none font-mono text-[10.5px] font-semibold text-red-600`,`−${t}`)]:[]];function G(e,t,r){let a=C.get(e),o=n(`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`,n(`span`,`truncate`,e));a&&(o.append(n(`span`,`flex-none font-semibold ${se[a.status]??``}`,a.status)),o.append(...W(a.add,a.del)));let s=n(`div`,`ml-auto flex flex-none items-center gap-2`);if(r&&s.append(r),t){let t=document.createElement(`a`);t.className=`flex-none text-neutral-400 hover:text-indigo-700 hover:underline`,t.href=L(`file`,{path:e}),t.download=i(e),t.textContent=`Download`,s.append(t)}return o.append(s),o}function he(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 K=we();function q(){F.classList.add(`hidden`),F.replaceChildren(),K.set([],[])}let ge=0,J=e=>e===ge;function _e(e){let t=++ge;return C.has(e)?Te(e,t):ve(e,t)}async function ve(e,t){if(!J(t))return;q();let r=L(`file`,{path:e}),i=n(`div`,`min-w-0`);if(P.classList.remove(`flex`,`flex-col`),P.replaceChildren(G(e,!0),i),oe.test(e)){let e=n(`img`,`max-w-full p-4`);e.src=r,e.onerror=()=>i.replaceChildren(R(`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,P.replaceChildren(G(e,!0),t),P.classList.add(`flex`,`flex-col`);return}i.append(R(`…`));let a=await fetch(r);if(!J(t))return;if(!a.ok){let{error:e}=await a.json().catch(()=>({}));i.replaceChildren(R(e??`HTTP ${a.status}`,`text-red-600`));return}if(!(a.headers.get(`content-type`)??``).startsWith(`text/plain`)){i.replaceChildren(R(`Binary file — use Download.`));return}let o=f(await a.text());if(!J(t))return;let s=await g(e);J(t)&&i.replaceChildren(u(o,s))}function ye(e){let t=[],n=0,r=0,i=!1,a=e.split(`
4
+ `);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 be(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 xe={add:`bg-emerald-400`,del:`bg-red-400`,mixed:`bg-amber-400`};function Se(e,t,r){F.replaceChildren(...e.map(e=>{let i=n(`button`,`pointer-events-auto absolute w-full cursor-pointer ${xe[e.tone]}`);return i.style.top=`${e.start/t*100}%`,i.style.height=`${Math.max(.4,(e.end-e.start+1)/t*100)}%`,i.title=`line ${Ce(r,e.start)}`,i.onclick=()=>r[e.start].scrollIntoView({block:`center`}),i})),F.classList.remove(`hidden`)}let Ce=(e,t)=>e[t]?.querySelector(`span`)?.textContent?.trim()||String(t+1);function we(){let t=[],r=[],i=-1,a=n(`span`,`flex-none text-[10.5px] text-neutral-400`),s=e=>{t.length&&(i=(i+e+t.length)%t.length,r[t[i].start].scrollIntoView({block:`center`}),a.textContent=`${i+1}/${t.length}`)},u=()=>t.length>0&&!o.classList.contains(`hidden`),d=(t,r,i,a)=>{let o=n(`button`,`icon-btn flex-none`,e(t));return o.onclick=()=>s(r),l(o,a,i,()=>s(r),u),o};return{el:n(`div`,`flex flex-none items-center gap-1`,a,d(c,-1,`Previous change`,[`p`,`k`]),d(ee,1,`Next change`,[`n`,`j`])),set(e,n){t=e,r=n,i=-1,a.textContent=`${e.length} change${e.length===1?``:`s`}`}}}async function Te(e,t){q(),P.classList.remove(`flex`,`flex-col`);let n=C.get(e)?.status!==`D`;P.replaceChildren(G(e,n),R(`…`));try{let{diff:i}=await r(I(`diff`,{base:b,head:S,file:e,context:`99999`}),`could not read this diff`);if(!J(t))return;if(!i||/^Binary files /m.test(i))return ve(e,t);let a=ye(i);he(a);let o=await g(e);if(!J(t))return;let s=u(a,o),c=[...s.children],l=be(a);K.set(l,c),P.replaceChildren(G(e,n,K.el),s),Se(l,a.length,c)}catch(n){J(t)&&P.replaceChildren(G(e,!1),R(String(n),`text-red-600`))}}let Y=()=>S&&b===`${S}~1`?S:``,Ee=()=>y.commits.find(e=>e.hash===Y())?.subject??``,De=()=>Y()?`${Y()} — ${Ee()||`commit`}`:`${b} ↔ ${S||`working tree`}`,Oe=(t,r,i,a)=>{let o=n(`button`,`flex h-6 w-6 flex-none cursor-pointer items-center justify-center rounded-md ${i?`bg-indigo-50 text-indigo-700`:`text-neutral-400 hover:bg-neutral-100 hover:text-neutral-700`}`);return o.append(e(t)),o.title=r,o.setAttribute(`aria-label`,r),o.setAttribute(`aria-pressed`,String(i)),o.onclick=a,o};function X(){let e=Oe(ne,`Show only files that differ`,w,()=>{w=!w,X(),U()}),t=Oe(te,T?`Restore folders`:`Collapse all folders`,T,()=>{if(T=!T,T)O=[...k],k.clear();else for(let e of O)k.add(e);X(),U()});if(!y.branch){M.replaceChildren(n(`div`,`flex items-center justify-end gap-1 p-1.5`,t));return}let r=D(De());if(Y()){let e=y.commits.find(e=>e.hash===Y());x(r,()=>e?ie(e):``)}else r.title=`Choose what to diff`;r.onclick=async()=>{await Me(),ae(r,y,{base:b,head:S},(e,t)=>{b=e,S=t,Z()},$())};let[i,a]=[0,0];for(let e of C.values())i+=e.add,a+=e.del;M.replaceChildren(n(`div`,`flex flex-col gap-1 p-2`,r,n(`div`,`flex items-center gap-1.5 pl-1`,n(`span`,`text-[11px] text-neutral-400`,`${C.size} changed file${C.size===1?``:`s`}`),...W(i,a),n(`span`,`ml-auto`),e,t)))}let ke=()=>{v&&_&&le(v,{cwd:_,base:b,head:S})},Ae=e=>{let t=e.replace(/~\d+$/,``);return!t||t===`HEAD`||y.refs.some(e=>e.name===t)||y.commits.some(e=>e.hash===t)};async function je(){if(C=new Map,!y.branch)return;let{files:e}=await r(I(`diff`,{base:b,head:S}),`could not read what changed`);for(let t of e)C.set(t.path,{status:t.status,add:t.add,del:t.del});if(C.size<=ce)for(let e of C.keys()){let t=e.split(`/`);for(let e=1;e<t.length;e++)k.add(t.slice(0,e).join(`/`))}}async function Z(){ke();try{await je()}catch(e){X(),N.replaceChildren(R(String(e),`text-red-600`));return}X(),U(),A&&_e(A)}function Q(){let t=D(_||`Choose a folder…`,`neutral`);t.className+=` max-w-72`,t.title=`Switch folder`,t.onclick=()=>p(t,y.worktrees.map(e=>({path:e.path,...e.branch?{hint:e.branch}:{}})),_||void 0,m);let r=n(`button`,`icon-btn`,e(d));r.type=`button`,r.title=`Close Files`,r.setAttribute(`aria-label`,`Close Files`),r.onclick=h,r.classList.add(`ml-auto`),ue.replaceChildren(t,n(`span`,`flex min-w-0 items-center gap-1 truncate font-mono text-[11.5px] text-neutral-400`,...y.branch?[e(re),n(`span`,`truncate`,y.branch)]:[`no git`]),r)}async function Me(){if(_)try{y=await r(I(`git`,{}),`could not read this repository`),Q()}catch(e){P.replaceChildren(R(String(e),`text-red-600`))}}let $=()=>y.refs.some(e=>e.name===`main`)?`main`:y.refs.some(e=>e.name===`master`)?`master`:`HEAD`;async function Ne(){if(Q(),A=null,j=null,k.clear(),q(),P.classList.remove(`flex`,`flex-col`),P.replaceChildren(R(`Select a file.`)),!_){M.replaceChildren(),N.replaceChildren(),P.replaceChildren(R(`No folder yet — pick one from the chip above.`));return}try{y=await r(I(`git`,{}),`could not read this repository`)}catch(e){y={branch:null,refs:[],commits:[],worktrees:[]},P.replaceChildren(R(String(e),`text-red-600`))}let e=E(v),t=e&&e.cwd===_&&Ae(e.base)&&Ae(e.head)?e:null;b=t?.base??$(),S=t?.head??``,Q(),await Z()}return a(o,e=>{let t=s(),n=t?.id??``,r=e?.startsWith(`/`)?e:(n===v?_:``)||E(n)?.cwd||t?.cwd||``;if(r===_&&n===v&&N.childElementCount){Q(),Me().then(Z);return}v=n,_=r,Ne()},()=>{q(),P.classList.remove(`flex`,`flex-col`),P.replaceChildren(R(`Select a file.`))})}export{O as createExplorerView};