/agent-activity
Streams what the agent is doing into the room, as rows the desktop client renders in an **events drawer** above the composer (collapsed: avatar, pulsing dots, the newest line rolling in; expanded: the live rows of the task in progress) and in the console dock's **Events** panel
Claude Code
$ npx -y skills add sonichi/sutando --skill agent-activity --agent claude-code
Codex
$ npx skills add sonichi/sutando --skill agent-activity -a codex -y
Cursor
$ npx skills add sonichi/sutando --skill agent-activity -a cursor -y
Gemini CLI
$ npx skills add sonichi/sutando --skill agent-activity -a gemini-cli -y
GitHub Copilot
$ npx skills add sonichi/sutando --skill agent-activity -a github-copilot -y
OpenCode
$ npx skills add sonichi/sutando --skill agent-activity -a opencode -y
Windsurf
$ npx skills add sonichi/sutando --skill agent-activity -a windsurf -y
SKILL.md
agent-activity.SKILL.mdagent-activity
Streams what the agent is doing into the room, as rows the desktop client renders in an **events drawer** above the composer (collapsed: avatar, pulsing dots, the newest line rolling in; expanded: the live rows of the task in progress) and in the console dock's **Events** panel (the full history). Client side: ag2-space/cinny-webclient #888.
Rows
One JSON object per line at `<workspace>/state/agent-activity.jsonl`, served to the client by the loopback media route as `/media/state/agent-activity.jsonl`:
| field | meaning | |--------|---------| | `ts` | epoch seconds | | `room` | room id the row belongs to; the drawer in that room shows it. Absent → dock only for ordinary activity; only a roomless `wedge` is global | | `kind` | `processing` (picked the message up) · `thinking` (narration while deciding) · `working` (the tool in use, one line) · `notice` (PR heartbeats and the like) · `wedge` (**stopped: the owner has to act** — always written roomless, and it shows through a drawer the owner dismissed, because a blocker is not activity, it is why activity stopped) · `done` | | `line` | the text, plain | | `task` | `{id, from, text}` — the user message this row belongs to (`from` an mxid, `text` ≤160 chars) | | `done` | `true` closes the task: all of its rows leave the drawer (the dock keeps them) | | `task.event` | event id of the user message (`source_message_id`); the client mounts the per-message card under it | | `task.into` | on a consolidated `done`: event id of the message whose reply answered this one too | | `agent` | mxid of the agent the task is answered as, on a task a named lane queued (a pool worker's own agent, a secondary homeserver's); absent on every other row, which is the engine's own agent's | | `queue` | on the `queued` row only: `{depth, position}` — how many tasks are pending and this one's 1-based place in the order the core takes them (`src/task_queue.py`) |
A `notice` row reading `queued` is written by the task watcher when the file lands, before any turn has it (the activity bus's QUEUED transition; `activity.py queued --task-file …` is the same row by hand, only for files that name a room and a message). With at least one task ahead the line reads `queued · N ahead` and carries `queue`.
`python3 $S/activity.py queue --task-file <workspace>/tasks/task-….txt` prints that task's `{"depth": N, "position": K}` now. When more than one task is pending, the first line to the task's own conversation names the position ("Got it, right after the one I'm on." for one ahead, "Got it, N in line before this one." for more); nothing else narrates the queue. When `tasks/` cannot be read it prints nothing, says why on stderr and exits 1: the position is unknown then, not zero.
A task's rows are **live** until its `done` row; a task-less row is live 15 minutes. The drawer shows only live rows and hides itself when none is live. Rows are flat: the client marks the first row of each task with a square dot, never folds.
Writing rows
S=skills/agent-activity/scripts
python3 $S/activity.py append "picked up your message" --kind processing \
--task-id task-… --from '@owner:server' --text "<their message>" [--room '!room:server']
python3 $S/activity.py append "PR #123 CI green" --kind notice
python3 $S/activity.py done "the drawer is one rolling line" --task-id task-…Prefer `--task-file <workspace>/tasks/task-….txt`: it fills the task id, `from`, `text` and the room from the task's own headers, so a row is always tagged with the room the message came from. Without it, `--room` defaults to the room of the owner's latest s.space message (`state/last-owner-activity.json`). With the hook installed, `processing` and `done` are written for you (see below); hand-written rows are for anything extra the owner asks to see.
Working and Thinking rows, automatically (hooks)
The skill declares Claude Code hooks in `manifest.json` (`./hooks/activity-hook.py` on `PreToolUse`, `PostToolUse`, `Stop`, `UserPromptSubmit` and `PermissionRequest`); `bash src/install-claude-hooks.sh` registers them like every other skill hook. The hook never depends on the agent remembering anything:
- **Processing, automatically.** The first `PreToolUse` in a session whose input names a task file
(`tasks/task-….txt`, a Read or a shell command) binds that task to the hook's own `session_id` in `state/agent-activity.sessions.json` and writes its `processing` row from the task's headers (room, sender, text). The agent's own `activity.py append --kind processing` still binds, for hand-written rows.
- **Done, automatically.** A `PostToolUse` whose input wrote `results/task-….txt` (Write, or a shell
redirect) writes the task's `done` row, only from the session the task is bound to and only if the result file exists once the tool has run: a denied or failed write closes nothing.
- **Working.** Every later `PreToolUse` in that session writes at most one `working` row for the open
task bound to it, in the task's room (the client shows a room's newest working row):
- a call with a `description` writes its first sentence (100 characters), and never a phase; the step
line wins;
- a call without one writes its rules phase word (`scripts/phase_line.py`, e.g. "Running tests"), when
the room does not show that phase already.
The phase model's refinement feeds the published task status only, never a room row.
- **Thinking.** `Stop` reads the last assistant text of *this session's* `transcript_path`
(complete lines only, so a row mid-write is never split) and writes it as `thinking`.
- **Turns.** Every event also folds into `state/agent-activity.turns.json`, keyed by the turn's
`prompt_id`: the tasks its prompt named (the watcher's `TASK_FILE:` lines), the dialog it is parked on (`PermissionRequest`), and its end (`Stop`). The hook stamps its tmux pane with the open turn (`@sutando_turn`), cleared by that turn's own `Stop`.
- **Stop interrupt** (`SUTA
Read more
agent-activity
Streams what the agent is doing into the room, as rows the desktop client renders in an **events drawer** above the composer (collapsed: avatar, pulsing dots, the newest line rolling in; expanded: the live rows of the task in progress) and in the console dock's **Events** panel (the full history). Client side: ag2-space/cinny-webclient #888.
Rows
One JSON object per line at `<workspace>/state/agent-activity.jsonl`, served to the client by the loopback media route as `/media/state/agent-activity.jsonl`:
| field | meaning | |--------|---------| | `ts` | epoch seconds | | `room` | room id the row belongs to; the drawer in that room shows it. Absent → dock only for ordinary activity; only a roomless `wedge` is global | | `kind` | `processing` (picked the message up) · `thinking` (narration while deciding) · `working` (the tool in use, one line) · `notice` (PR heartbeats and the like) · `wedge` (**stopped: the owner has to act** — always written roomless, and it shows through a drawer the owner dismissed, because a blocker is not activity, it is why activity stopped) · `done` | | `line` | the text, plain | | `task` | `{id, from, text}` — the user message this row belongs to (`from` an mxid, `text` ≤160 chars) | | `done` | `true` closes the task: all of its rows leave the drawer (the dock keeps them) | | `task.event` | event id of the user message (`source_message_id`); the client mounts the per-message card under it | | `task.into` | on a consolidated `done`: event id of the message whose reply answered this one too | | `agent` | mxid of the agent the task is answered as, on a task a named lane queued (a pool worker's own agent, a secondary homeserver's); absent on every other row, which is the engine's own agent's | | `queue` | on the `queued` row only: `{depth, position}` — how many tasks are pending and this one's 1-based place in the order the core takes them (`src/task_queue.py`) |
A `notice` row reading `queued` is written by the task watcher when the file lands, before any turn has it (the activity bus's QUEUED transition; `activity.py queued --task-file …` is the same row by hand, only for files that name a room and a message). With at least one task ahead the line reads `queued · N ahead` and carries `queue`.
`python3 $S/activity.py queue --task-file <workspace>/tasks/task-….txt` prints that task's `{"depth": N, "position": K}` now. When more than one task is pending, the first line to the task's own conversation names the position ("Got it, right after the one I'm on." for one ahead, "Got it, N in line before this one." for more); nothing else narrates the queue. When `tasks/` cannot be read it prints nothing, says why on stderr and exits 1: the position is unknown then, not zero.
A task's rows are **live** until its `done` row; a task-less row is live 15 minutes. The drawer shows only live rows and hides itself when none is live. Rows are flat: the client marks the first row of each task with a square dot, never folds.
Writing rows
S=skills/agent-activity/scripts
python3 $S/activity.py append "picked up your message" --kind processing \
--task-id task-… --from '@owner:server' --text "<their message>" [--room '!room:server']
python3 $S/activity.py append "PR #123 CI green" --kind notice
python3 $S/activity.py done "the drawer is one rolling line" --task-id task-…Prefer `--task-file <workspace>/tasks/task-….txt`: it fills the task id, `from`, `text` and the room from the task's own headers, so a row is always tagged with the room the message came from. Without it, `--room` defaults to the room of the owner's latest s.space message (`state/last-owner-activity.json`). With the hook installed, `processing` and `done` are written for you (see below); hand-written rows are for anything extra the owner asks to see.
Working and Thinking rows, automatically (hooks)
The skill declares Claude Code hooks in `manifest.json` (`./hooks/activity-hook.py` on `PreToolUse`, `PostToolUse`, `Stop`, `UserPromptSubmit` and `PermissionRequest`); `bash src/install-claude-hooks.sh` registers them like every other skill hook. The hook never depends on the agent remembering anything:
- **Processing, automatically.** The first `PreToolUse` in a session whose input names a task file
(`tasks/task-….txt`, a Read or a shell command) binds that task to the hook's own `session_id` in `state/agent-activity.sessions.json` and writes its `processing` row from the task's headers (room, sender, text). The agent's own `activity.py append --kind processing` still binds, for hand-written rows.
- **Done, automatically.** A `PostToolUse` whose input wrote `results/task-….txt` (Write, or a shell
redirect) writes the task's `done` row, only from the session the task is bound to and only if the result file exists once the tool has run: a denied or failed write closes nothing.
- **Working.** Every later `PreToolUse` in that session writes at most one `working` row for the open
task bound to it, in the task's room (the client shows a room's newest working row):
- a call with a `description` writes its first sentence (100 characters), and never a phase; the step
line wins;
- a call without one writes its rules phase word (`scripts/phase_line.py`, e.g. "Running tests"), when
the room does not show that phase already.
The phase model's refinement feeds the published task status only, never a room row.
- **Thinking.** `Stop` reads the last assistant text of *this session's* `transcript_path`
(complete lines only, so a row mid-write is never split) and writes it as `thinking`.
- **Turns.** Every event also folds into `state/agent-activity.turns.json`, keyed by the turn's
`prompt_id`: the tasks its prompt named (the watcher's `TASK_FILE:` lines), the dialog it is parked on (`PermissionRequest`), and its end (`Stop`). The hook stamps its tmux pane with the open turn (`@sutando_turn`), cleared by that turn's own `Stop`.
- **Stop interrupt** (`SUTA
My AI Stand — Realtime by Day, Rewriting Itself by Night. Summon my AI superpower. Voice, vision, screen, meetings, calls when I'm engaged. Learns my patterns, ships its own code when I'm not. Runs across my Macs, interacts with people & their Stands.
Other skills on sutando.
More skills from other plugins.
browser-use
Direct browser control via CDP for web interaction: automation, scraping, testing, screenshots, and site/app work. Also covers…
agent-browser
Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling…
Questions about agent-activity.
- Which coding agents can use agent-activity?
- agent-activity installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.

