agent-lookout 0.2.9 → 0.2.10
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 +17 -1
- package/PRIVACY.md +18 -16
- package/README.md +7 -6
- package/SECURITY.md +5 -4
- package/dist/assets/{HistoryPanel-Cr6VQlz-.js → HistoryPanel-C5-cd24R.js} +1 -1
- package/dist/assets/SessionPanel-CO5qG2uH.js +1 -0
- package/dist/assets/index-dMRHt2Y8.js +9 -0
- package/dist/cli/agent-lookout.js +4 -4
- package/dist/cli/{chunk-CMDNRNYP.js → chunk-HHZWG5HI.js} +1 -1
- package/dist/cli/{chunk-XV7BYWVY.js → chunk-ME7TBH3B.js} +2 -0
- package/dist/cli/{chunk-AHCLJEYU.js → chunk-WKRBVBCR.js} +1 -1
- package/dist/cli/{mcpServer-33QB6COH.js → mcpServer-7VLC6LZN.js} +6 -5
- package/dist/cli/{startCommand-XUZHHWZG.js → startCommand-FV2NXVQR.js} +121 -48
- package/dist/index.html +1 -1
- package/docs/API.md +15 -12
- package/docs/GUIDE.md +46 -14
- package/docs/INSTALL.md +5 -1
- package/package.json +1 -1
- package/dist/assets/SessionPanel-YVGY4vbo.js +0 -1
- package/dist/assets/index-DlDz48ci.js +0 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
Changes that a user of Agent Lookout would notice, newest first.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 0.2.10 - 2026-10-08
|
|
6
|
+
|
|
7
|
+
A Codex session's details show how many tokens its newest reply took, and the Mac app asks once whether to turn notifications on, then sends a test that says what macOS did with it.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- A Codex session's details give the token counts of its newest reply, as Codex recorded them: the input, with how much of it came from a cache, and the output, such as `182,431 in, 9,120 out`. The input is how much of the session's context is in use. Sessions read over SSH from another machine have them too. A session with none shows a dash and why. What each agent can report, in Sources, has a Tokens column: Yes for Codex, and No for Claude Code, the Antigravity CLI and status files, with the reason. `GET /api/sessions` gives the counts as `tokens`. Nothing else is kept of the line Codex writes them on, and no event, history, email, post, push, `agent-lookout mcp` answer or `agent-lookout status` holds them.
|
|
12
|
+
- The Mac app asks on the Overview, until you answer, whether to turn notifications on: "Get a notification when a session needs you?", with Turn on and Not now. Turn on turns them on and sends one test notification, so macOS asks then whether Agent Lookout may show notifications. If macOS does not show it, or has not answered yet, a note in the question's place says so and points to Settings, until you dismiss it or next open the Overview. Not now leaves them off. Once you choose, there or in Settings, it never asks again, and a browser tab never asks.
|
|
13
|
+
- In the Mac app, the Notifications card in Settings has Send a test, which shows one notification that says only that it is a test and says what macOS made of it: taken, not answered yet, or refused with macOS's reason and what to do in System Settings. Turning notifications on there sends one too. When macOS refused a test, or a notification the app showed itself while its window was closed, and has shown none since, the card says so. `GET /api/app/notifications` and `POST /api/app/notifications/test` are the app's own, and a test sends nothing off this Mac.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- In the Mac app, the Notifications card speaks of macOS and this app instead of your browser and a dashboard tab, says the app's setting is its own, so turning notifications on in a browser does not turn them on in the app, and says that macOS hides banners while the display is shared or mirrored unless that is allowed in System Settings. It no longer says notifications are on as if that meant you see them: while macOS refuses them, it says so. In a browser the card is unchanged.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Stopping Agent Lookout, or quitting the Mac app, while it was writing its history to disk could lose the last few seconds of the Events log and the charts. What that write held is now written as it stops.
|
|
6
22
|
|
|
7
23
|
## 0.2.9 - 2026-10-08
|
|
8
24
|
|
package/PRIVACY.md
CHANGED
|
@@ -6,7 +6,7 @@ For a Claude Code session that is waiting for you, Agent Lookout reads the last
|
|
|
6
6
|
|
|
7
7
|
Agent Lookout also reads what a Claude Code session last said, the text of its newest reply, from the end of its transcript. This is on by default. It reads it only when it is asked for, and the dashboard asks only while that session's details are open. It keeps at most the last 2,000 characters of the reply, in memory only, for at most 15 seconds after it was last asked for, and forgets it sooner on the first poll of Claude Code that does not list the session. It never sends it anywhere on its own. The local API has no password, so any program on this computer, and on a shared Mac another user account, can ask for it too, and so can any program on another computer while that computer reads this one over SSH. `AGENT_LOOKOUT_LAST_MESSAGE=off` turns it off, and the Mac app opens no port. [A session's last message](#a-sessions-last-message) says exactly what is read.
|
|
8
8
|
|
|
9
|
-
For Codex, Agent Lookout opens Codex's session files, which hold the whole conversation. It reads them to find when each turn started and ended, and keeps only that and the few fields listed below. It keeps no prompt, reply, command or output.
|
|
9
|
+
For Codex, Agent Lookout opens Codex's session files, which hold the whole conversation. It reads them to find when each turn started and ended, and how many tokens Codex counted for the newest reply, and keeps only that and the few fields listed below. It keeps no prompt, reply, command or output.
|
|
10
10
|
|
|
11
11
|
For the Antigravity CLI, `agy`, Agent Lookout opens the transcripts agy keeps, which hold the whole conversation. It reads the end of each to find the conversation's last step, and keeps only that step's kind, its status, whether it asks for a tool and its time. It keeps no prompt, reply, command or output. It also reads the command lines of the `agy` programs that are running, which can hold a prompt given with `-p`, only to tell a session from one of agy's own commands and to find the conversation one names, and keeps nothing else of them. It reads the log each running agy program keeps only for the lines that say which folder it works in, which conversation it has open and whether it waits for your approval of a tool, and keeps nothing else of it, and it reads the short title agy gives each conversation. [The Antigravity CLI's files](#the-antigravity-clis-files) says exactly what is read.
|
|
12
12
|
|
|
@@ -136,12 +136,14 @@ When the folder is there, every 2 seconds Agent Lookout:
|
|
|
136
136
|
|
|
137
137
|
- lists `sessions/` and the day folders for today and yesterday, `sessions/YYYY/MM/DD/`. While Codex has a session open whose file is in an older day folder, as a resumed session's can be, it lists every day folder: at most once every 5 seconds when Codex has newly opened such a session, and otherwise at most once every 30 seconds. Listing reads file names only.
|
|
138
138
|
- lists `thread-writer-locks/`, where Codex keeps one `<thread id>.lock` file for each session a Codex program has open. It reads the names and never opens these files.
|
|
139
|
-
- opens, read-only, each session file named `rollout-*.jsonl` in those day folders. It reads up to the first 2 MiB of a file to find its first `session_meta` line, and up to the last 8 MiB to find the last line that says a turn started or ended. After that it reads only what Codex has added, and only when the file's size or modified time has changed. A file not written to for a day, whose session no Codex program has open, is not opened. The file's modified time, which it already looks up to see whether the file has changed, is also passed to the dashboard as the time Codex last wrote to the session, to say how long a working session has been quiet. A subagent's file is one of these session files too, and its modified time counts as a write by the session that started it.
|
|
139
|
+
- opens, read-only, each session file named `rollout-*.jsonl` in those day folders. It reads up to the first 2 MiB of a file to find its first `session_meta` line, and up to the last 8 MiB to find the last line that says a turn started or ended and the newest line that counts a reply's tokens. After that it reads only what Codex has added, and only when the file's size or modified time has changed. A file not written to for a day, whose session no Codex program has open, is not opened. The file's modified time, which it already looks up to see whether the file has changed, is also passed to the dashboard as the time Codex last wrote to the session, to say how long a working session has been quiet. A subagent's file is one of these session files too, and its modified time counts as a write by the session that started it.
|
|
140
140
|
- opens `session_index.jsonl`, where Codex keeps the names you gave sessions, when its size or modified time has changed, and reads at most its last 4 MiB.
|
|
141
141
|
|
|
142
|
-
The bytes it reads are held in memory only while the file is read. A line is parsed only when it names `session_meta`, or names `event_msg` and looks like a turn line. From every other line, which includes your prompts, Codex's replies, the commands it ran and their output, Agent Lookout takes only the time the line was written.
|
|
142
|
+
The bytes it reads are held in memory only while the file is read. A line is parsed only when it names `session_meta`, or names `event_msg` and looks like a turn line or a token count line. From every other line, which includes your prompts, Codex's replies, the commands it ran and their output, Agent Lookout takes only the time the line was written.
|
|
143
143
|
|
|
144
|
-
From the `session_meta` line it keeps `id`, `timestamp`, `cwd`, `source`, `thread_source`, `parent_thread_id`, `originator` (the program that started the session, such as the Codex desktop app) and `cli_version` (the Codex version), and drops the rest, which includes Codex's instructions, its list of tools, git details and account IDs. From a turn line it keeps the kind of line, such as `task_started` or `task_complete`, its time, and whether its `turn_id` marks a turn the Codex desktop app imported from another agent. From `session_index.jsonl` it keeps `id` and `thread_name`.
|
|
144
|
+
From the `session_meta` line it keeps `id`, `timestamp`, `cwd`, `source`, `thread_source`, `parent_thread_id`, `originator` (the program that started the session, such as the Codex desktop app) and `cli_version` (the Codex version), and drops the rest, which includes Codex's instructions, its list of tools, git details and account IDs. From a turn line it keeps the kind of line, such as `task_started` or `task_complete`, its time, and whether its `turn_id` marks a turn the Codex desktop app imported from another agent. From the newest token count line, an `event_msg` of type `token_count`, it keeps three numbers of the newest reply, from `info.last_token_usage`: `input_tokens`, `cached_input_tokens` and `output_tokens`. It drops the rest of that line, which includes the session's running totals (`total_token_usage`), the model's context window and `rate_limits`, which holds your plan, your credits and how much of your usage limits you have used. From `session_index.jsonl` it keeps `id` and `thread_name`.
|
|
145
|
+
|
|
146
|
+
The three numbers are kept in memory only, with the rest of what it keeps of the session. They are shown in the session's details, and are in the session list the API serves on this computer, which any program on this computer can read, as it can every field there. They also go to another computer that names this one in `AGENT_LOOKOUT_REMOTES` and reads its sessions over SSH. No event, history, notification, email, webhook post, push, answer of `agent-lookout mcp` or output of `agent-lookout status` holds them.
|
|
145
147
|
|
|
146
148
|
A session file or `session_index.jsonl` that is a link, a named pipe or a device is never opened. The Codex folder and the folders inside it are followed when they are links.
|
|
147
149
|
|
|
@@ -256,7 +258,7 @@ Only when `AGENT_LOOKOUT_REMOTES` names other machines, Agent Lookout reads the
|
|
|
256
258
|
|
|
257
259
|
For each machine it runs `ssh -N -o BatchMode=yes -o ExitOnForwardFailure=yes -o ServerAliveInterval=15 -o ControlMaster=no -o ControlPath=none -L 127.0.0.1:<a free port>:127.0.0.1:<its port> -- <target>`, with the target you named, such as `dev@devbox.local`, after `--`, and with the port Agent Lookout listens on there, 4777 unless you give another. `ssh` is the one on your `PATH`, or in `/usr/bin`, `/opt/homebrew/bin` or `/usr/local/bin`, or the one `AGENT_LOOKOUT_SSH_BIN` names. It is started directly, by its full path, never through a shell, with nothing on its input, and given the environment Agent Lookout was started with, so it uses your `~/.ssh/config` and your ssh agent as it does in a terminal. With BatchMode it asks nothing, so Agent Lookout never sees, types or keeps a password, a passphrase or a key, and never answers a question about a host key. ssh runs no command on the other machine: it forwards one port on this computer's `127.0.0.1` to Agent Lookout's port on that machine's own `127.0.0.1`. With `ControlMaster=no` and `ControlPath=none` it makes a connection of its own every time, even when your ssh config shares connections, so it never joins an ssh you have open and never leaves one running in the background: the forward ends when that ssh ends. Agent Lookout keeps the last lines ssh writes about a failure, in memory, and shows the last of them on the machine's card in Sources, such as `Permission denied (publickey).` When ssh ends, it is started again after a wait, and when Agent Lookout stops, it ends it.
|
|
258
260
|
|
|
259
|
-
Through that port, every 2 seconds, it sends two requests and no others: `GET /api/health`, whose answer is the version of Agent Lookout there, and `GET /api/sessions`, whose answer is that machine's sessions and sources, as [docs/API.md](docs/API.md#get-apisessions) describes. The requests carry nothing of this computer's: no session, no setting, and not the `X-Agent-Lookout-Notifications` header, so they change nothing that machine's Agent Lookout believes about its own pages. It never asks that machine what a session there last said. Of the answer it keeps, in memory, what this computer shows of its own sessions: each session's name, folder, status, reason, the vendor's wording of a wait, its times, its app, its branch and repository name, its agent's name
|
|
261
|
+
Through that port, every 2 seconds, it sends two requests and no others: `GET /api/health`, whose answer is the version of Agent Lookout there, and `GET /api/sessions`, whose answer is that machine's sessions and sources, as [docs/API.md](docs/API.md#get-apisessions) describes. The requests carry nothing of this computer's: no session, no setting, and not the `X-Agent-Lookout-Notifications` header, so they change nothing that machine's Agent Lookout believes about its own pages. It never asks that machine what a session there last said. Of the answer it keeps, in memory, what this computer shows of its own sessions: each session's name, folder, status, reason, the vendor's wording of a wait, its times, its app, its branch and repository name, its agent's name, whether its process still runs and the token counts of its newest reply, when it has them. It drops each session's process ID, Jump, Stop and links, and any permission request that machine holds for it, with what the request would run, as the answer is read, so nothing about a session there can act on this computer. It keeps each source there by its name and its state, and what each of its agents can report. It does not take that machine's time rules, or its word on whether it is in quiet hours: this computer's own decide when a session there is stale, and when a wait there is reminded of or held back here.
|
|
260
262
|
|
|
261
263
|
That includes what a waiting session there is asking, such as `Run: npm test`, when that machine's Agent Lookout sends it, which it does unless `AGENT_LOOKOUT_WAITING_TEXT=off` is set there. With `AGENT_LOOKOUT_WAITING_TEXT=off` set on this computer, it is dropped as it arrives, and a waiting session there shows its reason alone, as one here does. Otherwise it is shown here as a waiting session's is, and in the notifications on this computer, and is kept no longer than the wait, as here. Like a session's on this computer, a session there and its events go into the history kept on this computer, without what it is asking, and into an email, a webhook post or a push as the settings for those say.
|
|
262
264
|
|
|
@@ -290,15 +292,15 @@ The `claude agents` command is Claude Code's own program, and it may contact Ant
|
|
|
290
292
|
|
|
291
293
|
The API refuses any request whose `Host` header is not `localhost`, `127.0.0.1` or `[::1]`, any request whose `Origin` header names another site, and any request the browser marks as cross-site. It sends no CORS headers. Together these stop a website you visit from reading your session names and paths through your browser.
|
|
292
294
|
|
|
293
|
-
Each request the dashboard makes carries one header of Agent Lookout's own, `X-Agent-Lookout-Notifications`, which says which events that page has notifications on for: `off`, `on`, which means a session starting to wait, or `on; events=` followed by the names of the events, such as `on; events=needs-you,finished`. It holds nothing else. It goes to Agent Lookout's own server and nowhere else. [Notifications](#notifications) says what the server does with it. The request the Jump button sends for a session in tmux, Terminal or iTerm2 carries one more, `X-Agent-Lookout-Action: jump`, which names what is asked for, and the request the Clear history button in Settings sends carries `X-Agent-Lookout-Action: clear-history`, with an empty body. The request Stop session sends carries `X-Agent-Lookout-Action: stop` and the session's ID, and the one that ends the sessions left running carries `X-Agent-Lookout-Action: clean-up`, with each session's ID and the moment its idle began. The one Allow and Deny send carries `X-Agent-Lookout-Action: answer`, with the session's ID, Agent Lookout's own ID for the request and the answer. The one the Time rules card sends carries `X-Agent-Lookout-Action: time-rules`, with the three rules, and the one the Permission rules card sends carries `X-Agent-Lookout-Action: permission-rules`, with one change to the rules. Send a test, on the ntfy and Pushover cards, sends `X-Agent-Lookout-Action: phone-test`, with `{"channel": "ntfy"}` or `{"channel": "pushover"}`. In the Mac app, the Updates card also sends `X-Agent-Lookout-Action: check-for-updates` and `install-update`, each with the body `{}`, and `update-setting`, with `{"automatic": true}` or `false`. The Menu bar card sends `menu-bar-setting`, with `{"show": true}` or `false`.
|
|
295
|
+
Each request the dashboard makes carries one header of Agent Lookout's own, `X-Agent-Lookout-Notifications`, which says which events that page has notifications on for: `off`, `on`, which means a session starting to wait, or `on; events=` followed by the names of the events, such as `on; events=needs-you,finished`. It holds nothing else. It goes to Agent Lookout's own server and nowhere else. [Notifications](#notifications) says what the server does with it. The request the Jump button sends for a session in tmux, Terminal or iTerm2 carries one more, `X-Agent-Lookout-Action: jump`, which names what is asked for, and the request the Clear history button in Settings sends carries `X-Agent-Lookout-Action: clear-history`, with an empty body. The request Stop session sends carries `X-Agent-Lookout-Action: stop` and the session's ID, and the one that ends the sessions left running carries `X-Agent-Lookout-Action: clean-up`, with each session's ID and the moment its idle began. The one Allow and Deny send carries `X-Agent-Lookout-Action: answer`, with the session's ID, Agent Lookout's own ID for the request and the answer. The one the Time rules card sends carries `X-Agent-Lookout-Action: time-rules`, with the three rules, and the one the Permission rules card sends carries `X-Agent-Lookout-Action: permission-rules`, with one change to the rules. Send a test, on the ntfy and Pushover cards, sends `X-Agent-Lookout-Action: phone-test`, with `{"channel": "ntfy"}` or `{"channel": "pushover"}`. In the Mac app, the Updates card also sends `X-Agent-Lookout-Action: check-for-updates` and `install-update`, each with the body `{}`, and `update-setting`, with `{"automatic": true}` or `false`. The Menu bar card sends `menu-bar-setting`, with `{"show": true}` or `false`. Send a test on the Notifications card, and turning notifications on there or with Turn on in the Overview's question, send `notification-test`, with `{}`.
|
|
294
296
|
|
|
295
297
|
The local server has no password. While Agent Lookout is running, another program on the same machine can request the same data, and on a shared computer so can another user account. That includes up to 2,000 characters of what a Claude Code session last said, as [A session's last message](#a-sessions-last-message) says. Such a program can also send that header, and so turn the notifications the server shows on or off. It can also ask the server to select a tmux pane, or bring a tab of Terminal or iTerm2 forward, as the Jump button does, to clear the history, as the Clear history button does, to stop a Claude Code session, or end the sessions left running, as Stop and End all… do, with every check of [Stopping a session](#stopping-a-session), to answer a permission prompt Agent Lookout is holding, as Allow and Deny do, to change the time rules or the permission rules, as Settings does, which includes adding an allow rule that then answers prompts without a press, and to send a test push, as Send a test does, which counts toward the hour's 20 for that channel. A program that can do that can already type into your terminal, but on a shared computer another account could not. The Mac app opens no port, so there only its own window can ask any of this. It shares the settings file, though. A program running as you can write `~/.agent-lookout/settings.json`, or start `npx agent-lookout` beside the app and ask that copy to change a rule, and the Mac app puts the time rules and permission rules the file then holds in force at its next poll, an allow rule included. `AGENT_LOOKOUT_STOP=off` takes stopping away, `AGENT_LOOKOUT_ANSWER=off` takes answering away, and `AGENT_LOOKOUT_LAST_MESSAGE=off` takes last messages away.
|
|
296
298
|
|
|
297
|
-
For a session in VS Code, the Jump button opens a `vscode://` address that contains the session ID. Your operating system hands it to VS Code. For a session in tmux, the Jump button sends a request that holds the session's ID to Agent Lookout's own server, which selects the pane as described under [`tmux`](#tmux). For a session in Terminal or iTerm2 it sends the same request, and the server brings the tab forward as described under [Terminal and iTerm2](#terminal-and-iterm2). That request, the one Clear history sends, the ones Stop session and End send, the one Allow and Deny send, the ones the Time rules and Permission rules cards send and the one Send a test sends are the eight that change anything, with the Mac app's
|
|
299
|
+
For a session in VS Code, the Jump button opens a `vscode://` address that contains the session ID. Your operating system hands it to VS Code. For a session in tmux, the Jump button sends a request that holds the session's ID to Agent Lookout's own server, which selects the pane as described under [`tmux`](#tmux). For a session in Terminal or iTerm2 it sends the same request, and the server brings the tab forward as described under [Terminal and iTerm2](#terminal-and-iterm2). That request, the one Clear history sends, the ones Stop session and End send, the one Allow and Deny send, the ones the Time rules and Permission rules cards send and the one Send a test sends on the ntfy and Pushover cards are the eight that change anything, with the Mac app's five for its updates, its menu bar and its test notification. The server refuses them from any website, though not from another program on this machine, as the paragraph above says, and the Mac app answers its five only for its own window. [SECURITY.md](SECURITY.md) lists its checks.
|
|
298
300
|
|
|
299
301
|
## Notifications
|
|
300
302
|
|
|
301
|
-
Notifications are off until you turn them on in Settings, or start Agent Lookout with `AGENT_LOOKOUT_NOTIFICATIONS=on`, which turns on only the ones the server shows, and only for a session starting to wait. Pressing Turn on notifications is the only thing in Agent Lookout that asks your browser for permission to show them.
|
|
303
|
+
Notifications are off until you turn them on in Settings, or in the Mac app with Turn on in the question the Overview asks until you answer it, or start Agent Lookout with `AGENT_LOOKOUT_NOTIFICATIONS=on`, which turns on only the ones the server shows, and only for a session starting to wait. Pressing Turn on notifications is the only thing in Agent Lookout that asks your browser for permission to show them. In the Mac app, Send a test on the Notifications card, which is there while notifications are off too, and turning them on there or on the Overview, show one test notification, titled Agent Lookout, that says only that it is a test. No network request is involved.
|
|
302
304
|
|
|
303
305
|
Once they are on, Settings lists four events, each with its own switch: a session starting to wait for you, finishing, failing, and ending, which is leaving the list without having finished or failed. The first is on and the other three are off until you change them. A notification is made only for the events switched on.
|
|
304
306
|
|
|
@@ -331,7 +333,7 @@ It holds none of Claude Code's own wording and no tag. As in the page's, what a
|
|
|
331
333
|
|
|
332
334
|
### Reminders and summaries
|
|
333
335
|
|
|
334
|
-
With the [time rules](docs/GUIDE.md#time-rules) on, the page and the server make two more kinds of notification, in the same two ways and by the same switches. A reminder of a long wait has the session's name as its title, with the machine's name for a session on another machine, and as its text how long it has waited and for what, with what it is asking after it, as in "Has waited 10 minutes for permission: Run: npm test". On the page it takes the place of the wait's own, with its tag. With Remind again on, the same reminder is made again at each interval while the session still waits, with how long it has waited by then, on the page each taking the place of the last, and none once the wait is answered or the session ends. The summary made when quiet hours end has "While quiet" as its title, and as its text the names of the sessions that waited, finished, failed or ended while they held, each with the machine's name for a session on another machine, with how long each waited, as in "checkout-flow waited 25 minutes and billing-webhooks finished". It holds nothing a session was asking. The page's has the tag `agent-lookout:while-quiet`, and stays until you clear it. During quiet hours neither the page nor the server makes any notification.
|
|
336
|
+
With the [time rules](docs/GUIDE.md#time-rules) on, the page and the server make two more kinds of notification, in the same two ways and by the same switches. A reminder of a long wait has the session's name as its title, with the machine's name for a session on another machine, and as its text how long it has waited and for what, with what it is asking after it, as in "Has waited 10 minutes for permission: Run: npm test". On the page it takes the place of the wait's own, with its tag. With Remind again on, the same reminder is made again at each interval while the session still waits, with how long it has waited by then, on the page each taking the place of the last, and none once the wait is answered or the session ends. The summary made when quiet hours end has "While quiet" as its title, and as its text the names of the sessions that waited, finished, failed or ended while they held, each with the machine's name for a session on another machine, with how long each waited, as in "checkout-flow waited 25 minutes and billing-webhooks finished". It holds nothing a session was asking. The page's has the tag `agent-lookout:while-quiet`, and stays until you clear it. During quiet hours neither the page nor the server makes any notification, except the Mac app's test notification, which you ask for.
|
|
335
337
|
|
|
336
338
|
The server shows them only while notifications are on, and only for the events switched on, and it learns both from the dashboard. Each request a dashboard page makes says, in the `X-Agent-Lookout-Notifications` header, which events are on when that page's choice is on and the browser allows notifications, and `off` otherwise. The server keeps the last thing a page said, in memory, for as long as it runs, and never writes it to disk. Before any page has said anything it is off, unless `AGENT_LOOKOUT_NOTIFICATIONS=on` was set when Agent Lookout started, which turns it on for a session starting to wait. A page that says `off` after that turns it off all the same. The header is listened to only on a request that passes the checks described under [Network](#network), so a page at another address cannot set it.
|
|
337
339
|
|
|
@@ -360,7 +362,7 @@ An email is plain text. Its subject is the session's name followed by the reason
|
|
|
360
362
|
|
|
361
363
|
With the [time rules](docs/GUIDE.md#time-rules) on, two more kinds of email can be sent. A reminder of a long wait is the email of a wait with a subject that says how long it has waited, such as "checkout-flow has waited 10 minutes for permission", and a line on how often reminders come. With Remind again on, the same email goes again at each interval while the session still waits, saying how long by then, and holds nothing more than the first. The summary sent when quiet hours end has the subject "While quiet:" followed by the first few sessions' names and what happened, and in its body, one line for each session that waited, finished, failed or ended while they held, with how long it waited and from when, or when it finished, failed or ended, at most 50, and a line on where quiet hours are set. A summary holds the sessions' names alone, nothing of what they were asking and no folder.
|
|
362
364
|
|
|
363
|
-
It holds no folder path, no prompt, none of Claude Code's own wording for the wait, no link, no image, nothing that reports back when it is opened and, unless you set `AGENT_LOOKOUT_EMAIL_ASKING=on` (below), nothing of what a waiting session is asking. Its headers are the ones the mail library writes for every email: From, with the name Agent Lookout and the sender's address, To, Subject, Date, Message-ID, and three that say it is plain text in UTF-8. A session's name and its folder's name are cut to 80 characters, and anything in them that would end a line becomes a space, so nothing in a name can add a header or a recipient.
|
|
365
|
+
It holds no folder path, no prompt, none of Claude Code's own wording for the wait, no token counts, no link, no image, nothing that reports back when it is opened and, unless you set `AGENT_LOOKOUT_EMAIL_ASKING=on` (below), nothing of what a waiting session is asking. Its headers are the ones the mail library writes for every email: From, with the name Agent Lookout and the sender's address, To, Subject, Date, Message-ID, and three that say it is plain text in UTF-8. A session's name and its folder's name are cut to 80 characters, and anything in them that would end a line becomes a space, so nothing in a name can add a header or a recipient.
|
|
364
366
|
|
|
365
367
|
#### What a waiting session is asking, in an email
|
|
366
368
|
|
|
@@ -463,7 +465,7 @@ With the [time rules](docs/GUIDE.md#time-rules) on, two more kinds of post can b
|
|
|
463
465
|
|
|
464
466
|
`from` and `to` are when Agent Lookout saw the quiet hours begin and end, and `items` holds each session that waited, finished, failed or ended while they held, at most 100, with `more` saying how many were left out. A session that waited and then finished, failed or ended is one item, whose `then` says what it did and when, as `{"event": "ended", "at": "..."}`. Each item's `session` holds what a post's always does, and no item holds anything a session was asking.
|
|
465
467
|
|
|
466
|
-
It holds no folder path, no prompt, none of Claude Code's own wording for the wait, no session ID or process ID, nothing about this computer and, unless you set `AGENT_LOOKOUT_WEBHOOK_ASKING=on` (below), nothing of what a waiting session is asking. Its headers are `Content-Type: application/json`, `Content-Length`, `User-Agent: Agent Lookout/` followed by the version, `Host` and `Connection: close`. It carries no cookie and no other header.
|
|
468
|
+
It holds no folder path, no prompt, none of Claude Code's own wording for the wait, no token counts, no session ID or process ID, nothing about this computer and, unless you set `AGENT_LOOKOUT_WEBHOOK_ASKING=on` (below), nothing of what a waiting session is asking. Its headers are `Content-Type: application/json`, `Content-Length`, `User-Agent: Agent Lookout/` followed by the version, `Host` and `Connection: close`. It carries no cookie and no other header.
|
|
467
469
|
|
|
468
470
|
A session's name, its folder's name and its agent's are each cut to 80 characters, and anything in them that would end a line becomes a space. In `text`, `<`, `>` and `&` are written as `<`, `>` and `&`, which Slack shows as the characters themselves, and each `@` is followed by a space of no width, so nothing in a name can mention someone, ping a channel or make a link that shows other words than its address. The JSON is written whole by Node's own writer, so nothing in a name can add a field. Slack does show a web address written out in a name as a link to that address.
|
|
469
471
|
|
|
@@ -524,7 +526,7 @@ It holds:
|
|
|
524
526
|
|
|
525
527
|
With the [time rules](docs/GUIDE.md#time-rules) on, two more kinds of push can be sent. A reminder of a long wait has a title that says how long it has waited, such as "checkout-flow has waited 10 minutes for permission", and a message that says since when, as in "Since 14:01 · storefront · VS Code · Claude Code". With Remind again on, the same push goes again at each interval while the session still waits, saying how long by then. The summary sent when quiet hours end has the title "While quiet", and as its message the names of the first four sessions that waited, finished, failed or ended while they held, with how long each waited, and how many more there were, as in "checkout-flow waited 25 minutes and billing-webhooks finished". It holds nothing a session was asking and no folder.
|
|
526
528
|
|
|
527
|
-
It holds no folder path, no prompt, none of Claude Code's own wording for the wait, no session ID or process ID, no link, not the dashboard's address, nothing about this computer and, unless you set `AGENT_LOOKOUT_NTFY_ASKING=on` (below), nothing of what a waiting session is asking. Its headers are `Content-Type: application/json`, `Content-Length`, `User-Agent: Agent Lookout/` followed by the version, `Authorization: Bearer` followed by the token only when `AGENT_LOOKOUT_NTFY_TOKEN` is set, `Host` and `Connection: close`. It carries no cookie and no other header. A session's name, its folder's name and its agent's are each cut to 80 characters, and anything in them that would end a line becomes a space. The title is cut to 250 characters and the message to 1,024. The JSON is written whole by Node's own writer, so nothing in a name can add a field, and it is not marked as Markdown, so ntfy shows the words as they are. The ntfy apps may show a web address written out in a name as a link.
|
|
529
|
+
It holds no folder path, no prompt, none of Claude Code's own wording for the wait, no token counts, no session ID or process ID, no link, not the dashboard's address, nothing about this computer and, unless you set `AGENT_LOOKOUT_NTFY_ASKING=on` (below), nothing of what a waiting session is asking. Its headers are `Content-Type: application/json`, `Content-Length`, `User-Agent: Agent Lookout/` followed by the version, `Authorization: Bearer` followed by the token only when `AGENT_LOOKOUT_NTFY_TOKEN` is set, `Host` and `Connection: close`. It carries no cookie and no other header. A session's name, its folder's name and its agent's are each cut to 80 characters, and anything in them that would end a line becomes a space. The title is cut to 250 characters and the message to 1,024. The JSON is written whole by Node's own writer, so nothing in a name can add a field, and it is not marked as Markdown, so ntfy shows the words as they are. The ntfy apps may show a web address written out in a name as a link.
|
|
528
530
|
|
|
529
531
|
#### What a waiting session is asking, in a push
|
|
530
532
|
|
|
@@ -636,7 +638,7 @@ For each tool the agent calls, it asks Agent Lookout's own server for the list o
|
|
|
636
638
|
|
|
637
639
|
It answers the app that started it, over stdout, and nothing and nobody else. An answer is one of three:
|
|
638
640
|
|
|
639
|
-
- For each session: its ID, its name, its agent, the other machine it runs on, when it runs on one read over SSH, its status, why it waits, the name of its folder and never the rest of the path, its branch or commit, its app, when its status began, and how long its agent has written nothing. With `AGENT_LOOKOUT_PULL_REQUESTS=on`, also the number of its branch's pull request and whether its checks are failing, pending or passing, and never its title. Never what a waiting session is asking, or what a session last said, which are read from its transcript.
|
|
641
|
+
- For each session: its ID, its name, its agent, the other machine it runs on, when it runs on one read over SSH, its status, why it waits, the name of its folder and never the rest of the path, its branch or commit, its app, when its status began, and how long its agent has written nothing. With `AGENT_LOOKOUT_PULL_REQUESTS=on`, also the number of its branch's pull request and whether its checks are failing, pending or passing, and never its title. Never what a waiting session is asking, or what a session last said, which are read from its transcript, and never the token counts of its newest reply.
|
|
640
642
|
- For each session that needs you: the same, with how long it has waited, and one sentence naming each of them and any agent that could not be read.
|
|
641
643
|
- For each source: its state, the sentence that says how it is read or what went wrong, which can name the folders Agent Lookout reads, such as `~/.claude/sessions`, and, for another machine, the SSH target you named, such as `dev@devbox.local`, and the last line ssh printed when it failed, and what its agent can report.
|
|
642
644
|
|
|
@@ -654,7 +656,7 @@ Nothing. Each answer is worked out from one reading of the list and is gone once
|
|
|
654
656
|
|
|
655
657
|
Agent Lookout keeps one thing about your sessions on disk: the history behind the Events log and the charts, so that they are still there after it restarts. [The history](#the-history) says what it holds, where it is, how long it is kept, how to clear it and how to turn it off. It also keeps its time rules and permission rules in a file, which holds nothing about your sessions: [The settings file](#the-settings-file). Apart from those, its own code writes no files, except the Mac app's, below.
|
|
656
658
|
|
|
657
|
-
The latest session list is held in memory only, and is gone when Agent Lookout stops. What a waiting session is asking is in that list only while it waits, and in no event or point of history, on disk or in memory. So is a permission request held for the dashboard, which is also kept in memory only while it is held. With `AGENT_LOOKOUT_EMAIL_ASKING=on`, `AGENT_LOOKOUT_WEBHOOK_ASKING=on`, `AGENT_LOOKOUT_NTFY_ASKING=on` or `AGENT_LOOKOUT_PUSHOVER_ASKING=on`, it is also in the email, post or push for that wait from the moment it is written until it is handed over, and nowhere else. So are the tmux panes and terminal tabs it last found, the branches and repositories it last read, with pull requests on the pull requests `gh` last gave and where each branch's is looked for, and what the dashboard pages last said about notifications. What a Claude Code session last said is never in the session list: it is held in memory apart from it, for at most 15 seconds after it was last asked for, as [A session's last message](#a-sessions-last-message) says.
|
|
659
|
+
The latest session list is held in memory only, and is gone when Agent Lookout stops. The token counts of a session's newest reply are held in memory only, with the session, and are in no event or point of history. What a waiting session is asking is in that list only while it waits, and in no event or point of history, on disk or in memory. So is a permission request held for the dashboard, which is also kept in memory only while it is held. With `AGENT_LOOKOUT_EMAIL_ASKING=on`, `AGENT_LOOKOUT_WEBHOOK_ASKING=on`, `AGENT_LOOKOUT_NTFY_ASKING=on` or `AGENT_LOOKOUT_PUSHOVER_ASKING=on`, it is also in the email, post or push for that wait from the moment it is written until it is handed over, and nowhere else. So are the tmux panes and terminal tabs it last found, the branches and repositories it last read, with pull requests on the pull requests `gh` last gave and where each branch's is looked for, and what the dashboard pages last said about notifications. What a Claude Code session last said is never in the session list: it is held in memory apart from it, for at most 15 seconds after it was last asked for, as [A session's last message](#a-sessions-last-message) says.
|
|
658
660
|
|
|
659
661
|
### The history
|
|
660
662
|
|
|
@@ -666,7 +668,7 @@ It writes there what it already holds in memory for the Events log and the chart
|
|
|
666
668
|
- Each history point, one for each poll: its time, and how many sessions were waiting on you, working, idle, and in all.
|
|
667
669
|
- When Agent Lookout started writing, and when the history was cleared.
|
|
668
670
|
|
|
669
|
-
Each event holds the session's name as the dashboard shows it: the name its agent gave it, from Claude Code's session file or the `claude` command's list, from Codex's `session_index.jsonl`, from the Antigravity CLI's title of the conversation or from a status file, or else the name of its folder, which for a Codex session is read from the start of its session file, and for an Antigravity CLI session from its program's log, or else, for an Antigravity CLI session, its conversation ID, the name of its folder in `brain/`. Nothing else from a transcript, from Codex's session files or from agy's transcripts and logs is written, and no folder path, branch, prompt or anything a waiting session is asking.
|
|
671
|
+
Each event holds the session's name as the dashboard shows it: the name its agent gave it, from Claude Code's session file or the `claude` command's list, from Codex's `session_index.jsonl`, from the Antigravity CLI's title of the conversation or from a status file, or else the name of its folder, which for a Codex session is read from the start of its session file, and for an Antigravity CLI session from its program's log, or else, for an Antigravity CLI session, its conversation ID, the name of its folder in `brain/`. Nothing else from a transcript, from Codex's session files or from agy's transcripts and logs is written, and no folder path, branch, prompt, token count or anything a waiting session is asking.
|
|
670
672
|
|
|
671
673
|
What it learns is written every 5 seconds, and once more as it stops. The files are JSON lines, one file for each day by the UTC clock, such as `v1-2026-10-06.jsonl`. A day that grows past 2 MB goes on in `v1-2026-10-06-2.jsonl`, and so on. Each day's file is deleted once that day ended more than 8 days ago, and the files hold 20 MB at most: before writing more than that, Agent Lookout deletes the oldest. When it starts, it reads the files back, and keeps in memory the last 1,000 events and the last six hours of points, as it does while it runs. For the Waits card it also keeps in memory, for 9 days, the events of sessions starting and stopping waiting on you, and the stretches of time it was measuring, which it works the card's totals out from. It reads only ordinary files in that folder of about 2 MB or less, does not follow a link, and passes over a line it cannot read. A file whose name says it was written by a later version of Agent Lookout, such as `v2-2026-10-06.jsonl`, is not read, written or deleted, for its age or by Clear history. A line dated more than a minute after the computer's clock was written while the clock was wrong, and is not read back.
|
|
672
674
|
|
|
@@ -694,7 +696,7 @@ The tools that run it write files of their own. None of these holds session data
|
|
|
694
696
|
|
|
695
697
|
The Mac app keeps the history in the same folder as `npx agent-lookout` and `npm start`, and four files of its own. `window-state.json`, in `~/Library/Application Support/Agent Lookout/`, holds the window's place and size and the colour of the theme it last showed. `update-state.json`, beside it, holds whether it checks for updates by itself, when it last checked, the last version it told you of and, during an install, the version being installed, as [Updates (Mac app only)](#updates-mac-app-only) says, and a newer version it downloads waits in the system's temporary folder until it is installed or the app quits. `menu-bar-state.json`, beside it, holds whether its icon is shown in the menu bar. Its log, `~/Library/Logs/Agent Lookout/main.log`, holds the errors it met and the lines `npm start` would print, such as the one that says an email setting is wrong. None of them holds a session's name, though an error in the log can name a file the app could not read. Electron, which the app is built on, keeps the page's local storage, below, and caches of its own in the first folder.
|
|
696
698
|
|
|
697
|
-
The dashboard saves seven values in your browser's local storage. Your theme choice is under the key `agent-lookout-theme`. Whether notifications are on is under the key `agent-lookout-notifications`, as `on` or `off`, and is written only when you turn them on or off. The events that send one are under the key `agent-lookout-notification-events`, as their names separated by commas, such as `needs-you,finished`, and are written only when you switch one. The apps the page has said macOS will ask about, when you first jumped to a tab of one, are under the key `agent-lookout-automation-note`, as `Terminal`, `iTerm2` or both separated by a comma. The last time the Events log was on screen is under the key `agent-lookout-last-looked`, as a number of milliseconds since 1970, and is written when the page goes out of sight or is closed. It is what the line in the Events log that marks where you left off is drawn from, and while that line still shows, the time kept is the line's own. It holds nothing about any session or event. Whether the Sessions card shows the list, the list grouped by repository or the board is under the key `agent-lookout-sessions-layout`, as `list`, `repositories` or `board`, and is written only when you choose one. The sessions left running that you hid until they change are under the key `agent-lookout-hidden-sessions`, as a list of each one's ID and the moment its idle began, at most 100, and are written only when you hide one. All seven belong to one browser at one address.
|
|
699
|
+
The dashboard saves seven values in your browser's local storage. Your theme choice is under the key `agent-lookout-theme`. Whether notifications are on is under the key `agent-lookout-notifications`, as `on` or `off`, and is written only when you turn them on or off, or press Not now in the Mac app's question on the Overview. The events that send one are under the key `agent-lookout-notification-events`, as their names separated by commas, such as `needs-you,finished`, and are written only when you switch one. The apps the page has said macOS will ask about, when you first jumped to a tab of one, are under the key `agent-lookout-automation-note`, as `Terminal`, `iTerm2` or both separated by a comma. The last time the Events log was on screen is under the key `agent-lookout-last-looked`, as a number of milliseconds since 1970, and is written when the page goes out of sight or is closed. It is what the line in the Events log that marks where you left off is drawn from, and while that line still shows, the time kept is the line's own. It holds nothing about any session or event. Whether the Sessions card shows the list, the list grouped by repository or the board is under the key `agent-lookout-sessions-layout`, as `list`, `repositories` or `board`, and is written only when you choose one. The sessions left running that you hid until they change are under the key `agent-lookout-hidden-sessions`, as a list of each one's ID and the moment its idle began, at most 100, and are written only when you hide one. All seven belong to one browser at one address.
|
|
698
700
|
|
|
699
701
|
## What is on screen
|
|
700
702
|
|
package/README.md
CHANGED
|
@@ -106,12 +106,12 @@ Claude Code, Codex and the Antigravity CLI need no setup. Any other agent writes
|
|
|
106
106
|
|
|
107
107
|
<!-- Checked against the adapters by tests/integration/collector/adapters/adapter.test.ts -->
|
|
108
108
|
|
|
109
|
-
| Agent | Needs you | Jump | Quiet for | Stop | Answer |
|
|
110
|
-
| --------------- | --------- | ------ | --------- | ------ | ------ |
|
|
111
|
-
| Claude Code | Yes | Partly | No | Partly | Partly |
|
|
112
|
-
| Codex | No | No | Yes | No | No |
|
|
113
|
-
| Antigravity CLI | Partly | No | Yes | No | No |
|
|
114
|
-
| Status files | Partly | No | Partly | No | No |
|
|
109
|
+
| Agent | Needs you | Jump | Quiet for | Tokens | Stop | Answer |
|
|
110
|
+
| --------------- | --------- | ------ | --------- | ------ | ------ | ------ |
|
|
111
|
+
| Claude Code | Yes | Partly | No | No | Partly | Partly |
|
|
112
|
+
| Codex | No | No | Yes | Yes | No | No |
|
|
113
|
+
| Antigravity CLI | Partly | No | Yes | No | No | No |
|
|
114
|
+
| Status files | Partly | No | Partly | No | No | No |
|
|
115
115
|
|
|
116
116
|
A Codex session waiting for approval shows as working, and so does an Antigravity CLI session waiting for an answer to a question. After 5 minutes its row says how long it has been quiet. An Antigravity CLI session needs you while agy asks you to approve a tool. Antigravity CLI support is new: it is built from what agy 1.3.1 documents and its program holds, and checked against one conversation. [What each agent can report](docs/GUIDE.md#what-each-agent-can-report) gives the reason for every Partly and No.
|
|
117
117
|
|
|
@@ -231,6 +231,7 @@ Yes. The app opens no port, so `status` and `mcp` need the npx copy. Turn notifi
|
|
|
231
231
|
| See every file it reads and all it can send | [PRIVACY.md](PRIVACY.md) |
|
|
232
232
|
| Know what it guards, or report a vulnerability | [SECURITY.md](SECURITY.md) |
|
|
233
233
|
| Call its local HTTP API | [docs/API.md](docs/API.md) |
|
|
234
|
+
| Learn what each part of the code does, and why | [docs/TOUR.md](docs/TOUR.md) |
|
|
234
235
|
| See how the parts fit together | [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) |
|
|
235
236
|
| Write tests | [tests/README.md](tests/README.md) |
|
|
236
237
|
| Install or remove the Claude Code plugin | [plugins/agent-lookout/README.md](plugins/agent-lookout/README.md) |
|
package/SECURITY.md
CHANGED
|
@@ -59,7 +59,7 @@ The Mac app answers a held request from its own notification of a wait, with Den
|
|
|
59
59
|
|
|
60
60
|
The permission rules answer a held request without a press. There are none until the person adds one, through `POST /api/settings/permission-rules`, which makes the checks above with `X-Agent-Lookout-Action: permission-rules` and takes a body of 1,024 bytes or less that is exactly one change: add, edit, move or remove one rule, named by its ID. The collector checks every rule itself, whatever the page checked: a tool's name of letters, digits, `_` and `-`, or `*`, a command for Bash alone, plain ASCII without `;`, `&`, `|`, `<`, `>`, parentheses, quotes, backslashes, `$`, braces or a wildcard but a final `:*`, and an allow rule that names one tool, never every tool of an MCP server, for Bash a command that does not begin with a program that runs another, such as `sudo`, `env`, `xargs` or a shell, or with one that sends requests or runs the code it is given, such as `curl`, `wget`, `nc`, `ssh`, `node`, `python` or `awk`, whatever its capitals or folder, with `.exe` or `.com` after it, or anything after its name that does not begin with a letter, such as a version, that names its subcommand where the program is a package manager or a build tool, such as `npm test` and never `npm` alone, that names no subcommand that fetches or runs code, such as `npm exec` or `docker run`, and no option through which the program runs another, such as `git -c`, and that does not name Agent Lookout's own folder, and never an edit, a plan or a question. A list in the file that holds a rule these checks refuse, such as one saved before a check was added, is no rule at all. It keeps 100 rules at most, in the settings file, written as the time rules are. A held request is put to the rules when the registry file first confirms its wait, and a deny or an allow rule answers it through `answer`, the same function a press calls, so it makes every check above: the same wait, Allow only for a request that offers Allow, and the same two fixed texts. Deny goes first, then ask, then allow. An allow rule also needs, for Bash, one plain command whose words are the rule's, word by word: no second command, substitution, variable, redirection, subshell, glob, quote, backslash, `~`, tab, space at either end, variable set before it, or letter outside ASCII, and no input but `timeout` and `run_in_background`, no subcommand that fetches or runs code before the arguments begin, such as `npm -- exec`, no option through which the program runs another, such as `go test -exec`, no word with `.agent-lookout` in it, no path, read also from the session's folder and made plain of `..`, `//` and `/./`, to the settings file, the socket or a folder above them up to `/`, and no path under `/.vol`. A session whose folder is the home folder or above gets no allow answer at all. Anything else is left to the person. A deny or an ask rule matches more loosely, since it only holds a request back: in any command of the line, the program by its name whatever its capitals or folder, its other words in order with any words between them, and from every word after a program that runs another. Each rule's answer is an event, with the tool and the rule's words, and never the command. A deny or an allow rule in Claude Code's own settings is applied before Claude Code runs the hook, so the hook, and these rules, never see a request those settle.
|
|
61
61
|
|
|
62
|
-
The Mac app answers
|
|
62
|
+
The Mac app answers eight more routes, under `/api/app/` on its own address, which no port reaches and which `npm start` and the npm package do not have: `GET /api/app/update`, `GET /api/app/menu-bar` and `GET /api/app/notifications`, and five POSTs that check GitHub for a newer version, install the one that is ready, turn the daily check on or off, show or hide the menu bar item (`POST /api/app/menu-bar/setting`, with `X-Agent-Lookout-Action: menu-bar-setting` and a body of exactly `{"show": true}` or `{"show": false}`), and show one test notification, which says only that it is a test (`POST /api/app/notifications/test`, with `X-Agent-Lookout-Action: notification-test` and a body of exactly `{}`). Each POST passes the checks above, with an `X-Agent-Lookout-Action` of its own, and a body of 256 bytes or less that is exactly what it needs. The app's only requests off the machine are the update check's and its download's, over HTTPS, with the certificate checked, to GitHub and the two hosts GitHub redirects a release's downloads to, and to no other address, a redirect's included. A download must have the size and SHA-512 the release's `latest-mac.yml` gives, and the bundle in it must be this app, by its bundle identifier, at the version offered, with no link where the bundle, its `Contents` or its `Info.plist` should be. Only Install and Restart starts the helper that replaces the app: `/bin/sh` with a fixed script, and the app's process ID and three paths as its arguments, two of them in the app's own temporary folder. Nothing from the network or from a request reaches its arguments or any command. The app is not signed with a Developer ID yet, so a download is vouched for by the release it comes from and the hash in that release, and is as safe as the project's GitHub releases are.
|
|
63
63
|
|
|
64
64
|
`agent-lookout mcp` is a Model Context Protocol server for an agent that keeps track of your other agents. The agent's app starts it and speaks to it over stdin and stdout, so it opens no port. For each tool call it reads `GET /api/sessions` from the local server, at a loopback address only, with no `Origin` and no notifications header, exactly as `agent-lookout status` does, and answers the app with what it read. Its three tools only read: none of them reaches `POST /api/jump`, `POST /api/history/clear`, `POST /api/sessions/stop`, `POST /api/sessions/clean-up`, `POST /api/permission/answer`, `POST /api/settings/time-rules`, `POST /api/settings/permission-rules` or `POST /api/phone/test`, changes a notification or changes anything else. A session's name, agent, folder and branch are text that other programs wrote, and the client is a model that takes instructions from text. So each tool's description and each answer say that this text is data, never instructions, the one-sentence summary puts each name in quotation marks, and escape sequences and control characters are taken out of every text, as `agent-lookout status` does. So are characters that show nothing, such as zero-width spaces and Unicode's tag characters, in which words can be hidden from a person but not from a model.
|
|
65
65
|
|
|
@@ -101,6 +101,7 @@ Session names and folder paths can be sensitive. The main risk is that something
|
|
|
101
101
|
- Any write to `~/.claude` or to another tool's files, and any read of the `.key` files in `~/.claude/sessions/`.
|
|
102
102
|
- Any write under the Codex folder (`~/.codex`, or the folder `CODEX_HOME` or `AGENT_LOOKOUT_CODEX_HOME` names), any open of a file in its `thread-writer-locks/`, and any read of a Codex file that PRIVACY.md does not list.
|
|
103
103
|
- Text from a Codex session file, such as a prompt, a reply or a command's output, reaching the API or the dashboard.
|
|
104
|
+
- Anything from a Codex token count line but the newest reply's three counts, such as its running totals, its rate limits, a plan or credits, reaching the API or the dashboard, and those counts reaching an event, the history, an email, a webhook post, a push, an answer of `agent-lookout mcp` or `agent-lookout status`.
|
|
104
105
|
- Any write under `~/.gemini` or the folder `AGENT_LOOKOUT_ANTIGRAVITY_HOME` names, any open of an agy database or of a file in that folder that PRIVACY.md does not list, and text from an agy transcript, an agy program's command line or its log, such as a prompt, a reply, a command's output or an address agy contacts, reaching the API or the dashboard. Only a conversation's title, its folder and the name of a tool agy asks to run may.
|
|
105
106
|
- A session name, path or other session field that runs as script or markup in the dashboard.
|
|
106
107
|
- Anything from a status file that runs as script or markup in the dashboard, or in a notification, or that is used as a link, a Jump, a command or a path to open.
|
|
@@ -120,11 +121,11 @@ Session names and folder paths can be sensitive. The main risk is that something
|
|
|
120
121
|
- A request to `POST /api/jump` that makes `osascript` run any script but the fixed one for Terminal or iTerm2, or with any argument but a terminal found for a listed session, or that makes Terminal or iTerm2 do anything but bring a tab forward: running a command, sending keys or text, or reading what a tab shows.
|
|
121
122
|
- A session name, or anything else from a session, that `osascript` reads as AppleScript or as one of its options instead of showing it as text.
|
|
122
123
|
- A Jump link that opens anything other than the intended `vscode://` address.
|
|
123
|
-
- A notification, or the browser's question about allowing them, appearing when you have not turned notifications on, in Settings or with `AGENT_LOOKOUT_NOTIFICATIONS=on`.
|
|
124
|
-
- A notification that holds anything but the session's name and the reason, or Finished, Failed or Ended, or for a reminder how long the session has waited and for what, or for the summary of quiet hours the names of the sessions and what happened to them, or that makes the browser fetch anything.
|
|
124
|
+
- A notification, or the browser's question about allowing them, appearing when you have not turned notifications on, in Settings, in the Mac app with Turn on in the Overview's question, or with `AGENT_LOOKOUT_NOTIFICATIONS=on`. The Mac app's test notification, shown when you press Send a test or turn notifications on in the app, is not one of these.
|
|
125
|
+
- A notification that holds anything but the session's name and the reason, or Finished, Failed or Ended, or for a reminder how long the session has waited and for what, or for the summary of quiet hours the names of the sessions and what happened to them, or for the Mac app's test notification anything but its fixed words, which say it is a test, or that makes the browser fetch anything.
|
|
125
126
|
- A request to `POST /api/settings/time-rules` that writes anything but the three rules to the settings file, writes to any other file, writes through a link, or changes anything else in Agent Lookout. A settings file that is read through a link, or that makes Agent Lookout read more than 64 KB, send anything anywhere, or run anything. Also a website, or a page served from anywhere but this machine, changing the time rules.
|
|
126
127
|
- A permission rule that lets through more than it says: an allow rule that answers a request its words do not name, a command line with more than one plain command, a request that offers Deny alone, a tool other than its own, or a request in another wait, an allow rule that answers one plain command whose program and words, in order, an ask or a deny rule names, or any rule that answers while `AGENT_LOOKOUT_ANSWER` is off. Also a rule's answer that writes anything but the two fixed answers, an event, history or settings answer that holds a request's command or input, a rule the collector takes that its own checks refuse, and a website, or a page served from anywhere but this machine, changing the rules.
|
|
127
|
-
- Quiet hours that let a notification, an email, a post or a push through while they hold, or a summary that holds more than PRIVACY.md lists. A test push, which the person asks for, is not held.
|
|
128
|
+
- Quiet hours that let a notification, an email, a post or a push through while they hold, or a summary that holds more than PRIVACY.md lists. A test push, or the Mac app's test notification, which the person asks for, is not held.
|
|
128
129
|
- A page at another address reading or steering the dashboard's notifications, or turning the server's own on or off.
|
|
129
130
|
- A tool of `agent-lookout mcp` that acts: that reaches `POST /api/jump`, a route that stops a session, the route that answers one or a route that changes a setting, sends the notifications header, or changes anything in Agent Lookout, in another tool or on this computer.
|
|
130
131
|
- `agent-lookout mcp` reading anything but `GET /api/sessions` from Agent Lookout at a loopback address, asking an address that is not loopback, opening a port, or making any other connection.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as y,j as t}from"./react-C2DiJaR6.js";import{b as G,t as K,r as q,f as U,a as C,c as X,d as V,h as _,m as J,H as Q,e as Z,D as ee,g as O,s as te,i as se,j as ne,S as oe,L as ae,C as re,k as ie,l as le,n as de}from"./index-
|
|
1
|
+
import{r as y,j as t}from"./react-C2DiJaR6.js";import{b as G,t as K,r as q,f as U,a as C,c as X,d as V,h as _,m as J,H as Q,e as Z,D as ee,g as O,s as te,i as se,j as ne,S as oe,L as ae,C as re,k as ie,l as le,n as de}from"./index-dMRHt2Y8.js";import"./motion-DlYOyClL.js";import"./radix-BH0JH8UU.js";const ce={"needs-you":"text-status-needs-you-edge",working:"text-status-working",idle:"text-status-idle"},I={top:12,right:12,bottom:24,left:30},L=220,ue=76,he=104,R=1/60;function me({samples:s,start:n,end:a,tone:g,label:w,describe:l,className:c}){const p=y.useRef(null),[u,k]=y.useState(0),[h,i]=y.useState(null);y.useEffect(()=>{const e=p.current;if(!e)return;const o=new ResizeObserver(([j])=>{j&&k(Math.floor(j.contentRect.width))});return o.observe(e),()=>o.disconnect()},[]);const r=I.left,m=Math.max(r+1,u-I.right),x=I.top,d=L-I.bottom,N=a-n;let T=0;for(const e of s)e.at>=n&&e.at<=a&&e.value>T&&(T=e.value);const A=V(T),b=G(s,{start:n,end:a},{left:r,right:m,top:x,bottom:d,ceiling:A.ceiling}),W=e=>r+(e-n)/N*(m-r),S=e=>d-e/A.ceiling*(d-x),B=K(n,a,Math.max(1,Math.floor((m-r)/ue))),M=h??1,H=n+M*N,E=q(b.runs,H),$=E?l(E.value):"Not measured",v=W(H),z=(e,o)=>{const j=o.left+r,f=m-r;i(Math.min(1,Math.max(0,(e-j)/f)))},Y=e=>{const j={ArrowLeft:f=>f-R,ArrowRight:f=>f+R,PageDown:f=>f-R*10,PageUp:f=>f+R*10,Home:()=>0,End:()=>1}[e.key];j&&(e.preventDefault(),i(Math.min(1,Math.max(0,j(h??1)))))};return t.jsxs("div",{ref:p,"data-slot":"history-chart",role:"slider",tabIndex:0,"aria-label":`${w}. Arrow keys read the count at each moment.`,"aria-orientation":"horizontal","aria-valuemin":0,"aria-valuemax":100,"aria-valuenow":Math.round(M*100),"aria-valuetext":`${C(H)}: ${$}`,onPointerMove:e=>z(e.clientX,e.currentTarget.getBoundingClientRect()),onPointerLeave:()=>i(null),onFocus:e=>{e.currentTarget.matches(":focus-visible")&&i(o=>o??1)},onBlur:()=>i(null),onKeyDown:Y,className:X("relative w-full cursor-crosshair rounded-row",ce[g],c),style:{height:L},children:[u>0&&b.unmeasured.map((e,o)=>t.jsx("div",{"aria-hidden":!0,"data-part":"unmeasured",className:"unmeasured-hatch absolute",style:{left:e.x,top:x,width:e.width,height:d-x}},o)),u>0&&t.jsxs("svg",{"aria-hidden":!0,width:u,height:L,className:"relative block",children:[A.ticks.map(e=>t.jsxs("g",{children:[t.jsx("line",{"data-part":"grid",x1:r,x2:m,y1:S(e),y2:S(e),stroke:e===0?"var(--rule-strong)":"var(--hairline)",strokeWidth:1,shapeRendering:"crispEdges"}),t.jsx("text",{x:r-8,y:S(e),dy:"0.32em",textAnchor:"end",className:"fill-ink-muted font-mono text-micro",children:e})]},e)),b.unmeasured.map((e,o)=>e.width>=he&&t.jsx("text",{"data-part":"unmeasured-label",x:e.x+8,y:x+14,className:"fill-ink-muted font-mono text-micro",stroke:"var(--glass-float)",strokeWidth:4,strokeLinejoin:"round",paintOrder:"stroke",children:"Not measured"},o)),b.areas.map((e,o)=>t.jsx("path",{d:e,fill:"currentColor",stroke:"none",opacity:.1},o)),b.lines.map((e,o)=>t.jsx("path",{"data-part":"line",d:e,fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"},o)),B.map(e=>{const o=W(e);return o<r+16||o>m-16?null:t.jsx("text",{"data-part":"time-tick",x:o,y:L-6,textAnchor:"middle",className:"fill-ink-muted font-mono text-micro",children:U(e)},e)}),h!==null&&t.jsxs("g",{"data-part":"crosshair",children:[t.jsx("line",{x1:v,x2:v,y1:x,y2:d,stroke:"var(--ink-muted)",strokeWidth:1,shapeRendering:"crispEdges"}),E&&t.jsx("circle",{cx:v,cy:S(Math.min(E.value,A.ceiling)),r:4.5,fill:"currentColor",stroke:"var(--glass-float)",strokeWidth:2})]})]}),h!==null&&u>0&&t.jsxs("div",{"aria-hidden":!0,"data-part":"reading",className:"pointer-events-none absolute z-10 rounded-row bg-glass-float px-2.5 py-1.5 font-mono text-fact whitespace-nowrap inset-ring inset-ring-rule",style:M>.62?{top:x,right:u-v+10}:{top:x,left:v+10},children:[t.jsx("span",{className:"block text-ink-muted",children:C(H)}),t.jsx("span",{className:"block text-ink",children:$})]})]})}const xe=6e4;function pe(s,n){const[a,g]=y.useState(null),[w,l]=y.useState(null),c=s>Q,p=n?.startedAt??null,u=n?_(n).at:null;if(y.useEffect(()=>{if(!c)return;let h=!0;const i=()=>{Z(s).then(m=>{h&&(g({windowMs:s,history:m}),l(null))},()=>{h&&l(s)})};i();const r=setInterval(i,xe);return()=>{h=!1,clearInterval(r)}},[c,s,p,u]),!c)return{history:n,status:n?"ready":"loading"};const k=a?.windowMs===s&&(p===null||a.history.startedAt===p)&&(u===null||_(a.history).at===u)?a.history:null;return k?{history:J(k,n),status:"ready"}:{history:null,status:w===s?"failed":"loading"}}const P={needsYou:{title:"Needs you",about:"How many sessions were waiting on you, over time.",tone:"needs-you",chart:"Sessions that need you",describe:s=>`${s} ${s===1?"needs":"need"} you`,explainer:"A session needs you when it has stopped and cannot go on without an answer: a permission to grant, a question, or a plan to approve. The line is how many were waiting at each moment."},working:{title:"Working",about:"How many sessions were in the middle of a task, over time.",tone:"working",chart:"Sessions working",describe:s=>`${s} working`,explainer:"A session is working while it is in the middle of a task. The line is how many were busy at each moment."},idle:{title:"Idle",about:"How many sessions were ready for a new prompt, over time.",tone:"idle",chart:"Idle sessions",describe:s=>`${s} idle`,explainer:s=>`A session is idle when it has finished what it was asked and is ready for a new prompt. One left idle ${s} or more is stale, and is counted under Stale instead. The line is how many were idle at each moment.`}},D="–";function F({label:s,value:n,caption:a}){return t.jsxs("div",{"data-slot":"figure",className:"min-w-0",children:[t.jsx("p",{className:"text-body font-medium text-ink-secondary",children:s}),t.jsx("p",{className:"mt-2 text-stat font-medium tabular-nums",children:n}),t.jsx("p",{className:"mt-2 text-caption text-ink-muted",children:a})]})}function fe({metric:s,state:n,now:a}){const[g,w]=y.useState("15m"),l=O.find(d=>d.id===g)??O[0],c=P[s],{history:p,status:u}=pe(l.ms,n.history),k=a-l.ms,h=p?te(p.points,s):[];let i=null;for(const d of h)d.at<k||d.at>a||(i===null||d.value>i)&&(i=d.value);const r=h[h.length-1],m=r&&a-r.at<=se?r.value:null,x=c.tone==="needs-you"&&ne(n.snapshot?.sessions)===0?"idle":c.tone;return t.jsxs(t.Fragment,{children:[t.jsxs("div",{className:"mt-5 flex flex-wrap items-end justify-between gap-x-6 gap-y-4",children:[t.jsxs("div",{className:"flex gap-6",children:[t.jsx(F,{label:"Now",value:m===null?D:String(m),caption:m===null?"Not being measured":"At the last check"}),t.jsx("div",{className:"border-l border-rule pl-6",children:t.jsx(F,{label:"Highest",value:i===null?D:String(i),caption:i===null?"Nothing measured":`In ${l.words}`})})]}),t.jsx(oe,{label:"Time window",value:g,onValueChange:w,options:O.map(({id:d,label:N})=>({value:d,label:N}))})]}),t.jsx("div",{className:"mt-5 h-55",children:u==="ready"?t.jsx(me,{samples:h,start:k,end:a,tone:x,label:`${c.chart} over ${l.words}`,describe:c.describe}):u==="loading"?t.jsx(ae,{label:`Reading ${l.words}`,className:"h-full"}):t.jsx(re,{tone:"error",title:`${ie(l.words)} could not be read`,children:t.jsx("p",{children:"Agent Lookout did not answer when asked for it. The page asks again every minute. The 15 minute window is still there."})})}),t.jsxs("div",{"data-slot":"explainer",className:"mt-5 rounded-inner bg-fill-quiet px-4 py-3 inset-ring inset-ring-hairline",children:[t.jsx("p",{className:"text-row font-semibold text-ink",children:"What this counts"}),t.jsx("p",{className:"mt-0.5 text-body text-ink-secondary",children:typeof c.explainer=="string"?c.explainer:c.explainer(le(de(n.snapshot?.timeRules)))}),t.jsxs("p",{className:"mt-1.5 text-body text-ink-secondary",children:["Hatched time was not measured: Agent Lookout was not running, or could not read its source, or the history was cleared then. It is never drawn as zero."," ",n.history?.kept?.where==="memory"?"History covers the last six hours and starts again when Agent Lookout restarts.":"History covers the last six hours, and is kept when Agent Lookout restarts."]})]})]})}function je({metric:s,open:n,onOpenChange:a,state:g,now:w}){const l=P[s];return t.jsx(ee,{open:n,onOpenChange:a,title:l.title,description:l.about,size:"wide",children:t.jsx(fe,{metric:s,state:g,now:w})})}export{je as HistoryPanel};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{r as o,j as t}from"./react-C2DiJaR6.js";import{L as M,c as D,o as C,F as j,T as _,p as U,q as Y,u as G,w as K,v as z,x as V,y as X,J as Q,z as Z,A as ee,R as te,D as se,B as ne,E as q,G as ae,I as re,n as le,K as oe,M as W,N as ie,O as ce,P as ue,Q as de,U as $,V as I,W as fe,X as me,f as T,Y as he,Z as pe,_ as xe,$ as ge,a0 as je,a1 as be,a2 as ke,a3 as ye,a4 as we,a5 as Ne,a6 as F,a7 as P,a8 as ve,a9 as Se}from"./index-dMRHt2Y8.js";import"./motion-DlYOyClL.js";import"./radix-BH0JH8UU.js";function Ee(e){const n=o.useRef(null),[s,a]=o.useState(!1);return o.useLayoutEffect(()=>{const r=n.current;if(!r)return;const d=()=>a(r.scrollHeight>r.clientHeight);d();const l=new ResizeObserver(d);l.observe(r);for(const c of r.children)l.observe(c);return()=>l.disconnect()},[e]),[n,s]}const Re="Last messages are not read from another machine.",Te="Its last message could not be read. The page asks again every two seconds.",Le="The start of a longer message is left out.";function E({children:e}){return t.jsx("p",{"data-part":"last-message-state",className:"py-2.5 text-body text-ink-secondary",children:e})}function Ae(e,n,s){switch(e){case"off":return n===void 0?"Last messages are off.":t.jsxs(t.Fragment,{children:["Last messages are off:"," ",t.jsx("code",{"data-slot":"fact",className:"font-mono text-fact",children:t.jsx(C,{children:n})})," ","is off."]});case"not-read":return`Agent Lookout does not read what ${s} sessions say.`;case"not-found":return"Its transcript was not found.";case"unreadable":return"Its transcript could not be read.";case"nothing-yet":return"It has not said anything yet.";case"too-far-back":return"Its last message is further back than the end of its transcript that Agent Lookout reads."}}function $e({text:e,cut:n}){const[s,a]=Ee(e);return t.jsxs("div",{className:"pt-2.5",children:[n&&t.jsx("p",{"data-part":"last-message-cut",className:"mb-1.5 text-caption text-ink-muted",children:Le}),t.jsx("div",{ref:s,"data-part":"last-message-scroll","data-overflows":a||void 0,role:a?"group":void 0,tabIndex:a?0:void 0,"aria-label":a?"Its last message":void 0,className:D("thin-scroll max-h-60 overflow-y-auto",a&&"focus-visible:-outline-offset-2"),children:t.jsx("p",{"data-part":"last-message-text",className:"text-body whitespace-pre-wrap text-ink wrap-anywhere select-text",children:e})})]})}function Fe({reading:e,remote:n,agent:s}){if(n)return t.jsx(E,{children:Re});if(e.status==="idle")return null;if(e.status==="loading")return t.jsx(M,{label:"Reading its last message",className:"justify-start px-0 py-2.5"});if(e.status==="failed"||e.answer===null)return t.jsx(E,{children:Te});const{answer:a}=e;return a.message!==null?t.jsx($e,{text:a.message.text,cut:a.message.cut}):t.jsx(E,{children:Ae(a.reason,a.setting,s)})}function Oe({pullRequest:e}){return t.jsxs(j,{label:"Pull request",children:[t.jsxs(_,{"data-part":"pull-request",href:e.url,target:"_blank",rel:"noreferrer noopener",lines:2,tooltip:`#${e.number} ${e.title}`,className:"wrap-anywhere underline decoration-rule-strong underline-offset-2 transition-colors duration-120 hover:decoration-ink",children:[t.jsxs("span",{className:"tabular-nums",children:["#",e.number]})," ",e.title]}),t.jsx("span",{"data-part":"pull-request-state",className:"block text-caption font-normal text-ink-secondary tabular-nums",children:U(e)})]})}const Me=new Intl.NumberFormat("en-US",{maximumFractionDigits:0,useGrouping:!0});function N(e){return Me.format(e)}function De(e){return`${N(e.input)} in, ${N(e.output)} out`}function Ce(e){return`${N(e.input)} tokens in, ${N(e.output)} tokens out`}const O="Newest reply";function _e(e){if(e.cached===void 0)return O;const n=e.cached===0?"none":N(e.cached);return`${O} · ${n} of the input from a cache`}const qe="Not recorded",We="None recorded yet";function Ie(e,n){const s=n.find(r=>r.id===e.source);return s?s.capabilities?s.capabilities.tokens:s.agents?.find(r=>r.label===e.agent)?.capabilities.tokens??null:null}function Pe(e,n){const s=Ie(e,n);return s?.level==="no"?s.reason:e.machine!==void 0?`${e.machine} sent none`:We}function Be({session:e,sources:n}){const{tokens:s}=e;return t.jsxs(j,{label:"Tokens",children:[s?t.jsxs("span",{"data-part":"tokens",className:"tabular-nums",children:[t.jsx("span",{"aria-hidden":!0,children:De(s)}),t.jsx("span",{className:"sr-only",children:Ce(s)})]}):t.jsxs("span",{"data-part":"tokens",children:[t.jsx("span",{"aria-hidden":!0,children:"–"}),t.jsx("span",{className:"sr-only",children:qe})]}),t.jsx("span",{"data-part":"tokens-note",className:"block text-caption font-normal text-pretty text-ink-secondary tabular-nums",children:s?_e(s):Pe(e,n)})]})}function He(e,n={}){const{open:s=!0,onStopped:a,request:r=Y}=n,[d,l]=o.useState({kind:"idle"}),[c,i]=o.useState(s);s!==c&&(i(s),!s&&!(d.kind==="confirming"&&d.busy)&&l({kind:"idle"}));const p=o.useRef(s);o.useEffect(()=>{p.current=s},[s]);const u=o.useId(),[m]=o.useState(()=>({stop:`${u}-stop`,cancel:`${u}-cancel`,outcome:`${u}-outcome`})),f=o.useRef(null),h=o.useRef(!1),g=o.useRef(!0);o.useEffect(()=>(g.current=!0,()=>{g.current=!1}),[]),o.useEffect(()=>{const k=f.current;f.current=null,k!==null&&document.getElementById(m[k])?.focus()},[d,m]);const x=o.useCallback(()=>{h.current||(f.current="cancel",l({kind:"confirming",busy:!1}))},[]),y=o.useCallback(()=>{h.current||(f.current="stop",l({kind:"idle"}))},[]),b=o.useCallback(()=>{h.current||(h.current=!0,l({kind:"confirming",busy:!0}),r(e).then(k=>{if(h.current=!1,!!g.current){if(k==="stopped"&&a?.(),!p.current){l({kind:"idle"});return}f.current="outcome",l({kind:"answered",outcome:k,at:Date.now()})}}))},[e,r,a]);return{step:d,ask:x,cancel:y,confirm:b,ids:m}}const Je={status:"idle",answer:null};function Ue(e){return e.message===null&&(e.reason==="off"||e.reason==="not-read")}function Ye(e,n){const{active:s,hidden:a,beat:r}=n,[d,l]=o.useState(null);d!==null&&(!s||d.sessionId!==e)&&l(null);const c=o.useRef(void 0),i=o.useRef(!1),p=o.useRef(!1),u=o.useRef(0),m=o.useRef(e),f=o.useRef(!1);if(o.useEffect(()=>(f.current=!0,()=>{f.current=!1}),[]),o.useEffect(()=>{if((!s||m.current!==e)&&(m.current=e,c.current=void 0,i.current=!1,p.current=!1,u.current+=1),!s||a||p.current||i.current||c.current===r)return;c.current=r,i.current=!0;const g=u.current;G(e).then(x=>{g===u.current&&(i.current=!1,!(!f.current||x==="busy")&&(Ue(x)&&(p.current=!0),l({sessionId:e,answer:x,failed:!1})))},()=>{g===u.current&&(i.current=!1,f.current&&l(x=>x?.sessionId===e&&x.answer!==null?x:{sessionId:e,answer:null,failed:!0}))})},[e,s,a,r]),!s)return Je;const h=d?.sessionId===e?d:null;return h?.answer?{status:"ready",answer:h.answer}:h?.failed?{status:"failed",answer:null}:{status:"loading",answer:null}}const Ge="Read over SSH. Jump, Stop, Allow and Deny work on this computer only, so use that machine to go to it, stop it or answer it.",B=[];function Ke(e,n){const s=CSS.escape(e),a=[...document.querySelectorAll(`:is([data-slot="session-row"], [data-slot="board-card"], [data-slot="hero-session"])[data-session="${s}"] a[data-part="name"]`)];return n!==null&&a.includes(n)?n:a.find(r=>!r.closest('[data-slot="hero-session"]'))??a[0]??null}function ze({session:e,gone:n,asOf:s,now:a}){const r=e.status==="finished"||e.status==="failed",d=pe(e),l=xe(e),c=d?"stale":l||n&&e.status==="needs-you"?"answered":e.status,i=e.statusSince,p=i!==null&&!l?Math.max(0,s-i):null,u=ge(e,s),m=q(e),f=m?je(e):null,h=m&&!n&&!e.ask&&e.waitingText?.trim()||null,g=i!==null?`${r?"at":d?"idle since":l?"asked for permission at":"since"} ${I(i,a)}`:"time not reported",x=[m?be(e):null,g,u&&ke(u)].filter(y=>typeof y=="string");return t.jsxs(j,{label:"Status",note:h?t.jsxs(t.Fragment,{children:[t.jsx(_,{"data-part":"asking",lines:2,className:"wrap-anywhere",children:h}),f&&t.jsx("span",{className:"block",children:f})]}):f??void 0,children:[t.jsxs("span",{className:"inline-flex items-center gap-2",children:[t.jsx(ye,{kind:c}),t.jsx("span",{"data-part":"status",children:d?"Stale":l?we:Ne[e.status]}),p!==null&&t.jsxs("span",{"data-part":"duration",className:"tabular-nums",children:[t.jsx("span",{className:"sr-only",children:r?` ${F(p)} ago`:` for ${F(p)}`}),t.jsxs("span",{"aria-hidden":!0,children:[P(p),r&&" ago"]})]})]}),t.jsx("span",{"data-part":"since",className:"block text-caption font-normal text-ink-secondary tabular-nums",children:x.join(", ")})]})}function Ve({session:e,state:n,now:s,asOf:a}){const r=o.useMemo(()=>ve({sessions:n.snapshot?.sessions??B,events:n.events,history:n.history,now:s,asOf:a,eventsFull:n.events.length>=W}),[n.snapshot,n.events,n.history,s,a]);if(!r)return t.jsxs(j,{label:"Waits",children:["Not known",t.jsx("span",{className:"block text-caption font-normal text-ink-secondary",children:"The history could not be read"})]});const d=r.sessions.find(i=>i.id===e.id),l=r.period.today?"today":`since ${T(r.period.from)}`;let c="None";if(d){const i=`${d.startKnown?"":"at least "}${P(d.ms)}`;c=d.times===1?`Once, ${i}`:`${d.times} times, ${i} in all`}return t.jsxs(j,{label:"Waits",children:[t.jsx("span",{"data-part":"waits",className:"tabular-nums",children:c}),t.jsx("span",{className:"block text-caption font-normal text-ink-secondary",children:l})]})}function R({title:e,aside:n,children:s,...a}){return t.jsxs("section",{...a,"aria-label":e,className:"mt-6",children:[t.jsxs("div",{className:"flex items-baseline justify-between gap-4 border-b border-hairline pb-2",children:[t.jsx("h3",{className:"text-body font-semibold text-ink",children:e}),n&&t.jsx("p",{className:"text-caption text-ink-muted",children:n})]}),s]})}function Xe({session:e,gone:n,state:s,now:a,asOf:r,lastMessage:d}){const l=s.snapshot?.sources??[],c=s.snapshot?.sessions??B,i=le(s.snapshot?.timeRules),p=oe(e.surface),u=s.events.length>=W,m=o.useMemo(()=>ie(s.events.filter(x=>x.sessionId===e.id),c),[s.events,c,e.id]),f=ce(s.events,s.history,u),h=o.useMemo(()=>s.history?ue({sessions:c,events:s.events,history:s.history,now:a,asOf:r,eventsFull:u,staleAfterMs:i}):null,[c,s.events,s.history,a,r,u,i]),g=h?.rows.find(x=>x.id===e.id);return t.jsxs(t.Fragment,{children:[n&&t.jsx("p",{"data-part":"gone",className:"mt-4 text-body text-ink-secondary",children:"This session has left the list. Here is what was last known of it."}),t.jsxs(de,{className:"mt-3",children:[t.jsx(ze,{session:e,gone:n,asOf:r,now:a}),t.jsx(j,{label:"Agent",children:$(e,l)}),e.machine!==void 0&&t.jsx(j,{label:"Machine",note:Ge,children:t.jsx("span",{"data-part":"machine",children:e.machine})}),p!==null&&t.jsx(j,{label:"App",children:p}),t.jsx(j,{label:"Folder",mono:e.cwd!==null,children:e.cwd!==null?t.jsx(C,{"data-part":"folder",className:"select-text",children:e.cwd}):"Not known"}),e.git?.repository!==void 0&&t.jsx(j,{label:"Repository",children:t.jsx("span",{"data-part":"repository",className:"wrap-anywhere",children:e.git.repository.name})}),e.git?.branch!==void 0?t.jsx(j,{label:"Branch",children:t.jsx("span",{"data-part":"branch",className:"wrap-anywhere",children:e.git.branch})}):e.git?.commit!==void 0&&t.jsx(j,{label:"Commit",mono:!0,children:e.git.commit}),e.git?.pullRequest!==void 0&&t.jsx(Oe,{pullRequest:e.git.pullRequest}),t.jsx(j,{label:"Started",children:t.jsx("span",{className:"tabular-nums",children:e.startedAt!==null?I(e.startedAt,a):"Not reported"})}),e.pid!==void 0&&t.jsx(j,{label:"Process",mono:!0,note:e.alive===!1?"The process has ended.":!n&&e.source==="claude-code"&&e.surface==="desktop"?fe:void 0,children:e.pid}),t.jsx(Be,{session:e,sources:l}),t.jsx(Ve,{session:e,state:s,now:a,asOf:r})]}),!n&&t.jsx(R,{"data-part":"last-message",title:"Last message",children:t.jsx(Fe,{reading:d,remote:e.machine!==void 0,agent:$(e,l)})}),t.jsx(R,{"data-part":"events",title:"Events",aside:f?`since ${T(f.at)}`:void 0,children:m.length>0?t.jsx(me,{entries:m,now:a,className:"pt-0.5"}):t.jsxs("p",{className:"py-2.5 text-body text-ink-secondary",children:["Nothing has happened to this session"," ",f?`since ${T(f.at)}`:"yet","."]})}),t.jsx(R,{"data-part":"timeline",title:"Timeline",aside:"last hour",children:h&&g?t.jsx(he,{timeline:h,rows:[g],named:!1,label:"Its last hour",className:"pt-3"}):t.jsx("p",{className:"py-2.5 text-body text-ink-secondary",children:h?"It did not run in the last hour.":"The last hour could not be read. The page asks again every two seconds."})})]})}function Qe({sessionId:e,open:n,onClose:s,state:a,now:r,onStopped:d,onAnswered:l}){const c=a.snapshot?.sessions.find(w=>w.id===e)??null,[i,p]=o.useState(null);c!==null&&c!==i?.session?p({session:c,at:a.lastOkAt??r}):c===null&&(i?.session.waitingText!==void 0||i?.session.ask!==void 0)&&p({...i,session:K(i.session)});const u=c??i?.session??null,m=c===null&&i!==null,f=z(),h=Ye(e,{active:n&&c!==null&&c.machine===void 0,hidden:f,beat:a.lastOkAt}),g=V(u??{id:e,source:"status-files",surface:"unknown",links:{}}),x=m&&i?i.at:a.phase==="stalled"&&a.lastOkAt!==null?a.lastOkAt:r,y=g.outcome!==null||g.asking!==null,b=He(e,{open:n,onStopped:d}),k=b.step.kind==="answered"&&c?.alive===!0&&(a.snapshot?.generatedAt??0)>b.step.at,H=b.step.kind==="answered"&&b.step.outcome==="stopped"&&!k,L=X(u??{id:e,source:"status-files",status:"unknown",cwd:null},H?!0:void 0),J=b.step.kind==="answered"||b.step.kind==="confirming"&&(b.step.busy||!m);let v,S,A="wide";return u?(v=u.name,S=t.jsxs(t.Fragment,{children:[!m&&t.jsx("div",{className:D("flex flex-wrap items-center gap-x-2 gap-y-1",y&&"mt-3"),children:t.jsx(Q,{session:u,jump:g})}),J&&t.jsx(Z,{session:u,stop:b,className:"mt-3"}),!m&&t.jsx(ee,{session:u,onAnswered:l,className:"mt-3"}),t.jsx(te,{resume:L,className:"mt-3"}),t.jsx(Xe,{session:u,gone:m,state:a,now:r,asOf:x,lastMessage:h})]})):a.snapshot===null?(v="Session",S=t.jsx(M,{label:"Reading sessions",className:"justify-start px-0 pt-5 pb-1"})):(v="No such session",A="sm",S=t.jsxs("div",{"data-part":"missing",children:[t.jsx("p",{className:"mt-2 text-body text-ink-secondary",children:"Agent Lookout is not watching a session at this address. It may have ended before this page was opened."}),t.jsx(Se,{className:"mt-4",onClick:s,children:"Go to the Overview"})]})),t.jsx(se,{open:n,onOpenChange:w=>{w||s()},title:v,size:A,focusTitle:!0,returnFocus:w=>Ke(e,w)??document.querySelector("main"),actions:u&&t.jsxs(t.Fragment,{children:[!m&&t.jsxs(t.Fragment,{children:[t.jsx(ne,{session:u,jump:g,variant:q(u)?"needs-you":"quiet"}),t.jsx(ae,{session:u,stop:b})]}),t.jsx(re,{session:u,resume:L,tooltip:!1})]}),children:S})}function nt({sessionId:e,onClose:n,state:s,now:a,onStopped:r,onAnswered:d}){const[l,c]=o.useState(e);return e!==null&&e!==l&&c(e),l===null?null:t.jsx(Qe,{sessionId:l,open:e!==null,onClose:n,state:s,now:a,onStopped:r,onAnswered:d},l)}export{nt as SessionPanel};
|