@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,7 +1,18 @@
1
1
  // What the workbench decided about a session, and nothing a transcript already
2
- // knows. Ownership of a row in Projects (`pinned`), a finished turn nobody has
3
- // looked at (`unread`), and the two places a hand put it in (`sort`,
4
- // `project_sort`).
2
+ // knows. A finished turn nobody has looked at (`unread`), and the rail's
3
+ // working set: the rank of a row kept on top (`sort`).
4
+ //
5
+ // The working set replaced pinning and dragging. Ordering the rail by activity
6
+ // made it jump — a scheduled task, a subagent, anyone's Slack message moved the
7
+ // row somebody was about to click — and ordering it by hand asked for the
8
+ // arranging. So: a session enters the set at the front when a human speaks to
9
+ // it and it is not in the set already, and members never move relative to each
10
+ // other until one is pushed out of the last slot. A three-week-old session is
11
+ // back on top the moment it is used; the four you switch between hold still.
12
+ //
13
+ // Remembered rather than derived on purpose: "the last time a human spoke" is
14
+ // there in the transcript and still jumps, because it reorders on every
15
+ // message. Only an order nothing recomputes can hold still.
5
16
  //
6
17
  // It used to mirror the summary too — cwd, title, created_at, last_active —
7
18
  // because listing sessions meant parsing every transcript on disk (~237ms) and
@@ -11,16 +22,22 @@
11
22
  // every turn, a title write on the first prompt, a second write on rename.
12
23
  // Deriving beats syncing; all of it is gone.
13
24
  //
14
- // `cwd` stayed, and is not a mirror: it is the key a project's manual place is
15
- // stamped on (`reorder`, and the sibling lookup in `pin`), and a session's
16
- // working directory is fixed when its transcript is created — an immutable key
17
- // needs no synchronising.
25
+ // `cwd` and `project_sort` stayed as columns nothing reads: they keyed the
26
+ // per-directory order the rail no longer has (migration 19).
27
+ //
28
+ // The rank does not bring that back: it is written only when a session outside
29
+ // the set is spoken to, not once per turn.
18
30
  //
19
31
  // One row per session rather than two JSON files: the unread flag is written at
20
32
  // the end of every turn, and rewriting a whole file on each of those writes
21
33
  // loses the entire set when the process dies mid-write — a truncated file reads
