@timqi/pier 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boards/boards.js +104 -41
- package/dist/channels/config.js +16 -0
- package/dist/channels/control.js +10 -3
- package/dist/channels/conversations.js +9 -0
- package/dist/channels/handoff.js +6 -2
- package/dist/channels/lark.js +3 -0
- package/dist/channels/panel.js +3 -6
- package/dist/channels/routes.js +3 -0
- package/dist/channels/slack.js +3 -0
- package/dist/channels/types.js +1 -0
- package/dist/main.js +3 -1
- package/dist/web/auth.js +6 -5
- package/dist/web/public/assets/{activity-DE9Dct4L.js → activity-dxSOEHmf.js} +1 -1
- package/dist/web/public/assets/activity-dxSOEHmf.js.br +0 -0
- package/dist/web/public/assets/activity-dxSOEHmf.js.gz +0 -0
- package/dist/web/public/assets/{boards-il6ceDEs.js → boards-BRwn5UZi.js} +1 -1
- package/dist/web/public/assets/boards-BRwn5UZi.js.br +0 -0
- package/dist/web/public/assets/boards-BRwn5UZi.js.gz +0 -0
- package/dist/web/public/assets/{explorer-JDO21aAs.js → explorer-CCJQ22Dp.js} +1 -1
- package/dist/web/public/assets/explorer-CCJQ22Dp.js.br +0 -0
- package/dist/web/public/assets/explorer-CCJQ22Dp.js.gz +0 -0
- package/dist/web/public/assets/index-CzV2cZEH.js +85 -0
- package/dist/web/public/assets/index-CzV2cZEH.js.br +0 -0
- package/dist/web/public/assets/index-CzV2cZEH.js.gz +0 -0
- package/dist/web/public/assets/{runs-CsINd08H.js → runs-DtvDXSfo.js} +1 -1
- package/dist/web/public/assets/runs-DtvDXSfo.js.br +0 -0
- package/dist/web/public/assets/runs-DtvDXSfo.js.gz +0 -0
- package/dist/web/public/assets/{settings-ChwAWwE_.js → settings-CJbHCEZo.js} +1 -1
- package/dist/web/public/assets/settings-CJbHCEZo.js.br +0 -0
- package/dist/web/public/assets/settings-CJbHCEZo.js.gz +0 -0
- package/dist/web/public/assets/{task-runs-I0eOy3HT.js → task-runs-DcORJXHU.js} +1 -1
- package/dist/web/public/assets/task-runs-DcORJXHU.js.br +0 -0
- package/dist/web/public/assets/task-runs-DcORJXHU.js.gz +0 -0
- package/dist/web/public/assets/{tasks-D3i9Kc5-.js → tasks-8l1M8EJU.js} +1 -1
- package/dist/web/public/assets/tasks-8l1M8EJU.js.br +0 -0
- package/dist/web/public/assets/tasks-8l1M8EJU.js.gz +0 -0
- package/dist/web/public/index.html +1 -1
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/server.js +1 -1
- package/package.json +1 -1
- package/skills/pier-boards/SKILL.md +1 -1
- package/dist/web/public/assets/activity-DE9Dct4L.js.br +0 -0
- package/dist/web/public/assets/activity-DE9Dct4L.js.gz +0 -0
- package/dist/web/public/assets/boards-il6ceDEs.js.br +0 -0
- package/dist/web/public/assets/boards-il6ceDEs.js.gz +0 -0
- package/dist/web/public/assets/explorer-JDO21aAs.js.br +0 -0
- package/dist/web/public/assets/explorer-JDO21aAs.js.gz +0 -0
- package/dist/web/public/assets/index-BNjWeodx.js +0 -85
- package/dist/web/public/assets/index-BNjWeodx.js.br +0 -0
- package/dist/web/public/assets/index-BNjWeodx.js.gz +0 -0
- package/dist/web/public/assets/runs-CsINd08H.js.br +0 -0
- package/dist/web/public/assets/runs-CsINd08H.js.gz +0 -0
- package/dist/web/public/assets/settings-ChwAWwE_.js.br +0 -0
- package/dist/web/public/assets/settings-ChwAWwE_.js.gz +0 -0
- package/dist/web/public/assets/task-runs-I0eOy3HT.js.br +0 -0
- package/dist/web/public/assets/task-runs-I0eOy3HT.js.gz +0 -0
- package/dist/web/public/assets/tasks-D3i9Kc5-.js.br +0 -0
- package/dist/web/public/assets/tasks-D3i9Kc5-.js.gz +0 -0
package/dist/boards/boards.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Boards: static pages an agent writes, derived by scanning $PIER_HOME/boards,
|
|
2
2
|
// never registered (docs/design/05-boards.md). Only <board>/site is reachable
|
|
3
|
-
// over HTTP, so a public board leaks nothing about how it was made.
|
|
4
|
-
//
|
|
5
|
-
|
|
3
|
+
// over HTTP, so a public board leaks nothing about how it was made. Bytes are
|
|
4
|
+
// served on two password-free prefixes, `/p/*` (published) and `/b/*` (a
|
|
5
|
+
// signed prefix the boundary mints), stylesheet included, and run sandboxed.
|
|
6
|
+
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
6
7
|
import { readdir, readFile, realpath, rename, stat, writeFile } from "node:fs/promises";
|
|
7
8
|
import { extname, join, resolve, sep } from "node:path";
|
|
8
9
|
import { logger } from "../log.js";
|
|
@@ -34,17 +35,47 @@ const TYPES = {
|
|
|
34
35
|
".txt": "text/plain; charset=utf-8",
|
|
35
36
|
".csv": "text/plain; charset=utf-8",
|
|
36
37
|
};
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
const CSP = "default-src 'self'; img-src 'self' data:;
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"frame-ancestors 'none'";
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
// Agent-written script, so it runs in an opaque origin: no `allow-same-origin`
|
|
39
|
+
// on any board, published or not, and the sandbox also removes forms, frames,
|
|
40
|
+
// popups and subresource requests. What that costs is the session cookie —
|
|
41
|
+
// an opaque-origin document sends none with its own assets — which is why a
|
|
42
|
+
// board is never served on a cookie-authorized URL (docs/design/05-boards.md).
|
|
43
|
+
const CSP = "sandbox allow-scripts; default-src 'self'; img-src 'self' data:; " +
|
|
44
|
+
"style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; " +
|
|
45
|
+
"connect-src 'none'; frame-src 'none'; worker-src 'none'; object-src 'none'; " +
|
|
46
|
+
"base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
|
|
47
|
+
/** Long enough to read a board without a second thought, short enough that a
|
|
48
|
+
* URL left in a history or a chat log stops working the same day. */
|
|
49
|
+
const VIEW_TTL_MS = 8 * 60 * 60_000;
|
|
50
|
+
/** Process-scoped: a view prefix is a read capability for one board, and
|
|
51
|
+
* nothing may outlive the instance that vouched for it. */
|
|
52
|
+
let viewKey = randomBytes(32);
|
|
53
|
+
/** Signing out must end the boards that session opened too — the pages carry
|
|
54
|
+
* no cookie to revoke, so the key they were signed with is what goes. */
|
|
55
|
+
export const rotateBoardViews = () => {
|
|
56
|
+
viewKey = randomBytes(32);
|
|
57
|
+
};
|
|
58
|
+
const sign = (slug, expires) => createHmac("sha256", viewKey).update(`${slug}\0${String(expires)}`).digest("base64url").slice(0, 22);
|
|
59
|
+
/** `<expiry in base36>-<signature>`: its own path segment, so the first hyphen
|
|
60
|
+
* is the cut and a hyphenated slug stays unambiguous. */
|
|
61
|
+
const mintView = (slug) => {
|
|
62
|
+
const expires = Date.now() + VIEW_TTL_MS;
|
|
63
|
+
return `${expires.toString(36)}-${sign(slug, expires)}`;
|
|
64
|
+
};
|
|
65
|
+
function validView(slug, view) {
|
|
66
|
+
const cut = view.indexOf("-");
|
|
67
|
+
if (cut < 1)
|
|
68
|
+
return false;
|
|
69
|
+
const stamp = view.slice(0, cut);
|
|
70
|
+
const expires = Number.parseInt(stamp, 36);
|
|
71
|
+
// Canonical only: `parseInt` stops at the first stray character, which would
|
|
72
|
+
// make one signature valid under several spellings of its own prefix.
|
|
73
|
+
if (!Number.isSafeInteger(expires) || expires.toString(36) !== stamp)
|
|
74
|
+
return false;
|
|
75
|
+
if (expires <= Date.now())
|
|
76
|
+
return false;
|
|
77
|
+
return sameToken(sign(slug, expires), view.slice(cut + 1));
|
|
78
|
+
}
|
|
48
79
|
/** Malformed boards are reported once, not on every scan. */
|
|
49
80
|
const warned = new Set();
|
|
50
81
|
/** The one place a slug becomes a path, so it is validated here (`../../etc`,
|
|
@@ -176,14 +207,7 @@ const sameToken = (want, got) => {
|
|
|
176
207
|
const digest = (s) => createHash("sha256").update(s).digest();
|
|
177
208
|
return timingSafeEqual(digest(want), digest(got));
|
|
178
209
|
};
|
|
179
|
-
async function serveFile(c, dir,
|
|
180
|
-
const { slug, token } = publicOnly ? publicKey(key) : { slug: key, token: "" };
|
|
181
|
-
const manifest = await readManifest(dir, slug);
|
|
182
|
-
// 404, never 403: a private board's existence is not public information, and
|
|
183
|
-
// a wrong token is the same non-answer as a wrong name.
|
|
184
|
-
if (!manifest || (publicOnly && (!manifest.public || !sameToken(manifest.token, token)))) {
|
|
185
|
-
return c.notFound();
|
|
186
|
-
}
|
|
210
|
+
async function serveFile(c, dir, slug, rest) {
|
|
187
211
|
const file = await resolveFile(dir, slug, rest);
|
|
188
212
|
if (!file)
|
|
189
213
|
return c.notFound();
|
|
@@ -193,17 +217,16 @@ async function serveFile(c, dir, key, rest, publicOnly) {
|
|
|
193
217
|
const headers = {
|
|
194
218
|
"content-type": type,
|
|
195
219
|
"x-content-type-options": "nosniff",
|
|
196
|
-
//
|
|
197
|
-
// a
|
|
198
|
-
"cache-control":
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
"
|
|
220
|
+
// Nothing is cached: both prefixes are revocable (unpublish, sign out,
|
|
221
|
+
// expiry), and a stored copy would outlive the revocation.
|
|
222
|
+
"cache-control": "no-store",
|
|
223
|
+
"content-security-policy": CSP,
|
|
224
|
+
// The URL is the credential on both prefixes; an outbound link must not carry it.
|
|
225
|
+
"referrer-policy": "no-referrer",
|
|
226
|
+
// A sandboxed page has an opaque origin. Fonts and module scripts need CORS
|
|
227
|
+
// even when their URLs are under the same board.
|
|
228
|
+
"access-control-allow-origin": "*",
|
|
202
229
|
};
|
|
203
|
-
// Public sandboxed pages have an opaque origin. Fonts and module scripts need
|
|
204
|
-
// CORS even when their URLs are under the same published board.
|
|
205
|
-
if (publicOnly)
|
|
206
|
-
headers["access-control-allow-origin"] = "*";
|
|
207
230
|
return c.body(await readFile(file), 200, headers);
|
|
208
231
|
}
|
|
209
232
|
export function registerBoardRoutes(app, dir = defaultBoardsDir()) {
|
|
@@ -231,6 +254,9 @@ export function registerBoardRoutes(app, dir = defaultBoardsDir()) {
|
|
|
231
254
|
if (!(await readManifest(dir, slug)))
|
|
232
255
|
return c.json({ error: "no such board" }, 404);
|
|
233
256
|
await rename(join(dir, slug), join(dir, `${slug}.deleted-${Date.now()}`));
|
|
257
|
+
// A capability names a slug, and a slug can be taken again: the prefixes
|
|
258
|
+
// handed out for the board that just died must not open its successor.
|
|
259
|
+
rotateBoardViews();
|
|
234
260
|
return c.json({ deleted: slug });
|
|
235
261
|
});
|
|
236
262
|
// Declared before the wildcards below: `_assets` is not a slug.
|
|
@@ -242,13 +268,50 @@ export function registerBoardRoutes(app, dir = defaultBoardsDir()) {
|
|
|
242
268
|
});
|
|
243
269
|
});
|
|
244
270
|
// Trailing slash matters: without it a board's relative asset paths resolve
|
|
245
|
-
// against
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
271
|
+
// against the prefix instead of the board.
|
|
272
|
+
app.get("/p/:key", (c) => c.redirect(`/p/${c.req.param("key")}/`));
|
|
273
|
+
app.get("/p/:key/*", async (c) => {
|
|
274
|
+
const key = c.req.param("key");
|
|
275
|
+
const { slug, token } = publicKey(key);
|
|
276
|
+
const manifest = await readManifest(dir, slug);
|
|
277
|
+
// 404, never 403: a private board's existence is not public information,
|
|
278
|
+
// and a wrong token is the same non-answer as a wrong name.
|
|
279
|
+
if (!manifest || !manifest.public || !sameToken(manifest.token, token))
|
|
280
|
+
return c.notFound();
|
|
281
|
+
return serveFile(c, dir, slug, c.req.path.slice(`/p/${key}/`.length));
|
|
282
|
+
});
|
|
283
|
+
// The operator's link, and the one place the password is spent on a board:
|
|
284
|
+
// it hands out a signed prefix instead of bytes, so the page that follows
|
|
285
|
+
// needs no cookie and can be sandboxed into an opaque origin.
|
|
286
|
+
const mint = async (c) => {
|
|
287
|
+
const slug = c.req.param("slug") ?? "";
|
|
288
|
+
// Signed before the read: a sign-out landing during it would otherwise
|
|
289
|
+
// hand this request a capability made with the key that replaced the
|
|
290
|
+
// revoked one.
|
|
291
|
+
const view = SLUG.test(slug) ? mintView(slug) : "";
|
|
292
|
+
// A board that is gone says so here, rather than after a redirect.
|
|
293
|
+
if (!view || !(await readManifest(dir, slug)))
|
|
294
|
+
return c.notFound();
|
|
295
|
+
const rest = c.req.path.slice(`/boards/${slug}`.length).replace(/^\//, "");
|
|
296
|
+
return c.redirect(`/b/${slug}/${view}/${rest}${new URL(c.req.url).search}`);
|
|
297
|
+
};
|
|
298
|
+
app.get("/boards/:slug", mint);
|
|
299
|
+
app.get("/boards/:slug/*", mint);
|
|
300
|
+
app.get("/b/:slug/:view", (c) => c.redirect(`${c.req.path}/${new URL(c.req.url).search}`));
|
|
301
|
+
app.get("/b/:slug/:view/*", async (c) => {
|
|
302
|
+
const slug = c.req.param("slug");
|
|
303
|
+
const view = c.req.param("view");
|
|
304
|
+
const rest = c.req.path.slice(`/b/${slug}/${view}/`.length);
|
|
305
|
+
if (!SLUG.test(slug))
|
|
306
|
+
return c.notFound();
|
|
307
|
+
// Expired, forged, or signed with a key that has since rotated: send it
|
|
308
|
+
// back through the boundary, which re-mints for a live session in one hop
|
|
309
|
+
// and asks a stranger for the password. Existence stays unsaid either way.
|
|
310
|
+
if (!validView(slug, view)) {
|
|
311
|
+
return c.redirect(`/boards/${slug}/${rest}${new URL(c.req.url).search}`);
|
|
312
|
+
}
|
|
313
|
+
if (!(await readManifest(dir, slug)))
|
|
314
|
+
return c.notFound();
|
|
315
|
+
return serveFile(c, dir, slug, rest);
|
|
316
|
+
});
|
|
254
317
|
}
|
package/dist/channels/config.js
CHANGED
|
@@ -91,6 +91,22 @@ export class ChannelStore {
|
|
|
91
91
|
}
|
|
92
92
|
this.save(platform, config);
|
|
93
93
|
}
|
|
94
|
+
/** A DM's id belongs to the bot it was opened with, so a new app or a rotated
|
|
95
|
+
* token leaves DMs no one can reach and nothing on screen tells them apart
|
|
96
|
+
* from the live ones — they are dropped. Group ids survive the swap. Returns
|
|
97
|
+
* the chat ids forgotten; the first identity ever seen forgets nothing. */
|
|
98
|
+
claimBot(platform, botId) {
|
|
99
|
+
const known = this.cached(platform).botId;
|
|
100
|
+
if (!botId || known === botId)
|
|
101
|
+
return [];
|
|
102
|
+
const config = this.get(platform);
|
|
103
|
+
const keep = (chat) => !known || chat.kind !== "dm";
|
|
104
|
+
const dropped = config.chats.filter((c) => !keep(c)).map((c) => c.id);
|
|
105
|
+
config.chats = config.chats.filter(keep);
|
|
106
|
+
config.botId = botId;
|
|
107
|
+
this.save(platform, config);
|
|
108
|
+
return dropped;
|
|
109
|
+
}
|
|
94
110
|
// On the per-message path: no clone, nothing here escapes.
|
|
95
111
|
policy(platform, chatId) {
|
|
96
112
|
const config = this.cached(platform);
|
package/dist/channels/control.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// What an adapter may do
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// What an adapter may do beyond handing a prompt over: act on a thread's
|
|
2
|
+
// session, and claim the identity its chats belong to. Not part of the
|
|
3
|
+
// `Channel` seam: the channel layer owns the router and the stores and hands
|
|
4
|
+
// adapters this narrow interface instead. Thin wrappers; no policy.
|
|
4
5
|
import { projectCwds } from "../core/identity.js";
|
|
5
6
|
import { chatOf, isChannelPlatform } from "./types.js";
|
|
6
7
|
export const NO_SESSION = "No session in this thread yet — start one first (Start in the panel).";
|
|
@@ -25,6 +26,12 @@ export function createControl({ router, factory, conversations, store, modelMenu
|
|
|
25
26
|
};
|
|
26
27
|
return {
|
|
27
28
|
launchFor,
|
|
29
|
+
claimBot(platform, botId) {
|
|
30
|
+
const dropped = store.claimBot(platform, botId);
|
|
31
|
+
for (const chatId of dropped)
|
|
32
|
+
conversations.forgetChat(platform, chatId);
|
|
33
|
+
return dropped;
|
|
34
|
+
},
|
|
28
35
|
knows: (key) => conversations.get(key) !== undefined,
|
|
29
36
|
abort: (key) => router.abortConversation(key),
|
|
30
37
|
working: (key) => router.sessionOf(key)?.state === "streaming",
|
|
@@ -52,6 +52,15 @@ export class ConversationStore {
|
|
|
52
52
|
const rows = this.db.prepare(`SELECT session_id FROM conversations`).all();
|
|
53
53
|
return new Set(rows.map((r) => r.session_id));
|
|
54
54
|
}
|
|
55
|
+
/** Every thread of one chat: a chat that is gone would otherwise keep its
|
|
56
|
+
* sessions bound to a conversation nothing can reach. */
|
|
57
|
+
forgetChat(channelId, chatId) {
|
|
58
|
+
const prefix = `${chatId}/`;
|
|
59
|
+
this.db.prepare(`
|
|
60
|
+
DELETE FROM conversations
|
|
61
|
+
WHERE channel_id = ? AND (conversation_id = ? OR substr(conversation_id, 1, ?) = ?)
|
|
62
|
+
`).run(channelId, chatId, prefix.length, prefix);
|
|
63
|
+
}
|
|
55
64
|
forget(key) {
|
|
56
65
|
this.db.prepare(`
|
|
57
66
|
DELETE FROM conversations WHERE channel_id = ? AND conversation_id = ?
|
package/dist/channels/handoff.js
CHANGED
|
@@ -42,9 +42,11 @@ export function createHandoff(deps) {
|
|
|
42
42
|
log(`session ${sessionId} continued in ${key.channelId}:${key.conversationId}`);
|
|
43
43
|
};
|
|
44
44
|
return {
|
|
45
|
+
// DMs only: a group's thread belongs to the group, and a session pushed
|
|
46
|
+
// into one answers everyone there.
|
|
45
47
|
targets: () => runtime.running().flatMap((platform) => store.get(platform).chats
|
|
46
|
-
.filter((chat) => chat.enabled)
|
|
47
|
-
.map((chat) => ({ platform, chatId: chat.id, name: chat.name
|
|
48
|
+
.filter((chat) => chat.enabled && chat.kind === "dm")
|
|
49
|
+
.map((chat) => ({ platform, chatId: chat.id, name: chat.name }))),
|
|
48
50
|
async continueIn({ sessionId, platform, chatId }) {
|
|
49
51
|
if (!runtime.running().includes(platform)) {
|
|
50
52
|
throw new HandoffError(409, `${platformName(platform)} is not running — enable it in Settings → Channels.`);
|
|
@@ -52,6 +54,8 @@ export function createHandoff(deps) {
|
|
|
52
54
|
const chat = store.chat(platform, chatId);
|
|
53
55
|
if (!chat?.enabled)
|
|
54
56
|
throw new HandoffError(chat ? 409 : 404, "That chat is not enabled for the bot.");
|
|
57
|
+
if (chat.kind !== "dm")
|
|
58
|
+
throw new HandoffError(409, "Only a direct message can continue a web session.");
|
|
55
59
|
const summary = await claimable(sessionId);
|
|
56
60
|
const base = publicUrl();
|
|
57
61
|
const note = {
|
package/dist/channels/lark.js
CHANGED
|
@@ -90,6 +90,9 @@ export class LarkChannel {
|
|
|
90
90
|
// Every chat with require-mention on goes silent; loud, not a debug line.
|
|
91
91
|
this.log("bot info returned no open_id: mention detection is disabled");
|
|
92
92
|
}
|
|
93
|
+
const dropped = this.deps.control?.claimBot("lark", this.me) ?? [];
|
|
94
|
+
if (dropped.length)
|
|
95
|
+
this.log(`bot is now ${this.me}: forgot ${dropped.length} DM(s) the previous bot opened`);
|
|
93
96
|
this.running = true;
|
|
94
97
|
void this.receipts.sweep(true);
|
|
95
98
|
this.socket = await this.api.connect({
|
package/dist/channels/panel.js
CHANGED
|
@@ -31,11 +31,8 @@ export const CWD_DRAFT_TAIL = "Start creates the session there.";
|
|
|
31
31
|
export const CWD_PLACEHOLDER = "/path/to/project";
|
|
32
32
|
export const QUESTION_TOO_LONG = "Your question is too long to hold — send it again after Start.";
|
|
33
33
|
const btn = (label, action) => ({ label, action });
|
|
34
|
-
/** A button label is the last
|
|
35
|
-
const shortDir = (path) =>
|
|
36
|
-
const parts = path.split("/").filter(Boolean);
|
|
37
|
-
return parts.length > 2 ? `…/${parts.slice(-2).join("/")}` : path;
|
|
38
|
-
};
|
|
34
|
+
/** A button label is the last segment; the numbered line above has the whole path. */
|
|
35
|
+
const shortDir = (path) => path.split("/").filter(Boolean).at(-1) ?? path;
|
|
39
36
|
/** One transcript line as a reader sees it: what was said, minus what was
|
|
40
37
|
* written for the model (the speaker header, attachment markers, the
|
|
41
38
|
* next-step block, a silent turn's reason). */
|
|
@@ -324,7 +321,7 @@ export class ChatPanel {
|
|
|
324
321
|
title: "Continue web session",
|
|
325
322
|
suffix: ` · page ${at + 1}/${pages}`,
|
|
326
323
|
lines: slice.length
|
|
327
|
-
? slice.map((s, i) => `${String(from + i + 1)}. ${this.esc(shortTitle(s))} · ${this.code(s.cwd
|
|
324
|
+
? slice.map((s, i) => `${String(from + i + 1)}. ${this.esc(shortTitle(s))} · ${this.code(shortDir(s.cwd))} · ${age(s.modified ?? s.createdAt, now)}`)
|
|
328
325
|
: [unavailable ?? "No unbound sessions."],
|
|
329
326
|
}],
|
|
330
327
|
picks: slice.map((s, i) => btn(`${String(from + i + 1)} ${shortTitle(s)}`, `session:${String(from + i)}`)),
|
package/dist/channels/routes.js
CHANGED
|
@@ -79,6 +79,9 @@ export function registerChannelRoutes(app, store, runtime, handoff) {
|
|
|
79
79
|
users: current.users,
|
|
80
80
|
chats: parseChats(body.chats, current.chats),
|
|
81
81
|
bindCode: current.bindCode,
|
|
82
|
+
// Learned at start, never edited here: a save that reset it would make the
|
|
83
|
+
// next start read as the first one and skip the swapped bot's cleanup.
|
|
84
|
+
botId: current.botId,
|
|
82
85
|
};
|
|
83
86
|
store.save(platform, next);
|
|
84
87
|
await runtime.reload();
|
package/dist/channels/slack.js
CHANGED
|
@@ -126,6 +126,9 @@ export class SlackChannel {
|
|
|
126
126
|
// Every channel with require-mention on goes silent; loud, not a debug line.
|
|
127
127
|
this.log("auth.test returned no user id: mention detection is disabled");
|
|
128
128
|
}
|
|
129
|
+
const dropped = this.deps.control?.claimBot("slack", this.me) ?? [];
|
|
130
|
+
if (dropped.length)
|
|
131
|
+
this.log(`bot is now ${this.me}: forgot ${dropped.length} DM(s) the previous bot opened`);
|
|
129
132
|
this.running = true;
|
|
130
133
|
void this.receipts.sweep(true);
|
|
131
134
|
this.socket = await this.api.connect((env) => this.onEnvelope(env, onMessage));
|
package/dist/channels/types.js
CHANGED
package/dist/main.js
CHANGED
|
@@ -11,7 +11,7 @@ import { CredentialStore } from "./agent/credentials.js";
|
|
|
11
11
|
import { IndexedListing } from "./agent/listing.js";
|
|
12
12
|
import { PiPackageStore } from "./agent/packages.js";
|
|
13
13
|
import { PiAgentFactory } from "./agent/pi.js";
|
|
14
|
-
import { defaultBoardsDir, registerBoardRoutes } from "./boards/boards.js";
|
|
14
|
+
import { defaultBoardsDir, registerBoardRoutes, rotateBoardViews } from "./boards/boards.js";
|
|
15
15
|
import { ChannelStore } from "./channels/config.js";
|
|
16
16
|
import { createControl } from "./channels/control.js";
|
|
17
17
|
import { ConversationStore, resolveConversation } from "./channels/conversations.js";
|
|
@@ -265,6 +265,8 @@ app.onError((err, c) => {
|
|
|
265
265
|
// Before every route: Hono runs middleware in registration order, so a surface
|
|
266
266
|
// added later is covered without knowing this exists.
|
|
267
267
|
const auth = new AuthStore(db);
|
|
268
|
+
// A board page carries no cookie, so the signing key is what a sign-out revokes.
|
|
269
|
+
auth.onRevoke(rotateBoardViews);
|
|
268
270
|
const passkeys = new PasskeyStore(db);
|
|
269
271
|
registerConfigShareRoute(app, configSync);
|
|
270
272
|
app.use("*", requireAuth(auth));
|
package/dist/web/auth.js
CHANGED
|
@@ -192,10 +192,11 @@ export class AuthStore {
|
|
|
192
192
|
}
|
|
193
193
|
const hash = (password, salt) => scryptSync(password, salt, KEY_BYTES).toString("hex");
|
|
194
194
|
const digest = (token) => createHash("sha256").update(token).digest("hex");
|
|
195
|
-
/** The login form, the passkey half of it (web/passkeys.ts)
|
|
196
|
-
* published boards and their stylesheet —
|
|
197
|
-
*
|
|
198
|
-
*
|
|
195
|
+
/** The login form, the passkey half of it (web/passkeys.ts), `/p/*` —
|
|
196
|
+
* published boards and their stylesheet — and `/b/*`, the signed prefix
|
|
197
|
+
* `/boards/*` redirects to once this boundary has passed, are the exemptions
|
|
198
|
+
* here (`/config-sync/:token` is mounted before this middleware,
|
|
199
|
+
* docs/architecture.md names them); `/boards/*` itself stays behind. */
|
|
199
200
|
function isPublic(method, path) {
|
|
200
201
|
if (path === "/login")
|
|
201
202
|
return method === "GET" || method === "HEAD" || method === "POST";
|
|
@@ -205,7 +206,7 @@ function isPublic(method, path) {
|
|
|
205
206
|
return method === "POST";
|
|
206
207
|
if (method !== "GET" && method !== "HEAD")
|
|
207
208
|
return false;
|
|
208
|
-
return path.startsWith("/p/");
|
|
209
|
+
return path.startsWith("/p/") || path.startsWith("/b/");
|
|
209
210
|
}
|
|
210
211
|
/** Constant-time equality that also hides length: both sides are digested. */
|
|
211
212
|
function sameSecret(a, b) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{C as e,G as t,J as n,O as r,P as i,Q as a,V as o,Y as s,Z as c,at as l,et as u,t as d,u as f}from"./index-
|
|
1
|
+
import{C as e,G as t,J as n,O as r,P as i,Q as a,V as o,Y as s,Z as c,at as l,et as u,t as d,u as f}from"./index-CzV2cZEH.js";var p=[[`path`,{d:`M3 5h.01`}],[`path`,{d:`M3 12h.01`}],[`path`,{d:`M3 19h.01`}],[`path`,{d:`M8 5h13`}],[`path`,{d:`M8 12h13`}],[`path`,{d:`M8 19h13`}]],m=[[`path`,{d:`m10.586 5.414-5.172 5.172`}],[`path`,{d:`m18.586 13.414-5.172 5.172`}],[`path`,{d:`M6 12h12`}],[`circle`,{cx:`12`,cy:`20`,r:`2`}],[`circle`,{cx:`12`,cy:`4`,r:`2`}],[`circle`,{cx:`20`,cy:`12`,r:`2`}],[`circle`,{cx:`4`,cy:`12`,r:`2`}]],h=(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},g=e=>e===null?`-`:n(Date.now()-e),_={invocation:{stroke:`stroke-neutral-400`,fill:`fill-neutral-400`},callback:{stroke:`stroke-cyan-500`,fill:`fill-cyan-500`,dash:`6 5`},message:{stroke:`stroke-amber-500`,fill:`fill-amber-500`,dash:`2 5`}},v={scheduler:`border-violet-200 bg-violet-50 text-violet-700`,console:`border-indigo-200 bg-indigo-50 text-indigo-700`,task:`border-amber-200 bg-amber-50 text-amber-700`,process:`border-dashed border-neutral-300 bg-neutral-50 text-neutral-500`,streaming:`border-emerald-200 bg-white font-medium text-neutral-800`,idle:`border-neutral-200 bg-white font-medium text-neutral-600`},y=300,b=4;function x(e,t){let n,r=null,i=!1,a=!1,o=()=>{clearTimeout(n),n=setTimeout(()=>{i||a||!r||r.dataset.closing!==void 0||(f(),r=null)},120)};e.onpointerenter=s=>{s.pointerType!==`touch`&&(i=!0,clearTimeout(n),!(r?.isConnected&&r.dataset.closing===void 0)&&(n=setTimeout(()=>{!e.isConnected||!i||(r=d(e,t),r.onpointerenter=()=>{a=!0,clearTimeout(n)},r.onpointerleave=()=>{a=!1,o()})},y)))},e.onpointerleave=()=>{i=!1,o()},e.onpointerdown=()=>clearTimeout(n)}function S(e){let t,n=!1;e.classList.add(`cursor-grab`),e.onpointerdown=r=>{n=!1,!(r.button!==0||r.pointerType===`touch`||r.target.closest(`[data-node], path`))&&(t={x:r.clientX,y:r.clientY,left:e.scrollLeft,top:e.scrollTop},e.setPointerCapture(r.pointerId),e.classList.add(`cursor-grabbing`,`select-none`))},e.onpointermove=r=>{if(!t)return;let i=r.clientX-t.x,a=r.clientY-t.y;(Math.abs(i)>b||Math.abs(a)>b)&&(n=!0),e.scrollLeft=t.left-i,e.scrollTop=t.top-a};let r=()=>{t=void 0,e.classList.remove(`cursor-grabbing`,`select-none`)};e.onpointerup=r,e.onpointercancel=r,e.addEventListener(`click`,e=>{n&&(e.stopPropagation(),e.preventDefault())},!0)}function C(n,d,f){let y=`sessions`,b=`active`,C={sessions:[],runs:[],messages:[]},w=``,T=a(async()=>{let e=b,t=await u(`/api/activity?scope=${e}`,`Failed to load activity`);if(!t.ok){w=``,n.replaceChildren(s(`p`,`p-4 text-[13px] text-red-600`,t.error));return}if(e!==b)return;let r=JSON.stringify(t.value);if(`${e}:${r}`===w)return;w=`${e}:${r}`;let i=t.value;C={...i,sessions:i.sessions.map(e=>({...e,title:l(e.title)}))},E()});function E(){let e=o(i([[`Sessions`,`sessions`,p],[`Relationships`,`dependencies`,m]],y,e=>{y=e,E()}),s(`span`,`ml-auto text-[11.5px] text-neutral-400`,`${C.sessions.length} sessions · ${C.runs.length} runs`),i([[`Current`,`active`],[`Last 24h`,`recent`]],b,e=>{b=e,E(),T()})),t=s(`div`,`automation-list min-h-0 flex-1 overflow-auto`);y===`sessions`?D(t):k(t),n.replaceChildren(e,t)}function D(e){let t=document.createElement(`table`);t.className=`w-full table-fixed text-left text-[12.5px]`,t.innerHTML=`<thead class="sticky top-0 bg-neutral-50 text-[10.5px] uppercase tracking-wide text-neutral-400 shadow-[inset_0_-1px_0_var(--color-neutral-200)]"><tr>
|
|
2
2
|
<th class="w-[50%] px-4 py-2 font-semibold md:w-[34%]">Session</th>
|
|
3
3
|
<th class="hidden w-[38%] px-2 py-2 font-semibold md:table-cell">Project</th>
|
|
4
4
|
<th class="px-2 py-2 font-semibold md:w-[14%]">State</th>
|
|
Binary file
|
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{$ as e,B as t,E as n,G as r,K as i,M as a,O as o,X as s,Y as c,et as l,it as u,rt as d,w as f}from"./index-
|
|
1
|
+
import{$ as e,B as t,E as n,G as r,K as i,M as a,O as o,X as s,Y as c,et as l,it as u,rt as d,w as f}from"./index-CzV2cZEH.js";var p=e=>e.public&&e.token?`/p/${e.slug}-${e.token}/`:`/boards/${e.slug}/`;function m(m,h){let g=[],_=``,v=c(`header`,`pagehead sticky top-[-8px] z-30 max-md:hidden`,a(`Boards`)),y=c(`div`,`px-4 pb-5 pt-1`);m.append(c(`div`,`min-h-0 flex-1 overflow-y-auto`,v,y));async function b(t,n){let r=await u(`/api/boards/${t.slug}`,{public:n},`PATCH`);if(r.ok)return t.token=(await r.json().catch(()=>({}))).token??``,w();_=await e(r,`Could not change ${t.slug}`),await T()}async function x(e){_=await d(`/api/boards/${e}`,`DELETE`,`Could not delete ${e}`)??``,await T()}function S(e){let n=c(`div`,`group flex flex-col gap-1 py-2.5`),r=document.createElement(`a`);r.className=`min-w-0 flex-1 truncate text-[13px] font-medium text-neutral-800 hover:text-indigo-700 max-md:basis-full`,r.href=p(e),r.target=`_blank`,r.rel=`noreferrer`,r.textContent=e.title;let a=c(`div`,`flex flex-wrap items-center gap-x-2 gap-y-1`,r),o=t(``,``,e.public,t=>{e.public=t,b(e,t)});o.className=`flex flex-none cursor-pointer items-center gap-1.5 text-[11.5px] text-neutral-500`,o.append(c(`span`,``,`Public`));let l=f(`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`);l.title=`Renames the folder on disk; nothing is erased`,l.onclick=()=>void x(e.slug);let u=i(`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)}`);u.title=`Copy the board's link`,a.append(c(`div`,`ml-auto flex flex-none items-center gap-2`,o,u,l)),n.append(a);let d=c(`div`,`flex flex-wrap items-center gap-x-3 gap-y-1 text-[11.5px] text-neutral-500`);d.append(c(`span`,`min-w-0 break-all font-mono text-neutral-400`,e.slug)),e.description&&d.append(c(`span`,`min-w-0 truncate`,e.description)),d.append(c(`span`,`flex-none text-neutral-400`,s(Date.parse(e.updatedAt))));for(let t of e.sessions){let e=f(t.slice(0,8),`flex-none cursor-pointer font-mono text-indigo-600 hover:underline`);e.title=`Open session ${t}`,e.onclick=()=>h(t),d.append(e)}if(e.public&&e.token){let t=c(`span`,`min-w-0 break-all font-mono text-neutral-400`,p(e));t.title=`Public URL — anyone holding this link can read the board, no password`,d.append(t)}return n.append(d),n}let C=[{title:`Public`,hint:`Readable at /p/<slug>-<token>/ by anyone holding the link.`,wanted:!0},{title:`Private`,hint:`Opened from here; the address that opens is a temporary link, good for 8 hours.`,wanted:!1}];function w(){let e=c(`div`,`mx-auto flex w-full max-w-3xl flex-col gap-4`);g.length===0&&e.append(o(`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 C){let a=g.filter(e=>e.public===i);a.length!==0&&e.append(n(t,r,c(`div`,`flex flex-col divide-y divide-neutral-200/70`,...a.map(S))))}y.replaceChildren(..._?[c(`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=r(m,()=>void T());return window.addEventListener(`focus`,()=>{E.visible&&T()}),E}export{m as createBoardsView};
|
|
Binary file
|
|
Binary file
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{G as e,M as t,O as n,W as r,X as i,Y as a,a as o,ct as s,f as c,g as l,h as u,l as d,m as f,mt as p,p as m,pt as ee,q as te,st as h,tt as g,u as _,v as ne,w as re,y as ie}from"./index-
|
|
1
|
+
import{G as e,M as t,O as n,W as r,X as i,Y as a,a as o,ct as s,f as c,g as l,h as u,l as d,m as f,mt as p,p as m,pt as ee,q as te,st as h,tt as g,u as _,v as ne,w as re,y as ie}from"./index-CzV2cZEH.js";var ae=[[`path`,{d:`M12 5v14`}],[`path`,{d:`m19 12-7 7-7-7`}]],oe=[[`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`}]],se=[[`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`}]],ce=[[`path`,{d:`M15 6a9 9 0 0 0-9 9V3`}],[`circle`,{cx:`18`,cy:`6`,r:`3`}],[`circle`,{cx:`6`,cy:`18`,r:`3`}]],v=[[`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`}]],y=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())}`},le=e=>[e.subject,e.body,[[e.author,e.email?`<${e.email}>`:``].filter(Boolean).join(` `),e.at?y(e.at):``].filter(Boolean).join(` · `)].filter(Boolean).join(`
|
|
2
2
|
|
|
3
3
|
`),b=null;function x(){b&&(b.remove(),b=null,document.removeEventListener(`pointerdown`,x,!0),window.removeEventListener(`scroll`,x,!0))}function ue(e,t){e.onmouseenter=()=>{x();let n=t();if(!n)return;b=a(`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`),b.textContent=n,document.body.append(b),document.addEventListener(`pointerdown`,x,!0),window.addEventListener(`scroll`,x,!0);let r=e.getBoundingClientRect();b.style.left=`${Math.max(8,Math.min(r.left,window.innerWidth-b.offsetWidth-8))}px`,b.style.top=r.bottom+6+b.offsetHeight>window.innerHeight?`${r.top-b.offsetHeight-6}px`:`${r.bottom+6}px`},e.onmouseleave=x}function S(e,t,n){let r=a(`button`,`flex w-full cursor-pointer items-center gap-1.5 px-2 py-1 text-left hover:bg-neutral-100`,h(ee,`h-3 w-3 text-indigo-600 ${e.value===t?``:`invisible`}`),a(`span`,`inline-flex flex-none items-center gap-1.5 font-mono text-[11.5px] text-neutral-800`,e.label??e.value));return e.subject&&r.append(a(`span`,`min-w-0 truncate text-[11px] text-neutral-400`,e.subject)),e.author&&r.append(a(`span`,`ml-auto flex-none pl-1.5 text-[10.5px] text-neutral-400`,`${e.author} · ${e.at?i(e.at):``}`)),ue(r,()=>le(e)),r.onclick=()=>n(e.value),r}function de(e,t,n,r,i){let{base:o,head:s}=n,l=()=>s&&o===`${s}~1`?s:``,u=l()?`commit`:`compare`,d=t.commits[0],f=e=>[...e?[{value:``,label:`Working tree`,subject:`the files on disk, uncommitted changes included`}]:[],{...d,value:`HEAD`},...t.refs.map(e=>({value:e.name,subject:e.subject})),...t.commits.map(e=>({...e,value:e.hash}))],p=a(`div`,`flex w-[38rem] max-w-full flex-col`);function m(){let e=(e,t)=>{let n=a(`button`,`cursor-pointer rounded-full px-2.5 py-0.5 text-[11.5px] ${u===t?`bg-indigo-50 font-medium text-indigo-700`:`text-neutral-500 hover:bg-neutral-100`}`,e);return n.onclick=()=>{u=t,m()},n},n=(e,t,n,r)=>a(`div`,`flex min-w-0 flex-1 flex-col`,a(`div`,`flex-none px-2 pb-0.5 text-[10.5px] font-semibold uppercase tracking-wide text-neutral-400`,e),a(`div`,`min-h-0 flex-1 overflow-y-auto`,...t.map(e=>S(e,n,r)))),c=u===`compare`?a(`div`,`flex h-72 min-h-0 divide-x divide-neutral-200 pb-1`,n(`base`,f(!1),o,e=>{o=e,m(),r(o,s)}),n(`to`,f(!0),s,e=>{s=e,m(),r(o,s)})):a(`div`,`flex h-72 min-h-0 flex-col overflow-y-auto pb-1`,...t.commits.map(e=>S({value:e.hash,...e},l(),e=>{o=`${e}~1`,s=e,_(),r(o,s)}))),d=a(`div`,`flex-none border-t border-neutral-200`,S({value:i,label:`${i} ↔ Working tree`,subject:`back to the default: the main line's tip vs the files on disk`},o===i&&s===``?i:``,()=>{o=i,s=``,m(),r(o,s)}));d.querySelector(`button > span`).prepend(h(v)),p.replaceChildren(a(`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`)),c,...u===`compare`?[d]:[])}m(),c(e,p)}var fe=/\.(png|jpe?g|gif|webp|avif|bmp|svg)$/i,C={A:`text-emerald-600`,M:`text-amber-600`,D:`text-red-600`},pe=30,w=`pier.filesPrefs`,T=50,E=()=>{try{let e=JSON.parse(localStorage.getItem(w)??`{}`);return e&&typeof e==`object`?e:{}}catch{return{}}},me=e=>{let t=e?E()[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},he=(e,t)=>{let n=E();delete n[e],n[e]=t;let r=Object.keys(n);for(let e of r.slice(0,Math.max(0,r.length-T)))delete n[e];try{localStorage.setItem(w,JSON.stringify(n))}catch(e){console.warn(`Files: could not remember this folder`,e)}},ge=`flex min-w-0 flex-none items-center gap-1.5 rounded-full px-2.5 py-1 font-mono text-[11.5px]`,_e=(e,t=`indigo`)=>re(e,`${ge} cursor-pointer truncate text-left ${t===`indigo`?`bg-indigo-50 font-medium text-indigo-700 hover:bg-indigo-100`:`bg-neutral-100 text-neutral-600 hover:bg-neutral-200`}`);function D(i,c,ee,_){let v=``,y=``,b={branch:null,refs:[],commits:[],worktrees:[]},x=`HEAD`,S=``,w=new Map,T=!0,E=!1,D=[],O=new Set,k=null,A=null,j=null,ve=a(`header`,`pagehead flex-nowrap`),M=a(`div`,`${l} flex-col items-stretch gap-1.5 px-3 py-2.5`),N=a(`div`,`min-h-0 flex-1 overflow-y-auto px-1.5 py-1.5`),ye=a(`aside`,`${ie} w-64 flex-none text-[12.5px] leading-5 max-md:h-1/2 max-md:w-full`,M,N),P=a(`div`,`${l} flex-wrap gap-2 px-4 py-2.5 font-mono text-[11.5px] text-neutral-500`),F=a(`div`,`h-full min-w-0 overflow-auto`),I=a(`div`,`pointer-events-none absolute inset-y-1 right-0.5 z-20 hidden w-2`),be=a(`div`,`relative min-h-0 flex-1`,F,I),xe=a(`section`,`${ie} min-h-0 min-w-0 flex-1`,P,be);i.append(ve,a(`div`,`flex min-h-0 flex-1 gap-3 px-4 pb-4 pt-1 max-md:flex-col`,ye,xe));let L=(e,t)=>`/api/explorer/${e}?${new URLSearchParams({root:v,...t})}`,R=(e,t)=>`/api/fs/${e}?${new URLSearchParams({root:v,...t})}`,z=(e,t=`text-neutral-400`)=>a(`p`,`px-3 py-2 text-[12.5px] ${t}`,e),B=e=>a(`div`,`flex h-full items-center justify-center p-6`,n(e)),Se=()=>T&&b.branch!==null,Ce=e=>{let t=0;for(let n of w.keys())n.startsWith(`${e}/`)&&t++;return t};function we(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 Se()&&(i=t.filter(e=>e.dir?Ce(`${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?Ee(`${r}${e.name}`,e.name):De(`${r}${e.name}`,e.name))}async function Te(e,t){try{let{entries:n}=await g(R(`ls`,{path:t}),`could not list this folder`);e.replaceChildren(...we(t,n)),e.childElementCount||e.append(z(Se()?`No changes.`:`Empty.`))}catch(n){let r=we(t,[]);r.length?e.replaceChildren(...r):e.replaceChildren(z(String(n),`text-red-600`))}}function Ee(e,t){let n=Ce(e),r=[a(`span`,`truncate`,t)];n&&r.push(a(`span`,`ml-auto flex-none rounded-full bg-amber-100/80 px-1.5 font-mono text-[10px] font-semibold text-amber-700`,String(n)));let{el:i,summary:o}=te(``,r);o.className+=` rounded-lg px-2 py-1 transition-colors hover:bg-neutral-100`;let s=a(`div`,`pl-3`);i.append(s);let c=!1,l=()=>{c||(c=!0,s.append(z(`…`)),Te(s,e))};return i.ontoggle=()=>{O[i.open?`add`:`delete`](e),i.open&&l()},((b.branch?T:!e.includes(`/`))&&!E||O.has(e))&&(i.open=!0,l()),i}function V(e){A?.classList.remove(`bg-indigo-50`,`font-medium`,`text-indigo-700`),A=e,e.classList.add(`bg-indigo-50`,`font-medium`,`text-indigo-700`)}function De(e,t){let n=w.get(e)?.status,r=a(`button`,`flex w-full cursor-pointer items-center gap-1.5 rounded-lg py-1 pl-6 pr-2 text-left transition-colors hover:bg-neutral-100`,a(`span`,`truncate`,t));return n&&r.append(a(`span`,`ml-auto flex-none font-mono text-[10.5px] font-semibold ${C[n]??`text-neutral-500`}`,n)),r.title=e,e===k&&V(r),e===j&&(j=null,k=e,V(r),J(e)),r.onclick=()=>{V(r),k=e,J(e)},r}let H=()=>void Te(N,``),U=(e,t)=>[...e?[a(`span`,`flex-none font-mono text-[10.5px] font-semibold text-emerald-600`,`+${e}`)]:[],...t?[a(`span`,`flex-none font-mono text-[10.5px] font-semibold text-red-600`,`−${t}`)]:[]];function W(e,t=!1,n){if(P.classList.toggle(`hidden`,!e),P.classList.toggle(`flex`,!!e),!e)return P.replaceChildren();let i=w.get(e),o=[a(`span`,`min-w-0 truncate`,e)];i&&(o.push(a(`span`,`flex-none font-semibold ${C[i.status]??``}`,i.status)),o.push(...U(i.add,i.del)));let s=a(`div`,`ml-auto flex flex-none items-center gap-2`);if(n&&s.append(n),t){let t=document.createElement(`a`);t.className=`btn flex-none text-[12px]`,t.href=R(`file`,{path:e}),t.download=r(e),t.textContent=`Download`,s.append(t)}P.replaceChildren(...o,s)}function Oe(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=Ie();function K(){I.classList.add(`hidden`),I.replaceChildren(),G.set([],[])}let ke=0,q=e=>e===ke;function J(e){let t=++ke;return w.has(e)?Le(e,t):Ae(e,t)}async function Ae(e,t){if(!q(t))return;K();let n=R(`file`,{path:e}),r=a(`div`,`min-w-0`);if(F.classList.remove(`flex`,`flex-col`),W(e,!0),F.replaceChildren(r),fe.test(e)){let e=a(`img`,`max-w-full p-4`);e.src=n,e.onerror=()=>r.replaceChildren(z(`Could not load image.`,`text-red-600`)),r.append(e);return}if(e.toLowerCase().endsWith(`.pdf`)){let e=document.createElement(`iframe`);e.className=`h-full w-full`,e.src=n,F.replaceChildren(e),F.classList.add(`flex`,`flex-col`);return}r.append(z(`…`));let i=await fetch(n);if(!q(t))return;if(!i.ok){let{error:e}=await i.json().catch(()=>({}));r.replaceChildren(z(e??`HTTP ${i.status}`,`text-red-600`));return}if(!(i.headers.get(`content-type`)??``).startsWith(`text/plain`)){r.replaceChildren(z(`Binary file — use Download.`));return}let o=f(await i.text());if(!q(t))return;let s=await u(e);q(t)&&r.replaceChildren(m(o,s))}function je(e){let t=[],n=0,r=0,i=!1,a=e.split(`
|
|
4
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 Me(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 Ne={add:`bg-emerald-400`,del:`bg-red-400`,mixed:`bg-amber-400`};function Pe(e,t,n){I.replaceChildren(...e.map(e=>{let r=a(`button`,`map-mark pointer-events-auto absolute left-1/2 w-1 -translate-x-1/2 cursor-pointer rounded-full ${Ne[e.tone]}`);return r.style.top=`${e.start/t*100}%`,r.style.height=`${Math.max(.4,(e.end-e.start+1)/t*100)}%`,r.title=`line ${Fe(n,e.start)}`,r.onclick=()=>n[e.start].scrollIntoView({block:`center`}),r})),I.classList.remove(`hidden`)}let Fe=(e,t)=>e[t]?.querySelector(`span`)?.textContent?.trim()||String(t+1);function Ie(){let e=[],t=[],n=-1,r=a(`span`,`flex-none text-[10.5px] text-neutral-400`),s=i=>{e.length&&(n=(n+i+e.length)%e.length,t[e[n].start].scrollIntoView({block:`center`}),r.textContent=`${n+1}/${e.length}`)},c=()=>e.length>0&&!i.classList.contains(`hidden`),l=(e,t,n,r)=>{let i=a(`button`,`icon-btn flex-none`,h(e));return i.onclick=()=>s(t),o(i,r,n,()=>s(t),c),i};return{el:a(`div`,`flex flex-none items-center gap-1`,r,l(p,-1,`Previous change`,[`p`,`k`]),l(ae,1,`Next change`,[`n`,`j`])),set(i,a){e=i,t=a,n=-1,r.textContent=`${i.length} change${i.length===1?``:`s`}`}}}async function Le(e,t){K(),F.classList.remove(`flex`,`flex-col`);let n=w.get(e)?.status!==`D`;W(e,n),F.replaceChildren(z(`…`));try{let{diff:r}=await g(L(`diff`,{base:x,head:S,file:e,context:`99999`}),`could not read this diff`);if(!q(t))return;if(!r||/^Binary files /m.test(r))return Ae(e,t);let i=je(r);Oe(i);let a=await u(e);if(!q(t))return;let o=m(i,a),s=[...o.children],c=Me(i);G.set(c,s),W(e,n,G.el),F.replaceChildren(o),Pe(c,i.length,s)}catch(n){if(!q(t))return;W(e),F.replaceChildren(z(String(n),`text-red-600`))}}let Y=()=>S&&x===`${S}~1`?S:``,Re=()=>b.commits.find(e=>e.hash===Y())?.subject??``,ze=()=>Y()?`${Y()} — ${Re()||`commit`}`:`${x} ↔ ${S||`working tree`}`,Be=(e,t,n,r)=>{let i=a(`button`,`icon-btn ${n?`bg-indigo-50 text-indigo-700 hover:bg-indigo-100 hover:text-indigo-700`:``}`);return i.append(h(e)),i.title=t,i.setAttribute(`aria-label`,t),i.setAttribute(`aria-pressed`,String(n)),i.onclick=r,i};function X(){let e=Be(se,`Show only files that differ`,T,()=>{T=!T,X(),H()}),t=Be(oe,E?`Restore folders`:`Collapse all folders`,E,()=>{if(E=!E,E)D=[...O],O.clear();else for(let e of D)O.add(e);X(),H()});if(!b.branch){M.replaceChildren(a(`div`,`flex items-center gap-1.5`,a(`span`,`min-w-0 flex-1 text-[11px] leading-snug text-neutral-400`,`Not a git repository — nothing to compare.`),t));return}let n=re(ze(),`${ne} font-mono`);if(Y()){let e=b.commits.find(e=>e.hash===Y());ue(n,()=>e?le(e):``)}else n.title=`Choose what to diff`;n.onclick=async()=>{await We(),de(n,b,{base:x,head:S},(e,t)=>{x=e,S=t,Z()},$())};let[r,i]=[0,0];for(let e of w.values())r+=e.add,i+=e.del;M.replaceChildren(n,a(`div`,`flex items-center gap-1.5 pl-0.5`,a(`span`,`text-[11px] text-neutral-400`,`${w.size} changed file${w.size===1?``:`s`}`),...U(r,i),a(`span`,`ml-auto`),e,t))}let Ve=()=>{y&&v&&he(y,{cwd:v,base:x,head:S})},He=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 Ue(){if(w=new Map,!b.branch)return;let{files:e}=await g(L(`diff`,{base:x,head:S}),`could not read what changed`);for(let t of e)w.set(t.path,{status:t.status,add:t.add,del:t.del});if(w.size<=pe)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 Z(){Ve();try{await Ue()}catch(e){X(),N.replaceChildren(z(String(e),`text-red-600`));return}X(),H(),k&&J(k)}function Q(){let e=_e(``,`neutral`);e.className+=` max-w-72`,e.append(a(`span`,`truncate max-md:hidden`,v||`Choose a folder…`),a(`span`,`truncate md:hidden`,v?r(v):`Choose a folder…`)),e.title=v?`${v} — switch folder`:`Choose a folder`,e.onclick=()=>d(e,b.worktrees.map(e=>({path:e.path,...e.branch?{hint:e.branch}:{}})),v||void 0,ee);let n=a(`button`,`icon-btn ml-auto`,h(s));n.type=`button`,n.title=`Close Files`,n.setAttribute(`aria-label`,`Close Files`),n.onclick=_;let i=b.branch?[a(`span`,`${ge} bg-neutral-100 text-neutral-600`,h(ce),a(`span`,`truncate`,b.branch))]:[];ve.replaceChildren(t(`Files`),e,...i,n)}async function We(){if(v)try{b=await g(L(`git`,{}),`could not read this repository`),Q()}catch(e){F.replaceChildren(B(String(e)))}}let $=()=>b.refs.some(e=>e.name===`main`)?`main`:b.refs.some(e=>e.name===`master`)?`master`:`HEAD`;async function Ge(e){if(Q(),k=null,A=null,O.clear(),j=e||null,e){T=!1;let t=e.split(`/`);for(let e=1;e<t.length;e++)O.add(t.slice(0,e).join(`/`))}if(K(),F.classList.remove(`flex`,`flex-col`),W(),F.replaceChildren(B(`Select a file.`)),!v){M.replaceChildren(),N.replaceChildren(),F.replaceChildren(B(`No folder yet — pick one from the chip above.`));return}try{b=await g(L(`git`,{}),`could not read this repository`)}catch(e){b={branch:null,refs:[],commits:[],worktrees:[]},F.replaceChildren(B(String(e)))}let t=me(y),n=t&&t.cwd===v&&He(t.base)&&He(t.head)?t:null;x=n?.base??$(),S=n?.head??``,Q(),await Z()}return e(i,(e,t)=>{let n=c(),r=n?.id??``,i=new URLSearchParams(t).get(`select`)??void 0,a=e?.startsWith(`/`)?e:(r===y?v:``)||me(r)?.cwd||n?.cwd||``;if(a===v&&r===y&&N.childElementCount&&(!i||i===k)){Q(),We().then(Z);return}y=r,v=a,Ge(i)},()=>{K(),F.classList.remove(`flex`,`flex-col`),W(),F.replaceChildren(B(`Select a file.`))})}export{D as createExplorerView};
|
|
Binary file
|
|
Binary file
|