@timqi/pier 0.1.0 → 0.1.2
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/README.md +58 -125
- package/dist/agent/config.js +6 -15
- package/dist/agent/credentials.js +11 -23
- package/dist/agent/events.js +42 -64
- package/dist/agent/listing.js +39 -92
- package/dist/agent/pi.js +103 -252
- package/dist/boards/boards.js +19 -29
- package/dist/channels/attach.js +14 -42
- package/dist/channels/chains.js +33 -37
- package/dist/channels/chunk.js +8 -28
- package/dist/channels/commands.js +3 -14
- package/dist/channels/config.js +33 -52
- package/dist/channels/control.js +4 -13
- package/dist/channels/conversations.js +8 -25
- package/dist/channels/dedup.js +8 -17
- package/dist/channels/gatekeeper.js +13 -23
- package/dist/channels/lark-api.js +23 -63
- package/dist/channels/lark-outbound.js +12 -44
- package/dist/channels/lark-panel.js +7 -23
- package/dist/channels/lark-render.js +18 -62
- package/dist/channels/lark.js +52 -141
- package/dist/channels/lines.js +13 -15
- package/dist/channels/panel.js +16 -36
- package/dist/channels/receipts.js +29 -52
- package/dist/channels/routes.js +3 -9
- package/dist/channels/runtime.js +12 -23
- package/dist/channels/slack-api.js +34 -86
- package/dist/channels/slack-directory.js +7 -23
- package/dist/channels/slack-outbound.js +12 -56
- package/dist/channels/slack-panel.js +4 -13
- package/dist/channels/slack-render.js +23 -91
- package/dist/channels/slack-tool.js +48 -171
- package/dist/channels/slack.js +73 -239
- package/dist/channels/telegram-api.js +8 -20
- package/dist/channels/telegram-panel.js +5 -21
- package/dist/channels/telegram-render.js +13 -40
- package/dist/channels/telegram.js +54 -146
- package/dist/channels/types.js +5 -16
- package/dist/cli.js +17 -41
- package/dist/config-sync.js +87 -4
- package/dist/core/hub.js +7 -20
- package/dist/core/identity.js +20 -59
- package/dist/core/inbound-file.js +15 -49
- package/dist/core/inbox.js +12 -34
- package/dist/core/queue.js +3 -5
- package/dist/core/reply.js +41 -142
- package/dist/core/router.js +209 -264
- package/dist/core/types.js +4 -0
- package/dist/db.js +88 -272
- package/dist/drain.js +57 -50
- package/dist/extensions/index.js +3 -11
- package/dist/extensions/web/anthropic.js +3 -9
- package/dist/extensions/web/artifacts.js +2 -5
- package/dist/extensions/web/content.js +4 -12
- package/dist/extensions/web/http.js +2 -6
- package/dist/extensions/web/language.js +8 -18
- package/dist/extensions/web/openai.js +1 -1
- package/dist/extensions/web/provider.js +5 -18
- package/dist/extensions/web/tools.js +19 -63
- package/dist/lock.js +98 -0
- package/dist/log.js +9 -26
- package/dist/main.js +84 -183
- package/dist/paths.js +10 -26
- package/dist/secrets.js +18 -45
- package/dist/service.js +31 -73
- package/dist/settings.js +19 -63
- package/dist/tasks/agent.js +24 -45
- package/dist/tasks/callbacks.js +8 -16
- package/dist/tasks/command.js +2 -6
- package/dist/tasks/definitions.js +39 -62
- package/dist/tasks/execution.js +41 -39
- package/dist/tasks/groups.js +8 -11
- package/dist/tasks/messages.js +88 -155
- package/dist/tasks/outbox.js +33 -54
- package/dist/tasks/routes.js +4 -7
- package/dist/tasks/runs.js +4 -9
- package/dist/tasks/service.js +29 -42
- package/dist/tasks/store.js +36 -27
- package/dist/tasks/tool.js +59 -60
- package/dist/tools-task.js +20 -60
- package/dist/tools.js +98 -325
- package/dist/update.js +20 -43
- package/dist/web/auth.js +118 -179
- package/dist/web/config-sync.js +2 -2
- package/dist/web/config.js +3 -7
- package/dist/web/explorer.js +10 -21
- package/dist/web/fs.js +20 -42
- package/dist/web/instance.js +35 -82
- package/dist/web/providers.js +5 -11
- package/dist/web/public/assets/{activity-Bl3vZukb.js → activity-DOr8dWeX.js} +1 -1
- package/dist/web/public/assets/activity-DOr8dWeX.js.br +0 -0
- package/dist/web/public/assets/activity-DOr8dWeX.js.gz +0 -0
- package/dist/web/public/assets/{boards-DYuf4Mlj.js → boards-CneyR23E.js} +1 -1
- package/dist/web/public/assets/boards-CneyR23E.js.br +0 -0
- package/dist/web/public/assets/boards-CneyR23E.js.gz +0 -0
- package/dist/web/public/assets/explorer-D0srXT1c.js +4 -0
- package/dist/web/public/assets/explorer-D0srXT1c.js.br +0 -0
- package/dist/web/public/assets/explorer-D0srXT1c.js.gz +0 -0
- package/dist/web/public/assets/index-DbFu15NN.js +85 -0
- package/dist/web/public/assets/index-DbFu15NN.js.br +0 -0
- package/dist/web/public/assets/index-DbFu15NN.js.gz +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css +2 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.br +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.gz +0 -0
- package/dist/web/public/assets/{runs-BLJu7EXN.js → runs-Cv0A-e08.js} +1 -1
- package/dist/web/public/assets/runs-Cv0A-e08.js.br +0 -0
- package/dist/web/public/assets/runs-Cv0A-e08.js.gz +0 -0
- package/dist/web/public/assets/{settings-BrdVh-Zi.js → settings-VmCjhGBd.js} +1 -1
- package/dist/web/public/assets/settings-VmCjhGBd.js.br +0 -0
- package/dist/web/public/assets/settings-VmCjhGBd.js.gz +0 -0
- package/dist/web/public/assets/{task-runs-CeQS1rxa.js → task-runs-CqpTV644.js} +1 -1
- package/dist/web/public/assets/task-runs-CqpTV644.js.br +0 -0
- package/dist/web/public/assets/task-runs-CqpTV644.js.gz +0 -0
- package/dist/web/public/assets/tasks-BffPVgXg.js +4 -0
- package/dist/web/public/assets/tasks-BffPVgXg.js.br +0 -0
- package/dist/web/public/assets/tasks-BffPVgXg.js.gz +0 -0
- package/dist/web/public/index.html +30 -16
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/sw.js +14 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/push.js +55 -77
- package/dist/web/route.js +3 -7
- package/dist/web/server.js +109 -190
- package/dist/web/session-state.js +13 -53
- package/dist/web/types.js +2 -4
- package/dist/web/webpush.js +10 -25
- package/docs/deploy.md +115 -330
- package/package.json +1 -1
- package/skills/pier-boards/SKILL.md +81 -160
- package/skills/pier-help/SKILL.md +23 -20
- package/skills/pier-slack/SKILL.md +2 -2
- package/skills/pier-tasks/SKILL.md +23 -15
- package/dist/config-sync-fetch.js +0 -84
- package/dist/limits.js +0 -14
- package/dist/web/public/assets/activity-Bl3vZukb.js.br +0 -0
- package/dist/web/public/assets/activity-Bl3vZukb.js.gz +0 -0
- package/dist/web/public/assets/boards-DYuf4Mlj.js.br +0 -0
- package/dist/web/public/assets/boards-DYuf4Mlj.js.gz +0 -0
- package/dist/web/public/assets/explorer-qJH_9nTE.js +0 -4
- package/dist/web/public/assets/explorer-qJH_9nTE.js.br +0 -0
- package/dist/web/public/assets/explorer-qJH_9nTE.js.gz +0 -0
- package/dist/web/public/assets/index-Dqdb-Eqt.js +0 -85
- package/dist/web/public/assets/index-Dqdb-Eqt.js.br +0 -0
- package/dist/web/public/assets/index-Dqdb-Eqt.js.gz +0 -0
- package/dist/web/public/assets/index-DzmMzvi_.css +0 -2
- package/dist/web/public/assets/index-DzmMzvi_.css.br +0 -0
- package/dist/web/public/assets/index-DzmMzvi_.css.gz +0 -0
- package/dist/web/public/assets/runs-BLJu7EXN.js.br +0 -0
- package/dist/web/public/assets/runs-BLJu7EXN.js.gz +0 -0
- package/dist/web/public/assets/settings-BrdVh-Zi.js.br +0 -0
- package/dist/web/public/assets/settings-BrdVh-Zi.js.gz +0 -0
- package/dist/web/public/assets/task-runs-CeQS1rxa.js.br +0 -0
- package/dist/web/public/assets/task-runs-CeQS1rxa.js.gz +0 -0
- package/dist/web/public/assets/tasks-bcb3fYdK.js +0 -4
- package/dist/web/public/assets/tasks-bcb3fYdK.js.br +0 -0
- 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
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
23
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
//
|
|
58
|
-
//
|
|
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
|
-
//
|
|
68
|
-
//
|
|
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
|
-
//
|
|
98
|
-
//
|
|
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
|
-
//
|
|
110
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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 : "";
|
package/dist/web/instance.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
// Routes about the Pier instance itself
|
|
2
|
-
//
|
|
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
|
|
7
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
//
|
|
25
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
53
|
-
//
|
|
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
|
-
/**
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
81
|
-
//
|
|
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
|
-
//
|
|
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
|
|
126
|
-
//
|
|
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
|
-
//
|
|
142
|
-
//
|
|
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
|
-
/**
|
|
174
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
//
|
|
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
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
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
|
|
278
|
-
//
|
|
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
|
-
//
|
|
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
|
|
288
|
-
//
|
|
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
|
-
//
|
|
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() });
|
package/dist/web/providers.js
CHANGED
|
@@ -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
|
-
/**
|
|
5
|
-
*
|
|
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
|
|
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
|
-
//
|
|
139
|
-
// "
|
|
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-
|
|
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-DbFu15NN.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>
|
|
Binary file
|
|
Binary file
|
|
@@ -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-
|
|
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-DbFu15NN.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};
|
|
Binary file
|
|
Binary file
|
|
@@ -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-DbFu15NN.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};
|
|
Binary file
|
|
Binary file
|