@alphacoder-v0/pi-loops 0.22.6 → 0.22.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ All notable changes to pi-loops are documented here. The format follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.22.7] - 2026-10-28
9
+
10
+ ### Changed
11
+ - **Both READMEs install with one line you can paste.** The install block held the npm command, a
12
+ GitHub command pinned to the current tag, a local checkout and `pi -e`. The pinned tag had to
13
+ change with every release, and the extra lines kept the block from being pasted as it is. The
14
+ block is now `pi install npm:@alphacoder-v0/pi-loops`; the text below it gives the checkout
15
+ routes to people who work on the code.
16
+
17
+ ### Fixed
18
+ - **The browser's thinking picker offers `max`.** Its fallback list, and the options in the page
19
+ before the script replaces them, was a copy of the thinking levels kept by hand. It had lost
20
+ `max`, which pi-loops accepts. It now offers every level pi accepts, and a test keeps the fallback
21
+ equal to `THINKING_LEVELS`. [#84](https://github.com/alphacoder-v0/pi-loops/pull/84)
22
+ - **In the browser, a reply shares one line with the work before it.** A turn with work took two
23
+ header lines: the work row's summary (`6 steps · thinking, read, bash`) and the reply's
24
+ `ASSISTANT` label with its copy button. The label and the copy button are now in the work row's
25
+ summary, and the reply no longer ends the stretch, so steps after the answer join the same row.
26
+ A reply with no work keeps its own label row. [#86](https://github.com/alphacoder-v0/pi-loops/pull/86)
27
+
8
28
  ## [0.22.6] - 2026-10-20
9
29
 
10
30
  ### Fixed
package/README.md CHANGED
@@ -37,15 +37,13 @@ one message, make sure you get an answer. pi-loops runs sub-agents while you are
37
37
  the first sign of credentials that do not work should not be an empty inbox tomorrow morning.
38
38
 
39
39
  ```bash
40
- pi install npm:@alphacoder-v0/pi-loops # from npm, following new releases
41
- pi install git:github.com/alphacoder-v0/pi-loops@v0.22.6 # or from GitHub, at a pinned tag
42
- pi install /path/to/pi-loops # or a local checkout — `pi install .` in this repo
43
- pi -e /path/to/pi-loops # or none of them: try it for one run, installing nothing
40
+ pi install npm:@alphacoder-v0/pi-loops
44
41
  ```
45
42
 
46
- Install one of them, not two. Two copies register the same tools, and pi refuses to load the
47
- second — `Tool "cron_create" conflicts with …`, and it exits. If you are working on the code, the
48
- checkout is the one to keep.
43
+ Working on the code? Install your checkout instead — `pi install .` in this repo — or try it for
44
+ one run with `pi -e .`, which installs nothing. Keep one copy, not two: two copies register the
45
+ same tools, and pi refuses to load the second — `Tool "cron_create" conflicts with …`, and it
46
+ exits.
49
47
 
50
48
  Then restart pi, and that is the whole install: `/cron`, `/inbox`, `/triggers` and `/goal` are
51
49
  registered by the extension itself, so they work with nothing on your `PATH` and no launcher. The
package/README.zh-CN.md CHANGED
@@ -44,12 +44,10 @@ pi-loops 自己没有任何运行时依赖。
44
44
  ### 2. 装上
45
45
 
46
46
  ```bash
47
- pi install npm:@alphacoder-v0/pi-loops # 从 npm 装,跟着新版本走
48
- pi install git:github.com/alphacoder-v0/pi-loops@v0.22.6 # 或者从 GitHub 装,钉住一个 tag
49
- pi install /path/to/pi-loops # 或者本地检出;本仓库里就是 pi install .
47
+ pi install npm:@alphacoder-v0/pi-loops
50
48
  ```
51
49
 
52
- **几种装法挑一种,不要装两份。** 两份副本注册同名工具,pi 会拒绝加载第二份并直接退出(`Tool "cron_create" conflicts with …`)。如果你在改这份代码,留本地检出那份。
50
+ 如果你在改这份代码,就改装本地检出:在本仓库里 `pi install .`。**只装一份,不要装两份。** 两份副本注册同名工具,pi 会拒绝加载第二份并直接退出(`Tool "cron_create" conflicts with …`)。
53
51
 
54
52
  重启 pi,装到这里就够了——那四个斜杠命令现在就能用。下面这段只跟 `pi-loops` 这个命令有关:浏览器窗口和几个 shell 子命令靠它,不要浏览器窗口可以先跳过。
55
53
 
@@ -67,9 +67,12 @@ weaker, and worth converting whenever one of them breaks.
67
67
  - [x] Assistant text, thinking, tool calls, tool results, errors, and the end of a turn.
68
68
  - [x] The conversation is what the conversation is for. A whole stretch of work — thinking and tool
69
69
  calls, interleaved as they arrive — is **one** row, closed: what is happening while it happens,
70
- what happened afterwards. One button in the header opens or closes every one of them.
70
+ what happened afterwards. The reply that follows it shares the row — its label and copy
71
+ button sit in the summary, so "ASSISTANT" and "6 steps · thinking, bash" are one line, not
72
+ two — and the stretch continues past the reply rather than being cut in two. One button in
73
+ the header opens or closes every one of them.
71
74
  `test/web-page.test.ts`: *a stretch of work is one row, and one button opens every one of
72
- them*.
75
+ them*, *a reply without work keeps its own label row*.
73
76
  - [x] Replies render as Markdown — headings, lists, quotes, rules, tables, inline code, fenced
74
77
  code, and http(s) links. Everything is escaped first: a reply is not trusted input, and a
75
78
  tool result quoted inside one is whatever some web page said. `test/web-page.test.ts`: *a
@@ -114,7 +117,7 @@ weaker, and worth converting whenever one of them breaks.
114
117
  `test/web-page.test.ts`: *a reply can show a picture*, *an image that came back from a tool*.
115
118
  - [x] A tool call and what it returned are one block, and it starts closed. A tool that prints two
116
119
  hundred lines must not push the conversation off the screen to do it.
117
- `test/web-page.test.ts`: *a run of tool calls is one row, and the next run is a new one*.
120
+ `test/web-page.test.ts`: *a run of tool calls is one row, and the reply shares it*.
118
121
  - [x] You are on the right in a bubble and the model is full-width prose: the shape says who is
119
122
  speaking before a word is read. Status lines are small, monospace and quiet — context, not
120
123
  conversation.
@@ -237,7 +240,9 @@ weaker, and worth converting whenever one of them breaks.
237
240
  heard of it*.
238
241
  - [x] The thinking levels offered are the ones this model has — asked for per model rather than
239
242
  drawn from one fixed list, because picking a level a model does not implement does nothing at
240
- all.
243
+ all. When the per-model list is unknown, the picker offers the whole menu pi accepts rather
244
+ than a list of its own.
245
+ `test/web-page.test.ts`: *the thinking-level picker offers every level pi knows, not a list of its own*.
241
246
  - [x] Attaching an image is refused, with the reason, by a model that cannot see one.
242
247
  - [x] Cost and token counts.
243
248
  - [x] Compact, undo (fork from your last message), find across the whole session including
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alphacoder-v0/pi-loops",
3
- "version": "0.22.6",
3
+ "version": "0.22.7",
4
4
  "description": "An automation layer for pi: cron + stateful loops with a triage inbox, maker/checker verification, dynamic triggers, MCP notifications and tools, lifecycle hooks, session archives. A non-invasive pi extension.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/web.mjs CHANGED
@@ -2222,6 +2222,12 @@ details.work{border:1px solid transparent;border-radius:10px;overflow:hidden}
2222
2222
  details.work[open]{border-color:var(--line);background:var(--side)}
2223
2223
  details.work:not([open])>summary{padding-left:0;padding-right:0}
2224
2224
  details.work>summary{cursor:pointer;display:flex;align-items:center;gap:8px;padding:7px 11px;font-family:var(--font-mono);font-size:12px;color:var(--muted);list-style:none}
2225
+ /* The reply that follows a stretch of work shares its row: the label and the copy button sit in
2226
+ the summary, so "ASSISTANT" and "6 steps · thinking, bash" are one line rather than two. */
2227
+ details.work>summary>span.tag{flex:0 0 auto;color:var(--faint);font-size:11px;letter-spacing:.03em;text-transform:uppercase}
2228
+ details.work>summary>button{border:0;background:none;color:var(--faint);padding:0 5px;font-size:11px;min-height:0;opacity:0;text-transform:none}
2229
+ details.work:hover>summary>button,details.work:focus-within>summary>button{opacity:1}
2230
+ @media (pointer:coarse){details.work>summary>button{opacity:.7}}
2225
2231
  details.work>summary::-webkit-details-marker{display:none}
2226
2232
  details.work>summary::before{content:"▸";color:var(--faint);flex:0 0 auto}
2227
2233
  details.work[open]>summary::before{content:"▾"}
@@ -2441,7 +2447,7 @@ dialog menu{display:flex;gap:8px;justify-content:flex-end;padding:0;margin:14px
2441
2447
  <span class="cwd" id="cwd"></span>
2442
2448
  <select id="model" title="Model"></select>
2443
2449
  <select id="thinking" title="Thinking level">
2444
- <option>off</option><option>minimal</option><option>low</option><option>medium</option><option>high</option><option>xhigh</option>
2450
+ <option>off</option><option>minimal</option><option>low</option><option>medium</option><option>high</option><option>xhigh</option><option>max</option>
2445
2451
  </select>
2446
2452
  <span class="grow">
2447
2453
  <span class="badge" id="queue" hidden></span>
@@ -3116,20 +3122,40 @@ function row(cls, role, text) {
3116
3122
  const el = document.createElement("div");
3117
3123
  el.className = "row " + cls;
3118
3124
  const b = document.createElement("span");
3125
+ // The answer that follows a turn's work shares its row: the label and its copy button go into
3126
+ // the summary, so "ASSISTANT" and "6 steps · thinking, bash" are one line, not two — and the
3127
+ // stretch continues past the answer rather than being cut in two.
3128
+ const shared = role === "assistant" && toolGroup;
3119
3129
  if (role) {
3120
- const r = document.createElement("div");
3121
- r.className = "role";
3122
- const name = document.createElement("span");
3123
- name.textContent = role;
3124
- r.append(name);
3125
- // Only where there is something worth copying: a status line is not.
3126
- if (cls === "" || cls === "tool" || cls === "user") r.append(copyBtn(() => b.raw ?? b.textContent ?? ""));
3127
- el.append(r);
3130
+ if (shared) {
3131
+ toolGroup.answerEl = b;
3132
+ if (!toolGroup.roleTag) {
3133
+ const name = document.createElement("span");
3134
+ name.className = "tag";
3135
+ name.textContent = role;
3136
+ const btn = copyBtn(() => {
3137
+ const a = (toolGroup && toolGroup.answerEl) ?? b;
3138
+ return a.raw ?? a.textContent ?? "";
3139
+ });
3140
+ toolGroup.summary.insertBefore(name, toolGroup.what);
3141
+ toolGroup.summary.insertBefore(btn, toolGroup.what);
3142
+ toolGroup.roleTag = name;
3143
+ }
3144
+ } else {
3145
+ const r = document.createElement("div");
3146
+ r.className = "role";
3147
+ const name = document.createElement("span");
3148
+ name.textContent = role;
3149
+ r.append(name);
3150
+ // Only where there is something worth copying: a status line is not.
3151
+ if (cls === "" || cls === "tool" || cls === "user") r.append(copyBtn(() => b.raw ?? b.textContent ?? ""));
3152
+ el.append(r);
3153
+ }
3128
3154
  }
3129
3155
  if (text) b.textContent = plain(text);
3130
3156
  el.append(b);
3131
3157
  clearEmpty();
3132
- endToolGroup();
3158
+ if (!shared) endToolGroup();
3133
3159
  feed.append(stamp(el));
3134
3160
  scroll();
3135
3161
  return b;
@@ -3174,6 +3200,7 @@ function toolGroupFor() {
3174
3200
  s.append(what);
3175
3201
  d.append(s);
3176
3202
  d.what = what;
3203
+ d.summary = s;
3177
3204
  d.names = [];
3178
3205
  clearEmpty();
3179
3206
  feed.append(stamp(d));
@@ -3759,7 +3786,7 @@ function drawModels(catalog, current) {
3759
3786
  /** Only the levels this model has. A level pi maps to null is a level that does nothing. */
3760
3787
  function drawThinking(levels, current) {
3761
3788
  const picker = $("thinking");
3762
- const list = (levels && levels.length ? levels : ["off", "minimal", "low", "medium", "high", "xhigh"]).map(String);
3789
+ const list = (levels && levels.length ? levels : ["off", "minimal", "low", "medium", "high", "xhigh", "max"]).map(String);
3763
3790
  if (picker.dataset.levels !== list.join(",")) {
3764
3791
  picker.dataset.levels = list.join(",");
3765
3792
  picker.innerHTML = "";