@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.
Files changed (85) hide show
  1. package/dist/agent/config-sync.js +123 -0
  2. package/dist/agent/config.js +95 -3
  3. package/dist/agent/events.js +10 -1
  4. package/dist/agent/pi.js +50 -28
  5. package/dist/config-sync-fetch.js +84 -0
  6. package/dist/config-sync-task.js +59 -0
  7. package/dist/config-sync.js +179 -0
  8. package/dist/core/hub.js +10 -3
  9. package/dist/core/reply.js +12 -2
  10. package/dist/core/router.js +162 -37
  11. package/dist/db.js +6 -0
  12. package/dist/main.js +31 -1
  13. package/dist/tasks/agent.js +11 -22
  14. package/dist/tasks/callbacks.js +1 -1
  15. package/dist/tasks/definitions.js +25 -16
  16. package/dist/tasks/execution.js +11 -3
  17. package/dist/tasks/messages.js +57 -14
  18. package/dist/tasks/routes.js +58 -10
  19. package/dist/tasks/runs.js +0 -5
  20. package/dist/tasks/service.js +9 -4
  21. package/dist/tasks/store.js +45 -0
  22. package/dist/tasks/tool.js +8 -4
  23. package/dist/web/config-sync.js +91 -0
  24. package/dist/web/public/assets/activity-Cw0LRVNI.js +5 -0
  25. package/dist/web/public/assets/activity-Cw0LRVNI.js.br +0 -0
  26. package/dist/web/public/assets/activity-Cw0LRVNI.js.gz +0 -0
  27. package/dist/web/public/assets/{boards-DEMQ51m1.js → boards-DMVbV84c.js} +1 -1
  28. package/dist/web/public/assets/boards-DMVbV84c.js.br +0 -0
  29. package/dist/web/public/assets/boards-DMVbV84c.js.gz +0 -0
  30. package/dist/web/public/assets/{code-DjWvqL94.js → code-C86xKk2k.js} +1 -1
  31. package/dist/web/public/assets/{explorer-BTGQ6Syc.js → explorer-DTvsy5Rs.js} +1 -1
  32. package/dist/web/public/assets/explorer-DTvsy5Rs.js.br +0 -0
  33. package/dist/web/public/assets/explorer-DTvsy5Rs.js.gz +0 -0
  34. package/dist/web/public/assets/{ghostty-web-D8CLFi_l.js → ghostty-web-CvSZragH.js} +1 -1
  35. package/dist/web/public/assets/ghostty-web-CvSZragH.js.br +0 -0
  36. package/dist/web/public/assets/ghostty-web-CvSZragH.js.gz +0 -0
  37. package/dist/web/public/assets/index-DHxoYoeq.js +79 -0
  38. package/dist/web/public/assets/index-DHxoYoeq.js.br +0 -0
  39. package/dist/web/public/assets/index-DHxoYoeq.js.gz +0 -0
  40. package/dist/web/public/assets/{index-yNocr797.css → index-DuZ2vERd.css} +1 -1
  41. package/dist/web/public/assets/index-DuZ2vERd.css.br +0 -0
  42. package/dist/web/public/assets/index-DuZ2vERd.css.gz +0 -0
  43. package/dist/web/public/assets/runs-jY40JBP-.js +1 -0
  44. package/dist/web/public/assets/runs-jY40JBP-.js.br +0 -0
  45. package/dist/web/public/assets/runs-jY40JBP-.js.gz +0 -0
  46. package/dist/web/public/assets/settings-Dux7PIKF.js +6 -0
  47. package/dist/web/public/assets/settings-Dux7PIKF.js.br +0 -0
  48. package/dist/web/public/assets/settings-Dux7PIKF.js.gz +0 -0
  49. package/dist/web/public/assets/task-runs-DKvtBVxS.js +3 -0
  50. package/dist/web/public/assets/task-runs-DKvtBVxS.js.br +0 -0
  51. package/dist/web/public/assets/task-runs-DKvtBVxS.js.gz +0 -0
  52. package/dist/web/public/assets/tasks-CqfrOVIt.js +4 -0
  53. package/dist/web/public/assets/tasks-CqfrOVIt.js.br +0 -0
  54. package/dist/web/public/assets/tasks-CqfrOVIt.js.gz +0 -0
  55. package/dist/web/public/assets/{terminal-zLyOvGto.js → terminal-DL_0yEy7.js} +2 -2
  56. package/dist/web/public/assets/terminal-DL_0yEy7.js.br +0 -0
  57. package/dist/web/public/assets/terminal-DL_0yEy7.js.gz +0 -0
  58. package/dist/web/public/index.html +13 -2
  59. package/dist/web/public/index.html.br +0 -0
  60. package/dist/web/public/index.html.gz +0 -0
  61. package/dist/web/server.js +65 -60
  62. package/package.json +2 -2
  63. package/skills/pier-tasks/SKILL.md +14 -9
  64. package/dist/web/public/assets/activity-BsoWK6vt.js +0 -5
  65. package/dist/web/public/assets/activity-BsoWK6vt.js.br +0 -0
  66. package/dist/web/public/assets/activity-BsoWK6vt.js.gz +0 -0
  67. package/dist/web/public/assets/boards-DEMQ51m1.js.br +0 -0
  68. package/dist/web/public/assets/boards-DEMQ51m1.js.gz +0 -0
  69. package/dist/web/public/assets/explorer-BTGQ6Syc.js.br +0 -0
  70. package/dist/web/public/assets/explorer-BTGQ6Syc.js.gz +0 -0
  71. package/dist/web/public/assets/ghostty-web-D8CLFi_l.js.br +0 -0
  72. package/dist/web/public/assets/ghostty-web-D8CLFi_l.js.gz +0 -0
  73. package/dist/web/public/assets/index-BFzmvpGX.js +0 -78
  74. package/dist/web/public/assets/index-BFzmvpGX.js.br +0 -0
  75. package/dist/web/public/assets/index-BFzmvpGX.js.gz +0 -0
  76. package/dist/web/public/assets/index-yNocr797.css.br +0 -0
  77. package/dist/web/public/assets/index-yNocr797.css.gz +0 -0
  78. package/dist/web/public/assets/settings-DSTKXN2o.js +0 -6
  79. package/dist/web/public/assets/settings-DSTKXN2o.js.br +0 -0
  80. package/dist/web/public/assets/settings-DSTKXN2o.js.gz +0 -0
  81. package/dist/web/public/assets/tasks-Dy1SCAE_.js +0 -4
  82. package/dist/web/public/assets/tasks-Dy1SCAE_.js.br +0 -0
  83. package/dist/web/public/assets/tasks-Dy1SCAE_.js.gz +0 -0
  84. package/dist/web/public/assets/terminal-zLyOvGto.js.br +0 -0
  85. 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
