@alphacoder-v0/pi-loops 0.22.5 → 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 +35 -0
- package/README.md +5 -7
- package/README.zh-CN.md +2 -4
- package/docs/web-ui-parity.md +9 -4
- package/package.json +1 -1
- package/src/sdk-runner.ts +6 -9
- package/src/web.mjs +38 -11
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,41 @@ 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
|
+
|
|
28
|
+
## [0.22.6] - 2026-10-20
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- **A finished loop run no longer leaves the shared extensions stale.** Every run in one project
|
|
32
|
+
shares one loaded extension set, and a finished run's `session.dispose()` invalidated the
|
|
33
|
+
runtime those extensions captured at load time. The restore that was meant to undo it read a
|
|
34
|
+
flag pi keeps in a closure, so it never did anything: from the second run on, every `pi.*`
|
|
35
|
+
call from a shared extension threw "This extension ctx is stale", an extension that gates
|
|
36
|
+
tool calls (pi-permission-system, say) blocked every tool call, and the event-bus listeners
|
|
37
|
+
the extensions registered at load were gone. The shared runtime is now never invalidated; each
|
|
38
|
+
run's own ExtensionRunner still is. The cost: a listener an extension adds inside a per-run
|
|
39
|
+
handler is kept for the life of the host. `test/sdk-runner.test.ts`: *a finished run does not
|
|
40
|
+
leave the shared extensions stale for the next run*. [#81](https://github.com/alphacoder-v0/pi-loops/pull/81)
|
|
41
|
+
by [@advaitpaliwal](https://github.com/advaitpaliwal).
|
|
42
|
+
|
|
8
43
|
## [0.22.5] - 2026-09-28
|
|
9
44
|
|
|
10
45
|
### Changed
|
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
|
|
41
|
-
pi install git:github.com/alphacoder-v0/pi-loops@v0.22.5 # 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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
48
|
-
pi install git:github.com/alphacoder-v0/pi-loops@v0.22.5 # 或者从 GitHub 装,钉住一个 tag
|
|
49
|
-
pi install /path/to/pi-loops # 或者本地检出;本仓库里就是 pi install .
|
|
47
|
+
pi install npm:@alphacoder-v0/pi-loops
|
|
50
48
|
```
|
|
51
49
|
|
|
52
|
-
|
|
50
|
+
如果你在改这份代码,就改装本地检出:在本仓库里 `pi install .`。**只装一份,不要装两份。** 两份副本注册同名工具,pi 会拒绝加载第二份并直接退出(`Tool "cron_create" conflicts with …`)。
|
|
53
51
|
|
|
54
52
|
重启 pi,装到这里就够了——那四个斜杠命令现在就能用。下面这段只跟 `pi-loops` 这个命令有关:浏览器窗口和几个 shell 子命令靠它,不要浏览器窗口可以先跳过。
|
|
55
53
|
|
package/docs/web-ui-parity.md
CHANGED
|
@@ -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.
|
|
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
|
|
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.
|
|
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/sdk-runner.ts
CHANGED
|
@@ -111,15 +111,12 @@ export function subSessionResources(req: Pick<SubagentRequest, "cwd">, deps: Pic
|
|
|
111
111
|
// staleness, which is what that message is really about, is untouched. The cost is that
|
|
112
112
|
// a `pi.*` action between runs reaches the most recently created session instead of
|
|
113
113
|
// throwing; pi rebinds those to the live session on the next run (`runner.js:160-175`).
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
invalidate(message);
|
|
121
|
-
(base.runtime as any).staleMessage = before;
|
|
122
|
-
};
|
|
114
|
+
// pi keeps the runtime's stale flag in a closure (`createExtensionRuntime`'s `state`, dist/core/extensions/
|
|
115
|
+
// loader.js), not on the runtime object, so it cannot be read back and restored after `invalidate()`.
|
|
116
|
+
// And `invalidate()` also unsubscribes every event-bus listener the shared extensions made when they
|
|
117
|
+
// loaded, which the next run needs (pi-mcp-adapter's runtime tool calls, say). The shared runtime lives
|
|
118
|
+
// as long as this loaded set, so it is never invalidated; each run's own ExtensionRunner still is.
|
|
119
|
+
base.runtime.invalidate = () => {};
|
|
123
120
|
return { ...base, extensions: [...base.extensions.filter((e) => !isInsideDir(ownDir, e.resolvedPath) && !isInsideDir(ownDir, e.path)), subagentGuardExtension(deps.log, deps.allowCommands?.() ?? [])] };
|
|
124
121
|
},
|
|
125
122
|
});
|
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
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
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 = "";
|