agent-lookout 0.2.7 → 0.2.8

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/README.md CHANGED
@@ -4,18 +4,18 @@ Shows the AI agent sessions on your computer in one browser page, and which need
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/agent-lookout)](https://www.npmjs.com/package/agent-lookout) [![CI](https://img.shields.io/github/actions/workflow/status/Olanetsoft/agent-lookout/ci.yml?branch=main&label=CI)](https://github.com/Olanetsoft/agent-lookout/actions/workflows/ci.yml) [![License](https://img.shields.io/npm/l/agent-lookout)](LICENSE) ![Node.js](https://img.shields.io/node/v/agent-lookout)
6
6
 
7
- It finds Claude Code and Codex sessions with no setup, and any agent that writes a status file. It never starts a session, stops one only when you press Stop and confirm, and answers a permission prompt only on Allow, Deny or a rule you added.
7
+ It finds Claude Code, Codex and Antigravity CLI sessions with no setup, and any agent that writes a status file. It never starts a session, stops one only when you press Stop and confirm, and answers a permission prompt only on Allow, Deny or a rule you added.
8
8
 
9
- By default Agent Lookout sends nothing anywhere. The one exception: the Mac app's daily check on GitHub for a newer version, which Settings turns off. Email, a webhook, pull requests through your `gh` and other machines over your `ssh` stay off until you set them up. [Privacy](#privacy)
9
+ By default Agent Lookout sends nothing anywhere. The one exception: the Mac app's daily check on GitHub for a newer version, which Settings turns off. Email, a webhook, pushes to your phone through ntfy or Pushover, pull requests through your `gh` and other machines over your `ssh` stay off until you set them up. [Privacy](#privacy)
10
10
 
11
11
  [Install](#install) · [Download for Mac](https://github.com/Olanetsoft/agent-lookout/releases/latest) · [Guide](docs/GUIDE.md) · [Privacy](#privacy) · [Changelog](CHANGELOG.md) · [For agents](#for-coding-agents) · [Roadmap](https://github.com/Olanetsoft/agent-lookout/milestones) · [Website](https://agent-lookout.vercel.app)
12
12
 
13
13
  <picture>
14
14
  <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Olanetsoft/agent-lookout/main/docs/images/dashboard-day.png">
15
- <img alt="The Overview. In the Needs you panel, checkout-flow has waited over 4 minutes for permission, with a Jump button, above bars of how long sessions waited and counts of working, idle and stale sessions. The Last hour chart is beside it. Below are the Sessions list, the Events log and the Timeline." src="https://raw.githubusercontent.com/Olanetsoft/agent-lookout/main/docs/images/dashboard-night.png">
15
+ <img alt="The Overview. In the Needs you panel, checkout-flow, a Claude Code session in Terminal, has waited 4 minutes 12 seconds for permission. Under Asks to run are the whole command, npm test, its description and the Deny and Allow buttons, and a Jump button is beside the timer. Below them are bars of how long sessions waited on you and counts of working, idle and stale sessions. The Last hour chart is beside the panel. Under both are the Sessions list, where billing-migration is marked devbox, the other machine it runs on, and the Events log." src="https://raw.githubusercontent.com/Olanetsoft/agent-lookout/main/docs/images/dashboard-night.png">
16
16
  </picture>
17
17
 
18
- _One session has waited over 4 minutes for permission. Jump takes you to it in VS Code._
18
+ _A Claude Code session asks to run `npm test`. With the Claude Code plugin, Allow or Deny answers it from the page, and Jump takes you to it in Terminal._
19
19
 
20
20
  ## Install
21
21
 
@@ -23,7 +23,7 @@ Start it with npx or the Mac app. Add the Claude Code plugin to either one to an
23
23
 
24
24
  ### With npx
25
25
 
26
- You need Node.js 22.12 or newer, and Claude Code, Codex or both.
26
+ You need Node.js 22.12 or newer, and Claude Code, Codex or the Antigravity CLI.
27
27
 
28
28
  ```sh
29
29
  npx agent-lookout
@@ -46,7 +46,7 @@ It prints `{"ok":true,"version":"…"}`. If the port is in use, add `--port 4778
46
46
 
47
47
  ### Mac app
48
48
 
49
- Download `Agent-Lookout-<version>-mac-arm64.dmg` for Apple silicon, or `Agent-Lookout-<version>-mac-x64.dmg` for Intel, from the [latest release](https://github.com/Olanetsoft/agent-lookout/releases/latest), open it and drag Agent Lookout to Applications. It needs no Node.js. It is not signed with an Apple Developer ID yet, so the first time you open it, go to System Settings › Privacy & Security and press Open Anyway.
49
+ Download `Agent-Lookout-<version>-mac-arm64.dmg` for Apple silicon, or `Agent-Lookout-<version>-mac-x64.dmg` for Intel, from the [latest release](https://github.com/Olanetsoft/agent-lookout/releases/latest), open it and drag Agent Lookout to Applications. It needs macOS 13 Ventura or later, and no Node.js. It is not signed with an Apple Developer ID yet, so the first time you open it, go to System Settings › Privacy & Security and press Open Anyway.
50
50
 
51
51
  Its icon in the menu bar shows how many sessions need you. It checks for a newer version about once a day and installs it when you press Install and Restart. [Mac app](docs/INSTALL.md#mac-app) has the steps.
52
52
 
@@ -73,8 +73,8 @@ It should print "Agent Lookout is running at http://127.0.0.1:4777".
73
73
  If it prints anything else, or says the port is in use, stop and tell
74
74
  me what it printed.
75
75
 
76
- Do not use sudo, and do not set up email, a webhook, pull requests
77
- or other machines. You are done when
76
+ Do not use sudo, and do not set up email, a webhook, phone pushes,
77
+ pull requests or other machines. You are done when
78
78
  curl -s http://127.0.0.1:4777/api/health prints {"ok":true and a
79
79
  version number. Tell me what it printed.
80
80
  ```
@@ -85,45 +85,47 @@ To run it from a clone, see [From the repository](docs/INSTALL.md#from-the-repos
85
85
 
86
86
  ## What it does
87
87
 
88
- | Part | What it does |
89
- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
90
- | [Needs you](docs/GUIDE.md#needs-you) | Lists the sessions waiting for permission or an answer, longest first, with what a Claude Code session asks. |
91
- | [Sessions](docs/GUIDE.md#sessions) | Lists the rest by status, by repository or on a board, with folder and branch. `/` searches every session. |
92
- | [Jump](docs/GUIDE.md#jump) | Takes you to a Claude Code session in VS Code, its tmux pane, or its Terminal or iTerm2 tab. |
93
- | [Stop and Resume](docs/GUIDE.md#stop-a-session) | Stop ends a Claude Code session, or those left running, after you confirm. Resume copies the command that reopens it. |
94
- | [Allow and Deny](docs/GUIDE.md#answer-a-permission-prompt) | Answers a Claude Code permission prompt from the page, with the plugin. [Rules](docs/GUIDE.md#permission-rules) you add can answer some for you. |
95
- | [Notifications](docs/GUIDE.md#notifications) and [time rules](docs/GUIDE.md#time-rules) | Tell you when a session waits, remind you of long waits and keep quiet hours. Off until turned on. |
96
- | [Pull requests](docs/GUIDE.md#pull-requests), [email](docs/GUIDE.md#email), [webhook](docs/GUIDE.md#webhook) | Show a session's pull request on github.com and its checks, through your `gh`, and email or post events to one address. Off until set up. |
97
- | [Another machine](docs/GUIDE.md#another-machine-over-ssh) | Shows the sessions of another machine running Agent Lookout, through your `ssh`. Off until you name one. |
98
- | [Your own agents](docs/GUIDE.md#your-own-agents) | Shows any other agent that writes one small JSON file per session. |
88
+ | Part | What it does |
89
+ | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
90
+ | [Needs you](docs/GUIDE.md#needs-you) | Lists the sessions waiting for permission or an answer, longest first, with what a Claude Code session asks. |
91
+ | [Sessions](docs/GUIDE.md#sessions) | Lists the rest by status, by repository or on a board, with folder and branch. `/` searches every session. |
92
+ | [Jump](docs/GUIDE.md#jump) | Takes you to a Claude Code session in VS Code, its tmux pane, or its Terminal or iTerm2 tab. |
93
+ | [Stop and Resume](docs/GUIDE.md#stop-a-session) | Stop ends a Claude Code session, or those left running, after you confirm. Resume copies the command that reopens it. |
94
+ | [Allow and Deny](docs/GUIDE.md#answer-a-permission-prompt) | Answers a Claude Code permission prompt from the page, or the Mac app's notification and menu bar, with the plugin. [Rules](docs/GUIDE.md#permission-rules) you add can answer some for you. |
95
+ | [Notifications](docs/GUIDE.md#notifications) and [time rules](docs/GUIDE.md#time-rules) | Tell you when a session waits, remind you of long waits and keep quiet hours. Off until turned on. |
96
+ | [Pull requests](docs/GUIDE.md#pull-requests), [email](docs/GUIDE.md#email), [webhook](docs/GUIDE.md#webhook) | Show a session's pull request on github.com and its checks, through your `gh`, and email or post events to one address. Off until set up. |
97
+ | [ntfy](docs/GUIDE.md#ntfy) and [Pushover](docs/GUIDE.md#pushover) | Push a wait, its reminders and what else you choose to your phone, with Send a test in Settings. Off until set up. |
98
+ | [Another machine](docs/GUIDE.md#another-machine-over-ssh) | Shows the sessions of another machine running Agent Lookout, through your `ssh`. Off until you name one. |
99
+ | [Your own agents](docs/GUIDE.md#your-own-agents) | Shows any other agent that writes one small JSON file per session. |
99
100
 
100
101
  The Overview also charts the last hour and how long sessions waited on you, from history kept on this computer. [The screen](docs/GUIDE.md#the-screen) covers each part.
101
102
 
102
103
  ## Supported agents and systems
103
104
 
104
- Claude Code and Codex need no setup. Any other agent writes one small JSON file per session to `~/.agent-lookout/sessions`. [Your own agents](docs/GUIDE.md#your-own-agents).
105
+ Claude Code, Codex and the Antigravity CLI need no setup. Any other agent writes one small JSON file per session to `~/.agent-lookout/sessions`. [Your own agents](docs/GUIDE.md#your-own-agents).
105
106
 
106
107
  <!-- Checked against the adapters by tests/integration/collector/adapters/adapter.test.ts -->
107
108
 
108
- | Agent | Needs you | Jump | Quiet for | Stop | Answer |
109
- | ------------ | --------- | ------ | --------- | ------ | ------ |
110
- | Claude Code | Yes | Partly | No | Partly | Partly |
111
- | Codex | No | No | Yes | No | No |
112
- | Status files | Partly | No | Partly | No | No |
113
-
114
- A Codex session waiting for approval shows as working. After 5 minutes its row says how long it has been quiet. [What each agent can report](docs/GUIDE.md#what-each-agent-can-report) gives the reason for every Partly and No.
115
-
116
- | | macOS | Linux | Windows |
117
- | ------------------------------------------ | ----- | ----- | ------- |
118
- | Dashboard, `status`, `mcp`, email, webhook | Yes | Yes | Yes |
119
- | Jump to VS Code | Yes | Yes | Yes |
120
- | Jump to a tmux pane | Yes | Yes | No |
121
- | Jump to Terminal or iTerm2 | Yes | No | No |
122
- | Stop, and ending sessions left running | Yes | Yes | No |
123
- | Allow and Deny, permission rules | Yes | Yes | No |
124
- | Resume | Yes | Yes | No |
125
- | Notifications with no tab open | Yes | No | No |
126
- | Mac app and menu bar | Yes | No | No |
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 |
115
+
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
+
118
+ | | macOS | Linux | Windows |
119
+ | -------------------------------------------------- | ----- | ----- | ------- |
120
+ | Dashboard, `status`, `mcp`, email, webhook, pushes | Yes | Yes | Yes |
121
+ | Jump to VS Code | Yes | Yes | Yes |
122
+ | Jump to a tmux pane | Yes | Yes | No |
123
+ | Jump to Terminal or iTerm2 | Yes | No | No |
124
+ | Stop, and ending sessions left running | Yes | Yes | No |
125
+ | Allow and Deny, permission rules | Yes | Yes | No |
126
+ | Resume | Yes | Yes | No |
127
+ | Notifications with no tab open | Yes | No | No |
128
+ | Mac app and menu bar | Yes | No | No |
127
129
 
128
130
  Developed and used on macOS. On Linux and Windows, CI runs the tests and starts the app, and no one has used it on those desktops yet. [On Linux](docs/GUIDE.md#on-linux) · [On Windows](docs/GUIDE.md#on-windows)
129
131
 
@@ -175,6 +177,7 @@ By default Agent Lookout sends nothing anywhere: no telemetry, analytics, crash
175
177
  Each of these is off until you set it up:
176
178
 
177
179
  - Email and the webhook, each to the one address you name.
180
+ - Pushes to your phone through ntfy, to the one topic you name, or Pushover, to the one user key you name.
178
181
  - Pull requests, through your own `gh`, with `AGENT_LOOKOUT_PULL_REQUESTS=on`.
179
182
  - Other machines, through your own `ssh`, named in `AGENT_LOOKOUT_REMOTES`.
180
183
  - The MCP server, whose answers the agent's app hands to its model.
@@ -187,8 +190,8 @@ It listens on a loopback address only, 127.0.0.1 unless you set another, turns a
187
190
  ## What it does not do yet
188
191
 
189
192
  - It cannot send a session a message, and Resume only copies the command.
190
- - Cloud sessions, Codex cloud tasks and browser chats do not appear.
191
- - Claude Code's and Codex's files are undocumented, so an update can hide sessions. [What breaks when Codex changes](docs/adapters/codex.md#what-breaks-when-codex-changes)
193
+ - Cloud sessions, Codex cloud tasks, browser chats and the Antigravity desktop app's conversations do not appear.
194
+ - Claude Code's and Codex's files are undocumented, and the Antigravity CLI's are documented only for its own agents, so an update can hide sessions. [What breaks when Codex changes](docs/adapters/codex.md#what-breaks-when-codex-changes), [and when Antigravity changes](docs/adapters/antigravity.md#what-breaks-when-antigravity-changes)
192
195
  - The Mac app is not signed with an Apple Developer ID yet, and there is no Linux or Windows app.
193
196
 
194
197
  [What it does not do yet](docs/GUIDE.md#what-it-does-not-do-yet) has the details, and the [milestones](https://github.com/Olanetsoft/agent-lookout/milestones) what is planned.
@@ -205,7 +208,7 @@ No. Stop needs a press and a confirm, and Allow and Deny need a press or a rule
205
208
  <details>
206
209
  <summary>Why does a Codex session never show as needing me?</summary>
207
210
 
208
- Codex's files do not record when it waits for your approval, so watch its Quiet for line instead.
211
+ Its files do not record when it waits for your approval, as far as Agent Lookout can read, so watch the Quiet for line instead. An Antigravity CLI session does show as needing you while agy asks you to approve a tool, which agy's own log records, but not while it waits for an answer to a question.
209
212
 
210
213
  </details>
211
214
 
@@ -241,7 +244,7 @@ To set Agent Lookout up for someone:
241
244
  - Run `npx --yes agent-lookout` in the background. It is ready when `curl -s http://127.0.0.1:4777/api/health` prints `{"ok":true`.
242
245
  - `npx agent-lookout status --json` prints the counts and the sessions that need you, and exits with 0, 1 or 2 as above.
243
246
  - `GET /api/sessions` lists every session. All routes answer on loopback only, and [docs/API.md](docs/API.md) lists them.
244
- - Do not use `sudo`, and do not set up email, a webhook, pull requests or other machines unless asked.
247
+ - Do not use `sudo`, and do not set up email, a webhook, phone pushes, pull requests or other machines unless asked.
245
248
 
246
249
  To change its code, read [AGENTS.md](AGENTS.md) first. It has the commands, the rules a change must keep and where tests go. Run `npm run check` before you finish. Codex, and Claude Code from version 2.1.277, read AGENTS.md on their own. If you keep a CLAUDE.md or CLAUDE.local.md of your own in the clone, which git ignores here, Claude Code reads that instead, so put the line `@AGENTS.md` in it.
247
250
 
package/SECURITY.md CHANGED
@@ -12,7 +12,7 @@ The project has one maintainer, so replies are best effort. Fixes land on `main`
12
12
 
13
13
  ## What the app handles
14
14
 
15
- Agent Lookout reads metadata about the Claude Code and Codex sessions on your machine, and, once you name them, on other machines of yours that run Agent Lookout, read over SSH: names, working directories, statuses and, for Claude Code, process IDs. For each session's folder it reads which git branch is checked out, and which repository the folder belongs to, from the repository's `.git` entry, `HEAD` and a worktree's `commondir` alone, and runs no git command. With `AGENT_LOOKOUT_PULL_REQUESTS=on`, and only then, it also reads that repository's `config` and its remote's `refs/remotes/<remote>/HEAD`, and runs the person's own `gh`, directly and never through a shell, as `gh pr view --repo github.com/<owner>/<name> --json number,title,state,isDraft,statusCheckRollup -- <branch>`, which sends GitHub the repository's owner and name and the branch's name: [PRIVACY.md](PRIVACY.md#gh). On a Mac it also lists every process's ID, parent, terminal and program name with `ps` when a new Claude Code session appears, to find the Terminal or iTerm2 tab it runs in, and keeps only the app and the session's terminal. It serves that to the dashboard over HTTP on a loopback address. Of Claude Code's transcripts it reads only the end of a waiting session's own, opened without following a link, and keeps one line of it, what the session is asking, until the wait ends. That line goes to the dashboard and to notifications on this machine, never to an email, a webhook post or `agent-lookout mcp`. It does open Codex's session files, which hold whole conversations, and keeps only when turns started and ended and a few fields. It writes to no agent's files. It ends a Claude Code session's process only when the person presses Stop and confirms, and answers a Claude Code permission prompt only when the person presses Allow or Deny, or when a permission rule the person added in Settings decides it, by the routes described below, and never on its own. It also reads the folder of status files, `~/.agent-lookout/sessions` or the one `AGENT_LOOKOUT_STATUS_DIR` names, where any program can write a small JSON file to show one of its sessions, and it writes nothing there. It keeps the Events log and the counts behind its charts in its own folder, `~/.agent-lookout/history`, which it makes with mode 700, in files of mode 600 that it neither reads nor writes through a link. They hold session IDs and names, statuses and times, and never what a waiting session is asking. `AGENT_LOOKOUT_HISTORY=off` keeps them in memory only. Beside them it keeps the time rules and the permission rules set in Settings in `~/.agent-lookout/settings.json`, or the file `AGENT_LOOKOUT_SETTINGS_FILE` names, of mode 600, which holds nothing about a session and which it neither reads nor writes through a link. [PRIVACY.md](PRIVACY.md) lists everything it reads, runs and keeps.
15
+ Agent Lookout reads metadata about the Claude Code, Codex and Antigravity CLI sessions on your machine, and, once you name them, on other machines of yours that run Agent Lookout, read over SSH: names, working directories, statuses and, for Claude Code, process IDs. For each session's folder it reads which git branch is checked out, and which repository the folder belongs to, from the repository's `.git` entry, `HEAD` and a worktree's `commondir` alone, and runs no git command. With `AGENT_LOOKOUT_PULL_REQUESTS=on`, and only then, it also reads that repository's `config` and its remote's `refs/remotes/<remote>/HEAD`, and runs the person's own `gh`, directly and never through a shell, as `gh pr view --repo github.com/<owner>/<name> --json number,title,state,isDraft,statusCheckRollup -- <branch>`, which sends GitHub the repository's owner and name and the branch's name: [PRIVACY.md](PRIVACY.md#gh). On a Mac it also lists every process's ID, parent, terminal and program name with `ps` when a new Claude Code session appears, to find the Terminal or iTerm2 tab it runs in, and keeps only the app and the session's terminal. It serves that to the dashboard over HTTP on a loopback address. Of Claude Code's transcripts it reads only the end of a waiting session's own, opened without following a link, and keeps one line of it, what the session is asking, until the wait ends. That line goes to the dashboard and to notifications on this machine, never to an email, a webhook post or `agent-lookout mcp`. It does open Codex's session files, which hold whole conversations, and keeps only when turns started and ended and a few fields. It writes to no agent's files. It ends a Claude Code session's process only when the person presses Stop and confirms, and answers a Claude Code permission prompt only when the person presses Allow or Deny, or when a permission rule the person added in Settings decides it, by the routes described below, and never on its own. It also reads the folder of status files, `~/.agent-lookout/sessions` or the one `AGENT_LOOKOUT_STATUS_DIR` names, where any program can write a small JSON file to show one of its sessions, and it writes nothing there. It keeps the Events log and the counts behind its charts in its own folder, `~/.agent-lookout/history`, which it makes with mode 700, in files of mode 600 that it neither reads nor writes through a link. They hold session IDs and names, statuses and times, and never what a waiting session is asking. `AGENT_LOOKOUT_HISTORY=off` keeps them in memory only. Beside them it keeps the time rules and the permission rules set in Settings in `~/.agent-lookout/settings.json`, or the file `AGENT_LOOKOUT_SETTINGS_FILE` names, of mode 600, which holds nothing about a session and which it neither reads nor writes through a link. [PRIVACY.md](PRIVACY.md) lists everything it reads, runs and keeps.
16
16
 
17
17
  With notifications turned on in Settings, the dashboard page hands a session's name and a fixed text, the reason it waits or Finished, Failed or Ended, to the browser's Notifications API, which passes them to the operating system. When no dashboard page is open, the local server shows the notification itself on macOS: it runs `/usr/bin/osascript` with a fixed script, and passes the name and the text to that script as arguments. Each dashboard page tells the server whether its notifications are on, in a header on its requests, and the server shows its own only while the last page to say so said they are on, or `AGENT_LOOKOUT_NOTIFICATIONS=on` was set and no page has said anything. Nothing is sent over the network for any of this. [PRIVACY.md](PRIVACY.md#notifications) has the details.
18
18
 
@@ -20,11 +20,13 @@ Email notifications are off unless they are set up in the environment Agent Look
20
20
 
21
21
  Webhook posts are off unless `AGENT_LOOKOUT_WEBHOOK_URL` is set in the environment Agent Lookout starts with. Then, for each event chosen, the collector sends one short JSON post to that one address, over HTTPS with the certificate checked, or over plain HTTP only to `127.0.0.1` or `localhost`. It holds the session's name, what happened, when, the name of its project folder, its app and its agent, and, for a wait with `AGENT_LOOKOUT_WEBHOOK_ASKING=on`, what the session is asking. It follows no redirect and tries nothing twice. The address is itself a secret: whoever has a Slack incoming webhook's address can post to its channel. So it lives in the environment of the Agent Lookout process and nowhere else: it is never written to a file, sent to the dashboard or printed. `GET /api/webhook` tells the dashboard only whether the webhook is on, the host, the events, the delay, whether a wait's post says what the session is asking, and how the last post went. [PRIVACY.md](PRIVACY.md#webhook) has the details.
22
22
 
23
+ Pushes to a phone are off unless they are set up in the environment Agent Lookout starts with: through ntfy with `AGENT_LOOKOUT_NTFY_URL`, the address of one topic, and an optional access token in `AGENT_LOOKOUT_NTFY_TOKEN`, or through Pushover with an application's token in `AGENT_LOOKOUT_PUSHOVER_TOKEN` and a user key in `AGENT_LOOKOUT_PUSHOVER_USER`. Then, for each event chosen, the collector sends one short JSON push: to the ntfy server named, at the address before the topic, with the topic in the body and the token, when there is one, only in an `Authorization: Bearer` header, or to `https://api.pushover.net/1/messages.json`, which no setting changes, with the token and the key only in the body. It goes over HTTPS with the certificate checked even when `NODE_TLS_REJECT_UNAUTHORIZED=0` is set, or over plain HTTP only to an ntfy server at `127.0.0.1` or `localhost`. The ntfy address may hold no user name, password, query or fragment, so no secret is ever in an address. A push holds the session's name, what happened, how long it waited or when it was seen, the name of its project folder, its app and its agent, and, for a wait and its reminders with `AGENT_LOOKOUT_NTFY_ASKING=on` or `AGENT_LOOKOUT_PUSHOVER_ASKING=on`, what the session is asking. It follows no redirect and tries nothing twice. The topic, the token and the key are secrets: whoever has an ntfy topic's name can read what is pushed there, and whoever has Pushover's token and key can push to the person's devices. So they live in the environment of the Agent Lookout process and nowhere else, and no sentence the collector makes, no log line and no answer holds one. `GET /api/ntfy` tells the dashboard only whether ntfy is on, the server's host, whether a token is set, the events, the delay, whether a wait's push says what the session is asking, and how the last push went, and `GET /api/pushover` the same without a host. [PRIVACY.md](PRIVACY.md#ntfy) has the details.
24
+
23
25
  Other machines are read only when `AGENT_LOOKOUT_REMOTES` names them, each as a name and a target. A name is letters, digits and dashes, and a target is a host alias, a host name or `user@host` made of letters, digits, dots, dashes and underscores, starting with a letter, a digit or an underscore, so no space, leading dash, option such as `-oProxyCommand=`, or shell character gets in. One entry that is not, and no machine is read. For each, the collector runs the person's own `ssh`, found on `PATH`, in a fixed place or by `AGENT_LOOKOUT_SSH_BIN`, by its full path, never through a shell, with nothing on its input, as `ssh -N -o BatchMode=yes -o ExitOnForwardFailure=yes -o ServerAliveInterval=15 -o ControlMaster=no -o ControlPath=none -L 127.0.0.1:<free port>:127.0.0.1:<port there> -- <target>`. The target comes after `--`, as one argument. ssh runs no command on the other machine and asks for nothing, so Agent Lookout never handles a key or a password. `ControlMaster=no` and `ControlPath=none`, given on the command line, come before the person's own ssh config, so each ssh is a connection of its own: it never joins an ssh the person has open, whose forward would outlive Agent Lookout, and never leaves one in the background through `ControlPersist`. When Agent Lookout stops, it sends each ssh SIGTERM, and SIGKILL to one that has not ended two seconds later. Through the tunnel the collector sends `GET /api/health` and `GET /api/sessions` to the Agent Lookout there and nothing else. The requests arrive on that machine's own loopback, so its `Host` and `Origin` checks pass as they do for its own page, and they carry no `Origin` and no notifications header. The page on this computer never talks to the other machine, only to its own server. The answer is read as untrusted: every field is checked and cleaned again, sessions there are given ids of their own, `remote:<name>:` and the id there, and their process ID, Jump, Stop, held permission request and links are dropped, so nothing in an answer can make this computer select a pane, bring a tab forward, open a link, stop a process or answer a permission prompt. [PRIVACY.md](PRIVACY.md#another-machine-over-ssh) says what is kept.
24
26
 
25
- While a tunnel is up, its end on this computer's `127.0.0.1` is open to every program and every user account on this computer, not only to Agent Lookout, and it leads to the other machine's Agent Lookout and its whole API, not only the two routes the collector asks for. Through it, a program here can read that machine's sessions and send the routes that change something there: `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` and `POST /api/settings/permission-rules`. Their `Origin` and action-header checks stop a website, but not a program that is not a browser, which can set those headers. Before a machine is named, only someone on that machine could do this. There is also a moment, before ssh has signed in and listens on the port it was given, when that port is free: another program here could listen on it first and answer as that machine's Agent Lookout, and its sessions would be shown as that machine's, with no Jump, no Stop and no Allow or Deny. So name a machine only from a computer whose other users and programs you trust as you trust those on that machine.
27
+ While a tunnel is up, its end on this computer's `127.0.0.1` is open to every program and every user account on this computer, not only to Agent Lookout, and it leads to the other machine's Agent Lookout and its whole API, not only the two routes the collector asks for. Through it, a program here can read that machine's sessions and send the routes that change something there: `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` and `POST /api/phone/test`. Their `Origin` and action-header checks stop a website, but not a program that is not a browser, which can set those headers. Before a machine is named, only someone on that machine could do this. There is also a moment, before ssh has signed in and listens on the port it was given, when that port is free: another program here could listen on it first and answer as that machine's Agent Lookout, and its sessions would be shown as that machine's, with no Jump, no Stop and no Allow or Deny. So name a machine only from a computer whose other users and programs you trust as you trust those on that machine.
26
28
 
27
- Seven routes of the local API change something: `POST /api/jump`, which the Jump button of a session in tmux, Terminal or iTerm2 sends, `POST /api/history/clear`, which the Clear history button in Settings sends, `POST /api/sessions/stop` and `POST /api/sessions/clean-up`, which Stop session and the ending of the sessions left running send, `POST /api/permission/answer`, which Allow and Deny send, `POST /api/settings/time-rules`, which the Time rules card in Settings sends, and `POST /api/settings/permission-rules`, which the Permission rules card in Settings sends. Every other route only reads. The two that stop sessions, and the one that answers, are described after the jump route, below. `POST /api/history/clear` deletes the history files in Agent Lookout's own folder and empties the Events log and the history in memory. Its body is exactly `{}`, nothing in it names a file, and it makes the same checks as the jump route, below, with `X-Agent-Lookout-Action: clear-history`. `POST /api/settings/time-rules` puts the time rules in force and writes them to the settings file. It makes the same checks as the jump route, with `X-Agent-Lookout-Action: time-rules`, and takes a body of 2,048 bytes or less that is exactly the three rules, each with exactly its own fields and every value in range, and nothing else. Nothing in it names a file: the file is the one Agent Lookout was started with, written whole to a new file of mode 600 that then takes its place, never through a link. The rules decide only when Agent Lookout tells you of something, and which idle sessions count as stale, and so which are offered under Left running: ending one still takes a press of End and every check below. `POST /api/settings/permission-rules`, described with the route that answers, below, changes the rules that answer a permission prompt without a press. The rest of this part is about `POST /api/jump`. For a session in tmux it can change three things, all inside tmux: which pane is selected in its window, which window is selected in its tmux session, and which tmux session each attached terminal is showing. For a session in a tab of Terminal or iTerm2, on macOS, it can bring that tab forward: select it in its window, bring the window back from the Dock if it was minimised and in front of the app's others, and bring the app to the front. It sends no keys, runs nothing inside a pane or a tab, starts no tmux server and starts no app.
29
+ Eight routes of the local API change something: `POST /api/jump`, which the Jump button of a session in tmux, Terminal or iTerm2 sends, `POST /api/history/clear`, which the Clear history button in Settings sends, `POST /api/sessions/stop` and `POST /api/sessions/clean-up`, which Stop session and the ending of the sessions left running send, `POST /api/permission/answer`, which Allow and Deny send, `POST /api/settings/time-rules`, which the Time rules card in Settings sends, `POST /api/settings/permission-rules`, which the Permission rules card in Settings sends, and `POST /api/phone/test`, which Send a test on the ntfy and Pushover cards sends. Every other route only reads. `POST /api/phone/test` sends one test push through the channel it names, which says only that it is a test and holds nothing of any session. It makes the same checks as the jump route, below, with `X-Agent-Lookout-Action: phone-test`, and takes a body of 64 bytes or less that is exactly `{"channel": "ntfy"}` or `{"channel": "pushover"}`. Nothing in it reaches the push or says where it goes: that is the channel's own setting, and a channel that is not set up is answered 409 and sent nothing. It sends one a second, one at a time, and counts toward the channel's 20 an hour, so a program on this computer that sends it again and again can spend that hour's pushes, and no more. The two that stop sessions, and the one that answers, are described after the jump route, below. `POST /api/history/clear` deletes the history files in Agent Lookout's own folder and empties the Events log and the history in memory. Its body is exactly `{}`, nothing in it names a file, and it makes the same checks as the jump route, below, with `X-Agent-Lookout-Action: clear-history`. `POST /api/settings/time-rules` puts the time rules in force and writes them to the settings file. It makes the same checks as the jump route, with `X-Agent-Lookout-Action: time-rules`, and takes a body of 2,048 bytes or less that is exactly the three rules, each with exactly its own fields and every value in range, and nothing else. Nothing in it names a file: the file is the one Agent Lookout was started with, written whole to a new file of mode 600 that then takes its place, never through a link. The rules decide only when Agent Lookout tells you of something, and which idle sessions count as stale, and so which are offered under Left running: ending one still takes a press of End and every check below. `POST /api/settings/permission-rules`, described with the route that answers, below, changes the rules that answer a permission prompt without a press. The rest of this part is about `POST /api/jump`. For a session in tmux it can change three things, all inside tmux: which pane is selected in its window, which window is selected in its tmux session, and which tmux session each attached terminal is showing. For a session in a tab of Terminal or iTerm2, on macOS, it can bring that tab forward: select it in its window, bring the window back from the Dock if it was minimised and in front of the app's others, and bring the app to the front. It sends no keys, runs nothing inside a pane or a tab, starts no tmux server and starts no app.
28
30
 
29
31
  A request to it names a session by its ID and nothing else. The server looks that session up in its own list and takes the pane it found for that session's process when it last asked tmux. It checks that the pane's ID is a `%` followed by digits, and gives it to a fixed list of tmux commands: `select-window`, `select-pane`, `display-message -p`, `list-clients` and `switch-client`. Nothing from the request is passed to tmux, and tmux is started directly, never through a shell. So the route can never be made to run another program, another tmux command or a command with other arguments. The most a request can do is choose which of the panes already found for a listed session is selected.
30
32
 
@@ -53,11 +55,13 @@ Then it sends SIGTERM to that one process, never SIGKILL, and waits up to 10 sec
53
55
 
54
56
  The route passes the checks above, with `X-Agent-Lookout-Action: answer`, and a body of 1,024 bytes or less that is exactly `{"sessionId": "...", "requestId": "...", "decision": "allow" | "deny"}`. The request ID is 128 random bits Agent Lookout made for the request it holds, so a page showing an older request cannot answer a newer one. It answers only a request it holds for that session under that ID, Allow only when the whole of what Allow allows was shown, which is never the case for an edit, a new file, a plan or a question, and only after it has read the session's registry file again and found it still says the session waits for permission, in the same wait, by its `statusUpdatedAt` and `waitingFor`, that the request was shown in. What it writes is one of two fixed texts: allow, or deny with a fixed sentence. It never sends `updatedInput` or `updatedPermissions`, so it cannot change what a tool does or save a rule, and Claude Code still applies its own deny rules after an allow. A request is let go, answered with nothing, once the session is no longer waiting for permission or is waiting in a later wait, the hook's connection closes, or `AGENT_LOOKOUT_ANSWER_WAIT` has passed, and the prompt in the session decides as it would without the plugin. `AGENT_LOOKOUT_ANSWER=off` opens no socket and removes the route.
55
57
 
58
+ The Mac app answers a held request from its own notification of a wait, with Deny and Allow buttons, and from its menu bar, with Deny and Allow items in a session's submenu. It has no route for that: each press names the session, the request ID the notification or the menu was made with, and the answer, and goes in the app's own process to the same function the route calls (`createAnswerer` in `src/collector/answers/answerRoute.ts`), so it makes every check above and is an answer by Agent Lookout in the Events log. Allow is there only when the collector offers it and the whole of the request is shown there as written: in a notification, one shell command with no other input, on one line of at most 40 characters with no tab, no space but the plain one, no two in a row and none at either end, and the notification's title, subtitle and text at most 256 bytes in all. Apple documents no limit to how much of a banner macOS draws, and Deny and Allow can be pressed without opening it, so the 40 is kept to about one line of a banner until a packaged build has been looked at; a command cut short would end in `…`. In the menu, Allow needs the heading and at most 20 lines, each of the command or of one other input, held to the same rule at 80 characters, the other inputs after a separator, which no line of a command can be, no input's name with a `:` in it, and each line drawn as written once each `&` is doubled, which rules out `...` and `(&)`. Before it answers, the app refuses a press made in the first second the notification or the menu was shown, or while macOS says the screen is locked, should a notification's buttons be pressed on the lock screen. A refused press sends nothing, and the app says why in a notification or in the menu. A notification with buttons is taken down once one of them is pressed, or once its request has gone. The menu is not made again while it may be on the screen: Electron tells the top menu of a submenu's closing as if it were its own, so the app counts openings against closings, and takes the menu as closed once an item's click is heard, or a minute after a closing when the counts do not meet. Each item names the session and the request ID it was made for, so a menu made earlier answers no other request.
59
+
56
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.
57
61
 
58
62
  The Mac app answers six 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` and `GET /api/app/menu-bar`, and four POSTs that check GitHub for a newer version, install the one that is ready, turn the daily check on or off, and 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}`). 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.
59
63
 
60
- `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` or `POST /api/settings/permission-rules`, 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.
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.
61
65
 
62
66
  The local API has no token, and the MCP server adds none, though one was suggested for it. The server opens no port of its own, and the API it reads is the same loopback API that the dashboard and `agent-lookout status` read without one. Websites and other computers are already kept away from that API by the `Host` and `Origin` checks and by its listening on loopback alone. Against websites and other computers a token would add nothing to those checks, and programs running as you could read a token as easily as the API. It could keep out other accounts on a shared computer, the known limit listed below, which includes another account asking the server to stop your Claude Code sessions, and is left for later. An API that can be reached some other way, such as from another computer, would need one too, and Agent Lookout has none. [docs/API.md](docs/API.md) describes every route and its checks.
63
67
 
@@ -67,7 +71,7 @@ Session names and folder paths can be sensitive. The main risk is that something
67
71
 
68
72
  - A website, or another machine on the network, reading the local API. That includes getting past the `Host` and `Origin` checks and DNS rebinding.
69
73
  - `npm start` listening on any address other than loopback.
70
- - Any network request from Agent Lookout itself to anything but its own local server, apart from the mail server named in `AGENT_LOOKOUT_SMTP_URL` once email is set up, the address in `AGENT_LOOKOUT_WEBHOOK_URL` once a webhook is set up, and the end on `127.0.0.1` of an SSH tunnel to a machine `AGENT_LOOKOUT_REMOTES` names.
74
+ - Any network request from Agent Lookout itself to anything but its own local server, apart from the mail server named in `AGENT_LOOKOUT_SMTP_URL` once email is set up, the address in `AGENT_LOOKOUT_WEBHOOK_URL` once a webhook is set up, the server named in `AGENT_LOOKOUT_NTFY_URL` once ntfy is set up, `api.pushover.net` once Pushover is set up, and the end on `127.0.0.1` of an SSH tunnel to a machine `AGENT_LOOKOUT_REMOTES` names.
71
75
  - `ssh` run while `AGENT_LOOKOUT_REMOTES` is unset or cannot be read, through a shell, with any arguments but the fixed ones and a target that passed the check, or with a target that ssh can read as an option. Also a forward that listens on anything but `127.0.0.1` here, or reaches anything but `127.0.0.1` there.
72
76
  - A request through the tunnel other than `GET /api/health` and `GET /api/sessions`, or one that carries anything of this computer's, such as its sessions or its notifications header.
73
77
  - Anything in another machine's answer that runs as script or markup in the dashboard, that becomes a Jump, a Stop, an Allow or Deny, a link or a command on this computer, or that passes for one of this computer's own sessions.
@@ -83,10 +87,17 @@ Session names and folder paths can be sensitive. The main risk is that something
83
87
  - A session's name, folder or anything else from a session that changes the structure of a post, adds a field or a header to it, or, in Slack or a service that reads Slack's format, mentions someone, pings a channel or makes a link that shows other words than its address.
84
88
  - A post sent over plain HTTP to an address that is not on this machine, or to a server whose certificate was not checked.
85
89
  - What a waiting session is asking reaching an email while `AGENT_LOOKOUT_EMAIL_ASKING` is not `on`, a post while `AGENT_LOOKOUT_WEBHOOK_ASKING` is not `on`, an email's headers, or the email or post for anything but a wait.
86
- - What a waiting session is asking being written to a file, an event or the history, or reaching `GET /api/email` or `GET /api/webhook`.
90
+ - What a waiting session is asking being written to a file, an event or the history, or reaching `GET /api/email`, `GET /api/webhook`, `GET /api/ntfy` or `GET /api/pushover`.
91
+ - A push sent, or a connection opened for one, while `AGENT_LOOKOUT_NTFY_URL` is unset, while either `AGENT_LOOKOUT_PUSHOVER_TOKEN` or `AGENT_LOOKOUT_PUSHOVER_USER` is unset, or while one of that channel's settings cannot be read.
92
+ - The ntfy topic, its token, Pushover's token or the user key reaching the dashboard, the API, an event, the history, the snapshot, a log, the console, a sentence Agent Lookout makes or a file, or going in an address rather than a header or a body.
93
+ - A push that holds more than PRIVACY.md lists, that goes to any address but the ntfy server named or `https://api.pushover.net/1/messages.json`, including by following a redirect, or that is sent over plain HTTP to an ntfy server that is not on this machine, or to a server whose certificate was not checked.
94
+ - What a waiting session is asking reaching a push while that channel's ASKING setting is not `on`, a push's title, or the push for anything but a wait and its reminders.
95
+ - A session's name, folder or anything else from a session that changes the structure of a push, adds a field or a header to it, or changes where it goes.
96
+ - A test push that holds anything of a session, that goes while the hourly limit is full, or that a website, or a page served from anywhere but this machine, can make the app send.
87
97
  - Any write to `~/.claude` or to another tool's files, and any read of the `.key` files in `~/.claude/sessions/`.
88
98
  - 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.
89
99
  - Text from a Codex session file, such as a prompt, a reply or a command's output, reaching the API or the dashboard.
100
+ - 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.
90
101
  - A session name, path or other session field that runs as script or markup in the dashboard.
91
102
  - 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.
92
103
  - A file outside the folder of status files being read through it: by a symbolic link inside it, by a folder inside it, by a file's name or by a path written in a file, such as its `cwd`.
@@ -99,7 +110,7 @@ Session names and folder paths can be sensitive. The main risk is that something
99
110
  - In the Mac app, a request, a release or anything from the network that makes it run anything but `/usr/bin/ditto`, `/usr/bin/plutil` and its fixed update helper, passes them arguments it did not make itself, installs a download whose size or SHA-512 is not the one the release gives or whose bundle is not Agent Lookout at the version offered, installs anything before Install and Restart is pressed, or makes a request to any host but GitHub's three for a release.
100
111
  - A website, or a page served from anywhere but this machine, making the app select a tmux pane or bring a tab of Terminal or iTerm2 forward.
101
112
  - A request to `POST /api/jump` that makes tmux do anything but select a pane found for a listed session, its window and its tmux session: running a command, sending keys, or reaching any other pane.
102
- - A request to `POST /api/permission/answer`, or anything sent to the answer socket, that answers a permission prompt Agent Lookout does not hold for that session under that request ID, that allows one whose whole request was not shown or that offers Deny alone, that answers one after the session's registry file stopped saying it waits, that writes anything but the two fixed answers, or that rewrites a tool's input or saves a permission rule. Also a website, or a page served from anywhere but this machine, making the app answer a prompt, a request's text reaching a file, an event, the history, an email, a webhook post or `agent-lookout mcp`, and the socket or its folder being open to another user.
113
+ - A request to `POST /api/permission/answer`, or anything sent to the answer socket, that answers a permission prompt Agent Lookout does not hold for that session under that request ID, that allows one whose whole request was not shown or that offers Deny alone, that answers one after the session's registry file stopped saying it waits, that writes anything but the two fixed answers, or that rewrites a tool's input or saves a permission rule. Also a website, or a page served from anywhere but this machine, making the app answer a prompt, a request's text reaching a file, an event, the history, an email, a webhook post or `agent-lookout mcp`, and the socket or its folder being open to another user. The same for a press of a button on the Mac app's notification or of an item in its menu bar, and also one that answers a request the notification or the menu did not show, one that allows a request that was not shown there whole as written, and one answered in the first second it was shown or while the screen was locked.
103
114
  - A request to `POST /api/sessions/stop` or `POST /api/sessions/clean-up` that signals any process but a listed Claude Code session's own, one whose registry file and start time were not confirmed at that moment, or one in the desktop app, that sends any signal but SIGTERM, that runs `claude` with any arguments but `stop` and a job's ID found for a listed session, or that ends a session that was not idle since the moment the page sent. Also a website, or a page served from anywhere but this machine, making the app stop a session.
104
115
  - A session's name, a tmux session's name, or anything else from a session or from tmux, that `tmux` reads as a command, a target or an option.
105
116
  - 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.
@@ -109,7 +120,7 @@ Session names and folder paths can be sensitive. The main risk is that something
109
120
  - 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.
110
121
  - 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.
111
122
  - 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.
112
- - Quiet hours that let a notification, an email or a post through while they hold, or a summary that holds more than PRIVACY.md lists.
123
+ - 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.
113
124
  - A page at another address reading or steering the dashboard's notifications, or turning the server's own on or off.
114
125
  - 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.
115
126
  - `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.
@@ -122,7 +133,7 @@ Session names and folder paths can be sensitive. The main risk is that something
122
133
  - A program on this machine that names itself after Terminal or iTerm2 in the list of processes, and so gives a session under it a Jump button. Pressing it asks the real app for a tab on that session's terminal, which it does not have.
123
134
  - A notification the server showed staying in Notification Centre after its session has moved on. The server cannot take one down, and PRIVACY.md says so.
124
135
  - The dev server started with Vite's `--host` flag, which makes it listen on the network. `npm start` refuses to listen on anything but loopback.
125
- - Attacks that need control of your user account first. Someone with that control can read `~/.claude` and `~/.codex` directly.
136
+ - Attacks that need control of your user account first. Someone with that control can read `~/.claude`, `~/.codex` and `~/.gemini` directly.
126
137
  - What the other machine's Agent Lookout reports about its own sessions. Someone who controls that machine, or its Agent Lookout, can make it list sessions that do not exist, and they are shown here as that machine's, marked with its name. They cannot act on this computer.
127
138
  - What `ssh` does with the person's own ssh config, such as a `ProxyCommand` or a `LocalCommand` set there for the host named. Agent Lookout gives ssh its fixed options and the target, and the rest is the person's config, as in a terminal.
128
139
  - Another program or user on this computer reaching the other machine's Agent Lookout through a tunnel's port while it is up, or listening on that port before ssh does, as described above. Keeping the port to this user alone would need a forward to a socket file, which this version does not make.
@@ -137,6 +148,10 @@ Session names and folder paths can be sensitive. The main risk is that something
137
148
  - Other programs running as your user reading the webhook's address from the environment of the Agent Lookout process, or from your shell's history.
138
149
  - What the service at the webhook's address, such as Slack or Discord, does with a post once it has been sent, including who can read the channel.
139
150
  - Slack showing a web address that is written out in a session's name as a link to that address. It shows the address as it is written.
151
+ - Other programs running as your user reading the ntfy topic, its token, Pushover's token or the user key from the environment of the Agent Lookout process, or from your shell's history.
152
+ - What ntfy, a server of your own, Pushover, Google's and Apple's push services and the apps on your devices do with a push once it has been sent, including how long they keep it and who can read it on a lock screen.
153
+ - Someone who guesses an ntfy topic's name and reads the pushes sent to it, on ntfy.sh or another server that lets anyone read a topic. The topic's name is the secret there, and the guide says how to make one no one can guess and how to add an access token.
154
+ - A program on this computer that sends `POST /api/phone/test` again and again, as the other routes that act can be sent, and spends the hour's 20 pushes for that channel.
140
155
  - Traffic from Claude Code's own `claude agents` command while Agent Lookout runs it. It may contact Anthropic the way Claude Code normally does.
141
156
  - Traffic from the person's own `gh` to GitHub while `AGENT_LOOKOUT_PULL_REQUESTS=on`, under gh's own login and settings.
142
157
  - Another program later served at the same address using the notification permission your browser gave that address. PRIVACY.md describes this.
@@ -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-DLPmOXDe.js";import"./motion-BAhdRuCw.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};
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-BWFkRW4g.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};
@@ -1 +1 @@
1
- import{j as e,r as m}from"./react-C2DiJaR6.js";import{F as j,T as $,p as L,o as W,w as C,u as P,q as _,J as B,c as I,v as J,A as H,R as K,D as Y,x as z,y as F,z as G,B as U,n as V,E as X,M as R,G as Q,I as Z,K as ee,N as te,O as se,P as ne,Q as O,U as ae,V as re,f as S,W as le,X as ie,Y as oe,Z as ce,_ as ue,$ as de,a0 as pe,a1 as me,a2 as he,a3 as xe,a4 as T,a5 as M,a6 as fe,L as je,a7 as be}from"./index-DLPmOXDe.js";import"./motion-BAhdRuCw.js";import"./radix-BH0JH8UU.js";function ge({pullRequest:t}){return e.jsxs(j,{label:"Pull request",children:[e.jsxs($,{"data-part":"pull-request",href:t.url,target:"_blank",rel:"noreferrer noopener",lines:2,tooltip:`#${t.number} ${t.title}`,className:"wrap-anywhere underline decoration-rule-strong underline-offset-2 transition-colors duration-120 hover:decoration-ink",children:[e.jsxs("span",{className:"tabular-nums",children:["#",t.number]})," ",t.title]}),e.jsx("span",{"data-part":"pull-request-state",className:"block text-caption font-normal text-ink-secondary tabular-nums",children:L(t)})]})}function ke(t,a={}){const{open:s=!0,onStopped:n,request:l=W}=a,[c,r]=m.useState({kind:"idle"}),[u,i]=m.useState(s);s!==u&&(i(s),!s&&!(c.kind==="confirming"&&c.busy)&&r({kind:"idle"}));const h=m.useRef(s);m.useEffect(()=>{h.current=s},[s]);const o=m.useId(),[d]=m.useState(()=>({stop:`${o}-stop`,cancel:`${o}-cancel`,outcome:`${o}-outcome`})),p=m.useRef(null),x=m.useRef(!1),b=m.useRef(!0);m.useEffect(()=>(b.current=!0,()=>{b.current=!1}),[]),m.useEffect(()=>{const g=p.current;p.current=null,g!==null&&document.getElementById(d[g])?.focus()},[c,d]);const f=m.useCallback(()=>{x.current||(p.current="cancel",r({kind:"confirming",busy:!1}))},[]),k=m.useCallback(()=>{x.current||(p.current="stop",r({kind:"idle"}))},[]),v=m.useCallback(()=>{x.current||(x.current=!0,r({kind:"confirming",busy:!0}),l(t).then(g=>{if(x.current=!1,!!b.current){if(g==="stopped"&&n?.(),!h.current){r({kind:"idle"});return}p.current="outcome",r({kind:"answered",outcome:g,at:Date.now()})}}))},[t,l,n]);return{step:c,ask:f,cancel:k,confirm:v,ids:d}}const ye="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.",q=[];function we(t,a){const s=CSS.escape(t),n=[...document.querySelectorAll(`:is([data-slot="session-row"], [data-slot="board-card"], [data-slot="hero-session"])[data-session="${s}"] a[data-part="name"]`)];return a!==null&&n.includes(a)?a:n.find(l=>!l.closest('[data-slot="hero-session"]'))??n[0]??null}function Ne({session:t,gone:a,asOf:s,now:n}){const l=t.status==="finished"||t.status==="failed",c=ie(t),r=oe(t),u=c?"stale":r||a&&t.status==="needs-you"?"answered":t.status,i=t.statusSince,h=i!==null&&!r?Math.max(0,s-i):null,o=ce(t,s),d=F(t),p=d?ue(t):null,x=d&&!a&&!t.ask&&t.waitingText?.trim()||null,b=i!==null?`${l?"at":c?"idle since":r?"asked for permission at":"since"} ${O(i,n)}`:"time not reported",f=[d?de(t):null,b,o&&pe(o)].filter(k=>typeof k=="string");return e.jsxs(j,{label:"Status",note:x?e.jsxs(e.Fragment,{children:[e.jsx($,{"data-part":"asking",lines:2,className:"wrap-anywhere",children:x}),p&&e.jsx("span",{className:"block",children:p})]}):p??void 0,children:[e.jsxs("span",{className:"inline-flex items-center gap-2",children:[e.jsx(me,{kind:u}),e.jsx("span",{"data-part":"status",children:c?"Stale":r?he:xe[t.status]}),h!==null&&e.jsxs("span",{"data-part":"duration",className:"tabular-nums",children:[e.jsx("span",{className:"sr-only",children:l?` ${T(h)} ago`:` for ${T(h)}`}),e.jsxs("span",{"aria-hidden":!0,children:[M(h),l&&" ago"]})]})]}),e.jsx("span",{"data-part":"since",className:"block text-caption font-normal text-ink-secondary tabular-nums",children:f.join(", ")})]})}function ve({session:t,state:a,now:s,asOf:n}){const l=m.useMemo(()=>fe({sessions:a.snapshot?.sessions??q,events:a.events,history:a.history,now:s,asOf:n,eventsFull:a.events.length>=R}),[a.snapshot,a.events,a.history,s,n]);if(!l)return e.jsxs(j,{label:"Waits",children:["Not known",e.jsx("span",{className:"block text-caption font-normal text-ink-secondary",children:"The history could not be read"})]});const c=l.sessions.find(i=>i.id===t.id),r=l.period.today?"today":`since ${S(l.period.from)}`;let u="None";if(c){const i=`${c.startKnown?"":"at least "}${M(c.ms)}`;u=c.times===1?`Once, ${i}`:`${c.times} times, ${i} in all`}return e.jsxs(j,{label:"Waits",children:[e.jsx("span",{"data-part":"waits",className:"tabular-nums",children:u}),e.jsx("span",{className:"block text-caption font-normal text-ink-secondary",children:r})]})}function E({title:t,aside:a,children:s,...n}){return e.jsxs("section",{...n,"aria-label":t,className:"mt-6",children:[e.jsxs("div",{className:"flex items-baseline justify-between gap-4 border-b border-hairline pb-2",children:[e.jsx("h3",{className:"text-body font-semibold text-ink",children:t}),a&&e.jsx("p",{className:"text-caption text-ink-muted",children:a})]}),s]})}function Se({session:t,gone:a,state:s,now:n,asOf:l}){const c=s.snapshot?.sources??[],r=s.snapshot?.sessions??q,u=V(s.snapshot?.timeRules),i=X(t.surface),h=s.events.length>=R,o=m.useMemo(()=>Q(s.events.filter(b=>b.sessionId===t.id),r),[s.events,r,t.id]),d=Z(s.events,s.history,h),p=m.useMemo(()=>s.history?ee({sessions:r,events:s.events,history:s.history,now:n,asOf:l,eventsFull:h,staleAfterMs:u}):null,[r,s.events,s.history,n,l,h,u]),x=p?.rows.find(b=>b.id===t.id);return e.jsxs(e.Fragment,{children:[a&&e.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."}),e.jsxs(te,{className:"mt-3",children:[e.jsx(Ne,{session:t,gone:a,asOf:l,now:n}),e.jsx(j,{label:"Agent",children:se(t,c)}),t.machine!==void 0&&e.jsx(j,{label:"Machine",note:ye,children:e.jsx("span",{"data-part":"machine",children:t.machine})}),i!==null&&e.jsx(j,{label:"App",children:i}),e.jsx(j,{label:"Folder",mono:t.cwd!==null,children:t.cwd!==null?e.jsx(ne,{"data-part":"folder",className:"select-text",children:t.cwd}):"Not known"}),t.git?.repository!==void 0&&e.jsx(j,{label:"Repository",children:e.jsx("span",{"data-part":"repository",className:"wrap-anywhere",children:t.git.repository.name})}),t.git?.branch!==void 0?e.jsx(j,{label:"Branch",children:e.jsx("span",{"data-part":"branch",className:"wrap-anywhere",children:t.git.branch})}):t.git?.commit!==void 0&&e.jsx(j,{label:"Commit",mono:!0,children:t.git.commit}),t.git?.pullRequest!==void 0&&e.jsx(ge,{pullRequest:t.git.pullRequest}),e.jsx(j,{label:"Started",children:e.jsx("span",{className:"tabular-nums",children:t.startedAt!==null?O(t.startedAt,n):"Not reported"})}),t.pid!==void 0&&e.jsx(j,{label:"Process",mono:!0,note:t.alive===!1?"The process has ended.":!a&&t.source==="claude-code"&&t.surface==="desktop"?ae:void 0,children:t.pid}),e.jsx(ve,{session:t,state:s,now:n,asOf:l})]}),e.jsx(E,{"data-part":"events",title:"Events",aside:d?`since ${S(d.at)}`:void 0,children:o.length>0?e.jsx(re,{entries:o,now:n,className:"pt-0.5"}):e.jsxs("p",{className:"py-2.5 text-body text-ink-secondary",children:["Nothing has happened to this session"," ",d?`since ${S(d.at)}`:"yet","."]})}),e.jsx(E,{"data-part":"timeline",title:"Timeline",aside:"last hour",children:p&&x?e.jsx(le,{timeline:p,rows:[x],named:!1,label:"Its last hour",className:"pt-3"}):e.jsx("p",{className:"py-2.5 text-body text-ink-secondary",children:p?"It did not run in the last hour.":"The last hour could not be read. The page asks again every two seconds."})})]})}function Ae({sessionId:t,open:a,onClose:s,state:n,now:l,onStopped:c,onAnswered:r}){const u=n.snapshot?.sessions.find(y=>y.id===t)??null,[i,h]=m.useState(null);u!==null&&u!==i?.session?h({session:u,at:n.lastOkAt??l}):u===null&&(i?.session.waitingText!==void 0||i?.session.ask!==void 0)&&h({...i,session:C(i.session)});const o=u??i?.session??null,d=u===null&&i!==null,p=P(o??{id:t,source:"status-files",surface:"unknown",links:{}}),x=d&&i?i.at:n.phase==="stalled"&&n.lastOkAt!==null?n.lastOkAt:l,b=p.outcome!==null||p.asking!==null,f=ke(t,{open:a,onStopped:c}),k=f.step.kind==="answered"&&u?.alive===!0&&(n.snapshot?.generatedAt??0)>f.step.at,v=f.step.kind==="answered"&&f.step.outcome==="stopped"&&!k,g=_(o??{id:t,source:"status-files",status:"unknown",cwd:null},v?!0:void 0),D=f.step.kind==="answered"||f.step.kind==="confirming"&&(f.step.busy||!d);let w,N,A="wide";return o?(w=o.name,N=e.jsxs(e.Fragment,{children:[!d&&e.jsx("div",{className:I("flex flex-wrap items-center gap-x-2 gap-y-1",b&&"mt-3"),children:e.jsx(B,{session:o,jump:p})}),D&&e.jsx(J,{session:o,stop:f,className:"mt-3"}),!d&&e.jsx(H,{session:o,onAnswered:r,className:"mt-3"}),e.jsx(K,{resume:g,className:"mt-3"}),e.jsx(Se,{session:o,gone:d,state:n,now:l,asOf:x})]})):n.snapshot===null?(w="Session",N=e.jsx(je,{label:"Reading sessions",className:"justify-start px-0 pt-5 pb-1"})):(w="No such session",A="sm",N=e.jsxs("div",{"data-part":"missing",children:[e.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."}),e.jsx(be,{className:"mt-4",onClick:s,children:"Go to the Overview"})]})),e.jsx(Y,{open:a,onOpenChange:y=>{y||s()},title:w,size:A,focusTitle:!0,returnFocus:y=>we(t,y)??document.querySelector("main"),actions:o&&e.jsxs(e.Fragment,{children:[!d&&e.jsxs(e.Fragment,{children:[e.jsx(z,{session:o,jump:p,variant:F(o)?"needs-you":"quiet"}),e.jsx(G,{session:o,stop:f})]}),e.jsx(U,{session:o,resume:g,tooltip:!1})]}),children:N})}function Re({sessionId:t,onClose:a,state:s,now:n,onStopped:l,onAnswered:c}){const[r,u]=m.useState(t);return t!==null&&t!==r&&u(t),r===null?null:e.jsx(Ae,{sessionId:r,open:t!==null,onClose:a,state:s,now:n,onStopped:l,onAnswered:c},r)}export{Re as SessionPanel};
1
+ import{j as e,r as m}from"./react-C2DiJaR6.js";import{F as j,T as $,p as L,o as W,w as C,u as P,q as _,J as B,c as I,v as J,A as H,R as K,D as Y,x as z,y as F,z as G,B as U,n as V,E as X,M as R,G as Q,I as Z,K as ee,N as te,O as se,P as ne,Q as O,U as ae,V as re,f as S,W as le,X as ie,Y as oe,Z as ce,_ as ue,$ as de,a0 as pe,a1 as me,a2 as he,a3 as xe,a4 as T,a5 as M,a6 as fe,L as je,a7 as be}from"./index-BWFkRW4g.js";import"./motion-DlYOyClL.js";import"./radix-BH0JH8UU.js";function ge({pullRequest:t}){return e.jsxs(j,{label:"Pull request",children:[e.jsxs($,{"data-part":"pull-request",href:t.url,target:"_blank",rel:"noreferrer noopener",lines:2,tooltip:`#${t.number} ${t.title}`,className:"wrap-anywhere underline decoration-rule-strong underline-offset-2 transition-colors duration-120 hover:decoration-ink",children:[e.jsxs("span",{className:"tabular-nums",children:["#",t.number]})," ",t.title]}),e.jsx("span",{"data-part":"pull-request-state",className:"block text-caption font-normal text-ink-secondary tabular-nums",children:L(t)})]})}function ke(t,a={}){const{open:s=!0,onStopped:n,request:l=W}=a,[c,r]=m.useState({kind:"idle"}),[u,i]=m.useState(s);s!==u&&(i(s),!s&&!(c.kind==="confirming"&&c.busy)&&r({kind:"idle"}));const h=m.useRef(s);m.useEffect(()=>{h.current=s},[s]);const o=m.useId(),[d]=m.useState(()=>({stop:`${o}-stop`,cancel:`${o}-cancel`,outcome:`${o}-outcome`})),p=m.useRef(null),x=m.useRef(!1),b=m.useRef(!0);m.useEffect(()=>(b.current=!0,()=>{b.current=!1}),[]),m.useEffect(()=>{const g=p.current;p.current=null,g!==null&&document.getElementById(d[g])?.focus()},[c,d]);const f=m.useCallback(()=>{x.current||(p.current="cancel",r({kind:"confirming",busy:!1}))},[]),k=m.useCallback(()=>{x.current||(p.current="stop",r({kind:"idle"}))},[]),v=m.useCallback(()=>{x.current||(x.current=!0,r({kind:"confirming",busy:!0}),l(t).then(g=>{if(x.current=!1,!!b.current){if(g==="stopped"&&n?.(),!h.current){r({kind:"idle"});return}p.current="outcome",r({kind:"answered",outcome:g,at:Date.now()})}}))},[t,l,n]);return{step:c,ask:f,cancel:k,confirm:v,ids:d}}const ye="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.",q=[];function we(t,a){const s=CSS.escape(t),n=[...document.querySelectorAll(`:is([data-slot="session-row"], [data-slot="board-card"], [data-slot="hero-session"])[data-session="${s}"] a[data-part="name"]`)];return a!==null&&n.includes(a)?a:n.find(l=>!l.closest('[data-slot="hero-session"]'))??n[0]??null}function Ne({session:t,gone:a,asOf:s,now:n}){const l=t.status==="finished"||t.status==="failed",c=ie(t),r=oe(t),u=c?"stale":r||a&&t.status==="needs-you"?"answered":t.status,i=t.statusSince,h=i!==null&&!r?Math.max(0,s-i):null,o=ce(t,s),d=F(t),p=d?ue(t):null,x=d&&!a&&!t.ask&&t.waitingText?.trim()||null,b=i!==null?`${l?"at":c?"idle since":r?"asked for permission at":"since"} ${O(i,n)}`:"time not reported",f=[d?de(t):null,b,o&&pe(o)].filter(k=>typeof k=="string");return e.jsxs(j,{label:"Status",note:x?e.jsxs(e.Fragment,{children:[e.jsx($,{"data-part":"asking",lines:2,className:"wrap-anywhere",children:x}),p&&e.jsx("span",{className:"block",children:p})]}):p??void 0,children:[e.jsxs("span",{className:"inline-flex items-center gap-2",children:[e.jsx(me,{kind:u}),e.jsx("span",{"data-part":"status",children:c?"Stale":r?he:xe[t.status]}),h!==null&&e.jsxs("span",{"data-part":"duration",className:"tabular-nums",children:[e.jsx("span",{className:"sr-only",children:l?` ${T(h)} ago`:` for ${T(h)}`}),e.jsxs("span",{"aria-hidden":!0,children:[M(h),l&&" ago"]})]})]}),e.jsx("span",{"data-part":"since",className:"block text-caption font-normal text-ink-secondary tabular-nums",children:f.join(", ")})]})}function ve({session:t,state:a,now:s,asOf:n}){const l=m.useMemo(()=>fe({sessions:a.snapshot?.sessions??q,events:a.events,history:a.history,now:s,asOf:n,eventsFull:a.events.length>=R}),[a.snapshot,a.events,a.history,s,n]);if(!l)return e.jsxs(j,{label:"Waits",children:["Not known",e.jsx("span",{className:"block text-caption font-normal text-ink-secondary",children:"The history could not be read"})]});const c=l.sessions.find(i=>i.id===t.id),r=l.period.today?"today":`since ${S(l.period.from)}`;let u="None";if(c){const i=`${c.startKnown?"":"at least "}${M(c.ms)}`;u=c.times===1?`Once, ${i}`:`${c.times} times, ${i} in all`}return e.jsxs(j,{label:"Waits",children:[e.jsx("span",{"data-part":"waits",className:"tabular-nums",children:u}),e.jsx("span",{className:"block text-caption font-normal text-ink-secondary",children:r})]})}function E({title:t,aside:a,children:s,...n}){return e.jsxs("section",{...n,"aria-label":t,className:"mt-6",children:[e.jsxs("div",{className:"flex items-baseline justify-between gap-4 border-b border-hairline pb-2",children:[e.jsx("h3",{className:"text-body font-semibold text-ink",children:t}),a&&e.jsx("p",{className:"text-caption text-ink-muted",children:a})]}),s]})}function Se({session:t,gone:a,state:s,now:n,asOf:l}){const c=s.snapshot?.sources??[],r=s.snapshot?.sessions??q,u=V(s.snapshot?.timeRules),i=X(t.surface),h=s.events.length>=R,o=m.useMemo(()=>Q(s.events.filter(b=>b.sessionId===t.id),r),[s.events,r,t.id]),d=Z(s.events,s.history,h),p=m.useMemo(()=>s.history?ee({sessions:r,events:s.events,history:s.history,now:n,asOf:l,eventsFull:h,staleAfterMs:u}):null,[r,s.events,s.history,n,l,h,u]),x=p?.rows.find(b=>b.id===t.id);return e.jsxs(e.Fragment,{children:[a&&e.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."}),e.jsxs(te,{className:"mt-3",children:[e.jsx(Ne,{session:t,gone:a,asOf:l,now:n}),e.jsx(j,{label:"Agent",children:se(t,c)}),t.machine!==void 0&&e.jsx(j,{label:"Machine",note:ye,children:e.jsx("span",{"data-part":"machine",children:t.machine})}),i!==null&&e.jsx(j,{label:"App",children:i}),e.jsx(j,{label:"Folder",mono:t.cwd!==null,children:t.cwd!==null?e.jsx(ne,{"data-part":"folder",className:"select-text",children:t.cwd}):"Not known"}),t.git?.repository!==void 0&&e.jsx(j,{label:"Repository",children:e.jsx("span",{"data-part":"repository",className:"wrap-anywhere",children:t.git.repository.name})}),t.git?.branch!==void 0?e.jsx(j,{label:"Branch",children:e.jsx("span",{"data-part":"branch",className:"wrap-anywhere",children:t.git.branch})}):t.git?.commit!==void 0&&e.jsx(j,{label:"Commit",mono:!0,children:t.git.commit}),t.git?.pullRequest!==void 0&&e.jsx(ge,{pullRequest:t.git.pullRequest}),e.jsx(j,{label:"Started",children:e.jsx("span",{className:"tabular-nums",children:t.startedAt!==null?O(t.startedAt,n):"Not reported"})}),t.pid!==void 0&&e.jsx(j,{label:"Process",mono:!0,note:t.alive===!1?"The process has ended.":!a&&t.source==="claude-code"&&t.surface==="desktop"?ae:void 0,children:t.pid}),e.jsx(ve,{session:t,state:s,now:n,asOf:l})]}),e.jsx(E,{"data-part":"events",title:"Events",aside:d?`since ${S(d.at)}`:void 0,children:o.length>0?e.jsx(re,{entries:o,now:n,className:"pt-0.5"}):e.jsxs("p",{className:"py-2.5 text-body text-ink-secondary",children:["Nothing has happened to this session"," ",d?`since ${S(d.at)}`:"yet","."]})}),e.jsx(E,{"data-part":"timeline",title:"Timeline",aside:"last hour",children:p&&x?e.jsx(le,{timeline:p,rows:[x],named:!1,label:"Its last hour",className:"pt-3"}):e.jsx("p",{className:"py-2.5 text-body text-ink-secondary",children:p?"It did not run in the last hour.":"The last hour could not be read. The page asks again every two seconds."})})]})}function Ae({sessionId:t,open:a,onClose:s,state:n,now:l,onStopped:c,onAnswered:r}){const u=n.snapshot?.sessions.find(y=>y.id===t)??null,[i,h]=m.useState(null);u!==null&&u!==i?.session?h({session:u,at:n.lastOkAt??l}):u===null&&(i?.session.waitingText!==void 0||i?.session.ask!==void 0)&&h({...i,session:C(i.session)});const o=u??i?.session??null,d=u===null&&i!==null,p=P(o??{id:t,source:"status-files",surface:"unknown",links:{}}),x=d&&i?i.at:n.phase==="stalled"&&n.lastOkAt!==null?n.lastOkAt:l,b=p.outcome!==null||p.asking!==null,f=ke(t,{open:a,onStopped:c}),k=f.step.kind==="answered"&&u?.alive===!0&&(n.snapshot?.generatedAt??0)>f.step.at,v=f.step.kind==="answered"&&f.step.outcome==="stopped"&&!k,g=_(o??{id:t,source:"status-files",status:"unknown",cwd:null},v?!0:void 0),D=f.step.kind==="answered"||f.step.kind==="confirming"&&(f.step.busy||!d);let w,N,A="wide";return o?(w=o.name,N=e.jsxs(e.Fragment,{children:[!d&&e.jsx("div",{className:I("flex flex-wrap items-center gap-x-2 gap-y-1",b&&"mt-3"),children:e.jsx(B,{session:o,jump:p})}),D&&e.jsx(J,{session:o,stop:f,className:"mt-3"}),!d&&e.jsx(H,{session:o,onAnswered:r,className:"mt-3"}),e.jsx(K,{resume:g,className:"mt-3"}),e.jsx(Se,{session:o,gone:d,state:n,now:l,asOf:x})]})):n.snapshot===null?(w="Session",N=e.jsx(je,{label:"Reading sessions",className:"justify-start px-0 pt-5 pb-1"})):(w="No such session",A="sm",N=e.jsxs("div",{"data-part":"missing",children:[e.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."}),e.jsx(be,{className:"mt-4",onClick:s,children:"Go to the Overview"})]})),e.jsx(Y,{open:a,onOpenChange:y=>{y||s()},title:w,size:A,focusTitle:!0,returnFocus:y=>we(t,y)??document.querySelector("main"),actions:o&&e.jsxs(e.Fragment,{children:[!d&&e.jsxs(e.Fragment,{children:[e.jsx(z,{session:o,jump:p,variant:F(o)?"needs-you":"quiet"}),e.jsx(G,{session:o,stop:f})]}),e.jsx(U,{session:o,resume:g,tooltip:!1})]}),children:N})}function Re({sessionId:t,onClose:a,state:s,now:n,onStopped:l,onAnswered:c}){const[r,u]=m.useState(t);return t!==null&&t!==r&&u(t),r===null?null:e.jsx(Ae,{sessionId:r,open:t!==null,onClose:a,state:s,now:n,onStopped:l,onAnswered:c},r)}export{Re as SessionPanel};