@timqi/pier 0.0.27 → 0.0.29

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 (72) hide show
  1. package/dist/agent/config-sync.js +39 -8
  2. package/dist/agent/config.js +72 -15
  3. package/dist/agent/events.js +24 -4
  4. package/dist/core/identity.js +17 -1
  5. package/dist/core/router.js +10 -0
  6. package/dist/db.js +27 -0
  7. package/dist/main.js +1 -0
  8. package/dist/settings.js +4 -1
  9. package/dist/tasks/service.js +4 -0
  10. package/dist/tasks/store.js +12 -0
  11. package/dist/tools.js +17 -3
  12. package/dist/web/public/assets/{activity-B44nbhXy.js → activity-D3m4L2IL.js} +2 -2
  13. package/dist/web/public/assets/activity-D3m4L2IL.js.br +0 -0
  14. package/dist/web/public/assets/activity-D3m4L2IL.js.gz +0 -0
  15. package/dist/web/public/assets/boards-BIObcQeX.js +1 -0
  16. package/dist/web/public/assets/boards-BIObcQeX.js.br +0 -0
  17. package/dist/web/public/assets/boards-BIObcQeX.js.gz +0 -0
  18. package/dist/web/public/assets/explorer-C_rSWPNB.js +4 -0
  19. package/dist/web/public/assets/explorer-C_rSWPNB.js.br +0 -0
  20. package/dist/web/public/assets/explorer-C_rSWPNB.js.gz +0 -0
  21. package/dist/web/public/assets/index-CX3fYZY5.css +2 -0
  22. package/dist/web/public/assets/index-CX3fYZY5.css.br +0 -0
  23. package/dist/web/public/assets/index-CX3fYZY5.css.gz +0 -0
  24. package/dist/web/public/assets/index-uFsZkKOQ.js +85 -0
  25. package/dist/web/public/assets/index-uFsZkKOQ.js.br +0 -0
  26. package/dist/web/public/assets/index-uFsZkKOQ.js.gz +0 -0
  27. package/dist/web/public/assets/runs-Ch6DZq6O.js +1 -0
  28. package/dist/web/public/assets/runs-Ch6DZq6O.js.br +0 -0
  29. package/dist/web/public/assets/runs-Ch6DZq6O.js.gz +0 -0
  30. package/dist/web/public/assets/settings-BWcEIEcv.js +5 -0
  31. package/dist/web/public/assets/settings-BWcEIEcv.js.br +0 -0
  32. package/dist/web/public/assets/settings-BWcEIEcv.js.gz +0 -0
  33. package/dist/web/public/assets/{task-runs-BW06ZEVz.js → task-runs-DPkwv2UE.js} +2 -2
  34. package/dist/web/public/assets/task-runs-DPkwv2UE.js.br +0 -0
  35. package/dist/web/public/assets/task-runs-DPkwv2UE.js.gz +0 -0
  36. package/dist/web/public/assets/tasks-DTiCi2mH.js +4 -0
  37. package/dist/web/public/assets/tasks-DTiCi2mH.js.br +0 -0
  38. package/dist/web/public/assets/tasks-DTiCi2mH.js.gz +0 -0
  39. package/dist/web/public/index.html +41 -27
  40. package/dist/web/public/index.html.br +0 -0
  41. package/dist/web/public/index.html.gz +0 -0
  42. package/dist/web/server.js +30 -76
  43. package/dist/web/session-state.js +55 -52
  44. package/package.json +1 -1
  45. package/skills/pier-tasks/SKILL.md +113 -79
  46. package/dist/web/public/assets/activity-B44nbhXy.js.br +0 -0
  47. package/dist/web/public/assets/activity-B44nbhXy.js.gz +0 -0
  48. package/dist/web/public/assets/boards-CeiKKBaY.js +0 -1
  49. package/dist/web/public/assets/boards-CeiKKBaY.js.br +0 -0
  50. package/dist/web/public/assets/boards-CeiKKBaY.js.gz +0 -0
  51. package/dist/web/public/assets/code-I0lxXuvE.js +0 -1
  52. package/dist/web/public/assets/explorer-BOeDogsY.js +0 -5
  53. package/dist/web/public/assets/explorer-BOeDogsY.js.br +0 -0
  54. package/dist/web/public/assets/explorer-BOeDogsY.js.gz +0 -0
  55. package/dist/web/public/assets/index-BmKvoulH.js +0 -84
  56. package/dist/web/public/assets/index-BmKvoulH.js.br +0 -0
  57. package/dist/web/public/assets/index-BmKvoulH.js.gz +0 -0
  58. package/dist/web/public/assets/index-CZeL3DfU.css +0 -2
  59. package/dist/web/public/assets/index-CZeL3DfU.css.br +0 -0
  60. package/dist/web/public/assets/index-CZeL3DfU.css.gz +0 -0
  61. package/dist/web/public/assets/runs-C4OlQT7W.js +0 -1
  62. package/dist/web/public/assets/runs-C4OlQT7W.js.br +0 -0
  63. package/dist/web/public/assets/runs-C4OlQT7W.js.gz +0 -0
  64. package/dist/web/public/assets/settings-Cn8i_QiP.js +0 -6
  65. package/dist/web/public/assets/settings-Cn8i_QiP.js.br +0 -0
  66. package/dist/web/public/assets/settings-Cn8i_QiP.js.gz +0 -0
  67. package/dist/web/public/assets/task-runs-BW06ZEVz.js.br +0 -0
  68. package/dist/web/public/assets/task-runs-BW06ZEVz.js.gz +0 -0
  69. package/dist/web/public/assets/tasks-DZzYlKsd.js +0 -4
  70. package/dist/web/public/assets/tasks-DZzYlKsd.js.br +0 -0
  71. package/dist/web/public/assets/tasks-DZzYlKsd.js.gz +0 -0
  72. package/dist/web/repos.js +0 -75
@@ -1,4 +1,5 @@
1
1
  // Credential-blind projection of the portable agent configuration.
2
+ import { isThinkingLevel } from "../core/types.js";
2
3
  /** Never leaves the instance, at any depth of a models.json provider: the
3
4
  * credentials, and the endpoint they authenticate against — a sharing link is
4
5
  * public. Every other field is metadata Pi's own schema already validates, so
@@ -52,20 +53,30 @@ function project(value, where, onPrivate) {
52
53
  return [[key, project(entry, `${where} ${key}`, onPrivate)]];
53
54
  }));
54
55
  }
56
+ /** A provider id that cannot address anything but its own record. */
57
+ function providerId(id) {
58
+ if (typeof id !== "string" || id.length > 100 || !/^[a-z0-9][a-z0-9._-]*$/.test(id) ||
59
+ ["constructor", "prototype"].includes(id)) {
60
+ throw new Error("invalid snapshot provider id");
61
+ }
62
+ return id;
63
+ }
64
+ /** A model id both sides can address — in a catalog or as the default. */
65
+ function modelId(id) {
66
+ if (typeof id !== "string" || !id.trim() || id !== id.trim() || id.length > 100) {
67
+ throw new Error("model id must be a non-empty trimmed string");
68
+ }
69
+ return id;
70
+ }
55
71
  /** The shape both sides must agree on: a provider id that cannot address
56
72
  * anything but its own record, and a catalog addressable by model id. */
57
73
  function portable(id, value, onPrivate) {
58
- if (id.length > 100 || !/^[a-z0-9][a-z0-9._-]*$/.test(id) || ["constructor", "prototype"].includes(id)) {
59
- throw new Error("invalid snapshot provider id");
60
- }
74
+ providerId(id);
61
75
  const provider = record(project(value, `provider ${id}`, onPrivate), `provider ${id}`);
62
76
  const models = provider.models ?? [];
63
77
  if (!Array.isArray(models))
64
78
  throw new Error("models must be an array");
65
- const ids = models.map((model) => record(model, "model").id);
66
- if (ids.some((modelId) => typeof modelId !== "string" || !modelId.trim() || modelId !== modelId.trim())) {
67
- throw new Error("model id must be a non-empty trimmed string");
68
- }
79
+ const ids = models.map((model) => modelId(record(model, "model").id));
69
80
  if (new Set(ids).size !== ids.length)
70
81
  throw new Error("duplicate model ids");
71
82
  const overrides = provider.modelOverrides ?? {};
@@ -91,11 +102,28 @@ export function snapshotProviders(providers = {}) {
91
102
  }
92
103
  return shared;
93
104
  }
105
+ /** A default model names a provider and a model; the provider is not required
106
+ * to be in models.json, because Pi's own built-ins never are. */
107
+ function normalizeDefaultModel(raw) {
108
+ if (raw === null)
109
+ return null;
110
+ const ref = record(raw, "default model");
111
+ only(ref, ["provider", "id"], "default model");
112
+ return { provider: providerId(ref.provider), id: modelId(ref.id) };
113
+ }
114
+ /** A default reasoning effort is one of the levels Pi accepts, nothing else. */
115
+ function normalizeDefaultThinkingLevel(raw) {
116
+ if (raw === null)
117
+ return null;
118
+ if (!isThinkingLevel(raw))
119
+ throw new Error("default reasoning effort must be a level Pi accepts");
120
+ return raw;
121
+ }
94
122
  /** Strict import boundary: a source that states a private field is refused,
95
123
  * never trusted as a mask over the local one. */
