@timqi/pier 0.1.14 → 0.3.0
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 +44 -22
- package/dist/agent/config.js +2 -2
- package/dist/agent/events.js +32 -3
- package/dist/agent/listing.js +14 -10
- package/dist/agent/packages.js +6 -10
- package/dist/agent/pi.js +113 -70
- package/dist/agent/roles.js +104 -0
- package/dist/agent/types.js +78 -0
- package/dist/boards/boards.js +42 -32
- package/dist/channels/chains.js +0 -4
- package/dist/channels/commands.js +7 -12
- package/dist/channels/config.js +27 -1
- package/dist/channels/control.js +5 -16
- package/dist/channels/conversations.js +4 -9
- package/dist/channels/lark-api.js +20 -26
- package/dist/channels/lark-outbound.js +21 -29
- package/dist/channels/lark-panel.js +1 -1
- package/dist/channels/lark.js +81 -39
- package/dist/channels/lines.js +33 -1
- package/dist/channels/panel.js +17 -107
- package/dist/channels/routes.js +11 -21
- package/dist/channels/runtime.js +79 -25
- package/dist/channels/slack-outbound.js +12 -14
- package/dist/channels/slack-panel.js +1 -1
- package/dist/channels/slack-render.js +1 -1
- package/dist/channels/slack-thread.js +65 -0
- package/dist/channels/slack.js +85 -124
- package/dist/channels/types.js +1 -1
- package/dist/cli.js +7 -2
- package/dist/config-sync.js +42 -37
- package/dist/core/chain.js +221 -0
- package/dist/core/identity.js +36 -13
- package/dist/core/inbound-file.js +11 -4
- package/dist/core/reply.js +111 -81
- package/dist/core/router.js +111 -118
- package/dist/core/search.js +65 -0
- package/dist/core/types.js +38 -77
- package/dist/db.js +41 -2
- package/dist/drain.js +1 -1
- package/dist/main.js +65 -40
- package/dist/settings.js +26 -19
- package/dist/socket.js +1 -0
- package/dist/tasks/agent.js +11 -5
- package/dist/tasks/callbacks.js +55 -5
- package/dist/tasks/cli.js +22 -11
- package/dist/tasks/definitions.js +30 -17
- package/dist/tasks/execution.js +11 -5
- package/dist/tasks/groups.js +5 -2
- package/dist/tasks/messages.js +7 -7
- package/dist/tasks/open-items.js +88 -0
- package/dist/tasks/operations.js +76 -39
- package/dist/tasks/outbox.js +19 -6
- package/dist/tasks/routes.js +19 -249
- package/dist/tasks/service.js +166 -34
- package/dist/tasks/store.js +123 -57
- package/dist/tasks/types.js +3 -0
- package/dist/tools.js +12 -5
- package/dist/web/auth.js +2 -2
- package/dist/web/config-sync.js +4 -5
- package/dist/web/config.js +9 -2
- package/dist/web/explorer.js +2 -2
- package/dist/web/fs.js +16 -1
- package/dist/web/instance.js +8 -5
- package/dist/web/packages.js +1 -1
- package/dist/web/passkeys.js +3 -3
- package/dist/web/providers.js +3 -3
- package/dist/web/public/assets/code-CWT27KH1.js +2 -0
- package/dist/web/public/assets/code-CWT27KH1.js.br +0 -0
- package/dist/web/public/assets/code-CWT27KH1.js.gz +0 -0
- package/dist/web/public/assets/explorer-C4DhXFCm.js +5 -0
- package/dist/web/public/assets/explorer-C4DhXFCm.js.br +0 -0
- package/dist/web/public/assets/explorer-C4DhXFCm.js.gz +0 -0
- package/dist/web/public/assets/highlight-CDSk2hRJ.js +72 -0
- package/dist/web/public/assets/highlight-CDSk2hRJ.js.br +0 -0
- package/dist/web/public/assets/highlight-CDSk2hRJ.js.gz +0 -0
- package/dist/web/public/assets/{hljs-tWqyD59G.js → hljs-BRz6a8Dq.js} +2 -2
- package/dist/web/public/assets/hljs-BRz6a8Dq.js.br +0 -0
- package/dist/web/public/assets/hljs-BRz6a8Dq.js.gz +0 -0
- package/dist/web/public/assets/index-D0ZYdweB.js +14 -0
- package/dist/web/public/assets/index-D0ZYdweB.js.br +0 -0
- package/dist/web/public/assets/index-D0ZYdweB.js.gz +0 -0
- package/dist/web/public/assets/index-JN0Ic8K5.css +2 -0
- package/dist/web/public/assets/index-JN0Ic8K5.css.br +0 -0
- package/dist/web/public/assets/index-JN0Ic8K5.css.gz +0 -0
- package/dist/web/public/assets/settings-DSTeuIyc.js +9 -0
- package/dist/web/public/assets/settings-DSTeuIyc.js.br +0 -0
- package/dist/web/public/assets/settings-DSTeuIyc.js.gz +0 -0
- package/dist/web/public/index.html +39 -109
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/manifest.webmanifest +0 -2
- package/dist/web/push.js +3 -3
- package/dist/web/server.js +89 -116
- package/dist/web/session-state.js +4 -40
- package/docs/deploy.md +5 -3
- package/package.json +2 -1
- package/skills/pier-boards/SKILL.md +14 -4
- package/skills/pier-help/SKILL.md +62 -23
- package/skills/pier-search/SKILL.md +27 -0
- package/skills/pier-tasks/SKILL.md +68 -18
- package/dist/channels/handoff.js +0 -94
- package/dist/web/public/assets/activity-Ds6fCHnb.js +0 -5
- package/dist/web/public/assets/activity-Ds6fCHnb.js.br +0 -0
- package/dist/web/public/assets/activity-Ds6fCHnb.js.gz +0 -0
- package/dist/web/public/assets/boards-BKCj6EwK.js +0 -1
- package/dist/web/public/assets/boards-BKCj6EwK.js.br +0 -0
- package/dist/web/public/assets/boards-BKCj6EwK.js.gz +0 -0
- package/dist/web/public/assets/explorer-DnTm975c.js +0 -4
- package/dist/web/public/assets/explorer-DnTm975c.js.br +0 -0
- package/dist/web/public/assets/explorer-DnTm975c.js.gz +0 -0
- package/dist/web/public/assets/hljs-tWqyD59G.js.br +0 -0
- package/dist/web/public/assets/hljs-tWqyD59G.js.gz +0 -0
- package/dist/web/public/assets/index-BQo-haPN.js +0 -85
- package/dist/web/public/assets/index-BQo-haPN.js.br +0 -0
- package/dist/web/public/assets/index-BQo-haPN.js.gz +0 -0
- package/dist/web/public/assets/index-DiHj0w1i.css +0 -2
- package/dist/web/public/assets/index-DiHj0w1i.css.br +0 -0
- package/dist/web/public/assets/index-DiHj0w1i.css.gz +0 -0
- package/dist/web/public/assets/runs-C_AthWcW.js +0 -1
- package/dist/web/public/assets/runs-C_AthWcW.js.br +0 -0
- package/dist/web/public/assets/runs-C_AthWcW.js.gz +0 -0
- package/dist/web/public/assets/settings-DDaAtFlc.js +0 -5
- package/dist/web/public/assets/settings-DDaAtFlc.js.br +0 -0
- package/dist/web/public/assets/settings-DDaAtFlc.js.gz +0 -0
- package/dist/web/public/assets/task-runs-0pvdITiV.js +0 -3
- package/dist/web/public/assets/task-runs-0pvdITiV.js.br +0 -0
- package/dist/web/public/assets/task-runs-0pvdITiV.js.gz +0 -0
- package/dist/web/public/assets/tasks-Bz29caHJ.js +0 -4
- package/dist/web/public/assets/tasks-Bz29caHJ.js.br +0 -0
- package/dist/web/public/assets/tasks-Bz29caHJ.js.gz +0 -0
package/dist/tasks/store.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// Every query this area makes against pier.db, and nothing else; the schema is
|
|
2
2
|
// db.ts's migration list.
|
|
3
3
|
import { pierDb, statements, transact } from "../db.js";
|
|
4
|
+
import { createdRole } from "./types.js";
|
|
4
5
|
const clamp = (limit, cap) => Math.min(Math.max(limit, 1), cap);
|
|
5
6
|
/** Kept unmatched probes per watch: one page of history, which is all any
|
|
6
7
|
* surface lists of a probe that found nothing. */
|
|
7
8
|
const KEPT_PROBES = 50;
|
|
9
|
+
/** The error of a run a restart or shutdown stopped: not a result, not a cancel. */
|
|
10
|
+
export const INTERRUPTED = "Pier restarted while the run was active";
|
|
8
11
|
export class TaskStore {
|
|
9
12
|
db;
|
|
10
13
|
/** Every query below is a fixed string, so each is compiled once. */
|
|
@@ -42,11 +45,8 @@ export class TaskStore {
|
|
|
42
45
|
listDueTasks(now) {
|
|
43
46
|
return this.#many("SELECT json FROM tasks WHERE next_run_at IS NOT NULL AND next_run_at <= ?", now);
|
|
44
47
|
}
|
|
45
|
-
listRuns(taskId, limit = 50
|
|
46
|
-
return this.#many(
|
|
47
|
-
SELECT json FROM task_runs WHERE task_id = ?
|
|
48
|
-
ORDER BY queued_at DESC LIMIT ? OFFSET ?
|
|
49
|
-
`, taskId, limit, offset);
|
|
48
|
+
listRuns(taskId, limit = 50) {
|
|
49
|
+
return this.#many("SELECT json FROM task_runs WHERE task_id = ? ORDER BY queued_at DESC LIMIT ?", taskId, limit);
|
|
50
50
|
}
|
|
51
51
|
getRun(id) {
|
|
52
52
|
return this.#one("SELECT json FROM task_runs WHERE id = ?", id);
|
|
@@ -79,57 +79,15 @@ export class TaskStore {
|
|
|
79
79
|
)
|
|
80
80
|
`).run(taskId, taskId, KEPT_PROBES);
|
|
81
81
|
}
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const where = [];
|
|
86
|
-
const params = [];
|
|
87
|
-
const add = (sql, value) => {
|
|
88
|
-
if (value !== undefined) {
|
|
89
|
-
where.push(sql);
|
|
90
|
-
params.push(value);
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
add("r.state = ?", query.state);
|
|
94
|
-
add("r.task_id = ?", query.taskId);
|
|
95
|
-
add("json_extract(r.json, '$.triggerSource') = ?", query.source);
|
|
96
|
-
add("r.queued_at >= ?", query.since);
|
|
97
|
-
add("r.queued_at <= ?", query.until);
|
|
98
|
-
// Keep this predicate identical to migration 17's partial index.
|
|
99
|
-
if (!query.showUnmatched)
|
|
100
|
-
where.push("NOT (r.state = 'succeeded' AND json_extract(r.json, '$.matched') IS 0)");
|
|
101
|
-
if (query.cursor) {
|
|
102
|
-
where.push("(r.queued_at, r.id) < (?, ?)");
|
|
103
|
-
params.push(query.cursor.queuedAt, query.cursor.id);
|
|
104
|
-
}
|
|
105
|
-
const limit = clamp(query.limit ?? 50, 200);
|
|
106
|
-
const rows = this.db.prepare(`
|
|
107
|
-
SELECT r.json, g.callback_state AS group_callback_state
|
|
108
|
-
FROM task_runs r LEFT JOIN task_groups g ON g.id = json_extract(r.json, '$.groupId')
|
|
109
|
-
${where.length ? `WHERE ${where.join(" AND ")}` : ""}
|
|
110
|
-
ORDER BY r.queued_at DESC, r.id DESC LIMIT ?
|
|
111
|
-
`).all(...params, limit + 1);
|
|
112
|
-
const runs = rows.slice(0, limit).map((row) => ({
|
|
113
|
-
...JSON.parse(row.json), groupCallbackState: row.group_callback_state,
|
|
114
|
-
}));
|
|
115
|
-
const last = runs.at(-1);
|
|
116
|
-
return {
|
|
117
|
-
runs,
|
|
118
|
-
nextCursor: rows.length > limit && last ? { queuedAt: last.queuedAt, id: last.id } : null,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
/** Activity never limits live work; only its optional history is bounded. */
|
|
122
|
-
activityRuns(since) {
|
|
123
|
-
const active = this.#many("SELECT json FROM task_runs WHERE state IN ('queued', 'running') ORDER BY queued_at DESC, id DESC");
|
|
124
|
-
if (since === undefined)
|
|
125
|
-
return active;
|
|
126
|
-
const recent = this.#many(`
|
|
82
|
+
/** Runs launched by any of `sessionIds`: in flight, or finished at or after `since`. */
|
|
83
|
+
ledgerRuns(sessionIds, since) {
|
|
84
|
+
return this.#many(`
|
|
127
85
|
SELECT json FROM task_runs
|
|
128
|
-
WHERE
|
|
86
|
+
WHERE json_extract(json, '$.invokedBySessionId') IN (SELECT value FROM json_each(?))
|
|
87
|
+
AND (state IN ('queued', 'running') OR json_extract(json, '$.finishedAt') >= ?)
|
|
129
88
|
AND NOT (state = 'succeeded' AND json_extract(json, '$.matched') IS 0)
|
|
130
89
|
ORDER BY queued_at DESC, id DESC LIMIT 200
|
|
131
|
-
`, since);
|
|
132
|
-
return [...active, ...recent].sort((a, b) => b.queuedAt - a.queuedAt || b.id.localeCompare(a.id));
|
|
90
|
+
`, JSON.stringify(sessionIds), since);
|
|
133
91
|
}
|
|
134
92
|
/** A `task` action's child runs; a cancel walks them. */
|
|
135
93
|
listChildRuns(parentRunId) {
|
|
@@ -177,15 +135,97 @@ export class TaskStore {
|
|
|
177
135
|
return new Map(rows.map((row) => [row.session_id, row.n]));
|
|
178
136
|
}
|
|
179
137
|
/** `fresh` is the one mode that makes a session rather than borrowing one;
|
|
180
|
-
* these are the agent's conversations with itself, which the
|
|
138
|
+
* these are the agent's conversations with itself, which the session list does not
|
|
139
|
+
* list. A feature lead's is the user's too, so it is not one of them. */
|
|
181
140
|
taskOwnedSessionIds() {
|
|
182
141
|
const rows = this.sql(`
|
|
183
142
|
SELECT DISTINCT json_extract(json, '$.context.sessionId') AS id
|
|
184
143
|
FROM task_runs
|
|
185
144
|
WHERE json_extract(json, '$.sessionMode') = 'fresh' AND id IS NOT NULL
|
|
145
|
+
AND json_extract(json, '$.context.definition.action.launch.role') IS NOT 'lead'
|
|
186
146
|
`).all();
|
|
187
147
|
return new Set(rows.map((row) => row.id));
|
|
188
148
|
}
|
|
149
|
+
/** The fresh run that made the session; a resume or a `--session`
|
|
150
|
+
* continuation never does, so the session's role is this run's for its life. */
|
|
151
|
+
creatorOf(sessionId) {
|
|
152
|
+
return this.#one(`
|
|
153
|
+
SELECT json FROM task_runs
|
|
154
|
+
WHERE json_extract(json, '$.targetSessionId') = ? AND json_extract(json, '$.sessionMode') = 'fresh'
|
|
155
|
+
ORDER BY queued_at LIMIT 1
|
|
156
|
+
`, sessionId);
|
|
157
|
+
}
|
|
158
|
+
roleOf(sessionId) {
|
|
159
|
+
const run = this.creatorOf(sessionId);
|
|
160
|
+
return run && createdRole(run);
|
|
161
|
+
}
|
|
162
|
+
leadPhaseOf(sessionId) {
|
|
163
|
+
const run = this.creatorOf(sessionId);
|
|
164
|
+
if (!run || createdRole(run) !== "lead" || run.context.definition.action.type !== "agent")
|
|
165
|
+
return undefined;
|
|
166
|
+
return run.context.definition.action.launch?.design ? "design" : "build";
|
|
167
|
+
}
|
|
168
|
+
/** Every lead session with its phase (as `leadPhaseOf`), its creating run,
|
|
169
|
+
* whether a run targeting it is queued or running, and — a design lead's —
|
|
170
|
+
* whether no run of it has reported `Design final:` yet, which leaves the
|
|
171
|
+
* design on the user; in one statement for the session list's listing. */
|
|
172
|
+
leads() {
|
|
173
|
+
const rows = this.sql(`
|
|
174
|
+
SELECT c.id, c.run_id, c.design, l.id IS NOT NULL AS live, f.id IS NOT NULL AS final FROM (
|
|
175
|
+
SELECT id AS run_id, json_extract(json, '$.targetSessionId') AS id,
|
|
176
|
+
json_extract(json, '$.context.definition.action.launch.role') AS role,
|
|
177
|
+
json_extract(json, '$.context.definition.action.launch.design') AS design,
|
|
178
|
+
ROW_NUMBER() OVER (PARTITION BY json_extract(json, '$.targetSessionId') ORDER BY queued_at) AS n
|
|
179
|
+
FROM task_runs
|
|
180
|
+
WHERE json_extract(json, '$.sessionMode') = 'fresh' AND json_extract(json, '$.targetSessionId') IS NOT NULL
|
|
181
|
+
) c
|
|
182
|
+
LEFT JOIN (
|
|
183
|
+
SELECT DISTINCT json_extract(json, '$.targetSessionId') AS id FROM task_runs WHERE state IN ('queued', 'running')
|
|
184
|
+
) l ON l.id = c.id
|
|
185
|
+
LEFT JOIN (
|
|
186
|
+
SELECT DISTINCT json_extract(json, '$.targetSessionId') AS id FROM task_runs
|
|
187
|
+
WHERE json_extract(json, '$.result.type') = 'agent'
|
|
188
|
+
AND instr(char(10) || json_extract(json, '$.result.text'), char(10) || 'Design final:') > 0
|
|
189
|
+
) f ON f.id = c.id
|
|
190
|
+
WHERE c.n = 1 AND c.role = 'lead'
|
|
191
|
+
`).all();
|
|
192
|
+
return new Map(rows.map((r) => {
|
|
193
|
+
const phase = r.design === 1 ? "design" : "build";
|
|
194
|
+
return [r.id, { phase, runId: r.run_id, runLive: r.live === 1, designOpen: phase === "design" && r.final === 0 }];
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
/** What a milestone resumes, and whose supervisor it reports to. */
|
|
198
|
+
latestRunForTarget(sessionId) {
|
|
199
|
+
// Two runs queued in one millisecond are common (a turn and the run it records); ids are random, rowid is insertion order.
|
|
200
|
+
return this.#one(`
|
|
201
|
+
SELECT json FROM task_runs WHERE json_extract(json, '$.targetSessionId') = ? ORDER BY queued_at DESC, rowid DESC LIMIT 1
|
|
202
|
+
`, sessionId);
|
|
203
|
+
}
|
|
204
|
+
/** Runs in flight whose result is owed to the session, their own or their
|
|
205
|
+
* unfinished group's; a finished group's losers still cancelling owe nothing. */
|
|
206
|
+
countOwedTo(sessionId) {
|
|
207
|
+
const row = this.sql(`
|
|
208
|
+
SELECT COUNT(*) AS n FROM task_runs r
|
|
209
|
+
WHERE r.state IN ('queued', 'running') AND (
|
|
210
|
+
json_extract(r.json, '$.callbackSessionId') = ?
|
|
211
|
+
OR EXISTS (SELECT 1 FROM task_groups g WHERE g.id = json_extract(r.json, '$.groupId')
|
|
212
|
+
AND g.finished_at IS NULL AND json_extract(g.json, '$.callbackSessionId') = ?))
|
|
213
|
+
`).get(sessionId, sessionId);
|
|
214
|
+
return row.n;
|
|
215
|
+
}
|
|
216
|
+
/** Whether a result is still coming to the session: a run in flight owed
|
|
217
|
+
* to it, or a finished run's or group's callback not yet delivered. */
|
|
218
|
+
awaitsResults(sessionId) {
|
|
219
|
+
if (this.countOwedTo(sessionId) > 0)
|
|
220
|
+
return true;
|
|
221
|
+
const row = this.sql(`
|
|
222
|
+
SELECT EXISTS (SELECT 1 FROM task_runs WHERE callback_state IN ('pending', 'failed')
|
|
223
|
+
AND json_extract(json, '$.callbackSessionId') = ?)
|
|
224
|
+
OR EXISTS (SELECT 1 FROM task_groups WHERE callback_state IN ('pending', 'failed')
|
|
225
|
+
AND json_extract(json, '$.callbackSessionId') = ?) AS owed
|
|
226
|
+
`).get(sessionId, sessionId);
|
|
227
|
+
return row.owed === 1;
|
|
228
|
+
}
|
|
189
229
|
listRunsForSession(sessionId, limit = 50) {
|
|
190
230
|
return this.#many(`
|
|
191
231
|
SELECT json FROM task_runs
|
|
@@ -228,8 +268,21 @@ export class TaskStore {
|
|
|
228
268
|
listMessages(runId) {
|
|
229
269
|
return this.#many("SELECT json FROM task_messages WHERE run_id = ? ORDER BY created_at", runId);
|
|
230
270
|
}
|
|
231
|
-
|
|
232
|
-
|
|
271
|
+
/** Follow-ups parked for the session until it idles: its queue shows them. */
|
|
272
|
+
pendingFollowUpsTo(sessionId) {
|
|
273
|
+
return this.#many(`
|
|
274
|
+
SELECT json FROM task_messages
|
|
275
|
+
WHERE state = 'pending' AND json_extract(json, '$.kind') = 'follow_up'
|
|
276
|
+
AND json_extract(json, '$.toSessionId') = ?
|
|
277
|
+
ORDER BY created_at
|
|
278
|
+
`, sessionId);
|
|
279
|
+
}
|
|
280
|
+
countPendingFollowUps(runId) {
|
|
281
|
+
const row = this.sql(`
|
|
282
|
+
SELECT COUNT(*) AS n FROM task_messages
|
|
283
|
+
WHERE run_id = ? AND state = 'pending' AND json_extract(json, '$.kind') = 'follow_up'
|
|
284
|
+
`).get(runId);
|
|
285
|
+
return row.n;
|
|
233
286
|
}
|
|
234
287
|
/** Each beside its run, so the sweep costs one query. A message whose run is
|
|
235
288
|
* gone comes back with `run: undefined`. */
|
|
@@ -266,7 +319,7 @@ export class TaskStore {
|
|
|
266
319
|
interruptRunning(now = Date.now()) {
|
|
267
320
|
return this.#many("SELECT json FROM task_runs WHERE state IN ('queued', 'running')").map((run) => {
|
|
268
321
|
run.state = "interrupted";
|
|
269
|
-
run.error =
|
|
322
|
+
run.error = INTERRUPTED;
|
|
270
323
|
run.finishedAt = now;
|
|
271
324
|
if (run.callbackSessionId)
|
|
272
325
|
run.callbackState = "pending";
|
|
@@ -274,4 +327,17 @@ export class TaskStore {
|
|
|
274
327
|
return run;
|
|
275
328
|
});
|
|
276
329
|
}
|
|
330
|
+
/** The continuous conversation's open items, oldest first; `runIds` as main wrote them. */
|
|
331
|
+
openItems() {
|
|
332
|
+
return this.sql("SELECT problem, stage, run_ids FROM open_items ORDER BY updated_at, rowid").all()
|
|
333
|
+
.map((r) => ({ problem: r.problem, stage: r.stage, runIds: JSON.parse(r.run_ids) }));
|
|
334
|
+
}
|
|
335
|
+
/** Main's markers, in reply order, as one write; answers how many rows changed. */
|
|
336
|
+
markOpenItems(markers, now) {
|
|
337
|
+
return this.transact(() => markers.reduce((n, m) => n + Number(m.op === "open"
|
|
338
|
+
? this.sql(`INSERT INTO open_items(problem, stage, run_ids, updated_at) VALUES (?, ?, ?, ?)
|
|
339
|
+
ON CONFLICT(problem) DO UPDATE SET stage = excluded.stage, run_ids = excluded.run_ids, updated_at = excluded.updated_at`)
|
|
340
|
+
.run(m.problem, m.stage, JSON.stringify(m.runIds), now).changes
|
|
341
|
+
: this.sql("DELETE FROM open_items WHERE problem = ?").run(m.problem).changes), 0));
|
|
342
|
+
}
|
|
277
343
|
}
|
package/dist/tasks/types.js
CHANGED
|
@@ -14,3 +14,6 @@ export const isTerminal = (state) => state === "succeeded" ||
|
|
|
14
14
|
state === "cancelled" ||
|
|
15
15
|
state === "interrupted" ||
|
|
16
16
|
state === "skipped";
|
|
17
|
+
/** The role a fresh run's session is created with and keeps: a lead's, or a
|
|
18
|
+
* worker's when a session launched it; a cron or watch run's has none. */
|
|
19
|
+
export const createdRole = ({ context: { definition: { action } }, invokedBySessionId }) => action.type === "agent" && action.launch?.role === "lead" ? "lead" : invokedBySessionId !== null ? "worker" : undefined;
|
package/dist/tools.js
CHANGED
|
@@ -22,8 +22,8 @@ export const MANAGED = [
|
|
|
22
22
|
`post_install = ["rtk", "init", "-g", "--agent", "pi", "--auto-patch"]\n` +
|
|
23
23
|
`pre_remove = ["rtk", "init", "--uninstall", "--agent", "pi", "--global", "--auto-patch"]`,
|
|
24
24
|
summary: "Compresses long bash output before it reaches the model. Its install hook " +
|
|
25
|
-
"writes its own Pi extension (extensions/rtk.ts,
|
|
26
|
-
"
|
|
25
|
+
"writes its own Pi extension (extensions/rtk.ts), and this switch is that " +
|
|
26
|
+
"extension's too — refreshed on every update.",
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
name: "rg",
|
|
@@ -392,6 +392,8 @@ export class ManagedTools {
|
|
|
392
392
|
#fetch;
|
|
393
393
|
#root;
|
|
394
394
|
#lock;
|
|
395
|
+
/** Why a tool is absent lives only here: `ubix list` never names one that failed to install. */
|
|
396
|
+
#lastSync;
|
|
395
397
|
#db;
|
|
396
398
|
constructor(options = {}) {
|
|
397
399
|
this.#exec = options.exec ?? spawnExec;
|
|
@@ -459,7 +461,8 @@ export class ManagedTools {
|
|
|
459
461
|
// The only thing that changes what `list` answers; drop the memo both sides.
|
|
460
462
|
this.#listed = undefined;
|
|
461
463
|
try {
|
|
462
|
-
|
|
464
|
+
this.#lastSync = await this.#converge(tools, customTools, fence);
|
|
465
|
+
return this.#lastSync;
|
|
463
466
|
}
|
|
464
467
|
finally {
|
|
465
468
|
this.#listed = undefined;
|
|
@@ -527,8 +530,12 @@ export class ManagedTools {
|
|
|
527
530
|
}
|
|
528
531
|
return base.map((entry) => {
|
|
529
532
|
const state = states.find((s) => s.name === entry.name);
|
|
530
|
-
if (!state)
|
|
531
|
-
|
|
533
|
+
if (!state) {
|
|
534
|
+
if (!entry.enabled)
|
|
535
|
+
return entry;
|
|
536
|
+
const synced = this.#lastSync?.entries.find((e) => e.name === entry.name);
|
|
537
|
+
return withBinary(entry, { error: synced?.error ?? "not installed — the last sync did not reach it" });
|
|
538
|
+
}
|
|
532
539
|
const gone = state.installed === true && state.exists === false;
|
|
533
540
|
// `npm:` lands in fnm's node prefix and `pixi:` in its own: installed,
|
|
534
541
|
// but not where Pier's PATH points, and the row has to say so.
|
package/dist/web/auth.js
CHANGED
|
@@ -261,7 +261,7 @@ export function noteFailure(client) {
|
|
|
261
261
|
else
|
|
262
262
|
failures.set(client, { count: 1, resetAt: Date.now() + WINDOW_MS });
|
|
263
263
|
}
|
|
264
|
-
|
|
264
|
+
const forgetFailures = (client) => void failures.delete(client);
|
|
265
265
|
/** Hosts, not schemes: TLS commonly terminates at the reverse proxy. */
|
|
266
266
|
function originMatches(origin, host) {
|
|
267
267
|
if (!origin)
|
|
@@ -341,7 +341,7 @@ export function openSession(c, store, client) {
|
|
|
341
341
|
store.revoke(previous.id);
|
|
342
342
|
setSessionCookie(c, store.open(client, c.req.header("user-agent") ?? ""));
|
|
343
343
|
}
|
|
344
|
-
|
|
344
|
+
const PASSWORD_OFF = "Password sign-in is off while a passkey is registered.";
|
|
345
345
|
export function registerAuthRoutes(app, store,
|
|
346
346
|
/** While it answers true the password is refused and the form offers only
|
|
347
347
|
* the passkey; read per request, so removing the last passkey needs no restart. */
|
package/dist/web/config-sync.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// public route is registered before auth; every management action is after it.
|
|
3
3
|
import { logger } from "../log.js";
|
|
4
4
|
const log = logger("config-sync");
|
|
5
|
+
/** The one answer every route that writes a synced field gives while the
|
|
6
|
+
* subscription is on: the source owns it, and the Console says where. */
|
|
7
|
+
export const managedRefusal = (c) => c.json({ error: "Managed by the configuration subscription — pause it under Agent → Configuration sync to edit here" }, 409);
|
|
5
8
|
export function registerConfigShareRoute(app, sync) {
|
|
6
9
|
app.get("/config-sync/:token", async (c) => {
|
|
7
10
|
c.header("cache-control", "no-store");
|
|
@@ -18,13 +21,9 @@ export function registerConfigShareRoute(app, sync) {
|
|
|
18
21
|
const published = await sync.published(token);
|
|
19
22
|
if (!published)
|
|
20
23
|
return c.json({ error: "not found" }, 404);
|
|
21
|
-
c.header("etag", published.etag);
|
|
22
24
|
c.header("x-content-type-options", "nosniff");
|
|
23
|
-
const tags = c.req.header("if-none-match")?.split(",").map((value) => value.trim().replace(/^W\//, ""));
|
|
24
|
-
if (tags?.includes(published.etag) || tags?.includes("*"))
|
|
25
|
-
return c.body(null, 304);
|
|
26
25
|
c.header("content-type", "application/json; charset=utf-8");
|
|
27
|
-
return c.body(published
|
|
26
|
+
return c.body(published);
|
|
28
27
|
}
|
|
29
28
|
catch {
|
|
30
29
|
// Never let the app's path logger print a capability URL.
|
package/dist/web/config.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// The agent files a scope is configured by, through the ConfigStore. Not via
|
|
2
2
|
// web/fs.ts: a scope is "global" or a cwd Pi already knows, never a browser path.
|
|
3
3
|
import { isThinkingLevel } from "../core/types.js";
|
|
4
|
+
import { SNAPSHOT_FILES } from "../agent/types.js";
|
|
4
5
|
import { normalizeModelRef } from "../settings.js";
|
|
6
|
+
import { managedRefusal } from "./config-sync.js";
|
|
5
7
|
import { guarded } from "./route.js";
|
|
6
|
-
export function registerConfigRoutes(app, { factory, config, onConfigWritten }) {
|
|
8
|
+
export function registerConfigRoutes(app, { factory, config, onConfigWritten, subscribed = () => false }) {
|
|
9
|
+
const managed = (scope, name) => scope.kind === "global" && subscribed() && SNAPSHOT_FILES.includes(name);
|
|
7
10
|
// Only cwds Pi already knows are accepted — never an arbitrary path.
|
|
8
11
|
const parseScope = async (raw) => {
|
|
9
12
|
if (!raw || raw === "global")
|
|
@@ -19,7 +22,7 @@ export function registerConfigRoutes(app, { factory, config, onConfigWritten })
|
|
|
19
22
|
return c.json({
|
|
20
23
|
// Where this scope's files live on disk — the UI labels "Global" with it.
|
|
21
24
|
dir: scope.kind === "global" ? config.globalDir : scope.cwd,
|
|
22
|
-
files: await config.listFiles(scope),
|
|
25
|
+
files: (await config.listFiles(scope)).map((file) => ({ ...file, managed: managed(scope, file.name) })),
|
|
23
26
|
});
|
|
24
27
|
});
|
|
25
28
|
guarded(app, "GET", "/api/config/files/:name", 400, async (c) => {
|
|
@@ -38,6 +41,8 @@ export function registerConfigRoutes(app, { factory, config, onConfigWritten })
|
|
|
38
41
|
return c.json({ error: "content and expected content required" }, 400);
|
|
39
42
|
}
|
|
40
43
|
const name = c.req.param("name");
|
|
44
|
+
if (managed(scope, name))
|
|
45
|
+
return managedRefusal(c);
|
|
41
46
|
await config.writeFile(scope, name, body.content, body.expected);
|
|
42
47
|
onConfigWritten?.();
|
|
43
48
|
return c.json({ ok: true, content: await config.readFile(scope, name) });
|
|
@@ -48,6 +53,8 @@ export function registerConfigRoutes(app, { factory, config, onConfigWritten })
|
|
|
48
53
|
return c.json(await config.readDefaults());
|
|
49
54
|
});
|
|
50
55
|
guarded(app, "PUT", "/api/config/defaults", 400, async (c) => {
|
|
56
|
+
if (subscribed())
|
|
57
|
+
return managedRefusal(c);
|
|
51
58
|
const defaults = parseDefaults(await c.req.json().catch(() => null));
|
|
52
59
|
if (!defaults) {
|
|
53
60
|
return c.json({ error: "defaultModel must be {provider, id} or null; defaultThinkingLevel a reasoning level or null" }, 400);
|
package/dist/web/explorer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// What git knows about a project directory, for the Files
|
|
1
|
+
// What git knows about a project directory, for the Files dialog. Every route
|
|
2
2
|
// runs git and nothing else; reading files is web/fs.ts.
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
import { promisify } from "node:util";
|
|
@@ -11,7 +11,7 @@ const REF_RE = /^[^-\s][^\s]*$/;
|
|
|
11
11
|
/** Output capped: a diff is display data, not an archive. */
|
|
12
12
|
const git = async (root, ...args) => (await run("git", ["-C", root, ...args], { maxBuffer: MAX_DIFF_BYTES })).stdout;
|
|
13
13
|
export function registerExplorerRoutes(app) {
|
|
14
|
-
// Not a repo → { branch: null }: the Files
|
|
14
|
+
// Not a repo → { branch: null }: the Files dialog draws no compare, no filter.
|
|
15
15
|
guarded(app, "GET", "/api/explorer/git", 404, async (c) => {
|
|
16
16
|
c.header("cache-control", "no-store");
|
|
17
17
|
const root = await scoped(c.req.query("root"));
|
package/dist/web/fs.js
CHANGED
|
@@ -51,7 +51,7 @@ export function fileHeaders(file, bytes, download = false) {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
export function registerFsRoutes(app) {
|
|
54
|
-
// The Files
|
|
54
|
+
// The Files dialog walks a project (`root` confines it); the cwd pickers walk
|
|
55
55
|
// from anywhere. Never contents, and never `.git`.
|
|
56
56
|
guarded(app, "GET", "/api/fs/ls", 404, async (c) => {
|
|
57
57
|
c.header("cache-control", "no-store");
|
|
@@ -67,6 +67,9 @@ export function registerFsRoutes(app) {
|
|
|
67
67
|
return c.json({ path: dir, parent: dir === top || up === dir ? null : up, entries });
|
|
68
68
|
});
|
|
69
69
|
guarded(app, "GET", "/api/fs/file", 404, async (c) => {
|
|
70
|
+
// The Files viewer frames a PDF; every other route stays DENY (auth.ts).
|
|
71
|
+
// On every answer, a 304 included: its headers replace the cached ones.
|
|
72
|
+
c.header("x-frame-options", "SAMEORIGIN");
|
|
70
73
|
const file = await scoped(c.req.query("root"), c.req.query("path"));
|
|
71
74
|
const handle = await open(file);
|
|
72
75
|
let streaming = false;
|
|
@@ -104,6 +107,18 @@ export function registerFsRoutes(app) {
|
|
|
104
107
|
await handle.close();
|
|
105
108
|
}
|
|
106
109
|
});
|
|
110
|
+
// Which of these absolute paths exist, file or folder: a reply's file
|
|
111
|
+
// reference is drawn only when it opens something. A path stat cannot reach
|
|
112
|
+
// (missing, unreadable) is the answer `false`, not a failure.
|
|
113
|
+
guarded(app, "POST", "/api/fs/exists", 400, async (c) => {
|
|
114
|
+
const body = await c.req.json().catch(() => null);
|
|
115
|
+
const paths = body?.paths;
|
|
116
|
+
if (!Array.isArray(paths) || paths.length > 1000 || !paths.every((p) => typeof p === "string")) {
|
|
117
|
+
return c.json({ error: "paths required" }, 400);
|
|
118
|
+
}
|
|
119
|
+
const exists = await Promise.all(paths.map((p) => (isAbsolute(p) ? stat(p).then(() => true, () => false) : false)));
|
|
120
|
+
return c.json({ exists });
|
|
121
|
+
});
|
|
107
122
|
// A name, never a path: traversal is rejected, not normalized.
|
|
108
123
|
guarded(app, "POST", "/api/fs/mkdir", 400, async (c) => {
|
|
109
124
|
const body = await c.req.json().catch(() => null);
|
package/dist/web/instance.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Routes about the Pier instance itself: settings, updates, secrets control,
|
|
2
2
|
// the browser's error reports. Nothing here touches a session.
|
|
3
3
|
import { logger } from "../log.js";
|
|
4
|
-
import { ACCENTS, normalizeAccent,
|
|
4
|
+
import { ACCENTS, normalizeAccent, parseModelMenu, normalizeModelRef, normalizePublicUrl, } from "../settings.js";
|
|
5
|
+
import { managedRefusal } from "./config-sync.js";
|
|
5
6
|
import { boundHost } from "./passkeys.js";
|
|
6
7
|
/** A rule that failed inside the transaction, distinct from a real fault: 400
|
|
7
8
|
* for a name this Pier does not have, 409 for a collision with current state. */
|
|
@@ -16,7 +17,7 @@ const withName = (current, { name, on }) => on ? [...new Set([...current, name])
|
|
|
16
17
|
/** A browser bug can fire in a loop, and the journal is shared. */
|
|
17
18
|
const CLIENT_LOG_PER_MINUTE = 60;
|
|
18
19
|
export function registerInstanceRoutes(app, deps) {
|
|
19
|
-
const { settings, passkeys, updates, updater = null, secrets, catalog, names = [], onUnlocked, onSettingsChanged, onToolsChanged, validateCustomTools, } = deps;
|
|
20
|
+
const { settings, passkeys, subscribed = () => false, updates, updater = null, secrets, catalog, names = [], onUnlocked, onSettingsChanged, onToolsChanged, validateCustomTools, } = deps;
|
|
20
21
|
const updateLog = logger("update");
|
|
21
22
|
// A busy Pier drains first, which can take minutes, and a response held that
|
|
22
23
|
// long dies at every proxy: past this cap the answer is "draining".
|
|
@@ -143,9 +144,11 @@ export function registerInstanceRoutes(app, deps) {
|
|
|
143
144
|
writes.push(() => settings.setAccent(accent));
|
|
144
145
|
}
|
|
145
146
|
if (body?.modelMenu !== undefined) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
if (subscribed())
|
|
148
|
+
return managedRefusal(c);
|
|
149
|
+
const menu = parseModelMenu(body.modelMenu);
|
|
150
|
+
if (typeof menu === "string")
|
|
151
|
+
return refuse(menu);
|
|
149
152
|
writes.push(() => settings.setModelMenu(menu));
|
|
150
153
|
}
|
|
151
154
|
if (body?.titleModel !== undefined) {
|
package/dist/web/packages.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The package registry (/api/packages*) through the PackageStore seam. A file
|
|
2
2
|
// of its own because the registry is not agent-file editing (config.ts).
|
|
3
|
-
import { PackageError } from "../
|
|
3
|
+
import { PackageError } from "../agent/types.js";
|
|
4
4
|
const STATUS = {
|
|
5
5
|
invalid: 400, missing: 404, busy: 409, refused: 409, unreachable: 502,
|
|
6
6
|
};
|
package/dist/web/passkeys.js
CHANGED
|
@@ -74,7 +74,7 @@ class Refusal extends Error {
|
|
|
74
74
|
/** The browser binds a credential to the RP ID and refuses any other host, so
|
|
75
75
|
* only an https public URL can name one; a passkey on a tunnel would be a
|
|
76
76
|
* lockout the moment the address changed. */
|
|
77
|
-
|
|
77
|
+
function relyingParty(publicUrl) {
|
|
78
78
|
if (!publicUrl.startsWith("https://")) {
|
|
79
79
|
return { reason: "passkeys need an https public URL (Settings → Instance)" };
|
|
80
80
|
}
|
|
@@ -186,7 +186,7 @@ const asBytes = (value, what) => {
|
|
|
186
186
|
};
|
|
187
187
|
/** COSE_Key → JWK, EC2 P-256 (ES256) or RSA (RS256). `createPublicKey`
|
|
188
188
|
* validates the point / modulus; the JWK is what the store keeps. */
|
|
189
|
-
|
|
189
|
+
function coseToJwk(key) {
|
|
190
190
|
const kty = key.get(1);
|
|
191
191
|
const alg = key.get(3);
|
|
192
192
|
const b64 = (label, what) => Buffer.from(asBytes(key.get(label), what)).toString("base64url");
|
|
@@ -211,7 +211,7 @@ export function coseToJwk(key) {
|
|
|
211
211
|
return jwk;
|
|
212
212
|
}
|
|
213
213
|
/** rpIdHash(32) flags(1) signCount(4) [aaguid(16) idLen(2) id COSE_Key] [extensions]. */
|
|
214
|
-
|
|
214
|
+
function parseAuthData(data) {
|
|
215
215
|
if (data.length < 37)
|
|
216
216
|
throw new Refusal("authenticator data too short", 400);
|
|
217
217
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
package/dist/web/providers.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// HTTP boundary for provider configuration.
|
|
2
|
-
import { isProviderApi, MODEL_EFFORTS, validateProviderSetup } from "../
|
|
2
|
+
import { isProviderApi, MODEL_EFFORTS, validateProviderSetup } from "../agent/types.js";
|
|
3
3
|
import { ProviderFlows } from "./provider-flows.js";
|
|
4
|
-
/** Shape here; the rules are
|
|
5
|
-
*
|
|
4
|
+
/** Shape here; the rules are agent/types.ts's validateProviderSetup, which
|
|
5
|
+
* web/ must not re-implement. */
|
|
6
6
|
function setupFrom(raw) {
|
|
7
7
|
if (typeof raw !== "object" || raw === null)
|
|
8
8
|
return null;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{J as e,r as t}from"./highlight-CDSk2hRJ.js";var n=2e4;function r(e){let t=e.split(`
|
|
2
|
+
`);return t.at(-1)===``&&t.pop(),t.map((e,t)=>({nums:[t+1],text:e,tone:``}))}function i(t,r){let i=t.length<=n?r:null,o=Math.max(2,...t.map(e=>e.nums.reduce((e,t)=>Math.max(e,String(t).length),0))),s=e(`div`,`py-2 font-mono text-[12px] leading-[1.5]`);for(let n of t){let t=e(`div`,`flex pl-2 pr-4 ${n.tone===`add`?`bg-emerald-50`:n.tone===`del`?`bg-red-50`:``}`);for(let r of n.nums){let n=e(`span`,`flex-none select-none pr-1.5 text-right text-neutral-300`,r===``?``:String(r));n.style.width=`calc(${o}ch + 0.375rem)`,t.append(n)}let r=n.nums.at(-1);typeof r==`number`&&(t.dataset.line=String(r)),t.append(a(n,i)),s.append(t)}return s}function a(n,r){let i=`min-w-0 flex-1 whitespace-pre-wrap [overflow-wrap:anywhere]`;if(!n.mark||n.mark[0]>=n.mark[1]){let e=t(n.text,r);return e.className=i,e}let[a,o]=n.mark,s=t(n.text.slice(a,o),r);return s.className=`rounded-xs ${n.tone===`add`?`bg-emerald-200/80`:`bg-red-200/70`}`,e(`span`,i,t(n.text.slice(0,a),r),s,t(n.text.slice(o),r))}export{r as n,i as t};
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{B as e,Et as t,J as n,M as r,Mt as i,T as a,U as o,V as s,X as c,Z as l,c as u,f as d,g as f,h as p,jt as m,k as h,n as ee,p as te,q as ne,rt as g,s as re,t as ie,wt as _,x as ae}from"./highlight-CDSk2hRJ.js";import{a as oe}from"./index-D0ZYdweB.js";import{n as se,t as ce}from"./code-CWT27KH1.js";var le=[[`path`,{d:`M12 5v14`}],[`path`,{d:`m19 12-7 7-7-7`}]],ue=[[`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`}]],de=[[`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`}]],fe=[[`path`,{d:`M15 6a9 9 0 0 0-9 9V3`}],[`circle`,{cx:`18`,cy:`6`,r:`3`}],[`circle`,{cx:`6`,cy:`18`,r:`3`}]],v=[[`path`,{d:`M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8`}],[`path`,{d:`M3 3v5h5`}]],y=e=>{let t=new Date(e),n=e=>String(e).padStart(2,`0`);return`${t.getFullYear()}-${n(t.getMonth()+1)}-${n(t.getDate())} ${n(t.getHours())}:${n(t.getMinutes())}:${n(t.getSeconds())}`},b=e=>[e.subject,e.body,[[e.author,e.email?`<${e.email}>`:``].filter(Boolean).join(` `),e.at?y(e.at):``].filter(Boolean).join(` · `)].filter(Boolean).join(`
|
|
2
|
+
|
|
3
|
+
`),x=null;function S(){x&&(x.remove(),x=null,document.removeEventListener(`pointerdown`,S,!0),window.removeEventListener(`scroll`,S,!0))}function pe(e,t){e.onmouseenter=()=>{S();let r=t();if(!r)return;x=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`),x.textContent=r,(e.closest(`dialog[open]`)??document.body).append(x),document.addEventListener(`pointerdown`,S,!0),window.addEventListener(`scroll`,S,!0);let i=e.getBoundingClientRect();x.style.left=`${Math.max(8,Math.min(i.left,window.innerWidth-x.offsetWidth-8))}px`,x.style.top=i.bottom+6+x.offsetHeight>window.innerHeight?`${i.top-x.offsetHeight-6}px`:`${i.bottom+6}px`},e.onmouseleave=S}function C(e,t,r){let i=n(`button`,`flex w-full cursor-pointer items-center gap-1.5 px-2 py-1 text-left hover:bg-neutral-100`,_(m,`h-3 w-3 text-indigo-600 ${e.value===t?``:`invisible`}`),n(`span`,`inline-flex flex-none items-center gap-1.5 font-mono text-[11.5px] text-neutral-800`,e.label??e.value));return e.subject&&i.append(n(`span`,`min-w-0 truncate text-[11px] text-neutral-400`,e.subject)),e.author&&i.append(n(`span`,`ml-auto flex-none pl-1.5 text-[10.5px] text-neutral-400`,`${e.author} · ${e.at?l(e.at):``}`)),pe(i,()=>b(e)),i.onclick=()=>r(e.value),i}function me(e,t,r,i,a){let{base:o,head:s}=r,c=()=>s&&o===`${s}~1`?s:``,l=c()?`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=n(`div`,`flex w-[38rem] max-w-full flex-col`);function h(){let e=(e,t)=>{let r=n(`button`,`cursor-pointer rounded-full px-2.5 py-0.5 text-[11.5px] ${l===t?`bg-indigo-50 font-medium text-indigo-700`:`text-neutral-500 hover:bg-neutral-100`}`,e);return r.onclick=()=>{l=t,h()},r},r=(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=>C(e,r,i)))),d=l===`compare`?n(`div`,`flex h-72 min-h-0 divide-x divide-neutral-200 pb-1`,r(`base`,p(!1),o,e=>{o=e,h(),i(o,s)}),r(`to`,p(!0),s,e=>{s=e,h(),i(o,s)})):n(`div`,`flex h-72 min-h-0 flex-col overflow-y-auto pb-1`,...t.commits.map(e=>C({value:e.hash,...e},c(),e=>{o=`${e}~1`,s=e,u(),i(o,s)}))),f=n(`div`,`flex-none border-t border-neutral-200`,C({value:a,label:`${a} ↔ Working tree`,subject:`back to the default: the main line's tip vs the files on disk`},o===a&&s===``?a:``,()=>{o=a,s=``,h(),i(o,s)}));f.querySelector(`button > span`).prepend(_(v)),m.replaceChildren(n(`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`)),d,...l===`compare`?[f]:[])}h(),d(e,m)}var he=e=>e.slice(0,Math.max(0,e.lastIndexOf(`/`))),ge=/\.(png|jpe?g|gif|webp|avif|bmp|svg)$/i,_e=/\.(md|markdown)$/i,ve={A:`text-emerald-600`,M:`text-amber-600`,D:`text-red-600`},ye=30,w=`pier.filesPrefs`,T=50,E=()=>{try{let e=JSON.parse(localStorage.getItem(w)??`{}`);return e&&typeof e==`object`?e:{}}catch{return{}}},be=e=>{let t=e?E()[e]:void 0;return typeof t?.cwd==`string`&&typeof t.base==`string`&&typeof t.head==`string`?{cwd:t.cwd,base:t.base,head:t.head}:null},xe=(e,t)=>{let n=E();delete n[e],n[e]=t;let r=Object.keys(n);for(let e of r.slice(0,Math.max(0,r.length-T)))delete n[e];try{localStorage.setItem(w,JSON.stringify(n))}catch(e){console.warn(`Files: could not remember this folder`,e)}},Se=`flex min-w-0 flex-none items-center gap-1.5 rounded-full px-2.5 py-1 font-mono text-[11.5px]`,Ce=(e,t=`indigo`)=>ae(e,`${Se} cursor-pointer truncate text-left ${t===`indigo`?`bg-indigo-50 font-medium text-indigo-700 hover:bg-indigo-100`:`bg-neutral-100 text-neutral-600 hover:bg-neutral-200`}`),D,O=()=>D??=N(e(`#files-dialog`)),k=(e,t,n,r)=>O()(e,t,n,r);function A(t){let n=e(`#files-dialog`);n.open?n.close():k(t)}function j(e,t){if(e&&t===e)return{root:e};let n=e&&t.startsWith(`${e}/`)?e:t.slice(0,t.lastIndexOf(`/`))||`/`,r=t.slice(n===`/`?1:n.length+1);return r?{root:n,select:r}:{root:n}}function we(e,t,n){if(!n||n.startsWith(`#`)||n.startsWith(`//`)||/^[a-z][a-z\d+.-]*:/i.test(n))return null;let r=n.replace(/[?#].*$/s,``);try{r=decodeURIComponent(r)}catch{}let i=(r.startsWith(`/`)?r===e||r.startsWith(`${e}/`)?``:e:t).split(`/`).filter(Boolean);for(let e of r.split(`/`))e===`..`?i.pop():e&&e!==`.`&&i.push(e);return`/${i.join(`/`)}`}var Te=e=>e.trim().toLowerCase().replace(/[^\p{L}\p{N}\s_-]/gu,``).replace(/\s/g,`-`);function M(e,t,n){let{root:r,select:i}=j(e?.cwd??``,t);k(e,r,i,n)}function N(e){let l=``,u=``,d={branch:null,refs:[],commits:[],worktrees:[]},m=`HEAD`,v=``,y=new Map,x=!0,S=!1,C=!1,w=[],T=new Set,E=null,D=null,O=null,k,A=n(`header`,`pagehead flex-nowrap`),M=n(`div`,`${te} flex-col items-stretch gap-1.5 px-3 py-2.5`),N=n(`div`,`min-h-0 flex-1 overflow-y-auto px-1.5 py-1.5`),Ee=n(`aside`,`${f} w-64 flex-none text-[12.5px] leading-5 max-md:h-1/2 max-md:w-full`,M,N),P=n(`div`,`${te} flex-wrap gap-2 px-4 py-2.5 font-mono text-[11.5px] text-neutral-500`),F=n(`div`,`h-full min-w-0 overflow-auto`),I=n(`div`,`pointer-events-none absolute inset-y-1 right-0.5 z-20 hidden w-2`),De=n(`div`,`relative min-h-0 flex-1`,F,I),Oe=n(`section`,`${f} min-h-0 min-w-0 flex-1`,P,De);e.append(A,n(`div`,`flex min-h-0 flex-1 gap-3 px-4 pb-4 pt-1 max-md:flex-col`,Ee,Oe));let L=(e,t)=>`/api/explorer/${e}?${new URLSearchParams({root:l,...t})}`,R=(e,t)=>`/api/fs/${e}?${new URLSearchParams({root:l,...t})}`,z=(e,t=`text-neutral-400`)=>n(`p`,`px-3 py-2 text-[12.5px] ${t}`,e),B=e=>n(`div`,`flex h-full items-center justify-center p-6`,a(e)),ke=()=>x&&d.branch!==null,Ae=e=>{let t=0;for(let n of y.keys())n.startsWith(`${e}/`)&&t++;return t};function je(e,t){let n=new Set(t.map(e=>e.name)),r=e?`${e}/`:``;for(let[e,i]of y){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 ke()&&(i=t.filter(e=>e.dir?Ae(`${r}${e.name}`)>0:y.has(`${r}${e.name}`))),i.sort((e,t)=>Number(t.dir)-Number(e.dir)||e.name.localeCompare(t.name)),O&&he(O)===e&&!n.has(o(O))&&(F.replaceChildren(B(`No such file: ${O}`)),O=null),i.map(e=>e.dir?Ne(`${r}${e.name}`,e.name):Pe(`${r}${e.name}`,e.name))}async function Me(e,t){try{let{entries:n}=await g(R(`ls`,{path:t}),`could not list this folder`);e.replaceChildren(...je(t,n)),e.childElementCount||e.append(z(ke()?`No changes.`:`Empty.`))}catch(n){let r=je(t,[]);r.length?e.replaceChildren(...r):e.replaceChildren(z(String(n),`text-red-600`))}}function Ne(e,t){let r=Ae(e),i=[n(`span`,`truncate`,t)];r&&i.push(n(`span`,`ml-auto flex-none rounded-full bg-amber-100/80 px-1.5 font-mono text-[10px] font-semibold text-amber-700`,String(r)));let{el:a,summary:o}=ne(``,i);o.className+=` rounded-lg px-2 py-1 transition-colors hover:bg-neutral-100`;let s=n(`div`,`pl-3`);a.append(s);let c=!1,l=()=>{c||(c=!0,s.append(z(`…`)),Me(s,e))};a.ontoggle=()=>{T[a.open?`add`:`delete`](e),a.open&&l()};let u=d.branch?x:!e.includes(`/`);return e===O&&(O=null),(u&&!C||T.has(e))&&(a.open=!0,l()),a}function V(e){D?.classList.remove(`bg-indigo-50`,`font-medium`,`text-indigo-700`),D=e,e.classList.add(`bg-indigo-50`,`font-medium`,`text-indigo-700`)}function Pe(e,t){let r=y.get(e)?.status,i=n(`button`,`flex w-full cursor-pointer items-center gap-1.5 rounded-lg py-1 pl-6 pr-2 text-left transition-colors hover:bg-neutral-100`,n(`span`,`truncate`,t));return r&&i.append(n(`span`,`ml-auto flex-none font-mono text-[10.5px] font-semibold ${ve[r]??`text-neutral-500`}`,r)),i.title=e,e===E&&V(i),e===O&&(O=null,E=e,V(i),q(e)),i.onclick=()=>{V(i),E=e,q(e)},i}let H=()=>void Me(N,``),Fe=(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 U(e,t=!1,...r){if(P.classList.toggle(`hidden`,!e),P.classList.toggle(`flex`,!!e),!e)return P.replaceChildren();let i=y.get(e),a=[n(`span`,`min-w-0 truncate`,e)];i&&(a.push(n(`span`,`flex-none font-semibold ${ve[i.status]??``}`,i.status)),a.push(...Fe(i.add,i.del)));let s=n(`div`,`ml-auto flex flex-none items-center gap-2`);if(s.append(...r),t){let t=document.createElement(`a`);t.className=`btn flex-none text-[12px]`,t.href=R(`file`,{path:e}),t.download=o(e),t.textContent=`Download`,s.append(t)}P.replaceChildren(...a,s)}function Ie(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=Je();function G(){I.classList.add(`hidden`),I.replaceChildren(),W.set([],[])}function Le(e,t){let n=e===E?k:void 0;if(n===void 0)return t;k=void 0;let r=t.querySelector(`[data-line="${String(n)}"]`);return r?.classList.add(`bg-indigo-100`),requestAnimationFrame(()=>r?.scrollIntoView({block:`center`})),t}let Re=0,K=e=>e===Re,ze=e=>_e.test(e)&&!S&&(e!==E||k===void 0);function Be(e,t){if(!_e.test(e))return[];let n=r([[`Rendered`,`rendered`],[`Source`,`source`]],t?`rendered`:`source`,n=>{n===`rendered`!==t&&(S=n===`source`,q(e))});return n.classList.add(`font-sans`),[n]}function Ve(e,t){let n=c(e);n.className=`md mx-auto max-w-3xl px-6 py-5 text-[14px] leading-relaxed`,ie(n),s(n);let r=`${l}/${he(t)}`,i=e=>{let t=j(l,e);return`/api/fs/file?${new URLSearchParams({root:t.root,path:t.select??``})}`};for(let e of n.querySelectorAll(`img`)){let t=we(l,r,e.getAttribute(`src`)??``);t&&e.setAttribute(`src`,i(t))}for(let e of n.querySelectorAll(`a`)){let t=e.getAttribute(`href`)??``;if(t.startsWith(`#`)){e.onclick=e=>{e.preventDefault();let r=t.slice(1);try{r=decodeURIComponent(r)}catch{}r=r.toLowerCase(),[...n.querySelectorAll(`h1, h2, h3, h4, h5, h6`)].find(e=>Te(e.textContent??``)===r)?.scrollIntoView({block:`start`})};continue}let a=we(l,r,t);a&&(e.setAttribute(`href`,i(a)),e.onclick=e=>{if(e.metaKey||e.ctrlKey||e.shiftKey||e.button!==0)return;e.preventDefault();let t=j(l,a);$(Q,t.root,t.select)})}return n}function q(e){let t=++Re;return y.has(e)?Ye(e,t):He(e,t)}async function He(e,t){if(!K(t))return;G();let r=R(`file`,{path:e}),i=n(`div`,`min-w-0`);F.classList.remove(`flex`,`flex-col`);let a=ze(e);if(U(e,!0,...Be(e,a)),F.replaceChildren(i),ge.test(e)){let e=n(`img`,`max-w-full p-4`);e.src=r,e.onerror=()=>i.replaceChildren(z(`Could not load image.`,`text-red-600`)),i.append(e);return}if(e.toLowerCase().endsWith(`.pdf`)){let e=document.createElement(`iframe`);e.className=`h-full w-full`,e.src=r,F.replaceChildren(e),F.classList.add(`flex`,`flex-col`);return}i.append(z(`…`));let o=await fetch(r);if(!K(t))return;if(!o.ok){let{error:e}=await o.json().catch(()=>({}));i.replaceChildren(z(e??`HTTP ${o.status}`,`text-red-600`));return}if(!(o.headers.get(`content-type`)??``).startsWith(`text/plain`)){i.replaceChildren(z(`Binary file — use Download.`));return}let s=await o.text();if(!K(t))return;if(a)return void i.replaceChildren(Ve(s,e));let c=se(s),l=await ee(e);K(t)&&i.replaceChildren(Le(e,ce(c,l)))}function Ue(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 We(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 Ge={add:`bg-emerald-400`,del:`bg-red-400`,mixed:`bg-amber-400`};function Ke(e,t,r){I.replaceChildren(...e.map(e=>{let i=n(`button`,`map-mark pointer-events-auto absolute left-1/2 w-1 -translate-x-1/2 cursor-pointer rounded-full ${Ge[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 ${qe(r,e.start)}`,i.onclick=()=>r[e.start].scrollIntoView({block:`center`}),i})),I.classList.remove(`hidden`)}let qe=(e,t)=>e[t]?.querySelector(`span`)?.textContent?.trim()||String(t+1);function Je(){let t=[],r=[],a=-1,o=n(`span`,`flex-none text-[10.5px] text-neutral-400`),s=e=>{t.length&&(a=(a+e+t.length)%t.length,r[t[a].start].scrollIntoView({block:`center`}),o.textContent=`${a+1}/${t.length}`)},c=()=>t.length>0&&e.open,l=(e,t,r,i)=>{let a=n(`button`,`icon-btn flex-none`,_(e));return a.onclick=()=>s(t),oe(a,i,r,()=>s(t),c),a};return{el:n(`div`,`flex flex-none items-center gap-1`,o,l(i,-1,`Previous change`,[`p`,`k`]),l(le,1,`Next change`,[`n`,`j`])),set(e,n){t=e,r=n,a=-1,o.textContent=`${e.length} change${e.length===1?``:`s`}`}}}async function Ye(e,t){G(),F.classList.remove(`flex`,`flex-col`);let n=y.get(e)?.status===`D`,r=!n,i=ze(e),a=Be(e,i);U(e,r,...a),F.replaceChildren(z(`…`));try{let{diff:o}=await g(L(`diff`,{base:m,head:v,file:e,context:`99999`}),`could not read this diff`);if(!K(t))return;if(!o||/^Binary files /m.test(o))return He(e,t);let s=Ue(o);if(i){let t=n?`add`:`del`,r=s.filter(e=>e.tone!==t&&e.nums.some(e=>e!==``)).map(e=>e.text).join(`
|
|
5
|
+
`);F.replaceChildren(Ve(r,e));return}Ie(s);let c=await ee(e);if(!K(t))return;let l=Le(e,ce(s,c)),u=[...l.children],d=We(s);W.set(d,u),U(e,r,...a,W.el),F.replaceChildren(l),Ke(d,s.length,u)}catch(n){if(!K(t))return;U(e),F.replaceChildren(z(String(n),`text-red-600`))}}let J=()=>v&&m===`${v}~1`?v:``,Xe=()=>d.commits.find(e=>e.hash===J())?.subject??``,Ze=()=>J()?`${J()} — ${Xe()||`commit`}`:`${m} ↔ ${v||`working tree`}`,Qe=(e,t,r,i)=>{let a=n(`button`,`icon-btn ${r?`bg-indigo-50 text-indigo-700 hover:bg-indigo-100 hover:text-indigo-700`:``}`);return a.append(_(e)),a.title=t,a.setAttribute(`aria-label`,t),a.setAttribute(`aria-pressed`,String(r)),a.onclick=i,a};function Y(){let e=Qe(de,`Show only files that differ`,x,()=>{x=!x,Y(),H()}),t=Qe(ue,C?`Restore folders`:`Collapse all folders`,C,()=>{if(C=!C,C)w=[...T],T.clear();else for(let e of w)T.add(e);Y(),H()});if(M.classList.toggle(`hidden`,!d.branch),M.classList.toggle(`flex`,!!d.branch),!d.branch)return void M.replaceChildren();let r=ae(Ze(),`${p} font-mono`);if(J()){let e=d.commits.find(e=>e.hash===J());pe(r,()=>e?b(e):``)}else r.title=`Choose what to diff`;r.onclick=async()=>{await nt(),me(r,d,{base:m,head:v},(e,t)=>{m=e,v=t,X()},rt())};let[i,a]=[0,0];for(let e of y.values())i+=e.add,a+=e.del;M.replaceChildren(r,n(`div`,`flex items-center gap-1.5 pl-0.5`,n(`span`,`text-[11px] text-neutral-400`,`${y.size} changed file${y.size===1?``:`s`}`),...Fe(i,a),n(`span`,`ml-auto`),e,t))}let $e=()=>{u&&l&&xe(u,{cwd:l,base:m,head:v})},et=e=>{let t=e.replace(/~\d+$/,``);return!t||t===`HEAD`||d.refs.some(e=>e.name===t)||d.commits.some(e=>e.hash===t)};async function tt(){if(y=new Map,!d.branch)return;let{files:e}=await g(L(`diff`,{base:m,head:v}),`could not read what changed`);for(let t of e)y.set(t.path,{status:t.status,add:t.add,del:t.del});if(y.size<=ye)for(let e of y.keys()){let t=e.split(`/`);for(let e=1;e<t.length;e++)T.add(t.slice(0,e).join(`/`))}}async function X(){$e();try{await tt()}catch(e){Y(),N.replaceChildren(z(String(e),`text-red-600`));return}Y(),H(),E&&q(E)}function Z(){let r=Ce(``,`neutral`);r.className+=` max-w-72`,r.append(n(`span`,`truncate max-md:hidden`,l||`Choose a folder…`),n(`span`,`truncate md:hidden`,l?o(l):`Choose a folder…`)),r.title=l?`${l} — switch folder`:`Choose a folder`,r.onclick=()=>re(r,d.worktrees.map(e=>({path:e.path,...e.branch?{hint:e.branch}:{}})),l||void 0,e=>$(Q,e));let i=n(`button`,`icon-btn ml-auto`,_(t));i.type=`button`,i.title=`Close Files`,i.setAttribute(`aria-label`,`Close Files`),i.onclick=()=>e.close();let a=d.branch?[n(`span`,`${Se} bg-neutral-100 text-neutral-600`,_(fe),n(`span`,`truncate`,d.branch))]:[];A.replaceChildren(h(`Files`),r,...a,i)}async function nt(){if(l)try{d=await g(L(`git`,{}),`could not read this repository`),Z()}catch(e){F.replaceChildren(B(String(e)))}}let rt=()=>d.refs.some(e=>e.name===`main`)?`main`:d.refs.some(e=>e.name===`master`)?`master`:`HEAD`;async function it(e){if(Z(),E=null,D=null,T.clear(),O=e||null,e){x=!1;let t=e.split(`/`);for(let e=1;e<=t.length;e++)T.add(t.slice(0,e).join(`/`))}if(G(),F.classList.remove(`flex`,`flex-col`),U(),F.replaceChildren(B(`Select a file.`)),!l){d={branch:null,refs:[],commits:[],worktrees:[]},Y(),N.replaceChildren(),F.replaceChildren(B(`No folder yet — pick one from the chip above.`));return}try{d=await g(L(`git`,{}),`could not read this repository`)}catch(e){d={branch:null,refs:[],commits:[],worktrees:[]},F.replaceChildren(B(String(e)))}let t=be(u),n=t&&t.cwd===l&&et(t.base)&&et(t.head)?t:null;m=n?.base??rt(),v=n?.head??``,Z(),await X()}e.addEventListener(`close`,()=>{G(),F.classList.remove(`flex`,`flex-col`),U(),F.replaceChildren(B(`Select a file.`)),D?.classList.remove(`bg-indigo-50`,`font-medium`,`text-indigo-700`),E=null,D=null});let Q;function $(t,n,r,i){Q=t;let a=t?.id??``;e.open||e.showModal();let o=n?.startsWith(`/`)?n:(a===u?l:``)||be(a)?.cwd||t?.cwd||``;if(o===l&&a===u&&N.childElementCount&&!r){Z(),nt().then(X);return}u=a,l=o,k=i,it(r)}return $}export{k as openFiles,M as openPath,A as toggleFiles};
|
|
Binary file
|
|
Binary file
|