@timqi/pier 0.0.19 → 0.0.21
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 +123 -0
- package/dist/agent/config.js +95 -3
- package/dist/agent/events.js +10 -1
- package/dist/agent/pi.js +50 -28
- package/dist/config-sync-fetch.js +84 -0
- package/dist/config-sync-task.js +59 -0
- package/dist/config-sync.js +179 -0
- package/dist/core/hub.js +10 -3
- package/dist/core/reply.js +12 -2
- package/dist/core/router.js +162 -37
- package/dist/db.js +6 -0
- package/dist/main.js +31 -1
- package/dist/tasks/agent.js +11 -22
- package/dist/tasks/callbacks.js +1 -1
- package/dist/tasks/definitions.js +25 -16
- package/dist/tasks/execution.js +11 -3
- package/dist/tasks/messages.js +57 -14
- package/dist/tasks/routes.js +58 -10
- package/dist/tasks/runs.js +0 -5
- package/dist/tasks/service.js +9 -4
- package/dist/tasks/store.js +45 -0
- package/dist/tasks/tool.js +8 -4
- package/dist/web/config-sync.js +91 -0
- package/dist/web/public/assets/activity-Cw0LRVNI.js +5 -0
- package/dist/web/public/assets/activity-Cw0LRVNI.js.br +0 -0
- package/dist/web/public/assets/activity-Cw0LRVNI.js.gz +0 -0
- package/dist/web/public/assets/{boards-DEMQ51m1.js → boards-DMVbV84c.js} +1 -1
- package/dist/web/public/assets/boards-DMVbV84c.js.br +0 -0
- package/dist/web/public/assets/boards-DMVbV84c.js.gz +0 -0
- package/dist/web/public/assets/{code-DjWvqL94.js → code-C86xKk2k.js} +1 -1
- package/dist/web/public/assets/{explorer-BTGQ6Syc.js → explorer-DTvsy5Rs.js} +1 -1
- package/dist/web/public/assets/explorer-DTvsy5Rs.js.br +0 -0
- package/dist/web/public/assets/explorer-DTvsy5Rs.js.gz +0 -0
- package/dist/web/public/assets/{ghostty-web-D8CLFi_l.js → ghostty-web-CvSZragH.js} +1 -1
- package/dist/web/public/assets/ghostty-web-CvSZragH.js.br +0 -0
- package/dist/web/public/assets/ghostty-web-CvSZragH.js.gz +0 -0
- package/dist/web/public/assets/index-DHxoYoeq.js +79 -0
- package/dist/web/public/assets/index-DHxoYoeq.js.br +0 -0
- package/dist/web/public/assets/index-DHxoYoeq.js.gz +0 -0
- package/dist/web/public/assets/{index-yNocr797.css → index-DuZ2vERd.css} +1 -1
- package/dist/web/public/assets/index-DuZ2vERd.css.br +0 -0
- package/dist/web/public/assets/index-DuZ2vERd.css.gz +0 -0
- package/dist/web/public/assets/runs-jY40JBP-.js +1 -0
- package/dist/web/public/assets/runs-jY40JBP-.js.br +0 -0
- package/dist/web/public/assets/runs-jY40JBP-.js.gz +0 -0
- package/dist/web/public/assets/settings-Dux7PIKF.js +6 -0
- package/dist/web/public/assets/settings-Dux7PIKF.js.br +0 -0
- package/dist/web/public/assets/settings-Dux7PIKF.js.gz +0 -0
- package/dist/web/public/assets/task-runs-DKvtBVxS.js +3 -0
- package/dist/web/public/assets/task-runs-DKvtBVxS.js.br +0 -0
- package/dist/web/public/assets/task-runs-DKvtBVxS.js.gz +0 -0
- package/dist/web/public/assets/tasks-CqfrOVIt.js +4 -0
- package/dist/web/public/assets/tasks-CqfrOVIt.js.br +0 -0
- package/dist/web/public/assets/tasks-CqfrOVIt.js.gz +0 -0
- package/dist/web/public/assets/{terminal-zLyOvGto.js → terminal-DL_0yEy7.js} +2 -2
- package/dist/web/public/assets/terminal-DL_0yEy7.js.br +0 -0
- package/dist/web/public/assets/terminal-DL_0yEy7.js.gz +0 -0
- package/dist/web/public/index.html +13 -2
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/server.js +65 -60
- package/package.json +2 -2
- package/skills/pier-tasks/SKILL.md +14 -9
- package/dist/web/public/assets/activity-BsoWK6vt.js +0 -5
- package/dist/web/public/assets/activity-BsoWK6vt.js.br +0 -0
- package/dist/web/public/assets/activity-BsoWK6vt.js.gz +0 -0
- package/dist/web/public/assets/boards-DEMQ51m1.js.br +0 -0
- package/dist/web/public/assets/boards-DEMQ51m1.js.gz +0 -0
- package/dist/web/public/assets/explorer-BTGQ6Syc.js.br +0 -0
- package/dist/web/public/assets/explorer-BTGQ6Syc.js.gz +0 -0
- package/dist/web/public/assets/ghostty-web-D8CLFi_l.js.br +0 -0
- package/dist/web/public/assets/ghostty-web-D8CLFi_l.js.gz +0 -0
- package/dist/web/public/assets/index-BFzmvpGX.js +0 -78
- package/dist/web/public/assets/index-BFzmvpGX.js.br +0 -0
- package/dist/web/public/assets/index-BFzmvpGX.js.gz +0 -0
- package/dist/web/public/assets/index-yNocr797.css.br +0 -0
- package/dist/web/public/assets/index-yNocr797.css.gz +0 -0
- package/dist/web/public/assets/settings-DSTKXN2o.js +0 -6
- package/dist/web/public/assets/settings-DSTKXN2o.js.br +0 -0
- package/dist/web/public/assets/settings-DSTKXN2o.js.gz +0 -0
- package/dist/web/public/assets/tasks-Dy1SCAE_.js +0 -4
- package/dist/web/public/assets/tasks-Dy1SCAE_.js.br +0 -0
- package/dist/web/public/assets/tasks-Dy1SCAE_.js.gz +0 -0
- package/dist/web/public/assets/terminal-zLyOvGto.js.br +0 -0
- package/dist/web/public/assets/terminal-zLyOvGto.js.gz +0 -0
|
@@ -127,11 +127,13 @@ export class TaskDefinitions {
|
|
|
127
127
|
factory;
|
|
128
128
|
router;
|
|
129
129
|
hub;
|
|
130
|
-
|
|
130
|
+
systemActions;
|
|
131
|
+
constructor(store, factory, router, hub, systemActions = {}) {
|
|
131
132
|
this.store = store;
|
|
132
133
|
this.factory = factory;
|
|
133
134
|
this.router = router;
|
|
134
135
|
this.hub = hub;
|
|
136
|
+
this.systemActions = systemActions;
|
|
135
137
|
}
|
|
136
138
|
list() { return this.store.listTasks(); }
|
|
137
139
|
get(id) {
|
|
@@ -145,7 +147,7 @@ export class TaskDefinitions {
|
|
|
145
147
|
// means manual. Update keeps requiring it: replacing a cron task with a
|
|
146
148
|
// draft that forgot its trigger must not silently unschedule it.
|
|
147
149
|
const value = record(raw);
|
|
148
|
-
const draft = await this.parseDraft(value && value.trigger === undefined ? { ...value, trigger: { type: "manual" } } : raw);
|
|
150
|
+
const draft = await this.parseDraft(value && value.trigger === undefined ? { ...value, trigger: { type: "manual" } } : raw, creator);
|
|
149
151
|
const now = Date.now();
|
|
150
152
|
const task = {
|
|
151
153
|
id: newId(),
|
|
@@ -176,7 +178,7 @@ export class TaskDefinitions {
|
|
|
176
178
|
this.assertOwner(old, by, "edited");
|
|
177
179
|
if (old.archived)
|
|
178
180
|
throw new Error("archived tasks cannot be edited");
|
|
179
|
-
const draft = await this.parseDraft(raw);
|
|
181
|
+
const draft = await this.parseDraft(raw, ownerOf(old) ? by : undefined);
|
|
180
182
|
const now = Date.now();
|
|
181
183
|
const task = {
|
|
182
184
|
...old,
|
|
@@ -252,7 +254,17 @@ export class TaskDefinitions {
|
|
|
252
254
|
throw new Error(`"${task.name}" is Pier's own ${owner} task: it is reconciled by Pier, not ${what}`);
|
|
253
255
|
}
|
|
254
256
|
}
|
|
255
|
-
|
|
257
|
+
/** Checked again at execution: persisted definitions can outlive registration. */
|
|
258
|
+
systemAction(name, owner) {
|
|
259
|
+
if (!owner || owner === "http" || owner.startsWith("session:") || owner !== name) {
|
|
260
|
+
throw new Error(`system action "${name}" requires its trusted owner`);
|
|
261
|
+
}
|
|
262
|
+
if (!Object.hasOwn(this.systemActions, name) || typeof this.systemActions[name] !== "function") {
|
|
263
|
+
throw new Error(`unregistered system action: ${name}`);
|
|
264
|
+
}
|
|
265
|
+
return this.systemActions[name];
|
|
266
|
+
}
|
|
267
|
+
async parseDraft(raw, owner) {
|
|
256
268
|
const value = record(raw);
|
|
257
269
|
if (!value)
|
|
258
270
|
throw new Error("task definition required");
|
|
@@ -279,8 +291,13 @@ export class TaskDefinitions {
|
|
|
279
291
|
this.get(taskId);
|
|
280
292
|
action = { type: "task", taskId };
|
|
281
293
|
}
|
|
294
|
+
else if (actionRaw.type === "system") {
|
|
295
|
+
const name = requiredString(actionRaw.name, "system action name");
|
|
296
|
+
this.systemAction(name, owner);
|
|
297
|
+
action = { type: "system", name };
|
|
298
|
+
}
|
|
282
299
|
else if (actionRaw.type === "agent") {
|
|
283
|
-
action = await this.parseAgentAction(actionRaw
|
|
300
|
+
action = await this.parseAgentAction(actionRaw);
|
|
284
301
|
}
|
|
285
302
|
else
|
|
286
303
|
throw new Error("unknown action type");
|
|
@@ -294,7 +311,7 @@ export class TaskDefinitions {
|
|
|
294
311
|
timeoutSeconds,
|
|
295
312
|
};
|
|
296
313
|
}
|
|
297
|
-
async parseAgentAction(raw
|
|
314
|
+
async parseAgentAction(raw) {
|
|
298
315
|
const prompt = requiredString(raw.prompt, "agent prompt");
|
|
299
316
|
const launch = parseLaunch(raw.launch);
|
|
300
317
|
const input = record(raw.session);
|
|
@@ -310,21 +327,13 @@ export class TaskDefinitions {
|
|
|
310
327
|
await this.assertDirectory(cwd);
|
|
311
328
|
session = { mode: "fresh", cwd };
|
|
312
329
|
}
|
|
313
|
-
else if (input?.mode === "fork") {
|
|
314
|
-
if (trigger.type !== "manual")
|
|
315
|
-
throw new Error("fork Agent tasks must use a manual trigger");
|
|
316
|
-
const cwd = typeof input.cwd === "string" && input.cwd.trim() ? input.cwd.trim() : undefined;
|
|
317
|
-
if (cwd)
|
|
318
|
-
await this.assertDirectory(cwd);
|
|
319
|
-
session = { mode: "fork", ...(cwd ? { cwd } : {}) };
|
|
320
|
-
}
|
|
321
330
|
else {
|
|
322
331
|
// Validation never mutates: a dedicated session is created explicitly
|
|
323
332
|
// (POST /api/sessions) and then referenced with mode:"reuse".
|
|
324
|
-
throw new Error('agent session policy required, e.g. {"mode":"fresh","cwd":"/abs/path"} or {"mode":"
|
|
333
|
+
throw new Error('agent session policy required, e.g. {"mode":"fresh","cwd":"/abs/path"} or {"mode":"reuse","sessionId":"..."}');
|
|
325
334
|
}
|
|
326
335
|
if (session.mode === "reuse" && launch)
|
|
327
|
-
throw new Error("launch policy only applies to fresh
|
|
336
|
+
throw new Error("launch policy only applies to fresh sessions");
|
|
328
337
|
return { type: "agent", session, prompt, ...(launch ? { launch } : {}) };
|
|
329
338
|
}
|
|
330
339
|
async parseCallback(raw) {
|
package/dist/tasks/execution.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// One queued run carried to a result: dispatched by action kind (a bash
|
|
2
|
-
// script, an agent session, another task
|
|
3
|
-
// exactly once.
|
|
4
|
-
// agent.ts; this file owns
|
|
2
|
+
// script, an agent session, another task or an owned system action), abortable
|
|
3
|
+
// while it goes, settled exactly once. Command and agent work lives in
|
|
4
|
+
// command.ts and agent.ts; this file owns the lifecycle they share.
|
|
5
5
|
import { logger } from "../log.js";
|
|
6
6
|
import { AgentTaskRunner } from "./agent.js";
|
|
7
7
|
import { TaskCallbacks } from "./callbacks.js";
|
|
@@ -125,6 +125,14 @@ export class TaskExecution {
|
|
|
125
125
|
}
|
|
126
126
|
return output;
|
|
127
127
|
}
|
|
128
|
+
if (action.type === "system") {
|
|
129
|
+
this.markRunning(run);
|
|
130
|
+
const handler = this.definitions.systemAction(action.name, run.context.definition.creator);
|
|
131
|
+
signal.throwIfAborted();
|
|
132
|
+
const text = await handler(signal);
|
|
133
|
+
signal.throwIfAborted();
|
|
134
|
+
return { type: "system", text };
|
|
135
|
+
}
|
|
128
136
|
if (action.type === "task") {
|
|
129
137
|
this.markRunning(run);
|
|
130
138
|
const child = this.host.runChild(action.taskId, run);
|
package/dist/tasks/messages.js
CHANGED
|
@@ -95,6 +95,30 @@ export class TaskMessenger {
|
|
|
95
95
|
this.changed(message);
|
|
96
96
|
continue;
|
|
97
97
|
}
|
|
98
|
+
// A reply that resumed a terminal run was never a system input: its text
|
|
99
|
+
// is the continuation's prompt, so the transcript read below would find
|
|
100
|
+
// nothing and re-inject a second copy, starting a turn no run owns. The
|
|
101
|
+
// continuation is the proof instead. `startedAt` is written one statement
|
|
102
|
+
// before the prompt reaches the session (agent.ts), so `delivered` here
|
|
103
|
+
// means "handed to the run that reports for it" — a `systemInput` that
|
|
104
|
+
// then throws fails *that* run, and the failure reaches the replier
|
|
105
|
+
// through its callback.
|
|
106
|
+
if (message.kind === "reply" && isTerminal(run.state)) {
|
|
107
|
+
const resumed = message.resumeRunId ? this.store.getRun(message.resumeRunId) : undefined;
|
|
108
|
+
if (resumed?.startedAt)
|
|
109
|
+
this.confirmed(message.id);
|
|
110
|
+
else if (!resumed)
|
|
111
|
+
this.abandon(message, `continuation ${message.resumeRunId ?? "(none)"} is gone`);
|
|
112
|
+
else if (isTerminal(resumed.state)) {
|
|
113
|
+
// Never started, and never will: say so rather than wait for a
|
|
114
|
+
// ceiling that would only report the same thing four minutes later.
|
|
115
|
+
// Both ends hear it here; the supervisor may also get the
|
|
116
|
+
// continuation's own cancelled callback, and a duplicate beats a
|
|
117
|
+
// special case that could suppress the only report either gets.
|
|
118
|
+
this.abandon(message, `continuation ${resumed.id} ${resumed.state} before it started`);
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
98
122
|
if ((message.nextAttemptAt ?? 0) > now)
|
|
99
123
|
continue;
|
|
100
124
|
const target = message.toSessionId || run.targetSessionId;
|
|
@@ -148,9 +172,11 @@ export class TaskMessenger {
|
|
|
148
172
|
this.deliver(reply, run, run.targetSessionId);
|
|
149
173
|
}
|
|
150
174
|
else if (isTerminal(run.state)) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
175
|
+
// Creating the continuation is not delivering the reply: the run is
|
|
176
|
+
// queued, and a restart or a cancel before it starts loses the prompt
|
|
177
|
+
// that carries the text. So the id is recorded and the sweep below
|
|
178
|
+
// settles it against the continuation's own `startedAt`.
|
|
179
|
+
reply.resumeRunId = this.resumeRun(run.id, this.format(reply, run), fromSessionId).id;
|
|
154
180
|
this.store.saveMessage(reply);
|
|
155
181
|
this.changed(reply);
|
|
156
182
|
}
|
|
@@ -208,6 +234,17 @@ export class TaskMessenger {
|
|
|
208
234
|
this.store.saveMessage(message);
|
|
209
235
|
this.changed(message);
|
|
210
236
|
}
|
|
237
|
+
/** Waiting is not a failed attempt: the message is in flight or the turn it
|
|
238
|
+
* has to wait for is still running, so it is tried again shortly and the
|
|
239
|
+
* ceiling is left for the deliveries that actually failed (the rule
|
|
240
|
+
* outbox.ts keeps for callbacks). */
|
|
241
|
+
defer(id) {
|
|
242
|
+
const message = this.store.getMessage(id);
|
|
243
|
+
if (!message || message.state !== "pending")
|
|
244
|
+
return;
|
|
245
|
+
message.nextAttemptAt = Date.now() + 1000;
|
|
246
|
+
this.store.saveMessage(message);
|
|
247
|
+
}
|
|
211
248
|
/** Counts one hand-off and says whether it may happen: a recipient that
|
|
212
249
|
* never records the message must not be re-sent once a second forever.
|
|
213
250
|
* False means the ceiling was reached and the message is now expired. */
|
|
@@ -247,10 +284,12 @@ export class TaskMessenger {
|
|
|
247
284
|
this.store.saveRun(run);
|
|
248
285
|
}
|
|
249
286
|
}
|
|
250
|
-
/**
|
|
251
|
-
*
|
|
287
|
+
/** Steer whatever someone is blocked on: a decision, and the reply that
|
|
288
|
+
* answers it — a follow-up lands only once the recipient runs out of tool
|
|
289
|
+
* calls, so each would wait out a whole turn (the reply waited 3 minutes
|
|
290
|
+
* behind one on 2026-09-07). Progress is a follow-up because nobody waits. */
|
|
252
291
|
mode(message) {
|
|
253
|
-
return message.kind === "
|
|
292
|
+
return message.kind === "follow_up" || message.kind === "progress" ? "follow_up" : "steer";
|
|
254
293
|
}
|
|
255
294
|
failed(id, error, spent) {
|
|
256
295
|
const message = this.store.getMessage(id);
|
|
@@ -285,14 +324,13 @@ export class TaskMessenger {
|
|
|
285
324
|
const recorded = async () => (await session.history()).some((turn) => turn.role === "system" && turn.origin?.kind === "task-message" && turn.origin.messageId === message.id);
|
|
286
325
|
if (await recorded())
|
|
287
326
|
return this.confirmed(message.id);
|
|
288
|
-
// Accepted and waiting in
|
|
289
|
-
// not recorded yet, and sending again would deliver the same
|
|
290
|
-
// Unlike a callback this is not deferred
|
|
291
|
-
//
|
|
292
|
-
// waiting for it
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
return;
|
|
327
|
+
// Accepted and waiting in the recipient's queue for the running turn to
|
|
328
|
+
// drain it: not recorded yet, and sending again would deliver the same
|
|
329
|
+
// guidance twice. Unlike a callback this is not deferred on a busy
|
|
330
|
+
// target — a steer's whole point is to reach the turn already running —
|
|
331
|
+
// so the queue is where it sits, and waiting for it costs no attempt.
|
|
332
|
+
if (await this.queued(session, message.id))
|
|
333
|
+
return this.defer(message.id);
|
|
296
334
|
spent = this.spend(message.id);
|
|
297
335
|
if (!spent)
|
|
298
336
|
return;
|
|
@@ -304,6 +342,11 @@ export class TaskMessenger {
|
|
|
304
342
|
this.failed(message.id, error, spent);
|
|
305
343
|
}
|
|
306
344
|
}
|
|
345
|
+
/** In the recipient's queue, by the id its origin carries — the transcript
|
|
346
|
+
* read above answers "landed", this answers "handed over and waiting". */
|
|
347
|
+
async queued(session, id) {
|
|
348
|
+
return (await session.pendingSystemInputs()).some((origin) => origin.kind === "task-message" && origin.messageId === id);
|
|
349
|
+
}
|
|
307
350
|
format(message, run) {
|
|
308
351
|
const title = run.context.definition.name;
|
|
309
352
|
if (message.kind === "progress") {
|
package/dist/tasks/routes.js
CHANGED
|
@@ -3,6 +3,47 @@
|
|
|
3
3
|
// its body, names the caller and hands the decision to TaskService — policy
|
|
4
4
|
// that lives here would be policy the task tool does not get.
|
|
5
5
|
import { record, requiredString } from "./definitions.js";
|
|
6
|
+
/** Reject malformed filters instead of silently widening a global query. */
|
|
7
|
+
function runQuery(params) {
|
|
8
|
+
const query = {};
|
|
9
|
+
const states = ["queued", "running", "succeeded", "failed", "cancelled", "interrupted", "skipped"];
|
|
10
|
+
const sources = ["manual", "cron", "watch", "agent", "task"];
|
|
11
|
+
if (params.state) {
|
|
12
|
+
if (!states.includes(params.state))
|
|
13
|
+
throw new Error("invalid state");
|
|
14
|
+
query.state = params.state;
|
|
15
|
+
}
|
|
16
|
+
if (params.source) {
|
|
17
|
+
if (!sources.includes(params.source))
|
|
18
|
+
throw new Error("invalid source");
|
|
19
|
+
query.source = params.source;
|
|
20
|
+
}
|
|
21
|
+
if (params.taskId)
|
|
22
|
+
query.taskId = params.taskId;
|
|
23
|
+
for (const key of ["since", "until", "limit"]) {
|
|
24
|
+
if (params[key] === undefined)
|
|
25
|
+
continue;
|
|
26
|
+
const value = Number(params[key]);
|
|
27
|
+
if (!params[key] || !Number.isSafeInteger(value) || value < 0 || (key === "limit" && (value < 1 || value > 200)))
|
|
28
|
+
throw new Error(`invalid ${key}`);
|
|
29
|
+
query[key] = value;
|
|
30
|
+
}
|
|
31
|
+
if (query.since !== undefined && query.until !== undefined && query.since > query.until)
|
|
32
|
+
throw new Error("since exceeds until");
|
|
33
|
+
if (params.showUnmatched !== undefined) {
|
|
34
|
+
if (!["true", "false"].includes(params.showUnmatched))
|
|
35
|
+
throw new Error("invalid showUnmatched");
|
|
36
|
+
query.showUnmatched = params.showUnmatched === "true";
|
|
37
|
+
}
|
|
38
|
+
if (params.cursor) {
|
|
39
|
+
const cursor = JSON.parse(params.cursor);
|
|
40
|
+
const value = record(cursor);
|
|
41
|
+
if (!value || !Number.isSafeInteger(value.queuedAt) || Number(value.queuedAt) < 0 || typeof value.id !== "string" || !value.id)
|
|
42
|
+
throw new Error("invalid cursor");
|
|
43
|
+
query.cursor = { queuedAt: Number(value.queuedAt), id: value.id };
|
|
44
|
+
}
|
|
45
|
+
return query;
|
|
46
|
+
}
|
|
6
47
|
const jsonBody = async (req) => req.json().catch(() => null);
|
|
7
48
|
export function registerTaskRoutes(app, tasks, activity) {
|
|
8
49
|
if (activity)
|
|
@@ -50,13 +91,21 @@ export function registerTaskRoutes(app, tasks, activity) {
|
|
|
50
91
|
messages,
|
|
51
92
|
});
|
|
52
93
|
});
|
|
94
|
+
app.get("/api/task-runs", (c) => {
|
|
95
|
+
try {
|
|
96
|
+
return c.json(tasks.queryRuns(runQuery(c.req.query())));
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
return c.json({ error: String(err) }, 400);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
53
102
|
app.get("/api/tasks", (c) => {
|
|
54
103
|
const trigger = c.req.query("trigger");
|
|
55
104
|
const state = c.req.query("state");
|
|
56
105
|
const kind = c.req.query("kind");
|
|
57
106
|
let rows = tasks.list();
|
|
58
107
|
// Subagent one-shots are hidden unless explicitly requested via ?kind=subagent.
|
|
59
|
-
rows =
|
|
108
|
+
rows = rows.filter((task) => task.kind === (kind ?? "task"));
|
|
60
109
|
if (trigger)
|
|
61
110
|
rows = rows.filter((task) => task.trigger.type === trigger);
|
|
62
111
|
if (state === "archived")
|
|
@@ -93,10 +142,7 @@ export function registerTaskRoutes(app, tasks, activity) {
|
|
|
93
142
|
});
|
|
94
143
|
app.get("/api/task-runs/:id", (c) => {
|
|
95
144
|
try {
|
|
96
|
-
|
|
97
|
-
// Same view as the task tool: a pending decision is the one run fact that
|
|
98
|
-
// lives on the messages, and HTTP callers need it too.
|
|
99
|
-
return c.json({ ...run, pendingDecisionId: tasks.openDecisionId(run.id) });
|
|
145
|
+
return c.json(tasks.getRunView(c.req.param("id")));
|
|
100
146
|
}
|
|
101
147
|
catch (err) {
|
|
102
148
|
return c.json({ error: String(err) }, 404);
|
|
@@ -181,13 +227,15 @@ export function registerTaskRoutes(app, tasks, activity) {
|
|
|
181
227
|
const body = record(await jsonBody(c.req));
|
|
182
228
|
const input = body && "input" in body ? body.input : null;
|
|
183
229
|
try {
|
|
184
|
-
|
|
230
|
+
// A caller that names a mode gets an answer about it: dropping an
|
|
231
|
+
// unsupported override would run the definition's own policy instead —
|
|
232
|
+
// on a reuse definition, work injected into a live session.
|
|
233
|
+
if (body?.sessionMode !== undefined && body.sessionMode !== "fresh") {
|
|
234
|
+
throw new Error(`unsupported sessionMode: ${String(body.sessionMode)}`);
|
|
235
|
+
}
|
|
236
|
+
const sessionMode = body?.sessionMode;
|
|
185
237
|
const sourceSessionId = typeof body?.sourceSessionId === "string" && body.sourceSessionId ? body.sourceSessionId : null;
|
|
186
238
|
const task = tasks.get(c.req.param("id"));
|
|
187
|
-
const effectiveMode = task.action.type === "agent" ? sessionMode ?? task.action.session.mode : null;
|
|
188
|
-
if (effectiveMode === "fork" && (!sourceSessionId || !(await tasks.sessionExists(sourceSessionId)))) {
|
|
189
|
-
throw new Error("fork requires a known sourceSessionId");
|
|
190
|
-
}
|
|
191
239
|
return c.json({ runId: tasks.run(task.id, input, "manual", null, {
|
|
192
240
|
invokedBySessionId: sourceSessionId,
|
|
193
241
|
sourceSessionId,
|
package/dist/tasks/runs.js
CHANGED
|
@@ -38,11 +38,6 @@ export class TaskRunQueue {
|
|
|
38
38
|
const sessionMode = definition.action.type === "agent"
|
|
39
39
|
? provenance.sessionMode ?? definition.action.session.mode
|
|
40
40
|
: null;
|
|
41
|
-
if (sessionMode === "fork" && !sourceSessionId)
|
|
42
|
-
throw new Error("fork requires a source session");
|
|
43
|
-
if (sessionMode === "fork" && (source === "cron" || source === "watch")) {
|
|
44
|
-
throw new Error("scheduled and watch runs cannot fork a caller session");
|
|
45
|
-
}
|
|
46
41
|
const targetSessionId = provenance.targetSessionId ?? (definition.action.type === "agent" && sessionMode === "reuse" && definition.action.session.mode === "reuse"
|
|
47
42
|
? definition.action.session.sessionId
|
|
48
43
|
: null);
|
package/dist/tasks/service.js
CHANGED
|
@@ -44,7 +44,7 @@ export class TaskService {
|
|
|
44
44
|
this.instance = instance;
|
|
45
45
|
const unreachable = (sessionId, what, why) => this.unreachable(sessionId, what, why);
|
|
46
46
|
this.messages = new TaskMessenger(store, router, hub, (runId, prompt, fromSessionId) => this.resume(runId, prompt, { invokedBySessionId: fromSessionId, callbackSessionId: fromSessionId, background: true }), unreachable);
|
|
47
|
-
this.definitions = new TaskDefinitions(store, factory, router, hub);
|
|
47
|
+
this.definitions = new TaskDefinitions(store, factory, router, hub, instance?.systemActions);
|
|
48
48
|
this.callbacks = new TaskCallbacks(store, router, (run) => this.changed(run), unreachable);
|
|
49
49
|
this.groups = new TaskGroups(store, router, {
|
|
50
50
|
getRun: (id) => this.getRun(id),
|
|
@@ -164,9 +164,6 @@ export class TaskService {
|
|
|
164
164
|
archive(id, by) {
|
|
165
165
|
return this.definitions.archive(id, by);
|
|
166
166
|
}
|
|
167
|
-
sessionExists(sessionId) {
|
|
168
|
-
return this.definitions.sessionExists(sessionId);
|
|
169
|
-
}
|
|
170
167
|
listRuns(taskId, limit = 50, offset = 0) {
|
|
171
168
|
this.get(taskId);
|
|
172
169
|
return this.store.listRuns(taskId, Math.min(Math.max(limit, 1), 200), Math.max(offset, 0));
|
|
@@ -177,6 +174,11 @@ export class TaskService {
|
|
|
177
174
|
throw new Error(`unknown task run: ${id}`);
|
|
178
175
|
return run;
|
|
179
176
|
}
|
|
177
|
+
getRunView(id) {
|
|
178
|
+
const run = this.getRun(id);
|
|
179
|
+
return { ...run, pendingDecisionId: this.openDecisionId(id),
|
|
180
|
+
groupCallbackState: run.groupId ? this.store.getGroup(run.groupId)?.callbackState ?? null : null };
|
|
181
|
+
}
|
|
180
182
|
listMessages(runId) {
|
|
181
183
|
this.getRun(runId);
|
|
182
184
|
return this.messages.list(runId);
|
|
@@ -184,6 +186,9 @@ export class TaskService {
|
|
|
184
186
|
openDecisionId(runId) {
|
|
185
187
|
return this.messages.openDecisionId(runId);
|
|
186
188
|
}
|
|
189
|
+
queryRuns(query = {}) {
|
|
190
|
+
return this.store.queryRuns(query);
|
|
191
|
+
}
|
|
187
192
|
recentRuns(limit = 100) {
|
|
188
193
|
return this.store.listRecentRuns(limit);
|
|
189
194
|
}
|
package/dist/tasks/store.js
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
import { pierDb, statements } from "../db.js";
|
|
6
6
|
const clamp = (limit, cap) => Math.min(Math.max(limit, 1), cap);
|
|
7
7
|
export class TaskStore {
|
|
8
|
+
db;
|
|
8
9
|
/** Every query below is a fixed string, so each is compiled once. */
|
|
9
10
|
sql;
|
|
10
11
|
constructor(db = pierDb()) {
|
|
12
|
+
this.db = db;
|
|
11
13
|
this.sql = statements(db);
|
|
12
14
|
}
|
|
13
15
|
// Every table is one JSON column plus query columns; these two are the only
|
|
@@ -56,6 +58,49 @@ export class TaskStore {
|
|
|
56
58
|
state=excluded.state, callback_state=excluded.callback_state, json=excluded.json
|
|
57
59
|
`).run(run.id, run.taskId, run.queuedAt, run.state, run.callbackState, JSON.stringify(run));
|
|
58
60
|
}
|
|
61
|
+
/** The global list: filters precede the limit, and the id breaks timestamp
|
|
62
|
+
* ties so a page boundary never repeats or skips a row. The statement is
|
|
63
|
+
* built per call and not cached — the filter set makes it. */
|
|
64
|
+
queryRuns(query = {}) {
|
|
65
|
+
const where = [];
|
|
66
|
+
const params = [];
|
|
67
|
+
const add = (sql, value) => {
|
|
68
|
+
if (value !== undefined) {
|
|
69
|
+
where.push(sql);
|
|
70
|
+
params.push(value);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
add("r.state = ?", query.state);
|
|
74
|
+
add("r.task_id = ?", query.taskId);
|
|
75
|
+
add("json_extract(r.json, '$.triggerSource') = ?", query.source);
|
|
76
|
+
add("r.queued_at >= ?", query.since);
|
|
77
|
+
add("r.queued_at <= ?", query.until);
|
|
78
|
+
// Keep this predicate identical to migration 17's partial index.
|
|
79
|
+
if (!query.showUnmatched)
|
|
80
|
+
where.push("NOT (r.state = 'succeeded' AND json_extract(r.json, '$.matched') IS 0)");
|
|
81
|
+
if (query.cursor) {
|
|
82
|
+
where.push("(r.queued_at, r.id) < (?, ?)");
|
|
83
|
+
params.push(query.cursor.queuedAt, query.cursor.id);
|
|
84
|
+
}
|
|
85
|
+
const limit = clamp(query.limit ?? 50, 200);
|
|
86
|
+
const rows = this.db.prepare(`
|
|
87
|
+
SELECT r.json, g.callback_state AS group_callback_state,
|
|
88
|
+
(SELECT m.id FROM task_messages m WHERE m.run_id = r.id
|
|
89
|
+
AND m.state IN ('pending', 'delivered') AND json_extract(m.json, '$.kind') = 'decision'
|
|
90
|
+
ORDER BY m.created_at, m.id LIMIT 1) AS decision_id
|
|
91
|
+
FROM task_runs r LEFT JOIN task_groups g ON g.id = json_extract(r.json, '$.groupId')
|
|
92
|
+
${where.length ? `WHERE ${where.join(" AND ")}` : ""}
|
|
93
|
+
ORDER BY r.queued_at DESC, r.id DESC LIMIT ?
|
|
94
|
+
`).all(...params, limit + 1);
|
|
95
|
+
const runs = rows.slice(0, limit).map((row) => ({
|
|
96
|
+
...JSON.parse(row.json), pendingDecisionId: row.decision_id, groupCallbackState: row.group_callback_state,
|
|
97
|
+
}));
|
|
98
|
+
const last = runs.at(-1);
|
|
99
|
+
return {
|
|
100
|
+
runs,
|
|
101
|
+
nextCursor: rows.length > limit && last ? { queuedAt: last.queuedAt, id: last.id } : null,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
59
104
|
listRecentRuns(limit = 100) {
|
|
60
105
|
return this.#many("SELECT json FROM task_runs ORDER BY queued_at DESC LIMIT ?", clamp(limit, 500));
|
|
61
106
|
}
|
package/dist/tasks/tool.js
CHANGED
|
@@ -78,7 +78,6 @@ const DraftSchema = Type.Object({
|
|
|
78
78
|
type: Type.Literal("agent"),
|
|
79
79
|
session: Type.Union([
|
|
80
80
|
Type.Object({ mode: Type.Literal("fresh"), cwd: Type.String() }),
|
|
81
|
-
Type.Object({ mode: Type.Literal("fork"), cwd: Type.Optional(Type.String()) }),
|
|
82
81
|
Type.Object({ mode: Type.Literal("reuse"), sessionId: Type.String() }),
|
|
83
82
|
]),
|
|
84
83
|
prompt: Type.String(),
|
|
@@ -102,7 +101,7 @@ export function taskToolSpec(execute) {
|
|
|
102
101
|
return {
|
|
103
102
|
name: "task",
|
|
104
103
|
label: "Pier Task",
|
|
105
|
-
description: "Manage durable Pier tasks and subagents. Agent tasks
|
|
104
|
+
description: "Manage durable Pier tasks and subagents. Agent tasks run in a fresh session or a reused one. Run executes a stored task by task_id, a one-shot subagent from an inline task draft, or a core-joined fan-out via tasks[] with join all|first. Get accepts run_id, group_id, or task_id for that task's recent runs. Every operation returns immediately: results, group joins, and decision replies arrive as callback messages. Use steer/follow_up/resume for child control and contact/reply for supervisor decisions. models lists the deployment's model menu (operator pins with intent notes, else the live catalog).",
|
|
106
105
|
parameters: Type.Object({
|
|
107
106
|
operation: strEnum("list", "create", "update", "run", "get", "cancel", "steer", "follow_up", "resume", "contact", "reply", "models"),
|
|
108
107
|
task_id: Type.Optional(Type.String()),
|
|
@@ -111,7 +110,7 @@ export function taskToolSpec(execute) {
|
|
|
111
110
|
message_id: Type.Optional(Type.String()),
|
|
112
111
|
message: Type.Optional(Type.String()),
|
|
113
112
|
reason: Type.Optional(strEnum("progress", "decision")),
|
|
114
|
-
session_mode: Type.Optional(strEnum("fresh"
|
|
113
|
+
session_mode: Type.Optional(strEnum("fresh")),
|
|
115
114
|
task: Type.Optional(DraftSchema),
|
|
116
115
|
// The same draft again, spelled out, cost more tokens in every session
|
|
117
116
|
// than the whole rest of this contract. One copy is the guidance; this
|
|
@@ -173,7 +172,12 @@ export async function handleTaskTool(host, definitions, store, messages, raw, ca
|
|
|
173
172
|
const task = draft
|
|
174
173
|
? await resolveDraft(definitions, draft, active, callerSessionId)
|
|
175
174
|
: resolveStored(definitions, input.task_id, active);
|
|
176
|
-
|
|
175
|
+
// Same as the HTTP route: a named mode the schema no longer offers is
|
|
176
|
+
// answered, not quietly swapped for the definition's own policy.
|
|
177
|
+
if (input.session_mode !== undefined && input.session_mode !== "fresh") {
|
|
178
|
+
throw new Error(`unsupported session_mode: ${String(input.session_mode)}`);
|
|
179
|
+
}
|
|
180
|
+
const sessionMode = input.session_mode;
|
|
177
181
|
let callbackSessionId = input.callback === "none" ? null : callerSessionId;
|
|
178
182
|
if (!active && callbackSessionId && typeof input.callback_session_id === "string") {
|
|
179
183
|
callbackSessionId = requiredString(input.callback_session_id, "callback_session_id");
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// HTTP presentation of the injected configuration subscription. The sole
|
|
2
|
+
// public route is registered before auth; every management action is after it.
|
|
3
|
+
import { logger } from "../log.js";
|
|
4
|
+
const log = logger("config-sync");
|
|
5
|
+
export function registerConfigShareRoute(app, sync) {
|
|
6
|
+
app.get("/config-sync/:token", async (c) => {
|
|
7
|
+
c.header("cache-control", "no-store");
|
|
8
|
+
c.header("x-frame-options", "DENY");
|
|
9
|
+
c.header("referrer-policy", "no-referrer");
|
|
10
|
+
if (c.req.method !== "GET") {
|
|
11
|
+
c.header("allow", "GET");
|
|
12
|
+
return c.body(null, 405);
|
|
13
|
+
}
|
|
14
|
+
const token = c.req.param("token");
|
|
15
|
+
if (!/^[a-f0-9]{64}$/.test(token))
|
|
16
|
+
return c.json({ error: "not found" }, 404);
|
|
17
|
+
try {
|
|
18
|
+
const published = await sync.published(token);
|
|
19
|
+
if (!published)
|
|
20
|
+
return c.json({ error: "not found" }, 404);
|
|
21
|
+
c.header("etag", published.etag);
|
|
22
|
+
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
|
+
c.header("content-type", "application/json; charset=utf-8");
|
|
27
|
+
return c.body(published.body);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Never let the app's path logger print a capability URL.
|
|
31
|
+
log.error("Could not export shared configuration");
|
|
32
|
+
return c.json({ error: "Could not export configuration" }, 500);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
// Reject other verbs here so neither auth redirects nor request-path logs
|
|
36
|
+
// can carry this capability to another surface.
|
|
37
|
+
app.all("/config-sync/:token", (c) => {
|
|
38
|
+
c.header("cache-control", "no-store");
|
|
39
|
+
c.header("allow", "GET");
|
|
40
|
+
return c.body(null, 405);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export function registerConfigSyncRoutes(app, deps) {
|
|
44
|
+
app.get("/api/config-sync", (c) => {
|
|
45
|
+
c.header("cache-control", "no-store");
|
|
46
|
+
return c.json(deps.status());
|
|
47
|
+
});
|
|
48
|
+
app.post("/api/config-sync", async (c) => {
|
|
49
|
+
c.header("cache-control", "no-store");
|
|
50
|
+
const body = await c.req.json().catch(() => null);
|
|
51
|
+
if (!body || typeof body.action !== "string")
|
|
52
|
+
return c.json({ error: "action required" }, 400);
|
|
53
|
+
try {
|
|
54
|
+
let result;
|
|
55
|
+
switch (body.action) {
|
|
56
|
+
case "publish":
|
|
57
|
+
await deps.sync.publish();
|
|
58
|
+
break;
|
|
59
|
+
case "revoke":
|
|
60
|
+
await deps.sync.revoke();
|
|
61
|
+
break;
|
|
62
|
+
case "subscribe": {
|
|
63
|
+
if (typeof body.url !== "string")
|
|
64
|
+
throw new Error("Source URL required");
|
|
65
|
+
result = await deps.sync.subscribe(body.url);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case "pause":
|
|
69
|
+
await deps.sync.pause();
|
|
70
|
+
break;
|
|
71
|
+
case "sync":
|
|
72
|
+
result = await deps.run();
|
|
73
|
+
break;
|
|
74
|
+
default: return c.json({ error: "Unknown configuration sync action" }, 400);
|
|
75
|
+
}
|
|
76
|
+
await deps.reconcile();
|
|
77
|
+
return c.json({ ...deps.status(), ...(result ? { result } : {}) });
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
// Application can succeed while reload fails; still reconcile the owned
|
|
81
|
+
// task to the persisted subscription switch in that case.
|
|
82
|
+
try {
|
|
83
|
+
await deps.reconcile();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
log.error("Could not reconcile configuration sync task");
|
|
87
|
+
}
|
|
88
|
+
return c.json({ error: err instanceof Error ? err.message : "Configuration sync failed", status: deps.status() }, 409);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{F as e,I as t,K as n,M as r,R as i,T as a,V as o,z as s}from"./index-DHxoYoeq.js";var c=(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},l=t=>t===null?`-`:e(Date.now()-t),u={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`}}},d=()=>u[document.documentElement.dataset.theme===`dark`?`dark`:`light`];function f(e,u,f){let p=`sessions`,m=`active`,h={sessions:[],runs:[],messages:[]},g=``,_=s(async()=>{let r=m,i=await o(`/api/activity?scope=${r}`,`Failed to load activity`);if(!i.ok){g=``,e.replaceChildren(t(`p`,`p-4 text-[13px] text-red-600`,i.error));return}if(r!==m)return;let a=JSON.stringify(i.value);if(`${r}:${a}`===g)return;g=`${r}:${a}`;let s=i.value;h={...s,sessions:s.sessions.map(e=>({...e,title:n(e.title)}))},v()});function v(){let n=t(`header`,`flex h-10 flex-none items-center gap-2 border-b border-neutral-200 px-4 max-md:hidden`,t(`span`,`font-medium`,`Activity`)),r=t(`div`,`tabstrip`);r.append(a(`Sessions`,p===`sessions`,()=>{p=`sessions`,v()}),a(`Relationships`,p===`dependencies`,()=>{p=`dependencies`,v()}));let i=t(`div`,`flex flex-none gap-1 border-b border-neutral-200 px-4 py-2`),o=e=>()=>{m=e,v(),_()};i.append(a(`Current`,m===`active`,o(`active`)),a(`Last 24h`,m===`recent`,o(`recent`)));let s=t(`div`,`min-h-0 flex-1 overflow-auto`);p===`sessions`?y(s):b(s),e.replaceChildren(n,r,i,s)}function y(e){let n=document.createElement(`table`);n.className=`w-full table-fixed text-left text-[12.5px]`,n.innerHTML=`<thead class="bg-neutral-50 text-[10.5px] uppercase text-neutral-400"><tr>
|
|
2
|
+
<th class="w-[50%] px-4 py-2 font-semibold md:w-[34%]">Session</th>
|
|
3
|
+
<th class="hidden w-[38%] px-2 py-2 font-semibold md:table-cell">Project</th>
|
|
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 r=document.createElement(`tbody`);for(let e of h.sessions){let n=document.createElement(`tr`);n.className=`cursor-pointer border-b border-neutral-100 hover:bg-neutral-50`,n.onclick=()=>u(e.id);let a=t(`button`,`block w-full cursor-pointer truncate text-left font-medium`,e.title??i(e.cwd));a.setAttribute(`type`,`button`),a.title=`${e.id}\n${e.cwd}`,a.onclick=t=>{t.stopPropagation(),u(e.id)},n.append(t(`td`,`px-4 py-2.5`,a,t(`div`,`truncate font-mono text-[11px] text-neutral-400 md:hidden`,e.cwd||`-`)),t(`td`,`hidden truncate px-2 py-2.5 font-mono text-[11.5px] md:table-cell`,e.cwd||`-`),t(`td`,`px-2 py-2.5`,t(`span`,`inline-flex items-center gap-1.5 whitespace-nowrap`,t(`span`,`inline-block h-2 w-2 flex-none rounded-full ${e.state===`streaming`?`animate-pulse bg-green-500`:`bg-neutral-300`}`),e.state)),t(`td`,`px-2 py-2.5 text-neutral-500`,l(e.stateSince))),r.append(n)}n.append(r),e.append(n),h.sessions.length||e.append(t(`p`,`p-4 text-[13px] text-neutral-400`,m===`active`?`No active sessions.`:`No sessions in the last 24 hours.`))}function b(e){let n=new Map,r=[],i=(e,t,r,i)=>{n.has(e)||n.set(e,{id:e,label:t,kind:r,state:i})};for(let e of h.sessions)i(e.id,e.title??e.id.slice(0,10),`session`,e.state);for(let e of h.runs){let t=e.invokedBySessionId??`scheduler`,n=e.targetSessionId??(e.context.definition.action.type===`bash`?`process:${e.id}`:`task:${e.id}`);i(t,t===`scheduler`?`Scheduler`:t.slice(0,10),t===`scheduler`?`scheduler`:`session`),i(n,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`),r.push({from:t,to:n,run:e,kind:`invocation`}),e.callbackSessionId&&(i(e.callbackSessionId,e.callbackSessionId.slice(0,10),`session`),r.push({from:n,to:e.callbackSessionId,run:e,kind:`callback`}))}for(let e of h.messages){if(!e.fromSessionId||!e.toSessionId||e.fromSessionId===e.toSessionId)continue;let t=h.runs.find(t=>t.id===e.runId);if(t){for(let t of[e.fromSessionId,e.toSessionId])i(t,t===`console`?`Console`:t.slice(0,10),t===`console`?`console`:`session`);r.push({from:e.fromSessionId,to:e.toSessionId,run:t,kind:`message`})}}if(!n.size){e.append(t(`p`,`p-4 text-[13px] text-neutral-400`,m===`active`?`No active dependencies.`:`No dependencies in the last 24 hours.`));return}let a=[...n.values()],o=new Set(r.map(e=>e.to)),s=new Map,l=a.filter(e=>!o.has(e.id)).map(e=>e.id);l.length||l.push(a[0].id);for(let e of l)s.set(e,0);for(;l.length;){let e=l.shift();for(let t of r)t.from!==e||s.has(t.to)||(s.set(t.to,s.get(e)+1),l.push(t.to))}for(let e of a)s.has(e.id)||s.set(e.id,0);let p=new Map;for(let e of a){let t=s.get(e.id);p.set(t,[...p.get(t)??[],e])}let g=Math.max(...p.keys())+1,_=Math.max(...[...p.values()].map(e=>e.length)),v=r.some(e=>e.from!==e.to&&s.get(e.from)===s.get(e.to))?40:0,y=56+g*180+(g-1)*110+v,b=56+_*46+(_-1)*30,x=new Map;for(let[e,t]of p)t.forEach((t,n)=>x.set(t.id,{x:28+e*290+90,y:28+n*76+23}));let S=c(`svg`,{width:String(y),height:String(b),viewBox:`0 0 ${y} ${b}`,class:`block`}),C=c(`defs`),w=d();for(let[e,t]of[[`activity-arrow`,w.edge],[`activity-arrow-cb`,w.callback],[`activity-arrow-msg`,w.message]]){let n=c(`marker`,{id:e,viewBox:`0 0 10 10`,refX:`9`,refY:`5`,markerWidth:`6`,markerHeight:`6`,orient:`auto-start-reverse`});n.append(c(`path`,{d:`M 0 0 L 10 5 L 0 10 z`,fill:t})),C.append(n)}S.append(C);let T=new Set;for(let e of r){let t=`${e.from}→${e.to}:${e.kind}`;if(T.has(t))continue;T.add(t);let n=x.get(e.from),r=x.get(e.to),i=c(`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`?w.callback:e.kind===`message`?w.message:w.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=()=>f(e.run.id),S.append(i)}let E=/[\u1100-\u11FF\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6]/,D=(e,t)=>{let n=0;for(let r=0;r<e.length;r++)if(n+=E.test(e[r])?2:1,n>t)return`${e.slice(0,r)}…`;return e};for(let e of a){let t=x.get(e.id),n=e.kind===`session`?e.state===`streaming`?w.streaming:w.idle:w[e.kind],r=c(`g`,{transform:`translate(${t.x},${t.y})`});e.kind===`session`&&r.classList.add(`cursor-pointer`),r.onclick=()=>{e.kind===`session`&&u(e.id)};let i=c(`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=c(`circle`,{cx:`-75`,cy:`0`,r:`3.5`,fill:n.stroke});if(e.state===`streaming`){let e=c(`animate`,{attributeName:`opacity`,values:`1;0.3;1`,dur:`1.6s`,repeatCount:`indefinite`});t.append(e)}r.append(t),a=-64}let o=c(`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=D(e.label,e.kind===`session`?22:25);let s=c(`title`);s.textContent=e.label===e.id?e.id:`${e.label}\n${e.id}`,r.append(o,s),S.append(r)}let O=e=>t(`span`,`inline-block h-2 w-2 rounded-full ${e}`);e.append(t(`div`,`w-max p-4`,S),t(`div`,`flex flex-wrap gap-x-5 gap-y-1 border-t border-neutral-200 px-4 py-2 text-[11px] text-neutral-500`,t(`span`,``,`Solid: task invocation`),t(`span`,`text-cyan-700`,`Dashed: callback`),t(`span`,`text-amber-700`,`Dotted: supervisor/control`),t(`span`,`inline-flex items-center gap-1.5`,O(`bg-emerald-500`),`streaming`),t(`span`,`inline-flex items-center gap-1.5`,O(`bg-neutral-300`),`idle`)))}let x=r(e,e=>{e===`dependencies`?p=`dependencies`:e===`sessions`&&(p=`sessions`),_()},()=>{e.replaceChildren(),h={sessions:[],runs:[],messages:[]},g=``});return window.addEventListener(`pier:theme`,()=>{x.visible&&v()}),Object.assign(x,{refresh(){x.visible&&_()}})}export{f as createActivityView};
|
|
Binary file
|
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{B as e,G as t,I as n,L as r,M as i,N as a,O as o,V as s,W as c,h as l}from"./index-
|
|
1
|
+
import{B as e,G as t,I as n,L as r,M as i,N as a,O as o,V as s,W as c,h as l}from"./index-DHxoYoeq.js";var u=e=>e.public&&e.token?`/p/${e.slug}-${e.token}/`:`/boards/${e.slug}/`;function d(d,f){let p=[],m=``,h=n(`header`,`sticky top-0 z-30 flex h-10 items-center gap-3 border-b border-neutral-200 bg-white px-4`,n(`span`,`font-medium max-md:hidden`,`Boards`)),g=n(`div`,`px-4 py-5`);d.append(n(`div`,`min-h-0 flex-1 overflow-y-auto`,h,g));async function _(n,r){let i=await t(`/api/boards/${n.slug}`,{public:r},`PATCH`);if(i.ok)return n.token=(await i.json().catch(()=>({}))).token??``,x();m=await e(i,`Could not change ${n.slug}`),await S()}async function v(e){m=await c(`/api/boards/${e}`,`DELETE`,`Could not delete ${e}`)??``,await S()}function y(e){let t=n(`div`,`group flex flex-col gap-1 border-b border-neutral-200/70 px-1 py-2.5 last:border-b-0`),i=document.createElement(`a`);i.className=`truncate font-medium text-neutral-800 hover:text-indigo-700`,i.href=u(e),i.target=`_blank`,i.rel=`noreferrer`,i.textContent=e.title;let s=n(`div`,`flex items-center gap-2`,i,n(`span`,`flex-none font-mono text-[11.5px] text-neutral-400`,e.slug)),c=o(``,``,e.public,t=>{e.public=t,_(e,t)});c.className=`ml-auto flex flex-none cursor-pointer items-center gap-1.5 text-[11.5px] text-neutral-500`,c.append(n(`span`,``,`Public`));let d=l(`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(e.slug);let p=a(`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(e)}`);p.title=`Copy the board's link`,s.append(c,p,d),t.append(s);let m=n(`div`,`flex flex-wrap items-center gap-x-3 gap-y-1 text-[11.5px] text-neutral-500`);e.description&&m.append(n(`span`,`min-w-0 truncate`,e.description)),m.append(n(`span`,`flex-none text-neutral-400`,r(Date.parse(e.updatedAt))));for(let t of e.sessions){let e=l(t.slice(0,8),`flex-none cursor-pointer font-mono text-indigo-600 hover:underline`);e.title=`Open session ${t}`,e.onclick=()=>f(t),m.append(e)}if(e.public&&e.token){let t=n(`span`,`flex-none font-mono text-neutral-400`,u(e));t.title=`Public URL — anyone holding this link can read the board, no password`,m.append(t)}return t.append(m),t}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=n(`div`,`mx-auto flex max-w-3xl flex-col`);p.length===0&&e.append(n(`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:r,wanted:i}of b){let a=p.filter(e=>e.public===i);if(a.length===0)continue;let o=n(`section`,`mb-6`);o.append(n(`h2`,`text-[13px] font-semibold text-neutral-700`,t)),o.append(n(`p`,`mb-1 text-[11.5px] leading-snug text-neutral-500`,r));let s=n(`div`,`rounded-xl border border-neutral-200 bg-white px-3`);s.append(...a.map(y)),o.append(s),e.append(o)}g.replaceChildren(...m?[n(`p`,`mb-3 text-[13px] text-red-600`,m)]:[],e)}async function S(){let e=await s(`/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
|