22
- // back as "no pins", which is indistinguishable from a fresh install.
23
- import { pierDb } from "../db.js";
34
+ // back as "nothing on top", which is indistinguishable from a fresh install.
35
+ import { pierDb, transact } from "../db.js";
36
+ /** How many rows the rail keeps on top. Larger than the handful anybody
37
+ * switches between (3–6), and small enough that every member is somewhere a
38
+ * hand can see: a set as deep as the page (20) would count a row nobody has
39
+ * scrolled to as "already up there" and never bring it back. */
40
+ export const WORKING_SET = 8;
24
41
  export class SessionStateStore {
25
42
  #db;
26
43
  constructor(db = pierDb()) {
@@ -34,61 +51,47 @@ export class SessionStateStore {
34
51
  this.#db.prepare(`INSERT INTO session_state(session_id, unread) VALUES (?, ?)
35
52
  ON CONFLICT(session_id) DO UPDATE SET unread = excluded.unread`).run(sessionId, unread ? 1 : 0);
36
53
  }
37
- /** Projects takes the session, or gives it up. Membership lasts until a hand
38
- * ends it: nothing expires, so what is pinned is what the rail draws until
39
- * the ✓ on the row says otherwise. */
40
- pin(sessionId, cwd, pinned) {
41
- // A new session joins a project that already has a place in the list.
42
- // Unranked it would sort on top — lifting the whole project with it, which
43
- // is the jump manual order exists to stop.
44
- const sibling = this.#db.prepare("SELECT project_sort AS rank FROM session_state WHERE cwd = ? AND project_sort IS NOT NULL LIMIT 1").get(cwd);
45
- this.#db.prepare(`INSERT INTO session_state(session_id, pinned, cwd, project_sort)
46
- VALUES (?, ?, ?, ?)
47
- ON CONFLICT(session_id) DO UPDATE SET
48
- pinned = excluded.pinned,
49
- cwd = excluded.cwd,
50
- project_sort = COALESCE(session_state.project_sort, excluded.project_sort)`).run(sessionId, pinned ? 1 : 0, cwd, sibling?.rank ?? null);
51
- }
52
- /** One drag = one write of the whole list it reordered: index is the place.
53
- * `sessions` are ids (a session's place inside its project), `projects` are
54
- * cwds, whose place is stamped on every session that has that cwd. */
55
- reorder(order) {
56
- const bySession = this.#db.prepare("UPDATE session_state SET sort = ? WHERE session_id = ?");
57
- const byCwd = this.#db.prepare("UPDATE session_state SET project_sort = ? WHERE cwd = ?");
58
- this.#tx(() => {
59
- order.sessions?.forEach((id, i) => bySession.run(i, id));
60
- order.projects?.forEach((cwd, i) => byCwd.run(i, cwd));
54
+ /** A human spoke to this session. Already in the working set → nothing moves
55
+ * and nothing is written; otherwise it takes the front slot and whatever
56
+ * sat in the last one falls out. Answers whether the order changed, so the
57
+ * caller broadcasts a re-list only when there is something to see. */
58
+ promote(sessionId) {
59
+ // `session_id` breaks a tie the migration can leave behind: several pinned
60
+ // rows nobody ever dragged share rank -1 until the first promotion
61
+ // renumbers them.
62
+ const ranked = this.#db.prepare("SELECT session_id AS id FROM session_state WHERE sort IS NOT NULL ORDER BY sort, session_id").all().map((r) => r.id);
63
+ if (ranked.includes(sessionId))
64
+ return false;
65
+ const kept = [sessionId, ...ranked].slice(0, WORKING_SET);
66
+ const evicted = ranked.filter((id) => !kept.includes(id));
67
+ const rank = this.#db.prepare(`INSERT INTO session_state(session_id, sort) VALUES (?, ?)
68
+ ON CONFLICT(session_id) DO UPDATE SET sort = excluded.sort`);
69
+ const drop = this.#db.prepare("UPDATE session_state SET sort = NULL WHERE session_id = ?");
70
+ // Every rank rewritten rather than the new one prepended: the places are
71
+ // 0..n and a half-renumbered set is an order nobody would recognize.
72
+ transact(this.#db, () => {
73
+ kept.forEach((id, i) => rank.run(id, i));
74
+ for (const id of evicted)
75
+ drop.run(id);
61
76
  });
77
+ return true;
62
78
  }
63
- /** Drop a session's organization row entirely — pin, order, unread. For
79
+ /** Drop a session's organization row entirely — rank and unread. For
64
80
  * ghosts: Pi persists a session only once its first assistant reply lands,
65
81
  * so a created-and-never-messaged one cannot be resumed, and its remembered
66
- * row would otherwise sit in the rail 404ing forever. */
82
+ * row would otherwise sit in the rail 404ing forever — holding a slot in the
83
+ * working set while it does. */
67
84
  forget(sessionId) {
68
85
  this.#db.prepare("DELETE FROM session_state WHERE session_id = ?").run(sessionId);
69
86
  }
70
- /** All-or-nothing: a half-written order is a list nobody arranged. */
71
- #tx(run) {
72
- this.#db.exec("BEGIN");
73
- try {
74
- run();
75
- this.#db.exec("COMMIT");
76
- }
77
- catch (err) {
78
- this.#db.exec("ROLLBACK");
79
- throw err;
80
- }
81
- }
82
87
  /** What this store knows about the sessions it knows anything about, for a
83
88
  * caller holding the listing. */
84
89
  flags() {
85
- const rows = this.#db.prepare(`SELECT session_id AS id, pinned, unread, sort, project_sort AS projectSort
86
- FROM session_state WHERE pinned = 1 OR unread = 1`).all();
90
+ const rows = this.#db.prepare(`SELECT session_id AS id, unread, sort
91
+ FROM session_state WHERE unread = 1 OR sort IS NOT NULL`).all();
87
92
  return new Map(rows.map((r) => [r.id, {
88
- pinned: r.pinned === 1,
89
93
  unread: r.unread === 1,
90
- ...(r.sort === null ? {} : { sort: r.sort }),
91
- ...(r.projectSort === null ? {} : { projectSort: r.projectSort }),
94
+ ...(r.sort === null ? {} : { rank: r.sort }),
92
95
  }]));
93
96
  }
94
97
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timqi/pier",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
4
4
  "description": "A self-hosted workspace for coding agents: web workbench and IM channels in front of Pi sessions",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": "github:timqi/pier",
@@ -9,14 +9,25 @@ description: Delegate work to Pier subagents with the task tool — one-shot, pa
9
9
 
10
10
  - Every run executes a persisted task definition. A `run` without `task_id`
11
11
  — a bare `prompt`, or a full `task` draft — creates and runs a one-shot
12
- subagent atomically; it is tagged `kind:"subagent"` and hidden from task
13
- lists. You never name or file it: the name is the prompt's first line.
12
+ subagent atomically: `kind:"subagent"`, hidden from `list`, named by the
13
+ prompt's first line. You never file it.
14
14
  - **Nothing blocks.** Every operation returns immediately; results, group
15
- joins, and decision replies arrive later as system follow-up messages in
16
- your session. Your only waiting primitive is ending your turn.
17
- - The child session survives the run — `resume` continues it with full
18
- context intact. Poll state without blocking via `{"operation":"get",
19
- "run_id":"..."}` (or `group_id`, or `task_id` for that task's recent runs).
15
+ joins and decision replies arrive as system follow-up messages once your
16
+ turn ends (batched when several are due). Ending your turn is the only wait.
17
+ - The child session survives the run — `resume` continues it with context
18
+ intact. `get` polls: `run_id` (full result), `group_id`, or `task_id` (10
19
+ most recent runs). Lists truncate results at 2000 chars, callbacks at 8000,
20
+ both saying so; `get run_id` is whole.
21
+ - Limits: a tree is ≤3 runs deep below the session that started it (depth
22
+ 0–2; the fourth `run` errors) and holds ≤16 runs below its root (depth ≥ 1,
23
+ resumes included) — your direct children are each a root, uncounted. 4
24
+ agent runs execute at once instance-wide; the rest wait for a slot without
25
+ error, but the timeout clock starts at queue time, so a run queued behind
26
+ long ones can time out unstarted.
27
+ - Timeout: 900s default → `failed` / `task timed out`; `timeoutSeconds`
28
+ 1–86400, draft form only. A Pier restart ends queued/running runs as
29
+ `interrupted` (callback still arrives) and refuses new root runs while
30
+ draining — retry after.
20
31
 
21
32
  ## Single delegation (the common case)
22
33
 
@@ -24,43 +35,49 @@ description: Delegate work to Pier subagents with the task tool — one-shot, pa
24
35
  {"operation":"run","prompt":"Review src/auth/*.ts for injection risks. Output: file:line + issue + fix."}
25
36
  ```
26
37
 
27
- That is the whole call: a fresh session in **your own directory**. Add `cwd`
28
- to place it elsewhere — absolute, or relative to your directory
29
- (`"cwd":"../pier.feature-x"` for a sibling worktree); it must already exist.
30
- `launch` picks a model/thinking level, `name` overrides the label. The full
31
- `task` draft form (`{"name","action":{"type":"agent","session":{...},"prompt"}}`)
32
- is for what the shorthand cannot say: `reuse` a session, `timeoutSeconds`,
33
- `callback`.
34
-
35
- Returns a run summary (`runId`, `state:"queued"`, ids). Finish your turn; the
36
- result arrives as a follow-up. `callback:"none"` silences it.
37
-
38
- A summary's `triggerSource` is who fired that run (`agent` when you did, plus
39
- `manual` / `cron` / `watch` / `task`); a definition's `trigger` is only its
40
- schedule policy, where `manual` means on-demand — by a human or by you.
41
-
42
- - `fresh`: clean context in a `cwd` (yours when omitted) — how a child
43
- normally starts.
44
- `reuse`: sends work to an existing session by id, continuing its history.
45
- There is no way to copy your context into a child: what the child needs, you
46
- write down.
47
- - **The handoff is the prompt.** A child cannot see your discussion, so state:
48
- the goal; the decisions *currently* in force (not the ones you argued out of);
49
- the constraints the user added late; absolute paths; what "done" means and how
50
- to verify it. Writing it is also the check — a decision you cannot state in a
51
- sentence was not settled.
52
- - The child inherits your current model unless `launch.model` is set.
53
- Unknown models fail with the available list in the error. `launch` applies to
38
+ That is the whole call: a fresh session in **your own directory**. `cwd`
39
+ places it elsewhere — absolute, or relative to yours (`"cwd":"../pier.x"` for
40
+ a sibling worktree); it must exist. `launch` picks model/thinking, `name`
41
+ overrides the label, `input` (any JSON) is appended to the prompt as
42
+ `<task_input>`. The `task` draft form is for what the shorthand cannot say —
43
+ `timeoutSeconds`, or `reuse` of an existing session:
44
+
45
+ ```json
46
+ {"operation":"run","task":{"name":"long build","timeoutSeconds":3600,"action":{"type":"agent","session":{"mode":"fresh","cwd":"/abs/path"},"prompt":"..."}}}
47
+ ```
48
+
49
+ Omit `trigger` in a draft (anything but `manual` errors); a draft's `callback`
50
+ is ignored on runs you fire — the result comes to you — and matters only for
51
+ `create`d schedules. Returns a run summary (`runId`, `taskId`,
52
+ `state:"queued"`, …); `callback:"none"` silences the result,
53
+ `callback_session_id` routes it to another existing session. A summary's
54
+ `triggerSource` is who fired the run (`agent` / `manual` / `cron` / `watch` /
55
+ `task`); a definition's `trigger` is only its schedule, `manual` meaning
56
+ on-demand — by a human or by you.
57
+
58
+ - `fresh`: clean context in a `cwd` (yours when omitted) — the normal start.
59
+ `reuse`: an existing session by id, continuing its history; the run waits
60
+ for it to go idle. Nothing copies your context into a child: what the child
61
+ needs, you write down.
62
+ - **The handoff is the prompt.** A child cannot see your discussion, so state
63
+ the goal, the decisions *currently* in force (not the ones argued out of),
64
+ late constraints, absolute paths, what "done" means and how to verify it.
65
+ Writing it is the check — a decision you cannot state in a sentence was not
66
+ settled.
67
+ - The child inherits your current model unless `launch.model` is set, and has
68
+ the same tools you do, `task` included. An unknown model id fails the *run*,
69
+ not the call — the error, with the first available ids, arrives in the
70
+ callback; an unknown `launch.thinking` fails the call. `launch` applies to
54
71
  `fresh` only — a `reuse` session owns its own model and tools.
55
- - The child has the same tools you do.
56
- - Runs time out after 900s by default; set `timeoutSeconds` in the draft for
57
- longer jobs.
72
+ - Stored roles: `run` with `task_id` (+ optional `input`);
73
+ `session_mode:"fresh"` runs a `reuse`-mode role in a fresh session instead.
74
+ Archived tasks refuse to run.
58
75
 
59
76
  ## Parallel fan-out (core-joined)
60
77
 
61
- Pass `tasks[]` (each entry a prompt string, `{"prompt","cwd"?}`, a full
62
- draft, or `{"task_id":"..."}`) and Pier joins the group in core — you never
63
- track run ids across turns:
78
+ `tasks[]` — 2+ entries, each a prompt string, `{"prompt","cwd"?,"launch"?,
79
+ "name"?}`, a full draft, or `{"task_id":"..."}`; never with `task`, `task_id`
80
+ or `session_mode` — and Pier joins the group in core; you never track run ids:
64
81
 
65
82
  ```json
66
83
  {"operation":"run","join":"all","tasks":[
@@ -68,21 +85,21 @@ track run ids across turns:
68
85
  {"prompt":"Review the tests for gaps. Output: missing case + file.","cwd":"packages/web"}]}
69
86
  ```
70
87
 
71
- - `join:"all"` (default): one aggregated follow-up when every member
72
- finishes, listing each member's name, state, and result.
73
- - `join:"first"`: the first finished run wins; the rest are cancelled but
74
- their sessions stay resumable (ids are in the callback).
75
- - `get`/`cancel` accept `group_id`: observe or stop the whole group as one
76
- unit.
77
- - Limits: 4 agents active at once, 16 children per tree, nesting caps at
78
- you → child → grandchild. Extra members queue rather than fail.
88
+ - `join:"all"` (default): one aggregated follow-up when every member is
89
+ terminal — each member's name, state, result.
90
+ - `join:"first"`: the first member to reach *any* terminal state wins, a
91
+ failure included; the rest are cancelled, their sessions resumable (run ids
92
+ in the callback).
93
+ - Members send no individual callbacks; `callback:"none"` silences the
94
+ group's. `get`/`cancel` take `group_id` for the whole group. A member that
95
+ cannot enqueue (limit, missing `cwd`) rolls the group back — nothing runs.
79
96
 
80
97
  ## Chains
81
98
 
82
99
  Run step 1, end your turn, receive its callback, splice the needed part into
83
- step 2's prompt. Children share nothing implicitly — every prompt must be
84
- self-contained: paths, acceptance criteria, expected output format. Branching and retries are your own logic between
85
- turns.
100
+ step 2's prompt. Children share nothing implicitly — every prompt is
101
+ self-contained: paths, acceptance criteria, output format. Branching and
102
+ retries are your own logic between turns.
86
103
 
87
104
  ## Choosing a model and thinking level
88
105
 
@@ -90,54 +107,71 @@ Omit `launch.model` to inherit your own model — the right default. When the
90
107
  task profile clearly differs, adjust in this order:
91
108
 
92
109
  - **Harder reasoning → raise `launch.thinking` first**, on the inherited
93
- model (`off` … `minimal` `low` `medium` `high` `xhigh` `max`). It needs no
94
- model name and is usually cheaper than switching models.
110
+ model (`off` … `minimal` `low` `medium` `high` `xhigh` `max`): no model
111
+ name needed, usually cheaper than switching models.
95
112
  - **Cheap bulk work** (listings, extraction, simple checks) → the same
96
- provider's smallest current model, with thinking low or off.
113
+ provider's smallest current model, thinking low or off.
97
114
  - **Cross-vendor second opinion** → another vendor's flagship.
98
115
 
99
116
  Never write a model id from memory — catalogs move under you.
100
- `{"operation":"models"}` is the authority: it returns the deployment's menu —
101
- operator-pinned models when set (`source:"menu"`), each with an intent note
102
- and often a usual `thinking` level, the curated live catalog otherwise
103
- (`source:"catalog"`). Prefer a pinned entry whose note matches the task and
104
- start from its thinking level. A wrong id still fails with the available
105
- list in the error, so a stale guess costs one call.
117
+ `{"operation":"models"}` is the authority: operator-pinned models when set
118
+ (`source:"menu"`, each with an intent note and often a usual `thinking`
119
+ level), else the curated live catalog (`source:"catalog"`). Prefer a pinned
120
+ entry whose note matches the task and start from its thinking level.
106
121
 
107
122
  ## Mid-run control
108
123
 
109
124
  - `steer` — interrupt a running child with corrected instructions
110
125
  - `follow_up` — queue a message for after its current turn
111
- - `resume` — continue a finished child in its same session; this expires any
126
+ - `resume` — continue a finished child in its same session, `message` as the
127
+ prompt: a new run (new `runId`, own callback, same depth); expires any
112
128
  unanswered decision on that run
113
- - `cancel` — stop a run (or a whole group via `group_id`); cancellation
114
- cascades to the run's own children
129
+ - `cancel` — stop a run, or a whole group via `group_id`; cascades to the
130
+ run's children; a terminal run is returned unchanged
131
+
132
+ `steer`/`follow_up` need a non-terminal run (else `resume`); one undelivered
133
+ when the run ends expires. `resume` needs a terminal run. All four accept only
134
+ runs you own — trees you started, or, inside a run, your own descendants.
135
+ `message`: non-empty, <16 KiB.
115
136
 
116
137
  ## Supervisor decisions (as a child)
117
138
 
118
139
  `{"operation":"contact","reason":"decision","message":"..."}` returns a
119
- receipt immediately. State what you are waiting for and **end your turn** —
120
- never spin or poll. The answer is steered into your current turn if your run is
121
- still active; if your run already finished, Pier resumes your session with the
122
- reply as the prompt. One open decision per run.
140
+ receipt immediately. State what you await and **end your turn** — never spin
141
+ or poll. The answer is steered into your current turn if your run is still
142
+ active; if it finished, Pier resumes your session with the reply as the
143
+ prompt. One open decision per run. `contact` works only inside a run a session
144
+ delegated — a cron/watch run has no supervisor. `reason:"progress"` is
145
+ fire-and-forget status for the parent.
123
146
 
124
147
  As a parent: the question arrives as a system input with a message id, steered
125
- into your current turn at the next step boundary so a blocked child is not
126
- waiting on the end of your turn; answer
127
- with `{"operation":"reply","message_id":"...","message":"..."}`. A run that
128
- finished while awaiting your answer shows `pendingDecisionId` in `get` and
129
- sends no completion callback — the question is the notification.
130
-
131
- `reason:"progress"` is fire-and-forget status for the parent.
148
+ into your current turn at the next step boundary so the child does not wait
149
+ out your turn. Answer with `{"operation":"reply","message_id":"...",
150
+ "message":"..."}` — only the addressed supervisor may; same text twice is a
151
+ no-op, different text an error. A run that finished while awaiting your answer
152
+ shows `pendingDecisionId` in `get` and sends no completion callback — the
153
+ question is the notification; your reply resumes it, and that continuation's
154
+ callback comes to you.
132
155
 
133
156
  ## Rules
134
157
 
135
158
  - One concern per child. For fan-out, let core join via `tasks[]` — never
136
159
  hand-aggregate run ids across turns.
160
+ - **A change to work a child already did → `resume` that run**, not a new
161
+ child: its session still holds the files it read and the decisions it made,
162
+ so the message can be a delta ("same file, also handle the empty input").
163
+ Go `fresh` when the new work shares nothing with the old — different area,
164
+ different goal — or when that session is long and mostly dead ends: clean
165
+ context plus a self-contained prompt beats a child sifting its own noise.
166
+ Lost the `run_id`? `get` with the summary's `task_id` lists that task's
167
+ recent runs — a one-shot subagent has one too.
137
168
  - `create` is for definitions that outlive one job: a schedule (`cron` /
138
- `watch`), or a role you will run by `task_id` again and again. It files a
139
- task the operator sees and has to archive by hand — never use it for a
140
- one-off; that is `run` with a prompt.
141
- - Inline drafts must use `action.type:"agent"`; triggers are forced manual;
142
- do not target another session with `reuse` from an inline draft.
169
+ `watch`), or a role run by `task_id` again and again. It files a task the
170
+ operator sees and must archive by hand — never for a one-off; that is `run`
171
+ with a prompt. `update` takes the whole draft again, `trigger` included. A
172
+ schedule reports to nobody unless its `callback` is
173
+ `{"type":"session","sessionId":"..."}` — `origin` is the invoker, which a
174
+ cron run lacks.
175
+ - Inside a run: Agent tasks only (stored or inline), no `reuse`, no
176
+ `create`/`update`.
143
177
  - After launching work, end your turn. The callback starts your next one.
@@ -1 +0,0 @@
1
- import{G as e,H as t,L as n,N as r,O as i,P as a,R as o,V as s,W as c,h as l}from"./index-BmKvoulH.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 _(t,n){let r=await e(`/api/boards/${t.slug}`,{public:n},`PATCH`);if(r.ok)return t.token=(await r.json().catch(()=>({}))).token??``,x();m=await s(r,`Could not change ${t.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`),r=document.createElement(`a`);r.className=`truncate font-medium text-neutral-800 hover:text-indigo-700`,r.href=u(e),r.target=`_blank`,r.rel=`noreferrer`,r.textContent=e.title;let s=n(`div`,`flex items-center gap-2`,r,n(`span`,`flex-none font-mono text-[11.5px] text-neutral-400`,e.slug)),c=i(``,``,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`,o(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 t(`/api/boards`,`Could not load boards`);if(!e.ok)return m=e.error,x();p=e.value,x(),m=``}let C=r(d,()=>void S());return window.addEventListener(`focus`,()=>{C.visible&&S()}),C}export{d as createBoardsView};
@@ -1 +0,0 @@
1
- import{L as e,d as t}from"./index-BmKvoulH.js";var n=2e4,r=e=>e.map((e,t)=>({nums:[t+1],text:e,tone:``}));function i(t,r){let i=t.length<=n?r:null,o=Math.max(2,...t.map(e=>e.nums.reduce((e,t)=>Math.max(e,String(t).length),0))),s=e(`div`,`py-2 font-mono text-[12px] leading-[1.5]`);for(let n of t){let t=e(`div`,`flex pl-2 pr-4 ${n.tone===`add`?`bg-emerald-50`:n.tone===`del`?`bg-red-50`:``}`);for(let r of n.nums){let n=e(`span`,`flex-none select-none pr-1.5 text-right text-neutral-300`,r===``?``:String(r));n.style.width=`calc(${o}ch + 0.375rem)`,t.append(n)}t.append(a(n,i)),s.append(t)}return s}function a(n,r){let i=`min-w-0 flex-1 whitespace-pre-wrap [overflow-wrap:anywhere]`;if(!n.mark||n.mark[0]>=n.mark[1]){let e=t(n.text,r);return e.className=i,e}let[a,o]=n.mark,s=t(n.text.slice(a,o),r);return s.className=`rounded-xs ${n.tone===`add`?`bg-emerald-200/80`:`bg-red-200/70`}`,e(`span`,i,t(n.text.slice(0,a),r),s,t(n.text.slice(o),r))}export{r as n,i as t};
@@ -1,5 +0,0 @@
1
- import{F as e,L as t,M as n,N as r,R as i,U as a,a as o,i as s,n as c,s as l,u}from"./index-BmKvoulH.js";import{n as d,t as f}from"./code-I0lxXuvE.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 _(e,n){e.onmouseenter=()=>{g();let r=n();if(!r)return;h=t(`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=r,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 v(e,n,r){let a=t(`button`,`flex w-full cursor-pointer items-center gap-1.5 px-2 py-1 text-left hover:bg-neutral-100`,t(`span`,`w-3 flex-none text-indigo-600`,e.value===n?`✓`:``),t(`span`,`flex-none font-mono text-[11.5px] text-neutral-800`,e.label??e.value));return e.subject&&a.append(t(`span`,`min-w-0 truncate text-[11px] text-neutral-400`,e.subject)),e.author&&a.append(t(`span`,`ml-auto flex-none pl-1.5 text-[10.5px] text-neutral-400`,`${e.author} · ${e.at?i(e.at):``}`)),_(a,()=>m(e)),a.onclick=()=>r(e.value),a}function ee(e,n,r,i,a){let{base:c,head:l}=r,u=()=>l&&c===`${l}~1`?l:``,d=u()?`commit`:`compare`,f=n.commits[0],p=e=>[...e?[{value:``,label:`Working tree`,subject:`the files on disk, uncommitted changes included`}]:[],{...f,value:`HEAD`},...n.refs.map(e=>({value:e.name,subject:e.subject})),...n.commits.map(e=>({...e,value:e.hash}))],m=t(`div`,`flex w-[38rem] max-w-full flex-col`);function h(){let e=(e,n)=>{let r=t(`button`,`cursor-pointer rounded-full px-2.5 py-0.5 text-[11.5px] ${d===n?`bg-indigo-50 font-medium text-indigo-700`:`text-neutral-500 hover:bg-neutral-100`}`,e);return r.onclick=()=>{d=n,h()},r},r=(e,n,r,i)=>t(`div`,`flex min-w-0 flex-1 flex-col`,t(`div`,`flex-none px-2 pb-0.5 text-[10.5px] font-semibold uppercase tracking-wide text-neutral-400`,e),t(`div`,`min-h-0 flex-1 overflow-y-auto`,...n.map(e=>v(e,r,i)))),o=d===`compare`?t(`div`,`flex h-72 min-h-0 divide-x divide-neutral-200 pb-1`,r(`base`,p(!1),c,e=>{c=e,h(),i(c,l)}),r(`to`,p(!0),l,e=>{l=e,h(),i(c,l)})):t(`div`,`flex h-72 min-h-0 flex-col overflow-y-auto pb-1`,...n.commits.map(e=>v({value:e.hash,...e},u(),e=>{c=`${e}~1`,l=e,s(),i(c,l)}))),f=t(`div`,`flex-none border-t border-neutral-200`,v({value:a,label:`↺ ${a} ↔ Working tree`,subject:`back to the default: the main line's tip vs the files on disk`},c===a&&l===``?a:``,()=>{c=a,l=``,h(),i(c,l)}));m.replaceChildren(t(`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`)),o,...d===`compare`?[f]:[])}h(),o(e,m)}var te=/\.(png|jpe?g|gif|webp|avif|bmp|svg)$/i,ne={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)}},C=(e,n=`indigo`)=>{let r=t(`button`,`min-w-0 cursor-pointer truncate rounded-md px-2 py-0.5 text-left font-mono text-[11.5px] ${n===`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 r.type=`button`,r};function w(i,o,s,p){let h=``,g=``,v={branch:null,refs:[],commits:[],worktrees:[]},y=`HEAD`,b=``,x=new Map,w=!0,T=!1,ae=[],E=new Set,D=null,O=null,k=t(`header`,`flex h-10 flex-none items-center gap-2 border-b border-neutral-200 bg-white px-3`),A=t(`div`,`flex-none border-b border-neutral-200`),j=t(`div`,`min-h-0 flex-1 overflow-y-auto py-1`),oe=t(`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=t(`div`,`h-full min-w-0 overflow-auto`),N=t(`div`,`pointer-events-none absolute inset-y-0 right-0 z-20 hidden w-2`),se=t(`section`,`relative min-h-0 min-w-0 flex-1`,M,N);i.append(k,t(`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,n=`text-neutral-400`)=>t(`p`,`px-4 py-3 text-[12.5px] ${n}`,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 w&&v.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 a(F(`ls`,{path:t}),`could not list this folder`);e.replaceChildren(...R(t,n)),e.childElementCount||e.append(I(w?`No changes.`:`Empty.`))}catch(n){let r=R(t,[]);r.length?e.replaceChildren(...r):e.replaceChildren(I(String(n),`text-red-600`))}}function ce(n,r){let i=L(n),a=[t(`span`,`truncate`,r)];i&&a.push(t(`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=t(`div`,`pl-3`);o.append(c);let l=!1,u=()=>{l||(l=!0,c.append(I(`…`)),z(c,n))};return o.ontoggle=()=>{E[o.open?`add`:`delete`](n),o.open&&u()},(w&&v.branch&&!T||E.has(n))&&(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,n){let r=x.get(e)?.status,i=t(`button`,`flex w-full cursor-pointer items-center gap-1.5 px-2 py-0.5 pl-6 text-left hover:bg-neutral-100`,t(`span`,`truncate`,n));return r&&i.append(t(`span`,`ml-auto flex-none font-mono text-[10.5px] font-semibold ${ne[r]??`text-neutral-500`}`,r)),i.title=e,e===D&&B(i),i.onclick=()=>{B(i),D=e,de(e)},i}let V=()=>void z(j,``),H=(e,n)=>[...e?[t(`span`,`flex-none font-mono text-[10.5px] font-semibold text-emerald-600`,`+${e}`)]:[],...n?[t(`span`,`flex-none font-mono text-[10.5px] font-semibold text-red-600`,`−${n}`)]:[]];function U(e,r,i){let a=x.get(e),o=t(`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`,t(`span`,`truncate`,e));a&&(o.append(t(`span`,`flex-none font-semibold ${ne[a.status]??``}`,a.status)),o.append(...H(a.add,a.del)));let s=t(`div`,`ml-auto flex flex-none items-center gap-2`);if(i&&s.append(i),r){let t=document.createElement(`a`);t.className=`flex-none text-neutral-400 hover:text-indigo-700 hover:underline`,t.href=F(`file`,{path:e}),t.download=n(e),t.textContent=`Download`,s.append(t)}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=_e();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)?ve(e,t):J(e,t)}async function J(e,n){if(!q(n))return;G();let r=F(`file`,{path:e}),i=t(`div`,`min-w-0`);if(M.classList.remove(`flex`,`flex-col`),M.replaceChildren(U(e,!0),i),te.test(e)){let e=t(`img`,`max-w-full p-4`);e.src=r,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=r,M.replaceChildren(U(e,!0),t),M.classList.add(`flex`,`flex-col`);return}i.append(I(`…`));let a=await fetch(r);if(!q(n))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 o=(await a.text()).split(`
4
- `);if(!q(n))return;o.at(-1)===``&&o.pop();let s=await u(e);q(n)&&i.replaceChildren(f(d(o),s))}function fe(e){let t=[],n=0,r=0,i=!1,a=e.split(`
5
- `);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 pe(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 me={add:`bg-emerald-400`,del:`bg-red-400`,mixed:`bg-amber-400`};function he(e,n,r){N.replaceChildren(...e.map(e=>{let i=t(`button`,`pointer-events-auto absolute w-full cursor-pointer ${me[e.tone]}`);return i.style.top=`${e.start/n*100}%`,i.style.height=`${Math.max(.4,(e.end-e.start+1)/n*100)}%`,i.title=`line ${ge(r,e.start)}`,i.onclick=()=>r[e.start].scrollIntoView({block:`center`}),i})),N.classList.remove(`hidden`)}let ge=(e,t)=>e[t]?.querySelector(`span`)?.textContent?.trim()||String(t+1);function _e(){let e=[],n=[],r=-1,a=t(`span`,`flex-none text-[10.5px] text-neutral-400`),o=t=>{e.length&&(r=(r+t+e.length)%e.length,n[e[r].start].scrollIntoView({block:`center`}),a.textContent=`${r+1}/${e.length}`)},s=()=>e.length>0&&!i.classList.contains(`hidden`),c=(e,n,r,i)=>{let a=t(`button`,`icon-btn flex-none`,e);return a.onclick=()=>o(n),l(a,i,r,()=>o(n),s),a};return{el:t(`div`,`flex flex-none items-center gap-1`,a,c(`↑`,-1,`Previous change`,[`p`,`k`]),c(`↓`,1,`Next change`,[`n`,`j`])),set(t,i){e=t,n=i,r=-1,a.textContent=`${t.length} change${t.length===1?``:`s`}`}}}async function ve(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 a(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 J(e,t);let i=fe(r);ue(i);let o=await u(e);if(!q(t))return;let s=f(i,o),c=[...s.children],l=pe(i);W.set(l,c),M.replaceChildren(U(e,n,W.el),s),he(l,i.length,c)}catch(n){q(t)&&M.replaceChildren(U(e,!1),I(String(n),`text-red-600`))}}let Y=()=>b&&y===`${b}~1`?b:``,ye=()=>v.commits.find(e=>e.hash===Y())?.subject??``,be=()=>Y()?`${Y()} — ${ye()||`commit`}`:`${y} ↔ ${b||`working tree`}`,xe=(e,n,r,i)=>{let a=t(`button`,`flex h-6 w-6 flex-none cursor-pointer items-center justify-center rounded-md ${r?`bg-indigo-50 text-indigo-700`:`text-neutral-400 hover:bg-neutral-100 hover:text-neutral-700`}`);return a.innerHTML=e,a.title=n,a.onclick=i,a};function X(){let e=xe(`<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`,w,()=>{w=!w,X(),V()}),n=xe(`<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>`,T?`Restore folders`:`Collapse all folders`,T,()=>{if(T=!T,T)ae=[...E],E.clear();else for(let e of ae)E.add(e);X(),V()});if(!v.branch){A.replaceChildren(t(`div`,`flex items-center justify-end gap-1 p-1.5`,n));return}let r=C(be());if(Y()){let e=v.commits.find(e=>e.hash===Y());_(r,()=>e?m(e):``)}else r.title=`Choose what to diff`;r.onclick=async()=>{await Te(),ee(r,v,{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(t(`div`,`flex flex-col gap-1 p-2`,r,t(`div`,`flex items-center gap-1.5 pl-1`,t(`span`,`text-[11px] text-neutral-400`,`${x.size} changed file${x.size===1?``:`s`}`),...H(i,a),t(`span`,`ml-auto`),e,n)))}let Se=()=>{g&&h&&ie(g,{cwd:h,base:y,head:b})},Ce=e=>{let t=e.replace(/~\d+$/,``);return!t||t===`HEAD`||v.refs.some(e=>e.name===t)||v.commits.some(e=>e.hash===t)};async function we(){if(x=new Map,!v.branch)return;let{files:e}=await a(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(){Se();try{await we()}catch(e){X(),j.replaceChildren(I(String(e),`text-red-600`));return}X(),V(),D&&de(D)}function Q(){let e=C(h||`Choose a folder…`,`neutral`);e.className+=` max-w-72`,e.title=`Switch folder`,e.onclick=()=>c(e,v.worktrees.map(e=>({path:e.path,...e.branch?{hint:e.branch}:{}})),h||void 0,s);let n=t(`button`,`icon-btn`,`✕`);n.type=`button`,n.title=`Close Files`,n.setAttribute(`aria-label`,`Close Files`),n.onclick=p,n.classList.add(`ml-auto`),k.replaceChildren(e,t(`span`,`truncate font-mono text-[11.5px] text-neutral-400`,v.branch?`⎇ ${v.branch}`:`no git`),n)}async function Te(){if(h)try{v=await a(P(`git`,{}),`could not read this repository`),Q()}catch(e){M.replaceChildren(I(String(e),`text-red-600`))}}let $=()=>v.refs.some(e=>e.name===`main`)?`main`:v.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{v=await a(P(`git`,{}),`could not read this repository`)}catch(e){v={branch:null,refs:[],commits:[],worktrees:[]},M.replaceChildren(I(String(e),`text-red-600`))}let e=S(g),t=e&&e.cwd===h&&Ce(e.base)&&Ce(e.head)?e:null;y=t?.base??$(),b=t?.head??``,Q(),await Z()}return r(i,e=>{let t=o(),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{w as createExplorerView};