@timqi/pier 0.0.26 → 0.0.28
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/agent/config-sync.js +28 -8
- package/dist/agent/config.js +64 -15
- package/dist/core/identity.js +17 -1
- package/dist/db.js +9 -0
- package/dist/main.js +1 -0
- package/dist/settings.js +4 -1
- package/dist/tasks/service.js +4 -0
- package/dist/tasks/store.js +12 -0
- package/dist/tools.js +17 -3
- package/dist/web/public/assets/{activity-BJ7YoxQo.js → activity-DVamP8fs.js} +2 -2
- package/dist/web/public/assets/activity-DVamP8fs.js.br +0 -0
- package/dist/web/public/assets/activity-DVamP8fs.js.gz +0 -0
- package/dist/web/public/assets/boards-BKtjPwus.js +1 -0
- package/dist/web/public/assets/boards-BKtjPwus.js.br +0 -0
- package/dist/web/public/assets/boards-BKtjPwus.js.gz +0 -0
- package/dist/web/public/assets/explorer-CjO9NAKQ.js +4 -0
- package/dist/web/public/assets/explorer-CjO9NAKQ.js.br +0 -0
- package/dist/web/public/assets/explorer-CjO9NAKQ.js.gz +0 -0
- package/dist/web/public/assets/{index-CZeL3DfU.css → index-C23sXyS3.css} +1 -1
- package/dist/web/public/assets/index-C23sXyS3.css.br +0 -0
- package/dist/web/public/assets/index-C23sXyS3.css.gz +0 -0
- package/dist/web/public/assets/index-DzWnvhXb.js +85 -0
- package/dist/web/public/assets/index-DzWnvhXb.js.br +0 -0
- package/dist/web/public/assets/index-DzWnvhXb.js.gz +0 -0
- package/dist/web/public/assets/runs-CxIZAx1s.js +1 -0
- package/dist/web/public/assets/runs-CxIZAx1s.js.br +0 -0
- package/dist/web/public/assets/runs-CxIZAx1s.js.gz +0 -0
- package/dist/web/public/assets/settings-B8JWLbpL.js +5 -0
- package/dist/web/public/assets/settings-B8JWLbpL.js.br +0 -0
- package/dist/web/public/assets/settings-B8JWLbpL.js.gz +0 -0
- package/dist/web/public/assets/{task-runs-DsOxAyRj.js → task-runs-BZ1brkLC.js} +2 -2
- package/dist/web/public/assets/task-runs-BZ1brkLC.js.br +0 -0
- package/dist/web/public/assets/task-runs-BZ1brkLC.js.gz +0 -0
- package/dist/web/public/assets/tasks-BoYiBQpq.js +4 -0
- package/dist/web/public/assets/tasks-BoYiBQpq.js.br +0 -0
- package/dist/web/public/assets/tasks-BoYiBQpq.js.gz +0 -0
- package/dist/web/public/index.html +7 -6
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/server.js +29 -62
- package/dist/web/session-state.js +14 -32
- package/package.json +1 -1
- package/dist/web/public/assets/activity-BJ7YoxQo.js.br +0 -0
- package/dist/web/public/assets/activity-BJ7YoxQo.js.gz +0 -0
- package/dist/web/public/assets/boards-7GDMK7s8.js +0 -1
- package/dist/web/public/assets/boards-7GDMK7s8.js.br +0 -0
- package/dist/web/public/assets/boards-7GDMK7s8.js.gz +0 -0
- package/dist/web/public/assets/code-8o-S-MIZ.js +0 -1
- package/dist/web/public/assets/explorer-DBusW0TA.js +0 -5
- package/dist/web/public/assets/explorer-DBusW0TA.js.br +0 -0
- package/dist/web/public/assets/explorer-DBusW0TA.js.gz +0 -0
- package/dist/web/public/assets/index-CZeL3DfU.css.br +0 -0
- package/dist/web/public/assets/index-CZeL3DfU.css.gz +0 -0
- package/dist/web/public/assets/index-CquEUmXo.js +0 -84
- package/dist/web/public/assets/index-CquEUmXo.js.br +0 -0
- package/dist/web/public/assets/index-CquEUmXo.js.gz +0 -0
- package/dist/web/public/assets/runs-BjcdVVmW.js +0 -1
- package/dist/web/public/assets/runs-BjcdVVmW.js.br +0 -0
- package/dist/web/public/assets/runs-BjcdVVmW.js.gz +0 -0
- package/dist/web/public/assets/settings-DH3Nlk9f.js +0 -6
- package/dist/web/public/assets/settings-DH3Nlk9f.js.br +0 -0
- package/dist/web/public/assets/settings-DH3Nlk9f.js.gz +0 -0
- package/dist/web/public/assets/task-runs-DsOxAyRj.js.br +0 -0
- package/dist/web/public/assets/task-runs-DsOxAyRj.js.gz +0 -0
- package/dist/web/public/assets/tasks-CYfdrdF2.js +0 -4
- package/dist/web/public/assets/tasks-CYfdrdF2.js.br +0 -0
- package/dist/web/public/assets/tasks-CYfdrdF2.js.gz +0 -0
- package/dist/web/repos.js +0 -75
|
@@ -52,20 +52,30 @@ function project(value, where, onPrivate) {
|
|
|
52
52
|
return [[key, project(entry, `${where} ${key}`, onPrivate)]];
|
|
53
53
|
}));
|
|
54
54
|
}
|
|
55
|
+
/** A provider id that cannot address anything but its own record. */
|
|
56
|
+
function providerId(id) {
|
|
57
|
+
if (typeof id !== "string" || id.length > 100 || !/^[a-z0-9][a-z0-9._-]*$/.test(id) ||
|
|
58
|
+
["constructor", "prototype"].includes(id)) {
|
|
59
|
+
throw new Error("invalid snapshot provider id");
|
|
60
|
+
}
|
|
61
|
+
return id;
|
|
62
|
+
}
|
|
63
|
+
/** A model id both sides can address — in a catalog or as the default. */
|
|
64
|
+
function modelId(id) {
|
|
65
|
+
if (typeof id !== "string" || !id.trim() || id !== id.trim() || id.length > 100) {
|
|
66
|
+
throw new Error("model id must be a non-empty trimmed string");
|
|
67
|
+
}
|
|
68
|
+
return id;
|
|
69
|
+
}
|
|
55
70
|
/** The shape both sides must agree on: a provider id that cannot address
|
|
56
71
|
* anything but its own record, and a catalog addressable by model id. */
|
|
57
72
|
function portable(id, value, onPrivate) {
|
|
58
|
-
|
|
59
|
-
throw new Error("invalid snapshot provider id");
|
|
60
|
-
}
|
|
73
|
+
providerId(id);
|
|
61
74
|
const provider = record(project(value, `provider ${id}`, onPrivate), `provider ${id}`);
|
|
62
75
|
const models = provider.models ?? [];
|
|
63
76
|
if (!Array.isArray(models))
|
|
64
77
|
throw new Error("models must be an array");
|
|
65
|
-
const ids = models.map((model) => record(model, "model").id);
|
|
66
|
-
if (ids.some((modelId) => typeof modelId !== "string" || !modelId.trim() || modelId !== modelId.trim())) {
|
|
67
|
-
throw new Error("model id must be a non-empty trimmed string");
|
|
68
|
-
}
|
|
78
|
+
const ids = models.map((model) => modelId(record(model, "model").id));
|
|
69
79
|
if (new Set(ids).size !== ids.length)
|
|
70
80
|
throw new Error("duplicate model ids");
|
|
71
81
|
const overrides = provider.modelOverrides ?? {};
|
|
@@ -91,11 +101,20 @@ export function snapshotProviders(providers = {}) {
|
|
|
91
101
|
}
|
|
92
102
|
return shared;
|
|
93
103
|
}
|
|
104
|
+
/** A default model names a provider and a model; the provider is not required
|
|
105
|
+
* to be in models.json, because Pi's own built-ins never are. */
|
|
106
|
+
function normalizeDefaultModel(raw) {
|
|
107
|
+
if (raw === null)
|
|
108
|
+
return null;
|
|
109
|
+
const ref = record(raw, "default model");
|
|
110
|
+
only(ref, ["provider", "id"], "default model");
|
|
111
|
+
return { provider: providerId(ref.provider), id: modelId(ref.id) };
|
|
112
|
+
}
|
|
94
113
|
/** Strict import boundary: a source that states a private field is refused,
|
|
95
114
|
* never trusted as a mask over the local one. */
|
|
96
115
|
export function normalizeAgentSnapshot(raw) {
|
|
97
116
|
const snapshot = record(raw, "agent snapshot");
|
|
98
|
-
only(snapshot, ["files", "providers"], "agent snapshot");
|
|
117
|
+
only(snapshot, ["files", "providers", "defaultModel"], "agent snapshot");
|
|
99
118
|
const files = record(snapshot.files, "snapshot files");
|
|
100
119
|
only(files, ["SYSTEM.md", "AGENTS.md"], "snapshot files");
|
|
101
120
|
for (const name of ["SYSTEM.md", "AGENTS.md"]) {
|
|
@@ -110,6 +129,7 @@ export function normalizeAgentSnapshot(raw) {
|
|
|
110
129
|
return {
|
|
111
130
|
files: { "SYSTEM.md": files["SYSTEM.md"], "AGENTS.md": files["AGENTS.md"] },
|
|
112
131
|
providers,
|
|
132
|
+
...(snapshot.defaultModel !== undefined ? { defaultModel: normalizeDefaultModel(snapshot.defaultModel) } : {}),
|
|
113
133
|
};
|
|
114
134
|
}
|
|
115
135
|
/** A model or override arrives whole; only the local private fields survive it. */
|
package/dist/agent/config.js
CHANGED
|
@@ -9,7 +9,9 @@ import { pierPath } from "../paths.js";
|
|
|
9
9
|
import { mergeSnapshotProviders, normalizeAgentSnapshot, snapshotProviders } from "./config-sync.js";
|
|
10
10
|
const GLOBAL_FILES = ["SYSTEM.md", "AGENTS.md", "settings.json", "models.json"];
|
|
11
11
|
const PROJECT_FILES = ["AGENTS.md"];
|
|
12
|
-
|
|
12
|
+
// settings.json is on the list for one field: the default model. Everything
|
|
13
|
+
// else in it is machine-local and survives an import untouched.
|
|
14
|
+
const SNAPSHOT_FILES = ["SYSTEM.md", "AGENTS.md", "models.json", "settings.json"];
|
|
13
15
|
const RESOURCE_DEPTH = 3; // extensions/skills nest at most a couple of levels
|
|
14
16
|
/** Pier owns the Pi runtime dir: config lives in the syncable `~/.pier/pi`
|
|
15
17
|
* repo, not `~/.pi`. main.ts exports this as PI_CODING_AGENT_DIR so the SDK's
|
|
@@ -211,13 +213,14 @@ export class PiConfigStore {
|
|
|
211
213
|
}
|
|
212
214
|
exportSnapshot() {
|
|
213
215
|
return this.#withWrite(async () => {
|
|
214
|
-
const [system, agents, raw] = await Promise.all(SNAPSHOT_FILES.map((name) => readNullable(join(this.agentDir, name))));
|
|
216
|
+
const [system, agents, raw, rawSettings] = await Promise.all(SNAPSHOT_FILES.map((name) => readNullable(join(this.agentDir, name))));
|
|
215
217
|
const parsed = raw?.trim() ? parseModels(raw) : {};
|
|
216
218
|
if (!parsed)
|
|
217
219
|
throw new Error("models.json must be valid JSON before exporting configuration");
|
|
218
220
|
return normalizeAgentSnapshot({
|
|
219
221
|
files: { "SYSTEM.md": system, "AGENTS.md": agents },
|
|
220
222
|
providers: snapshotProviders(parsed.providers),
|
|
223
|
+
defaultModel: defaultModelRef(rawSettings),
|
|
221
224
|
});
|
|
222
225
|
});
|
|
223
226
|
}
|
|
@@ -233,7 +236,13 @@ export class PiConfigStore {
|
|
|
233
236
|
const providers = mergeSnapshotProviders(parsed.providers, incoming.providers);
|
|
234
237
|
const models = JSON.stringify(providers) === JSON.stringify(parsed.providers ?? {})
|
|
235
238
|
? raw : `${JSON.stringify({ ...parsed, providers }, null, 2)}\n`;
|
|
236
|
-
const
|
|
239
|
+
const rawSettings = before[3];
|
|
240
|
+
// A source that never states a default (an older one) leaves the local
|
|
241
|
+
// settings.json alone; `null` is a stated "no default" and clears it.
|
|
242
|
+
const settings = incoming.defaultModel === undefined
|
|
243
|
+
? rawSettings
|
|
244
|
+
: withDefaultModel(rawSettings, incoming.defaultModel);
|
|
245
|
+
const after = [incoming.files["SYSTEM.md"], incoming.files["AGENTS.md"], models, settings];
|
|
237
246
|
const changes = names.flatMap((name, index) => before[index] === after[index] ? [] : [{
|
|
238
247
|
path: join(this.agentDir, name), before: before[index], after: after[index],
|
|
239
248
|
temp: join(this.agentDir, `${name}.${process.pid}.${randomUUID()}.tmp`),
|
|
@@ -450,21 +459,61 @@ function restoreHeaders(incoming, current) {
|
|
|
450
459
|
headers[name] = original;
|
|
451
460
|
}
|
|
452
461
|
}
|
|
453
|
-
|
|
462
|
+
/** settings.json is read for one pair and never rewritten wholesale. Malformed
|
|
463
|
+
* JSON is refused rather than read as "no default": that would publish, or
|
|
464
|
+
* import, a cleared default nobody asked for. */
|
|
465
|
+
function readSettings(raw) {
|
|
466
|
+
if (!raw?.trim())
|
|
467
|
+
return {};
|
|
468
|
+
const parsed = parseObject(raw);
|
|
469
|
+
if (!parsed)
|
|
470
|
+
throw new Error("settings.json must be valid JSON before synchronizing configuration");
|
|
471
|
+
return parsed;
|
|
472
|
+
}
|
|
473
|
+
/** Pi writes defaultProvider and defaultModel as a pair; half of one is a hand
|
|
474
|
+
* edit that a shared default cannot represent, so it is refused. */
|
|
475
|
+
function defaultModelRef(raw) {
|
|
476
|
+
const settings = readSettings(raw);
|
|
477
|
+
const provider = settings.defaultProvider;
|
|
478
|
+
const id = settings.defaultModel;
|
|
479
|
+
if (provider === undefined && id === undefined)
|
|
480
|
+
return null;
|
|
481
|
+
if (typeof provider !== "string" || typeof id !== "string") {
|
|
482
|
+
throw new Error("settings.json must name defaultProvider and defaultModel together as strings");
|
|
483
|
+
}
|
|
484
|
+
return { provider, id };
|
|
485
|
+
}
|
|
486
|
+
/** The incoming default replaces the local pair and nothing else; unchanged
|
|
487
|
+
* content returns the original bytes so the import stays a no-op. */
|
|
488
|
+
function withDefaultModel(raw, model) {
|
|
489
|
+
const settings = readSettings(raw);
|
|
490
|
+
const next = { ...settings };
|
|
491
|
+
if (model) {
|
|
492
|
+
next.defaultProvider = model.provider;
|
|
493
|
+
next.defaultModel = model.id;
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
delete next.defaultProvider;
|
|
497
|
+
delete next.defaultModel;
|
|
498
|
+
}
|
|
499
|
+
return JSON.stringify(next) === JSON.stringify(settings) ? raw : `${JSON.stringify(next, null, 2)}\n`;
|
|
500
|
+
}
|
|
501
|
+
const parseObject = (raw) => {
|
|
454
502
|
try {
|
|
455
|
-
|
|
456
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
457
|
-
return null;
|
|
458
|
-
const providers = parsed.providers;
|
|
459
|
-
if (providers !== undefined) {
|
|
460
|
-
if (typeof providers !== "object" || providers === null || Array.isArray(providers))
|
|
461
|
-
return null;
|
|
462
|
-
if (Object.values(providers).some((provider) => typeof provider !== "object" || provider === null || Array.isArray(provider)))
|
|
463
|
-
return null;
|
|
464
|
-
}
|
|
465
|
-
return parsed;
|
|
503
|
+
return asRecord(JSON.parse(raw)) ?? null;
|
|
466
504
|
}
|
|
467
505
|
catch {
|
|
468
506
|
return null;
|
|
469
507
|
}
|
|
508
|
+
};
|
|
509
|
+
function parseModels(raw) {
|
|
510
|
+
const parsed = parseObject(raw);
|
|
511
|
+
if (!parsed)
|
|
512
|
+
return null;
|
|
513
|
+
if (parsed.providers !== undefined) {
|
|
514
|
+
const providers = asRecord(parsed.providers);
|
|
515
|
+
if (!providers || Object.values(providers).some((provider) => !asRecord(provider)))
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
return parsed;
|
|
470
519
|
}
|
package/dist/core/identity.js
CHANGED
|
@@ -90,6 +90,12 @@ export function splitSpeaker(text) {
|
|
|
90
90
|
text: text.slice(m[0].length),
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
|
+
/** Trailing lines that open a `[name](file://` link — or a last line that is
|
|
94
|
+
* only `[name` because the clip fell inside it. */
|
|
95
|
+
const ATTACHMENT_LINES = /(?:\n\[[^\]\n]*(?:\]\(\s*<?file:\/\/[^\n]*|$))+$/;
|
|
96
|
+
/** A long code span that opens the message, when something else follows it.
|
|
97
|
+
* Long, because `npm test` before "fails" is the subject, not a paste. */
|
|
98
|
+
const LEADING_CODE_SPAN = /^\s*`[^`\n]{40,}`\s+(\S[\s\S]*)$/;
|
|
93
99
|
/**
|
|
94
100
|
* A session titled by its first prompt inherits that prompt's header, and the
|
|
95
101
|
* header is for the model: anything a person reads — a list row, a session
|
|
@@ -97,6 +103,11 @@ export function splitSpeaker(text) {
|
|
|
97
103
|
* the workbench ever opened. So the speaker comes off and what they said is
|
|
98
104
|
* the title. Here rather than in a UI module because the push notification
|
|
99
105
|
* needs the same answer and a second copy of this would drift (AGENTS.md §3).
|
|
106
|
+
*
|
|
107
|
+
* Two more things a first prompt carries that a title should not: the
|
|
108
|
+
* attachment lines a channel appended (`[name](file:///…)`, often cut mid-name
|
|
109
|
+
* by the title clip), and a pasted log line in backticks ahead of the actual
|
|
110
|
+
* question — dropped only when a question follows; a paste alone stays.
|
|
100
111
|
*/
|
|
101
112
|
export function readableTitle(title) {
|
|
102
113
|
if (!title)
|
|
@@ -106,5 +117,10 @@ export function readableTitle(title) {
|
|
|
106
117
|
// change what the sidebar's search is matching against for nothing.
|
|
107
118
|
if (text === title)
|
|
108
119
|
return title;
|
|
109
|
-
|
|
120
|
+
const said = text
|
|
121
|
+
.replace(ATTACHMENT_LINES, "")
|
|
122
|
+
.replace(LEADING_CODE_SPAN, "$1")
|
|
123
|
+
.replace(/\s+/g, " ")
|
|
124
|
+
.trim();
|
|
125
|
+
return said || title;
|
|
110
126
|
}
|
package/dist/db.js
CHANGED
|
@@ -322,6 +322,15 @@ const MIGRATIONS = [
|
|
|
322
322
|
-- all of them: the query is by the delegating session, which lived only in
|
|
323
323
|
-- the JSON — without this every session open was a full scan of task_runs.
|
|
324
324
|
CREATE INDEX task_runs_invoked_by ON task_runs(json_extract(json, '$.invokedBySessionId'), queued_at DESC);
|
|
325
|
+
`,
|
|
326
|
+
// 19 — the rail is one flat list, and pinned means "stuck to the top".
|
|
327
|
+
`
|
|
328
|
+
-- pinned used to mean "listed under Projects", and every session created in
|
|
329
|
+
-- the workbench was. Now it means on top of the list, and nobody put a
|
|
330
|
+
-- historical session there: kept as-is, every web session ever made would
|
|
331
|
+
-- land in the pinned section. project_sort stays as a column nothing reads
|
|
332
|
+
-- — a SQLite column drop rewrites the table for a NULL nobody pays for.
|
|
333
|
+
UPDATE session_state SET pinned = 0;
|
|
325
334
|
`,
|
|
326
335
|
];
|
|
327
336
|
/**
|
package/dist/main.js
CHANGED
|
@@ -382,6 +382,7 @@ app.route("/", createServer({
|
|
|
382
382
|
reload: () => reloadInstance(true),
|
|
383
383
|
backgroundRuns: (id) => tasks.backgroundRuns(id),
|
|
384
384
|
activeBackgroundRunCounts: () => tasks.activeBackgroundRunCounts(),
|
|
385
|
+
taskSessions: () => tasks.taskSessions(),
|
|
385
386
|
channelOf: (id) => conversations.channelOf(id),
|
|
386
387
|
}));
|
|
387
388
|
const port = Number(process.env.PORT ?? 3141);
|
package/dist/settings.js
CHANGED
|
@@ -113,7 +113,10 @@ export class SettingsStore {
|
|
|
113
113
|
autoUpdate: this.#value("autoUpdate") === "1",
|
|
114
114
|
extensions: this.#json("extensions", normalizeExtensions, "a list of names") ?? [],
|
|
115
115
|
tools: this.#json("tools", normalizeTools, "a list of names") ?? [],
|
|
116
|
-
|
|
116
|
+
// `"drop"`: a stored row whose name the bundled catalog has since taken
|
|
117
|
+
// is redundant, not malformed — rejecting the setting over it would take
|
|
118
|
+
// every other tool declared beside it (see normalizeCustomTools).
|
|
119
|
+
customTools: this.#json("customTools", (raw) => normalizeCustomTools(raw, [], "drop"), "a list of {name, spec}") ?? [],
|
|
117
120
|
};
|
|
118
121
|
}
|
|
119
122
|
/**
|
package/dist/tasks/service.js
CHANGED
|
@@ -217,6 +217,10 @@ export class TaskService {
|
|
|
217
217
|
activeBackgroundRunCounts() {
|
|
218
218
|
return this.store.countActiveBackgroundRunsBySession();
|
|
219
219
|
}
|
|
220
|
+
/** The sessions runs made for themselves, for a list of the operator's own. */
|
|
221
|
+
taskSessions() {
|
|
222
|
+
return this.store.taskOwnedSessionIds();
|
|
223
|
+
}
|
|
220
224
|
run(taskId, input = null, source = "manual", parentRunId = null, provenance = {}) {
|
|
221
225
|
this.refusePaused(parentRunId);
|
|
222
226
|
const task = this.get(taskId);
|
package/dist/tasks/store.js
CHANGED
|
@@ -143,6 +143,18 @@ export class TaskStore {
|
|
|
143
143
|
`).all();
|
|
144
144
|
return new Map(rows.map((row) => [row.session_id, row.n]));
|
|
145
145
|
}
|
|
146
|
+
/** Every session a run created for itself — `fresh` is the one mode that
|
|
147
|
+
* makes a session rather than borrowing one, and a resumed run reuses the
|
|
148
|
+
* session its `fresh` predecessor already stamped. These are the agent's
|
|
149
|
+
* conversations with itself, which the rail does not list. */
|
|
150
|
+
taskOwnedSessionIds() {
|
|
151
|
+
const rows = this.sql(`
|
|
152
|
+
SELECT DISTINCT json_extract(json, '$.context.sessionId') AS id
|
|
153
|
+
FROM task_runs
|
|
154
|
+
WHERE json_extract(json, '$.sessionMode') = 'fresh' AND id IS NOT NULL
|
|
155
|
+
`).all();
|
|
156
|
+
return new Set(rows.map((row) => row.id));
|
|
157
|
+
}
|
|
146
158
|
listRunsForSession(sessionId, limit = 50) {
|
|
147
159
|
return this.#many(`
|
|
148
160
|
SELECT json FROM task_runs
|
package/dist/tools.js
CHANGED
|
@@ -127,7 +127,18 @@ export const CUSTOM_TOOL_RULES = `each custom tool needs a name (letters, digits
|
|
|
127
127
|
export function normalizeCustomTools(raw,
|
|
128
128
|
/** Names this instance already answers to that this file cannot see — the
|
|
129
129
|
* bundled extensions live behind the Pi SDK, so main.ts hands them in. */
|
|
130
|
-
reserved = []
|
|
130
|
+
reserved = [],
|
|
131
|
+
/**
|
|
132
|
+
* What a name Pier already manages means. At the route: a refusal, because
|
|
133
|
+
* the operator is declaring it now and can pick another. Reading a stored
|
|
134
|
+
* row: `"drop"`, because the catalog grew into that name *after* the row was
|
|
135
|
+
* written — jq shipped as a bundled tool and turned the operator's own jq
|
|
136
|
+
* row into a whole setting Pier refused, dropping every *other* tool
|
|
137
|
+
* declared beside it. The bundled row installs the same binary, so the
|
|
138
|
+
* entry is redundant rather than wrong. Left in the row on purpose: a Pier
|
|
139
|
+
* that stops bundling that name finds the declaration still there.
|
|
140
|
+
*/
|
|
141
|
+
managedName = "reject") {
|
|
131
142
|
// Case-insensitively: two names differing only in case are one filename on a
|
|
132
143
|
// case-insensitive filesystem, and one switch whose meaning depends on which
|
|
133
144
|
// ran last.
|
|
@@ -145,8 +156,11 @@ reserved = []) {
|
|
|
145
156
|
const cleanName = name.trim();
|
|
146
157
|
if (!TOOL_NAME.test(cleanName))
|
|
147
158
|
return null;
|
|
148
|
-
if (taken.has(cleanName.toLowerCase()))
|
|
149
|
-
|
|
159
|
+
if (taken.has(cleanName.toLowerCase())) {
|
|
160
|
+
if (managedName === "reject")
|
|
161
|
+
return null;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
150
164
|
if (tools.some((tool) => tool.name.toLowerCase() === cleanName.toLowerCase()))
|
|
151
165
|
return null;
|
|
152
166
|
// The migration: a stored `{name, spec}` is the block it always meant.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{A as e,B as t,L as n,P as r,R as i,U as a,V as o,h as s,q as c,w as l,y as u}from"./index-DzWnvhXb.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?`-`:n(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(n,p,h){let g=`sessions`,_=`active`,v={sessions:[],runs:[],messages:[]},y=``,b=o(async()=>{let e=_,t=await a(`/api/activity?scope=${e}`,`Failed to load activity`);if(!t.ok){y=``,n.replaceChildren(i(`p`,`p-4 text-[13px] text-red-600`,t.error));return}if(e!==_)return;let r=JSON.stringify(t.value);if(`${e}:${r}`===y)return;y=`${e}:${r}`;let o=t.value;v={...o,sessions:o.sessions.map(e=>({...e,title:c(e.title)}))},x()});function x(){let t=e(l([[`Sessions`,`sessions`],[`Relationships`,`dependencies`]],g,e=>{g=e,x()}),i(`span`,`ml-auto text-[11.5px] text-neutral-400`,`${v.sessions.length} sessions · ${v.runs.length} runs`),l([[`Current`,`active`],[`Last 24h`,`recent`]],_,e=>{_=e,x(),b()})),r=i(`div`,`min-h-0 flex-1 overflow-auto`);g===`sessions`?S(r):w(r),n.replaceChildren(t,r)}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>
|
|
5
|
-
<th class="px-2 py-2 font-semibold">In state</th></tr></thead>`;let
|
|
5
|
+
<th class="px-2 py-2 font-semibold">In state</th></tr></thead>`;let r=document.createElement(`tbody`);for(let e of v.sessions){let n=document.createElement(`tr`);n.className=`cursor-pointer border-b border-neutral-100 transition-colors hover:bg-neutral-50`,n.onclick=()=>p(e.id);let a=i(`button`,`block w-full cursor-pointer truncate text-left font-medium text-neutral-800`,e.title??t(e.cwd));a.setAttribute(`type`,`button`),a.title=`${e.id}\n${e.cwd}`,a.onclick=t=>{t.stopPropagation(),p(e.id)},n.append(i(`td`,`px-4 py-2.5`,a,i(`div`,`truncate font-mono text-[11px] text-neutral-400 md:hidden`,e.cwd||`-`)),i(`td`,`hidden truncate px-2 py-2.5 font-mono text-[11.5px] text-neutral-500 md:table-cell`,e.cwd||`-`),i(`td`,`px-2 py-2.5`,C(e.state)),i(`td`,`px-2 py-2.5 font-mono text-[11.5px] text-neutral-400`,f(e.stateSince))),r.append(n)}n.append(r),e.append(n),v.sessions.length||e.append(i(`div`,`p-4`,u(_===`active`?`No active sessions.`:`No sessions in the last 24 hours.`)))}function C(e){return e===`streaming`?s(e,`bg-emerald-50 text-emerald-700 ring-emerald-200`,`animate-pulse bg-emerald-500`):s(e,`bg-neutral-100 text-neutral-600 ring-neutral-200`,`bg-neutral-300`)}function w(e){let t=new Map,n=[],r=(e,n,r,i)=>{t.has(e)||t.set(e,{id:e,label:n,kind:r,state:i})};for(let e of v.sessions)r(e.id,e.title??e.id.slice(0,10),`session`,e.state);for(let e of v.runs){let t=e.invokedBySessionId??`scheduler`,i=e.targetSessionId??(e.context.definition.action.type===`bash`?`process:${e.id}`:`task:${e.id}`);r(t,t===`scheduler`?`Scheduler`:t.slice(0,10),t===`scheduler`?`scheduler`:`session`),r(i,e.targetSessionId?e.targetSessionId.slice(0,10):e.context.definition.action.type===`bash`?`Bash process`:`Task`,e.targetSessionId?`session`:e.context.definition.action.type===`bash`?`process`:`task`),n.push({from:t,to:i,run:e,kind:`invocation`}),e.callbackSessionId&&(r(e.callbackSessionId,e.callbackSessionId.slice(0,10),`session`),n.push({from:i,to:e.callbackSessionId,run:e,kind:`callback`}))}for(let e of v.messages){if(!e.fromSessionId||!e.toSessionId||e.fromSessionId===e.toSessionId)continue;let t=v.runs.find(t=>t.id===e.runId);if(t){for(let t of[e.fromSessionId,e.toSessionId])r(t,t===`console`?`Console`:t.slice(0,10),t===`console`?`console`:`session`);n.push({from:e.fromSessionId,to:e.toSessionId,run:t,kind:`message`})}}if(!t.size){e.append(i(`div`,`p-4`,u(_===`active`?`No active dependencies.`:`No dependencies in the last 24 hours.`)));return}let a=[...t.values()],o=new Set(n.map(e=>e.to)),s=new Map,c=a.filter(e=>!o.has(e.id)).map(e=>e.id);c.length||c.push(a[0].id);for(let e of c)s.set(e,0);for(;c.length;){let e=c.shift();for(let t of n)t.from!==e||s.has(t.to)||(s.set(t.to,s.get(e)+1),c.push(t.to))}for(let e of a)s.has(e.id)||s.set(e.id,0);let l=new Map;for(let e of a){let t=s.get(e.id);l.set(t,[...l.get(t)??[],e])}let f=Math.max(...l.keys())+1,g=Math.max(...[...l.values()].map(e=>e.length)),y=n.some(e=>e.from!==e.to&&s.get(e.from)===s.get(e.to))?40:0,b=56+f*180+(f-1)*110+y,x=56+g*46+(g-1)*30,S=new Map;for(let[e,t]of l)t.forEach((t,n)=>S.set(t.id,{x:28+e*290+90,y:28+n*76+23}));let C=d(`svg`,{width:String(b),height:String(x),viewBox:`0 0 ${b} ${x}`,class:`block`}),w=d(`defs`),T=m();for(let[e,t]of[[`activity-arrow`,T.edge],[`activity-arrow-cb`,T.callback],[`activity-arrow-msg`,T.message]]){let n=d(`marker`,{id:e,viewBox:`0 0 10 10`,refX:`9`,refY:`5`,markerWidth:`6`,markerHeight:`6`,orient:`auto-start-reverse`});n.append(d(`path`,{d:`M 0 0 L 10 5 L 0 10 z`,fill:t})),w.append(n)}C.append(w);let E=new Set;for(let e of n){let t=`${e.from}→${e.to}:${e.kind}`;if(E.has(t))continue;E.add(t);let n=S.get(e.from),r=S.get(e.to),i=d(`path`,{d:n.x===r.x?`M ${n.x+90} ${n.y} C ${n.x+90+46} ${n.y}, ${r.x+90+46} ${r.y}, ${r.x+90} ${r.y}`:(()=>{let e=r.x>n.x,t=n.x+(e?90:-90),i=r.x+(e?-90:90),a=Math.max(36,Math.abs(i-t)*.45)*(e?1:-1);return`M ${t} ${n.y} C ${t+a} ${n.y}, ${i-a} ${r.y}, ${i} ${r.y}`})(),fill:`none`,stroke:e.kind===`callback`?T.callback:e.kind===`message`?T.message:T.edge,"stroke-width":`1.5`,"marker-end":e.kind===`callback`?`url(#activity-arrow-cb)`:e.kind===`message`?`url(#activity-arrow-msg)`:`url(#activity-arrow)`});e.kind===`callback`&&i.setAttribute(`stroke-dasharray`,`6 5`),e.kind===`message`&&i.setAttribute(`stroke-dasharray`,`2 5`),i.classList.add(`cursor-pointer`),i.onclick=()=>h(e.run.id),C.append(i)}let D=/[\u1100-\u11FF\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6]/,O=(e,t)=>{let n=0;for(let r=0;r<e.length;r++)if(n+=D.test(e[r])?2:1,n>t)return`${e.slice(0,r)}…`;return e};for(let e of a){let t=S.get(e.id),n=e.kind===`session`?e.state===`streaming`?T.streaming:T.idle:T[e.kind],r=d(`g`,{transform:`translate(${t.x},${t.y})`});e.kind===`session`&&r.classList.add(`cursor-pointer`),r.onclick=()=>{e.kind===`session`&&p(e.id)};let i=d(`rect`,{x:`-90`,y:`-23`,width:`180`,height:`46`,rx:`8`,fill:n.fill,stroke:n.stroke,"stroke-width":e.kind===`session`&&e.state===`streaming`?`1.5`:`1`});n.dash&&i.setAttribute(`stroke-dasharray`,n.dash),r.append(i);let a=-78;if(e.kind===`session`){let t=d(`circle`,{cx:`-75`,cy:`0`,r:`3.5`,fill:n.stroke});if(e.state===`streaming`){let e=d(`animate`,{attributeName:`opacity`,values:`1;0.3;1`,dur:`1.6s`,repeatCount:`indefinite`});t.append(e)}r.append(t),a=-64}let o=d(`text`,{x:String(a),"text-anchor":`start`,"dominant-baseline":`middle`,"font-size":`11.5`,"font-weight":e.kind===`session`?`500`:`400`,fill:n.text});o.textContent=O(e.label,e.kind===`session`?22:25);let s=d(`title`);s.textContent=e.label===e.id?e.id:`${e.label}\n${e.id}`,r.append(o,s),C.append(r)}let k=e=>i(`span`,`inline-block h-2 w-2 rounded-full ${e}`),A=e=>i(`span`,`inline-block h-0 w-6 border-t-2 ${e}`);e.append(i(`div`,`w-max p-4`,C),i(`div`,`flex flex-wrap gap-x-5 gap-y-1.5 border-t border-neutral-200 bg-neutral-50/60 px-4 py-2 text-[11px] text-neutral-500`,i(`span`,`inline-flex items-center gap-2`,A(`border-solid border-neutral-400`),`task invocation`),i(`span`,`inline-flex items-center gap-2 text-cyan-700`,A(`border-dashed border-cyan-500`),`callback`),i(`span`,`inline-flex items-center gap-2 text-amber-700`,A(`border-dotted border-amber-500`),`supervisor / control`),i(`span`,`inline-flex items-center gap-1.5`,k(`bg-emerald-500`),`streaming`),i(`span`,`inline-flex items-center gap-1.5`,k(`bg-neutral-300`),`idle`)))}let T=r(n,e=>{e===`dependencies`?g=`dependencies`:e===`sessions`&&(g=`sessions`),b()},()=>{n.replaceChildren(),v={sessions:[],runs:[],messages:[]},y=``});return window.addEventListener(`pier:theme`,()=>{T.visible&&x()}),Object.assign(T,{refresh(){T.visible&&b()}})}export{h as createActivityView};
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{F as e,G as t,H as n,K as r,P as i,R as a,U as o,g as s,k as c,z as l}from"./index-DzWnvhXb.js";var u=e=>e.public&&e.token?`/p/${e.slug}-${e.token}/`:`/boards/${e.slug}/`;function d(d,f){let p=[],m=``,h=a(`header`,`sticky top-0 z-30 flex h-10 items-center gap-3 border-b border-neutral-200 bg-white px-4`,a(`span`,`font-medium max-md:hidden`,`Boards`)),g=a(`div`,`px-4 py-5`);d.append(a(`div`,`min-h-0 flex-1 overflow-y-auto`,h,g));async function _(e,t){let i=await r(`/api/boards/${e.slug}`,{public:t},`PATCH`);if(i.ok)return e.token=(await i.json().catch(()=>({}))).token??``,x();m=await n(i,`Could not change ${e.slug}`),await S()}async function v(e){m=await t(`/api/boards/${e}`,`DELETE`,`Could not delete ${e}`)??``,await S()}function y(t){let n=a(`div`,`group flex flex-col gap-1 border-b border-neutral-200/70 px-1 py-2.5 last:border-b-0`),r=document.createElement(`a`);r.className=`truncate font-medium text-neutral-800 hover:text-indigo-700`,r.href=u(t),r.target=`_blank`,r.rel=`noreferrer`,r.textContent=t.title;let i=a(`div`,`flex items-center gap-2`,r,a(`span`,`flex-none font-mono text-[11.5px] text-neutral-400`,t.slug)),o=c(``,``,t.public,e=>{t.public=e,_(t,e)});o.className=`ml-auto flex flex-none cursor-pointer items-center gap-1.5 text-[11.5px] text-neutral-500`,o.append(a(`span`,``,`Public`));let d=s(`Delete`,`flex-none cursor-pointer text-[11.5px] text-neutral-400 opacity-0 transition-opacity hover:text-red-600 group-hover:opacity-100`);d.title=`Renames the folder on disk; nothing is erased`,d.onclick=()=>void v(t.slug);let p=e(`flex-none cursor-pointer text-[11.5px] text-neutral-400 opacity-0 transition-opacity hover:text-indigo-600 group-hover:opacity-100`,()=>`${location.origin}${u(t)}`);p.title=`Copy the board's link`,i.append(o,p,d),n.append(i);let m=a(`div`,`flex flex-wrap items-center gap-x-3 gap-y-1 text-[11.5px] text-neutral-500`);t.description&&m.append(a(`span`,`min-w-0 truncate`,t.description)),m.append(a(`span`,`flex-none text-neutral-400`,l(Date.parse(t.updatedAt))));for(let e of t.sessions){let t=s(e.slice(0,8),`flex-none cursor-pointer font-mono text-indigo-600 hover:underline`);t.title=`Open session ${e}`,t.onclick=()=>f(e),m.append(t)}if(t.public&&t.token){let e=a(`span`,`flex-none font-mono text-neutral-400`,u(t));e.title=`Public URL — anyone holding this link can read the board, no password`,m.append(e)}return n.append(m),n}let b=[{title:`Public`,hint:`Readable at /p/<slug>-<token>/ by anyone holding the link.`,wanted:!0},{title:`Private`,hint:`Reachable from the Console only.`,wanted:!1}];function x(){let e=a(`div`,`mx-auto flex max-w-3xl flex-col`);p.length===0&&e.append(a(`p`,`rounded-xl border border-dashed border-neutral-300 px-4 py-6 text-center text-[12.5px] text-neutral-500`,`No boards yet. Ask an agent to build one — it writes a folder under ~/.pier/boards.`));for(let{title:t,hint:n,wanted:r}of b){let i=p.filter(e=>e.public===r);if(i.length===0)continue;let o=a(`section`,`mb-6`);o.append(a(`h2`,`text-[13px] font-semibold text-neutral-700`,t)),o.append(a(`p`,`mb-1 text-[11.5px] leading-snug text-neutral-500`,n));let s=a(`div`,`rounded-xl border border-neutral-200 bg-white px-3`);s.append(...i.map(y)),o.append(s),e.append(o)}g.replaceChildren(...m?[a(`p`,`mb-3 text-[13px] text-red-600`,m)]:[],e)}async function S(){let e=await o(`/api/boards`,`Could not load boards`);if(!e.ok)return m=e.error,x();p=e.value,x(),m=``}let C=i(d,()=>void S());return window.addEventListener(`focus`,()=>{C.visible&&S()}),C}export{d as createBoardsView};
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{I as e,N as t,P as n,R as r,W as i,a,d as o,f as s,i as c,n as l,s as u,u as d,z as f}from"./index-DzWnvhXb.js";var p=e=>{let t=new Date(e),n=e=>String(e).padStart(2,`0`);return`${t.getFullYear()}-${n(t.getMonth()+1)}-${n(t.getDate())} ${n(t.getHours())}:${n(t.getMinutes())}:${n(t.getSeconds())}`},m=e=>[e.subject,e.body,[[e.author,e.email?`<${e.email}>`:``].filter(Boolean).join(` `),e.at?p(e.at):``].filter(Boolean).join(` · `)].filter(Boolean).join(`
|
|
2
|
+
|
|
3
|
+
`),h=null;function g(){h&&(h.remove(),h=null,document.removeEventListener(`pointerdown`,g,!0),window.removeEventListener(`scroll`,g,!0))}function ee(e,t){e.onmouseenter=()=>{g();let n=t();if(!n)return;h=r(`div`,`pointer-events-none fixed z-50 max-w-96 whitespace-pre-wrap rounded-lg bg-neutral-800/95 px-3 py-2 font-sans text-[11.5px] leading-snug text-neutral-100 shadow-lg`),h.textContent=n,document.body.append(h),document.addEventListener(`pointerdown`,g,!0),window.addEventListener(`scroll`,g,!0);let i=e.getBoundingClientRect();h.style.left=`${Math.max(8,Math.min(i.left,window.innerWidth-h.offsetWidth-8))}px`,h.style.top=i.bottom+6+h.offsetHeight>window.innerHeight?`${i.top-h.offsetHeight-6}px`:`${i.bottom+6}px`},e.onmouseleave=g}function _(e,t,n){let i=r(`button`,`flex w-full cursor-pointer items-center gap-1.5 px-2 py-1 text-left hover:bg-neutral-100`,r(`span`,`w-3 flex-none text-indigo-600`,e.value===t?`✓`:``),r(`span`,`flex-none font-mono text-[11.5px] text-neutral-800`,e.label??e.value));return e.subject&&i.append(r(`span`,`min-w-0 truncate text-[11px] text-neutral-400`,e.subject)),e.author&&i.append(r(`span`,`ml-auto flex-none pl-1.5 text-[10.5px] text-neutral-400`,`${e.author} · ${e.at?f(e.at):``}`)),ee(i,()=>m(e)),i.onclick=()=>n(e.value),i}function te(e,t,n,i,o){let{base:s,head:l}=n,u=()=>l&&s===`${l}~1`?l:``,d=u()?`commit`:`compare`,f=t.commits[0],p=e=>[...e?[{value:``,label:`Working tree`,subject:`the files on disk, uncommitted changes included`}]:[],{...f,value:`HEAD`},...t.refs.map(e=>({value:e.name,subject:e.subject})),...t.commits.map(e=>({...e,value:e.hash}))],m=r(`div`,`flex w-[38rem] max-w-full flex-col`);function h(){let e=(e,t)=>{let n=r(`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 n.onclick=()=>{d=t,h()},n},n=(e,t,n,i)=>r(`div`,`flex min-w-0 flex-1 flex-col`,r(`div`,`flex-none px-2 pb-0.5 text-[10.5px] font-semibold uppercase tracking-wide text-neutral-400`,e),r(`div`,`min-h-0 flex-1 overflow-y-auto`,...t.map(e=>_(e,n,i)))),a=d===`compare`?r(`div`,`flex h-72 min-h-0 divide-x divide-neutral-200 pb-1`,n(`base`,p(!1),s,e=>{s=e,h(),i(s,l)}),n(`to`,p(!0),l,e=>{l=e,h(),i(s,l)})):r(`div`,`flex h-72 min-h-0 flex-col overflow-y-auto pb-1`,...t.commits.map(e=>_({value:e.hash,...e},u(),e=>{s=`${e}~1`,l=e,c(),i(s,l)}))),f=r(`div`,`flex-none border-t border-neutral-200`,_({value:o,label:`↺ ${o} ↔ Working tree`,subject:`back to the default: the main line's tip vs the files on disk`},s===o&&l===``?o:``,()=>{s=o,l=``,h(),i(s,l)}));m.replaceChildren(r(`div`,`flex flex-none items-center gap-1 border-b border-neutral-200 px-2 py-1.5`,e(`Compare two points`,`compare`),e(`One commit`,`commit`)),a,...d===`compare`?[f]:[])}h(),a(e,m)}var ne=/\.(png|jpe?g|gif|webp|avif|bmp|svg)$/i,v={A:`text-emerald-600`,M:`text-amber-600`,D:`text-red-600`},re=30,y=`pier.filesPrefs`,b=50,x=()=>{try{let e=JSON.parse(localStorage.getItem(y)??`{}`);return e&&typeof e==`object`?e:{}}catch{return{}}},S=e=>{let t=e?x()[e]:void 0;return typeof t?.cwd==`string`&&typeof t.base==`string`&&typeof t.head==`string`?{cwd:t.cwd,base:t.base,head:t.head}:null},ie=(e,t)=>{let n=x();delete n[e],n[e]=t;let r=Object.keys(n);for(let e of r.slice(0,Math.max(0,r.length-b)))delete n[e];try{localStorage.setItem(y,JSON.stringify(n))}catch(e){console.warn(`Files: could not remember this folder`,e)}},ae=(e,t=`indigo`)=>{let n=r(`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 n.type=`button`,n};function C(a,c,f,p){let h=``,g=``,_={branch:null,refs:[],commits:[],worktrees:[]},y=`HEAD`,b=``,x=new Map,C=!0,w=!1,T=[],E=new Set,D=null,O=null,k=r(`header`,`flex h-10 flex-none items-center gap-2 border-b border-neutral-200 bg-white px-3`),A=r(`div`,`flex-none border-b border-neutral-200`),j=r(`div`,`min-h-0 flex-1 overflow-y-auto py-1`),oe=r(`aside`,`flex w-64 flex-none flex-col border-r border-neutral-200 text-[12.5px] max-md:h-2/5 max-md:w-full max-md:border-b max-md:border-r-0`,A,j),M=r(`div`,`h-full min-w-0 overflow-auto`),N=r(`div`,`pointer-events-none absolute inset-y-0 right-0 z-20 hidden w-2`),se=r(`section`,`relative min-h-0 min-w-0 flex-1`,M,N);a.append(k,r(`div`,`flex min-h-0 flex-1 max-md:flex-col`,oe,se));let P=(e,t)=>`/api/explorer/${e}?${new URLSearchParams({root:h,...t})}`,F=(e,t)=>`/api/fs/${e}?${new URLSearchParams({root:h,...t})}`,I=(e,t=`text-neutral-400`)=>r(`p`,`px-4 py-3 text-[12.5px] ${t}`,e),L=e=>{let t=0;for(let n of x.keys())n.startsWith(`${e}/`)&&t++;return t};function R(e,t){let n=new Set(t.map(e=>e.name)),r=e?`${e}/`:``;for(let[e,i]of x){if(i.status!==`D`||!e.startsWith(r))continue;let a=e.slice(r.length).split(`/`)[0];n.has(a)||(n.add(a),t.push({name:a,dir:e.slice(r.length).includes(`/`)}))}let i=t;return C&&_.branch&&(i=t.filter(e=>e.dir?L(`${r}${e.name}`)>0:x.has(`${r}${e.name}`))),i.sort((e,t)=>Number(t.dir)-Number(e.dir)||e.name.localeCompare(t.name)),i.map(e=>e.dir?ce(`${r}${e.name}`,e.name):le(`${r}${e.name}`,e.name))}async function z(e,t){try{let{entries:n}=await i(F(`ls`,{path:t}),`could not list this folder`);e.replaceChildren(...R(t,n)),e.childElementCount||e.append(I(C?`No changes.`:`Empty.`))}catch(n){let r=R(t,[]);r.length?e.replaceChildren(...r):e.replaceChildren(I(String(n),`text-red-600`))}}function ce(t,n){let i=L(t),a=[r(`span`,`truncate`,n)];i&&a.push(r(`span`,`ml-auto flex-none rounded bg-amber-100/80 px-1 font-mono text-[10px] text-amber-700`,String(i)));let{el:o,summary:s}=e(``,a);s.className+=` px-2 py-0.5 hover:bg-neutral-100`;let c=r(`div`,`pl-3`);o.append(c);let l=!1,u=()=>{l||(l=!0,c.append(I(`…`)),z(c,t))};return o.ontoggle=()=>{E[o.open?`add`:`delete`](t),o.open&&u()},(C&&_.branch&&!w||E.has(t))&&(o.open=!0,u()),o}function B(e){O?.classList.remove(`bg-indigo-50`,`text-indigo-700`),O=e,e.classList.add(`bg-indigo-50`,`text-indigo-700`)}function le(e,t){let n=x.get(e)?.status,i=r(`button`,`flex w-full cursor-pointer items-center gap-1.5 px-2 py-0.5 pl-6 text-left hover:bg-neutral-100`,r(`span`,`truncate`,t));return n&&i.append(r(`span`,`ml-auto flex-none font-mono text-[10.5px] font-semibold ${v[n]??`text-neutral-500`}`,n)),i.title=e,e===D&&B(i),i.onclick=()=>{B(i),D=e,de(e)},i}let V=()=>void z(j,``),H=(e,t)=>[...e?[r(`span`,`flex-none font-mono text-[10.5px] font-semibold text-emerald-600`,`+${e}`)]:[],...t?[r(`span`,`flex-none font-mono text-[10.5px] font-semibold text-red-600`,`−${t}`)]:[]];function U(e,n,i){let a=x.get(e),o=r(`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`,r(`span`,`truncate`,e));a&&(o.append(r(`span`,`flex-none font-semibold ${v[a.status]??``}`,a.status)),o.append(...H(a.add,a.del)));let s=r(`div`,`ml-auto flex flex-none items-center gap-2`);if(i&&s.append(i),n){let n=document.createElement(`a`);n.className=`flex-none text-neutral-400 hover:text-indigo-700 hover:underline`,n.href=F(`file`,{path:e}),n.download=t(e),n.textContent=`Download`,s.append(n)}return o.append(s),o}function ue(e){let t=0;for(;t<e.length;){if(e[t].tone!==`del`){t++;continue}let n=t;for(;t<e.length&&e[t].tone===`del`;)t++;let r=t;for(;t<e.length&&e[t].tone===`add`;)t++;let i=Math.min(r-n,t-r);for(let t=0;t<i;t++){let i=e[n+t],a=e[r+t],o=Math.min(i.text.length,a.text.length),s=0;for(;s<o&&i.text[s]===a.text[s];)s++;let c=0;for(;c<o-s&&i.text[i.text.length-1-c]===a.text[a.text.length-1-c];)c++;s+c!==0&&(i.mark=[s,i.text.length-c],a.mark=[s,a.text.length-c])}}}let W=ve();function G(){N.classList.add(`hidden`),N.replaceChildren(),W.set([],[])}let K=0,q=e=>e===K;function de(e){let t=++K;return x.has(e)?ye(e,t):fe(e,t)}async function fe(e,t){if(!q(t))return;G();let n=F(`file`,{path:e}),i=r(`div`,`min-w-0`);if(M.classList.remove(`flex`,`flex-col`),M.replaceChildren(U(e,!0),i),ne.test(e)){let e=r(`img`,`max-w-full p-4`);e.src=n,e.onerror=()=>i.replaceChildren(I(`Could not load image.`,`text-red-600`)),i.append(e);return}if(e.toLowerCase().endsWith(`.pdf`)){let t=document.createElement(`iframe`);t.className=`h-full w-full`,t.src=n,M.replaceChildren(U(e,!0),t),M.classList.add(`flex`,`flex-col`);return}i.append(I(`…`));let a=await fetch(n);if(!q(t))return;if(!a.ok){let{error:e}=await a.json().catch(()=>({}));i.replaceChildren(I(e??`HTTP ${a.status}`,`text-red-600`));return}if(!(a.headers.get(`content-type`)??``).startsWith(`text/plain`)){i.replaceChildren(I(`Binary file — use Download.`));return}let c=o(await a.text());if(!q(t))return;let l=await s(e);q(t)&&i.replaceChildren(d(c,l))}function pe(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 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 he={add:`bg-emerald-400`,del:`bg-red-400`,mixed:`bg-amber-400`};function ge(e,t,n){N.replaceChildren(...e.map(e=>{let i=r(`button`,`pointer-events-auto absolute w-full cursor-pointer ${he[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 ${_e(n,e.start)}`,i.onclick=()=>n[e.start].scrollIntoView({block:`center`}),i})),N.classList.remove(`hidden`)}let _e=(e,t)=>e[t]?.querySelector(`span`)?.textContent?.trim()||String(t+1);function ve(){let e=[],t=[],n=-1,i=r(`span`,`flex-none text-[10.5px] text-neutral-400`),o=r=>{e.length&&(n=(n+r+e.length)%e.length,t[e[n].start].scrollIntoView({block:`center`}),i.textContent=`${n+1}/${e.length}`)},s=()=>e.length>0&&!a.classList.contains(`hidden`),c=(e,t,n,i)=>{let a=r(`button`,`icon-btn flex-none`,e);return a.onclick=()=>o(t),u(a,i,n,()=>o(t),s),a};return{el:r(`div`,`flex flex-none items-center gap-1`,i,c(`↑`,-1,`Previous change`,[`p`,`k`]),c(`↓`,1,`Next change`,[`n`,`j`])),set(r,a){e=r,t=a,n=-1,i.textContent=`${r.length} change${r.length===1?``:`s`}`}}}async function ye(e,t){G(),M.classList.remove(`flex`,`flex-col`);let n=x.get(e)?.status!==`D`;M.replaceChildren(U(e,n),I(`…`));try{let{diff:r}=await i(P(`diff`,{base:y,head:b,file:e,context:`99999`}),`could not read this diff`);if(!q(t))return;if(!r||/^Binary files /m.test(r))return fe(e,t);let a=pe(r);ue(a);let o=await s(e);if(!q(t))return;let c=d(a,o),l=[...c.children],u=me(a);W.set(u,l),M.replaceChildren(U(e,n,W.el),c),ge(u,a.length,l)}catch(n){q(t)&&M.replaceChildren(U(e,!1),I(String(n),`text-red-600`))}}let J=()=>b&&y===`${b}~1`?b:``,be=()=>_.commits.find(e=>e.hash===J())?.subject??``,xe=()=>J()?`${J()} — ${be()||`commit`}`:`${y} ↔ ${b||`working tree`}`,Se=(e,t,n,i)=>{let a=r(`button`,`flex h-6 w-6 flex-none cursor-pointer items-center justify-center rounded-md ${n?`bg-indigo-50 text-indigo-700`:`text-neutral-400 hover:bg-neutral-100 hover:text-neutral-700`}`);return a.innerHTML=e,a.title=t,a.onclick=i,a};function Y(){let e=Se(`<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" class="h-3.5 w-3.5"><path d="M2.5 3h11l-4.25 5v4.5l-2.5 1.25V8L2.5 3z" stroke-linejoin="round"/></svg>`,`Show only files that differ`,C,()=>{C=!C,Y(),V()}),t=Se(`<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" class="h-3.5 w-3.5"><path d="m4.5 2.5 3.5 3 3.5-3M4.5 13.5l3.5-3 3.5 3" stroke-linecap="round" stroke-linejoin="round"/></svg>`,w?`Restore folders`:`Collapse all folders`,w,()=>{if(w=!w,w)T=[...E],E.clear();else for(let e of T)E.add(e);Y(),V()});if(!_.branch){A.replaceChildren(r(`div`,`flex items-center justify-end gap-1 p-1.5`,t));return}let n=ae(xe());if(J()){let e=_.commits.find(e=>e.hash===J());ee(n,()=>e?m(e):``)}else n.title=`Choose what to diff`;n.onclick=async()=>{await Te(),te(n,_,{base:y,head:b},(e,t)=>{y=e,b=t,Z()},$())};let[i,a]=[0,0];for(let e of x.values())i+=e.add,a+=e.del;A.replaceChildren(r(`div`,`flex flex-col gap-1 p-2`,n,r(`div`,`flex items-center gap-1.5 pl-1`,r(`span`,`text-[11px] text-neutral-400`,`${x.size} changed file${x.size===1?``:`s`}`),...H(i,a),r(`span`,`ml-auto`),e,t)))}let Ce=()=>{g&&h&&ie(g,{cwd:h,base:y,head:b})},X=e=>{let t=e.replace(/~\d+$/,``);return!t||t===`HEAD`||_.refs.some(e=>e.name===t)||_.commits.some(e=>e.hash===t)};async function we(){if(x=new Map,!_.branch)return;let{files:e}=await i(P(`diff`,{base:y,head:b}),`could not read what changed`);for(let t of e)x.set(t.path,{status:t.status,add:t.add,del:t.del});if(x.size<=re)for(let e of x.keys()){let t=e.split(`/`);for(let e=1;e<t.length;e++)E.add(t.slice(0,e).join(`/`))}}async function Z(){Ce();try{await we()}catch(e){Y(),j.replaceChildren(I(String(e),`text-red-600`));return}Y(),V(),D&&de(D)}function Q(){let e=ae(h||`Choose a folder…`,`neutral`);e.className+=` max-w-72`,e.title=`Switch folder`,e.onclick=()=>l(e,_.worktrees.map(e=>({path:e.path,...e.branch?{hint:e.branch}:{}})),h||void 0,f);let t=r(`button`,`icon-btn`,`✕`);t.type=`button`,t.title=`Close Files`,t.setAttribute(`aria-label`,`Close Files`),t.onclick=p,t.classList.add(`ml-auto`),k.replaceChildren(e,r(`span`,`truncate font-mono text-[11.5px] text-neutral-400`,_.branch?`⎇ ${_.branch}`:`no git`),t)}async function Te(){if(h)try{_=await i(P(`git`,{}),`could not read this repository`),Q()}catch(e){M.replaceChildren(I(String(e),`text-red-600`))}}let $=()=>_.refs.some(e=>e.name===`main`)?`main`:_.refs.some(e=>e.name===`master`)?`master`:`HEAD`;async function Ee(){if(Q(),D=null,O=null,E.clear(),G(),M.classList.remove(`flex`,`flex-col`),M.replaceChildren(I(`Select a file.`)),!h){A.replaceChildren(),j.replaceChildren(),M.replaceChildren(I(`No folder yet — pick one from the chip above.`));return}try{_=await i(P(`git`,{}),`could not read this repository`)}catch(e){_={branch:null,refs:[],commits:[],worktrees:[]},M.replaceChildren(I(String(e),`text-red-600`))}let e=S(g),t=e&&e.cwd===h&&X(e.base)&&X(e.head)?e:null;y=t?.base??$(),b=t?.head??``,Q(),await Z()}return n(a,e=>{let t=c(),n=t?.id??``,r=e?.startsWith(`/`)?e:(n===g?h:``)||S(n)?.cwd||t?.cwd||``;if(r===h&&n===g&&j.childElementCount){Q(),Te().then(Z);return}g=n,h=r,Ee()},()=>{G(),M.classList.remove(`flex`,`flex-col`),M.replaceChildren(I(`Select a file.`))})}export{C as createExplorerView};
|
|
Binary file
|
|
Binary file
|