96
124
  export function normalizeAgentSnapshot(raw) {
97
125
  const snapshot = record(raw, "agent snapshot");
98
- only(snapshot, ["files", "providers"], "agent snapshot");
126
+ only(snapshot, ["files", "providers", "defaultModel", "defaultThinkingLevel"], "agent snapshot");
99
127
  const files = record(snapshot.files, "snapshot files");
100
128
  only(files, ["SYSTEM.md", "AGENTS.md"], "snapshot files");
101
129
  for (const name of ["SYSTEM.md", "AGENTS.md"]) {
@@ -110,6 +138,9 @@ export function normalizeAgentSnapshot(raw) {
110
138
  return {
111
139
  files: { "SYSTEM.md": files["SYSTEM.md"], "AGENTS.md": files["AGENTS.md"] },
112
140
  providers,
141
+ ...(snapshot.defaultModel !== undefined ? { defaultModel: normalizeDefaultModel(snapshot.defaultModel) } : {}),
142
+ ...(snapshot.defaultThinkingLevel !== undefined
143
+ ? { defaultThinkingLevel: normalizeDefaultThinkingLevel(snapshot.defaultThinkingLevel) } : {}),
113
144
  };
114
145
  }
115
146
  /** A model or override arrives whole; only the local private fields survive it. */
@@ -9,7 +9,10 @@ import { pierPath } from "../paths.js";
9
9
  import { mergeSnapshotProviders, normalizeAgentSnapshot, snapshotProviders } from "./config-sync.js";
10
10
  const GLOBAL_FILES = ["SYSTEM.md", "AGENTS.md", "settings.json", "models.json"];
11
11
  const PROJECT_FILES = ["AGENTS.md"];
12
- const SNAPSHOT_FILES = ["SYSTEM.md", "AGENTS.md", "models.json"];
12
+ // settings.json is on the list for two fields: the default model and its
13
+ // reasoning effort. Everything else in it is machine-local and survives an
14
+ // import untouched.
15
+ const SNAPSHOT_FILES = ["SYSTEM.md", "AGENTS.md", "models.json", "settings.json"];
13
16
  const RESOURCE_DEPTH = 3; // extensions/skills nest at most a couple of levels
14
17
  /** Pier owns the Pi runtime dir: config lives in the syncable `~/.pier/pi`
15
18
  * repo, not `~/.pi`. main.ts exports this as PI_CODING_AGENT_DIR so the SDK's
@@ -211,13 +214,18 @@ export class PiConfigStore {
211
214
  }
212
215
  exportSnapshot() {
213
216
  return this.#withWrite(async () => {
214
- const [system, agents, raw] = await Promise.all(SNAPSHOT_FILES.map((name) => readNullable(join(this.agentDir, name))));
217
+ const [system, agents, raw, rawSettings] = await Promise.all(SNAPSHOT_FILES.map((name) => readNullable(join(this.agentDir, name))));
215
218
  const parsed = raw?.trim() ? parseModels(raw) : {};
216
219
  if (!parsed)
217
220
  throw new Error("models.json must be valid JSON before exporting configuration");
221
+ const settings = readSettings(rawSettings);
218
222
  return normalizeAgentSnapshot({
219
223
  files: { "SYSTEM.md": system, "AGENTS.md": agents },
220
224
  providers: snapshotProviders(parsed.providers),
225
+ defaultModel: defaultModelRef(settings),
226
+ // Unlike the pair, a lone level needs no reading of its own; the
227
+ // snapshot boundary is the one place that says which ones exist.
228
+ defaultThinkingLevel: settings.defaultThinkingLevel ?? null,
221
229
  });
222
230
  });
223
231
  }
@@ -233,7 +241,8 @@ export class PiConfigStore {
233
241
  const providers = mergeSnapshotProviders(parsed.providers, incoming.providers);
234
242
  const models = JSON.stringify(providers) === JSON.stringify(parsed.providers ?? {})
235
243
  ? raw : `${JSON.stringify({ ...parsed, providers }, null, 2)}\n`;
236
- const after = [incoming.files["SYSTEM.md"], incoming.files["AGENTS.md"], models];
244
+ const settings = withDefaults(before[3], incoming);
245
+ const after = [incoming.files["SYSTEM.md"], incoming.files["AGENTS.md"], models, settings];
237
246
  const changes = names.flatMap((name, index) => before[index] === after[index] ? [] : [{
238
247
  path: join(this.agentDir, name), before: before[index], after: after[index],
239
248
  temp: join(this.agentDir, `${name}.${process.pid}.${randomUUID()}.tmp`),
@@ -450,21 +459,69 @@ function restoreHeaders(incoming, current) {
450
459
  headers[name] = original;
451
460
  }
452
461
  }
453
- function parseModels(raw) {
462
+ /** settings.json is read for the deployment defaults and never rewritten
463
+ * wholesale. Malformed JSON is refused rather than read as "no default":
464
+ * that would publish, or import, a cleared default nobody asked for. */
465
+ function readSettings(raw) {
466
+ if (!raw?.trim())
467
+ return {};
468
+ const parsed = parseObject(raw);
469
+ if (!parsed)
470
+ throw new Error("settings.json must be valid JSON before synchronizing configuration");
471
+ return parsed;
472
+ }
473
+ /** Pi writes defaultProvider and defaultModel as a pair; half of one is a hand
474
+ * edit that a shared default cannot represent, so it is refused. */
475
+ function defaultModelRef(settings) {
476
+ const provider = settings.defaultProvider;
477
+ const id = settings.defaultModel;
478
+ if (provider === undefined && id === undefined)
479
+ return null;
480
+ if (typeof provider !== "string" || typeof id !== "string") {
481
+ throw new Error("settings.json must name defaultProvider and defaultModel together as strings");
482
+ }
483
+ return { provider, id };
484
+ }
485
+ /** The incoming defaults replace their own fields and nothing else; a field
486
+ * the source never states (an older one) leaves the local settings.json
487
+ * alone, and `null` is a stated "no default" that clears it. Unchanged
488
+ * content returns the original bytes so the import stays a no-op. */
489
+ function withDefaults(raw, incoming) {
490
+ if (incoming.defaultModel === undefined && incoming.defaultThinkingLevel === undefined)
491
+ return raw;
492
+ const settings = readSettings(raw);
493
+ const next = { ...settings };
494
+ const set = (key, value) => {
495
+ if (value === undefined)
496
+ delete next[key];
497
+ else
498
+ next[key] = value;
499
+ };
500
+ if (incoming.defaultModel !== undefined) {
501
+ set("defaultProvider", incoming.defaultModel?.provider);
502
+ set("defaultModel", incoming.defaultModel?.id);
503
+ }
504
+ if (incoming.defaultThinkingLevel !== undefined) {
505
+ set("defaultThinkingLevel", incoming.defaultThinkingLevel ?? undefined);
506
+ }
507
+ return JSON.stringify(next) === JSON.stringify(settings) ? raw : `${JSON.stringify(next, null, 2)}\n`;
508
+ }
509
+ const parseObject = (raw) => {
454
510
  try {
455
- const parsed = JSON.parse(raw);
456
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
457
- return null;
458
- const providers = parsed.providers;
459
- if (providers !== undefined) {
460
- if (typeof providers !== "object" || providers === null || Array.isArray(providers))
461
- return null;
462
- if (Object.values(providers).some((provider) => typeof provider !== "object" || provider === null || Array.isArray(provider)))
463
- return null;
464
- }
465
- return parsed;
511
+ return asRecord(JSON.parse(raw)) ?? null;
466
512
  }
467
513
  catch {
468
514
  return null;
469
515
  }
516
+ };
517
+ function parseModels(raw) {
518
+ const parsed = parseObject(raw);
519
+ if (!parsed)
520
+ return null;
521
+ if (parsed.providers !== undefined) {
522
+ const providers = asRecord(parsed.providers);
523
+ if (!providers || Object.values(providers).some((provider) => !asRecord(provider)))
524
+ return null;
525
+ }
526
+ return parsed;
470
527
  }
@@ -3,6 +3,7 @@
3
3
  // and Pi types never leak past the seam. The golden-table test in
4
4
  // events.test.ts is the mapping's spec; extend types.ts before adding events.
5
5
  import { isThinkingLevel, MAX_STEP_OUTPUT } from "../core/types.js";
6
+ const hasToolCalls = (message) => Array.isArray(message?.content) && message.content.some((part) => part.type === "toolCall");
6
7
  export function textOf(content) {
7
8
  if (typeof content === "string")
8
9
  return content;
@@ -109,6 +110,7 @@ export function toChatTurns(messages) {
109
110
  const turns = [];
110
111
  let steps = []; // activity seen since the last emitted turn
111
112
  const pendingTools = new Map();
113
+ let candidate; // text-only reply, until another assistant message follows
112
114
  const flush = (role, text, meta, origin, at) => {
113
115
  const turn = { role, text };
114
116
  if (meta)
@@ -142,6 +144,7 @@ export function toChatTurns(messages) {
142
144
  }
143
145
  const origin = systemOrigin(m);
144
146
  if (origin) {
147
+ candidate = undefined;
145
148
  const text = textOf(m.content);
146
149
  if (text)
147
150
  flush("system", text, undefined, origin, m.timestamp);
@@ -149,9 +152,22 @@ export function toChatTurns(messages) {
149
152
  }
150
153
  if (m.role !== "user" && m.role !== "assistant")
151
154
  continue;
155
+ if (m.role === "user")
156
+ candidate = undefined;
157
+ else if (candidate) {
158
+ // Some providers separate commentary from the following tool-call message.
159
+ turns.pop();
160
+ steps = candidate.steps ?? [];
161
+ steps.push({ kind: "progress", text: candidate.text });
162
+ candidate = undefined;
163
+ }
164
+ const hasTools = m.role === "assistant" && hasToolCalls(m);
152
165
  if (m.role === "assistant" && Array.isArray(m.content)) {
153
166
  for (const part of m.content) {
154
- if (part.type === "thinking" && part.thinking) {
167
+ if (hasTools && part.type === "text" && part.text) {
168
+ steps.push({ kind: "progress", text: part.text });
169
+ }
170
+ else if (part.type === "thinking" && part.thinking) {
155
171
  steps.push({ kind: "thinking", text: part.thinking });
156
172
  }
157
173
  else if (part.type === "toolCall") {
@@ -168,10 +184,12 @@ export function toChatTurns(messages) {
168
184
  }
169
185
  }
170
186
  const text = textOf(m.content);
171
- // step-only assistant messages keep buffering activity
172
- if (!text)
187
+ // Tool-bearing messages are intermediate work, even when they include text.
188
+ if (!text || hasTools)
173
189
  continue;
174
190
  flush(m.role, text, m.role === "assistant" ? turnMetaAt(messages, i) : undefined, undefined, m.timestamp);
191
+ if (m.role === "assistant")
192
+ candidate = turns[turns.length - 1];
175
193
  }
176
194
  // Activity with no answer after it (aborted run) still belongs on the page.
177
195
  if (steps.length)
@@ -199,7 +217,7 @@ export function toSessionEvents(e) {
199
217
  ? final.errorMessage || "unknown agent error"
200
218
  : undefined;
201
219
  const out = [
202
- { type: "turn-end", text: textOf(final?.content), ...(failure ? { error: failure } : {}) },
220
+ { type: "turn-end", text: hasToolCalls(final) ? "" : textOf(final?.content), ...(failure ? { error: failure } : {}) },
203
221
  ];
204
222
  if (failure)
205
223
  out.push({ type: "error", message: failure });
@@ -221,6 +239,8 @@ export function toSessionEvents(e) {
221
239
  const m = e.message;
222
240
  if (!m)
223
241
  return [];
242
+ if (m.role === "assistant")
243
+ return [{ type: "text-start" }];
224
244
  const origin = systemOrigin(m);
225
245
  const text = textOf(m.content);
226
246
  if (origin)
@@ -90,6 +90,12 @@ export function splitSpeaker(text) {
90
90
  text: text.slice(m[0].length),
91
91
  };
92
92
  }
93
+ /** Trailing lines that open a `[name](file://` link — or a last line that is
94
+ * only `[name` because the clip fell inside it. */
95
+ const ATTACHMENT_LINES = /(?:\n\[[^\]\n]*(?:\]\(\s*<?file:\/\/[^\n]*|$))+$/;
96
+ /** A long code span that opens the message, when something else follows it.
97
+ * Long, because `npm test` before "fails" is the subject, not a paste. */
98
+ const LEADING_CODE_SPAN = /^\s*`[^`\n]{40,}`\s+(\S[\s\S]*)$/;
93
99
  /**
94
100
  * A session titled by its first prompt inherits that prompt's header, and the
95
101
  * header is for the model: anything a person reads — a list row, a session
@@ -97,6 +103,11 @@ export function splitSpeaker(text) {
97
103
  * the workbench ever opened. So the speaker comes off and what they said is
98
104
  * the title. Here rather than in a UI module because the push notification
99
105
  * needs the same answer and a second copy of this would drift (AGENTS.md §3).
106
+ *
107
+ * Two more things a first prompt carries that a title should not: the
108
+ * attachment lines a channel appended (`[name](file:///…)`, often cut mid-name
109
+ * by the title clip), and a pasted log line in backticks ahead of the actual
110
+ * question — dropped only when a question follows; a paste alone stays.
100
111
  */
101
112
  export function readableTitle(title) {
102
113
  if (!title)
@@ -106,5 +117,10 @@ export function readableTitle(title) {
106
117
  // change what the sidebar's search is matching against for nothing.
107
118
  if (text === title)
108
119
  return title;
109
- return text.replace(/\s+/g, " ").trim() || title;
120
+ const said = text
121
+ .replace(ATTACHMENT_LINES, "")
122
+ .replace(LEADING_CODE_SPAN, "$1")
123
+ .replace(/\s+/g, " ")
124
+ .trim();
125
+ return said || title;
110
126
  }
@@ -78,6 +78,7 @@ export class Router {
78
78
  * the process exits when the drain ends — `endDrain` exists for the one
79
79
  * caller that drains *speculatively* and may not get to exit. */
80
80
  draining = false;
81
+ spokenTo;
81
82
  constructor(hub,
82
83
  /** Create or resume the session owning a conversation (wired in main.ts). */
83
84
  resolve) {
@@ -87,6 +88,14 @@ export class Router {
87
88
  registerChannel(channel) {
88
89
  this.channels.set(channel.id, channel);
89
90
  }
91
+ /** Told which session a human just spoke to — every message from a chat or
92
+ * the workbench passes through `dispatch`, and nothing a task or a subagent
93
+ * starts does. Registered rather than a constructor argument: the one
94
+ * listener is the rail's working set (web/session-state.ts), built with the
95
+ * web surface long after the router. */
96
+ onSpokenTo(listener) {
97
+ this.spokenTo = listener;
98
+ }
90
99
  /**
91
100
  * Tell the conversation something went wrong, then the event stream.
92
101
  *
@@ -602,6 +611,7 @@ export class Router {
602
611
  // closed must not start the turn the drain just declared finished with.
603
612
  if (this.draining)
604
613
  this.refuseDraining(msg.key);
614
+ this.spokenTo?.(session.id);
605
615
  const { action, text } = decide(msg, session.state);
606
616
  // A group chat is many people talking into one session; without a speaker
607
617
  // line the agent cannot tell them apart or mention anyone back. Emitted
package/dist/db.js CHANGED
@@ -322,6 +322,33 @@ const MIGRATIONS = [
322
322
  -- all of them: the query is by the delegating session, which lived only in
323
323
  -- the JSON — without this every session open was a full scan of task_runs.
324
324
  CREATE INDEX task_runs_invoked_by ON task_runs(json_extract(json, '$.invokedBySessionId'), queued_at DESC);
325
+ `,
326
+ // 19 — the rail is one flat list, and pinned means "stuck to the top".
327
+ `
328
+ -- pinned used to mean "listed under Projects", and every session created in
329
+ -- the workbench was. Now it means on top of the list, and nobody put a
330
+ -- historical session there: kept as-is, every web session ever made would
331
+ -- land in the pinned section. project_sort stays as a column nothing reads
332
+ -- — a SQLite column drop rewrites the table for a NULL nobody pays for.
333
+ UPDATE session_state SET pinned = 0;
334
+ `,
335
+ // 20 — the top of the rail is maintained, not arranged: no pin, no drag.
336
+ `
337
+ -- sort was the place a hand dragged a pinned row to; it is now the rank in
338
+ -- the working set the rail keeps on top, which a session enters by being
339
+ -- spoken to and leaves by being pushed out of the last slot. The pinned rows
340
+ -- are that set's first members — they are what somebody was working on — in
341
+ -- the order they were arranged in; never-dragged ones sorted first, so -1 is
342
+ -- the rank that keeps them there.
343
+ UPDATE session_state SET sort = -1 WHERE pinned = 1 AND sort IS NULL;
344
+ UPDATE session_state SET sort = NULL WHERE pinned = 0;
345
+ -- The set has a size (web/session-state.ts); more pins than that is a list
346
+ -- the promotion rule would never have built.
347
+ UPDATE session_state SET sort = NULL WHERE session_id IN (
348
+ SELECT session_id FROM session_state WHERE sort IS NOT NULL
349
+ ORDER BY sort, session_id LIMIT -1 OFFSET 8
350
+ );
351
+ ALTER TABLE session_state DROP COLUMN pinned;
325
352
  `,
326
353
  ];
327
354
  /**
package/dist/main.js CHANGED
@@ -382,6 +382,7 @@ app.route("/", createServer({
382
382
  reload: () => reloadInstance(true),
383
383
  backgroundRuns: (id) => tasks.backgroundRuns(id),
384
384
  activeBackgroundRunCounts: () => tasks.activeBackgroundRunCounts(),
385
+ taskSessions: () => tasks.taskSessions(),
385
386
  channelOf: (id) => conversations.channelOf(id),
386
387
  }));
387
388
  const port = Number(process.env.PORT ?? 3141);
package/dist/settings.js CHANGED
@@ -113,7 +113,10 @@ export class SettingsStore {
113
113
  autoUpdate: this.#value("autoUpdate") === "1",
114
114
  extensions: this.#json("extensions", normalizeExtensions, "a list of names") ?? [],
115
115
  tools: this.#json("tools", normalizeTools, "a list of names") ?? [],
116
- customTools: this.#json("customTools", normalizeCustomTools, "a list of {name, spec}") ?? [],
116
+ // `"drop"`: a stored row whose name the bundled catalog has since taken
117
+ // is redundant, not malformed — rejecting the setting over it would take
118
+ // every other tool declared beside it (see normalizeCustomTools).
119
+ customTools: this.#json("customTools", (raw) => normalizeCustomTools(raw, [], "drop"), "a list of {name, spec}") ?? [],
117
120
  };
118
121
  }
119
122
  /**
@@ -217,6 +217,10 @@ export class TaskService {
217
217
  activeBackgroundRunCounts() {
218
218
  return this.store.countActiveBackgroundRunsBySession();
219
219
  }
220
+ /** The sessions runs made for themselves, for a list of the operator's own. */
221
+ taskSessions() {
222
+ return this.store.taskOwnedSessionIds();
223
+ }
220
224
  run(taskId, input = null, source = "manual", parentRunId = null, provenance = {}) {
221
225
  this.refusePaused(parentRunId);
222
226
  const task = this.get(taskId);
@@ -143,6 +143,18 @@ export class TaskStore {
143
143
  `).all();
144
144
  return new Map(rows.map((row) => [row.session_id, row.n]));
145
145
  }
146
+ /** Every session a run created for itself — `fresh` is the one mode that
147
+ * makes a session rather than borrowing one, and a resumed run reuses the
148
+ * session its `fresh` predecessor already stamped. These are the agent's
149
+ * conversations with itself, which the rail does not list. */
150
+ taskOwnedSessionIds() {
151
+ const rows = this.sql(`
152
+ SELECT DISTINCT json_extract(json, '$.context.sessionId') AS id
153
+ FROM task_runs
154
+ WHERE json_extract(json, '$.sessionMode') = 'fresh' AND id IS NOT NULL
155
+ `).all();
156
+ return new Set(rows.map((row) => row.id));
157
+ }
146
158
  listRunsForSession(sessionId, limit = 50) {
147
159
  return this.#many(`
148
160
  SELECT json FROM task_runs
package/dist/tools.js CHANGED
@@ -127,7 +127,18 @@ export const CUSTOM_TOOL_RULES = `each custom tool needs a name (letters, digits
127
127
  export function normalizeCustomTools(raw,
128
128
  /** Names this instance already answers to that this file cannot see — the
129
129
  * bundled extensions live behind the Pi SDK, so main.ts hands them in. */
130
- reserved = []) {
130
+ reserved = [],
131
+ /**
132
+ * What a name Pier already manages means. At the route: a refusal, because
133
+ * the operator is declaring it now and can pick another. Reading a stored
134
+ * row: `"drop"`, because the catalog grew into that name *after* the row was
135
+ * written — jq shipped as a bundled tool and turned the operator's own jq
136
+ * row into a whole setting Pier refused, dropping every *other* tool
137
+ * declared beside it. The bundled row installs the same binary, so the
138
+ * entry is redundant rather than wrong. Left in the row on purpose: a Pier
139
+ * that stops bundling that name finds the declaration still there.
140
+ */
141
+ managedName = "reject") {
131
142
  // Case-insensitively: two names differing only in case are one filename on a
132
143
  // case-insensitive filesystem, and one switch whose meaning depends on which
133
144
  // ran last.
@@ -145,8 +156,11 @@ reserved = []) {
145
156
  const cleanName = name.trim();
146
157
  if (!TOOL_NAME.test(cleanName))
147
158
  return null;
148
- if (taken.has(cleanName.toLowerCase()))
149
- return null;
159
+ if (taken.has(cleanName.toLowerCase())) {
160
+ if (managedName === "reject")
161
+ return null;
162
+ continue;
163
+ }
150
164
  if (tools.some((tool) => tool.name.toLowerCase() === cleanName.toLowerCase()))
151
165
  return null;
152
166
  // The migration: a stored `{name, spec}` is the block it always meant.
@@ -1,5 +1,5 @@
1
- import{B as e,C as t,H as n,I as r,K as i,L as a,N as o,k as s,m as c,v as l,z as u}from"./index-BmKvoulH.js";var d=(e,t={})=>{let n=document.createElementNS(`http://www.w3.org/2000/svg`,e);for(let[e,r]of Object.entries(t))n.setAttribute(e,r);return n},f=e=>e===null?`-`:r(Date.now()-e),p={light:{edge:`#a3a3a3`,callback:`#0891b2`,message:`#d97706`,scheduler:{fill:`#f5f3ff`,stroke:`#a78bfa`,text:`#5b21b6`},console:{fill:`#eff6ff`,stroke:`#93c5fd`,text:`#1d4ed8`},task:{fill:`#fffbeb`,stroke:`#fbbf24`,text:`#92400e`},process:{fill:`#f5f5f5`,stroke:`#a3a3a3`,text:`#525252`,dash:`4 3`},streaming:{fill:`#ffffff`,stroke:`#10b981`,text:`#262626`},idle:{fill:`#fafafa`,stroke:`#d4d4d4`,text:`#737373`}},dark:{edge:`#7a7a7a`,callback:`#3fc3dd`,message:`#e0a13a`,scheduler:{fill:`#2b2440`,stroke:`#7d63c9`,text:`#c9b8f5`},console:{fill:`#1f2a3d`,stroke:`#4a7fbf`,text:`#a8c8f0`},task:{fill:`#3a2f1c`,stroke:`#b8862a`,text:`#f0d49a`},process:{fill:`#262626`,stroke:`#5c5c5c`,text:`#a3a3a3`,dash:`4 3`},streaming:{fill:`#22302a`,stroke:`#10b981`,text:`#e5e5e5`},idle:{fill:`#212121`,stroke:`#4a4a4a`,text:`#9a9a9a`}}},m=()=>p[document.documentElement.dataset.theme===`dark`?`dark`:`light`];function h(r,p,h){let g=`sessions`,_=`active`,v={sessions:[],runs:[],messages:[]},y=``,b=e(async()=>{let e=_,t=await n(`/api/activity?scope=${e}`,`Failed to load activity`);if(!t.ok){y=``,r.replaceChildren(a(`p`,`p-4 text-[13px] text-red-600`,t.error));return}if(e!==_)return;let o=JSON.stringify(t.value);if(`${e}:${o}`===y)return;y=`${e}:${o}`;let s=t.value;v={...s,sessions:s.sessions.map(e=>({...e,title:i(e.title)}))},x()});function x(){let e=s(t([[`Sessions`,`sessions`],[`Relationships`,`dependencies`]],g,e=>{g=e,x()}),a(`span`,`ml-auto text-[11.5px] text-neutral-400`,`${v.sessions.length} sessions · ${v.runs.length} runs`),t([[`Current`,`active`],[`Last 24h`,`recent`]],_,e=>{_=e,x(),b()})),n=a(`div`,`min-h-0 flex-1 overflow-auto`);g===`sessions`?S(n):w(n),r.replaceChildren(e,n)}function S(e){let t=document.createElement(`table`);t.className=`w-full table-fixed text-left text-[12.5px]`,t.innerHTML=`<thead class="sticky top-0 bg-neutral-50 text-[10.5px] uppercase tracking-wide text-neutral-400 shadow-[inset_0_-1px_0_var(--color-neutral-200)]"><tr>
1
+ import{A as e,B as t,L as n,P as r,R as i,U as a,V as o,h as s,q as c,w as l,y as u}from"./index-uFsZkKOQ.js";var d=(e,t={})=>{let n=document.createElementNS(`http://www.w3.org/2000/svg`,e);for(let[e,r]of Object.entries(t))n.setAttribute(e,r);return n},f=e=>e===null?`-`:n(Date.now()-e),p={light:{edge:`#a3a3a3`,callback:`#0891b2`,message:`#d97706`,scheduler:{fill:`#f5f3ff`,stroke:`#a78bfa`,text:`#5b21b6`},console:{fill:`#eff6ff`,stroke:`#93c5fd`,text:`#1d4ed8`},task:{fill:`#fffbeb`,stroke:`#fbbf24`,text:`#92400e`},process:{fill:`#f5f5f5`,stroke:`#a3a3a3`,text:`#525252`,dash:`4 3`},streaming:{fill:`#ffffff`,stroke:`#10b981`,text:`#262626`},idle:{fill:`#fafafa`,stroke:`#d4d4d4`,text:`#737373`}},dark:{edge:`#7a7a7a`,callback:`#3fc3dd`,message:`#e0a13a`,scheduler:{fill:`#2b2440`,stroke:`#7d63c9`,text:`#c9b8f5`},console:{fill:`#1f2a3d`,stroke:`#4a7fbf`,text:`#a8c8f0`},task:{fill:`#3a2f1c`,stroke:`#b8862a`,text:`#f0d49a`},process:{fill:`#262626`,stroke:`#5c5c5c`,text:`#a3a3a3`,dash:`4 3`},streaming:{fill:`#22302a`,stroke:`#10b981`,text:`#e5e5e5`},idle:{fill:`#212121`,stroke:`#4a4a4a`,text:`#9a9a9a`}}},m=()=>p[document.documentElement.dataset.theme===`dark`?`dark`:`light`];function h(n,p,h){let g=`sessions`,_=`active`,v={sessions:[],runs:[],messages:[]},y=``,b=o(async()=>{let e=_,t=await a(`/api/activity?scope=${e}`,`Failed to load activity`);if(!t.ok){y=``,n.replaceChildren(i(`p`,`p-4 text-[13px] text-red-600`,t.error));return}if(e!==_)return;let r=JSON.stringify(t.value);if(`${e}:${r}`===y)return;y=`${e}:${r}`;let o=t.value;v={...o,sessions:o.sessions.map(e=>({...e,title:c(e.title)}))},x()});function x(){let t=e(l([[`Sessions`,`sessions`],[`Relationships`,`dependencies`]],g,e=>{g=e,x()}),i(`span`,`ml-auto text-[11.5px] text-neutral-400`,`${v.sessions.length} sessions · ${v.runs.length} runs`),l([[`Current`,`active`],[`Last 24h`,`recent`]],_,e=>{_=e,x(),b()})),r=i(`div`,`min-h-0 flex-1 overflow-auto`);g===`sessions`?S(r):w(r),n.replaceChildren(t,r)}function S(e){let n=document.createElement(`table`);n.className=`w-full table-fixed text-left text-[12.5px]`,n.innerHTML=`<thead class="sticky top-0 bg-neutral-50 text-[10.5px] uppercase tracking-wide text-neutral-400 shadow-[inset_0_-1px_0_var(--color-neutral-200)]"><tr>
2
2
  <th class="w-[50%] px-4 py-2 font-semibold md:w-[34%]">Session</th>
3
3
  <th class="hidden w-[38%] px-2 py-2 font-semibold md:table-cell">Project</th>
4
4
  <th class="px-2 py-2 font-semibold md:w-[14%]">State</th>
5
- <th class="px-2 py-2 font-semibold">In state</th></tr></thead>`;let n=document.createElement(`tbody`);for(let e of v.sessions){let t=document.createElement(`tr`);t.className=`cursor-pointer border-b border-neutral-100 transition-colors hover:bg-neutral-50`,t.onclick=()=>p(e.id);let r=a(`button`,`block w-full cursor-pointer truncate text-left font-medium text-neutral-800`,e.title??u(e.cwd));r.setAttribute(`type`,`button`),r.title=`${e.id}\n${e.cwd}`,r.onclick=t=>{t.stopPropagation(),p(e.id)},t.append(a(`td`,`px-4 py-2.5`,r,a(`div`,`truncate font-mono text-[11px] text-neutral-400 md:hidden`,e.cwd||`-`)),a(`td`,`hidden truncate px-2 py-2.5 font-mono text-[11.5px] text-neutral-500 md:table-cell`,e.cwd||`-`),a(`td`,`px-2 py-2.5`,C(e.state)),a(`td`,`px-2 py-2.5 font-mono text-[11.5px] text-neutral-400`,f(e.stateSince))),n.append(t)}t.append(n),e.append(t),v.sessions.length||e.append(a(`div`,`p-4`,l(_===`active`?`No active sessions.`:`No sessions in the last 24 hours.`)))}function C(e){return e===`streaming`?c(e,`bg-emerald-50 text-emerald-700 ring-emerald-200`,`animate-pulse bg-emerald-500`):c(e,`bg-neutral-100 text-neutral-600 ring-neutral-200`,`bg-neutral-300`)}function w(e){let t=new Map,n=[],r=(e,n,r,i)=>{t.has(e)||t.set(e,{id:e,label:n,kind:r,state:i})};for(let e of v.sessions)r(e.id,e.title??e.id.slice(0,10),`session`,e.state);for(let e of v.runs){let t=e.invokedBySessionId??`scheduler`,i=e.targetSessionId??(e.context.definition.action.type===`bash`?`process:${e.id}`:`task:${e.id}`);r(t,t===`scheduler`?`Scheduler`:t.slice(0,10),t===`scheduler`?`scheduler`:`session`),r(i,e.targetSessionId?e.targetSessionId.slice(0,10):e.context.definition.action.type===`bash`?`Bash process`:`Task`,e.targetSessionId?`session`:e.context.definition.action.type===`bash`?`process`:`task`),n.push({from:t,to:i,run:e,kind:`invocation`}),e.callbackSessionId&&(r(e.callbackSessionId,e.callbackSessionId.slice(0,10),`session`),n.push({from:i,to:e.callbackSessionId,run:e,kind:`callback`}))}for(let e of v.messages){if(!e.fromSessionId||!e.toSessionId||e.fromSessionId===e.toSessionId)continue;let t=v.runs.find(t=>t.id===e.runId);if(t){for(let t of[e.fromSessionId,e.toSessionId])r(t,t===`console`?`Console`:t.slice(0,10),t===`console`?`console`:`session`);n.push({from:e.fromSessionId,to:e.toSessionId,run:t,kind:`message`})}}if(!t.size){e.append(a(`div`,`p-4`,l(_===`active`?`No active dependencies.`:`No dependencies in the last 24 hours.`)));return}let i=[...t.values()],o=new Set(n.map(e=>e.to)),s=new Map,c=i.filter(e=>!o.has(e.id)).map(e=>e.id);c.length||c.push(i[0].id);for(let e of c)s.set(e,0);for(;c.length;){let e=c.shift();for(let t of n)t.from!==e||s.has(t.to)||(s.set(t.to,s.get(e)+1),c.push(t.to))}for(let e of i)s.has(e.id)||s.set(e.id,0);let u=new Map;for(let e of i){let t=s.get(e.id);u.set(t,[...u.get(t)??[],e])}let f=Math.max(...u.keys())+1,g=Math.max(...[...u.values()].map(e=>e.length)),y=n.some(e=>e.from!==e.to&&s.get(e.from)===s.get(e.to))?40:0,b=56+f*180+(f-1)*110+y,x=56+g*46+(g-1)*30,S=new Map;for(let[e,t]of u)t.forEach((t,n)=>S.set(t.id,{x:28+e*290+90,y:28+n*76+23}));let C=d(`svg`,{width:String(b),height:String(x),viewBox:`0 0 ${b} ${x}`,class:`block`}),w=d(`defs`),T=m();for(let[e,t]of[[`activity-arrow`,T.edge],[`activity-arrow-cb`,T.callback],[`activity-arrow-msg`,T.message]]){let n=d(`marker`,{id:e,viewBox:`0 0 10 10`,refX:`9`,refY:`5`,markerWidth:`6`,markerHeight:`6`,orient:`auto-start-reverse`});n.append(d(`path`,{d:`M 0 0 L 10 5 L 0 10 z`,fill:t})),w.append(n)}C.append(w);let E=new Set;for(let e of n){let t=`${e.from}→${e.to}:${e.kind}`;if(E.has(t))continue;E.add(t);let n=S.get(e.from),r=S.get(e.to),i=d(`path`,{d:n.x===r.x?`M ${n.x+90} ${n.y} C ${n.x+90+46} ${n.y}, ${r.x+90+46} ${r.y}, ${r.x+90} ${r.y}`:(()=>{let e=r.x>n.x,t=n.x+(e?90:-90),i=r.x+(e?-90:90),a=Math.max(36,Math.abs(i-t)*.45)*(e?1:-1);return`M ${t} ${n.y} C ${t+a} ${n.y}, ${i-a} ${r.y}, ${i} ${r.y}`})(),fill:`none`,stroke:e.kind===`callback`?T.callback:e.kind===`message`?T.message:T.edge,"stroke-width":`1.5`,"marker-end":e.kind===`callback`?`url(#activity-arrow-cb)`:e.kind===`message`?`url(#activity-arrow-msg)`:`url(#activity-arrow)`});e.kind===`callback`&&i.setAttribute(`stroke-dasharray`,`6 5`),e.kind===`message`&&i.setAttribute(`stroke-dasharray`,`2 5`),i.classList.add(`cursor-pointer`),i.onclick=()=>h(e.run.id),C.append(i)}let D=/[\u1100-\u11FF\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6]/,O=(e,t)=>{let n=0;for(let r=0;r<e.length;r++)if(n+=D.test(e[r])?2:1,n>t)return`${e.slice(0,r)}…`;return e};for(let e of i){let t=S.get(e.id),n=e.kind===`session`?e.state===`streaming`?T.streaming:T.idle:T[e.kind],r=d(`g`,{transform:`translate(${t.x},${t.y})`});e.kind===`session`&&r.classList.add(`cursor-pointer`),r.onclick=()=>{e.kind===`session`&&p(e.id)};let i=d(`rect`,{x:`-90`,y:`-23`,width:`180`,height:`46`,rx:`8`,fill:n.fill,stroke:n.stroke,"stroke-width":e.kind===`session`&&e.state===`streaming`?`1.5`:`1`});n.dash&&i.setAttribute(`stroke-dasharray`,n.dash),r.append(i);let a=-78;if(e.kind===`session`){let t=d(`circle`,{cx:`-75`,cy:`0`,r:`3.5`,fill:n.stroke});if(e.state===`streaming`){let e=d(`animate`,{attributeName:`opacity`,values:`1;0.3;1`,dur:`1.6s`,repeatCount:`indefinite`});t.append(e)}r.append(t),a=-64}let o=d(`text`,{x:String(a),"text-anchor":`start`,"dominant-baseline":`middle`,"font-size":`11.5`,"font-weight":e.kind===`session`?`500`:`400`,fill:n.text});o.textContent=O(e.label,e.kind===`session`?22:25);let s=d(`title`);s.textContent=e.label===e.id?e.id:`${e.label}\n${e.id}`,r.append(o,s),C.append(r)}let k=e=>a(`span`,`inline-block h-2 w-2 rounded-full ${e}`),A=e=>a(`span`,`inline-block h-0 w-6 border-t-2 ${e}`);e.append(a(`div`,`w-max p-4`,C),a(`div`,`flex flex-wrap gap-x-5 gap-y-1.5 border-t border-neutral-200 bg-neutral-50/60 px-4 py-2 text-[11px] text-neutral-500`,a(`span`,`inline-flex items-center gap-2`,A(`border-solid border-neutral-400`),`task invocation`),a(`span`,`inline-flex items-center gap-2 text-cyan-700`,A(`border-dashed border-cyan-500`),`callback`),a(`span`,`inline-flex items-center gap-2 text-amber-700`,A(`border-dotted border-amber-500`),`supervisor / control`),a(`span`,`inline-flex items-center gap-1.5`,k(`bg-emerald-500`),`streaming`),a(`span`,`inline-flex items-center gap-1.5`,k(`bg-neutral-300`),`idle`)))}let T=o(r,e=>{e===`dependencies`?g=`dependencies`:e===`sessions`&&(g=`sessions`),b()},()=>{r.replaceChildren(),v={sessions:[],runs:[],messages:[]},y=``});return window.addEventListener(`pier:theme`,()=>{T.visible&&x()}),Object.assign(T,{refresh(){T.visible&&b()}})}export{h as createActivityView};
5
+ <th class="px-2 py-2 font-semibold">In state</th></tr></thead>`;let r=document.createElement(`tbody`);for(let e of v.sessions){let n=document.createElement(`tr`);n.className=`cursor-pointer border-b border-neutral-100 transition-colors hover:bg-neutral-50`,n.onclick=()=>p(e.id);let a=i(`button`,`block w-full cursor-pointer truncate text-left font-medium text-neutral-800`,e.title??t(e.cwd));a.setAttribute(`type`,`button`),a.title=`${e.id}\n${e.cwd}`,a.onclick=t=>{t.stopPropagation(),p(e.id)},n.append(i(`td`,`px-4 py-2.5`,a,i(`div`,`truncate font-mono text-[11px] text-neutral-400 md:hidden`,e.cwd||`-`)),i(`td`,`hidden truncate px-2 py-2.5 font-mono text-[11.5px] text-neutral-500 md:table-cell`,e.cwd||`-`),i(`td`,`px-2 py-2.5`,C(e.state)),i(`td`,`px-2 py-2.5 font-mono text-[11.5px] text-neutral-400`,f(e.stateSince))),r.append(n)}n.append(r),e.append(n),v.sessions.length||e.append(i(`div`,`p-4`,u(_===`active`?`No active sessions.`:`No sessions in the last 24 hours.`)))}function C(e){return e===`streaming`?s(e,`bg-emerald-50 text-emerald-700 ring-emerald-200`,`animate-pulse bg-emerald-500`):s(e,`bg-neutral-100 text-neutral-600 ring-neutral-200`,`bg-neutral-300`)}function w(e){let t=new Map,n=[],r=(e,n,r,i)=>{t.has(e)||t.set(e,{id:e,label:n,kind:r,state:i})};for(let e of v.sessions)r(e.id,e.title??e.id.slice(0,10),`session`,e.state);for(let e of v.runs){let t=e.invokedBySessionId??`scheduler`,i=e.targetSessionId??(e.context.definition.action.type===`bash`?`process:${e.id}`:`task:${e.id}`);r(t,t===`scheduler`?`Scheduler`:t.slice(0,10),t===`scheduler`?`scheduler`:`session`),r(i,e.targetSessionId?e.targetSessionId.slice(0,10):e.context.definition.action.type===`bash`?`Bash process`:`Task`,e.targetSessionId?`session`:e.context.definition.action.type===`bash`?`process`:`task`),n.push({from:t,to:i,run:e,kind:`invocation`}),e.callbackSessionId&&(r(e.callbackSessionId,e.callbackSessionId.slice(0,10),`session`),n.push({from:i,to:e.callbackSessionId,run:e,kind:`callback`}))}for(let e of v.messages){if(!e.fromSessionId||!e.toSessionId||e.fromSessionId===e.toSessionId)continue;let t=v.runs.find(t=>t.id===e.runId);if(t){for(let t of[e.fromSessionId,e.toSessionId])r(t,t===`console`?`Console`:t.slice(0,10),t===`console`?`console`:`session`);n.push({from:e.fromSessionId,to:e.toSessionId,run:t,kind:`message`})}}if(!t.size){e.append(i(`div`,`p-4`,u(_===`active`?`No active dependencies.`:`No dependencies in the last 24 hours.`)));return}let a=[...t.values()],o=new Set(n.map(e=>e.to)),s=new Map,c=a.filter(e=>!o.has(e.id)).map(e=>e.id);c.length||c.push(a[0].id);for(let e of c)s.set(e,0);for(;c.length;){let e=c.shift();for(let t of n)t.from!==e||s.has(t.to)||(s.set(t.to,s.get(e)+1),c.push(t.to))}for(let e of a)s.has(e.id)||s.set(e.id,0);let l=new Map;for(let e of a){let t=s.get(e.id);l.set(t,[...l.get(t)??[],e])}let f=Math.max(...l.keys())+1,g=Math.max(...[...l.values()].map(e=>e.length)),y=n.some(e=>e.from!==e.to&&s.get(e.from)===s.get(e.to))?40:0,b=56+f*180+(f-1)*110+y,x=56+g*46+(g-1)*30,S=new Map;for(let[e,t]of l)t.forEach((t,n)=>S.set(t.id,{x:28+e*290+90,y:28+n*76+23}));let C=d(`svg`,{width:String(b),height:String(x),viewBox:`0 0 ${b} ${x}`,class:`block`}),w=d(`defs`),T=m();for(let[e,t]of[[`activity-arrow`,T.edge],[`activity-arrow-cb`,T.callback],[`activity-arrow-msg`,T.message]]){let n=d(`marker`,{id:e,viewBox:`0 0 10 10`,refX:`9`,refY:`5`,markerWidth:`6`,markerHeight:`6`,orient:`auto-start-reverse`});n.append(d(`path`,{d:`M 0 0 L 10 5 L 0 10 z`,fill:t})),w.append(n)}C.append(w);let E=new Set;for(let e of n){let t=`${e.from}→${e.to}:${e.kind}`;if(E.has(t))continue;E.add(t);let n=S.get(e.from),r=S.get(e.to),i=d(`path`,{d:n.x===r.x?`M ${n.x+90} ${n.y} C ${n.x+90+46} ${n.y}, ${r.x+90+46} ${r.y}, ${r.x+90} ${r.y}`:(()=>{let e=r.x>n.x,t=n.x+(e?90:-90),i=r.x+(e?-90:90),a=Math.max(36,Math.abs(i-t)*.45)*(e?1:-1);return`M ${t} ${n.y} C ${t+a} ${n.y}, ${i-a} ${r.y}, ${i} ${r.y}`})(),fill:`none`,stroke:e.kind===`callback`?T.callback:e.kind===`message`?T.message:T.edge,"stroke-width":`1.5`,"marker-end":e.kind===`callback`?`url(#activity-arrow-cb)`:e.kind===`message`?`url(#activity-arrow-msg)`:`url(#activity-arrow)`});e.kind===`callback`&&i.setAttribute(`stroke-dasharray`,`6 5`),e.kind===`message`&&i.setAttribute(`stroke-dasharray`,`2 5`),i.classList.add(`cursor-pointer`),i.onclick=()=>h(e.run.id),C.append(i)}let D=/[\u1100-\u11FF\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6]/,O=(e,t)=>{let n=0;for(let r=0;r<e.length;r++)if(n+=D.test(e[r])?2:1,n>t)return`${e.slice(0,r)}…`;return e};for(let e of a){let t=S.get(e.id),n=e.kind===`session`?e.state===`streaming`?T.streaming:T.idle:T[e.kind],r=d(`g`,{transform:`translate(${t.x},${t.y})`});e.kind===`session`&&r.classList.add(`cursor-pointer`),r.onclick=()=>{e.kind===`session`&&p(e.id)};let i=d(`rect`,{x:`-90`,y:`-23`,width:`180`,height:`46`,rx:`8`,fill:n.fill,stroke:n.stroke,"stroke-width":e.kind===`session`&&e.state===`streaming`?`1.5`:`1`});n.dash&&i.setAttribute(`stroke-dasharray`,n.dash),r.append(i);let a=-78;if(e.kind===`session`){let t=d(`circle`,{cx:`-75`,cy:`0`,r:`3.5`,fill:n.stroke});if(e.state===`streaming`){let e=d(`animate`,{attributeName:`opacity`,values:`1;0.3;1`,dur:`1.6s`,repeatCount:`indefinite`});t.append(e)}r.append(t),a=-64}let o=d(`text`,{x:String(a),"text-anchor":`start`,"dominant-baseline":`middle`,"font-size":`11.5`,"font-weight":e.kind===`session`?`500`:`400`,fill:n.text});o.textContent=O(e.label,e.kind===`session`?22:25);let s=d(`title`);s.textContent=e.label===e.id?e.id:`${e.label}\n${e.id}`,r.append(o,s),C.append(r)}let k=e=>i(`span`,`inline-block h-2 w-2 rounded-full ${e}`),A=e=>i(`span`,`inline-block h-0 w-6 border-t-2 ${e}`);e.append(i(`div`,`w-max p-4`,C),i(`div`,`flex flex-wrap gap-x-5 gap-y-1.5 border-t border-neutral-200 bg-neutral-50/60 px-4 py-2 text-[11px] text-neutral-500`,i(`span`,`inline-flex items-center gap-2`,A(`border-solid border-neutral-400`),`task invocation`),i(`span`,`inline-flex items-center gap-2 text-cyan-700`,A(`border-dashed border-cyan-500`),`callback`),i(`span`,`inline-flex items-center gap-2 text-amber-700`,A(`border-dotted border-amber-500`),`supervisor / control`),i(`span`,`inline-flex items-center gap-1.5`,k(`bg-emerald-500`),`streaming`),i(`span`,`inline-flex items-center gap-1.5`,k(`bg-neutral-300`),`idle`)))}let T=r(n,e=>{e===`dependencies`?g=`dependencies`:e===`sessions`&&(g=`sessions`),b()},()=>{n.replaceChildren(),v={sessions:[],runs:[],messages:[]},y=``});return window.addEventListener(`pier:theme`,()=>{T.visible&&x()}),Object.assign(T,{refresh(){T.visible&&b()}})}export{h as createActivityView};
@@ -0,0 +1 @@
1
+ import{F as e,G as t,H as n,K as r,P as i,R as a,U as o,g as s,k as c,z as l}from"./index-uFsZkKOQ.js";var u=e=>e.public&&e.token?`/p/${e.slug}-${e.token}/`:`/boards/${e.slug}/`;function d(d,f){let p=[],m=``,h=a(`header`,`sticky top-0 z-30 flex h-10 items-center gap-3 border-b border-neutral-200 bg-white px-4`,a(`span`,`font-medium max-md:hidden`,`Boards`)),g=a(`div`,`px-4 py-5`);d.append(a(`div`,`min-h-0 flex-1 overflow-y-auto`,h,g));async function _(e,t){let i=await r(`/api/boards/${e.slug}`,{public:t},`PATCH`);if(i.ok)return e.token=(await i.json().catch(()=>({}))).token??``,x();m=await n(i,`Could not change ${e.slug}`),await S()}async function v(e){m=await t(`/api/boards/${e}`,`DELETE`,`Could not delete ${e}`)??``,await S()}function y(t){let n=a(`div`,`group flex flex-col gap-1 border-b border-neutral-200/70 px-1 py-2.5 last:border-b-0`),r=document.createElement(`a`);r.className=`truncate font-medium text-neutral-800 hover:text-indigo-700`,r.href=u(t),r.target=`_blank`,r.rel=`noreferrer`,r.textContent=t.title;let i=a(`div`,`flex items-center gap-2`,r,a(`span`,`flex-none font-mono text-[11.5px] text-neutral-400`,t.slug)),o=c(``,``,t.public,e=>{t.public=e,_(t,e)});o.className=`ml-auto flex flex-none cursor-pointer items-center gap-1.5 text-[11.5px] text-neutral-500`,o.append(a(`span`,``,`Public`));let d=s(`Delete`,`flex-none cursor-pointer text-[11.5px] text-neutral-400 opacity-0 transition-opacity hover:text-red-600 group-hover:opacity-100`);d.title=`Renames the folder on disk; nothing is erased`,d.onclick=()=>void v(t.slug);let p=e(`flex-none cursor-pointer text-[11.5px] text-neutral-400 opacity-0 transition-opacity hover:text-indigo-600 group-hover:opacity-100`,()=>`${location.origin}${u(t)}`);p.title=`Copy the board's link`,i.append(o,p,d),n.append(i);let m=a(`div`,`flex flex-wrap items-center gap-x-3 gap-y-1 text-[11.5px] text-neutral-500`);t.description&&m.append(a(`span`,`min-w-0 truncate`,t.description)),m.append(a(`span`,`flex-none text-neutral-400`,l(Date.parse(t.updatedAt))));for(let e of t.sessions){let t=s(e.slice(0,8),`flex-none cursor-pointer font-mono text-indigo-600 hover:underline`);t.title=`Open session ${e}`,t.onclick=()=>f(e),m.append(t)}if(t.public&&t.token){let e=a(`span`,`flex-none font-mono text-neutral-400`,u(t));e.title=`Public URL — anyone holding this link can read the board, no password`,m.append(e)}return n.append(m),n}let b=[{title:`Public`,hint:`Readable at /p/<slug>-<token>/ by anyone holding the link.`,wanted:!0},{title:`Private`,hint:`Reachable from the Console only.`,wanted:!1}];function x(){let e=a(`div`,`mx-auto flex max-w-3xl flex-col`);p.length===0&&e.append(a(`p`,`rounded-xl border border-dashed border-neutral-300 px-4 py-6 text-center text-[12.5px] text-neutral-500`,`No boards yet. Ask an agent to build one — it writes a folder under ~/.pier/boards.`));for(let{title:t,hint:n,wanted:r}of b){let i=p.filter(e=>e.public===r);if(i.length===0)continue;let o=a(`section`,`mb-6`);o.append(a(`h2`,`text-[13px] font-semibold text-neutral-700`,t)),o.append(a(`p`,`mb-1 text-[11.5px] leading-snug text-neutral-500`,n));let s=a(`div`,`rounded-xl border border-neutral-200 bg-white px-3`);s.append(...i.map(y)),o.append(s),e.append(o)}g.replaceChildren(...m?[a(`p`,`mb-3 text-[13px] text-red-600`,m)]:[],e)}async function S(){let e=await o(`/api/boards`,`Could not load boards`);if(!e.ok)return m=e.error,x();p=e.value,x(),m=``}let C=i(d,()=>void S());return window.addEventListener(`focus`,()=>{C.visible&&S()}),C}export{d as createBoardsView};
@@ -0,0 +1,4 @@
1
+ import{I as e,N as t,P as n,R as r,W as i,a,d as o,f as s,i as c,n as l,s as u,u as d,z as f}from"./index-uFsZkKOQ.js";var p=e=>{let t=new Date(e),n=e=>String(e).padStart(2,`0`);return`${t.getFullYear()}-${n(t.getMonth()+1)}-${n(t.getDate())} ${n(t.getHours())}:${n(t.getMinutes())}:${n(t.getSeconds())}`},m=e=>[e.subject,e.body,[[e.author,e.email?`<${e.email}>`:``].filter(Boolean).join(` `),e.at?p(e.at):``].filter(Boolean).join(` · `)].filter(Boolean).join(`
2
+
3
+ `),h=null;function g(){h&&(h.remove(),h=null,document.removeEventListener(`pointerdown`,g,!0),window.removeEventListener(`scroll`,g,!0))}function ee(e,t){e.onmouseenter=()=>{g();let n=t();if(!n)return;h=r(`div`,`pointer-events-none fixed z-50 max-w-96 whitespace-pre-wrap rounded-lg bg-neutral-800/95 px-3 py-2 font-sans text-[11.5px] leading-snug text-neutral-100 shadow-lg`),h.textContent=n,document.body.append(h),document.addEventListener(`pointerdown`,g,!0),window.addEventListener(`scroll`,g,!0);let i=e.getBoundingClientRect();h.style.left=`${Math.max(8,Math.min(i.left,window.innerWidth-h.offsetWidth-8))}px`,h.style.top=i.bottom+6+h.offsetHeight>window.innerHeight?`${i.top-h.offsetHeight-6}px`:`${i.bottom+6}px`},e.onmouseleave=g}function _(e,t,n){let i=r(`button`,`flex w-full cursor-pointer items-center gap-1.5 px-2 py-1 text-left hover:bg-neutral-100`,r(`span`,`w-3 flex-none text-indigo-600`,e.value===t?`✓`:``),r(`span`,`flex-none font-mono text-[11.5px] text-neutral-800`,e.label??e.value));return e.subject&&i.append(r(`span`,`min-w-0 truncate text-[11px] text-neutral-400`,e.subject)),e.author&&i.append(r(`span`,`ml-auto flex-none pl-1.5 text-[10.5px] text-neutral-400`,`${e.author} · ${e.at?f(e.at):``}`)),ee(i,()=>m(e)),i.onclick=()=>n(e.value),i}function te(e,t,n,i,o){let{base:s,head:l}=n,u=()=>l&&s===`${l}~1`?l:``,d=u()?`commit`:`compare`,f=t.commits[0],p=e=>[...e?[{value:``,label:`Working tree`,subject:`the files on disk, uncommitted changes included`}]:[],{...f,value:`HEAD`},...t.refs.map(e=>({value:e.name,subject:e.subject})),...t.commits.map(e=>({...e,value:e.hash}))],m=r(`div`,`flex w-[38rem] max-w-full flex-col`);function h(){let e=(e,t)=>{let n=r(`button`,`cursor-pointer rounded-full px-2.5 py-0.5 text-[11.5px] ${d===t?`bg-indigo-50 font-medium text-indigo-700`:`text-neutral-500 hover:bg-neutral-100`}`,e);return n.onclick=()=>{d=t,h()},n},n=(e,t,n,i)=>r(`div`,`flex min-w-0 flex-1 flex-col`,r(`div`,`flex-none px-2 pb-0.5 text-[10.5px] font-semibold uppercase tracking-wide text-neutral-400`,e),r(`div`,`min-h-0 flex-1 overflow-y-auto`,...t.map(e=>_(e,n,i)))),a=d===`compare`?r(`div`,`flex h-72 min-h-0 divide-x divide-neutral-200 pb-1`,n(`base`,p(!1),s,e=>{s=e,h(),i(s,l)}),n(`to`,p(!0),l,e=>{l=e,h(),i(s,l)})):r(`div`,`flex h-72 min-h-0 flex-col overflow-y-auto pb-1`,...t.commits.map(e=>_({value:e.hash,...e},u(),e=>{s=`${e}~1`,l=e,c(),i(s,l)}))),f=r(`div`,`flex-none border-t border-neutral-200`,_({value:o,label:`↺ ${o} ↔ Working tree`,subject:`back to the default: the main line's tip vs the files on disk`},s===o&&l===``?o:``,()=>{s=o,l=``,h(),i(s,l)}));m.replaceChildren(r(`div`,`flex flex-none items-center gap-1 border-b border-neutral-200 px-2 py-1.5`,e(`Compare two points`,`compare`),e(`One commit`,`commit`)),a,...d===`compare`?[f]:[])}h(),a(e,m)}var ne=/\.(png|jpe?g|gif|webp|avif|bmp|svg)$/i,v={A:`text-emerald-600`,M:`text-amber-600`,D:`text-red-600`},re=30,y=`pier.filesPrefs`,b=50,x=()=>{try{let e=JSON.parse(localStorage.getItem(y)??`{}`);return e&&typeof e==`object`?e:{}}catch{return{}}},S=e=>{let t=e?x()[e]:void 0;return typeof t?.cwd==`string`&&typeof t.base==`string`&&typeof t.head==`string`?{cwd:t.cwd,base:t.base,head:t.head}:null},ie=(e,t)=>{let n=x();delete n[e],n[e]=t;let r=Object.keys(n);for(let e of r.slice(0,Math.max(0,r.length-b)))delete n[e];try{localStorage.setItem(y,JSON.stringify(n))}catch(e){console.warn(`Files: could not remember this folder`,e)}},ae=(e,t=`indigo`)=>{let n=r(`button`,`min-w-0 cursor-pointer truncate rounded-md px-2 py-0.5 text-left font-mono text-[11.5px] ${t===`indigo`?`bg-indigo-50 font-medium text-indigo-700 hover:bg-indigo-100`:`bg-neutral-100 text-neutral-600 hover:bg-neutral-200`}`,e);return n.type=`button`,n};function C(a,c,f,p){let h=``,g=``,_={branch:null,refs:[],commits:[],worktrees:[]},y=`HEAD`,b=``,x=new Map,C=!0,w=!1,T=[],E=new Set,D=null,O=null,k=r(`header`,`flex h-10 flex-none items-center gap-2 border-b border-neutral-200 bg-white px-3`),A=r(`div`,`flex-none border-b border-neutral-200`),j=r(`div`,`min-h-0 flex-1 overflow-y-auto py-1`),oe=r(`aside`,`flex w-64 flex-none flex-col border-r border-neutral-200 text-[12.5px] max-md:h-2/5 max-md:w-full max-md:border-b max-md:border-r-0`,A,j),M=r(`div`,`h-full min-w-0 overflow-auto`),N=r(`div`,`pointer-events-none absolute inset-y-0 right-0 z-20 hidden w-2`),se=r(`section`,`relative min-h-0 min-w-0 flex-1`,M,N);a.append(k,r(`div`,`flex min-h-0 flex-1 max-md:flex-col`,oe,se));let P=(e,t)=>`/api/explorer/${e}?${new URLSearchParams({root:h,...t})}`,F=(e,t)=>`/api/fs/${e}?${new URLSearchParams({root:h,...t})}`,I=(e,t=`text-neutral-400`)=>r(`p`,`px-4 py-3 text-[12.5px] ${t}`,e),L=e=>{let t=0;for(let n of x.keys())n.startsWith(`${e}/`)&&t++;return t};function R(e,t){let n=new Set(t.map(e=>e.name)),r=e?`${e}/`:``;for(let[e,i]of x){if(i.status!==`D`||!e.startsWith(r))continue;let a=e.slice(r.length).split(`/`)[0];n.has(a)||(n.add(a),t.push({name:a,dir:e.slice(r.length).includes(`/`)}))}let i=t;return C&&_.branch&&(i=t.filter(e=>e.dir?L(`${r}${e.name}`)>0:x.has(`${r}${e.name}`))),i.sort((e,t)=>Number(t.dir)-Number(e.dir)||e.name.localeCompare(t.name)),i.map(e=>e.dir?ce(`${r}${e.name}`,e.name):le(`${r}${e.name}`,e.name))}async function z(e,t){try{let{entries:n}=await i(F(`ls`,{path:t}),`could not list this folder`);e.replaceChildren(...R(t,n)),e.childElementCount||e.append(I(C?`No changes.`:`Empty.`))}catch(n){let r=R(t,[]);r.length?e.replaceChildren(...r):e.replaceChildren(I(String(n),`text-red-600`))}}function ce(t,n){let i=L(t),a=[r(`span`,`truncate`,n)];i&&a.push(r(`span`,`ml-auto flex-none rounded bg-amber-100/80 px-1 font-mono text-[10px] text-amber-700`,String(i)));let{el:o,summary:s}=e(``,a);s.className+=` px-2 py-0.5 hover:bg-neutral-100`;let c=r(`div`,`pl-3`);o.append(c);let l=!1,u=()=>{l||(l=!0,c.append(I(`…`)),z(c,t))};return o.ontoggle=()=>{E[o.open?`add`:`delete`](t),o.open&&u()},(C&&_.branch&&!w||E.has(t))&&(o.open=!0,u()),o}function B(e){O?.classList.remove(`bg-indigo-50`,`text-indigo-700`),O=e,e.classList.add(`bg-indigo-50`,`text-indigo-700`)}function le(e,t){let n=x.get(e)?.status,i=r(`button`,`flex w-full cursor-pointer items-center gap-1.5 px-2 py-0.5 pl-6 text-left hover:bg-neutral-100`,r(`span`,`truncate`,t));return n&&i.append(r(`span`,`ml-auto flex-none font-mono text-[10.5px] font-semibold ${v[n]??`text-neutral-500`}`,n)),i.title=e,e===D&&B(i),i.onclick=()=>{B(i),D=e,de(e)},i}let V=()=>void z(j,``),H=(e,t)=>[...e?[r(`span`,`flex-none font-mono text-[10.5px] font-semibold text-emerald-600`,`+${e}`)]:[],...t?[r(`span`,`flex-none font-mono text-[10.5px] font-semibold text-red-600`,`−${t}`)]:[]];function U(e,n,i){let a=x.get(e),o=r(`div`,`sticky top-0 z-10 flex items-center gap-2 border-b border-neutral-200 bg-white px-4 py-1.5 font-mono text-[11.5px] text-neutral-500`,r(`span`,`truncate`,e));a&&(o.append(r(`span`,`flex-none font-semibold ${v[a.status]??``}`,a.status)),o.append(...H(a.add,a.del)));let s=r(`div`,`ml-auto flex flex-none items-center gap-2`);if(i&&s.append(i),n){let n=document.createElement(`a`);n.className=`flex-none text-neutral-400 hover:text-indigo-700 hover:underline`,n.href=F(`file`,{path:e}),n.download=t(e),n.textContent=`Download`,s.append(n)}return o.append(s),o}function ue(e){let t=0;for(;t<e.length;){if(e[t].tone!==`del`){t++;continue}let n=t;for(;t<e.length&&e[t].tone===`del`;)t++;let r=t;for(;t<e.length&&e[t].tone===`add`;)t++;let i=Math.min(r-n,t-r);for(let t=0;t<i;t++){let i=e[n+t],a=e[r+t],o=Math.min(i.text.length,a.text.length),s=0;for(;s<o&&i.text[s]===a.text[s];)s++;let c=0;for(;c<o-s&&i.text[i.text.length-1-c]===a.text[a.text.length-1-c];)c++;s+c!==0&&(i.mark=[s,i.text.length-c],a.mark=[s,a.text.length-c])}}}let W=ve();function G(){N.classList.add(`hidden`),N.replaceChildren(),W.set([],[])}let K=0,q=e=>e===K;function de(e){let t=++K;return x.has(e)?ye(e,t):fe(e,t)}async function fe(e,t){if(!q(t))return;G();let n=F(`file`,{path:e}),i=r(`div`,`min-w-0`);if(M.classList.remove(`flex`,`flex-col`),M.replaceChildren(U(e,!0),i),ne.test(e)){let e=r(`img`,`max-w-full p-4`);e.src=n,e.onerror=()=>i.replaceChildren(I(`Could not load image.`,`text-red-600`)),i.append(e);return}if(e.toLowerCase().endsWith(`.pdf`)){let t=document.createElement(`iframe`);t.className=`h-full w-full`,t.src=n,M.replaceChildren(U(e,!0),t),M.classList.add(`flex`,`flex-col`);return}i.append(I(`…`));let a=await fetch(n);if(!q(t))return;if(!a.ok){let{error:e}=await a.json().catch(()=>({}));i.replaceChildren(I(e??`HTTP ${a.status}`,`text-red-600`));return}if(!(a.headers.get(`content-type`)??``).startsWith(`text/plain`)){i.replaceChildren(I(`Binary file — use Download.`));return}let c=o(await a.text());if(!q(t))return;let l=await s(e);q(t)&&i.replaceChildren(d(c,l))}function pe(e){let t=[],n=0,r=0,i=!1,a=e.split(`
4
+ `);a.at(-1)===``&&a.pop();for(let e of a){let a=/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(e);if(a){i&&t.push({nums:[``,``],text:`⋯`,tone:``}),[n,r]=[Number(a[1]),Number(a[2])],i=!0;continue}!i||e.startsWith(`\\`)||(e.startsWith(`+`)?t.push({nums:[``,r++],text:e.slice(1),tone:`add`}):e.startsWith(`-`)?t.push({nums:[n++,``],text:e.slice(1),tone:`del`}):t.push({nums:[n++,r++],text:e.slice(1),tone:``}))}return t}function me(e){let t=[];for(let n=0;n<e.length;n++){let r=e[n].tone;if(!r)continue;let i=t.at(-1);i&&i.end===n-1?(i.end=n,(r===`add`&&i.tone===`del`||r===`del`&&i.tone===`add`)&&(i.tone=`mixed`)):t.push({start:n,end:n,tone:r})}return t}let he={add:`bg-emerald-400`,del:`bg-red-400`,mixed:`bg-amber-400`};function ge(e,t,n){N.replaceChildren(...e.map(e=>{let i=r(`button`,`pointer-events-auto absolute w-full cursor-pointer ${he[e.tone]}`);return i.style.top=`${e.start/t*100}%`,i.style.height=`${Math.max(.4,(e.end-e.start+1)/t*100)}%`,i.title=`line ${_e(n,e.start)}`,i.onclick=()=>n[e.start].scrollIntoView({block:`center`}),i})),N.classList.remove(`hidden`)}let _e=(e,t)=>e[t]?.querySelector(`span`)?.textContent?.trim()||String(t+1);function ve(){let e=[],t=[],n=-1,i=r(`span`,`flex-none text-[10.5px] text-neutral-400`),o=r=>{e.length&&(n=(n+r+e.length)%e.length,t[e[n].start].scrollIntoView({block:`center`}),i.textContent=`${n+1}/${e.length}`)},s=()=>e.length>0&&!a.classList.contains(`hidden`),c=(e,t,n,i)=>{let a=r(`button`,`icon-btn flex-none`,e);return a.onclick=()=>o(t),u(a,i,n,()=>o(t),s),a};return{el:r(`div`,`flex flex-none items-center gap-1`,i,c(`↑`,-1,`Previous change`,[`p`,`k`]),c(`↓`,1,`Next change`,[`n`,`j`])),set(r,a){e=r,t=a,n=-1,i.textContent=`${r.length} change${r.length===1?``:`s`}`}}}async function ye(e,t){G(),M.classList.remove(`flex`,`flex-col`);let n=x.get(e)?.status!==`D`;M.replaceChildren(U(e,n),I(`…`));try{let{diff:r}=await i(P(`diff`,{base:y,head:b,file:e,context:`99999`}),`could not read this diff`);if(!q(t))return;if(!r||/^Binary files /m.test(r))return fe(e,t);let a=pe(r);ue(a);let o=await s(e);if(!q(t))return;let c=d(a,o),l=[...c.children],u=me(a);W.set(u,l),M.replaceChildren(U(e,n,W.el),c),ge(u,a.length,l)}catch(n){q(t)&&M.replaceChildren(U(e,!1),I(String(n),`text-red-600`))}}let J=()=>b&&y===`${b}~1`?b:``,be=()=>_.commits.find(e=>e.hash===J())?.subject??``,xe=()=>J()?`${J()} — ${be()||`commit`}`:`${y} ↔ ${b||`working tree`}`,Se=(e,t,n,i)=>{let a=r(`button`,`flex h-6 w-6 flex-none cursor-pointer items-center justify-center rounded-md ${n?`bg-indigo-50 text-indigo-700`:`text-neutral-400 hover:bg-neutral-100 hover:text-neutral-700`}`);return a.innerHTML=e,a.title=t,a.onclick=i,a};function Y(){let e=Se(`<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" class="h-3.5 w-3.5"><path d="M2.5 3h11l-4.25 5v4.5l-2.5 1.25V8L2.5 3z" stroke-linejoin="round"/></svg>`,`Show only files that differ`,C,()=>{C=!C,Y(),V()}),t=Se(`<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" class="h-3.5 w-3.5"><path d="m4.5 2.5 3.5 3 3.5-3M4.5 13.5l3.5-3 3.5 3" stroke-linecap="round" stroke-linejoin="round"/></svg>`,w?`Restore folders`:`Collapse all folders`,w,()=>{if(w=!w,w)T=[...E],E.clear();else for(let e of T)E.add(e);Y(),V()});if(!_.branch){A.replaceChildren(r(`div`,`flex items-center justify-end gap-1 p-1.5`,t));return}let n=ae(xe());if(J()){let e=_.commits.find(e=>e.hash===J());ee(n,()=>e?m(e):``)}else n.title=`Choose what to diff`;n.onclick=async()=>{await Te(),te(n,_,{base:y,head:b},(e,t)=>{y=e,b=t,Z()},$())};let[i,a]=[0,0];for(let e of x.values())i+=e.add,a+=e.del;A.replaceChildren(r(`div`,`flex flex-col gap-1 p-2`,n,r(`div`,`flex items-center gap-1.5 pl-1`,r(`span`,`text-[11px] text-neutral-400`,`${x.size} changed file${x.size===1?``:`s`}`),...H(i,a),r(`span`,`ml-auto`),e,t)))}let Ce=()=>{g&&h&&ie(g,{cwd:h,base:y,head:b})},X=e=>{let t=e.replace(/~\d+$/,``);return!t||t===`HEAD`||_.refs.some(e=>e.name===t)||_.commits.some(e=>e.hash===t)};async function we(){if(x=new Map,!_.branch)return;let{files:e}=await i(P(`diff`,{base:y,head:b}),`could not read what changed`);for(let t of e)x.set(t.path,{status:t.status,add:t.add,del:t.del});if(x.size<=re)for(let e of x.keys()){let t=e.split(`/`);for(let e=1;e<t.length;e++)E.add(t.slice(0,e).join(`/`))}}async function Z(){Ce();try{await we()}catch(e){Y(),j.replaceChildren(I(String(e),`text-red-600`));return}Y(),V(),D&&de(D)}function Q(){let e=ae(h||`Choose a folder…`,`neutral`);e.className+=` max-w-72`,e.title=`Switch folder`,e.onclick=()=>l(e,_.worktrees.map(e=>({path:e.path,...e.branch?{hint:e.branch}:{}})),h||void 0,f);let t=r(`button`,`icon-btn`,`✕`);t.type=`button`,t.title=`Close Files`,t.setAttribute(`aria-label`,`Close Files`),t.onclick=p,t.classList.add(`ml-auto`),k.replaceChildren(e,r(`span`,`truncate font-mono text-[11.5px] text-neutral-400`,_.branch?`⎇ ${_.branch}`:`no git`),t)}async function Te(){if(h)try{_=await i(P(`git`,{}),`could not read this repository`),Q()}catch(e){M.replaceChildren(I(String(e),`text-red-600`))}}let $=()=>_.refs.some(e=>e.name===`main`)?`main`:_.refs.some(e=>e.name===`master`)?`master`:`HEAD`;async function Ee(){if(Q(),D=null,O=null,E.clear(),G(),M.classList.remove(`flex`,`flex-col`),M.replaceChildren(I(`Select a file.`)),!h){A.replaceChildren(),j.replaceChildren(),M.replaceChildren(I(`No folder yet — pick one from the chip above.`));return}try{_=await i(P(`git`,{}),`could not read this repository`)}catch(e){_={branch:null,refs:[],commits:[],worktrees:[]},M.replaceChildren(I(String(e),`text-red-600`))}let e=S(g),t=e&&e.cwd===h&&X(e.base)&&X(e.head)?e:null;y=t?.base??$(),b=t?.head??``,Q(),await Z()}return n(a,e=>{let t=c(),n=t?.id??``,r=e?.startsWith(`/`)?e:(n===g?h:``)||S(n)?.cwd||t?.cwd||``;if(r===h&&n===g&&j.childElementCount){Q(),Te().then(Z);return}g=n,h=r,Ee()},()=>{G(),M.classList.remove(`flex`,`flex-col`),M.replaceChildren(I(`Select a file.`))})}export{C as createExplorerView};