- constructor(store, factory, router, hub) {
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
- async parseDraft(raw) {
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, trigger);
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, trigger) {
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":"fork"} or {"mode":"reuse","sessionId":"..."}');
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 or fork sessions");
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) {
@@ -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), abortable while it goes, settled
3
- // exactly once. What the two action kinds actually do lives in command.ts and
4
- // agent.ts; this file owns only the lifecycle they share.
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);
@@ -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
- this.resumeRun(run.id, this.format(reply, run), fromSessionId);
152
- reply.state = "delivered";
153
- reply.deliveredAt = Date.now();
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
- /** A decision steers a follow-up would land only after the supervisor runs
251
- * out of tool calls, leaving the child waiting out the whole turn. */
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 === "steer" || message.kind === "decision" ? "steer" : "follow_up";
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 Pi's queue for the running turn to drain it:
289
- // not recorded yet, and sending again would deliver the same steer twice.
290
- // Unlike a callback this is not deferred a steer's whole point is to
291
- // reach the turn already running so the queue is where it sits, and
292
- // waiting for it to drain costs no attempt.
293
- const queue = await session.pendingQueue();
294
- if ([...queue.steering, ...queue.followUp].some((text) => text.includes(message.id)))
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") {
@@ -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 = kind ? rows.filter((task) => task.kind === kind) : rows.filter((task) => task.kind !== "subagent");
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
- const run = tasks.getRun(c.req.param("id"));
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
- const sessionMode = body?.sessionMode === "fresh" || body?.sessionMode === "fork" ? body.sessionMode : undefined;
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,
@@ -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);
@@ -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
  }
@@ -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
  }
@@ -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 support reused, fresh, or forked sessions. 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).",
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", "fork")),
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
- const sessionMode = input.session_mode === "fresh" || input.session_mode === "fork" ? input.session_mode : undefined;
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};
@@ -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-BFzmvpGX.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};
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};