@dinhtungdu/watcher 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +77 -0
- package/README.md +20 -0
- package/docs/adr/0001-agent-switcher-architecture.md +15 -0
- package/docs/adr/0002-agent-switcher-focus-layout.md +15 -0
- package/docs/adr/0003-terminal-backends.md +13 -0
- package/docs/adr/0004-agent-integrations-and-events.md +40 -0
- package/docs/assets/watcher-terminal-preview.png +0 -0
- package/docs/requirements.md +189 -0
- package/package.json +40 -0
- package/src/activation.ts +58 -0
- package/src/agentEventReducer.ts +336 -0
- package/src/agentEvents.ts +188 -0
- package/src/agents/claude.ts +16 -0
- package/src/agents/codex.ts +16 -0
- package/src/agents/opencode.ts +16 -0
- package/src/agents/pi.ts +16 -0
- package/src/agents/registry.ts +54 -0
- package/src/agents/types.ts +28 -0
- package/src/ansiShell.ts +104 -0
- package/src/cli.ts +73 -0
- package/src/daemon.ts +101 -0
- package/src/discovery.ts +85 -0
- package/src/eventCommand.ts +114 -0
- package/src/git.ts +28 -0
- package/src/integrationsInstaller.ts +218 -0
- package/src/ipc.ts +56 -0
- package/src/model.ts +94 -0
- package/src/runningAgentPanes.ts +96 -0
- package/src/snapshot.ts +8 -0
- package/src/stalled.ts +93 -0
- package/src/surfaceIdentity.ts +18 -0
- package/src/switcherLayout.ts +435 -0
- package/src/terminalTarget.ts +58 -0
- package/src/text.ts +114 -0
- package/src/tmux.ts +24 -0
- package/src/tmuxContext.ts +58 -0
package/CONTEXT.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Watcher
|
|
2
|
+
|
|
3
|
+
Watcher helps a user observe running agent sessions and jump to the relevant workspace.
|
|
4
|
+
|
|
5
|
+
## Language
|
|
6
|
+
|
|
7
|
+
**Agent Pane**:
|
|
8
|
+
An interactive workspace running an agent session that Watcher can surface and activate. Agent Panes are identified by a Terminal Backend and that backend's local surface identity.
|
|
9
|
+
_Avoid_: Agent activity, agent shell, bot pane
|
|
10
|
+
|
|
11
|
+
**Agent Status**:
|
|
12
|
+
The current condition of an Agent Pane: working, needs input, idle, unknown, or stalled. Agent Status is used for priority, visual attention, and detail context; it does not decide whether a running Agent Pane appears in the Agent Switcher.
|
|
13
|
+
_Avoid_: Activity, state, health
|
|
14
|
+
|
|
15
|
+
**Pending Assistant Message**:
|
|
16
|
+
A completed assistant message observed before the agent turn has finished. It may be promoted to the final assistant message when the Agent Pane becomes idle, but it is not itself the final assistant result.
|
|
17
|
+
_Avoid_: Last message, final answer, assistant summary
|
|
18
|
+
|
|
19
|
+
**Needs Input Agent**:
|
|
20
|
+
An Agent Pane that explicitly needs human input, permission, or a decision before continuing. Recent activity may remain visible as context for what needs the input.
|
|
21
|
+
_Avoid_: Blocked agent, waiting agent, stuck agent
|
|
22
|
+
|
|
23
|
+
**Stalled Agent**:
|
|
24
|
+
An Agent Pane that appears to be working but has had no output, title, or Watcher Agent Event change for a configured time window.
|
|
25
|
+
_Avoid_: Stuck agent
|
|
26
|
+
|
|
27
|
+
**Agent Integration**:
|
|
28
|
+
Watcher’s knowledge of how to detect, install support for, and interpret events from a specific agent program. Capability metadata describes what Watcher currently supports, not every upstream feature the agent may theoretically expose.
|
|
29
|
+
_Avoid_: Agent provider, agent backend, agent plugin, agent abstraction
|
|
30
|
+
|
|
31
|
+
**Watcher Agent Event**:
|
|
32
|
+
A normalized event describing agent progress, such as a user message, assistant message, tool activity, need for input, or completion. Agent Integrations translate agent-specific hooks and plugins into Watcher Agent Events before sending them to Watcher.
|
|
33
|
+
_Avoid_: Raw hook event, agent callback, daemon event
|
|
34
|
+
|
|
35
|
+
**Observation Capability**:
|
|
36
|
+
The currently active way Watcher can observe a specific Agent Pane, such as integration events, assistant deltas, or terminal preview. Observation Capability describes what is true for one running Agent Pane, not what an Agent Integration or Terminal Backend could theoretically support.
|
|
37
|
+
_Avoid_: Integration capability, supported feature, provider capability
|
|
38
|
+
|
|
39
|
+
**Agent Event Source**:
|
|
40
|
+
An installed hook, plugin, extension, or stream connection that emits Watcher Agent Events for an Agent Pane.
|
|
41
|
+
_Avoid_: Status hook, silent hook, auto hook
|
|
42
|
+
|
|
43
|
+
**Event Surface Identity**:
|
|
44
|
+
The backend-aware identity an Agent Event Source reports so Watcher can associate a Watcher Agent Event with the correct Agent Pane. Its canonical key combines Terminal Backend and backend-local surface id, such as `tmux:%7`.
|
|
45
|
+
_Avoid_: Pane id, hook target, routing metadata
|
|
46
|
+
|
|
47
|
+
**Repo Group**:
|
|
48
|
+
A collection of Agent Panes that belong to the same source repository. Repo Groups contain one or more Worktree Groups.
|
|
49
|
+
_Avoid_: Project group, repo bucket
|
|
50
|
+
|
|
51
|
+
**Worktree Group**:
|
|
52
|
+
A collection of Agent Panes that share the same Git worktree path within a Repo Group. The branch is displayed as context, but the worktree path is the identity.
|
|
53
|
+
_Avoid_: Branch group, session group
|
|
54
|
+
|
|
55
|
+
**Path Fallback Group**:
|
|
56
|
+
A collection of Agent Panes grouped by path when Watcher cannot determine repository and worktree identity.
|
|
57
|
+
_Avoid_: Unknown repo, ungrouped panes
|
|
58
|
+
|
|
59
|
+
**Terminal Backend**:
|
|
60
|
+
A local terminal environment that can discover, identify, and activate Agent Panes. tmux is Watcher's current Terminal Backend.
|
|
61
|
+
_Avoid_: Provider, terminal type, transport
|
|
62
|
+
|
|
63
|
+
**Running Agent Pane**:
|
|
64
|
+
An Agent Pane whose Terminal Backend surface still exists and is running a known agent process. Agent Event Sources provide Agent Status and activity for Running Agent Panes, but stale daemon history alone is not liveness: if tmux is available and the known agent process is gone, the Agent Pane is no longer running even if its shell pane remains. Running Agent Panes are shown regardless of Agent Status, including `idle`.
|
|
65
|
+
_Avoid_: Non-terminated agent pane, active-only session, actionable-only pane
|
|
66
|
+
|
|
67
|
+
**Agent Pane Activation**:
|
|
68
|
+
The user action of jumping from Watcher to the selected Agent Pane.
|
|
69
|
+
_Avoid_: Switch, attach, focus
|
|
70
|
+
|
|
71
|
+
**Watcher Daemon**:
|
|
72
|
+
The background process that receives Watcher Agent Events and serves the current Agent Pane snapshot to the TUI.
|
|
73
|
+
_Avoid_: Server, backend, monitor
|
|
74
|
+
|
|
75
|
+
**Agent Switcher**:
|
|
76
|
+
The terminal interface that lists running Agent Panes and activates the selected pane.
|
|
77
|
+
_Avoid_: Dashboard, monitor, agent list
|
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Watcher
|
|
2
|
+
|
|
3
|
+
A tmux-native switcher for local coding agents.
|
|
4
|
+
|
|
5
|
+
Built because I live in the terminal and already use tmux. Watcher is deliberately not trying to reinvent the wheel: tmux already owns windows, panes, sessions, keybindings, and muscle memory. Watcher just shows which agent panes need attention, previews enough context, then jumps straight back into tmux.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
watcher
|
|
11
|
+
watcher integrations install pi
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Tmux binding example:
|
|
15
|
+
|
|
16
|
+
```tmux
|
|
17
|
+
bind -n M-s new-window -n watcher "watcher"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This keeps Watcher one keystroke away: `Alt-s` opens the switcher in a tmux window, `j/k` picks an agent, and `Enter` jumps to it.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Agent Switcher architecture
|
|
2
|
+
|
|
3
|
+
Watcher uses explicit Agent Integrations as the primary source for Agent Status and activity, with terminal-backend process/title/output heuristics as fallback while the Agent Switcher is open. Integrations call a small `watcher event <agent> <event>` CLI shim, which reads backend identity and forwards normalized Watcher Agent Events to the Watcher Daemon over a local Unix socket. Agent Event Sources are installed with `watcher integrations install`. The daemon stores snapshots keyed by backend-local surface id; the Agent Switcher discovers current panes across supported local Terminal Backends, shows all running agent panes including idle panes, uses Agent Status for priority/context rather than visibility, and activates the selected pane through backend-specific commands.
|
|
4
|
+
|
|
5
|
+
## Considered Options
|
|
6
|
+
|
|
7
|
+
- Scrape terminal output only: rejected because task/status inference from arbitrary TUI output is noisy and agent-specific.
|
|
8
|
+
- Require hooks only: rejected because unsupported or not-yet-restarted agents should still be discoverable best-effort.
|
|
9
|
+
- Poll terminal backends from the daemon continuously: rejected because capture polling is only needed while the switcher UI is open.
|
|
10
|
+
- Use Ink: rejected by product preference.
|
|
11
|
+
- Use `@opentui/core`: rejected after Bun/runtime compatibility issues and prototype mismatch; render the accepted prototype-style ANSI terminal frame directly for MVP.
|
|
12
|
+
|
|
13
|
+
## Consequences
|
|
14
|
+
|
|
15
|
+
Supported agents need explicit `watcher integrations install` before rich status works. Existing running agents may need restart/reload before integrations emit events. Integrations stay small because they shell out to the Watcher CLI instead of embedding socket protocol code in every agent hook/plugin. Remote terminal backends are out of scope for MVP; run Watcher on the remote host separately.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Agent Switcher focus layout
|
|
2
|
+
|
|
3
|
+
Watcher organizes the Agent Switcher as `repo > worktree/branch > sessions`, with worktrees from the same repository grouped together and all running Agent Panes shown under each worktree, including idle panes. The worktree path is the grouping identity because branch names alone are ambiguous; branch names are displayed as context, and panes without Git metadata fall back to path grouping.
|
|
4
|
+
|
|
5
|
+
## Considered Options
|
|
6
|
+
|
|
7
|
+
- Flat status-first list: rejected because it scatters related work across the switcher and makes multi-worktree repositories harder to scan.
|
|
8
|
+
- Repo-only grouping: rejected because multiple worktrees in the same repository represent separate task contexts.
|
|
9
|
+
- Showing only one pane per worktree: rejected because users still need to choose between multiple running sessions in the same branch/worktree.
|
|
10
|
+
- Text status badges in rows: rejected because they add noise; rows use a colored status dot and the details pane carries exact status.
|
|
11
|
+
- Status-gated visibility: rejected because Watcher is a switcher, not only an alert queue. Agent Status affects sort priority and context; all running agents remain visible, including idle panes.
|
|
12
|
+
|
|
13
|
+
## Consequences
|
|
14
|
+
|
|
15
|
+
The switcher needs best-effort Git metadata for repo, branch, and worktree path, plus a path fallback when Git metadata is unavailable. It also needs Agent Status for priority sorting and selected-pane context even though status no longer gates visibility. Activation continues to exit the TUI before jumping to the selected Agent Pane, so Watcher remains a focus switcher rather than a dashboard to keep open.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Terminal Backend abstraction
|
|
2
|
+
|
|
3
|
+
Watcher keeps a Terminal Target seam so core Agent Switcher logic does not depend directly on tmux field names. tmux remains the only supported backend. Native Ghostty support was investigated and postponed because current stable Ghostty does not provide both reliable cross-platform surface identity and cross-platform activation control.
|
|
4
|
+
|
|
5
|
+
## Considered Options
|
|
6
|
+
|
|
7
|
+
- Keep tmux fields in the core Agent Pane model: rejected because it spreads tmux vocabulary through grouping, rendering, stalled detection, and activation code.
|
|
8
|
+
- Add Ghostty as a runtime backend now: rejected because support would be partial or platform-specific.
|
|
9
|
+
- Use macOS AppleScript for Ghostty activation: rejected because Watcher should support every platform Ghostty supports, and macOS-only automation would turn Terminal Backend into a platform-specific footgun.
|
|
10
|
+
|
|
11
|
+
## Consequences
|
|
12
|
+
|
|
13
|
+
Core switcher code should depend on Terminal Target helpers instead of tmux-specific fields. Runtime support remains tmux-only until another terminal backend can provide identity, discovery, stalled-signal inputs, and activation without platform-specific hacks.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Agent Integrations and normalized Watcher Agent Events
|
|
2
|
+
|
|
3
|
+
Watcher observes existing agent sessions running in terminal panes. It will model agent-specific support as Agent Integrations that emit normalized Watcher Agent Events, rather than centering the architecture on ACP or raw agent-specific hook events.
|
|
4
|
+
|
|
5
|
+
## Considered Options
|
|
6
|
+
|
|
7
|
+
- Use ACP as the primary integration path: rejected because Watcher's core workflow is observing and activating already-running terminal sessions, while ACP is best suited to sessions launched and owned by an ACP client.
|
|
8
|
+
- Let the Watcher Daemon understand raw events from each agent: rejected because Pi, Codex, OpenCode, and other agents expose different hook/plugin/event names and payloads; putting every dialect in the daemon would make it the integration junk drawer.
|
|
9
|
+
- Keep the hook model named around status: rejected because integrations now report user messages, assistant messages, tool activity, assistant deltas, input needs, and completion, not only Agent Status.
|
|
10
|
+
- Model terminal preview as an Agent Integration capability: rejected because terminal preview comes from the Terminal Backend, not from a specific agent program.
|
|
11
|
+
|
|
12
|
+
## Decision
|
|
13
|
+
|
|
14
|
+
Watcher will use **Agent Integrations** as the agent-specific abstraction. An Agent Integration owns detection, install/status behavior for its Agent Event Source, capability metadata, and translation from agent-specific hooks/plugins/events into normalized **Watcher Agent Events**.
|
|
15
|
+
|
|
16
|
+
Agent Event Sources call:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
watcher event <agent> <event>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The event name and payload at this CLI boundary are normalized Watcher Agent Events, not raw Pi/Codex/OpenCode event names. The legacy `watcher hook` command is not retained because Watcher does not yet have external users.
|
|
23
|
+
|
|
24
|
+
Watcher will use `watcher integrations install` and `watcher integrations status` as the user-facing commands for installing and inspecting Agent Event Sources.
|
|
25
|
+
|
|
26
|
+
Watcher Agent Events include semantic activity such as user messages, assistant messages, optional assistant deltas, tool activity, input needs, completion, and errors. High-frequency assistant deltas must be coalesced by the Agent Integration before shelling out to `watcher event`; integrations must not spawn one process per token.
|
|
27
|
+
|
|
28
|
+
Agent Integrations and Terminal Backends remain separate axes. Terminal Backends own discovery, activation, and terminal preview capabilities. Agent Integrations own semantic agent events and assistant delta support. Per-pane Observation Capability describes what is actually active for a specific Agent Pane.
|
|
29
|
+
|
|
30
|
+
Event routing uses backend-aware Event Surface Identity. Event payloads may provide a `surface` identity; otherwise `watcher event` may fall back to `$TMUX_PANE` as a tmux Event Surface Identity. `AgentPane.id` is an internal canonical Event Surface Identity key such as `tmux:%7`; user-facing terminal labels come from the Terminal Target.
|
|
31
|
+
|
|
32
|
+
## Consequences
|
|
33
|
+
|
|
34
|
+
The daemon can stay focused on applying normalized Watcher Agent Events to Agent Pane state instead of learning every agent's native event dialect.
|
|
35
|
+
|
|
36
|
+
Pi remains the first fully functional Agent Integration during the refactor. OpenCode, Codex, and Claude can be added to the integration registry for detection/capability metadata before their installers are implemented.
|
|
37
|
+
|
|
38
|
+
Terminal preview is intentionally left as a separate implementation slice after the Agent Integration/event refactor.
|
|
39
|
+
|
|
40
|
+
Existing generated Pi hooks and CLI command names will be broken by the refactor and must be reinstalled, which is acceptable before public adoption.
|
|
Binary file
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Watcher MVP Requirements
|
|
2
|
+
|
|
3
|
+
Watcher is a tmux-wide Agent Switcher. It lists running agent panes across all local tmux sessions and activates the selected pane.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
- Local tmux only.
|
|
8
|
+
- tmux is the supported backend for discovery, Event Surface Identity, stalled detection, and activation.
|
|
9
|
+
- Native Ghostty support is not part of the MVP.
|
|
10
|
+
- Bun/TypeScript CLI named `watcher`.
|
|
11
|
+
- Prototype-aligned terminal switcher UI rendered by the Bun CLI.
|
|
12
|
+
- In-memory Watcher Daemon.
|
|
13
|
+
- Explicit Agent Event Sources where supported; backend/process/title/output fallback while the TUI is open.
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
- `watcher`: open Agent Switcher.
|
|
18
|
+
- `watcher daemon`: run Watcher Daemon.
|
|
19
|
+
- `watcher event <agent> <event>`: CLI shim called by Agent Integrations; reads JSON from stdin and `$TMUX_PANE`.
|
|
20
|
+
- `watcher integrations install <agents...>`: install supported Agent Event Sources.
|
|
21
|
+
- `watcher integrations status`: show Agent Event Source install status.
|
|
22
|
+
|
|
23
|
+
## Statuses
|
|
24
|
+
|
|
25
|
+
Agent Status describes priority and context for a running Agent Pane. It does not decide visibility; all running Agent Panes are shown.
|
|
26
|
+
|
|
27
|
+
- `working`: agent is handling a prompt.
|
|
28
|
+
- `needs_input`: agent needs human input, permission, or decision.
|
|
29
|
+
- `stalled`: agent appears working but has no output, title, or Watcher Agent Event change for 5 minutes.
|
|
30
|
+
- `unknown`: known agent process exists but no reliable status yet.
|
|
31
|
+
- `idle`: agent finished or is inactive; still shown while the agent pane exists so the switcher can activate it.
|
|
32
|
+
|
|
33
|
+
## Switcher Layout
|
|
34
|
+
|
|
35
|
+
Show all running Agent Panes with statuses `needs_input`, `stalled`, `working`, `unknown`, and `idle`. Hide only panes whose tmux pane no longer exists.
|
|
36
|
+
|
|
37
|
+
Use the final prototype layout: `repo > worktree/branch > sessions`.
|
|
38
|
+
|
|
39
|
+
Group the list as:
|
|
40
|
+
|
|
41
|
+
1. Repo Group
|
|
42
|
+
2. Worktree Group
|
|
43
|
+
3. Agent Pane rows
|
|
44
|
+
|
|
45
|
+
Repo and worktree rules:
|
|
46
|
+
|
|
47
|
+
- Group all worktrees from the same repository under one Repo Group.
|
|
48
|
+
- Key Worktree Groups by Git worktree path, not branch name alone.
|
|
49
|
+
- Show branch and worktree path in each Worktree Group header.
|
|
50
|
+
- If repository/worktree metadata is unavailable, use a Path Fallback Group keyed by pane path.
|
|
51
|
+
- Show all running Agent Panes within each Worktree Group, including `idle` panes.
|
|
52
|
+
|
|
53
|
+
Each session row should show only:
|
|
54
|
+
|
|
55
|
+
- colored status dot
|
|
56
|
+
- agent type
|
|
57
|
+
- task/prompt summary
|
|
58
|
+
|
|
59
|
+
Do not show status text badges, time columns, tmux target, cwd, current action, or message previews inline in rows.
|
|
60
|
+
|
|
61
|
+
Selected Agent Pane details should show:
|
|
62
|
+
|
|
63
|
+
- exact Agent Status
|
|
64
|
+
- agent type
|
|
65
|
+
- repo, branch, and worktree path, or path fallback
|
|
66
|
+
- task/prompt summary
|
|
67
|
+
- current tool/action when available
|
|
68
|
+
- last assistant message preview when available
|
|
69
|
+
- tmux session/window/pane
|
|
70
|
+
- last update age
|
|
71
|
+
|
|
72
|
+
Responsive layout:
|
|
73
|
+
|
|
74
|
+
- wide terminals: list on the left with a large details pane on the right
|
|
75
|
+
- medium/narrow terminals: list-first layout with selected summary at the bottom
|
|
76
|
+
- selected row: full-width, theme-adaptive reverse highlight; no hardcoded colors and no selection triangle
|
|
77
|
+
|
|
78
|
+
Ordering applies at Repo Group, Worktree Group, and Agent Pane row levels:
|
|
79
|
+
|
|
80
|
+
1. Repo Groups by highest-priority running Agent Pane, then newest update.
|
|
81
|
+
2. Worktree Groups by highest-priority running Agent Pane, then newest update.
|
|
82
|
+
3. Agent Pane rows within a Worktree Group by status priority, then newest update.
|
|
83
|
+
|
|
84
|
+
Status priority:
|
|
85
|
+
|
|
86
|
+
1. `needs_input`
|
|
87
|
+
2. `stalled`
|
|
88
|
+
3. `working`
|
|
89
|
+
4. `unknown`
|
|
90
|
+
5. `idle`
|
|
91
|
+
|
|
92
|
+
Within the same highest-priority status, newest update first.
|
|
93
|
+
|
|
94
|
+
## Backend Discovery
|
|
95
|
+
|
|
96
|
+
While the switcher is open, tmux discovery should:
|
|
97
|
+
|
|
98
|
+
- poll `tmux list-panes -a` every 2 seconds
|
|
99
|
+
- detect known agent processes by pane command and one-level process tree scan from `pane_pid`
|
|
100
|
+
- capture pane tail/hash for candidate agent panes only
|
|
101
|
+
- derive `stalled` when status is `working` and no Watcher Agent Event, title, or output change occurs for 5 minutes
|
|
102
|
+
|
|
103
|
+
Native Ghostty discovery, Event Surface Identity, and activation are out of MVP. tmux sessions running inside Ghostty remain supported through the tmux backend.
|
|
104
|
+
|
|
105
|
+
No ghost panes: if a tmux pane no longer exists, hide it.
|
|
106
|
+
|
|
107
|
+
## Activation
|
|
108
|
+
|
|
109
|
+
- tmux inside tmux: switch client to target session, select target window, select target pane.
|
|
110
|
+
- tmux outside tmux: select target window/pane first, then attach to target session.
|
|
111
|
+
- Use tmux pane ids like `%42`.
|
|
112
|
+
- Exit TUI before activation/attach/focus.
|
|
113
|
+
|
|
114
|
+
## Agent Event Sources
|
|
115
|
+
|
|
116
|
+
Agent Integrations shell out to `watcher event <agent> <event>` rather than embedding daemon protocol code.
|
|
117
|
+
|
|
118
|
+
`watcher event`:
|
|
119
|
+
|
|
120
|
+
- accepts only known Agent Integrations and normalized Watcher Agent Event names
|
|
121
|
+
- accepts `--quiet` only as `watcher event --quiet <agent> <event>` for generated Agent Event Sources
|
|
122
|
+
- reads Event Surface Identity from the event payload when present
|
|
123
|
+
- falls back to `$TMUX_PANE` as a tmux Event Surface Identity
|
|
124
|
+
- reads normalized Watcher Agent Event JSON from stdin; empty stdin is treated as `{}`
|
|
125
|
+
- validates the canonical payload schema; schema/agent/event errors are loud outside quiet mode and silent in quiet mode
|
|
126
|
+
- starts daemon if absent, retries briefly
|
|
127
|
+
- exits 0 silently if daemon unavailable
|
|
128
|
+
- never breaks the agent
|
|
129
|
+
- must not be called once per token; high-frequency streaming events should be coalesced by the Agent Integration before shelling out
|
|
130
|
+
|
|
131
|
+
Watcher Agent Event mapping:
|
|
132
|
+
|
|
133
|
+
- `session-started` -> `unknown`
|
|
134
|
+
- `user-message` / `assistant-delta` / `assistant-message` / `tool-started` / `tool-updated` / `tool-finished` -> `working`
|
|
135
|
+
- `needs-input` -> `needs_input`
|
|
136
|
+
- `agent-finished` -> `idle`
|
|
137
|
+
- `error` -> `needs_input` with reason `error`
|
|
138
|
+
|
|
139
|
+
Watcher Agent Event payload rules:
|
|
140
|
+
|
|
141
|
+
- event payload identity uses `surface`, not `target`
|
|
142
|
+
- message `text` fields must be strings
|
|
143
|
+
- `assistant-delta.text` is the current accumulated partial assistant text, not an incremental token chunk
|
|
144
|
+
- `tool-updated.text` is the current summarized tool state, not an append-only chunk
|
|
145
|
+
- tool `input` and `output` may be JSON values and are compacted centrally
|
|
146
|
+
- unknown extra fields are ignored, but required canonical fields are strict
|
|
147
|
+
- large text fields are capped centrally at ingestion; normal truncation and wrapping belong to display code
|
|
148
|
+
|
|
149
|
+
Agent Pane state rules:
|
|
150
|
+
|
|
151
|
+
- `AgentPane.id` is an internal canonical Event Surface Identity key such as `tmux:%42`; user-facing terminal labels come from the Terminal Target
|
|
152
|
+
- `user-message` sets `userMessage` and row `summary`
|
|
153
|
+
- assistant/tool events never replace `summary` once `userMessage` exists
|
|
154
|
+
- `assistant-delta` updates running activity and `currentAction = "Responding"`, but does not update `lastMessage`
|
|
155
|
+
- `assistant-message` stores a Pending Assistant Message and updates activity, but does not update `lastMessage` until completion
|
|
156
|
+
- `agent-finished` clears activity/current action and sets `lastMessage` from non-empty `finalMessage`, else Pending Assistant Message, else previous `lastMessage`
|
|
157
|
+
- activity items merge by id, are kept newest-first, and are limited to 3 while non-idle
|
|
158
|
+
- `needs-input` preserves recent activity context and may mark referenced activity as waiting
|
|
159
|
+
|
|
160
|
+
## Pi MVP Agent Event Source
|
|
161
|
+
|
|
162
|
+
Install global extension:
|
|
163
|
+
|
|
164
|
+
`~/.pi/agent/extensions/watcher-status.ts`
|
|
165
|
+
|
|
166
|
+
Behavior:
|
|
167
|
+
|
|
168
|
+
- `session_start` -> `watcher event pi session-started`
|
|
169
|
+
- `before_agent_start` -> `watcher event pi user-message` with prompt
|
|
170
|
+
- `message_end` -> `watcher event pi assistant-message` with last completed assistant message
|
|
171
|
+
- `tool_execution_start/update/end` -> `watcher event pi tool-started/tool-updated/tool-finished`
|
|
172
|
+
- `agent_end` -> `watcher event pi agent-finished` with last assistant message when available
|
|
173
|
+
|
|
174
|
+
Installer:
|
|
175
|
+
|
|
176
|
+
- overwrite only files with current or legacy Watcher markers
|
|
177
|
+
- report legacy Watcher-managed files as `outdated` until reinstalled
|
|
178
|
+
- refuse to overwrite non-Watcher file
|
|
179
|
+
- after install, tell user to `/reload` existing Pi panes or restart them
|
|
180
|
+
|
|
181
|
+
## Non-goals MVP
|
|
182
|
+
|
|
183
|
+
- remote SSH/tmux relay
|
|
184
|
+
- native Ghostty support before stable foreground PID/TTY support and cross-platform activation control
|
|
185
|
+
- session resume/restore
|
|
186
|
+
- history database
|
|
187
|
+
- unread/dismiss workflow
|
|
188
|
+
- parsing arbitrary terminal output for task text
|
|
189
|
+
- automatic silent global config mutation
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dinhtungdu/watcher",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"author": "",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/dinhtungdu/watcher.git"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"@types/bun": "^1.3.14",
|
|
11
|
+
"@types/node": "^25.9.1",
|
|
12
|
+
"typescript": "^5.9.3"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"watcher": "src/cli.ts"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/dinhtungdu/watcher/issues"
|
|
19
|
+
},
|
|
20
|
+
"description": "tmux-wide Agent Switcher for agent panes",
|
|
21
|
+
"directories": {
|
|
22
|
+
"doc": "docs"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"src",
|
|
26
|
+
"docs",
|
|
27
|
+
"CONTEXT.md"
|
|
28
|
+
],
|
|
29
|
+
"homepage": "https://github.com/dinhtungdu/watcher#readme",
|
|
30
|
+
"keywords": [],
|
|
31
|
+
"license": "ISC",
|
|
32
|
+
"packageManager": "bun@1.3.13",
|
|
33
|
+
"scripts": {
|
|
34
|
+
"start": "bun src/cli.ts",
|
|
35
|
+
"build": "bunx tsc -p tsconfig.json --noEmit",
|
|
36
|
+
"lint": "bunx tsc -p tsconfig.json --noEmit",
|
|
37
|
+
"test": "bun test test/*.test.ts"
|
|
38
|
+
},
|
|
39
|
+
"type": "module"
|
|
40
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { AgentPane, TmuxTarget } from './model.js';
|
|
2
|
+
import { paneTargetLabel } from './terminalTarget.js';
|
|
3
|
+
import { CommandRunner, nodeCommandRunner } from './tmux.js';
|
|
4
|
+
|
|
5
|
+
export interface TerminalCommand {
|
|
6
|
+
file: string;
|
|
7
|
+
args: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ActivationOptions {
|
|
11
|
+
insideTmux?: boolean;
|
|
12
|
+
runner?: CommandRunner;
|
|
13
|
+
env?: NodeJS.ProcessEnv;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function requireTmuxTarget(target: TmuxTarget): { paneId: string; session: string; window: string } {
|
|
17
|
+
const paneId = target.paneId;
|
|
18
|
+
const session = target.sessionName;
|
|
19
|
+
const window = target.windowIndex;
|
|
20
|
+
if (!paneId || !session || window === undefined) {
|
|
21
|
+
throw new Error(`Cannot activate ${target.id}: missing tmux session/window/pane target`);
|
|
22
|
+
}
|
|
23
|
+
return { paneId, session, window };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function activationTargetLabel(pane: AgentPane): string {
|
|
27
|
+
return paneTargetLabel(pane);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildTmuxActivationCommands(target: TmuxTarget, insideTmux: boolean): TerminalCommand[] {
|
|
31
|
+
const required = requireTmuxTarget(target);
|
|
32
|
+
const windowTarget = `${required.session}:${required.window}`;
|
|
33
|
+
if (insideTmux) {
|
|
34
|
+
return [
|
|
35
|
+
{ file: 'tmux', args: ['switch-client', '-t', required.session] },
|
|
36
|
+
{ file: 'tmux', args: ['select-window', '-t', windowTarget] },
|
|
37
|
+
{ file: 'tmux', args: ['select-pane', '-t', required.paneId] },
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
return [
|
|
41
|
+
{ file: 'tmux', args: ['select-window', '-t', windowTarget] },
|
|
42
|
+
{ file: 'tmux', args: ['select-pane', '-t', required.paneId] },
|
|
43
|
+
{ file: 'tmux', args: ['attach-session', '-t', required.session] },
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function buildActivationCommands(pane: AgentPane, insideTmux: boolean): TerminalCommand[] {
|
|
48
|
+
return buildTmuxActivationCommands(pane.target, insideTmux);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function activateAgentPane(pane: AgentPane, options: ActivationOptions = {}): Promise<void> {
|
|
52
|
+
const runner = options.runner ?? nodeCommandRunner;
|
|
53
|
+
const env = options.env ?? process.env;
|
|
54
|
+
const insideTmux = options.insideTmux ?? Boolean(env.TMUX);
|
|
55
|
+
for (const command of buildActivationCommands(pane, insideTmux)) {
|
|
56
|
+
await runner.execFile(command.file, command.args, { timeout: 5000, env });
|
|
57
|
+
}
|
|
58
|
+
}
|