@alphacoder-v0/pi-loops 0.22.8 → 0.24.0
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 +664 -0
- package/CONTRACT.md +7 -7
- package/README.md +91 -63
- package/README.zh-CN.md +59 -39
- package/docs/cli.md +272 -83
- package/docs/configuration.md +86 -24
- package/docs/design.md +395 -56
- package/docs/downstream.md +2 -2
- package/docs/goal.md +12 -3
- package/docs/hooks.md +6 -7
- package/docs/loops.md +288 -59
- package/docs/mcp.md +22 -14
- package/docs/recipes.md +6 -0
- package/docs/screenshot.png +0 -0
- package/docs/triggers.md +120 -26
- package/docs/troubleshooting.md +37 -26
- package/docs/web-ui-parity.md +691 -65
- package/package.json +4 -3
- package/recipes/autoresearch/recipe.toml +1 -0
- package/recipes/changelog-draft/recipe.toml +1 -0
- package/recipes/ci-sweeper/recipe.toml +1 -0
- package/recipes/daily-digest/recipe.toml +1 -0
- package/recipes/deps-sweeper/recipe.toml +1 -0
- package/recipes/ecosystem/recipe.toml +1 -0
- package/recipes/issue-loop/recipe.toml +1 -0
- package/recipes/pr-watch/recipe.toml +1 -0
- package/skills/pi-loops/SKILL.md +6 -4
- package/src/activity.ts +199 -0
- package/src/archive.ts +21 -9
- package/src/args.ts +6 -2
- package/src/cli-entry.mjs +12 -3
- package/src/cli.ts +746 -125
- package/src/config.ts +53 -5
- package/src/conversation-rows.ts +469 -0
- package/src/danger.ts +227 -6
- package/src/engine.ts +1240 -0
- package/src/goal.ts +71 -0
- package/src/held-read.ts +27 -0
- package/src/hooks.ts +49 -18
- package/src/host-control-channel.ts +455 -40
- package/src/host-control.ts +602 -111
- package/src/host-entry.mjs +5 -3
- package/src/host-runtime.ts +455 -42
- package/src/host.ts +659 -123
- package/src/inbox-card.ts +278 -0
- package/src/inbox.ts +139 -62
- package/src/job-edit.ts +7 -1
- package/src/job-health.ts +14 -2
- package/src/job-lines.ts +201 -13
- package/src/job-owner.ts +74 -13
- package/src/job-tune.ts +110 -0
- package/src/lease.ts +537 -0
- package/src/lock.ts +7 -2
- package/src/mcp-pool.ts +66 -14
- package/src/mcp.ts +1 -1
- package/src/open-file.ts +54 -0
- package/src/page-children.ts +1507 -0
- package/src/page-devices.ts +192 -0
- package/src/page-server.ts +3150 -0
- package/src/path-precheck.ts +215 -0
- package/src/paths.ts +24 -0
- package/src/pi-floor.ts +10 -0
- package/src/pi-loops.ts +1301 -450
- package/src/pi-resolver.d.mts +1 -0
- package/src/pi-resolver.mjs +110 -10
- package/src/presence.ts +12 -63
- package/src/protocol.ts +10 -1
- package/src/recipe.ts +43 -1
- package/src/requests.ts +354 -0
- package/src/rule-lines.ts +68 -0
- package/src/run-card.ts +105 -0
- package/src/runner.ts +18 -1
- package/src/schedule.ts +1 -2
- package/src/scheduler.ts +595 -245
- package/src/sdk-runner.ts +321 -27
- package/src/seen.ts +278 -0
- package/src/session-head.ts +21 -0
- package/src/share.ts +12 -2
- package/src/snapshot.ts +3 -2
- package/src/store.ts +81 -8
- package/src/tools.ts +196 -65
- package/src/trigger-runtime.ts +164 -127
- package/src/triggers.ts +39 -7
- package/src/trust.ts +46 -2
- package/src/ui-prefs.ts +16 -5
- package/src/watch-plan.ts +174 -0
- package/src/web.d.mts +3 -0
- package/src/web.mjs +2260 -2340
- package/src/window-executor.ts +459 -0
- package/src/window-guard.ts +158 -0
- package/docs/agents/issue-tracker.md +0 -34
package/docs/web-ui-parity.md
CHANGED
|
@@ -20,8 +20,83 @@ weaker, and worth converting whenever one of them breaks.
|
|
|
20
20
|
- [x] Submitting while a turn is running queues instead of racing it, the way the TUI does — and a
|
|
21
21
|
slash command typed into it runs while the turn runs, as it does in the terminal.
|
|
22
22
|
`test/web.test.ts`: *a command typed while a turn is running reaches pi as one it can run*.
|
|
23
|
-
- [x] The queue is visible and can be cleared.
|
|
24
|
-
|
|
23
|
+
- [x] The queue is visible and can be cleared. `test/web-page.test.ts`: *the queue badge shows what is
|
|
24
|
+
queued and clears it*.
|
|
25
|
+
- [x] "queued N ✕" clears all N: every row it counts, pi-loops' included, is cancelled as its own ✕
|
|
26
|
+
would cancel it, and pi's own queue is emptied, as 0.22.8's clear emptied pi's whole queue.
|
|
27
|
+
`test/web.test.ts`: *clear cancels every row the badge counts, pi-loops' included, and empties
|
|
28
|
+
pi's own queue*.
|
|
29
|
+
- [x] What pi-loops addressed to this conversation is in the same queue as your messages, with its
|
|
30
|
+
source ("you", "cron nightly", "rule 3f2a…"), in the order it runs, cancellable one at a time.
|
|
31
|
+
A chat message is a request row in `requests/<session>.jsonl`, the file the pi holding the
|
|
32
|
+
session drains; the page reads the file for the list and the count. What pi itself still holds
|
|
33
|
+
(a slash command typed while busy) is listed after them as "pi", counted with them, and cleared
|
|
34
|
+
with them. `test/web-page.test.ts`: *the queue lists requests from every source, in order*.
|
|
35
|
+
- [x] A message you sent survives pi stopping and is offered, never sent on its own, by the next
|
|
36
|
+
page: "not sent before pi stopped", with send and discard. `test/web.test.ts`: *a message typed
|
|
37
|
+
before pi exits is offered, not sent, by the next page*.
|
|
38
|
+
- [x] A send pi refuses (compaction in progress, no credentials) comes back to the composer with the
|
|
39
|
+
error, images included. `test/web.test.ts`: *a send pi refuses comes back to the composer with
|
|
40
|
+
the error*.
|
|
41
|
+
- [x] A session also open in a terminal is answered by that terminal, and the page says so: "open in
|
|
42
|
+
a terminal: answered there", with no pid. `test/web.test.ts`: *a page's message for
|
|
43
|
+
a session a terminal holds is answered there*; `test/web-page.test.ts`: *a send answered
|
|
44
|
+
elsewhere says so, and one pi refused comes back to the composer with the error*.
|
|
45
|
+
- [x] It says so before you send, too: "Your terminal has this conversation. What you send here is
|
|
46
|
+
answered there." above the composer, and the composer's placeholder says where the answer
|
|
47
|
+
goes. Read from `/state`'s `holder`, which follows the rule `/prompt` uses for the line after a
|
|
48
|
+
send, so the two never disagree, and which does not depend on the list. Not shown for a
|
|
49
|
+
terminal that stands aside or a `pi -p` run, which do not answer. `test/web-page.test.ts`: *a
|
|
50
|
+
conversation a terminal holds shows the held line above the composer; one that stands aside,
|
|
51
|
+
or this page's own, does not*, *the held line comes from /state, so a list that fails still
|
|
52
|
+
shows it, and the chat still works*; `test/page-server.test.ts`: */state says who holds the
|
|
53
|
+
conversation, by the rule /prompt follows*.
|
|
54
|
+
- [x] The line promises only what happens. A message reaches the terminal only from a page pi with
|
|
55
|
+
pi-loops in it; one without (its launch did not load pi-loops) answers the message itself, and
|
|
56
|
+
the line then says so: answered here, beside the terminal. A conversation opened from the list
|
|
57
|
+
starts with the pi arguments its project's latest conversation was opened with, so a launch
|
|
58
|
+
that loads pi-loops with `-e` loads it there too; they are never taken from the request. A
|
|
59
|
+
row's `terminal` tag has a title only when that terminal answers nothing sent to it: the one
|
|
60
|
+
holder tag with a title, because its label cannot say that.
|
|
61
|
+
`test/web-page.test.ts`: *a terminal's conversation in a page whose pi has no pi-loops says it
|
|
62
|
+
is answered here, not there*, *a terminal that answers nothing is not said to answer, and that
|
|
63
|
+
is the one holder tag with a title*;
|
|
64
|
+
`test/page-server.test.ts`: */state says whether this page's pi has pi-loops, which is what
|
|
65
|
+
sends a message to the terminal*, *a conversation opened with no arguments of its own starts
|
|
66
|
+
with those of its project's latest conversation*.
|
|
67
|
+
- [x] A terminal that holds the session and cannot answer (its window stands aside, because its copy
|
|
68
|
+
of the conversation is behind the file; a one-shot `pi -p` run) is passed over: it says so on
|
|
69
|
+
its lease, the page's own pi answers your message, and the page does not call it "answered
|
|
70
|
+
there". `test/person-rows.test.ts`: *a browser message for a conversation whose terminal stands
|
|
71
|
+
aside, or runs pi -p, is answered once, by the page's own pi*; `test/web.test.ts`: *a message
|
|
72
|
+
nobody took says why after fifteen seconds, and can be sent here instead*.
|
|
73
|
+
- [x] A message of yours nobody took in fifteen seconds while pi is idle says so on its queue row,
|
|
74
|
+
"not taken (why)", and offers "send it here instead": the row leaves the queue, under
|
|
75
|
+
its lock and only while no holder has stamped it, and the text goes to the page's pi over rpc,
|
|
76
|
+
as every message went before 0.24; a message pi already has is refused, never answered twice.
|
|
77
|
+
When the page's own pi is the one standing aside there is no send-here, since anything written
|
|
78
|
+
then branches the conversation; the row and the send both say /resume. A terminal that holds
|
|
79
|
+
the session and delivers is not called unanswering: "another pi holds this conversation and has not
|
|
80
|
+
taken it yet (it may be busy)", and its offer reads "answer here instead (a branch the other pi will not
|
|
81
|
+
see)". `test/web.test.ts`: *a message nobody took says why after fifteen seconds, and can be
|
|
82
|
+
sent here instead*, *a page whose own pi stands aside says so and does not offer to send around
|
|
83
|
+
it*, *a holder that delivers but has not taken the message is not called unanswering, and the
|
|
84
|
+
offer says it forks*; `test/web-page.test.ts`: *a message nobody took says why and offers to send
|
|
85
|
+
it here, unless this page's pi is the one standing aside*, *a send into a page whose own pi
|
|
86
|
+
stands aside says so, and that pi reads it again*; `test/engine.test.ts`: *a row removed between
|
|
87
|
+
the drain's read and its hand-over is not handed over*. A page's own pi that stands aside (it
|
|
88
|
+
took the conversation back from a terminal that wrote after it read the file) reads the
|
|
89
|
+
conversation again from disk once it is idle, so its messages are answered without a /resume:
|
|
90
|
+
`test/page-server.test.ts`: *a page's pi that took its conversation back standing aside reads
|
|
91
|
+
it again from disk, once per wait*.
|
|
92
|
+
- [x] A page whose pi has no pi-loops in it still chats: the text goes to pi over rpc, no row is
|
|
93
|
+
written, and the queue shown is pi's own. The command list this is decided on is asked for
|
|
94
|
+
again after every slash command and when it is older than thirty seconds, so `/reload` is seen.
|
|
95
|
+
`test/web.test.ts`: *without pi-loops in the page's pi, chat goes over rpc as before, and no row
|
|
96
|
+
is written*, *a command list that changes is asked for again before a message is routed on it*.
|
|
97
|
+
- [x] Abort stops the current turn. `test/web-page.test.ts`: *the stop button asks the server to abort
|
|
98
|
+
the turn*; `test/web.test.ts`: *the run button sends pi the job's run command, and refuses an id
|
|
99
|
+
it cannot use* (which also posts the abort route).
|
|
25
100
|
- [x] Busy, queued count, and "pi exited" are all visible without opening a terminal.
|
|
26
101
|
`test/web-page.test.ts`: *a tool call and a dead pi both reach the page*.
|
|
27
102
|
- [x] Prompt history: ArrowUp and ArrowDown, but only while the caret is on the first or last line,
|
|
@@ -55,8 +130,10 @@ weaker, and worth converting whenever one of them breaks.
|
|
|
55
130
|
`test/web-page.test.ts`: *a phone's soft keyboard cannot steal the tap on send*.
|
|
56
131
|
- [x] What is in the composer survives a reload, and belongs to the tab that typed it: the header's
|
|
57
132
|
reload button, the stale-page banner and a phone that discards a backgrounded tab all come
|
|
58
|
-
back to the same text, and a second tab of the same address keeps its own.
|
|
59
|
-
|
|
133
|
+
back to the same text, and a second tab of the same address keeps its own. A draft belongs to
|
|
134
|
+
its conversation too: a row of the list loads another one in the same tab, and the box there
|
|
135
|
+
starts empty. `test/web-page.test.ts`: *a reload keeps what is in the composer, in that tab
|
|
136
|
+
only*, *a draft typed in another conversation of this tab stays with that conversation*.
|
|
60
137
|
- [x] The composer grows with what is typed into it, up to the ceiling the stylesheet declares, and
|
|
61
138
|
comes back down when the text goes or is sent — a multi-line prompt is written and read at its
|
|
62
139
|
own height rather than through a one-line slot. `test/web-page.test.ts`: *the composer grows
|
|
@@ -64,6 +141,28 @@ weaker, and worth converting whenever one of them breaks.
|
|
|
64
141
|
|
|
65
142
|
## What the feed shows
|
|
66
143
|
|
|
144
|
+
- [x] A conversation a terminal holds shows what the terminal wrote. The page reads it from the
|
|
145
|
+
file, never by switching its own pi to it. The terminal says on its lease when a turn starts
|
|
146
|
+
and ends, and the held line says "answering since 12:03" meanwhile; when the turn is over and
|
|
147
|
+
the file holds a new message, the page reads it again once, with "your terminal answered" as
|
|
148
|
+
the reason. A reload that starts while another one replays what it held draws each message
|
|
149
|
+
once. The run cards saved in the conversation are drawn where they were written, after a
|
|
150
|
+
reload too. A file over 32 MB is not read by the page; the page's pi answers it.
|
|
151
|
+
`test/page-server.test.ts`: *a terminal-held conversation is read from its file, never
|
|
152
|
+
by switching the page's pi, and read again once when the terminal's turn ends*; *a
|
|
153
|
+
terminal-held conversation's run cards come back with its messages, in file order*; *a held
|
|
154
|
+
conversation's file over 32 MB is not read in the always-on process*; *what a terminal added
|
|
155
|
+
is found to hold a message when a large result comes after the message line starts*; *a
|
|
156
|
+
pi-loops view is redacted and capped on the server before the page sees it*; *the page's pi's
|
|
157
|
+
conversation comes back with its run cards in place, asked fresh each time*; *the page's pi's
|
|
158
|
+
conversation over 32 MB is answered with its messages alone*. `test/web-page.test.ts`:
|
|
159
|
+
*a held conversation read again shows its three run cards, and a card saved while the page
|
|
160
|
+
looks is drawn as it lands*; *a run card in the transcript and replayed as it landed is drawn
|
|
161
|
+
once, by its entry id*.
|
|
162
|
+
`test/web-page.test.ts`: *a reload that starts while another replays what it held draws each
|
|
163
|
+
message once, in order*; *the terminal's answer arrives as one numbered reread*; *while the
|
|
164
|
+
terminal answers, the held line says since when*. `test/lease.test.ts`: *shouldReadAgain*.
|
|
165
|
+
|
|
67
166
|
- [x] Assistant text, thinking, tool calls, tool results, errors, and the end of a turn.
|
|
68
167
|
- [x] The conversation is what the conversation is for. A whole stretch of work — thinking and tool
|
|
69
168
|
calls, interleaved as they arrive — is **one** row, closed: what is happening while it happens,
|
|
@@ -72,7 +171,8 @@ weaker, and worth converting whenever one of them breaks.
|
|
|
72
171
|
two — and the stretch continues past the reply rather than being cut in two. One button in
|
|
73
172
|
the header opens or closes every one of them.
|
|
74
173
|
`test/web-page.test.ts`: *a stretch of work is one row, and one button opens every one of
|
|
75
|
-
them*, *a reply without work keeps its own label row
|
|
174
|
+
them*, *a reply without work keeps its own label row*, *the work row's live line is cut in
|
|
175
|
+
characters, not the units a string is counted in*.
|
|
76
176
|
- [x] Replies render as Markdown — headings, lists, quotes, rules, tables, inline code, fenced
|
|
77
177
|
code, and http(s) links. Everything is escaped first: a reply is not trusted input, and a
|
|
78
178
|
tool result quoted inside one is whatever some web page said. `test/web-page.test.ts`: *a
|
|
@@ -92,7 +192,10 @@ weaker, and worth converting whenever one of them breaks.
|
|
|
92
192
|
never arrives silently either: there is a way back to the newest.
|
|
93
193
|
`test/web-page.test.ts`: *an update while you are reading back does not yank the page, and the
|
|
94
194
|
pill is the way back*.
|
|
95
|
-
- [x] Every block says when it happened
|
|
195
|
+
- [x] Every block says when it happened: in a tooltip on a mouse, and written under the block on a
|
|
196
|
+
touch screen, which has no hover to show a tooltip with. `test/web-page.test.ts`: *every block
|
|
197
|
+
keeps its time where a touch screen can show it, not only in a tooltip* (the data; the
|
|
198
|
+
coarse-pointer rule that draws it is checked by reading).
|
|
96
199
|
- [x] Every message, tool call and result can be copied, including over plain http where the
|
|
97
200
|
clipboard API is unavailable. The button waits for a hover on a mouse and is simply always
|
|
98
201
|
there on a touch screen, which has no hover to wait for.
|
|
@@ -122,21 +225,25 @@ weaker, and worth converting whenever one of them breaks.
|
|
|
122
225
|
speaking before a word is read. Status lines are small, monospace and quiet — context, not
|
|
123
226
|
conversation.
|
|
124
227
|
- [x] The conversation has a reading width. A line the width of a 27-inch monitor is not readable.
|
|
125
|
-
- [x] An empty
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
the
|
|
228
|
+
- [x] An empty conversation is a blank feed above a focused composer, whose placeholder says what
|
|
229
|
+
to type; the first message is its first row. `test/web-page.test.ts`: *an empty conversation
|
|
230
|
+
is a blank feed, and the first message is its first row*.
|
|
231
|
+
- [x] The conversation's whole id is the last line of the ⋯ sheet, one click to select and copy;
|
|
232
|
+
⋯ is there at every width. `test/web-page.test.ts`: *the ⋯ sheet ends with the conversation's
|
|
233
|
+
whole id, to copy*.
|
|
130
234
|
|
|
131
235
|
## Automation, which is the reason this project exists
|
|
132
236
|
|
|
133
237
|
- [x] Jobs, loops and rules: what they are, whether they are enabled, and what the last error was.
|
|
238
|
+
A disabled card says `disabled` in words, as `/cron` does, as well as being drawn faint.
|
|
239
|
+
`test/web-page.test.ts`: *a disabled job or rule says so in words, not only by being faint*.
|
|
134
240
|
- [x] **What this project has is what this project has, and the rest is counted rather than
|
|
135
241
|
dropped.** The store is machine-wide and this list is not, so a job made in another directory
|
|
136
242
|
says so as a count — without it, a job somewhere else is indistinguishable from a job that is
|
|
137
|
-
gone.
|
|
138
|
-
|
|
139
|
-
|
|
243
|
+
gone. Loops and rules are counted and labelled apart, each line counted as its command counts
|
|
244
|
+
it: one number covering both agrees with neither command. The project switcher above is the
|
|
245
|
+
way there; the terminal's `/cron all` and `/triggers rules --all` list them. The inbox counts
|
|
246
|
+
other projects' new findings the same way, with no command.
|
|
140
247
|
What counts as "this project" is what the extension says it is — symlinks resolved, `$HOME`
|
|
141
248
|
too broad to be one — because a panel that disagrees with the command is worse than either.
|
|
142
249
|
`test/web.test.ts`: *the panel and /cron agree about what this project is*.
|
|
@@ -144,11 +251,14 @@ weaker, and worth converting whenever one of them breaks.
|
|
|
144
251
|
- [x] **When each job runs next, whatever its schedule.** The page used to work this out itself and
|
|
145
252
|
understood only `every <interval>`, so a job on `0 9 * * *` — the first example in the
|
|
146
253
|
README — showed nothing. A second cron parser in a page with no dependencies was the wrong
|
|
147
|
-
fix:
|
|
148
|
-
`next-runs.json` beside the store when one of them changes
|
|
254
|
+
fix: pi-loops has the evaluator, and writes the answers to
|
|
255
|
+
`next-runs.json` beside the store when one of them changes; when pi-loops is not running, a window
|
|
256
|
+
writes the same file, so a person with only plain jobs still sees their next run. Another
|
|
257
|
+
conversation's plain job is marked as `/cron` marks it, from the same function: open in a
|
|
258
|
+
terminal, in the page, in the background, or resume it to run. A time that has already passed is
|
|
149
259
|
shown as no next run rather than as a past one, a job that is disabled or belongs to another
|
|
150
260
|
machine is not promised one, and a next run that is not today says which day it is.
|
|
151
|
-
`test/scheduler.test.ts`: *
|
|
261
|
+
`test/scheduler.test.ts`: *pi-loops writes when each job runs next*.
|
|
152
262
|
`test/web.test.ts`: *the panel shows the next run of a cron-expression job*.
|
|
153
263
|
`test/web-page.test.ts`: *a next run in April does not render as a time of day*.
|
|
154
264
|
- [x] The name of a job is never abbreviated: it is what `/cron set <name> …` takes, so a clipped
|
|
@@ -165,26 +275,343 @@ weaker, and worth converting whenever one of them breaks.
|
|
|
165
275
|
redraw*; *a rule's condition and action are shown in full*; *a job with a last error says so
|
|
166
276
|
before it is opened*.
|
|
167
277
|
`test/web.test.ts`: *a key in a job's prompt does not reach the page*.
|
|
168
|
-
- [x] Run a job now.
|
|
169
|
-
|
|
170
|
-
- [x]
|
|
171
|
-
|
|
278
|
+
- [x] Run a job now. `test/web.test.ts`: *the run button sends pi the job's run command, and refuses an
|
|
279
|
+
id it cannot use*.
|
|
280
|
+
- [x] **Stop this run** on a loop row that is running, with an inline confirm in the row (*Stop this
|
|
281
|
+
run of pr-watch? Its notes are not updated. The next run happens on schedule.*, or *It will not
|
|
282
|
+
run again by itself.* for a one-shot) and two buttons, **Stop it** and **Keep running**. No
|
|
283
|
+
dialog and no key handler: closing the confirm by any means sends nothing (Keep running, or a
|
|
284
|
+
click anywhere outside the row), and the 8-second redraw keeps it open. Stop this run is shown
|
|
285
|
+
only for a run pi-loops in the background holds. Stop it posts the run's whole id to
|
|
286
|
+
`POST /loops/stop`, which starts no pi; the row says *stopping…*, then *Stopped by you.* when
|
|
287
|
+
the run's record says a person stopped it, *The run ended.* when it ended some other way first,
|
|
288
|
+
*stopping is taking longer than usual* after 8 seconds, or *It finished before the stop reached
|
|
289
|
+
it.*; a run that ends while the confirm is open says *This run has finished.* The waiting
|
|
290
|
+
page's Stop it takes the same path. At 390 px the buttons stack at full width.
|
|
291
|
+
`test/web-page.test.ts`: *a running loop row has Stop this run; the confirm opens inline,
|
|
292
|
+
survives a redraw, and Keep running closes it with nothing sent*; *Stop it posts the whole run id
|
|
293
|
+
to /loops/stop; the row says stopping, then stopped by you once the run is gone*; *a run that
|
|
294
|
+
ends while its confirm is open says so; a stop that came too late says the run finished first*;
|
|
295
|
+
*the loop row's confirm is built around clicks only, and at 390 px its buttons stack at full
|
|
296
|
+
width and at least 32 px tall*; *a click anywhere outside the loop row closes its confirm and sends
|
|
297
|
+
nothing; a running row says running once*; `test/page-server.test.ts`: *POST /loops/stop: the whole run id
|
|
298
|
+
goes to pi-loops, which answers at once; a run that already ended says finished*; *the waiting page's Stop it goes through the same stop as a loop row's, as
|
|
299
|
+
the page's*; *a loop row carries its run, its signal and one suggestion; apply writes one patch
|
|
300
|
+
and a cron_control_plane row, Keep only the kept key, a stale key is 409* (Stop only for a run
|
|
301
|
+
pi-loops holds); *the loop
|
|
302
|
+
routes take the checks every POST takes, a 1 KB body, exactly their fields, and whole ids;
|
|
303
|
+
nothing refused writes or starts a pi*. *Stop it posts the whole run id to /loops/stop…* also
|
|
304
|
+
checks *The run ended.* when the record does not say a person stopped it.
|
|
305
|
+
- [x] A loop row shows its worth when it is not zero, only the parts that are not (*6 findings in 30
|
|
306
|
+
days · 6 dismissed · quiet ×12*), and at
|
|
307
|
+
most one suggestion, **Run hourly instead** or **Pause**, with **Keep**. Nothing applies itself.
|
|
308
|
+
Applying posts the loop's id and the suggestion's key to `POST /loops/tune`; the server works
|
|
309
|
+
the suggestion out again and refuses a key it would not suggest now, and applying writes a
|
|
310
|
+
`cron_control_plane` audit row. Keep changes nothing but the loop's memory of the answer, and the
|
|
311
|
+
suggestion stays dismissed. `test/web-page.test.ts`: *a loop row shows its 30-day signal and
|
|
312
|
+
one suggestion; Keep and the suggestion post only the job, the key and the choice*; *right
|
|
313
|
+
after Apply the row's next run and the note say one time, in one format, and the note is set
|
|
314
|
+
apart from the signal*; the row never shows a next time earlier than now: once the applied
|
|
315
|
+
time has passed, it shows the store's (*an applied next run that has passed gives way to the
|
|
316
|
+
store's, so a row never shows a next time earlier than now*); *a
|
|
317
|
+
suggestion that changed under the page says so instead of acting; nothing applies itself*;
|
|
318
|
+
`test/page-server.test.ts`: *a loop row carries its run, its signal and one suggestion; apply
|
|
319
|
+
writes one patch and a cron_control_plane row, Keep only the kept key, a stale key is 409*; *a
|
|
320
|
+
loop whose findings were all dismissed is offered Pause, and applying it turns the loop off*;
|
|
321
|
+
`test/job-tune.test.ts`.
|
|
322
|
+
- [x] A rule that waits for a path says so on its row: *waits for ~/build.done · next look 09:22 · no
|
|
323
|
+
model call until the file exists*. `test/web-page.test.ts`: *a rule that waits for a path says
|
|
324
|
+
what it waits for and when it looks next*; `test/page-server.test.ts`: *a rule that waits for a
|
|
325
|
+
path says so on its row, with the next look from the process that checks rules*.
|
|
326
|
+
- [x] The inbox as cards above the conversations: decisions first under **Needs you**, each with
|
|
327
|
+
**Waiting on** and **If you don't** when the loop said them, then news. A card names its loop
|
|
328
|
+
only when the list holds more than one loop's findings, and says where a claim runs only when
|
|
329
|
+
that is not the conversation on screen (another conversation's or project's finding, or a
|
|
330
|
+
terminal that takes no messages). **See the run** is there only when the finding has a run.
|
|
331
|
+
Every card carries its finding's whole id; no button names a position. `test/web-page.test.ts`:
|
|
332
|
+
*the inbox shows decisions before news, each row only with a value, and where a claim runs
|
|
333
|
+
only when that is not here*. `test/page-server.test.ts`: *GET /inbox: one project's cards, decisions first,
|
|
334
|
+
fifty at most, a torn line left out, and starts no pi*.
|
|
335
|
+
- [x] **Yes, do it** on a decision and **Look into it** on news claim the finding into the
|
|
336
|
+
conversation that created its loop, and the card says where it runs: queued for your
|
|
337
|
+
terminal, running in a conversation open in the page, starting one, or waiting on pi-loops'
|
|
338
|
+
scheduled turn. `test/web-page.test.ts`: *Yes posts the finding's id and this conversation,
|
|
339
|
+
nothing else, and shows where it runs*. `test/page-server.test.ts`: *a claim from the page
|
|
340
|
+
queues the born-left inbox: row for the terminal that holds the conversation, once*; *a claim
|
|
341
|
+
for a conversation nobody holds starts its pi and rings it with the row's fingerprint*; *a
|
|
342
|
+
claim for a conversation whose pi has no pi-loops sends that pi nothing*.
|
|
343
|
+
- [x] A finding whose loop belongs to its project (a starter added before its conversation was
|
|
344
|
+
saved) runs in the conversation you claim it from: **Yes, do it here** claims it into the one
|
|
345
|
+
open here. `test/web-page.test.ts`: *a project-owned finding runs in the conversation it is
|
|
346
|
+
claimed from: Yes, do it here, and no line to say so*. `test/page-server.test.ts`: *a
|
|
347
|
+
project-owned finding (a starter's loop installed before its conversation was saved) says it
|
|
348
|
+
runs where it is claimed from, and is claimed into the asking conversation*.
|
|
349
|
+
`test/scoping.test.ts`: *a recipe installed in a conversation pi has not saved makes a
|
|
350
|
+
project-owned loop; in a saved one the loop keeps its conversation*. Live: J7 checks the
|
|
351
|
+
starter's job has no creating conversation.
|
|
352
|
+
- [x] A finding whose conversation is gone is claimed into the one open here, when it is in the
|
|
353
|
+
finding's project (**Yes, do it here**). `test/page-server.test.ts`: *a claim whose
|
|
354
|
+
conversation is gone runs in the page's own when it is in the project, and is refused
|
|
355
|
+
otherwise*. Listed for another project, it says "its conversation is gone; runs in a new
|
|
356
|
+
conversation in <project>", and **Yes, do it** starts that conversation (as + New does
|
|
357
|
+
there), claims the finding into it and goes to it; one that cannot start claims nothing and
|
|
358
|
+
says why. `test/web-page.test.ts`: *a card whose conversation is gone runs in this one in its
|
|
359
|
+
own project, and in a new conversation there in another project*.
|
|
360
|
+
- [x] A project-owned finding listed for another project says "runs in a new conversation in
|
|
361
|
+
<project>", and **Yes, do it** starts that conversation, claims the finding into it and goes
|
|
362
|
+
to it, as a gone one does; in the open conversation's project it stays as above.
|
|
363
|
+
`test/web-page.test.ts`: *a project-owned card listed for another project runs in a new
|
|
364
|
+
conversation there: started, claimed into, and opened*.
|
|
365
|
+
- [x] The Loops pane lists loops and rules and nothing else; the list's cards and the tab title
|
|
366
|
+
count the inbox. The desktop rail and the phone's tab bar call the list
|
|
367
|
+
**Inbox**. `test/web-page.test.ts`: *the Loops pane lists loops: no inbox count and no summary
|
|
368
|
+
line, and its rail button says Inbox for the list*.
|
|
369
|
+
- [x] Every loop row is ▸, its name and schedule, and **run** on its first line; a long name moves
|
|
370
|
+
the schedule under it, never ▸ or run onto a line of their own. In a pane of loops, the plain
|
|
371
|
+
job is the one marked: its schedule ends `· in its conversation`.
|
|
372
|
+
`test/web-page.test.ts`: *a loop row is its name, schedule and run on one line; the 30-day
|
|
373
|
+
line only when it is not zero*.
|
|
374
|
+
- [x] **No…** and **Dismiss…** take a reason chip (`Not a bug`, `Already fixed`, `Duplicate`, `Too
|
|
375
|
+
noisy`, `Not now`), a typed reason of at most 500 characters, or none; the card then says
|
|
376
|
+
whether the loop's next run hears it. `test/web-page.test.ts`: *No then Not a bug posts that
|
|
377
|
+
reason and says it goes to the loop's next run*; *a typed reason is sent as typed, and the
|
|
378
|
+
field holds at most 500 characters*. `test/page-server.test.ts`: *a dismiss from the page
|
|
379
|
+
stores the reason for the loop's next run and says so; past 500 characters it is refused*.
|
|
380
|
+
- [x] What was found while you were away, above the cards: "Since yesterday 23:10 · 3 findings, 1
|
|
381
|
+
from this conversation · $0.41", counted per project by the function the
|
|
382
|
+
terminal's session-start line uses, each part kept whole when the line wraps; "N from this
|
|
383
|
+
conversation" only when that is not every finding (`test/web-page.test.ts`: *the away card
|
|
384
|
+
names the findings from this conversation only when they are not all of them, as src/seen.ts
|
|
385
|
+
does*). Runs are the
|
|
386
|
+
activity strip's to count, so a card shows only when something was found. The time is the browser's local time measured from the
|
|
387
|
+
server's clock. The first card after a load is scrolled into view, ahead of the open
|
|
388
|
+
conversation's row (measured at 390 px by J6, test/journey/journeys/j06-away.mjs, step *the
|
|
389
|
+
away card on a phone*). `test/web-page.test.ts`: *the away card says what was found since the cursor,
|
|
390
|
+
leaves runs to the strip, and has Mark all seen*; *the page's
|
|
391
|
+
since words are src/seen.ts's, across midnight, in two time zones*; *the away card wraps
|
|
392
|
+
between its parts and keeps the inbox card's buttons*. `test/away-agree.test.ts`: *the
|
|
393
|
+
terminal's helper and the page's GET /inbox give the same away summary from the same files*.
|
|
394
|
+
`test/page-server.test.ts`: *GET /inbox carries the project's away summary, counted as the
|
|
395
|
+
terminal counts it*.
|
|
396
|
+
- [x] **Mark all seen** moves the project's cursor to what the card counted and clears the
|
|
397
|
+
terminal's next session-start line too; the findings stay new and the cards stay. A refused
|
|
398
|
+
mark keeps the card and says why. `test/web-page.test.ts`: *the away card: the first window's
|
|
399
|
+
words, nothing without a finding, and a refused mark keeps the card*. `test/page-server.test.ts`: *POST /inbox/seen takes the checks every POST takes, a
|
|
400
|
+
512-byte body, a known project and a time not in the future, and writes only seen.json*.
|
|
401
|
+
- [x] A project with no loop and no rule offers three starters above the cards (daily-digest,
|
|
402
|
+
pr-watch, changelog-draft), each its name, a `≈ $N/day` chip when it costs something, its
|
|
403
|
+
recipe's `line` (one sentence of at most twelve words; its summary when it has none), whole, and
|
|
404
|
+
**Add**. Only for the open conversation's project, since Add installs there.
|
|
405
|
+
`test/web-page.test.ts`: *a starter's line shows whole, never cut, and a starter that costs
|
|
406
|
+
nothing shows no cost*. `test/recipe.test.ts`: *every packaged recipe says what it does in one
|
|
407
|
+
sentence of at most twelve words*; *a starter carries its line whole, and a recipe without one
|
|
408
|
+
carries its summary*. `test/page-server.test.ts`: *GET /inbox offers three starters, in the design's
|
|
409
|
+
order, only for the open conversation's project and only while it has no loop and no rule*.
|
|
410
|
+
- [x] **Add** sends `/recipe add <name> --level report` to this conversation's pi, exactly what a
|
|
411
|
+
person may type in the composer, and the wizard's questions are this page's dialogs; closing
|
|
412
|
+
one installs nothing and Add is live again. A refusal says why. In a conversation a terminal
|
|
413
|
+
holds, Add is replaced by the terminal command, with **Copy**. `test/web-page.test.ts`: *an
|
|
414
|
+
empty project shows three starters, each its name, one line and Add, and Add runs the wizard
|
|
415
|
+
with the constant command*; *a starter the pi refuses says why; a terminal-held conversation shows
|
|
416
|
+
the terminal command instead of Add*.
|
|
417
|
+
- [x] An open reason box keeps its text, its selection and its focus when the list is drawn again
|
|
418
|
+
(the 15-second poll, or an action on another card), and closing it with **Cancel** or **No…**
|
|
419
|
+
keeps the text for when it is opened again. `test/web-page.test.ts`: *an open reason box
|
|
420
|
+
keeps its text, selection and focus across the list's redraw and a button's*.
|
|
421
|
+
- [x] A finding another page or a terminal handled first says so, and a post that failed keeps its
|
|
422
|
+
buttons. `test/web-page.test.ts`: *a finding another page already handled says so; a failed
|
|
423
|
+
post keeps the buttons*.
|
|
424
|
+
- [x] A finding's text is shown as text: HTML in it makes no element. `test/web-page.test.ts`: *a
|
|
425
|
+
finding's HTML is shown as text and creates no element*.
|
|
426
|
+
- [x] **See the run** opens the run's transcript read-only, folded and redacted, and its close
|
|
427
|
+
button closes it.
|
|
428
|
+
`test/web-page.test.ts`: *See the run opens the run's lines read-only, as text*.
|
|
429
|
+
`test/page-server.test.ts`: *See the run reads a run's transcript under sessions/ and nothing
|
|
430
|
+
else*.
|
|
431
|
+
- [x] On a phone, inbox buttons are at least 32 px tall (40 px on a touch screen) and a card wraps
|
|
432
|
+
rather than scroll sideways. Measured in a real browser at 390 px by the J3 journey, in its
|
|
433
|
+
step *the inbox pane on a phone* (test/journey/journeys/j03-decide.mjs: every visible card
|
|
434
|
+
button's `offsetHeight`, and `scrollWidth` against `innerWidth`); the rules themselves are
|
|
435
|
+
pinned by `test/web-page.test.ts`: *inbox card buttons are at least 32 px tall and the card
|
|
436
|
+
wraps rather than scroll sideways*.
|
|
437
|
+
- [x] A claim's card follows the server: queued while its row waits, then "Handed to that
|
|
438
|
+
conversation." once the row is gone, also when it is gone before the page looks again; the
|
|
439
|
+
list's and the panel's counts are asked for again with it. A disabled button looks disabled
|
|
440
|
+
and does not change on hover. `test/web-page.test.ts`: *a claim's card follows the server*;
|
|
441
|
+
*a claim whose row is taken before the page looks again says it was handed over, at once*;
|
|
442
|
+
a claim whose row is cancelled from this page's queue says "Cancelled; nothing was sent. The
|
|
443
|
+
finding stays claimed." instead (*a claim cancelled from this page's queue says nothing was
|
|
444
|
+
sent and the finding stays claimed, not that it was handed over*);
|
|
445
|
+
*a disabled card button looks disabled, and a disabled button does not change on hover*.
|
|
446
|
+
- [x] **Loop activity** between the conversation and the composer: closed, one line ("loop
|
|
447
|
+
activity · 5 runs today · 1 failed · 1 rule result"); open, a row for each loop run and each
|
|
448
|
+
matched or failed rule check of this project over the last day, newest first, at most 100. A
|
|
449
|
+
run that carded in the conversation on the screen says `in chat`, and no other row does; a removed loop says `(removed)`; a rule result names its
|
|
450
|
+
condition and points at its card. Only a loop's first three runs and its failures write into
|
|
451
|
+
the conversation (docs/loops.md "Run cards"), so the strip is where every other run is seen.
|
|
452
|
+
The rule rows come from the last 256 KB of the trigger audit, which can hold less than a day
|
|
453
|
+
when checks write long summaries. All text is drawn as text. `test/web-page.test.ts`: *the
|
|
454
|
+
activity strip is one line until opened, then lists every run and rule result as text*; *the
|
|
455
|
+
strip draws 96 items open within 200 ms*; *the strip says in chat only for a carded run of the
|
|
456
|
+
conversation on the screen*. `test/page-server.test.ts`: *GET /activity: one
|
|
457
|
+
project's runs and rule results*. `test/activity.test.ts`: *five runs of a loop are five items*.
|
|
458
|
+
- [x] On a phone the strip stays one line closed and opens as a sheet that scrolls inside itself;
|
|
459
|
+
nothing in it scrolls sideways. The open strip takes its own room below the conversation, and a
|
|
460
|
+
conversation read to its end stays at its end. `test/web-page.test.ts`: *the strip stays one
|
|
461
|
+
line on a phone and nothing in it scrolls sideways*; *opening the strip keeps the end of the
|
|
462
|
+
conversation in view*.
|
|
463
|
+
- [x] The tab title counts this conversation's project: new decisions, then other new findings,
|
|
464
|
+
each part left out at zero: `(1⚑ 2) pi-loops`, `(2) pi-loops`, `pi-loops`. A finding with no
|
|
465
|
+
project counts in every project, as the inbox lists it; a conversation in no project known
|
|
466
|
+
counts nothing and gets no strip rows. An installed page shows the decisions as its badge too.
|
|
467
|
+
`test/web-page.test.ts`: *the tab title counts this project's new decisions, then its news,
|
|
468
|
+
each left out at zero*. `test/activity.test.ts`: *a finding with no cwd is counted in the title
|
|
469
|
+
of every project, as the inbox lists it*; *a conversation in no project known is told nothing
|
|
470
|
+
of any project*.
|
|
471
|
+
- [x] Notifications, off until switched on in the session menu, fire only while the tab is hidden:
|
|
472
|
+
a new decision anywhere on the machine, a loop entering a streak of three failures, the daily
|
|
473
|
+
budget reached. Never for a run, news, a rule result, a visible tab or what was there when the
|
|
474
|
+
page loaded; each fires once. Permission is asked only from the switch. On plain http (a phone
|
|
475
|
+
over a tailnet without https) the switch says it needs https or localhost. The page asks on its
|
|
476
|
+
eight-second poll, which a browser slows to about once a minute in a hidden tab, so a
|
|
477
|
+
notification can be up to a minute late. `test/web-page.test.ts`: *a notification fires only
|
|
478
|
+
while the tab is hidden*; *on plain http the notification switch says it needs https or
|
|
479
|
+
localhost and does nothing*.
|
|
480
|
+
- [x] A rule result's card names the rule's condition on its origin line, and its headline is what
|
|
481
|
+
the check said after its `matched <rule-id>`. `test/web-page.test.ts`: *a rule result's card
|
|
482
|
+
names its rule's condition on the origin line*. `test/inbox-card.test.ts`: *a rule result's
|
|
483
|
+
card names the rule's condition, redacted and clipped*; *a rule result's headline is what the
|
|
484
|
+
check said after its `matched <rule-id>` token*.
|
|
485
|
+
- [x] While nothing runs loops, a loop's card promises no next run and says `paused: <why>`, and
|
|
486
|
+
the status bar names the reason. `test/web.test.ts`: *with nothing running loops, the panel
|
|
487
|
+
promises no next run, says paused, and still shows the last check*.
|
|
488
|
+
- [x] Every time on the page is in one format, the away card's 24-hour words ("15:05", "yesterday
|
|
489
|
+
15:05", "tomorrow 09:00", "Apr 3 2027 09:00"), the same as the terminal's.
|
|
490
|
+
`test/web-page.test.ts`: *every time on the page is in one format, the away card's 24-hour
|
|
491
|
+
words, ahead as well as behind*.
|
|
492
|
+
- [x] Runtime: MCP servers and their state, hook count and events, tool count, last poll, snapshot
|
|
493
|
+
age and version. Whether pi-loops is running is the status bar's.
|
|
172
494
|
- [x] A count leads to the list behind it — "24 tools" answers the wrong question on its own.
|
|
173
495
|
`test/web-page.test.ts`: *a count in the panel leads to the list behind it*.
|
|
174
|
-
- [x] Goal state and the
|
|
496
|
+
- [x] Goal state when a goal is set, and pi-loops for the runtime, each one click open.
|
|
497
|
+
`test/web-page.test.ts`: *the Loops pane shows Goal only while a goal is set, and has no This
|
|
498
|
+
conversation section*.
|
|
175
499
|
- [x] A malformed job in a store file does not blank the panel. `test/web-page.test.ts`: *a job
|
|
176
500
|
with the wrong types in it does not blank the sidebar*.
|
|
177
|
-
- [x]
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
501
|
+
- [x] The panel is the loops rail, and it never disappears: what a loop is doing is the reason to
|
|
502
|
+
open this on a phone. Wide, it is a column beside the conversation, which the rail's Loops
|
|
503
|
+
button gives back to the conversation, remembered; up to 1100 px it is a drawer from that
|
|
504
|
+
button; on a phone (760 px and less) it is the Loops tab.
|
|
505
|
+
- [x] Thirteen controls do not fit across a narrow conversation. Below 1000 px of conversation,
|
|
506
|
+
whatever is beside it, the ones you reach for mid-conversation (undo, find, compact and resume)
|
|
507
|
+
stay in the header on one row with ⋯, and the header stays two rows at a 1280 or 1440 px
|
|
508
|
+
window; reload, save, share, clear, add device, theme and notifications go behind ⋯. The
|
|
509
|
+
sheet lists all of them, moved — not copied — into it. On a phone (760 px and less) every one is behind ⋯.
|
|
510
|
+
`test/web-page.test.ts`: *the header's secondary actions move into a sheet and back*, *below
|
|
511
|
+
1000 px of conversation, undo, find, compact and resume stay in the header and the rest go
|
|
512
|
+
behind ⋯*.
|
|
513
|
+
|
|
514
|
+
- [x] **A watch from one sentence.** `+ Watch for…` beside `+ New` opens the box *Watch for…*: one
|
|
515
|
+
sentence field, named for a screen reader, and three example sentences (English and Chinese);
|
|
516
|
+
an example fills the box. `Read it` sends the
|
|
517
|
+
sentence as the ordinary chat message `Watch for: <sentence>`, queued and cancellable like
|
|
518
|
+
any other; `cron_create` and `new_trigger` answer it by asking the person, through pi's own
|
|
519
|
+
select, before they create anything.
|
|
520
|
+
`test/web-page.test.ts`: *the Watch for… box opens with three examples and no caption, and an
|
|
521
|
+
example fills it*.
|
|
522
|
+
- [x] A sentence with no time word and nothing to wait for gets the card "This reads like something
|
|
523
|
+
to do now, not something to watch." from the page itself: nothing is sent and no model is
|
|
524
|
+
called. `test/web-page.test.ts`: *a sentence with no time and nothing to wait for gets the now
|
|
525
|
+
card, and nothing is sent*.
|
|
526
|
+
- [x] The question is a card, in the box and in the transcript, drawn from the select's title rows
|
|
527
|
+
and options alone: the kind (*On a schedule* / *When something happens*), when it runs or
|
|
528
|
+
waits for, how often the background looks for a rule, what it does, for a schedule whose
|
|
529
|
+
result goes to the inbox what it remembers and whether a checker reviews each finding, the
|
|
530
|
+
policy's reason when the action is one the unattended guard would refuse, and the choices:
|
|
531
|
+
Result (this conversation or the inbox; a rule's inbox chip reads *Put it in my inbox*, since
|
|
532
|
+
a matched rule's result reaches the inbox and the loop activity strip), for a rule Repeats
|
|
533
|
+
(once or every time), and for a schedule in the inbox Checks (none or each finding). The chips start on the first option, which is the model's own choice. A select that
|
|
534
|
+
is not a watch question stays a dialog. Nothing is written until the person answers.
|
|
535
|
+
`test/web-page.test.ts`: *a watch question draws the card from its title and options, in the
|
|
536
|
+
box and in the transcript, and no dialog*, *a select that is not a watch question stays a
|
|
537
|
+
dialog*; `test/scoping.test.ts`: *with a person in their own turn, new_trigger asks through
|
|
538
|
+
pi's select and Set it up creates the rule in the same call*.
|
|
539
|
+
- [x] `Set it up` answers the select with the one option the chips name, and `Cancel` with
|
|
540
|
+
*Cancel*, through `/ui-response` like any dialog; one answer per question. Cancel writes
|
|
541
|
+
nothing to `jobs.json` or `triggers.json`. Cancel comes first so Enter hits it.
|
|
542
|
+
`test/web-page.test.ts`: *the card's chips name the option Set it up sends, and Cancel sends
|
|
543
|
+
Cancel*; `test/page-server.test.ts`: *a watch question is a select like any dialog:
|
|
544
|
+
/ui-response carries the chosen option to pi, and the /watch route is gone*;
|
|
545
|
+
`test/scoping.test.ts`: *Cancel, Esc, a timeout and an abort all return cancelled in the same
|
|
546
|
+
call and leave jobs.json and triggers.json as they were*.
|
|
547
|
+
- [x] The tool's result in the same turn turns every card of the question into a receipt (*Set up
|
|
548
|
+
dyn-… · shows here · once*, *Not set up*, or *Not set up:* and why the create failed).
|
|
549
|
+
`test/web-page.test.ts`: *the tool's result in the same turn turns every card of the question
|
|
550
|
+
into a receipt*.
|
|
551
|
+
- [x] A card in the box that is set up or cancelled closes the box, the way its close button does,
|
|
552
|
+
and gives the focus back to the composer; the transcript's card keeps the receipt. A create
|
|
553
|
+
that failed keeps the box open, with the sentence in it to change. A watch that is set up
|
|
554
|
+
refreshes the page's state and the inbox at once, so the starters and the inbox count do not
|
|
555
|
+
wait for the next timer. `test/web-page.test.ts`: *Set it up closes the Watch for… box, gives
|
|
556
|
+
the composer back and refreshes the state and the inbox; a failure keeps the box*.
|
|
557
|
+
- [x] The card shows the whole action and condition, redacted, never cut; an action's further lines
|
|
558
|
+
stay in its own row. `test/watch-plan.test.ts`: *the card shows the whole action and
|
|
559
|
+
condition, redacted, and says when a loop is checked*, *askTitle indents a value's further
|
|
560
|
+
lines, so no line of an action can pass for a row of its own*.
|
|
561
|
+
- [x] On a short screen the box's body scrolls, Read it stays in the box's own menu, a card's
|
|
562
|
+
Cancel and Set it up stick to the bottom of what is in view, and a card is scrolled to the top
|
|
563
|
+
of the box when it is drawn, with its status and buttons as one footer so the bar never
|
|
564
|
+
covers the status. Closing the box keeps a waiting card; it is there when you reopen it, and a
|
|
565
|
+
settled one goes. `test/web-page.test.ts`: *closing the box keeps a waiting card; it is there
|
|
566
|
+
when you reopen it*.
|
|
567
|
+
- [x] The now-check knows weekdays (plural too), *nightly*, *noon*, *midnight*, *daily*, *weekly*,
|
|
568
|
+
*at 9am*, *in an hour*, *twice a day*, *weeknights*, *later today* and the Chinese time
|
|
569
|
+
words, and a sentence queued behind a busy turn waits for its own turn's question.
|
|
570
|
+
`test/web-page.test.ts`: *a sentence with no time and nothing to wait for gets the now card,
|
|
571
|
+
and nothing is sent*, *a sentence queued behind a busy turn waits for its own turn's question,
|
|
572
|
+
not the earlier turn's reply*.
|
|
573
|
+
- [x] A question waits thirty minutes, then pi cancels it and the tool says so: the card says *Not
|
|
574
|
+
set up: nobody answered in time*. A question pi answered on its own (an abort, the turn's end)
|
|
575
|
+
says *Not set up* and sends nothing. One still waiting when a tab opens is drawn once, below
|
|
576
|
+
the conversation; one a tool asked whose run ended is not offered again, and one a slash
|
|
577
|
+
command opened during the run (`/session-share`) still is. `test/web-page.test.ts`: *a
|
|
578
|
+
question pi answered on its own is not left waiting: the turn's end, or the question's own
|
|
579
|
+
time, says it was not set up*, *a question still waiting when the page opens is drawn once,
|
|
580
|
+
after the transcript*; `test/page-children.test.ts`: *a dialog a tool asked is dropped
|
|
581
|
+
when the run ends: pi answered it already, and a later tab is not offered it*.
|
|
582
|
+
- [x] A turn that ends with no question shows pi's reply in the box (a question of pi's own, for
|
|
583
|
+
instance) and keeps the sentence for editing; ninety seconds with neither a card nor the end
|
|
584
|
+
of the turn says pi did not answer. `test/web-page.test.ts`: *a turn that ends with no plan
|
|
585
|
+
shows pi's reply and keeps the box*.
|
|
586
|
+
- [x] A conversation a terminal holds is answered there (owner decision O2): both tools ask in that
|
|
587
|
+
terminal before they set anything up, and the box says so.
|
|
588
|
+
`test/web-page.test.ts`: *a terminal that holds the conversation answers the sentence there*.
|
|
589
|
+
- [x] **The terminal asks the same question.** In a terminal, in the person's own turn, both tools
|
|
590
|
+
show pi's selector with the card's rows as its title and the same options, the model's choice
|
|
591
|
+
first; it replaces `new_trigger`'s confirm, so there is one question, never two. A scheduled
|
|
592
|
+
turn, a sub-agent and pi-loops in the background ask nobody. `test/scoping.test.ts`: *the answer
|
|
593
|
+
is promote_to_chat and fire_once for a rule, and stateful and verify for a schedule*, *a
|
|
594
|
+
sub-agent, a scheduled turn and a pi with no dialog keep today's gate: new_trigger asks the
|
|
595
|
+
host's confirm once and cron_create creates*; J8 in `test/journey/journeys/j08-watch.mjs`
|
|
596
|
+
drives the selector in tmux.
|
|
597
|
+
- [x] At 390 px the box and the card fit: nothing scrolls sideways, the chips and buttons wrap, and
|
|
598
|
+
every button is at least 32 px tall (J8 in `test/journey/journeys/j08-watch.mjs`).
|
|
182
599
|
|
|
183
600
|
## How it looks
|
|
184
601
|
|
|
185
602
|
- [x] Light and dark are both designed: every surface is a named colour, not a translucent grey
|
|
186
603
|
over whatever the browser paints, so the page looks the same on a machine whose default
|
|
187
|
-
background is not white.
|
|
604
|
+
background is not white. The colours are Inspire's, in both schemes: three paper levels
|
|
605
|
+
instead of borders (light `#f2f2f7`, `#ffffff`, `#efeff4`; dark `#000000`, `#1c1c1e`,
|
|
606
|
+
`#2c2c2e`), one amber for what you can act on (`#9a5500` light, `#ffb340` dark), and colour
|
|
607
|
+
for state only, so a hue other than amber is a fault. No colour is written outside the token
|
|
608
|
+
block, except the QR code's black on white, which a scanner needs. The Background view is
|
|
609
|
+
drawn from the same block. `test/web-page.test.ts`: *the stylesheet carries Inspire's paper
|
|
610
|
+
and amber in both schemes, and no colour outside its tokens*; `test/page-server.test.ts`: *the
|
|
611
|
+
Background view is drawn in the page's palette, both schemes, and links back to the page*.
|
|
612
|
+
- [x] Focus is visible wherever the keyboard puts it: an amber ring on `:focus-visible`, and no
|
|
613
|
+
rule hides it. `test/web-page.test.ts`: *the stylesheet carries Inspire's paper and amber in
|
|
614
|
+
both schemes, and no colour outside its tokens*.
|
|
188
615
|
- [x] The stored theme is applied before the first paint. Applied later, a dark page renders light
|
|
189
616
|
and then blinks. `test/web-page.test.ts`: *the stored theme is applied before the first
|
|
190
617
|
paint*.
|
|
@@ -203,6 +630,13 @@ weaker, and worth converting whenever one of them breaks.
|
|
|
203
630
|
- [x] A confirmation shows what is about to happen apart from the reasoning about it, so it cannot
|
|
204
631
|
be read as prose and waved through. `test/web-page.test.ts`: *a confirmation shows what is
|
|
205
632
|
about to run*.
|
|
633
|
+
- [x] A question with a countdown (the scheduled-turn guard, a control-plane confirm) closes when
|
|
634
|
+
its time is up and says `timed out with no answer: refused`, since pi refuses it then and tells
|
|
635
|
+
the page nothing; an approve after that is neither sent nor reported as approved, and the next
|
|
636
|
+
question gets the dialog. A browser that attaches later is not offered it, and one offered again
|
|
637
|
+
carries only the time it has left. `test/web-page.test.ts`: *a question pi times out closes
|
|
638
|
+
itself, says it was refused, and a late approve is not reported*; `test/web.test.ts`: *a dialog
|
|
639
|
+
pi times out is not offered again, and one replayed carries the time it has left*.
|
|
206
640
|
- [x] Enter does not approve: the focus starts on cancel, which is written before approve so the
|
|
207
641
|
first menu button is the one Enter hits. `test/web-page.test.ts`: *Enter in a confirmation
|
|
208
642
|
dialog hits cancel, not approve*.
|
|
@@ -244,30 +678,91 @@ weaker, and worth converting whenever one of them breaks.
|
|
|
244
678
|
than a list of its own.
|
|
245
679
|
`test/web-page.test.ts`: *the thinking-level picker offers every level pi knows, not a list of its own*.
|
|
246
680
|
- [x] Attaching an image is refused, with the reason, by a model that cannot see one.
|
|
681
|
+
`test/web-page.test.ts`: *a model that cannot see images has attach disabled and a pasted image
|
|
682
|
+
refused*.
|
|
247
683
|
- [x] Cost and token counts.
|
|
248
684
|
- [x] Compact, undo (fork from your last message), find across the whole session including
|
|
249
685
|
abandoned branches, export to HTML, and share as a redacted gist. Undo reloads the
|
|
250
686
|
conversation through the one path every other thing that empties the feed uses, so an undone
|
|
251
|
-
session is as empty as a new one
|
|
252
|
-
|
|
687
|
+
session is as empty as a new one. `test/web-page.test.ts`: *undoing the only message empties
|
|
688
|
+
the conversation and puts the message back in the composer*.
|
|
253
689
|
- [x] Compaction says what it did — what the context was and what it became, and what the summary
|
|
254
690
|
cost — and what it keeps can be steered: `/compact keep the API shapes`, as in the terminal.
|
|
255
691
|
A line that reports neither is a line asking to be taken on faith.
|
|
256
692
|
`test/web.test.ts`: *the compact button can steer what the summary keeps*.
|
|
257
693
|
`test/web-page.test.ts`: *clear, resume and compact are typed as well as clicked*.
|
|
694
|
+
- [x] **This project's conversations are listed beside the conversation, and reading the list starts
|
|
695
|
+
no pi.** Each row is the conversation's title (its `/name`, else the first thing said), when it
|
|
696
|
+
last changed, and who holds it: `terminal`, `open` (the page), `pi-loops` (in the background), or
|
|
697
|
+
no tag; `· open here` when the page also has it. Grouped Today and Earlier when both are there,
|
|
698
|
+
the open one marked.
|
|
699
|
+
A row loads that conversation's own page, so Back, a new tab and a bookmark work as links do.
|
|
700
|
+
Conversations from before 0.23, or from a pi without pi-loops, are listed and open too: pi's
|
|
701
|
+
own session files are what is read. `test/web-page.test.ts`: *the list shows this project's
|
|
702
|
+
conversations, grouped Today and Earlier when both are there, with the open one marked, and
|
|
703
|
+
each row is its address*, *a row says who holds it: terminal, open, pi-loops, or nothing at all*, *showing the
|
|
704
|
+
list asks for no other conversation, and rides on the page's own poll at most every fifteen
|
|
705
|
+
seconds*, *a title cannot carry markup or reorder its line*; `test/page-server.test.ts`: *GET
|
|
706
|
+
/conversations lists a project's conversations from pi's files, behind the token, and starts no
|
|
707
|
+
pi*, *a conversation no lease names opens from pi's own session file, and a made-up id is still
|
|
708
|
+
404*; `test/conversation-rows.test.ts`.
|
|
709
|
+
- [x] A row says what needs you (`⚑ N`, a loop it made is waiting on a decision) and has a dot when
|
|
710
|
+
it changed since this browser last looked; a terminal's conversation never has the dot. The
|
|
711
|
+
conversation on the screen is read as of each poll, so the one you watched never gets it back.
|
|
712
|
+
`test/web-page.test.ts`: *the list has no tabs: every row is shown, the dot marks what changed
|
|
713
|
+
since this browser last looked*, *the conversation on the screen is read as of now, not as of
|
|
714
|
+
the list's last draw*.
|
|
715
|
+
- [x] Another project is one choice away: the switcher lists every project pi has conversations in,
|
|
716
|
+
and those with only a loop or a lease, each by its folder's name with the whole path as its
|
|
717
|
+
title. It is the one place the page names the project. `test/web-page.test.ts`: *choosing
|
|
718
|
+
another project asks for that project's list by its cwd*.
|
|
719
|
+
- [x] + New starts the conversation in the project the list shows. In the open conversation's
|
|
720
|
+
project it is a new session of this page's pi, as before; in another project the server starts
|
|
721
|
+
a pi there (`POST /conversations/new`, a project the list offers and nothing else) and the
|
|
722
|
+
page goes to it, or says why not. `test/web-page.test.ts`: *+ New starts the conversation in
|
|
723
|
+
the project the list shows: a new session here for the open one's, POST /conversations/new and
|
|
724
|
+
its address for another*; `test/page-server.test.ts`: *POST /conversations/new starts one pi in
|
|
725
|
+
a project the page lists, with that project's arguments, and refuses any other cwd without
|
|
726
|
+
starting one*. A project with no arguments of its own still gets pi-loops when the launch
|
|
727
|
+
loaded it with `-e`, and nothing else of another project's: *a new conversation in a project
|
|
728
|
+
with no arguments of its own loads pi-loops with the -e another page's pi was given, and
|
|
729
|
+
nothing else of it*.
|
|
730
|
+
- [x] The list, the composer and send are reachable by keyboard: one row is in the Tab order, the
|
|
731
|
+
arrow keys, Home and End move between rows, Enter opens one, and focus is shown.
|
|
732
|
+
`test/web-page.test.ts`: *the keyboard moves through the list one row at a time, and Enter opens
|
|
733
|
+
the row*.
|
|
734
|
+
- [x] A phone shows one pane at a time, Inbox (the list), Chat and Loops, from tabs at the bottom;
|
|
735
|
+
the pane is in the address, so Back returns to the one before and a row opens on Chat.
|
|
736
|
+
`test/web-page.test.ts`: *on a phone one pane shows at a time, picked by the Inbox, Chat and
|
|
737
|
+
Loops tabs, and a row opens on Chat*.
|
|
738
|
+
- [x] The status bar says whether pi-loops is running in the background (or why loops and checks
|
|
739
|
+
are paused). It is the one place that says pi-loops runs, and its link to the Background view.
|
|
740
|
+
`test/web-page.test.ts`: *the status bar follows the host file the server reads, not the
|
|
741
|
+
snapshot the session last wrote, and the Loops pane does not repeat it*, *the status bar says
|
|
742
|
+
whether pi-loops runs*.
|
|
258
743
|
- [x] **Starting over does not mean going back to a terminal.** The context is finished with far
|
|
259
744
|
more often than the window is: a new session and going back to an earlier one are both here,
|
|
260
|
-
by button and by `/clear`, `/new` and `/resume` typed
|
|
745
|
+
by button (the list's + New is the header's clear) and by `/clear`, `/new` and `/resume` typed
|
|
746
|
+
in the composer — the habit comes from a
|
|
261
747
|
terminal and the muscle memory arrives with it. Neither deletes anything: the session being
|
|
262
748
|
left is a file that `resume` lists, labelled by what was said in it rather than by a filename.
|
|
263
749
|
`test/web.test.ts`: *a new session is a path pi has not written yet, and going back is one it
|
|
264
750
|
has*. `test/web-page.test.ts`: *the session you are in is not offered as one to go back to*.
|
|
751
|
+
- [x] Going back to a conversation that is already open in the page, in another tab, goes to it:
|
|
752
|
+
the server answers with that conversation's address and the page opens it, as 0.22.8
|
|
753
|
+
switched to it. `test/web-page.test.ts`: *resuming a conversation that is already open in the
|
|
754
|
+
page goes to it*.
|
|
265
755
|
- [x] **The session commands are two words here.** `/sessions` prints this project's sessions into
|
|
266
756
|
the feed — short id, when, what was first said — and `/session export [path]` / `/session
|
|
267
757
|
import <path>` are the archive commands, sent as `/session-export` / `/session-import`. pi's
|
|
268
758
|
own `/session` is a terminal command that does not exist over rpc, which is why the extension
|
|
269
759
|
had to hyphenate its names in the terminal and why the page is free to spell them plainly.
|
|
270
760
|
`test/web-page.test.ts`: */sessions and /session export are typed here*.
|
|
761
|
+
- [x] A session an extension switches to inside pi is followed: `/inbox open` (a finding's run,
|
|
762
|
+
forked into a new conversation) and `/session import <path> --resume` reload the page onto
|
|
763
|
+
it, as a switch made with the buttons does. A rule's result, or a finding from another
|
|
764
|
+
project, is refused with the reason. `test/web.test.ts`: *a session an extension switched to
|
|
765
|
+
inside pi is followed: the page is told to reload*.
|
|
271
766
|
- [x] A session started here is a session pi-loops recognises. pi names the sessions it starts after
|
|
272
767
|
their id and the front end cannot know that id in time, so anything identifying a session by
|
|
273
768
|
its file name is wrong about this one — which parked every inject-and-run cron job made in a
|
|
@@ -286,24 +781,52 @@ weaker, and worth converting whenever one of them breaks.
|
|
|
286
781
|
a model it was never had with. A model whose credentials are gone is reported rather than
|
|
287
782
|
forced. `test/web.test.ts`: *going back to an earlier session puts it back on the model it was
|
|
288
783
|
last using*.
|
|
289
|
-
- [x] `--continue
|
|
290
|
-
two windows.
|
|
291
|
-
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
784
|
+
- [x] `--continue` and `--session <id|prefix>` open that conversation in the page, so a session
|
|
785
|
+
moves between the two windows. `--resume` is pi's picker, which the page has as **resume**;
|
|
786
|
+
`pi-loops --resume` says so rather than open a picker nobody can see.
|
|
787
|
+
`test/cli.test.ts`: *--continue and --session name a conversation pi already has, and leave
|
|
788
|
+
pi's other arguments*, *the page's address flags are refused with the config.toml lines that
|
|
789
|
+
replace them*.
|
|
790
|
+
- [x] **The page outlives the terminal.** pi-loops in the background serves it, and `pi-loops`
|
|
791
|
+
opens it and exits 0, so closing that terminal ends nothing: the page still answers a message,
|
|
792
|
+
and `pi-loops status` counts it as an open conversation. `test/journey/journeys/j01-close-terminal.mjs`
|
|
793
|
+
(J1); `test/cli.test.ts`: *pi-loops asks the running pi-loops to open the conversation, prints
|
|
794
|
+
its page and exits 0*.
|
|
795
|
+
- [x] **One pi per conversation.** Each conversation open in the page has its own `pi --mode rpc`,
|
|
796
|
+
and two opens of one share one start. A conversation with no tab, no turn, no question waiting
|
|
797
|
+
and nothing unsent stops after 15 minutes; at most six are open, and opening a seventh stops the
|
|
798
|
+
least recently used idle one, or says, on a page with a way back, that none is idle.
|
|
799
|
+
`test/page-children.test.ts`: *two opens of one conversation share one start*, *an idle child is
|
|
800
|
+
stopped after the idle time, and one somebody needs never is*, *opening one more than the cap
|
|
801
|
+
stops the least recently used idle child, and none idle is full*; `test/page-server.test.ts`:
|
|
802
|
+
*a conversation that cannot open because the page is full answers a page with the reason and a
|
|
803
|
+
way back*.
|
|
804
|
+
- [x] **Opening a conversation pi-loops is running a scheduled turn in waits for that turn**, on a
|
|
805
|
+
page that names it and has **Stop it**; opening it then would abort the turn. A conversation
|
|
806
|
+
whose pi keeps stopping shows pi's last words and **Start again** instead of starting it a
|
|
807
|
+
fourth time. `test/page-children.test.ts`: *opening reuses, waits for a turn pi-loops is
|
|
808
|
+
running, refuses after the crash budget, else starts*; `test/web.test.ts`: *a pi that refuses
|
|
809
|
+
to start leaves the server answering, and its page says why*.
|
|
810
|
+
- [x] **The Background view.** What pi-loops in the background is doing, in the page's words: every
|
|
811
|
+
conversation with where it is open (a terminal, this page, the background), its loops, rules
|
|
812
|
+
and what waits for it, and what waits in `pi-loops status`'s words, with no pid, every time in
|
|
813
|
+
the page's own format, at `/background`, behind the page's token or a paired browser's device
|
|
814
|
+
cookie. `/background.json` keeps every field. `test/page-server.test.ts`: *GET / with no
|
|
815
|
+
conversation to open is the Background view; /background and /background.json answer to the
|
|
816
|
+
token or a paired browser*, *the Background view shows what waits in the words pi-loops status
|
|
817
|
+
uses, where it is open left to the table*, *the Background view uses the page's words and the
|
|
818
|
+
page's times*, *the Background view's times are written by the page's own clock, from the
|
|
819
|
+
page*. The status bar's "pi-loops running in the background" is the one link to it, and a phone
|
|
820
|
+
shows the status bar under its Loops pane; it links back to the page,
|
|
821
|
+
follows the theme chosen on the page before its first paint, and draws a conversation's title
|
|
822
|
+
as text, as the list does. `test/page-server.test.ts`: *the Background view draws a
|
|
823
|
+
conversation's title as text: no escape sequence or bidi override*; `test/web-page.test.ts`:
|
|
824
|
+
*the status bar is the one way to the Background view, and a phone shows it under the Loops
|
|
825
|
+
pane*.
|
|
826
|
+
- [x] The bare address does not land on the Background view because the conversation looked at
|
|
827
|
+
last was empty and never saved: it opens a new conversation in that one's project.
|
|
828
|
+
`test/page-server.test.ts`: *GET / after the last conversation was an empty one pi never saved
|
|
829
|
+
opens a new conversation in its project, not the Background view*.
|
|
307
830
|
- [x] **A window older than what is installed says so, across the top, with the button that fixes
|
|
308
831
|
it.** A tab left open across an upgrade looks exactly like a current one — the panel even
|
|
309
832
|
shows a version, but that is the server's, read live — and three rounds of "no reply appears"
|
|
@@ -312,21 +835,121 @@ weaker, and worth converting whenever one of them breaks.
|
|
|
312
835
|
|
|
313
836
|
### Held, with the reason
|
|
314
837
|
|
|
838
|
+
- **A watch card posts only the plan's id, the verdict and the two choices to `POST /c/<sid>/watch`.**
|
|
839
|
+
The route, the desk that kept the plan and the `/pi-loops watch` line pi read from its stdin are
|
|
840
|
+
gone: the card answers pi's own select through `/ui-response`, as every dialog does, with one
|
|
841
|
+
option of a fixed vocabulary that the tool checks against what it offered.
|
|
842
|
+
- **The `pi-loops:watch` note as the receipt, and Read it again on a card that failed or was gone.**
|
|
843
|
+
The tool result in the same turn is the receipt now, and the model reads that result; nothing
|
|
844
|
+
else is written into the conversation. A failure keeps the box open with the sentence in it, so
|
|
845
|
+
the box's own Read it does what Read it again did; a plan cannot be gone while its question is open.
|
|
846
|
+
- **A Set it up with no note in a minute gives the buttons back.** The answer goes to the select pi
|
|
847
|
+
is waiting on, in the same process, so there is no second step that can stall; a question pi
|
|
848
|
+
stopped waiting for says *Not set up* at the turn's end or its own timeout instead.
|
|
849
|
+
|
|
850
|
+
- **An empty conversation that says what it is ("A pi session, in a browser.").** Owner decision
|
|
851
|
+
for 0.24.0, the layout explains: a blank conversation with a focused composer explains itself,
|
|
852
|
+
and the composer's placeholder says what to type. The undo that empties a conversation still goes
|
|
853
|
+
through the one reload path (*undoing the only message empties the conversation and puts the
|
|
854
|
+
message back in the composer*).
|
|
855
|
+
- **The hint row under the composer ("Enter send · Shift+Enter newline · paste images").** It
|
|
856
|
+
repeated the placeholder. Enter sends and Shift+Enter inserts a newline as before (the first line
|
|
857
|
+
of this list), and pasting an image still attaches it. The conversation's id it showed is the last
|
|
858
|
+
line of the ⋯ sheet.
|
|
859
|
+
- **A starter's meta line (`reads only`, the budget in words, "asks you to describe your tracker
|
|
860
|
+
first") and the "or describe a loop" link.** `reads only` was true of every starter; the cost is
|
|
861
|
+
a chip by the name; the wizard asks for the tracker when it needs it; and `+ Watch for…` above the
|
|
862
|
+
list is the one way to describe a loop.
|
|
863
|
+
- **A dim `not said` under a decision, the meta line on every card, and a greyed See the run.** A row
|
|
864
|
+
shows only when the loop gave it a value, a card says where a claim runs only when that is not the
|
|
865
|
+
conversation on screen, and See the run shows only when there is a run to see. The claim's own
|
|
866
|
+
answer still says where it runs (*Yes posts the finding's id and this conversation, nothing else,
|
|
867
|
+
and shows where it runs*).
|
|
868
|
+
- **"pi-loops running in the background (pid N): loops and checks run there" in the Loops pane, its
|
|
869
|
+
link "What pi-loops is doing in the background", and the rail's Background icon.** The status bar
|
|
870
|
+
says it and is the link to the Background view, on a phone under the Loops pane; a paused reason
|
|
871
|
+
is said there too.
|
|
872
|
+
- **The Loops pane's summary line ("inbox 3 new (1 decision) · 2 job(s) · 0 rule(s)") and its "last
|
|
873
|
+
run" line.** The inbox's cards and the tab title count the inbox; the pane lists the loops; the
|
|
874
|
+
activity strip lists every run.
|
|
875
|
+
- **The away card's run count and its Show what needs me button.** The activity strip counts runs
|
|
876
|
+
and their failures; the **Needs you** heading leads to the decisions.
|
|
877
|
+
- **The Loops pane's This conversation section (its name, message count and model), and Goal's
|
|
878
|
+
`none`.** The conversation is on screen; the model is in the header. Goal shows only while a goal
|
|
879
|
+
is set.
|
|
880
|
+
- **A loop row's `runs N`.** The activity strip counts runs.
|
|
881
|
+
- **The status bar's loop count ("2 loops") and its terminal count ("1 conversation open in a
|
|
882
|
+
terminal").** The pane lists them; the row's `terminal` tag says it.
|
|
883
|
+
- **The All, Needs you and Unread tabs above the list, and their counts.** They counted and filtered
|
|
884
|
+
the conversation rows only, yet sat above the finding cards and shared their **Needs you**
|
|
885
|
+
heading. The cards are the view of what needs you, and each row keeps its `⚑ N` and its unread
|
|
886
|
+
dot (*the list has no tabs: every row is shown, the dot marks what changed since this browser last
|
|
887
|
+
looked*).
|
|
888
|
+
- **The Watch card's terminal line (`/new-trigger …`, `/cron add …`), its *Model: only once the path
|
|
889
|
+
exists* row, and the `WATCH` tag over it in the transcript.** The rows are the plan; the
|
|
890
|
+
terminal's question keeps the line. Which model runs, and when, is a cost detail the person does
|
|
891
|
+
not choose. The card's heading says what the card is.
|
|
892
|
+
- **The Watch for… box's label, its "For example" caption, its project in the title, and the plan
|
|
893
|
+
card's "Not right?" line.** The title and the placeholder say what the box takes, the examples
|
|
894
|
+
are examples, and *Read it* reads the edited sentence again. The project is the one the list
|
|
895
|
+
shows above the button.
|
|
896
|
+
|
|
897
|
+
- **`/inbox open` (start a new conversation from the run that filed a finding).** A fork writes a
|
|
898
|
+
conversation, and the page's **See the run** covers looking at the run. The terminal keeps it.
|
|
899
|
+
|
|
900
|
+
- **"After Stop, nothing queued starts a turn until you send again", and the "send queued (n)"
|
|
901
|
+
button.** Owner decision, 2026-11-02: Esc in the terminal and Stop in the browser end the current
|
|
902
|
+
turn only, as in 0.22.8, and what is queued runs once pi is idle, as messages queued in any chat
|
|
903
|
+
do. In exchange the queue is visible and cancellable everywhere: the page keeps its per-row ✕ and
|
|
904
|
+
"queued N ✕", and the terminal has `/cron queue [cancel <n|id>|clear]`. `test/web.test.ts`: *after
|
|
905
|
+
Stop, the queue is untouched and runs on its own: Stop is pi's abort and nothing else*;
|
|
906
|
+
`test/engine.test.ts`: *after Stop, the person's queued rows run once pi is idle, oldest first, one
|
|
907
|
+
lane*; `test/web-page.test.ts`: *the queue lists requests from every source, in order* (no
|
|
908
|
+
send-queued button after Stop).
|
|
315
909
|
- **pi's own built-in slash commands.** They do not exist in `pi --mode rpc`; there is nothing to
|
|
316
910
|
call. The extension's own commands work.
|
|
317
911
|
- **`/login`.** Its OAuth flow has no rpc equivalent. Log in once with `pi` and the rest follows.
|
|
912
|
+
- **`--no-auth`, and loopback only under it.** The page is now an always-on chat with a shell
|
|
913
|
+
behind it, so an unauthenticated page is refused in 0.24: there is no flag that serves one, and
|
|
914
|
+
`pi-loops --no-auth` exits 2 saying so. The token is required on every address.
|
|
915
|
+
- **`--host`, `--allow-host` and `--port` on the command line.** The page outlives the command, so
|
|
916
|
+
a flag on it would last until the next restart; its address is `[page]` in `config.toml`, and
|
|
917
|
+
each flag exits 2 with the line to write there. A `--port` that is not a number is refused the
|
|
918
|
+
same way, and a `[page] port` that is not one is reported at startup and leaves 4173.
|
|
919
|
+
`test/cli.test.ts`: *the page's address flags are refused with the config.toml lines that replace
|
|
920
|
+
them*.
|
|
921
|
+
- **`PI_WEB_TOKEN`.** pi-loops in the background serves the page, and it is not started from the
|
|
922
|
+
shell that would set the variable, so an override there would hold only until the next restart.
|
|
923
|
+
The token is `web-token` in the loops directory and nothing else: write your own there (letters,
|
|
924
|
+
digits, `-` and `_`, at least 8 of them) and run `pi-loops restart`. A new token signs every
|
|
925
|
+
paired device out (`test/page-server.test.ts`: *a new token signs every paired browser out*).
|
|
926
|
+
- **Ctrl-C in the terminal ends the session the way `/quit` does.** No terminal holds the page any
|
|
927
|
+
more: `pi-loops` has exited before there is anything to press Ctrl-C on. `pi-loops stop` is what
|
|
928
|
+
ends every conversation in the page, and each pi is still asked to quit with the signal it
|
|
929
|
+
handles (SIGTERM, which pi takes as `/quit`) and waited for.
|
|
930
|
+
- **The terminal is told what became of the automation on the way out.** There is no way out from
|
|
931
|
+
the terminal now: `pi-loops` prints the page's address and exits, and pi-loops keeps running
|
|
932
|
+
loops whatever the page does. `pi-loops status` shows the page and what is open in it.
|
|
318
933
|
|
|
319
934
|
## Transport
|
|
320
935
|
|
|
321
|
-
- [x] Loopback by default.
|
|
322
|
-
`test/
|
|
936
|
+
- [x] Loopback by default. `[page] host` binds elsewhere, and only then does a bare address in Host get
|
|
937
|
+
in. `test/page-server.test.ts`: *a foreign Host is refused, whatever it carries*.
|
|
938
|
+
- [x] Every write is checked for where it came from before its body is read. A POST carries the
|
|
939
|
+
token in the `x-pi-web-token` header, never in its URL and never in a cookie; it is JSON or it
|
|
940
|
+
is refused. A form post that the browser attached the device cookie to writes nothing.
|
|
941
|
+
`test/page-server.test.ts`: *a POST takes the token from the header, never from its URL*, *a
|
|
942
|
+
refused POST writes nothing under the loops directory*.
|
|
943
|
+
- [x] The token never rides in a cookie, which every port on this machine is sent. A reload gets
|
|
944
|
+
it by trading the device cookie and the secret the page keeps for this origin; the event
|
|
945
|
+
stream, a file and a preview open with a key minted for that address.
|
|
946
|
+
`test/page-server.test.ts`: *a sniffed device cookie: a server on another port receives it,
|
|
947
|
+
and nothing it does with it reaches /state, /prompt or the token*, *the event stream, a file
|
|
948
|
+
and a preview open with a key minted for them, and not with the cookie or ?token=*.
|
|
323
949
|
- [x] A name you put in front of this — a reverse proxy, a hostname on your own network — is named
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
- [x] A `--port` that is not a port is refused out loud rather than served somewhere else.
|
|
328
|
-
`test/web.test.ts`: *a --port that is not a number is refused rather than quietly served
|
|
329
|
-
somewhere else*.
|
|
950
|
+
in `[page] allow_host`, and nothing else is accepted under it.
|
|
951
|
+
`test/page-server.test.ts`: *an allow-listed name passes, in any case*, *a foreign Host is
|
|
952
|
+
refused, whatever it carries*.
|
|
330
953
|
- [x] **Every route keeps its own guards, the escape hatch included.** `/rpc` carries anything in
|
|
331
954
|
pi's protocol this front end has not grown a button for, and refuses the commands that do have
|
|
332
955
|
a route here — reaching pi through it skipped the mid-turn refusal, the "one of this project's
|
|
@@ -336,26 +959,29 @@ weaker, and worth converting whenever one of them breaks.
|
|
|
336
959
|
`test/web.test.ts`: *the escape hatch cannot be used to skip a route's own guards*.
|
|
337
960
|
- [x] The routes that only read are only read from: `/state`, `/history` and `/stats` answer GET and
|
|
338
961
|
nothing else. `test/web.test.ts`: *the routes that only read are only read from*.
|
|
339
|
-
- [x] A request another site started is refused on every route, whatever
|
|
340
|
-
attached. `test/web.test.ts`: *a request another site started is refused*.
|
|
962
|
+
- [x] A request another site started is refused on every route, whatever the browser attached. `test/web.test.ts`: *a request another site started is refused*.
|
|
341
963
|
- [x] One address that does not change between launches, and a device is added by pointing its
|
|
342
964
|
camera at a QR — with the six digits as the fallback, never a curl command.
|
|
343
|
-
`test/web.test.ts`: *the token outlives the
|
|
965
|
+
`test/web.test.ts`: *the token outlives the server*, *a phone gets in with the six-digit
|
|
344
966
|
code*. `test/web-page.test.ts`: *adding a device shows a code and something to point a camera
|
|
345
967
|
at*, *the QR encoder still produces the matrix a scanner was shown*. A guess spends one of
|
|
346
968
|
twenty tries, so a browser that is already signed in is recognised as signed in first —
|
|
347
969
|
otherwise reloading a bookmark with a stale code on it burns the budget the phone needs.
|
|
348
970
|
`test/web.test.ts`: *a signed-in browser reloading a stale pairing code does not spend a
|
|
349
971
|
guess*.
|
|
350
|
-
- [x] Pairing survives an upgrade: the token
|
|
351
|
-
in across restarts and new versions.
|
|
972
|
+
- [x] Pairing survives an upgrade: the token and the paired devices are files outside the package,
|
|
973
|
+
so a device stays signed in across restarts and new versions. A browser paired before 0.24.0
|
|
974
|
+
pairs once more, because its cookie held the token, which no cookie may hold now.
|
|
975
|
+
`test/web.test.ts`: *the token outlives the server*. `test/page-server.test.ts`: *a new token
|
|
976
|
+
signs every paired browser out*.
|
|
352
977
|
- [x] Events are incremental. The backlog a late-joining browser replays is bounded.
|
|
353
978
|
`test/web.test.ts`: *the event backlog a late-joining browser replays is bounded*.
|
|
354
979
|
- [x] Events carry no credential and no oversized tool payload; the image bytes a message already
|
|
355
980
|
contains travel with it, and the page renders them rather than fetching them. The page itself
|
|
356
|
-
is handed the token — in
|
|
981
|
+
is handed the token — in a first visit's HTML, or for the device secret at `/token` — and
|
|
982
|
+
nothing else is. The
|
|
357
983
|
one event that could have carried a credential was pi's stderr, broadcast when it exits to say
|
|
358
984
|
why: a provider that refuses to authenticate prints the key it was refused with, so that tail
|
|
359
|
-
is capped and redacted before it leaves this process.
|
|
360
|
-
|
|
985
|
+
is capped and redacted before it leaves this process. `host.log`, in the owner's 0600 loops
|
|
986
|
+
directory, has all of it, uncapped but masked by the same `maskSecrets`.
|
|
361
987
|
`test/web.test.ts`: *a credential in pi's dying words does not reach the page*.
|